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