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