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