xref: /openbmc/entity-manager/src/entity_manager/entity_manager.cpp (revision dbf95b2c54c5a40d1ea44d650eb6aab2a4c34ba5)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright 2018 Intel Corporation
3 
4 #include "entity_manager.hpp"
5 
6 #include "../utils.hpp"
7 #include "../variant_visitors.hpp"
8 #include "configuration.hpp"
9 #include "dbus_interface.hpp"
10 #include "log_device_inventory.hpp"
11 #include "overlay.hpp"
12 #include "perform_scan.hpp"
13 #include "topology.hpp"
14 #include "utils.hpp"
15 
16 #include <boost/asio/io_context.hpp>
17 #include <boost/asio/post.hpp>
18 #include <boost/asio/steady_timer.hpp>
19 #include <boost/container/flat_set.hpp>
20 #include <boost/range/iterator_range.hpp>
21 #include <nlohmann/json.hpp>
22 #include <phosphor-logging/lg2.hpp>
23 #include <sdbusplus/asio/connection.hpp>
24 #include <sdbusplus/asio/object_server.hpp>
25 #include <xyz/openbmc_project/Association/Definitions/common.hpp>
26 #include <xyz/openbmc_project/Inventory/Item/Bmc/common.hpp>
27 #include <xyz/openbmc_project/Inventory/Item/System/common.hpp>
28 #include <xyz/openbmc_project/Inventory/Item/common.hpp>
29 
30 #include <filesystem>
31 #include <flat_map>
32 #include <fstream>
33 #include <functional>
34 #include <map>
35 #include <regex>
36 constexpr const char* tempConfigDir = "/tmp/configuration/";
37 constexpr const char* lastConfiguration = "/tmp/configuration/last.json";
38 
39 static constexpr std::array<const char*, 6> settableInterfaces = {
40     "FanProfile", "Pid", "Pid.Zone", "Stepwise", "Thresholds", "Polling"};
41 
42 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
43 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
44 
45 sdbusplus::asio::PropertyPermission getPermission(const std::string& interface)
46 {
47     return std::find(settableInterfaces.begin(), settableInterfaces.end(),
48                      interface) != settableInterfaces.end()
49                ? sdbusplus::asio::PropertyPermission::readWrite
50                : sdbusplus::asio::PropertyPermission::readOnly;
51 }
52 
53 EntityManager::EntityManager(
54     std::shared_ptr<sdbusplus::asio::connection>& systemBus,
55     boost::asio::io_context& io,
56     const std::vector<std::filesystem::path>& configurationDirectories) :
57     systemBus(systemBus),
58     objServer(sdbusplus::asio::object_server(systemBus, /*skipManager=*/true)),
59     configuration(configurationDirectories), lastJson(nlohmann::json::object()),
60     systemConfiguration(nlohmann::json::object()), io(io),
61     dbus_interface(io, objServer), powerStatus(*systemBus),
62     propertiesChangedTimer(io)
63 {
64     // All other objects that EntityManager currently support are under the
65     // inventory subtree.
66     // See the discussion at
67     // https://discord.com/channels/775381525260664832/1018929092009144380
68     objServer.add_manager("/xyz/openbmc_project/inventory");
69 
70     entityIface = objServer.add_interface("/xyz/openbmc_project/EntityManager",
71                                           "xyz.openbmc_project.EntityManager");
72     entityIface->register_method("ReScan", [this]() {
73         propertiesChangedCallback();
74     });
75     dbus_interface::tryIfaceInitialize(entityIface);
76 
77     initFilters(configuration.probeInterfaces);
78 }
79 
80 void EntityManager::postToDbus(const nlohmann::json& newConfiguration)
81 {
82     std::map<std::string, std::string> newBoards; // path -> name
83 
84     // iterate through boards
85     for (const auto& [boardId, boardConfig] : newConfiguration.items())
86     {
87         postBoardToDBus(boardId, boardConfig, newBoards);
88     }
89 
90     for (const auto& [assocPath, assocPropValue] :
91          topology.getAssocs(std::views::keys(newBoards)))
92     {
93         auto findBoard = newBoards.find(assocPath);
94         if (findBoard == newBoards.end())
95         {
96             continue;
97         }
98 
99         auto ifacePtr = dbus_interface.createInterface(
100             assocPath,
101             sdbusplus::common::xyz::openbmc_project::association::Definitions::
102                 interface,
103             findBoard->second);
104 
105         ifacePtr->register_property("Associations", assocPropValue);
106         dbus_interface::tryIfaceInitialize(ifacePtr);
107     }
108 }
109 
110 void EntityManager::postBoardToDBus(
111     const std::string& boardId, const nlohmann::json& boardConfig,
112     std::map<std::string, std::string>& newBoards)
113 {
114     std::string boardName = boardConfig["Name"];
115     std::string boardNameOrig = boardConfig["Name"];
116     std::string jsonPointerPath = "/" + boardId;
117     // loop through newConfiguration, but use values from system
118     // configuration to be able to modify via dbus later
119     auto boardValues = systemConfiguration[boardId];
120     auto findBoardType = boardValues.find("Type");
121     std::string boardType;
122     if (findBoardType != boardValues.end() &&
123         findBoardType->type() == nlohmann::json::value_t::string)
124     {
125         boardType = findBoardType->get<std::string>();
126         std::regex_replace(boardType.begin(), boardType.begin(),
127                            boardType.end(), illegalDbusMemberRegex, "_");
128     }
129     else
130     {
131         lg2::error("Unable to find type for {BOARD} reverting to Chassis.",
132                    "BOARD", boardName);
133         boardType = "Chassis";
134     }
135 
136     const std::string boardPath =
137         em_utils::buildInventorySystemPath(boardName, boardType);
138 
139     std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface =
140         dbus_interface.createInterface(
141             boardPath,
142             sdbusplus::common::xyz::openbmc_project::inventory::Item::interface,
143             boardName);
144 
145     const std::string invItemIntf = std::format(
146         "{}.{}",
147         sdbusplus::common::xyz::openbmc_project::inventory::Item::interface,
148         boardType);
149 
150     std::shared_ptr<sdbusplus::asio::dbus_interface> boardIface =
151         dbus_interface.createInterface(boardPath, invItemIntf, boardNameOrig);
152 
153     dbus_interface.createAddObjectMethod(jsonPointerPath, boardPath,
154                                          systemConfiguration, boardNameOrig);
155 
156     dbus_interface.populateInterfaceFromJson(
157         systemConfiguration, jsonPointerPath, boardIface, boardValues);
158     jsonPointerPath += "/";
159     // iterate through board properties
160     for (const auto& [propName, propValue] : boardValues.items())
161     {
162         if (propValue.type() == nlohmann::json::value_t::object)
163         {
164             std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
165                 dbus_interface.createInterface(boardPath, propName,
166                                                boardNameOrig);
167 
168             dbus_interface.populateInterfaceFromJson(
169                 systemConfiguration, jsonPointerPath + propName, iface,
170                 propValue);
171         }
172     }
173 
174     nlohmann::json::iterator exposes = boardValues.find("Exposes");
175     if (exposes == boardValues.end())
176     {
177         return;
178     }
179     // iterate through exposes
180     jsonPointerPath += "Exposes/";
181 
182     // store the board level pointer so we can modify it on the way down
183     std::string jsonPointerPathBoard = jsonPointerPath;
184     size_t exposesIndex = -1;
185     for (nlohmann::json& item : *exposes)
186     {
187         postExposesRecordsToDBus(item, exposesIndex, boardNameOrig,
188                                  jsonPointerPath, jsonPointerPathBoard,
189                                  boardPath, boardType);
190     }
191 
192     newBoards.emplace(boardPath, boardNameOrig);
193 }
194 
195 void EntityManager::postExposesRecordsToDBus(
196     nlohmann::json& item, size_t& exposesIndex,
197     const std::string& boardNameOrig, std::string jsonPointerPath,
198     const std::string& jsonPointerPathBoard, const std::string& boardPath,
199     const std::string& boardType)
200 {
201     exposesIndex++;
202     jsonPointerPath = jsonPointerPathBoard;
203     jsonPointerPath += std::to_string(exposesIndex);
204 
205     auto findName = item.find("Name");
206     if (findName == item.end())
207     {
208         lg2::error("cannot find name in field {ITEM}", "ITEM", item);
209         return;
210     }
211     auto findStatus = item.find("Status");
212     // if status is not found it is assumed to be status = 'okay'
213     if (findStatus != item.end())
214     {
215         if (*findStatus == "disabled")
216         {
217             return;
218         }
219     }
220     auto findType = item.find("Type");
221     std::string itemType;
222     if (findType != item.end())
223     {
224         itemType = findType->get<std::string>();
225         std::regex_replace(itemType.begin(), itemType.begin(), itemType.end(),
226                            illegalDbusPathRegex, "_");
227     }
228     else
229     {
230         itemType = "unknown";
231     }
232     std::string itemName = findName->get<std::string>();
233     std::regex_replace(itemName.begin(), itemName.begin(), itemName.end(),
234                        illegalDbusMemberRegex, "_");
235     std::string ifacePath = boardPath;
236     ifacePath += "/";
237     ifacePath += itemName;
238 
239     if (itemType == "BMC")
240     {
241         std::shared_ptr<sdbusplus::asio::dbus_interface> bmcIface =
242             dbus_interface.createInterface(
243                 ifacePath,
244                 sdbusplus::common::xyz::openbmc_project::inventory::item::Bmc::
245                     interface,
246                 boardNameOrig);
247         dbus_interface.populateInterfaceFromJson(
248             systemConfiguration, jsonPointerPath, bmcIface, item,
249             getPermission(itemType));
250     }
251     else if (itemType == "System")
252     {
253         std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface =
254             dbus_interface.createInterface(
255                 ifacePath,
256                 sdbusplus::common::xyz::openbmc_project::inventory::item::
257                     System::interface,
258                 boardNameOrig);
259         dbus_interface.populateInterfaceFromJson(
260             systemConfiguration, jsonPointerPath, systemIface, item,
261             getPermission(itemType));
262     }
263 
264     for (const auto& [name, config] : item.items())
265     {
266         jsonPointerPath = jsonPointerPathBoard;
267         jsonPointerPath.append(std::to_string(exposesIndex))
268             .append("/")
269             .append(name);
270 
271         if (!postConfigurationRecord(name, config, boardNameOrig, itemType,
272                                      jsonPointerPath, ifacePath))
273         {
274             break;
275         }
276     }
277 
278     std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface =
279         dbus_interface.createInterface(
280             ifacePath, "xyz.openbmc_project.Configuration." + itemType,
281             boardNameOrig);
282 
283     dbus_interface.populateInterfaceFromJson(
284         systemConfiguration, jsonPointerPath, itemIface, item,
285         getPermission(itemType));
286 
287     topology.addBoard(boardPath, boardType, boardNameOrig, item);
288 }
289 
290 bool EntityManager::postConfigurationRecord(
291     const std::string& name, nlohmann::json& config,
292     const std::string& boardNameOrig, const std::string& itemType,
293     const std::string& jsonPointerPath, const std::string& ifacePath)
294 {
295     if (config.type() == nlohmann::json::value_t::object)
296     {
297         std::string ifaceName = "xyz.openbmc_project.Configuration.";
298         ifaceName.append(itemType).append(".").append(name);
299 
300         std::shared_ptr<sdbusplus::asio::dbus_interface> objectIface =
301             dbus_interface.createInterface(ifacePath, ifaceName, boardNameOrig);
302 
303         dbus_interface.populateInterfaceFromJson(
304             systemConfiguration, jsonPointerPath, objectIface, config,
305             getPermission(name));
306     }
307     else if (config.type() == nlohmann::json::value_t::array)
308     {
309         size_t index = 0;
310         if (config.empty())
311         {
312             return true;
313         }
314         bool isLegal = true;
315         auto type = config[0].type();
316         if (type != nlohmann::json::value_t::object)
317         {
318             return true;
319         }
320 
321         // verify legal json
322         for (const auto& arrayItem : config)
323         {
324             if (arrayItem.type() != type)
325             {
326                 isLegal = false;
327                 break;
328             }
329         }
330         if (!isLegal)
331         {
332             lg2::error("dbus format error {JSON}", "JSON", config);
333             return false;
334         }
335 
336         for (auto& arrayItem : config)
337         {
338             std::string ifaceName = "xyz.openbmc_project.Configuration.";
339             ifaceName.append(itemType).append(".").append(name);
340             ifaceName.append(std::to_string(index));
341 
342             std::shared_ptr<sdbusplus::asio::dbus_interface> objectIface =
343                 dbus_interface.createInterface(ifacePath, ifaceName,
344                                                boardNameOrig);
345 
346             dbus_interface.populateInterfaceFromJson(
347                 systemConfiguration,
348                 jsonPointerPath + "/" + std::to_string(index), objectIface,
349                 arrayItem, getPermission(name));
350             index++;
351         }
352     }
353 
354     return true;
355 }
356 
357 static bool deviceRequiresPowerOn(const nlohmann::json& entity)
358 {
359     auto powerState = entity.find("PowerState");
360     if (powerState == entity.end())
361     {
362         return false;
363     }
364 
365     const auto* ptr = powerState->get_ptr<const std::string*>();
366     if (ptr == nullptr)
367     {
368         return false;
369     }
370 
371     return *ptr == "On" || *ptr == "BiosPost";
372 }
373 
374 static void pruneDevice(const nlohmann::json& systemConfiguration,
375                         const bool powerOff, const bool scannedPowerOff,
376                         const std::string& name, const nlohmann::json& device)
377 {
378     if (systemConfiguration.contains(name))
379     {
380         return;
381     }
382 
383     if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff))
384     {
385         return;
386     }
387 
388     logDeviceRemoved(device);
389 }
390 
391 void EntityManager::startRemovedTimer(boost::asio::steady_timer& timer,
392                                       nlohmann::json& systemConfiguration)
393 {
394     if (systemConfiguration.empty() || lastJson.empty())
395     {
396         return; // not ready yet
397     }
398     if (scannedPowerOn)
399     {
400         return;
401     }
402 
403     if (!powerStatus.isPowerOn() && scannedPowerOff)
404     {
405         return;
406     }
407 
408     timer.expires_after(std::chrono::seconds(10));
409     timer.async_wait(
410         [&systemConfiguration, this](const boost::system::error_code& ec) {
411             if (ec == boost::asio::error::operation_aborted)
412             {
413                 return;
414             }
415 
416             bool powerOff = !powerStatus.isPowerOn();
417             for (const auto& [name, device] : lastJson.items())
418             {
419                 pruneDevice(systemConfiguration, powerOff, scannedPowerOff,
420                             name, device);
421             }
422 
423             scannedPowerOff = true;
424             if (!powerOff)
425             {
426                 scannedPowerOn = true;
427             }
428         });
429 }
430 
431 void EntityManager::pruneConfiguration(bool powerOff, const std::string& name,
432                                        const nlohmann::json& device)
433 {
434     if (powerOff && deviceRequiresPowerOn(device))
435     {
436         // power not on yet, don't know if it's there or not
437         return;
438     }
439 
440     auto& ifaces = dbus_interface.getDeviceInterfaces(device);
441     for (auto& iface : ifaces)
442     {
443         auto sharedPtr = iface.lock();
444         if (!!sharedPtr)
445         {
446             objServer.remove_interface(sharedPtr);
447         }
448     }
449 
450     ifaces.clear();
451     systemConfiguration.erase(name);
452     topology.remove(device["Name"].get<std::string>());
453     logDeviceRemoved(device);
454 }
455 
456 void EntityManager::publishNewConfiguration(
457     const size_t& instance, const size_t count,
458     boost::asio::steady_timer& timer, // Gerrit discussion:
459     // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6
460     //
461     // Discord discussion:
462     // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854
463     //
464     // NOLINTNEXTLINE(performance-unnecessary-value-param)
465     const nlohmann::json newConfiguration)
466 {
467     loadOverlays(newConfiguration, io);
468 
469     boost::asio::post(io, [this]() {
470         if (!writeJsonFiles(systemConfiguration))
471         {
472             lg2::error("Error writing json files");
473         }
474     });
475 
476     boost::asio::post(io, [this, &instance, count, &timer, newConfiguration]() {
477         postToDbus(newConfiguration);
478         if (count == instance)
479         {
480             startRemovedTimer(timer, systemConfiguration);
481         }
482     });
483 }
484 
485 // main properties changed entry
486 void EntityManager::propertiesChangedCallback()
487 {
488     propertiesChangedInstance++;
489     size_t count = propertiesChangedInstance;
490 
491     propertiesChangedTimer.expires_after(std::chrono::milliseconds(500));
492 
493     // setup an async wait as we normally get flooded with new requests
494     propertiesChangedTimer.async_wait(
495         [this, count](const boost::system::error_code& ec) {
496             if (ec == boost::asio::error::operation_aborted)
497             {
498                 // we were cancelled
499                 return;
500             }
501             if (ec)
502             {
503                 lg2::error("async wait error {ERR}", "ERR", ec.message());
504                 return;
505             }
506 
507             if (propertiesChangedInProgress)
508             {
509                 propertiesChangedCallback();
510                 return;
511             }
512             propertiesChangedInProgress = true;
513 
514             nlohmann::json oldConfiguration = systemConfiguration;
515             auto missingConfigurations = std::make_shared<nlohmann::json>();
516             *missingConfigurations = systemConfiguration;
517 
518             auto perfScan = std::make_shared<scan::PerformScan>(
519                 *this, *missingConfigurations, configuration.configurations, io,
520                 [this, count, oldConfiguration, missingConfigurations]() {
521                     // this is something that since ac has been applied to the
522                     // bmc we saw, and we no longer see it
523                     bool powerOff = !powerStatus.isPowerOn();
524                     for (const auto& [name, device] :
525                          missingConfigurations->items())
526                     {
527                         pruneConfiguration(powerOff, name, device);
528                     }
529                     nlohmann::json newConfiguration = systemConfiguration;
530 
531                     deriveNewConfiguration(oldConfiguration, newConfiguration);
532 
533                     for (const auto& [_, device] : newConfiguration.items())
534                     {
535                         logDeviceAdded(device);
536                     }
537 
538                     propertiesChangedInProgress = false;
539 
540                     boost::asio::post(io, [this, newConfiguration, count] {
541                         publishNewConfiguration(
542                             std::ref(propertiesChangedInstance), count,
543                             std::ref(propertiesChangedTimer), newConfiguration);
544                     });
545                 });
546             perfScan->run();
547         });
548 }
549 
550 // Check if InterfacesAdded payload contains an iface that needs probing.
551 static bool iaContainsProbeInterface(
552     sdbusplus::message_t& msg,
553     const std::unordered_set<std::string>& probeInterfaces)
554 {
555     sdbusplus::message::object_path path;
556     DBusObject interfaces;
557     msg.read(path, interfaces);
558     return std::ranges::any_of(interfaces | std::views::keys,
559                                [&probeInterfaces](const auto& ifaceName) {
560                                    return probeInterfaces.contains(ifaceName);
561                                });
562 }
563 
564 // Check if InterfacesRemoved payload contains an iface that needs probing.
565 static bool irContainsProbeInterface(
566     sdbusplus::message_t& msg,
567     const std::unordered_set<std::string>& probeInterfaces)
568 {
569     sdbusplus::message::object_path path;
570     std::vector<std::string> interfaces;
571     msg.read(path, interfaces);
572     return std::ranges::any_of(interfaces,
573                                [&probeInterfaces](const auto& ifaceName) {
574                                    return probeInterfaces.contains(ifaceName);
575                                });
576 }
577 
578 void EntityManager::handleCurrentConfigurationJson()
579 {
580     if (EM_CACHE_CONFIGURATION && em_utils::fwVersionIsSame())
581     {
582         if (std::filesystem::is_regular_file(currentConfiguration))
583         {
584             // this file could just be deleted, but it's nice for debug
585             std::filesystem::create_directory(tempConfigDir);
586             std::filesystem::remove(lastConfiguration);
587             std::filesystem::copy(currentConfiguration, lastConfiguration);
588             std::filesystem::remove(currentConfiguration);
589 
590             std::ifstream jsonStream(lastConfiguration);
591             if (jsonStream.good())
592             {
593                 auto data = nlohmann::json::parse(jsonStream, nullptr, false);
594                 if (data.is_discarded())
595                 {
596                     lg2::error("syntax error in {PATH}", "PATH",
597                                lastConfiguration);
598                 }
599                 else
600                 {
601                     lastJson = std::move(data);
602                 }
603             }
604             else
605             {
606                 lg2::error("unable to open {PATH}", "PATH", lastConfiguration);
607             }
608         }
609     }
610     else
611     {
612         // not an error, just logging at this level to make it in the journal
613         std::error_code ec;
614         lg2::error("Clearing previous configuration");
615         std::filesystem::remove(currentConfiguration, ec);
616     }
617 }
618 
619 void EntityManager::registerCallback(const std::string& path)
620 {
621     if (dbusMatches.contains(path))
622     {
623         return;
624     }
625 
626     lg2::debug("creating PropertiesChanged match on {PATH}", "PATH", path);
627 
628     std::function<void(sdbusplus::message_t & message)> eventHandler =
629         [&](sdbusplus::message_t&) { propertiesChangedCallback(); };
630 
631     sdbusplus::bus::match_t match(
632         static_cast<sdbusplus::bus_t&>(*systemBus),
633         "type='signal',member='PropertiesChanged',path='" + path + "'",
634         eventHandler);
635     dbusMatches.emplace(path, std::move(match));
636 }
637 
638 // We need a poke from DBus for static providers that create all their
639 // objects prior to claiming a well-known name, and thus don't emit any
640 // org.freedesktop.DBus.Properties signals.  Similarly if a process exits
641 // for any reason, expected or otherwise, we'll need a poke to remove
642 // entities from DBus.
643 void EntityManager::initFilters(
644     const std::unordered_set<std::string>& probeInterfaces)
645 {
646     nameOwnerChangedMatch = std::make_unique<sdbusplus::bus::match_t>(
647         static_cast<sdbusplus::bus_t&>(*systemBus),
648         sdbusplus::bus::match::rules::nameOwnerChanged(),
649         [this](sdbusplus::message_t& m) {
650             auto [name, oldOwner,
651                   newOwner] = m.unpack<std::string, std::string, std::string>();
652 
653             if (name.starts_with(':'))
654             {
655                 // We should do nothing with unique-name connections.
656                 return;
657             }
658 
659             propertiesChangedCallback();
660         });
661 
662     // We also need a poke from DBus when new interfaces are created or
663     // destroyed.
664     interfacesAddedMatch = std::make_unique<sdbusplus::bus::match_t>(
665         static_cast<sdbusplus::bus_t&>(*systemBus),
666         sdbusplus::bus::match::rules::interfacesAdded(),
667         [this, probeInterfaces](sdbusplus::message_t& msg) {
668             if (iaContainsProbeInterface(msg, probeInterfaces))
669             {
670                 propertiesChangedCallback();
671             }
672         });
673 
674     interfacesRemovedMatch = std::make_unique<sdbusplus::bus::match_t>(
675         static_cast<sdbusplus::bus_t&>(*systemBus),
676         sdbusplus::bus::match::rules::interfacesRemoved(),
677         [this, probeInterfaces](sdbusplus::message_t& msg) {
678             if (irContainsProbeInterface(msg, probeInterfaces))
679             {
680                 propertiesChangedCallback();
681             }
682         });
683 }
684