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