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