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