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