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