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