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