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