1 /* 2 // Copyright (c) 2018 Intel Corporation 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 */ 16 /// \file entity_manager.cpp 17 18 #include "entity_manager.hpp" 19 20 #include "../utils.hpp" 21 #include "../variant_visitors.hpp" 22 #include "configuration.hpp" 23 #include "dbus_interface.hpp" 24 #include "log_device_inventory.hpp" 25 #include "overlay.hpp" 26 #include "perform_scan.hpp" 27 #include "phosphor-logging/lg2.hpp" 28 #include "topology.hpp" 29 #include "utils.hpp" 30 31 #include <boost/algorithm/string/case_conv.hpp> 32 #include <boost/algorithm/string/classification.hpp> 33 #include <boost/algorithm/string/predicate.hpp> 34 #include <boost/algorithm/string/replace.hpp> 35 #include <boost/algorithm/string/split.hpp> 36 #include <boost/asio/io_context.hpp> 37 #include <boost/asio/post.hpp> 38 #include <boost/asio/steady_timer.hpp> 39 #include <boost/container/flat_map.hpp> 40 #include <boost/container/flat_set.hpp> 41 #include <boost/range/iterator_range.hpp> 42 #include <nlohmann/json.hpp> 43 #include <sdbusplus/asio/connection.hpp> 44 #include <sdbusplus/asio/object_server.hpp> 45 46 #include <filesystem> 47 #include <fstream> 48 #include <functional> 49 #include <iostream> 50 #include <map> 51 #include <regex> 52 constexpr const char* tempConfigDir = "/tmp/configuration/"; 53 constexpr const char* lastConfiguration = "/tmp/configuration/last.json"; 54 55 static constexpr std::array<const char*, 6> settableInterfaces = { 56 "FanProfile", "Pid", "Pid.Zone", "Stepwise", "Thresholds", "Polling"}; 57 58 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]"); 59 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]"); 60 61 sdbusplus::asio::PropertyPermission getPermission(const std::string& interface) 62 { 63 return std::find(settableInterfaces.begin(), settableInterfaces.end(), 64 interface) != settableInterfaces.end() 65 ? sdbusplus::asio::PropertyPermission::readWrite 66 : sdbusplus::asio::PropertyPermission::readOnly; 67 } 68 69 EntityManager::EntityManager( 70 std::shared_ptr<sdbusplus::asio::connection>& systemBus, 71 boost::asio::io_context& io) : 72 systemBus(systemBus), 73 objServer(sdbusplus::asio::object_server(systemBus, /*skipManager=*/true)), 74 lastJson(nlohmann::json::object()), 75 systemConfiguration(nlohmann::json::object()), io(io), 76 powerStatus(*systemBus), propertiesChangedTimer(io) 77 { 78 // All other objects that EntityManager currently support are under the 79 // inventory subtree. 80 // See the discussion at 81 // https://discord.com/channels/775381525260664832/1018929092009144380 82 objServer.add_manager("/xyz/openbmc_project/inventory"); 83 84 entityIface = objServer.add_interface("/xyz/openbmc_project/EntityManager", 85 "xyz.openbmc_project.EntityManager"); 86 entityIface->register_method("ReScan", [this]() { 87 propertiesChangedCallback(); 88 }); 89 dbus_interface::tryIfaceInitialize(entityIface); 90 91 initFilters(configuration.probeInterfaces); 92 } 93 94 void EntityManager::postToDbus(const nlohmann::json& newConfiguration) 95 { 96 std::map<std::string, std::string> newBoards; // path -> name 97 98 // iterate through boards 99 for (const auto& [boardId, boardConfig] : newConfiguration.items()) 100 { 101 std::string boardName = boardConfig["Name"]; 102 std::string boardNameOrig = boardConfig["Name"]; 103 std::string jsonPointerPath = "/" + boardId; 104 // loop through newConfiguration, but use values from system 105 // configuration to be able to modify via dbus later 106 auto boardValues = systemConfiguration[boardId]; 107 auto findBoardType = boardValues.find("Type"); 108 std::string boardType; 109 if (findBoardType != boardValues.end() && 110 findBoardType->type() == nlohmann::json::value_t::string) 111 { 112 boardType = findBoardType->get<std::string>(); 113 std::regex_replace(boardType.begin(), boardType.begin(), 114 boardType.end(), illegalDbusMemberRegex, "_"); 115 } 116 else 117 { 118 std::cerr << "Unable to find type for " << boardName 119 << " reverting to Chassis.\n"; 120 boardType = "Chassis"; 121 } 122 std::string boardtypeLower = boost::algorithm::to_lower_copy(boardType); 123 124 std::regex_replace(boardName.begin(), boardName.begin(), 125 boardName.end(), illegalDbusMemberRegex, "_"); 126 std::string boardPath = "/xyz/openbmc_project/inventory/system/"; 127 boardPath += boardtypeLower; 128 boardPath += "/"; 129 boardPath += boardName; 130 131 std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface = 132 dbus_interface.createInterface(objServer, boardPath, 133 "xyz.openbmc_project.Inventory.Item", 134 boardName); 135 136 std::shared_ptr<sdbusplus::asio::dbus_interface> boardIface = 137 dbus_interface.createInterface( 138 objServer, boardPath, 139 "xyz.openbmc_project.Inventory.Item." + boardType, 140 boardNameOrig); 141 142 dbus_interface.createAddObjectMethod( 143 io, jsonPointerPath, boardPath, systemConfiguration, objServer, 144 boardNameOrig); 145 146 dbus_interface::populateInterfaceFromJson( 147 io, systemConfiguration, jsonPointerPath, boardIface, boardValues, 148 objServer); 149 jsonPointerPath += "/"; 150 // iterate through board properties 151 for (const auto& [propName, propValue] : boardValues.items()) 152 { 153 if (propValue.type() == nlohmann::json::value_t::object) 154 { 155 std::shared_ptr<sdbusplus::asio::dbus_interface> iface = 156 dbus_interface.createInterface(objServer, boardPath, 157 propName, boardNameOrig); 158 159 dbus_interface::populateInterfaceFromJson( 160 io, systemConfiguration, jsonPointerPath + propName, iface, 161 propValue, objServer); 162 } 163 } 164 165 auto exposes = boardValues.find("Exposes"); 166 if (exposes == boardValues.end()) 167 { 168 continue; 169 } 170 // iterate through exposes 171 jsonPointerPath += "Exposes/"; 172 173 // store the board level pointer so we can modify it on the way down 174 std::string jsonPointerPathBoard = jsonPointerPath; 175 size_t exposesIndex = -1; 176 for (auto& item : *exposes) 177 { 178 exposesIndex++; 179 jsonPointerPath = jsonPointerPathBoard; 180 jsonPointerPath += std::to_string(exposesIndex); 181 182 auto findName = item.find("Name"); 183 if (findName == item.end()) 184 { 185 std::cerr << "cannot find name in field " << item << "\n"; 186 continue; 187 } 188 auto findStatus = item.find("Status"); 189 // if status is not found it is assumed to be status = 'okay' 190 if (findStatus != item.end()) 191 { 192 if (*findStatus == "disabled") 193 { 194 continue; 195 } 196 } 197 auto findType = item.find("Type"); 198 std::string itemType; 199 if (findType != item.end()) 200 { 201 itemType = findType->get<std::string>(); 202 std::regex_replace(itemType.begin(), itemType.begin(), 203 itemType.end(), illegalDbusPathRegex, "_"); 204 } 205 else 206 { 207 itemType = "unknown"; 208 } 209 std::string itemName = findName->get<std::string>(); 210 std::regex_replace(itemName.begin(), itemName.begin(), 211 itemName.end(), illegalDbusMemberRegex, "_"); 212 std::string ifacePath = boardPath; 213 ifacePath += "/"; 214 ifacePath += itemName; 215 216 if (itemType == "BMC") 217 { 218 std::shared_ptr<sdbusplus::asio::dbus_interface> bmcIface = 219 dbus_interface.createInterface( 220 objServer, ifacePath, 221 "xyz.openbmc_project.Inventory.Item.Bmc", 222 boardNameOrig); 223 dbus_interface::populateInterfaceFromJson( 224 io, systemConfiguration, jsonPointerPath, bmcIface, item, 225 objServer, getPermission(itemType)); 226 } 227 else if (itemType == "System") 228 { 229 std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface = 230 dbus_interface.createInterface( 231 objServer, ifacePath, 232 "xyz.openbmc_project.Inventory.Item.System", 233 boardNameOrig); 234 dbus_interface::populateInterfaceFromJson( 235 io, systemConfiguration, jsonPointerPath, systemIface, item, 236 objServer, getPermission(itemType)); 237 } 238 239 for (const auto& [name, config] : item.items()) 240 { 241 jsonPointerPath = jsonPointerPathBoard; 242 jsonPointerPath.append(std::to_string(exposesIndex)) 243 .append("/") 244 .append(name); 245 if (config.type() == nlohmann::json::value_t::object) 246 { 247 std::string ifaceName = 248 "xyz.openbmc_project.Configuration."; 249 ifaceName.append(itemType).append(".").append(name); 250 251 std::shared_ptr<sdbusplus::asio::dbus_interface> 252 objectIface = dbus_interface.createInterface( 253 objServer, ifacePath, ifaceName, boardNameOrig); 254 255 dbus_interface::populateInterfaceFromJson( 256 io, systemConfiguration, jsonPointerPath, objectIface, 257 config, objServer, getPermission(name)); 258 } 259 else if (config.type() == nlohmann::json::value_t::array) 260 { 261 size_t index = 0; 262 if (config.empty()) 263 { 264 continue; 265 } 266 bool isLegal = true; 267 auto type = config[0].type(); 268 if (type != nlohmann::json::value_t::object) 269 { 270 continue; 271 } 272 273 // verify legal json 274 for (const auto& arrayItem : config) 275 { 276 if (arrayItem.type() != type) 277 { 278 isLegal = false; 279 break; 280 } 281 } 282 if (!isLegal) 283 { 284 std::cerr << "dbus format error" << config << "\n"; 285 break; 286 } 287 288 for (auto& arrayItem : config) 289 { 290 std::string ifaceName = 291 "xyz.openbmc_project.Configuration."; 292 ifaceName.append(itemType).append(".").append(name); 293 ifaceName.append(std::to_string(index)); 294 295 std::shared_ptr<sdbusplus::asio::dbus_interface> 296 objectIface = dbus_interface.createInterface( 297 objServer, ifacePath, ifaceName, boardNameOrig); 298 299 dbus_interface::populateInterfaceFromJson( 300 io, systemConfiguration, 301 jsonPointerPath + "/" + std::to_string(index), 302 objectIface, arrayItem, objServer, 303 getPermission(name)); 304 index++; 305 } 306 } 307 } 308 309 std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface = 310 dbus_interface.createInterface( 311 objServer, ifacePath, 312 "xyz.openbmc_project.Configuration." + itemType, 313 boardNameOrig); 314 315 dbus_interface::populateInterfaceFromJson( 316 io, systemConfiguration, jsonPointerPath, itemIface, item, 317 objServer, getPermission(itemType)); 318 319 topology.addBoard(boardPath, boardType, boardNameOrig, item); 320 } 321 322 newBoards.emplace(boardPath, boardNameOrig); 323 } 324 325 for (const auto& [assocPath, assocPropValue] : 326 topology.getAssocs(newBoards)) 327 { 328 auto findBoard = newBoards.find(assocPath); 329 if (findBoard == newBoards.end()) 330 { 331 continue; 332 } 333 334 auto ifacePtr = dbus_interface.createInterface( 335 objServer, assocPath, "xyz.openbmc_project.Association.Definitions", 336 findBoard->second); 337 338 ifacePtr->register_property("Associations", assocPropValue); 339 dbus_interface::tryIfaceInitialize(ifacePtr); 340 } 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 std::cerr << "Error writing json files\n"; 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 std::cerr << "async wait error " << ec << "\n"; 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 std::set<std::string> interfaceSet; 544 std::set<std::string> intersect; 545 546 msg.read(path, interfaces); 547 548 std::for_each(interfaces.begin(), interfaces.end(), 549 [&interfaceSet](const auto& iface) { 550 interfaceSet.insert(iface.first); 551 }); 552 553 std::set_intersection(interfaceSet.begin(), interfaceSet.end(), 554 probeInterfaces.begin(), probeInterfaces.end(), 555 std::inserter(intersect, intersect.end())); 556 return !intersect.empty(); 557 } 558 559 // Check if InterfacesRemoved payload contains an iface that needs probing. 560 static bool irContainsProbeInterface( 561 sdbusplus::message_t& msg, 562 const std::unordered_set<std::string>& probeInterfaces) 563 { 564 sdbusplus::message::object_path path; 565 std::set<std::string> interfaces; 566 std::set<std::string> intersect; 567 568 msg.read(path, interfaces); 569 570 std::set_intersection(interfaces.begin(), interfaces.end(), 571 probeInterfaces.begin(), probeInterfaces.end(), 572 std::inserter(intersect, intersect.end())); 573 return !intersect.empty(); 574 } 575 576 void EntityManager::handleCurrentConfigurationJson() 577 { 578 if (em_utils::fwVersionIsSame()) 579 { 580 if (std::filesystem::is_regular_file(currentConfiguration)) 581 { 582 // this file could just be deleted, but it's nice for debug 583 std::filesystem::create_directory(tempConfigDir); 584 std::filesystem::remove(lastConfiguration); 585 std::filesystem::copy(currentConfiguration, lastConfiguration); 586 std::filesystem::remove(currentConfiguration); 587 588 std::ifstream jsonStream(lastConfiguration); 589 if (jsonStream.good()) 590 { 591 auto data = nlohmann::json::parse(jsonStream, nullptr, false); 592 if (data.is_discarded()) 593 { 594 std::cerr 595 << "syntax error in " << lastConfiguration << "\n"; 596 } 597 else 598 { 599 lastJson = std::move(data); 600 } 601 } 602 else 603 { 604 std::cerr << "unable to open " << lastConfiguration << "\n"; 605 } 606 } 607 } 608 else 609 { 610 // not an error, just logging at this level to make it in the journal 611 std::cerr << "Clearing previous configuration\n"; 612 std::filesystem::remove(currentConfiguration); 613 } 614 } 615 616 void EntityManager::registerCallback(const std::string& path) 617 { 618 if (dbusMatches.contains(path)) 619 { 620 return; 621 } 622 623 lg2::debug("creating PropertiesChanged match on {PATH}", "PATH", path); 624 625 std::function<void(sdbusplus::message_t & message)> eventHandler = 626 [&](sdbusplus::message_t&) { propertiesChangedCallback(); }; 627 628 sdbusplus::bus::match_t match( 629 static_cast<sdbusplus::bus_t&>(*systemBus), 630 "type='signal',member='PropertiesChanged',path='" + path + "'", 631 eventHandler); 632 dbusMatches.emplace(path, std::move(match)); 633 } 634 635 // We need a poke from DBus for static providers that create all their 636 // objects prior to claiming a well-known name, and thus don't emit any 637 // org.freedesktop.DBus.Properties signals. Similarly if a process exits 638 // for any reason, expected or otherwise, we'll need a poke to remove 639 // entities from DBus. 640 void EntityManager::initFilters( 641 const std::unordered_set<std::string>& probeInterfaces) 642 { 643 nameOwnerChangedMatch = std::make_unique<sdbusplus::bus::match_t>( 644 static_cast<sdbusplus::bus_t&>(*systemBus), 645 sdbusplus::bus::match::rules::nameOwnerChanged(), 646 [this](sdbusplus::message_t& m) { 647 auto [name, oldOwner, 648 newOwner] = m.unpack<std::string, std::string, std::string>(); 649 650 if (name.starts_with(':')) 651 { 652 // We should do nothing with unique-name connections. 653 return; 654 } 655 656 propertiesChangedCallback(); 657 }); 658 659 // We also need a poke from DBus when new interfaces are created or 660 // destroyed. 661 interfacesAddedMatch = std::make_unique<sdbusplus::bus::match_t>( 662 static_cast<sdbusplus::bus_t&>(*systemBus), 663 sdbusplus::bus::match::rules::interfacesAdded(), 664 [this, probeInterfaces](sdbusplus::message_t& msg) { 665 if (iaContainsProbeInterface(msg, probeInterfaces)) 666 { 667 propertiesChangedCallback(); 668 } 669 }); 670 671 interfacesRemovedMatch = std::make_unique<sdbusplus::bus::match_t>( 672 static_cast<sdbusplus::bus_t&>(*systemBus), 673 sdbusplus::bus::match::rules::interfacesRemoved(), 674 [this, probeInterfaces](sdbusplus::message_t& msg) { 675 if (irContainsProbeInterface(msg, probeInterfaces)) 676 { 677 propertiesChangedCallback(); 678 } 679 }); 680 } 681