1 /*
2 // Copyright (c) 2018 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 */
16 /// \file entity_manager.cpp
17 
18 #include "entity_manager.hpp"
19 
20 #include "overlay.hpp"
21 #include "topology.hpp"
22 #include "utils.hpp"
23 #include "variant_visitors.hpp"
24 
25 #include <boost/algorithm/string/case_conv.hpp>
26 #include <boost/algorithm/string/classification.hpp>
27 #include <boost/algorithm/string/predicate.hpp>
28 #include <boost/algorithm/string/replace.hpp>
29 #include <boost/algorithm/string/split.hpp>
30 #include <boost/asio/io_context.hpp>
31 #include <boost/asio/post.hpp>
32 #include <boost/asio/steady_timer.hpp>
33 #include <boost/container/flat_map.hpp>
34 #include <boost/container/flat_set.hpp>
35 #include <boost/range/iterator_range.hpp>
36 #include <nlohmann/json.hpp>
37 #include <sdbusplus/asio/connection.hpp>
38 #include <sdbusplus/asio/object_server.hpp>
39 
40 #include <charconv>
41 #include <filesystem>
42 #include <fstream>
43 #include <functional>
44 #include <iostream>
45 #include <map>
46 #include <regex>
47 #include <variant>
48 constexpr const char* hostConfigurationDirectory = SYSCONF_DIR "configurations";
49 constexpr const char* configurationDirectory = PACKAGE_DIR "configurations";
50 constexpr const char* schemaDirectory = PACKAGE_DIR "configurations/schemas";
51 constexpr const char* tempConfigDir = "/tmp/configuration/";
52 constexpr const char* lastConfiguration = "/tmp/configuration/last.json";
53 constexpr const char* currentConfiguration = "/var/configuration/system.json";
54 constexpr const char* globalSchema = "global.json";
55 
56 const boost::container::flat_map<const char*, probe_type_codes, CmpStr>
57     probeTypes{{{"FALSE", probe_type_codes::FALSE_T},
58                 {"TRUE", probe_type_codes::TRUE_T},
59                 {"AND", probe_type_codes::AND},
60                 {"OR", probe_type_codes::OR},
61                 {"FOUND", probe_type_codes::FOUND},
62                 {"MATCH_ONE", probe_type_codes::MATCH_ONE}}};
63 
64 static constexpr std::array<const char*, 6> settableInterfaces = {
65     "FanProfile", "Pid", "Pid.Zone", "Stepwise", "Thresholds", "Polling"};
66 using JsonVariantType =
67     std::variant<std::vector<std::string>, std::vector<double>, std::string,
68                  int64_t, uint64_t, double, int32_t, uint32_t, int16_t,
69                  uint16_t, uint8_t, bool>;
70 
71 // store reference to all interfaces so we can destroy them later
72 boost::container::flat_map<
73     std::string, std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>>
74     inventory;
75 
76 // todo: pass this through nicer
77 std::shared_ptr<sdbusplus::asio::connection> systemBus;
78 nlohmann::json lastJson;
79 Topology topology;
80 
81 boost::asio::io_context io;
82 
83 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
84 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
85 
86 FoundProbeTypeT findProbeType(const std::string& probe)
87 {
88     boost::container::flat_map<const char*, probe_type_codes,
89                                CmpStr>::const_iterator probeType;
90     for (probeType = probeTypes.begin(); probeType != probeTypes.end();
91          ++probeType)
92     {
93         if (probe.find(probeType->first) != std::string::npos)
94         {
95             return probeType;
96         }
97     }
98 
99     return std::nullopt;
100 }
101 
102 static std::shared_ptr<sdbusplus::asio::dbus_interface>
103     createInterface(sdbusplus::asio::object_server& objServer,
104                     const std::string& path, const std::string& interface,
105                     const std::string& parent, bool checkNull = false)
106 {
107     // on first add we have no reason to check for null before add, as there
108     // won't be any. For dynamically added interfaces, we check for null so that
109     // a constant delete/add will not create a memory leak
110 
111     auto ptr = objServer.add_interface(path, interface);
112     auto& dataVector = inventory[parent];
113     if (checkNull)
114     {
115         auto it = std::find_if(dataVector.begin(), dataVector.end(),
116                                [](const auto& p) { return p.expired(); });
117         if (it != dataVector.end())
118         {
119             *it = ptr;
120             return ptr;
121         }
122     }
123     dataVector.emplace_back(ptr);
124     return ptr;
125 }
126 
127 // writes output files to persist data
128 bool writeJsonFiles(const nlohmann::json& systemConfiguration)
129 {
130     std::filesystem::create_directory(configurationOutDir);
131     std::ofstream output(currentConfiguration);
132     if (!output.good())
133     {
134         return false;
135     }
136     output << systemConfiguration.dump(4);
137     output.close();
138     return true;
139 }
140 
141 template <typename JsonType>
142 bool setJsonFromPointer(const std::string& ptrStr, const JsonType& value,
143                         nlohmann::json& systemConfiguration)
144 {
145     try
146     {
147         nlohmann::json::json_pointer ptr(ptrStr);
148         nlohmann::json& ref = systemConfiguration[ptr];
149         ref = value;
150         return true;
151     }
152     catch (const std::out_of_range&)
153     {
154         return false;
155     }
156 }
157 
158 // template function to add array as dbus property
159 template <typename PropertyType>
160 void addArrayToDbus(const std::string& name, const nlohmann::json& array,
161                     sdbusplus::asio::dbus_interface* iface,
162                     sdbusplus::asio::PropertyPermission permission,
163                     nlohmann::json& systemConfiguration,
164                     const std::string& jsonPointerString)
165 {
166     std::vector<PropertyType> values;
167     for (const auto& property : array)
168     {
169         auto ptr = property.get_ptr<const PropertyType*>();
170         if (ptr != nullptr)
171         {
172             values.emplace_back(*ptr);
173         }
174     }
175 
176     if (permission == sdbusplus::asio::PropertyPermission::readOnly)
177     {
178         iface->register_property(name, values);
179     }
180     else
181     {
182         iface->register_property(
183             name, values,
184             [&systemConfiguration,
185              jsonPointerString{std::string(jsonPointerString)}](
186                 const std::vector<PropertyType>& newVal,
187                 std::vector<PropertyType>& val) {
188             val = newVal;
189             if (!setJsonFromPointer(jsonPointerString, val,
190                                     systemConfiguration))
191             {
192                 std::cerr << "error setting json field\n";
193                 return -1;
194             }
195             if (!writeJsonFiles(systemConfiguration))
196             {
197                 std::cerr << "error setting json file\n";
198                 return -1;
199             }
200             return 1;
201         });
202     }
203 }
204 
205 template <typename PropertyType>
206 void addProperty(const std::string& name, const PropertyType& value,
207                  sdbusplus::asio::dbus_interface* iface,
208                  nlohmann::json& systemConfiguration,
209                  const std::string& jsonPointerString,
210                  sdbusplus::asio::PropertyPermission permission)
211 {
212     if (permission == sdbusplus::asio::PropertyPermission::readOnly)
213     {
214         iface->register_property(name, value);
215         return;
216     }
217     iface->register_property(
218         name, value,
219         [&systemConfiguration,
220          jsonPointerString{std::string(jsonPointerString)}](
221             const PropertyType& newVal, PropertyType& val) {
222         val = newVal;
223         if (!setJsonFromPointer(jsonPointerString, val, 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("Delete",
245                            [&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 = nlohmann::json::parse(schemaFile, nullptr,
522                                                       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(), dbusName.end(),
548                            illegalDbusMemberRegex, "_");
549 
550         std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
551             createInterface(objServer, path + "/" + dbusName,
552                             "xyz.openbmc_project.Configuration." + *type, board,
553                             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     std::map<std::string, std::string> newBoards; // path -> name
571 
572     // iterate through boards
573     for (const auto& [boardId, boardConfig] : newConfiguration.items())
574     {
575         std::string boardName = boardConfig["Name"];
576         std::string boardNameOrig = 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 " << boardName
593                       << " reverting to Chassis.\n";
594             boardType = "Chassis";
595         }
596         std::string boardtypeLower = boost::algorithm::to_lower_copy(boardType);
597 
598         std::regex_replace(boardName.begin(), boardName.begin(),
599                            boardName.end(), illegalDbusMemberRegex, "_");
600         std::string boardPath = "/xyz/openbmc_project/inventory/system/";
601         boardPath += boardtypeLower;
602         boardPath += "/";
603         boardPath += boardName;
604 
605         std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface =
606             createInterface(objServer, boardPath,
607                             "xyz.openbmc_project.Inventory.Item", boardName);
608 
609         std::shared_ptr<sdbusplus::asio::dbus_interface> boardIface =
610             createInterface(objServer, boardPath,
611                             "xyz.openbmc_project.Inventory.Item." + boardType,
612                             boardNameOrig);
613 
614         createAddObjectMethod(jsonPointerPath, boardPath, systemConfiguration,
615                               objServer, boardNameOrig);
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, boardPath, propName,
627                                     boardNameOrig);
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 = boardPath;
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                                 boardNameOrig);
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                                     boardNameOrig);
697                 populateInterfaceFromJson(systemConfiguration, jsonPointerPath,
698                                           bmcIface, item, objServer,
699                                           getPermission(itemType));
700             }
701             else if (itemType == "System")
702             {
703                 std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface =
704                     createInterface(objServer, ifacePath,
705                                     "xyz.openbmc_project.Inventory.Item.System",
706                                     boardNameOrig);
707                 populateInterfaceFromJson(systemConfiguration, jsonPointerPath,
708                                           systemIface, item, objServer,
709                                           getPermission(itemType));
710             }
711 
712             populateInterfaceFromJson(systemConfiguration, jsonPointerPath,
713                                       itemIface, item, objServer,
714                                       getPermission(itemType));
715 
716             for (const auto& [name, config] : item.items())
717             {
718                 jsonPointerPath = jsonPointerPathBoard;
719                 jsonPointerPath.append(std::to_string(exposesIndex))
720                     .append("/")
721                     .append(name);
722                 if (config.type() == nlohmann::json::value_t::object)
723                 {
724                     std::string ifaceName =
725                         "xyz.openbmc_project.Configuration.";
726                     ifaceName.append(itemType).append(".").append(name);
727 
728                     std::shared_ptr<sdbusplus::asio::dbus_interface>
729                         objectIface = createInterface(objServer, ifacePath,
730                                                       ifaceName, boardNameOrig);
731 
732                     populateInterfaceFromJson(
733                         systemConfiguration, jsonPointerPath, objectIface,
734                         config, objServer, getPermission(name));
735                 }
736                 else if (config.type() == nlohmann::json::value_t::array)
737                 {
738                     size_t index = 0;
739                     if (config.empty())
740                     {
741                         continue;
742                     }
743                     bool isLegal = true;
744                     auto type = config[0].type();
745                     if (type != nlohmann::json::value_t::object)
746                     {
747                         continue;
748                     }
749 
750                     // verify legal json
751                     for (const auto& arrayItem : config)
752                     {
753                         if (arrayItem.type() != type)
754                         {
755                             isLegal = false;
756                             break;
757                         }
758                     }
759                     if (!isLegal)
760                     {
761                         std::cerr << "dbus format error" << config << "\n";
762                         break;
763                     }
764 
765                     for (auto& arrayItem : config)
766                     {
767                         std::string ifaceName =
768                             "xyz.openbmc_project.Configuration.";
769                         ifaceName.append(itemType).append(".").append(name);
770                         ifaceName.append(std::to_string(index));
771 
772                         std::shared_ptr<sdbusplus::asio::dbus_interface>
773                             objectIface = createInterface(
774                                 objServer, ifacePath, ifaceName, boardNameOrig);
775 
776                         populateInterfaceFromJson(
777                             systemConfiguration,
778                             jsonPointerPath + "/" + std::to_string(index),
779                             objectIface, arrayItem, objServer,
780                             getPermission(name));
781                         index++;
782                     }
783                 }
784             }
785 
786             topology.addBoard(boardPath, boardType, boardNameOrig, item);
787         }
788 
789         newBoards.emplace(boardPath, boardNameOrig);
790     }
791 
792     for (const auto& [assocPath, assocPropValue] :
793          topology.getAssocs(newBoards))
794     {
795         auto findBoard = newBoards.find(assocPath);
796         if (findBoard == newBoards.end())
797         {
798             continue;
799         }
800 
801         auto ifacePtr = createInterface(
802             objServer, assocPath, "xyz.openbmc_project.Association.Definitions",
803             findBoard->second);
804 
805         ifacePtr->register_property("Associations", assocPropValue);
806         ifacePtr->initialize();
807     }
808 }
809 
810 // reads json files out of the filesystem
811 bool loadConfigurations(std::list<nlohmann::json>& configurations)
812 {
813     // find configuration files
814     std::vector<std::filesystem::path> jsonPaths;
815     if (!findFiles(
816             std::vector<std::filesystem::path>{configurationDirectory,
817                                                hostConfigurationDirectory},
818             R"(.*\.json)", jsonPaths))
819     {
820         std::cerr << "Unable to find any configuration files in "
821                   << configurationDirectory << "\n";
822         return false;
823     }
824 
825     std::ifstream schemaStream(std::string(schemaDirectory) + "/" +
826                                globalSchema);
827     if (!schemaStream.good())
828     {
829         std::cerr
830             << "Cannot open schema file,  cannot validate JSON, exiting\n\n";
831         std::exit(EXIT_FAILURE);
832         return false;
833     }
834     nlohmann::json schema = nlohmann::json::parse(schemaStream, nullptr, false);
835     if (schema.is_discarded())
836     {
837         std::cerr
838             << "Illegal schema file detected, cannot validate JSON, exiting\n";
839         std::exit(EXIT_FAILURE);
840         return false;
841     }
842 
843     for (auto& jsonPath : jsonPaths)
844     {
845         std::ifstream jsonStream(jsonPath.c_str());
846         if (!jsonStream.good())
847         {
848             std::cerr << "unable to open " << jsonPath.string() << "\n";
849             continue;
850         }
851         auto data = nlohmann::json::parse(jsonStream, nullptr, false);
852         if (data.is_discarded())
853         {
854             std::cerr << "syntax error in " << jsonPath.string() << "\n";
855             continue;
856         }
857         /*
858          * todo(james): reenable this once less things are in flight
859          *
860         if (!validateJson(schema, data))
861         {
862             std::cerr << "Error validating " << jsonPath.string() << "\n";
863             continue;
864         }
865         */
866 
867         if (data.type() == nlohmann::json::value_t::array)
868         {
869             for (auto& d : data)
870             {
871                 configurations.emplace_back(d);
872             }
873         }
874         else
875         {
876             configurations.emplace_back(data);
877         }
878     }
879     return true;
880 }
881 
882 static bool deviceRequiresPowerOn(const nlohmann::json& entity)
883 {
884     auto powerState = entity.find("PowerState");
885     if (powerState == entity.end())
886     {
887         return false;
888     }
889 
890     const auto* ptr = powerState->get_ptr<const std::string*>();
891     if (ptr == nullptr)
892     {
893         return false;
894     }
895 
896     return *ptr == "On" || *ptr == "BiosPost";
897 }
898 
899 static void pruneDevice(const nlohmann::json& systemConfiguration,
900                         const bool powerOff, const bool scannedPowerOff,
901                         const std::string& name, const nlohmann::json& device)
902 {
903     if (systemConfiguration.contains(name))
904     {
905         return;
906     }
907 
908     if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff))
909     {
910         return;
911     }
912 
913     logDeviceRemoved(device);
914 }
915 
916 void startRemovedTimer(boost::asio::steady_timer& timer,
917                        nlohmann::json& systemConfiguration)
918 {
919     static bool scannedPowerOff = false;
920     static bool scannedPowerOn = false;
921 
922     if (systemConfiguration.empty() || lastJson.empty())
923     {
924         return; // not ready yet
925     }
926     if (scannedPowerOn)
927     {
928         return;
929     }
930 
931     if (!isPowerOn() && scannedPowerOff)
932     {
933         return;
934     }
935 
936     timer.expires_after(std::chrono::seconds(10));
937     timer.async_wait(
938         [&systemConfiguration](const boost::system::error_code& ec) {
939         if (ec == boost::asio::error::operation_aborted)
940         {
941             return;
942         }
943 
944         bool powerOff = !isPowerOn();
945         for (const auto& [name, device] : lastJson.items())
946         {
947             pruneDevice(systemConfiguration, powerOff, scannedPowerOff, name,
948                         device);
949         }
950 
951         scannedPowerOff = true;
952         if (!powerOff)
953         {
954             scannedPowerOn = true;
955         }
956     });
957 }
958 
959 static std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
960     getDeviceInterfaces(const nlohmann::json& device)
961 {
962     return inventory[device["Name"].get<std::string>()];
963 }
964 
965 static void pruneConfiguration(nlohmann::json& systemConfiguration,
966                                sdbusplus::asio::object_server& objServer,
967                                bool powerOff, const std::string& name,
968                                const nlohmann::json& device)
969 {
970     if (powerOff && deviceRequiresPowerOn(device))
971     {
972         // power not on yet, don't know if it's there or not
973         return;
974     }
975 
976     auto& ifaces = getDeviceInterfaces(device);
977     for (auto& iface : ifaces)
978     {
979         auto sharedPtr = iface.lock();
980         if (!!sharedPtr)
981         {
982             objServer.remove_interface(sharedPtr);
983         }
984     }
985 
986     ifaces.clear();
987     systemConfiguration.erase(name);
988     topology.remove(device["Name"].get<std::string>());
989     logDeviceRemoved(device);
990 }
991 
992 static void deriveNewConfiguration(const nlohmann::json& oldConfiguration,
993                                    nlohmann::json& newConfiguration)
994 {
995     for (auto it = newConfiguration.begin(); it != newConfiguration.end();)
996     {
997         auto findKey = oldConfiguration.find(it.key());
998         if (findKey != oldConfiguration.end())
999         {
1000             it = newConfiguration.erase(it);
1001         }
1002         else
1003         {
1004             it++;
1005         }
1006     }
1007 }
1008 
1009 static void publishNewConfiguration(
1010     const size_t& instance, const size_t count,
1011     boost::asio::steady_timer& timer, nlohmann::json& systemConfiguration,
1012     // Gerrit discussion:
1013     // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6
1014     //
1015     // Discord discussion:
1016     // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854
1017     //
1018     // NOLINTNEXTLINE(performance-unnecessary-value-param)
1019     const nlohmann::json newConfiguration,
1020     sdbusplus::asio::object_server& objServer)
1021 {
1022     loadOverlays(newConfiguration);
1023 
1024     boost::asio::post(io, [systemConfiguration]() {
1025         if (!writeJsonFiles(systemConfiguration))
1026         {
1027             std::cerr << "Error writing json files\n";
1028         }
1029     });
1030 
1031     boost::asio::post(io, [&instance, count, &timer, newConfiguration,
1032                            &systemConfiguration, &objServer]() {
1033         postToDbus(newConfiguration, systemConfiguration, objServer);
1034         if (count == instance)
1035         {
1036             startRemovedTimer(timer, systemConfiguration);
1037         }
1038     });
1039 }
1040 
1041 // main properties changed entry
1042 void propertiesChangedCallback(nlohmann::json& systemConfiguration,
1043                                sdbusplus::asio::object_server& objServer)
1044 {
1045     static bool inProgress = false;
1046     static boost::asio::steady_timer timer(io);
1047     static size_t instance = 0;
1048     instance++;
1049     size_t count = instance;
1050 
1051     timer.expires_after(std::chrono::seconds(5));
1052 
1053     // setup an async wait as we normally get flooded with new requests
1054     timer.async_wait([&systemConfiguration, &objServer,
1055                       count](const boost::system::error_code& ec) {
1056         if (ec == boost::asio::error::operation_aborted)
1057         {
1058             // we were cancelled
1059             return;
1060         }
1061         if (ec)
1062         {
1063             std::cerr << "async wait error " << ec << "\n";
1064             return;
1065         }
1066 
1067         if (inProgress)
1068         {
1069             propertiesChangedCallback(systemConfiguration, objServer);
1070             return;
1071         }
1072         inProgress = true;
1073 
1074         nlohmann::json oldConfiguration = systemConfiguration;
1075         auto missingConfigurations = std::make_shared<nlohmann::json>();
1076         *missingConfigurations = systemConfiguration;
1077 
1078         std::list<nlohmann::json> configurations;
1079         if (!loadConfigurations(configurations))
1080         {
1081             std::cerr << "Could not load configurations\n";
1082             inProgress = false;
1083             return;
1084         }
1085 
1086         auto perfScan = std::make_shared<PerformScan>(
1087             systemConfiguration, *missingConfigurations, configurations,
1088             objServer,
1089             [&systemConfiguration, &objServer, count, oldConfiguration,
1090              missingConfigurations]() {
1091             // this is something that since ac has been applied to the bmc
1092             // we saw, and we no longer see it
1093             bool powerOff = !isPowerOn();
1094             for (const auto& [name, device] : missingConfigurations->items())
1095             {
1096                 pruneConfiguration(systemConfiguration, objServer, powerOff,
1097                                    name, device);
1098             }
1099 
1100             nlohmann::json newConfiguration = systemConfiguration;
1101 
1102             deriveNewConfiguration(oldConfiguration, newConfiguration);
1103 
1104             for (const auto& [_, device] : newConfiguration.items())
1105             {
1106                 logDeviceAdded(device);
1107             }
1108 
1109             inProgress = false;
1110 
1111             boost::asio::post(
1112                 io, std::bind_front(publishNewConfiguration, std::ref(instance),
1113                                     count, std::ref(timer),
1114                                     std::ref(systemConfiguration),
1115                                     newConfiguration, std::ref(objServer)));
1116         });
1117         perfScan->run();
1118     });
1119 }
1120 
1121 // Extract the D-Bus interfaces to probe from the JSON config files.
1122 static std::set<std::string> getProbeInterfaces()
1123 {
1124     std::set<std::string> interfaces;
1125     std::list<nlohmann::json> configurations;
1126     if (!loadConfigurations(configurations))
1127     {
1128         return interfaces;
1129     }
1130 
1131     for (auto it = configurations.begin(); it != configurations.end();)
1132     {
1133         auto findProbe = it->find("Probe");
1134         if (findProbe == it->end())
1135         {
1136             std::cerr << "configuration file missing probe:\n " << *it << "\n";
1137             it++;
1138             continue;
1139         }
1140 
1141         nlohmann::json probeCommand;
1142         if ((*findProbe).type() != nlohmann::json::value_t::array)
1143         {
1144             probeCommand = nlohmann::json::array();
1145             probeCommand.push_back(*findProbe);
1146         }
1147         else
1148         {
1149             probeCommand = *findProbe;
1150         }
1151 
1152         for (const nlohmann::json& probeJson : probeCommand)
1153         {
1154             const std::string* probe = probeJson.get_ptr<const std::string*>();
1155             if (probe == nullptr)
1156             {
1157                 std::cerr << "Probe statement wasn't a string, can't parse";
1158                 continue;
1159             }
1160             // Skip it if the probe cmd doesn't contain an interface.
1161             if (findProbeType(*probe))
1162             {
1163                 continue;
1164             }
1165 
1166             // syntax requires probe before first open brace
1167             auto findStart = probe->find('(');
1168             if (findStart != std::string::npos)
1169             {
1170                 std::string interface = probe->substr(0, findStart);
1171                 interfaces.emplace(interface);
1172             }
1173         }
1174         it++;
1175     }
1176 
1177     return interfaces;
1178 }
1179 
1180 // Check if InterfacesAdded payload contains an iface that needs probing.
1181 static bool
1182     iaContainsProbeInterface(sdbusplus::message_t& msg,
1183                              const std::set<std::string>& probeInterfaces)
1184 {
1185     sdbusplus::message::object_path path;
1186     DBusObject interfaces;
1187     std::set<std::string> interfaceSet;
1188     std::set<std::string> intersect;
1189 
1190     msg.read(path, interfaces);
1191 
1192     std::for_each(interfaces.begin(), interfaces.end(),
1193                   [&interfaceSet](const auto& iface) {
1194         interfaceSet.insert(iface.first);
1195     });
1196 
1197     std::set_intersection(interfaceSet.begin(), interfaceSet.end(),
1198                           probeInterfaces.begin(), probeInterfaces.end(),
1199                           std::inserter(intersect, intersect.end()));
1200     return !intersect.empty();
1201 }
1202 
1203 // Check if InterfacesRemoved payload contains an iface that needs probing.
1204 static bool
1205     irContainsProbeInterface(sdbusplus::message_t& msg,
1206                              const std::set<std::string>& probeInterfaces)
1207 {
1208     sdbusplus::message::object_path path;
1209     std::set<std::string> interfaces;
1210     std::set<std::string> intersect;
1211 
1212     msg.read(path, interfaces);
1213 
1214     std::set_intersection(interfaces.begin(), interfaces.end(),
1215                           probeInterfaces.begin(), probeInterfaces.end(),
1216                           std::inserter(intersect, intersect.end()));
1217     return !intersect.empty();
1218 }
1219 
1220 int main()
1221 {
1222     // setup connection to dbus
1223     systemBus = std::make_shared<sdbusplus::asio::connection>(io);
1224     systemBus->request_name("xyz.openbmc_project.EntityManager");
1225 
1226     // The EntityManager object itself doesn't expose any properties.
1227     // No need to set up ObjectManager for the |EntityManager| object.
1228     sdbusplus::asio::object_server objServer(systemBus, /*skipManager=*/true);
1229 
1230     // All other objects that EntityManager currently support are under the
1231     // inventory subtree.
1232     // See the discussion at
1233     // https://discord.com/channels/775381525260664832/1018929092009144380
1234     objServer.add_manager("/xyz/openbmc_project/inventory");
1235 
1236     std::shared_ptr<sdbusplus::asio::dbus_interface> entityIface =
1237         objServer.add_interface("/xyz/openbmc_project/EntityManager",
1238                                 "xyz.openbmc_project.EntityManager");
1239 
1240     // to keep reference to the match / filter objects so they don't get
1241     // destroyed
1242 
1243     nlohmann::json systemConfiguration = nlohmann::json::object();
1244 
1245     std::set<std::string> probeInterfaces = getProbeInterfaces();
1246 
1247     // We need a poke from DBus for static providers that create all their
1248     // objects prior to claiming a well-known name, and thus don't emit any
1249     // org.freedesktop.DBus.Properties signals.  Similarly if a process exits
1250     // for any reason, expected or otherwise, we'll need a poke to remove
1251     // entities from DBus.
1252     sdbusplus::bus::match_t nameOwnerChangedMatch(
1253         static_cast<sdbusplus::bus_t&>(*systemBus),
1254         sdbusplus::bus::match::rules::nameOwnerChanged(),
1255         [&](sdbusplus::message_t& m) {
1256         auto [name, oldOwner,
1257               newOwner] = m.unpack<std::string, std::string, std::string>();
1258 
1259         if (name.starts_with(':'))
1260         {
1261             // We should do nothing with unique-name connections.
1262             return;
1263         }
1264 
1265         propertiesChangedCallback(systemConfiguration, objServer);
1266     });
1267     // We also need a poke from DBus when new interfaces are created or
1268     // destroyed.
1269     sdbusplus::bus::match_t interfacesAddedMatch(
1270         static_cast<sdbusplus::bus_t&>(*systemBus),
1271         sdbusplus::bus::match::rules::interfacesAdded(),
1272         [&](sdbusplus::message_t& msg) {
1273         if (iaContainsProbeInterface(msg, probeInterfaces))
1274         {
1275             propertiesChangedCallback(systemConfiguration, objServer);
1276         }
1277     });
1278     sdbusplus::bus::match_t interfacesRemovedMatch(
1279         static_cast<sdbusplus::bus_t&>(*systemBus),
1280         sdbusplus::bus::match::rules::interfacesRemoved(),
1281         [&](sdbusplus::message_t& msg) {
1282         if (irContainsProbeInterface(msg, probeInterfaces))
1283         {
1284             propertiesChangedCallback(systemConfiguration, objServer);
1285         }
1286     });
1287 
1288     boost::asio::post(io, [&]() {
1289         propertiesChangedCallback(systemConfiguration, objServer);
1290     });
1291 
1292     entityIface->register_method("ReScan", [&]() {
1293         propertiesChangedCallback(systemConfiguration, objServer);
1294     });
1295     entityIface->initialize();
1296 
1297     if (fwVersionIsSame())
1298     {
1299         if (std::filesystem::is_regular_file(currentConfiguration))
1300         {
1301             // this file could just be deleted, but it's nice for debug
1302             std::filesystem::create_directory(tempConfigDir);
1303             std::filesystem::remove(lastConfiguration);
1304             std::filesystem::copy(currentConfiguration, lastConfiguration);
1305             std::filesystem::remove(currentConfiguration);
1306 
1307             std::ifstream jsonStream(lastConfiguration);
1308             if (jsonStream.good())
1309             {
1310                 auto data = nlohmann::json::parse(jsonStream, nullptr, false);
1311                 if (data.is_discarded())
1312                 {
1313                     std::cerr << "syntax error in " << lastConfiguration
1314                               << "\n";
1315                 }
1316                 else
1317                 {
1318                     lastJson = std::move(data);
1319                 }
1320             }
1321             else
1322             {
1323                 std::cerr << "unable to open " << lastConfiguration << "\n";
1324             }
1325         }
1326     }
1327     else
1328     {
1329         // not an error, just logging at this level to make it in the journal
1330         std::cerr << "Clearing previous configuration\n";
1331         std::filesystem::remove(currentConfiguration);
1332     }
1333 
1334     // some boards only show up after power is on, we want to not say they are
1335     // removed until the same state happens
1336     setupPowerMatch(systemBus);
1337 
1338     io.run();
1339 
1340     return 0;
1341 }
1342