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