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