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