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