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, true);
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                                                   true);
851     if (schema.is_discarded())
852     {
853         std::cerr
854             << "Illegal schema file detected, cannot validate JSON, exiting\n";
855         std::exit(EXIT_FAILURE);
856         return false;
857     }
858 
859     for (auto& jsonPath : jsonPaths)
860     {
861         std::ifstream jsonStream(jsonPath.c_str());
862         if (!jsonStream.good())
863         {
864             std::cerr << "unable to open " << jsonPath.string() << "\n";
865             continue;
866         }
867         auto data = nlohmann::json::parse(jsonStream, nullptr, false, true);
868         if (data.is_discarded())
869         {
870             std::cerr << "syntax error in " << jsonPath.string() << "\n";
871             continue;
872         }
873         /*
874          * todo(james): reenable this once less things are in flight
875          *
876         if (!validateJson(schema, data))
877         {
878             std::cerr << "Error validating " << jsonPath.string() << "\n";
879             continue;
880         }
881         */
882 
883         if (data.type() == nlohmann::json::value_t::array)
884         {
885             for (auto& d : data)
886             {
887                 configurations.emplace_back(d);
888             }
889         }
890         else
891         {
892             configurations.emplace_back(data);
893         }
894     }
895     return true;
896 }
897 
898 static bool deviceRequiresPowerOn(const nlohmann::json& entity)
899 {
900     auto powerState = entity.find("PowerState");
901     if (powerState == entity.end())
902     {
903         return false;
904     }
905 
906     const auto* ptr = powerState->get_ptr<const std::string*>();
907     if (ptr == nullptr)
908     {
909         return false;
910     }
911 
912     return *ptr == "On" || *ptr == "BiosPost";
913 }
914 
915 static void pruneDevice(const nlohmann::json& systemConfiguration,
916                         const bool powerOff, const bool scannedPowerOff,
917                         const std::string& name, const nlohmann::json& device)
918 {
919     if (systemConfiguration.contains(name))
920     {
921         return;
922     }
923 
924     if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff))
925     {
926         return;
927     }
928 
929     logDeviceRemoved(device);
930 }
931 
932 void startRemovedTimer(boost::asio::steady_timer& timer,
933                        nlohmann::json& systemConfiguration)
934 {
935     static bool scannedPowerOff = false;
936     static bool scannedPowerOn = false;
937 
938     if (systemConfiguration.empty() || lastJson.empty())
939     {
940         return; // not ready yet
941     }
942     if (scannedPowerOn)
943     {
944         return;
945     }
946 
947     if (!isPowerOn() && scannedPowerOff)
948     {
949         return;
950     }
951 
952     timer.expires_after(std::chrono::seconds(10));
953     timer.async_wait(
954         [&systemConfiguration](const boost::system::error_code& ec) {
955         if (ec == boost::asio::error::operation_aborted)
956         {
957             return;
958         }
959 
960         bool powerOff = !isPowerOn();
961         for (const auto& [name, device] : lastJson.items())
962         {
963             pruneDevice(systemConfiguration, powerOff, scannedPowerOff, name,
964                         device);
965         }
966 
967         scannedPowerOff = true;
968         if (!powerOff)
969         {
970             scannedPowerOn = true;
971         }
972     });
973 }
974 
975 static std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
976     getDeviceInterfaces(const nlohmann::json& device)
977 {
978     return inventory[device["Name"].get<std::string>()];
979 }
980 
981 static void pruneConfiguration(nlohmann::json& systemConfiguration,
982                                sdbusplus::asio::object_server& objServer,
983                                bool powerOff, const std::string& name,
984                                const nlohmann::json& device)
985 {
986     if (powerOff && deviceRequiresPowerOn(device))
987     {
988         // power not on yet, don't know if it's there or not
989         return;
990     }
991 
992     auto& ifaces = getDeviceInterfaces(device);
993     for (auto& iface : ifaces)
994     {
995         auto sharedPtr = iface.lock();
996         if (!!sharedPtr)
997         {
998             objServer.remove_interface(sharedPtr);
999         }
1000     }
1001 
1002     ifaces.clear();
1003     systemConfiguration.erase(name);
1004     topology.remove(device["Name"].get<std::string>());
1005     logDeviceRemoved(device);
1006 }
1007 
1008 static void deriveNewConfiguration(const nlohmann::json& oldConfiguration,
1009                                    nlohmann::json& newConfiguration)
1010 {
1011     for (auto it = newConfiguration.begin(); it != newConfiguration.end();)
1012     {
1013         auto findKey = oldConfiguration.find(it.key());
1014         if (findKey != oldConfiguration.end())
1015         {
1016             it = newConfiguration.erase(it);
1017         }
1018         else
1019         {
1020             it++;
1021         }
1022     }
1023 }
1024 
1025 static void publishNewConfiguration(
1026     const size_t& instance, const size_t count,
1027     boost::asio::steady_timer& timer, nlohmann::json& systemConfiguration,
1028     // Gerrit discussion:
1029     // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6
1030     //
1031     // Discord discussion:
1032     // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854
1033     //
1034     // NOLINTNEXTLINE(performance-unnecessary-value-param)
1035     const nlohmann::json newConfiguration,
1036     sdbusplus::asio::object_server& objServer)
1037 {
1038     loadOverlays(newConfiguration);
1039 
1040     boost::asio::post(io, [systemConfiguration]() {
1041         if (!writeJsonFiles(systemConfiguration))
1042         {
1043             std::cerr << "Error writing json files\n";
1044         }
1045     });
1046 
1047     boost::asio::post(io, [&instance, count, &timer, newConfiguration,
1048                            &systemConfiguration, &objServer]() {
1049         postToDbus(newConfiguration, systemConfiguration, objServer);
1050         if (count == instance)
1051         {
1052             startRemovedTimer(timer, systemConfiguration);
1053         }
1054     });
1055 }
1056 
1057 // main properties changed entry
1058 void propertiesChangedCallback(nlohmann::json& systemConfiguration,
1059                                sdbusplus::asio::object_server& objServer)
1060 {
1061     static bool inProgress = false;
1062     static boost::asio::steady_timer timer(io);
1063     static size_t instance = 0;
1064     instance++;
1065     size_t count = instance;
1066 
1067     timer.expires_after(std::chrono::seconds(5));
1068 
1069     // setup an async wait as we normally get flooded with new requests
1070     timer.async_wait([&systemConfiguration, &objServer,
1071                       count](const boost::system::error_code& ec) {
1072         if (ec == boost::asio::error::operation_aborted)
1073         {
1074             // we were cancelled
1075             return;
1076         }
1077         if (ec)
1078         {
1079             std::cerr << "async wait error " << ec << "\n";
1080             return;
1081         }
1082 
1083         if (inProgress)
1084         {
1085             propertiesChangedCallback(systemConfiguration, objServer);
1086             return;
1087         }
1088         inProgress = true;
1089 
1090         nlohmann::json oldConfiguration = systemConfiguration;
1091         auto missingConfigurations = std::make_shared<nlohmann::json>();
1092         *missingConfigurations = systemConfiguration;
1093 
1094         std::list<nlohmann::json> configurations;
1095         if (!loadConfigurations(configurations))
1096         {
1097             std::cerr << "Could not load configurations\n";
1098             inProgress = false;
1099             return;
1100         }
1101 
1102         auto perfScan = std::make_shared<PerformScan>(
1103             systemConfiguration, *missingConfigurations, configurations,
1104             objServer,
1105             [&systemConfiguration, &objServer, count, oldConfiguration,
1106              missingConfigurations]() {
1107             // this is something that since ac has been applied to the bmc
1108             // we saw, and we no longer see it
1109             bool powerOff = !isPowerOn();
1110             for (const auto& [name, device] : missingConfigurations->items())
1111             {
1112                 pruneConfiguration(systemConfiguration, objServer, powerOff,
1113                                    name, device);
1114             }
1115 
1116             nlohmann::json newConfiguration = systemConfiguration;
1117 
1118             deriveNewConfiguration(oldConfiguration, newConfiguration);
1119 
1120             for (const auto& [_, device] : newConfiguration.items())
1121             {
1122                 logDeviceAdded(device);
1123             }
1124 
1125             inProgress = false;
1126 
1127             boost::asio::post(
1128                 io, std::bind_front(publishNewConfiguration, std::ref(instance),
1129                                     count, std::ref(timer),
1130                                     std::ref(systemConfiguration),
1131                                     newConfiguration, std::ref(objServer)));
1132         });
1133         perfScan->run();
1134     });
1135 }
1136 
1137 // Extract the D-Bus interfaces to probe from the JSON config files.
1138 static std::set<std::string> getProbeInterfaces()
1139 {
1140     std::set<std::string> interfaces;
1141     std::list<nlohmann::json> configurations;
1142     if (!loadConfigurations(configurations))
1143     {
1144         return interfaces;
1145     }
1146 
1147     for (auto it = configurations.begin(); it != configurations.end();)
1148     {
1149         auto findProbe = it->find("Probe");
1150         if (findProbe == it->end())
1151         {
1152             std::cerr << "configuration file missing probe:\n " << *it << "\n";
1153             it++;
1154             continue;
1155         }
1156 
1157         nlohmann::json probeCommand;
1158         if ((*findProbe).type() != nlohmann::json::value_t::array)
1159         {
1160             probeCommand = nlohmann::json::array();
1161             probeCommand.push_back(*findProbe);
1162         }
1163         else
1164         {
1165             probeCommand = *findProbe;
1166         }
1167 
1168         for (const nlohmann::json& probeJson : probeCommand)
1169         {
1170             const std::string* probe = probeJson.get_ptr<const std::string*>();
1171             if (probe == nullptr)
1172             {
1173                 std::cerr << "Probe statement wasn't a string, can't parse";
1174                 continue;
1175             }
1176             // Skip it if the probe cmd doesn't contain an interface.
1177             if (findProbeType(*probe))
1178             {
1179                 continue;
1180             }
1181 
1182             // syntax requires probe before first open brace
1183             auto findStart = probe->find('(');
1184             if (findStart != std::string::npos)
1185             {
1186                 std::string interface = probe->substr(0, findStart);
1187                 interfaces.emplace(interface);
1188             }
1189         }
1190         it++;
1191     }
1192 
1193     return interfaces;
1194 }
1195 
1196 // Check if InterfacesAdded payload contains an iface that needs probing.
1197 static bool
1198     iaContainsProbeInterface(sdbusplus::message_t& msg,
1199                              const std::set<std::string>& probeInterfaces)
1200 {
1201     sdbusplus::message::object_path path;
1202     DBusObject interfaces;
1203     std::set<std::string> interfaceSet;
1204     std::set<std::string> intersect;
1205 
1206     msg.read(path, interfaces);
1207 
1208     std::for_each(interfaces.begin(), interfaces.end(),
1209                   [&interfaceSet](const auto& iface) {
1210         interfaceSet.insert(iface.first);
1211     });
1212 
1213     std::set_intersection(interfaceSet.begin(), interfaceSet.end(),
1214                           probeInterfaces.begin(), probeInterfaces.end(),
1215                           std::inserter(intersect, intersect.end()));
1216     return !intersect.empty();
1217 }
1218 
1219 // Check if InterfacesRemoved payload contains an iface that needs probing.
1220 static bool
1221     irContainsProbeInterface(sdbusplus::message_t& msg,
1222                              const std::set<std::string>& probeInterfaces)
1223 {
1224     sdbusplus::message::object_path path;
1225     std::set<std::string> interfaces;
1226     std::set<std::string> intersect;
1227 
1228     msg.read(path, interfaces);
1229 
1230     std::set_intersection(interfaces.begin(), interfaces.end(),
1231                           probeInterfaces.begin(), probeInterfaces.end(),
1232                           std::inserter(intersect, intersect.end()));
1233     return !intersect.empty();
1234 }
1235 
1236 int main()
1237 {
1238     // setup connection to dbus
1239     systemBus = std::make_shared<sdbusplus::asio::connection>(io);
1240     systemBus->request_name("xyz.openbmc_project.EntityManager");
1241 
1242     // The EntityManager object itself doesn't expose any properties.
1243     // No need to set up ObjectManager for the |EntityManager| object.
1244     sdbusplus::asio::object_server objServer(systemBus, /*skipManager=*/true);
1245 
1246     // All other objects that EntityManager currently support are under the
1247     // inventory subtree.
1248     // See the discussion at
1249     // https://discord.com/channels/775381525260664832/1018929092009144380
1250     objServer.add_manager("/xyz/openbmc_project/inventory");
1251 
1252     std::shared_ptr<sdbusplus::asio::dbus_interface> entityIface =
1253         objServer.add_interface("/xyz/openbmc_project/EntityManager",
1254                                 "xyz.openbmc_project.EntityManager");
1255 
1256     // to keep reference to the match / filter objects so they don't get
1257     // destroyed
1258 
1259     nlohmann::json systemConfiguration = nlohmann::json::object();
1260 
1261     std::set<std::string> probeInterfaces = getProbeInterfaces();
1262 
1263     // We need a poke from DBus for static providers that create all their
1264     // objects prior to claiming a well-known name, and thus don't emit any
1265     // org.freedesktop.DBus.Properties signals.  Similarly if a process exits
1266     // for any reason, expected or otherwise, we'll need a poke to remove
1267     // entities from DBus.
1268     sdbusplus::bus::match_t nameOwnerChangedMatch(
1269         static_cast<sdbusplus::bus_t&>(*systemBus),
1270         sdbusplus::bus::match::rules::nameOwnerChanged(),
1271         [&](sdbusplus::message_t& m) {
1272         auto [name, oldOwner,
1273               newOwner] = m.unpack<std::string, std::string, std::string>();
1274 
1275         if (name.starts_with(':'))
1276         {
1277             // We should do nothing with unique-name connections.
1278             return;
1279         }
1280 
1281         propertiesChangedCallback(systemConfiguration, objServer);
1282     });
1283     // We also need a poke from DBus when new interfaces are created or
1284     // destroyed.
1285     sdbusplus::bus::match_t interfacesAddedMatch(
1286         static_cast<sdbusplus::bus_t&>(*systemBus),
1287         sdbusplus::bus::match::rules::interfacesAdded(),
1288         [&](sdbusplus::message_t& msg) {
1289         if (iaContainsProbeInterface(msg, probeInterfaces))
1290         {
1291             propertiesChangedCallback(systemConfiguration, objServer);
1292         }
1293     });
1294     sdbusplus::bus::match_t interfacesRemovedMatch(
1295         static_cast<sdbusplus::bus_t&>(*systemBus),
1296         sdbusplus::bus::match::rules::interfacesRemoved(),
1297         [&](sdbusplus::message_t& msg) {
1298         if (irContainsProbeInterface(msg, probeInterfaces))
1299         {
1300             propertiesChangedCallback(systemConfiguration, objServer);
1301         }
1302     });
1303 
1304     boost::asio::post(io, [&]() {
1305         propertiesChangedCallback(systemConfiguration, objServer);
1306     });
1307 
1308     entityIface->register_method("ReScan", [&]() {
1309         propertiesChangedCallback(systemConfiguration, objServer);
1310     });
1311     tryIfaceInitialize(entityIface);
1312 
1313     if (fwVersionIsSame())
1314     {
1315         if (std::filesystem::is_regular_file(currentConfiguration))
1316         {
1317             // this file could just be deleted, but it's nice for debug
1318             std::filesystem::create_directory(tempConfigDir);
1319             std::filesystem::remove(lastConfiguration);
1320             std::filesystem::copy(currentConfiguration, lastConfiguration);
1321             std::filesystem::remove(currentConfiguration);
1322 
1323             std::ifstream jsonStream(lastConfiguration);
1324             if (jsonStream.good())
1325             {
1326                 auto data = nlohmann::json::parse(jsonStream, nullptr, false);
1327                 if (data.is_discarded())
1328                 {
1329                     std::cerr << "syntax error in " << lastConfiguration
1330                               << "\n";
1331                 }
1332                 else
1333                 {
1334                     lastJson = std::move(data);
1335                 }
1336             }
1337             else
1338             {
1339                 std::cerr << "unable to open " << lastConfiguration << "\n";
1340             }
1341         }
1342     }
1343     else
1344     {
1345         // not an error, just logging at this level to make it in the journal
1346         std::cerr << "Clearing previous configuration\n";
1347         std::filesystem::remove(currentConfiguration);
1348     }
1349 
1350     // some boards only show up after power is on, we want to not say they are
1351     // removed until the same state happens
1352     setupPowerMatch(systemBus);
1353 
1354     io.run();
1355 
1356     return 0;
1357 }
1358