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