1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright 2018 Intel Corporation 3 4 #include "perform_scan.hpp" 5 6 #include "perform_probe.hpp" 7 #include "utils.hpp" 8 9 #include <boost/asio/steady_timer.hpp> 10 #include <phosphor-logging/lg2.hpp> 11 12 #include <charconv> 13 #include <flat_map> 14 #include <flat_set> 15 16 using GetSubTreeType = std::vector< 17 std::pair<std::string, 18 std::vector<std::pair<std::string, std::vector<std::string>>>>>; 19 20 constexpr const int32_t maxMapperDepth = 0; 21 22 struct DBusInterfaceInstance 23 { 24 std::string busName; 25 std::string path; 26 std::string interface; 27 }; 28 29 void getInterfaces( 30 const DBusInterfaceInstance& instance, 31 const std::vector<std::shared_ptr<probe::PerformProbe>>& probeVector, 32 const std::shared_ptr<scan::PerformScan>& scan, boost::asio::io_context& io, 33 size_t retries = 5) 34 { 35 if (retries == 0U) 36 { 37 lg2::error("retries exhausted on {BUSNAME} {PATH} {INTF}", "BUSNAME", 38 instance.busName, "PATH", instance.path, "INTF", 39 instance.interface); 40 return; 41 } 42 43 scan->_em.systemBus->async_method_call( 44 [instance, scan, probeVector, retries, 45 &io](boost::system::error_code& errc, 46 const DBusInterface& resp) mutable { 47 if (errc) 48 { 49 lg2::error("error calling getall on {BUSNAME} {PATH} {INTF}", 50 "BUSNAME", instance.busName, "PATH", instance.path, 51 "INTF", instance.interface); 52 53 auto timer = std::make_shared<boost::asio::steady_timer>(io); 54 timer->expires_after(std::chrono::seconds(2)); 55 56 timer->async_wait([timer, instance, scan, probeVector, retries, 57 &io](const boost::system::error_code&) { 58 getInterfaces(instance, probeVector, scan, io, retries - 1); 59 }); 60 return; 61 } 62 63 scan->dbusProbeObjects[std::string(instance.path)] 64 [std::string(instance.interface)] = resp; 65 }, 66 instance.busName, instance.path, "org.freedesktop.DBus.Properties", 67 "GetAll", instance.interface); 68 } 69 70 static void processDbusObjects( 71 std::vector<std::shared_ptr<probe::PerformProbe>>& probeVector, 72 const std::shared_ptr<scan::PerformScan>& scan, 73 const GetSubTreeType& interfaceSubtree, boost::asio::io_context& io) 74 { 75 for (const auto& [path, object] : interfaceSubtree) 76 { 77 // Get a PropertiesChanged callback for all interfaces on this path. 78 scan->_em.registerCallback(path); 79 80 for (const auto& [busname, ifaces] : object) 81 { 82 for (const std::string& iface : ifaces) 83 { 84 // The 3 default org.freedeskstop interfaces (Peer, 85 // Introspectable, and Properties) are returned by 86 // the mapper but don't have properties, so don't bother 87 // with the GetAll call to save some cycles. 88 if (!iface.starts_with("org.freedesktop")) 89 { 90 getInterfaces({busname, path, iface}, probeVector, scan, 91 io); 92 } 93 } 94 } 95 } 96 } 97 98 // Populates scan->dbusProbeObjects with all interfaces and properties 99 // for the paths that own the interfaces passed in. 100 void findDbusObjects( 101 std::vector<std::shared_ptr<probe::PerformProbe>>&& probeVector, 102 std::flat_set<std::string, std::less<>>&& interfaces, 103 const std::shared_ptr<scan::PerformScan>& scan, boost::asio::io_context& io, 104 size_t retries = 5) 105 { 106 // Filter out interfaces already obtained. 107 for (const auto& [path, probeInterfaces] : scan->dbusProbeObjects) 108 { 109 for (const auto& [interface, _] : probeInterfaces) 110 { 111 interfaces.erase(interface); 112 } 113 } 114 if (interfaces.empty()) 115 { 116 return; 117 } 118 119 // find all connections in the mapper that expose a specific type 120 scan->_em.systemBus->async_method_call( 121 [interfaces, probeVector{std::move(probeVector)}, scan, retries, 122 &io](boost::system::error_code& ec, 123 const GetSubTreeType& interfaceSubtree) mutable { 124 if (ec) 125 { 126 if (ec.value() == ENOENT) 127 { 128 return; // wasn't found by mapper 129 } 130 lg2::error("Error communicating to mapper."); 131 132 if (retries == 0U) 133 { 134 // if we can't communicate to the mapper something is very 135 // wrong 136 std::exit(EXIT_FAILURE); 137 } 138 139 auto timer = std::make_shared<boost::asio::steady_timer>(io); 140 timer->expires_after(std::chrono::seconds(10)); 141 142 timer->async_wait( 143 [timer, interfaces{std::move(interfaces)}, scan, 144 probeVector{std::move(probeVector)}, retries, 145 &io](const boost::system::error_code&) mutable { 146 findDbusObjects(std::move(probeVector), 147 std::move(interfaces), scan, io, 148 retries - 1); 149 }); 150 return; 151 } 152 153 processDbusObjects(probeVector, scan, interfaceSubtree, io); 154 }, 155 "xyz.openbmc_project.ObjectMapper", 156 "/xyz/openbmc_project/object_mapper", 157 "xyz.openbmc_project.ObjectMapper", "GetSubTree", "/", maxMapperDepth, 158 interfaces); 159 } 160 161 static std::string getRecordName(const DBusInterface& probe, 162 const std::string& probeName) 163 { 164 if (probe.empty()) 165 { 166 return probeName; 167 } 168 169 // use an array so alphabetical order from the flat_map is maintained 170 auto device = nlohmann::json::array(); 171 for (const auto& devPair : probe) 172 { 173 device.push_back(devPair.first); 174 std::visit([&device](auto&& v) { device.push_back(v); }, 175 devPair.second); 176 } 177 178 // hashes are hard to distinguish, use the non-hashed version if we want 179 // debug 180 // return probeName + device.dump(); 181 182 return std::to_string(std::hash<std::string>{}(probeName + device.dump())); 183 } 184 185 scan::PerformScan::PerformScan( 186 EntityManager& em, nlohmann::json& missingConfigurations, 187 std::vector<nlohmann::json>& configurations, boost::asio::io_context& io, 188 std::function<void()>&& callback) : 189 _em(em), _missingConfigurations(missingConfigurations), 190 _configurations(configurations), _callback(std::move(callback)), io(io) 191 {} 192 193 static void pruneRecordExposes(nlohmann::json& record) 194 { 195 auto findExposes = record.find("Exposes"); 196 if (findExposes == record.end()) 197 { 198 return; 199 } 200 201 auto copy = nlohmann::json::array(); 202 for (auto& expose : *findExposes) 203 { 204 if (!expose.is_null()) 205 { 206 copy.emplace_back(expose); 207 } 208 } 209 *findExposes = copy; 210 } 211 212 static void recordDiscoveredIdentifiers( 213 std::set<nlohmann::json>& usedNames, std::list<size_t>& indexes, 214 const std::string& probeName, const nlohmann::json& record) 215 { 216 size_t indexIdx = probeName.find('$'); 217 if (indexIdx == std::string::npos) 218 { 219 return; 220 } 221 222 auto nameIt = record.find("Name"); 223 if (nameIt == record.end()) 224 { 225 lg2::error("Last JSON Illegal"); 226 return; 227 } 228 229 int index = 0; 230 auto str = nameIt->get<std::string>().substr(indexIdx); 231 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) 232 const char* endPtr = str.data() + str.size(); 233 auto [p, ec] = std::from_chars(str.data(), endPtr, index); 234 if (ec != std::errc()) 235 { 236 return; // non-numeric replacement 237 } 238 239 usedNames.insert(nameIt.value()); 240 241 auto usedIt = std::find(indexes.begin(), indexes.end(), index); 242 if (usedIt != indexes.end()) 243 { 244 indexes.erase(usedIt); 245 } 246 } 247 248 static bool extractExposeActionRecordNames(std::vector<std::string>& matches, 249 const std::string& exposeKey, 250 nlohmann::json& exposeValue) 251 { 252 const std::string* exposeValueStr = 253 exposeValue.get_ptr<const std::string*>(); 254 if (exposeValueStr != nullptr) 255 { 256 matches.emplace_back(*exposeValueStr); 257 return true; 258 } 259 260 const nlohmann::json::array_t* exarr = 261 exposeValue.get_ptr<const nlohmann::json::array_t*>(); 262 if (exarr != nullptr) 263 { 264 for (const auto& value : *exarr) 265 { 266 if (!value.is_string()) 267 { 268 lg2::error("Value is invalid type {VALUE}", "VALUE", value); 269 break; 270 } 271 matches.emplace_back(value); 272 } 273 274 return true; 275 } 276 277 lg2::error("Value is invalid type {KEY}", "KEY", exposeKey); 278 return false; 279 } 280 281 static std::optional<std::vector<std::string>::iterator> findExposeActionRecord( 282 std::vector<std::string>& matches, const nlohmann::json& record) 283 { 284 const auto& name = (record)["Name"].get_ref<const std::string&>(); 285 auto compare = [&name](const std::string& s) { return s == name; }; 286 auto matchIt = std::find_if(matches.begin(), matches.end(), compare); 287 288 if (matchIt == matches.end()) 289 { 290 return std::nullopt; 291 } 292 293 return matchIt; 294 } 295 296 static void applyBindExposeAction(nlohmann::json::object_t& exposedObject, 297 nlohmann::json::object_t& expose, 298 const std::string& propertyName) 299 { 300 if (propertyName.starts_with("Bind")) 301 { 302 std::string bind = propertyName.substr(sizeof("Bind") - 1); 303 exposedObject["Status"] = "okay"; 304 expose[bind] = exposedObject; 305 } 306 } 307 308 static void applyDisableExposeAction(nlohmann::json::object_t& exposedObject, 309 const std::string& propertyName) 310 { 311 if (propertyName == "DisableNode") 312 { 313 exposedObject["Status"] = "disabled"; 314 } 315 } 316 317 static void applyConfigExposeActions( 318 std::vector<std::string>& matches, nlohmann::json::object_t& expose, 319 const std::string& propertyName, nlohmann::json::array_t& configExposes) 320 { 321 for (auto& exposedObject : configExposes) 322 { 323 auto match = findExposeActionRecord(matches, exposedObject); 324 if (match) 325 { 326 matches.erase(*match); 327 nlohmann::json::object_t* exposedObjectObj = 328 exposedObject.get_ptr<nlohmann::json::object_t*>(); 329 if (exposedObjectObj == nullptr) 330 { 331 lg2::error("Exposed object wasn't a object: {JSON}", "JSON", 332 exposedObject.dump()); 333 continue; 334 } 335 336 applyBindExposeAction(*exposedObjectObj, expose, propertyName); 337 applyDisableExposeAction(*exposedObjectObj, propertyName); 338 } 339 } 340 } 341 342 static void applyExposeActions( 343 nlohmann::json& systemConfiguration, const std::string& recordName, 344 nlohmann::json::object_t& expose, const std::string& exposeKey, 345 nlohmann::json& exposeValue) 346 { 347 bool isBind = exposeKey.starts_with("Bind"); 348 bool isDisable = exposeKey == "DisableNode"; 349 bool isExposeAction = isBind || isDisable; 350 351 if (!isExposeAction) 352 { 353 return; 354 } 355 356 std::vector<std::string> matches; 357 358 if (!extractExposeActionRecordNames(matches, exposeKey, exposeValue)) 359 { 360 return; 361 } 362 363 for (const auto& [configId, config] : systemConfiguration.items()) 364 { 365 // don't disable ourselves 366 if (isDisable && configId == recordName) 367 { 368 continue; 369 } 370 371 auto configListFind = config.find("Exposes"); 372 if (configListFind == config.end()) 373 { 374 continue; 375 } 376 377 nlohmann::json::array_t* configList = 378 configListFind->get_ptr<nlohmann::json::array_t*>(); 379 if (configList == nullptr) 380 { 381 continue; 382 } 383 applyConfigExposeActions(matches, expose, exposeKey, *configList); 384 } 385 386 if (!matches.empty()) 387 { 388 lg2::error( 389 "configuration file dependency error, could not find {KEY} {VALUE}", 390 "KEY", exposeKey, "VALUE", exposeValue); 391 } 392 } 393 394 static std::string generateDeviceName( 395 const std::set<nlohmann::json>& usedNames, const DBusObject& dbusObject, 396 size_t foundDeviceIdx, const std::string& nameTemplate, 397 std::optional<std::string>& replaceStr) 398 { 399 nlohmann::json copyForName = nameTemplate; 400 std::optional<std::string> replaceVal = em_utils::templateCharReplace( 401 copyForName, dbusObject, foundDeviceIdx, replaceStr); 402 403 if (!replaceStr && replaceVal) 404 { 405 if (usedNames.contains(nameTemplate)) 406 { 407 replaceStr = replaceVal; 408 em_utils::templateCharReplace(copyForName, dbusObject, 409 foundDeviceIdx, replaceStr); 410 } 411 } 412 413 if (replaceStr) 414 { 415 lg2::error( 416 "Duplicates found, replacing {STR} with found device index. Consider fixing template to not have duplicates", 417 "STR", *replaceStr); 418 } 419 const std::string* ret = copyForName.get_ptr<const std::string*>(); 420 if (ret == nullptr) 421 { 422 lg2::error("Device name wasn't a string: ${JSON}", "JSON", 423 copyForName.dump()); 424 return ""; 425 } 426 return *ret; 427 } 428 static void applyTemplateAndExposeActions( 429 const std::string& recordName, const DBusObject& dbusObject, 430 size_t foundDeviceIdx, const std::optional<std::string>& replaceStr, 431 nlohmann::json& value, nlohmann::json& systemConfiguration) 432 { 433 nlohmann::json::object_t* exposeObj = 434 value.get_ptr<nlohmann::json::object_t*>(); 435 if (exposeObj != nullptr) 436 { 437 return; 438 } 439 for (auto& [key, value] : *exposeObj) 440 { 441 em_utils::templateCharReplace(value, dbusObject, foundDeviceIdx, 442 replaceStr); 443 444 applyExposeActions(systemConfiguration, recordName, *exposeObj, key, 445 value); 446 } 447 }; 448 449 void scan::PerformScan::updateSystemConfiguration( 450 const nlohmann::json& recordRef, const std::string& probeName, 451 FoundDevices& foundDevices) 452 { 453 _passed = true; 454 passedProbes.push_back(probeName); 455 456 std::set<nlohmann::json> usedNames; 457 std::list<size_t> indexes(foundDevices.size()); 458 std::iota(indexes.begin(), indexes.end(), 1); 459 460 // copy over persisted configurations and make sure we remove 461 // indexes that are already used 462 for (auto itr = foundDevices.begin(); itr != foundDevices.end();) 463 { 464 std::string recordName = getRecordName(itr->interface, probeName); 465 466 auto record = _em.systemConfiguration.find(recordName); 467 if (record == _em.systemConfiguration.end()) 468 { 469 record = _em.lastJson.find(recordName); 470 if (record == _em.lastJson.end()) 471 { 472 itr++; 473 continue; 474 } 475 476 pruneRecordExposes(*record); 477 478 _em.systemConfiguration[recordName] = *record; 479 } 480 _missingConfigurations.erase(recordName); 481 482 // We've processed the device, remove it and advance the 483 // iterator 484 itr = foundDevices.erase(itr); 485 recordDiscoveredIdentifiers(usedNames, indexes, probeName, *record); 486 } 487 488 std::optional<std::string> replaceStr; 489 490 DBusObject emptyObject; 491 DBusInterface emptyInterface; 492 emptyObject.emplace(std::string{}, emptyInterface); 493 494 for (const auto& [foundDevice, path] : foundDevices) 495 { 496 // Need all interfaces on this path so that template 497 // substitutions can be done with any of the contained 498 // properties. If the probe that passed didn't use an 499 // interface, such as if it was just TRUE, then 500 // templateCharReplace will just get passed in an empty 501 // map. 502 auto objectIt = dbusProbeObjects.find(path); 503 const DBusObject& dbusObject = (objectIt == dbusProbeObjects.end()) 504 ? emptyObject 505 : objectIt->second; 506 507 const nlohmann::json::object_t* recordPtr = 508 recordRef.get_ptr<const nlohmann::json::object_t*>(); 509 if (recordPtr == nullptr) 510 { 511 lg2::error("Failed to parse record {JSON}", "JSON", 512 recordRef.dump()); 513 continue; 514 } 515 nlohmann::json::object_t record = *recordPtr; 516 std::string recordName = getRecordName(foundDevice, probeName); 517 size_t foundDeviceIdx = indexes.front(); 518 indexes.pop_front(); 519 520 // check name first so we have no duplicate names 521 auto getName = record.find("Name"); 522 if (getName == record.end()) 523 { 524 lg2::error("Record Missing Name! {JSON}", "JSON", recordRef.dump()); 525 continue; // this should be impossible at this level 526 } 527 528 const std::string* name = getName->second.get_ptr<const std::string*>(); 529 if (name == nullptr) 530 { 531 lg2::error("Name wasn't a string: {JSON}", "JSON", 532 recordRef.dump()); 533 continue; 534 } 535 536 std::string deviceName = generateDeviceName( 537 usedNames, dbusObject, foundDeviceIdx, *name, replaceStr); 538 539 usedNames.insert(deviceName); 540 541 for (auto& keyPair : record) 542 { 543 if (keyPair.first != "Name") 544 { 545 // "Probe" string does not contain template variables 546 // Handle left-over variables for "Exposes" later below 547 const bool handleLeftOver = 548 (keyPair.first != "Probe") && (keyPair.first != "Exposes"); 549 em_utils::templateCharReplace(keyPair.second, dbusObject, 550 foundDeviceIdx, replaceStr, 551 handleLeftOver); 552 } 553 } 554 555 // insert into configuration temporarily to be able to 556 // reference ourselves 557 558 _em.systemConfiguration[recordName] = record; 559 560 auto findExpose = record.find("Exposes"); 561 if (findExpose == record.end()) 562 { 563 continue; 564 } 565 566 nlohmann::json::array_t* exposeArr = 567 findExpose->second.get_ptr<nlohmann::json::array_t*>(); 568 if (exposeArr != nullptr) 569 { 570 for (auto& value : *exposeArr) 571 { 572 applyTemplateAndExposeActions(recordName, dbusObject, 573 foundDeviceIdx, replaceStr, value, 574 _em.systemConfiguration); 575 } 576 } 577 else 578 { 579 applyTemplateAndExposeActions( 580 recordName, dbusObject, foundDeviceIdx, replaceStr, 581 findExpose->second, _em.systemConfiguration); 582 } 583 584 // If we end up here and the path is empty, we have Probe: "True" 585 // and we dont want that to show up in the associations. 586 if (!path.empty()) 587 { 588 auto boardType = record.find("Type")->second.get<std::string>(); 589 auto boardName = record.find("Name")->second.get<std::string>(); 590 std::string boardInventoryPath = 591 em_utils::buildInventorySystemPath(boardName, boardType); 592 _em.topology.addProbePath(boardInventoryPath, path); 593 } 594 595 // overwrite ourselves with cleaned up version 596 _em.systemConfiguration[recordName] = record; 597 _missingConfigurations.erase(recordName); 598 } 599 } 600 601 void scan::PerformScan::run() 602 { 603 std::flat_set<std::string, std::less<>> dbusProbeInterfaces; 604 std::vector<std::shared_ptr<probe::PerformProbe>> dbusProbePointers; 605 606 for (auto it = _configurations.begin(); it != _configurations.end();) 607 { 608 // check for poorly formatted fields, probe must be an array 609 auto findProbe = it->find("Probe"); 610 if (findProbe == it->end()) 611 { 612 lg2::error("configuration file missing probe:\n {JSON}", "JSON", 613 *it); 614 it = _configurations.erase(it); 615 continue; 616 } 617 618 auto findName = it->find("Name"); 619 if (findName == it->end()) 620 { 621 lg2::error("configuration file missing name:\n {JSON}", "JSON", 622 *it); 623 it = _configurations.erase(it); 624 continue; 625 } 626 627 const std::string* probeName = findName->get_ptr<const std::string*>(); 628 if (probeName == nullptr) 629 { 630 lg2::error("Name wasn't a string? {JSON}", "JSON", *it); 631 it = _configurations.erase(it); 632 continue; 633 } 634 635 if (std::find(passedProbes.begin(), passedProbes.end(), *probeName) != 636 passedProbes.end()) 637 { 638 it = _configurations.erase(it); 639 continue; 640 } 641 642 nlohmann::json& recordRef = *it; 643 std::vector<std::string> probeCommand; 644 nlohmann::json::array_t* probeCommandArrayPtr = 645 findProbe->get_ptr<nlohmann::json::array_t*>(); 646 if (probeCommandArrayPtr != nullptr) 647 { 648 for (const auto& probe : *probeCommandArrayPtr) 649 { 650 const std::string* probeStr = 651 probe.get_ptr<const std::string*>(); 652 if (probeStr == nullptr) 653 { 654 lg2::error("Probe statement wasn't a string, can't parse"); 655 return; 656 } 657 probeCommand.push_back(*probeStr); 658 } 659 } 660 else 661 { 662 const std::string* probeStr = 663 findProbe->get_ptr<const std::string*>(); 664 if (probeStr == nullptr) 665 { 666 lg2::error("Probe statement wasn't a string, can't parse"); 667 return; 668 } 669 probeCommand.push_back(*probeStr); 670 } 671 672 // store reference to this to children to makes sure we don't get 673 // destroyed too early 674 auto thisRef = shared_from_this(); 675 auto probePointer = std::make_shared<probe::PerformProbe>( 676 recordRef, probeCommand, *probeName, thisRef); 677 678 // parse out dbus probes by discarding other probe types, store in a 679 // map 680 for (const std::string& probe : probeCommand) 681 { 682 if (probe::findProbeType(probe)) 683 { 684 continue; 685 } 686 // syntax requires probe before first open brace 687 auto findStart = probe.find('('); 688 std::string interface = probe.substr(0, findStart); 689 dbusProbeInterfaces.emplace(interface); 690 dbusProbePointers.emplace_back(probePointer); 691 } 692 it++; 693 } 694 695 // probe vector stores a shared_ptr to each PerformProbe that cares 696 // about a dbus interface 697 findDbusObjects(std::move(dbusProbePointers), 698 std::move(dbusProbeInterfaces), shared_from_this(), io); 699 } 700 701 scan::PerformScan::~PerformScan() 702 { 703 if (_passed) 704 { 705 auto nextScan = std::make_shared<PerformScan>( 706 _em, _missingConfigurations, _configurations, io, 707 std::move(_callback)); 708 nextScan->passedProbes = std::move(passedProbes); 709 nextScan->dbusProbeObjects = std::move(dbusProbeObjects); 710 boost::asio::post(_em.io, [nextScan]() { nextScan->run(); }); 711 } 712 else 713 { 714 _callback(); 715 } 716 } 717