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