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