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