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