1 /*
2 // Copyright (c) 2018 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 */
16 /// \file entity_manager.cpp
17 
18 #include "entity_manager.hpp"
19 
20 #include "overlay.hpp"
21 #include "topology.hpp"
22 #include "utils.hpp"
23 #include "variant_visitors.hpp"
24 
25 #include <boost/algorithm/string/case_conv.hpp>
26 #include <boost/algorithm/string/classification.hpp>
27 #include <boost/algorithm/string/predicate.hpp>
28 #include <boost/algorithm/string/replace.hpp>
29 #include <boost/algorithm/string/split.hpp>
30 #include <boost/asio/io_context.hpp>
31 #include <boost/asio/post.hpp>
32 #include <boost/asio/steady_timer.hpp>
33 #include <boost/container/flat_map.hpp>
34 #include <boost/container/flat_set.hpp>
35 #include <boost/range/iterator_range.hpp>
36 #include <nlohmann/json.hpp>
37 #include <sdbusplus/asio/connection.hpp>
38 #include <sdbusplus/asio/object_server.hpp>
39 
40 #include <charconv>
41 #include <filesystem>
42 #include <fstream>
43 #include <functional>
44 #include <iostream>
45 #include <map>
46 #include <regex>
47 #include <variant>
48 constexpr const char* hostConfigurationDirectory = SYSCONF_DIR "configurations";
49 constexpr const char* configurationDirectory = PACKAGE_DIR "configurations";
50 constexpr const char* schemaDirectory = PACKAGE_DIR "configurations/schemas";
51 constexpr const char* tempConfigDir = "/tmp/configuration/";
52 constexpr const char* lastConfiguration = "/tmp/configuration/last.json";
53 constexpr const char* currentConfiguration = "/var/configuration/system.json";
54 constexpr const char* globalSchema = "global.json";
55 
56 const boost::container::flat_map<const char*, probe_type_codes, CmpStr>
57     probeTypes{{{"FALSE", probe_type_codes::FALSE_T},
58                 {"TRUE", probe_type_codes::TRUE_T},
59                 {"AND", probe_type_codes::AND},
60                 {"OR", probe_type_codes::OR},
61                 {"FOUND", probe_type_codes::FOUND},
62                 {"MATCH_ONE", probe_type_codes::MATCH_ONE}}};
63 
64 static constexpr std::array<const char*, 6> settableInterfaces = {
65     "FanProfile", "Pid", "Pid.Zone", "Stepwise", "Thresholds", "Polling"};
66 using JsonVariantType =
67     std::variant<std::vector<std::string>, std::vector<double>, std::string,
68                  int64_t, uint64_t, double, int32_t, uint32_t, int16_t,
69                  uint16_t, uint8_t, bool>;
70 
71 // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
72 // store reference to all interfaces so we can destroy them later
73 boost::container::flat_map<
74     std::string, std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>>
75     inventory;
76 
77 // todo: pass this through nicer
78 std::shared_ptr<sdbusplus::asio::connection> systemBus;
79 nlohmann::json lastJson;
80 Topology topology;
81 
82 boost::asio::io_context io;
83 // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
84 
85 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
86 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
87 
tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface> & iface)88 void tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface>& iface)
89 {
90     try
91     {
92         iface->initialize();
93     }
94     catch (std::exception& e)
95     {
96         std::cerr << "Unable to initialize dbus interface : " << e.what()
97                   << "\n"
98                   << "object Path : " << iface->get_object_path() << "\n"
99                   << "interface name : " << iface->get_interface_name() << "\n";
100     }
101 }
102 
findProbeType(const std::string & probe)103 FoundProbeTypeT findProbeType(const std::string& probe)
104 {
105     boost::container::flat_map<const char*, probe_type_codes,
106                                CmpStr>::const_iterator probeType;
107     for (probeType = probeTypes.begin(); probeType != probeTypes.end();
108          ++probeType)
109     {
110         if (probe.find(probeType->first) != std::string::npos)
111         {
112             return probeType;
113         }
114     }
115 
116     return std::nullopt;
117 }
118 
119 static std::shared_ptr<sdbusplus::asio::dbus_interface>
createInterface(sdbusplus::asio::object_server & objServer,const std::string & path,const std::string & interface,const std::string & parent,bool checkNull=false)120     createInterface(sdbusplus::asio::object_server& objServer,
121                     const std::string& path, const std::string& interface,
122                     const std::string& parent, bool checkNull = false)
123 {
124     // on first add we have no reason to check for null before add, as there
125     // won't be any. For dynamically added interfaces, we check for null so that
126     // a constant delete/add will not create a memory leak
127 
128     auto ptr = objServer.add_interface(path, interface);
129     auto& dataVector = inventory[parent];
130     if (checkNull)
131     {
132         auto it = std::find_if(dataVector.begin(), dataVector.end(),
133                                [](const auto& p) { return p.expired(); });
134         if (it != dataVector.end())
135         {
136             *it = ptr;
137             return ptr;
138         }
139     }
140     dataVector.emplace_back(ptr);
141     return ptr;
142 }
143 
144 // writes output files to persist data
writeJsonFiles(const nlohmann::json & systemConfiguration)145 bool writeJsonFiles(const nlohmann::json& systemConfiguration)
146 {
147     std::filesystem::create_directory(configurationOutDir);
148     std::ofstream output(currentConfiguration);
149     if (!output.good())
150     {
151         return false;
152     }
153     output << systemConfiguration.dump(4);
154     output.close();
155     return true;
156 }
157 
158 template <typename JsonType>
setJsonFromPointer(const std::string & ptrStr,const JsonType & value,nlohmann::json & systemConfiguration)159 bool setJsonFromPointer(const std::string& ptrStr, const JsonType& value,
160                         nlohmann::json& systemConfiguration)
161 {
162     try
163     {
164         nlohmann::json::json_pointer ptr(ptrStr);
165         nlohmann::json& ref = systemConfiguration[ptr];
166         ref = value;
167         return true;
168     }
169     catch (const std::out_of_range&)
170     {
171         return false;
172     }
173 }
174 
175 // template function to add array as dbus property
176 template <typename PropertyType>
addArrayToDbus(const std::string & name,const nlohmann::json & array,sdbusplus::asio::dbus_interface * iface,sdbusplus::asio::PropertyPermission permission,nlohmann::json & systemConfiguration,const std::string & jsonPointerString)177 void addArrayToDbus(const std::string& name, const nlohmann::json& array,
178                     sdbusplus::asio::dbus_interface* iface,
179                     sdbusplus::asio::PropertyPermission permission,
180                     nlohmann::json& systemConfiguration,
181                     const std::string& jsonPointerString)
182 {
183     std::vector<PropertyType> values;
184     for (const auto& property : array)
185     {
186         auto ptr = property.get_ptr<const PropertyType*>();
187         if (ptr != nullptr)
188         {
189             values.emplace_back(*ptr);
190         }
191     }
192 
193     if (permission == sdbusplus::asio::PropertyPermission::readOnly)
194     {
195         iface->register_property(name, values);
196     }
197     else
198     {
199         iface->register_property(
200             name, values,
201             [&systemConfiguration,
202              jsonPointerString{std::string(jsonPointerString)}](
203                 const std::vector<PropertyType>& newVal,
204                 std::vector<PropertyType>& val) {
205                 val = newVal;
206                 if (!setJsonFromPointer(jsonPointerString, val,
207                                         systemConfiguration))
208                 {
209                     std::cerr << "error setting json field\n";
210                     return -1;
211                 }
212                 if (!writeJsonFiles(systemConfiguration))
213                 {
214                     std::cerr << "error setting json file\n";
215                     return -1;
216                 }
217                 return 1;
218             });
219     }
220 }
221 
222 template <typename PropertyType>
addProperty(const std::string & name,const PropertyType & value,sdbusplus::asio::dbus_interface * iface,nlohmann::json & systemConfiguration,const std::string & jsonPointerString,sdbusplus::asio::PropertyPermission permission)223 void addProperty(const std::string& name, const PropertyType& value,
224                  sdbusplus::asio::dbus_interface* iface,
225                  nlohmann::json& systemConfiguration,
226                  const std::string& jsonPointerString,
227                  sdbusplus::asio::PropertyPermission permission)
228 {
229     if (permission == sdbusplus::asio::PropertyPermission::readOnly)
230     {
231         iface->register_property(name, value);
232         return;
233     }
234     iface->register_property(
235         name, value,
236         [&systemConfiguration,
237          jsonPointerString{std::string(jsonPointerString)}](
238             const PropertyType& newVal, PropertyType& val) {
239             val = newVal;
240             if (!setJsonFromPointer(jsonPointerString, val,
241                                     systemConfiguration))
242             {
243                 std::cerr << "error setting json field\n";
244                 return -1;
245             }
246             if (!writeJsonFiles(systemConfiguration))
247             {
248                 std::cerr << "error setting json file\n";
249                 return -1;
250             }
251             return 1;
252         });
253 }
254 
createDeleteObjectMethod(const std::string & jsonPointerPath,const std::shared_ptr<sdbusplus::asio::dbus_interface> & iface,sdbusplus::asio::object_server & objServer,nlohmann::json & systemConfiguration)255 void createDeleteObjectMethod(
256     const std::string& jsonPointerPath,
257     const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
258     sdbusplus::asio::object_server& objServer,
259     nlohmann::json& systemConfiguration)
260 {
261     std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface;
262     iface->register_method(
263         "Delete", [&objServer, &systemConfiguration, interface,
264                    jsonPointerPath{std::string(jsonPointerPath)}]() {
265             std::shared_ptr<sdbusplus::asio::dbus_interface> dbusInterface =
266                 interface.lock();
267             if (!dbusInterface)
268             {
269                 // this technically can't happen as the pointer is pointing to
270                 // us
271                 throw DBusInternalError();
272             }
273             nlohmann::json::json_pointer ptr(jsonPointerPath);
274             systemConfiguration[ptr] = nullptr;
275 
276             // todo(james): dig through sdbusplus to find out why we can't
277             // delete it in a method call
278             boost::asio::post(io, [&objServer, dbusInterface]() mutable {
279                 objServer.remove_interface(dbusInterface);
280             });
281 
282             if (!writeJsonFiles(systemConfiguration))
283             {
284                 std::cerr << "error setting json file\n";
285                 throw DBusInternalError();
286             }
287         });
288 }
289 
290 // adds simple json types to interface's properties
populateInterfaceFromJson(nlohmann::json & systemConfiguration,const std::string & jsonPointerPath,std::shared_ptr<sdbusplus::asio::dbus_interface> & iface,nlohmann::json & dict,sdbusplus::asio::object_server & objServer,sdbusplus::asio::PropertyPermission permission=sdbusplus::asio::PropertyPermission::readOnly)291 void populateInterfaceFromJson(
292     nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
293     std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
294     nlohmann::json& dict, sdbusplus::asio::object_server& objServer,
295     sdbusplus::asio::PropertyPermission permission =
296         sdbusplus::asio::PropertyPermission::readOnly)
297 {
298     for (const auto& [key, value] : dict.items())
299     {
300         auto type = value.type();
301         bool array = false;
302         if (value.type() == nlohmann::json::value_t::array)
303         {
304             array = true;
305             if (value.empty())
306             {
307                 continue;
308             }
309             type = value[0].type();
310             bool isLegal = true;
311             for (const auto& arrayItem : value)
312             {
313                 if (arrayItem.type() != type)
314                 {
315                     isLegal = false;
316                     break;
317                 }
318             }
319             if (!isLegal)
320             {
321                 std::cerr << "dbus format error" << value << "\n";
322                 continue;
323             }
324         }
325         if (type == nlohmann::json::value_t::object)
326         {
327             continue; // handled elsewhere
328         }
329 
330         std::string path = jsonPointerPath;
331         path.append("/").append(key);
332         if (permission == sdbusplus::asio::PropertyPermission::readWrite)
333         {
334             // all setable numbers are doubles as it is difficult to always
335             // create a configuration file with all whole numbers as decimals
336             // i.e. 1.0
337             if (array)
338             {
339                 if (value[0].is_number())
340                 {
341                     type = nlohmann::json::value_t::number_float;
342                 }
343             }
344             else if (value.is_number())
345             {
346                 type = nlohmann::json::value_t::number_float;
347             }
348         }
349 
350         switch (type)
351         {
352             case (nlohmann::json::value_t::boolean):
353             {
354                 if (array)
355                 {
356                     // todo: array of bool isn't detected correctly by
357                     // sdbusplus, change it to numbers
358                     addArrayToDbus<uint64_t>(key, value, iface.get(),
359                                              permission, systemConfiguration,
360                                              path);
361                 }
362 
363                 else
364                 {
365                     addProperty(key, value.get<bool>(), iface.get(),
366                                 systemConfiguration, path, permission);
367                 }
368                 break;
369             }
370             case (nlohmann::json::value_t::number_integer):
371             {
372                 if (array)
373                 {
374                     addArrayToDbus<int64_t>(key, value, iface.get(), permission,
375                                             systemConfiguration, path);
376                 }
377                 else
378                 {
379                     addProperty(key, value.get<int64_t>(), iface.get(),
380                                 systemConfiguration, path,
381                                 sdbusplus::asio::PropertyPermission::readOnly);
382                 }
383                 break;
384             }
385             case (nlohmann::json::value_t::number_unsigned):
386             {
387                 if (array)
388                 {
389                     addArrayToDbus<uint64_t>(key, value, iface.get(),
390                                              permission, systemConfiguration,
391                                              path);
392                 }
393                 else
394                 {
395                     addProperty(key, value.get<uint64_t>(), iface.get(),
396                                 systemConfiguration, path,
397                                 sdbusplus::asio::PropertyPermission::readOnly);
398                 }
399                 break;
400             }
401             case (nlohmann::json::value_t::number_float):
402             {
403                 if (array)
404                 {
405                     addArrayToDbus<double>(key, value, iface.get(), permission,
406                                            systemConfiguration, path);
407                 }
408 
409                 else
410                 {
411                     addProperty(key, value.get<double>(), iface.get(),
412                                 systemConfiguration, path, permission);
413                 }
414                 break;
415             }
416             case (nlohmann::json::value_t::string):
417             {
418                 if (array)
419                 {
420                     addArrayToDbus<std::string>(key, value, iface.get(),
421                                                 permission, systemConfiguration,
422                                                 path);
423                 }
424                 else
425                 {
426                     addProperty(key, value.get<std::string>(), iface.get(),
427                                 systemConfiguration, path, permission);
428                 }
429                 break;
430             }
431             default:
432             {
433                 std::cerr << "Unexpected json type in system configuration "
434                           << key << ": " << value.type_name() << "\n";
435                 break;
436             }
437         }
438     }
439     if (permission == sdbusplus::asio::PropertyPermission::readWrite)
440     {
441         createDeleteObjectMethod(jsonPointerPath, iface, objServer,
442                                  systemConfiguration);
443     }
444     tryIfaceInitialize(iface);
445 }
446 
getPermission(const std::string & interface)447 sdbusplus::asio::PropertyPermission getPermission(const std::string& interface)
448 {
449     return std::find(settableInterfaces.begin(), settableInterfaces.end(),
450                      interface) != settableInterfaces.end()
451                ? sdbusplus::asio::PropertyPermission::readWrite
452                : sdbusplus::asio::PropertyPermission::readOnly;
453 }
454 
createAddObjectMethod(const std::string & jsonPointerPath,const std::string & path,nlohmann::json & systemConfiguration,sdbusplus::asio::object_server & objServer,const std::string & board)455 void createAddObjectMethod(
456     const std::string& jsonPointerPath, const std::string& path,
457     nlohmann::json& systemConfiguration,
458     sdbusplus::asio::object_server& objServer, const std::string& board)
459 {
460     std::shared_ptr<sdbusplus::asio::dbus_interface> iface = createInterface(
461         objServer, path, "xyz.openbmc_project.AddObject", board);
462 
463     iface->register_method(
464         "AddObject",
465         [&systemConfiguration, &objServer,
466          jsonPointerPath{std::string(jsonPointerPath)}, path{std::string(path)},
467          board](const boost::container::flat_map<std::string, JsonVariantType>&
468                     data) {
469             nlohmann::json::json_pointer ptr(jsonPointerPath);
470             nlohmann::json& base = systemConfiguration[ptr];
471             auto findExposes = base.find("Exposes");
472 
473             if (findExposes == base.end())
474             {
475                 throw std::invalid_argument("Entity must have children.");
476             }
477 
478             // this will throw invalid-argument to sdbusplus if invalid json
479             nlohmann::json newData{};
480             for (const auto& item : data)
481             {
482                 nlohmann::json& newJson = newData[item.first];
483                 std::visit(
484                     [&newJson](auto&& val) {
485                         newJson = std::forward<decltype(val)>(val);
486                     },
487                     item.second);
488             }
489 
490             auto findName = newData.find("Name");
491             auto findType = newData.find("Type");
492             if (findName == newData.end() || findType == newData.end())
493             {
494                 throw std::invalid_argument("AddObject missing Name or Type");
495             }
496             const std::string* type = findType->get_ptr<const std::string*>();
497             const std::string* name = findName->get_ptr<const std::string*>();
498             if (type == nullptr || name == nullptr)
499             {
500                 throw std::invalid_argument("Type and Name must be a string.");
501             }
502 
503             bool foundNull = false;
504             size_t lastIndex = 0;
505             // we add in the "exposes"
506             for (const auto& expose : *findExposes)
507             {
508                 if (expose.is_null())
509                 {
510                     foundNull = true;
511                     continue;
512                 }
513 
514                 if (expose["Name"] == *name && expose["Type"] == *type)
515                 {
516                     throw std::invalid_argument(
517                         "Field already in JSON, not adding");
518                 }
519 
520                 if (foundNull)
521                 {
522                     continue;
523                 }
524 
525                 lastIndex++;
526             }
527 
528             std::ifstream schemaFile(std::string(schemaDirectory) + "/" +
529                                      boost::to_lower_copy(*type) + ".json");
530             // todo(james) we might want to also make a list of 'can add'
531             // interfaces but for now I think the assumption if there is a
532             // schema avaliable that it is allowed to update is fine
533             if (!schemaFile.good())
534             {
535                 throw std::invalid_argument(
536                     "No schema avaliable, cannot validate.");
537             }
538             nlohmann::json schema =
539                 nlohmann::json::parse(schemaFile, nullptr, false, true);
540             if (schema.is_discarded())
541             {
542                 std::cerr << "Schema not legal" << *type << ".json\n";
543                 throw DBusInternalError();
544             }
545             if (!validateJson(schema, newData))
546             {
547                 throw std::invalid_argument("Data does not match schema");
548             }
549             if (foundNull)
550             {
551                 findExposes->at(lastIndex) = newData;
552             }
553             else
554             {
555                 findExposes->push_back(newData);
556             }
557             if (!writeJsonFiles(systemConfiguration))
558             {
559                 std::cerr << "Error writing json files\n";
560                 throw DBusInternalError();
561             }
562             std::string dbusName = *name;
563 
564             std::regex_replace(dbusName.begin(), dbusName.begin(),
565                                dbusName.end(), illegalDbusMemberRegex, "_");
566 
567             std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
568                 createInterface(objServer, path + "/" + dbusName,
569                                 "xyz.openbmc_project.Configuration." + *type,
570                                 board, true);
571             // permission is read-write, as since we just created it, must be
572             // runtime modifiable
573             populateInterfaceFromJson(
574                 systemConfiguration,
575                 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
576                 interface, newData, objServer,
577                 sdbusplus::asio::PropertyPermission::readWrite);
578         });
579     tryIfaceInitialize(iface);
580 }
581 
postToDbus(const nlohmann::json & newConfiguration,nlohmann::json & systemConfiguration,sdbusplus::asio::object_server & objServer)582 void postToDbus(const nlohmann::json& newConfiguration,
583                 nlohmann::json& systemConfiguration,
584                 sdbusplus::asio::object_server& objServer)
585 
586 {
587     std::map<std::string, std::string> newBoards; // path -> name
588 
589     // iterate through boards
590     for (const auto& [boardId, boardConfig] : newConfiguration.items())
591     {
592         std::string boardName = boardConfig["Name"];
593         std::string boardNameOrig = boardConfig["Name"];
594         std::string jsonPointerPath = "/" + boardId;
595         // loop through newConfiguration, but use values from system
596         // configuration to be able to modify via dbus later
597         auto boardValues = systemConfiguration[boardId];
598         auto findBoardType = boardValues.find("Type");
599         std::string boardType;
600         if (findBoardType != boardValues.end() &&
601             findBoardType->type() == nlohmann::json::value_t::string)
602         {
603             boardType = findBoardType->get<std::string>();
604             std::regex_replace(boardType.begin(), boardType.begin(),
605                                boardType.end(), illegalDbusMemberRegex, "_");
606         }
607         else
608         {
609             std::cerr << "Unable to find type for " << boardName
610                       << " reverting to Chassis.\n";
611             boardType = "Chassis";
612         }
613         std::string boardtypeLower = boost::algorithm::to_lower_copy(boardType);
614 
615         std::regex_replace(boardName.begin(), boardName.begin(),
616                            boardName.end(), illegalDbusMemberRegex, "_");
617         std::string boardPath = "/xyz/openbmc_project/inventory/system/";
618         boardPath += boardtypeLower;
619         boardPath += "/";
620         boardPath += boardName;
621 
622         std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface =
623             createInterface(objServer, boardPath,
624                             "xyz.openbmc_project.Inventory.Item", boardName);
625 
626         std::shared_ptr<sdbusplus::asio::dbus_interface> boardIface =
627             createInterface(objServer, boardPath,
628                             "xyz.openbmc_project.Inventory.Item." + boardType,
629                             boardNameOrig);
630 
631         createAddObjectMethod(jsonPointerPath, boardPath, systemConfiguration,
632                               objServer, boardNameOrig);
633 
634         populateInterfaceFromJson(systemConfiguration, jsonPointerPath,
635                                   boardIface, boardValues, objServer);
636         jsonPointerPath += "/";
637         // iterate through board properties
638         for (const auto& [propName, propValue] : boardValues.items())
639         {
640             if (propValue.type() == nlohmann::json::value_t::object)
641             {
642                 std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
643                     createInterface(objServer, boardPath, propName,
644                                     boardNameOrig);
645 
646                 populateInterfaceFromJson(systemConfiguration,
647                                           jsonPointerPath + propName, iface,
648                                           propValue, objServer);
649             }
650         }
651 
652         auto exposes = boardValues.find("Exposes");
653         if (exposes == boardValues.end())
654         {
655             continue;
656         }
657         // iterate through exposes
658         jsonPointerPath += "Exposes/";
659 
660         // store the board level pointer so we can modify it on the way down
661         std::string jsonPointerPathBoard = jsonPointerPath;
662         size_t exposesIndex = -1;
663         for (auto& item : *exposes)
664         {
665             exposesIndex++;
666             jsonPointerPath = jsonPointerPathBoard;
667             jsonPointerPath += std::to_string(exposesIndex);
668 
669             auto findName = item.find("Name");
670             if (findName == item.end())
671             {
672                 std::cerr << "cannot find name in field " << item << "\n";
673                 continue;
674             }
675             auto findStatus = item.find("Status");
676             // if status is not found it is assumed to be status = 'okay'
677             if (findStatus != item.end())
678             {
679                 if (*findStatus == "disabled")
680                 {
681                     continue;
682                 }
683             }
684             auto findType = item.find("Type");
685             std::string itemType;
686             if (findType != item.end())
687             {
688                 itemType = findType->get<std::string>();
689                 std::regex_replace(itemType.begin(), itemType.begin(),
690                                    itemType.end(), illegalDbusPathRegex, "_");
691             }
692             else
693             {
694                 itemType = "unknown";
695             }
696             std::string itemName = findName->get<std::string>();
697             std::regex_replace(itemName.begin(), itemName.begin(),
698                                itemName.end(), illegalDbusMemberRegex, "_");
699             std::string ifacePath = boardPath;
700             ifacePath += "/";
701             ifacePath += itemName;
702 
703             std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface =
704                 createInterface(objServer, ifacePath,
705                                 "xyz.openbmc_project.Configuration." + itemType,
706                                 boardNameOrig);
707 
708             if (itemType == "BMC")
709             {
710                 std::shared_ptr<sdbusplus::asio::dbus_interface> bmcIface =
711                     createInterface(objServer, ifacePath,
712                                     "xyz.openbmc_project.Inventory.Item.Bmc",
713                                     boardNameOrig);
714                 populateInterfaceFromJson(systemConfiguration, jsonPointerPath,
715                                           bmcIface, item, objServer,
716                                           getPermission(itemType));
717             }
718             else if (itemType == "System")
719             {
720                 std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface =
721                     createInterface(objServer, ifacePath,
722                                     "xyz.openbmc_project.Inventory.Item.System",
723                                     boardNameOrig);
724                 populateInterfaceFromJson(systemConfiguration, jsonPointerPath,
725                                           systemIface, item, objServer,
726                                           getPermission(itemType));
727             }
728 
729             populateInterfaceFromJson(systemConfiguration, jsonPointerPath,
730                                       itemIface, item, objServer,
731                                       getPermission(itemType));
732 
733             for (const auto& [name, config] : item.items())
734             {
735                 jsonPointerPath = jsonPointerPathBoard;
736                 jsonPointerPath.append(std::to_string(exposesIndex))
737                     .append("/")
738                     .append(name);
739                 if (config.type() == nlohmann::json::value_t::object)
740                 {
741                     std::string ifaceName =
742                         "xyz.openbmc_project.Configuration.";
743                     ifaceName.append(itemType).append(".").append(name);
744 
745                     std::shared_ptr<sdbusplus::asio::dbus_interface>
746                         objectIface = createInterface(objServer, ifacePath,
747                                                       ifaceName, boardNameOrig);
748 
749                     populateInterfaceFromJson(
750                         systemConfiguration, jsonPointerPath, objectIface,
751                         config, objServer, getPermission(name));
752                 }
753                 else if (config.type() == nlohmann::json::value_t::array)
754                 {
755                     size_t index = 0;
756                     if (config.empty())
757                     {
758                         continue;
759                     }
760                     bool isLegal = true;
761                     auto type = config[0].type();
762                     if (type != nlohmann::json::value_t::object)
763                     {
764                         continue;
765                     }
766 
767                     // verify legal json
768                     for (const auto& arrayItem : config)
769                     {
770                         if (arrayItem.type() != type)
771                         {
772                             isLegal = false;
773                             break;
774                         }
775                     }
776                     if (!isLegal)
777                     {
778                         std::cerr << "dbus format error" << config << "\n";
779                         break;
780                     }
781 
782                     for (auto& arrayItem : config)
783                     {
784                         std::string ifaceName =
785                             "xyz.openbmc_project.Configuration.";
786                         ifaceName.append(itemType).append(".").append(name);
787                         ifaceName.append(std::to_string(index));
788 
789                         std::shared_ptr<sdbusplus::asio::dbus_interface>
790                             objectIface = createInterface(
791                                 objServer, ifacePath, ifaceName, boardNameOrig);
792 
793                         populateInterfaceFromJson(
794                             systemConfiguration,
795                             jsonPointerPath + "/" + std::to_string(index),
796                             objectIface, arrayItem, objServer,
797                             getPermission(name));
798                         index++;
799                     }
800                 }
801             }
802 
803             topology.addBoard(boardPath, boardType, boardNameOrig, item);
804         }
805 
806         newBoards.emplace(boardPath, boardNameOrig);
807     }
808 
809     for (const auto& [assocPath, assocPropValue] :
810          topology.getAssocs(newBoards))
811     {
812         auto findBoard = newBoards.find(assocPath);
813         if (findBoard == newBoards.end())
814         {
815             continue;
816         }
817 
818         auto ifacePtr = createInterface(
819             objServer, assocPath, "xyz.openbmc_project.Association.Definitions",
820             findBoard->second);
821 
822         ifacePtr->register_property("Associations", assocPropValue);
823         tryIfaceInitialize(ifacePtr);
824     }
825 }
826 
827 // reads json files out of the filesystem
loadConfigurations(std::list<nlohmann::json> & configurations)828 bool loadConfigurations(std::list<nlohmann::json>& configurations)
829 {
830     // find configuration files
831     std::vector<std::filesystem::path> jsonPaths;
832     if (!findFiles(
833             std::vector<std::filesystem::path>{configurationDirectory,
834                                                hostConfigurationDirectory},
835             R"(.*\.json)", jsonPaths))
836     {
837         std::cerr << "Unable to find any configuration files in "
838                   << configurationDirectory << "\n";
839         return false;
840     }
841 
842     std::ifstream schemaStream(
843         std::string(schemaDirectory) + "/" + globalSchema);
844     if (!schemaStream.good())
845     {
846         std::cerr
847             << "Cannot open schema file,  cannot validate JSON, exiting\n\n";
848         std::exit(EXIT_FAILURE);
849         return false;
850     }
851     nlohmann::json schema =
852         nlohmann::json::parse(schemaStream, nullptr, false, true);
853     if (schema.is_discarded())
854     {
855         std::cerr
856             << "Illegal schema file detected, cannot validate JSON, exiting\n";
857         std::exit(EXIT_FAILURE);
858         return false;
859     }
860 
861     for (auto& jsonPath : jsonPaths)
862     {
863         std::ifstream jsonStream(jsonPath.c_str());
864         if (!jsonStream.good())
865         {
866             std::cerr << "unable to open " << jsonPath.string() << "\n";
867             continue;
868         }
869         auto data = nlohmann::json::parse(jsonStream, nullptr, false, true);
870         if (data.is_discarded())
871         {
872             std::cerr << "syntax error in " << jsonPath.string() << "\n";
873             continue;
874         }
875         /*
876          * todo(james): reenable this once less things are in flight
877          *
878         if (!validateJson(schema, data))
879         {
880             std::cerr << "Error validating " << jsonPath.string() << "\n";
881             continue;
882         }
883         */
884 
885         if (data.type() == nlohmann::json::value_t::array)
886         {
887             for (auto& d : data)
888             {
889                 configurations.emplace_back(d);
890             }
891         }
892         else
893         {
894             configurations.emplace_back(data);
895         }
896     }
897     return true;
898 }
899 
deviceRequiresPowerOn(const nlohmann::json & entity)900 static bool deviceRequiresPowerOn(const nlohmann::json& entity)
901 {
902     auto powerState = entity.find("PowerState");
903     if (powerState == entity.end())
904     {
905         return false;
906     }
907 
908     const auto* ptr = powerState->get_ptr<const std::string*>();
909     if (ptr == nullptr)
910     {
911         return false;
912     }
913 
914     return *ptr == "On" || *ptr == "BiosPost";
915 }
916 
pruneDevice(const nlohmann::json & systemConfiguration,const bool powerOff,const bool scannedPowerOff,const std::string & name,const nlohmann::json & device)917 static void pruneDevice(const nlohmann::json& systemConfiguration,
918                         const bool powerOff, const bool scannedPowerOff,
919                         const std::string& name, const nlohmann::json& device)
920 {
921     if (systemConfiguration.contains(name))
922     {
923         return;
924     }
925 
926     if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff))
927     {
928         return;
929     }
930 
931     logDeviceRemoved(device);
932 }
933 
startRemovedTimer(boost::asio::steady_timer & timer,nlohmann::json & systemConfiguration)934 void startRemovedTimer(boost::asio::steady_timer& timer,
935                        nlohmann::json& systemConfiguration)
936 {
937     static bool scannedPowerOff = false;
938     static bool scannedPowerOn = false;
939 
940     if (systemConfiguration.empty() || lastJson.empty())
941     {
942         return; // not ready yet
943     }
944     if (scannedPowerOn)
945     {
946         return;
947     }
948 
949     if (!isPowerOn() && scannedPowerOff)
950     {
951         return;
952     }
953 
954     timer.expires_after(std::chrono::seconds(10));
955     timer.async_wait(
956         [&systemConfiguration](const boost::system::error_code& ec) {
957             if (ec == boost::asio::error::operation_aborted)
958             {
959                 return;
960             }
961 
962             bool powerOff = !isPowerOn();
963             for (const auto& [name, device] : lastJson.items())
964             {
965                 pruneDevice(systemConfiguration, powerOff, scannedPowerOff,
966                             name, device);
967             }
968 
969             scannedPowerOff = true;
970             if (!powerOff)
971             {
972                 scannedPowerOn = true;
973             }
974         });
975 }
976 
977 static std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
getDeviceInterfaces(const nlohmann::json & device)978     getDeviceInterfaces(const nlohmann::json& device)
979 {
980     return inventory[device["Name"].get<std::string>()];
981 }
982 
pruneConfiguration(nlohmann::json & systemConfiguration,sdbusplus::asio::object_server & objServer,bool powerOff,const std::string & name,const nlohmann::json & device)983 static void pruneConfiguration(nlohmann::json& systemConfiguration,
984                                sdbusplus::asio::object_server& objServer,
985                                bool powerOff, const std::string& name,
986                                const nlohmann::json& device)
987 {
988     if (powerOff && deviceRequiresPowerOn(device))
989     {
990         // power not on yet, don't know if it's there or not
991         return;
992     }
993 
994     auto& ifaces = getDeviceInterfaces(device);
995     for (auto& iface : ifaces)
996     {
997         auto sharedPtr = iface.lock();
998         if (!!sharedPtr)
999         {
1000             objServer.remove_interface(sharedPtr);
1001         }
1002     }
1003 
1004     ifaces.clear();
1005     systemConfiguration.erase(name);
1006     topology.remove(device["Name"].get<std::string>());
1007     logDeviceRemoved(device);
1008 }
1009 
deriveNewConfiguration(const nlohmann::json & oldConfiguration,nlohmann::json & newConfiguration)1010 static void deriveNewConfiguration(const nlohmann::json& oldConfiguration,
1011                                    nlohmann::json& newConfiguration)
1012 {
1013     for (auto it = newConfiguration.begin(); it != newConfiguration.end();)
1014     {
1015         auto findKey = oldConfiguration.find(it.key());
1016         if (findKey != oldConfiguration.end())
1017         {
1018             it = newConfiguration.erase(it);
1019         }
1020         else
1021         {
1022             it++;
1023         }
1024     }
1025 }
1026 
publishNewConfiguration(const size_t & instance,const size_t count,boost::asio::steady_timer & timer,nlohmann::json & systemConfiguration,const nlohmann::json newConfiguration,sdbusplus::asio::object_server & objServer)1027 static void publishNewConfiguration(
1028     const size_t& instance, const size_t count,
1029     boost::asio::steady_timer& timer, nlohmann::json& systemConfiguration,
1030     // Gerrit discussion:
1031     // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6
1032     //
1033     // Discord discussion:
1034     // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854
1035     //
1036     // NOLINTNEXTLINE(performance-unnecessary-value-param)
1037     const nlohmann::json newConfiguration,
1038     sdbusplus::asio::object_server& objServer)
1039 {
1040     loadOverlays(newConfiguration);
1041 
1042     boost::asio::post(io, [systemConfiguration]() {
1043         if (!writeJsonFiles(systemConfiguration))
1044         {
1045             std::cerr << "Error writing json files\n";
1046         }
1047     });
1048 
1049     boost::asio::post(io, [&instance, count, &timer, newConfiguration,
1050                            &systemConfiguration, &objServer]() {
1051         postToDbus(newConfiguration, systemConfiguration, objServer);
1052         if (count == instance)
1053         {
1054             startRemovedTimer(timer, systemConfiguration);
1055         }
1056     });
1057 }
1058 
1059 // main properties changed entry
propertiesChangedCallback(nlohmann::json & systemConfiguration,sdbusplus::asio::object_server & objServer)1060 void propertiesChangedCallback(nlohmann::json& systemConfiguration,
1061                                sdbusplus::asio::object_server& objServer)
1062 {
1063     static bool inProgress = false;
1064     static boost::asio::steady_timer timer(io);
1065     static size_t instance = 0;
1066     instance++;
1067     size_t count = instance;
1068 
1069     timer.expires_after(std::chrono::milliseconds(500));
1070 
1071     // setup an async wait as we normally get flooded with new requests
1072     timer.async_wait([&systemConfiguration, &objServer,
1073                       count](const boost::system::error_code& ec) {
1074         if (ec == boost::asio::error::operation_aborted)
1075         {
1076             // we were cancelled
1077             return;
1078         }
1079         if (ec)
1080         {
1081             std::cerr << "async wait error " << ec << "\n";
1082             return;
1083         }
1084 
1085         if (inProgress)
1086         {
1087             propertiesChangedCallback(systemConfiguration, objServer);
1088             return;
1089         }
1090         inProgress = true;
1091 
1092         nlohmann::json oldConfiguration = systemConfiguration;
1093         auto missingConfigurations = std::make_shared<nlohmann::json>();
1094         *missingConfigurations = systemConfiguration;
1095 
1096         std::list<nlohmann::json> configurations;
1097         if (!loadConfigurations(configurations))
1098         {
1099             std::cerr << "Could not load configurations\n";
1100             inProgress = false;
1101             return;
1102         }
1103 
1104         auto perfScan = std::make_shared<PerformScan>(
1105             systemConfiguration, *missingConfigurations, configurations,
1106             objServer,
1107             [&systemConfiguration, &objServer, count, oldConfiguration,
1108              missingConfigurations]() {
1109                 // this is something that since ac has been applied to the bmc
1110                 // we saw, and we no longer see it
1111                 bool powerOff = !isPowerOn();
1112                 for (const auto& [name, device] :
1113                      missingConfigurations->items())
1114                 {
1115                     pruneConfiguration(systemConfiguration, objServer, powerOff,
1116                                        name, device);
1117                 }
1118 
1119                 nlohmann::json newConfiguration = systemConfiguration;
1120 
1121                 deriveNewConfiguration(oldConfiguration, newConfiguration);
1122 
1123                 for (const auto& [_, device] : newConfiguration.items())
1124                 {
1125                     logDeviceAdded(device);
1126                 }
1127 
1128                 inProgress = false;
1129 
1130                 boost::asio::post(
1131                     io, std::bind_front(
1132                             publishNewConfiguration, std::ref(instance), count,
1133                             std::ref(timer), std::ref(systemConfiguration),
1134                             newConfiguration, std::ref(objServer)));
1135             });
1136         perfScan->run();
1137     });
1138 }
1139 
1140 // Extract the D-Bus interfaces to probe from the JSON config files.
getProbeInterfaces()1141 static std::set<std::string> getProbeInterfaces()
1142 {
1143     std::set<std::string> interfaces;
1144     std::list<nlohmann::json> configurations;
1145     if (!loadConfigurations(configurations))
1146     {
1147         return interfaces;
1148     }
1149 
1150     for (auto it = configurations.begin(); it != configurations.end();)
1151     {
1152         auto findProbe = it->find("Probe");
1153         if (findProbe == it->end())
1154         {
1155             std::cerr << "configuration file missing probe:\n " << *it << "\n";
1156             it++;
1157             continue;
1158         }
1159 
1160         nlohmann::json probeCommand;
1161         if ((*findProbe).type() != nlohmann::json::value_t::array)
1162         {
1163             probeCommand = nlohmann::json::array();
1164             probeCommand.push_back(*findProbe);
1165         }
1166         else
1167         {
1168             probeCommand = *findProbe;
1169         }
1170 
1171         for (const nlohmann::json& probeJson : probeCommand)
1172         {
1173             const std::string* probe = probeJson.get_ptr<const std::string*>();
1174             if (probe == nullptr)
1175             {
1176                 std::cerr << "Probe statement wasn't a string, can't parse";
1177                 continue;
1178             }
1179             // Skip it if the probe cmd doesn't contain an interface.
1180             if (findProbeType(*probe))
1181             {
1182                 continue;
1183             }
1184 
1185             // syntax requires probe before first open brace
1186             auto findStart = probe->find('(');
1187             if (findStart != std::string::npos)
1188             {
1189                 std::string interface = probe->substr(0, findStart);
1190                 interfaces.emplace(interface);
1191             }
1192         }
1193         it++;
1194     }
1195 
1196     return interfaces;
1197 }
1198 
1199 // Check if InterfacesAdded payload contains an iface that needs probing.
iaContainsProbeInterface(sdbusplus::message_t & msg,const std::set<std::string> & probeInterfaces)1200 static bool iaContainsProbeInterface(
1201     sdbusplus::message_t& msg, const std::set<std::string>& probeInterfaces)
1202 {
1203     sdbusplus::message::object_path path;
1204     DBusObject interfaces;
1205     std::set<std::string> interfaceSet;
1206     std::set<std::string> intersect;
1207 
1208     msg.read(path, interfaces);
1209 
1210     std::for_each(interfaces.begin(), interfaces.end(),
1211                   [&interfaceSet](const auto& iface) {
1212                       interfaceSet.insert(iface.first);
1213                   });
1214 
1215     std::set_intersection(interfaceSet.begin(), interfaceSet.end(),
1216                           probeInterfaces.begin(), probeInterfaces.end(),
1217                           std::inserter(intersect, intersect.end()));
1218     return !intersect.empty();
1219 }
1220 
1221 // Check if InterfacesRemoved payload contains an iface that needs probing.
irContainsProbeInterface(sdbusplus::message_t & msg,const std::set<std::string> & probeInterfaces)1222 static bool irContainsProbeInterface(
1223     sdbusplus::message_t& msg, const std::set<std::string>& probeInterfaces)
1224 {
1225     sdbusplus::message::object_path path;
1226     std::set<std::string> interfaces;
1227     std::set<std::string> intersect;
1228 
1229     msg.read(path, interfaces);
1230 
1231     std::set_intersection(interfaces.begin(), interfaces.end(),
1232                           probeInterfaces.begin(), probeInterfaces.end(),
1233                           std::inserter(intersect, intersect.end()));
1234     return !intersect.empty();
1235 }
1236 
main()1237 int main()
1238 {
1239     // setup connection to dbus
1240     systemBus = std::make_shared<sdbusplus::asio::connection>(io);
1241     systemBus->request_name("xyz.openbmc_project.EntityManager");
1242 
1243     // The EntityManager object itself doesn't expose any properties.
1244     // No need to set up ObjectManager for the |EntityManager| object.
1245     sdbusplus::asio::object_server objServer(systemBus, /*skipManager=*/true);
1246 
1247     // All other objects that EntityManager currently support are under the
1248     // inventory subtree.
1249     // See the discussion at
1250     // https://discord.com/channels/775381525260664832/1018929092009144380
1251     objServer.add_manager("/xyz/openbmc_project/inventory");
1252 
1253     std::shared_ptr<sdbusplus::asio::dbus_interface> entityIface =
1254         objServer.add_interface("/xyz/openbmc_project/EntityManager",
1255                                 "xyz.openbmc_project.EntityManager");
1256 
1257     // to keep reference to the match / filter objects so they don't get
1258     // destroyed
1259 
1260     nlohmann::json systemConfiguration = nlohmann::json::object();
1261 
1262     std::set<std::string> probeInterfaces = getProbeInterfaces();
1263 
1264     // We need a poke from DBus for static providers that create all their
1265     // objects prior to claiming a well-known name, and thus don't emit any
1266     // org.freedesktop.DBus.Properties signals.  Similarly if a process exits
1267     // for any reason, expected or otherwise, we'll need a poke to remove
1268     // entities from DBus.
1269     sdbusplus::bus::match_t nameOwnerChangedMatch(
1270         static_cast<sdbusplus::bus_t&>(*systemBus),
1271         sdbusplus::bus::match::rules::nameOwnerChanged(),
1272         [&](sdbusplus::message_t& m) {
1273             auto [name, oldOwner,
1274                   newOwner] = m.unpack<std::string, std::string, std::string>();
1275 
1276             if (name.starts_with(':'))
1277             {
1278                 // We should do nothing with unique-name connections.
1279                 return;
1280             }
1281 
1282             propertiesChangedCallback(systemConfiguration, objServer);
1283         });
1284     // We also need a poke from DBus when new interfaces are created or
1285     // destroyed.
1286     sdbusplus::bus::match_t interfacesAddedMatch(
1287         static_cast<sdbusplus::bus_t&>(*systemBus),
1288         sdbusplus::bus::match::rules::interfacesAdded(),
1289         [&](sdbusplus::message_t& msg) {
1290             if (iaContainsProbeInterface(msg, probeInterfaces))
1291             {
1292                 propertiesChangedCallback(systemConfiguration, objServer);
1293             }
1294         });
1295     sdbusplus::bus::match_t interfacesRemovedMatch(
1296         static_cast<sdbusplus::bus_t&>(*systemBus),
1297         sdbusplus::bus::match::rules::interfacesRemoved(),
1298         [&](sdbusplus::message_t& msg) {
1299             if (irContainsProbeInterface(msg, probeInterfaces))
1300             {
1301                 propertiesChangedCallback(systemConfiguration, objServer);
1302             }
1303         });
1304 
1305     boost::asio::post(io, [&]() {
1306         propertiesChangedCallback(systemConfiguration, objServer);
1307     });
1308 
1309     entityIface->register_method("ReScan", [&]() {
1310         propertiesChangedCallback(systemConfiguration, objServer);
1311     });
1312     tryIfaceInitialize(entityIface);
1313 
1314     if (fwVersionIsSame())
1315     {
1316         if (std::filesystem::is_regular_file(currentConfiguration))
1317         {
1318             // this file could just be deleted, but it's nice for debug
1319             std::filesystem::create_directory(tempConfigDir);
1320             std::filesystem::remove(lastConfiguration);
1321             std::filesystem::copy(currentConfiguration, lastConfiguration);
1322             std::filesystem::remove(currentConfiguration);
1323 
1324             std::ifstream jsonStream(lastConfiguration);
1325             if (jsonStream.good())
1326             {
1327                 auto data = nlohmann::json::parse(jsonStream, nullptr, false);
1328                 if (data.is_discarded())
1329                 {
1330                     std::cerr
1331                         << "syntax error in " << lastConfiguration << "\n";
1332                 }
1333                 else
1334                 {
1335                     lastJson = std::move(data);
1336                 }
1337             }
1338             else
1339             {
1340                 std::cerr << "unable to open " << lastConfiguration << "\n";
1341             }
1342         }
1343     }
1344     else
1345     {
1346         // not an error, just logging at this level to make it in the journal
1347         std::cerr << "Clearing previous configuration\n";
1348         std::filesystem::remove(currentConfiguration);
1349     }
1350 
1351     // some boards only show up after power is on, we want to not say they are
1352     // removed until the same state happens
1353     setupPowerMatch(systemBus);
1354 
1355     io.run();
1356 
1357     return 0;
1358 }
1359