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