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