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