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