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