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