/* // Copyright (c) 2018 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. */ /// \file perform_scan.cpp #include "entity_manager.hpp" #include #include #include #include #include /* Hacks from splitting entity_manager.cpp */ // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) extern std::shared_ptr systemBus; extern nlohmann::json lastJson; extern void propertiesChangedCallback(nlohmann::json& systemConfiguration, sdbusplus::asio::object_server& objServer); // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) using GetSubTreeType = std::vector< std::pair>>>>; constexpr const int32_t maxMapperDepth = 0; constexpr const bool debug = false; struct DBusInterfaceInstance { std::string busName; std::string path; std::string interface; }; void getInterfaces( const DBusInterfaceInstance& instance, const std::vector>& probeVector, const std::shared_ptr& scan, size_t retries = 5) { if (retries == 0U) { std::cerr << "retries exhausted on " << instance.busName << " " << instance.path << " " << instance.interface << "\n"; return; } systemBus->async_method_call( [instance, scan, probeVector, retries](boost::system::error_code& errc, const DBusInterface& resp) { if (errc) { std::cerr << "error calling getall on " << instance.busName << " " << instance.path << " " << instance.interface << "\n"; auto timer = std::make_shared(io); timer->expires_after(std::chrono::seconds(2)); timer->async_wait([timer, instance, scan, probeVector, retries](const boost::system::error_code&) { getInterfaces(instance, probeVector, scan, retries - 1); }); return; } scan->dbusProbeObjects[instance.path][instance.interface] = resp; }, instance.busName, instance.path, "org.freedesktop.DBus.Properties", "GetAll", instance.interface); if constexpr (debug) { std::cerr << __LINE__ << "\n"; } } static void registerCallback(nlohmann::json& systemConfiguration, sdbusplus::asio::object_server& objServer, const std::string& path) { static boost::container::flat_map dbusMatches; auto find = dbusMatches.find(path); if (find != dbusMatches.end()) { return; } std::function eventHandler = [&](sdbusplus::message_t&) { propertiesChangedCallback(systemConfiguration, objServer); }; sdbusplus::bus::match_t match( static_cast(*systemBus), "type='signal',member='PropertiesChanged',path='" + path + "'", eventHandler); dbusMatches.emplace(path, std::move(match)); } static void processDbusObjects(std::vector>& probeVector, const std::shared_ptr& scan, const GetSubTreeType& interfaceSubtree) { for (const auto& [path, object] : interfaceSubtree) { // Get a PropertiesChanged callback for all interfaces on this path. registerCallback(scan->_systemConfiguration, scan->objServer, path); for (const auto& [busname, ifaces] : object) { for (const std::string& iface : ifaces) { // The 3 default org.freedeskstop interfaces (Peer, // Introspectable, and Properties) are returned by // the mapper but don't have properties, so don't bother // with the GetAll call to save some cycles. if (!boost::algorithm::starts_with(iface, "org.freedesktop")) { getInterfaces({busname, path, iface}, probeVector, scan); } } } } } // Populates scan->dbusProbeObjects with all interfaces and properties // for the paths that own the interfaces passed in. void findDbusObjects(std::vector>&& probeVector, boost::container::flat_set&& interfaces, const std::shared_ptr& scan, size_t retries = 5) { // Filter out interfaces already obtained. for (const auto& [path, probeInterfaces] : scan->dbusProbeObjects) { for (const auto& [interface, _] : probeInterfaces) { interfaces.erase(interface); } } if (interfaces.empty()) { return; } // find all connections in the mapper that expose a specific type systemBus->async_method_call( [interfaces, probeVector{std::move(probeVector)}, scan, retries](boost::system::error_code& ec, const GetSubTreeType& interfaceSubtree) mutable { if (ec) { if (ec.value() == ENOENT) { return; // wasn't found by mapper } std::cerr << "Error communicating to mapper.\n"; if (retries == 0U) { // if we can't communicate to the mapper something is very // wrong std::exit(EXIT_FAILURE); } auto timer = std::make_shared(io); timer->expires_after(std::chrono::seconds(10)); timer->async_wait([timer, interfaces{std::move(interfaces)}, scan, probeVector{std::move(probeVector)}, retries]( const boost::system::error_code&) mutable { findDbusObjects(std::move(probeVector), std::move(interfaces), scan, retries - 1); }); return; } processDbusObjects(probeVector, scan, interfaceSubtree); }, "xyz.openbmc_project.ObjectMapper", "/xyz/openbmc_project/object_mapper", "xyz.openbmc_project.ObjectMapper", "GetSubTree", "/", maxMapperDepth, interfaces); if constexpr (debug) { std::cerr << __LINE__ << "\n"; } } static std::string getRecordName(const DBusInterface& probe, const std::string& probeName) { if (probe.empty()) { return probeName; } // use an array so alphabetical order from the flat_map is maintained auto device = nlohmann::json::array(); for (const auto& devPair : probe) { device.push_back(devPair.first); std::visit([&device](auto&& v) { device.push_back(v); }, devPair.second); } // hashes are hard to distinguish, use the non-hashed version if we want // debug if constexpr (debug) { return probeName + device.dump(); } return std::to_string(std::hash{}(probeName + device.dump())); } PerformScan::PerformScan(nlohmann::json& systemConfiguration, nlohmann::json& missingConfigurations, std::list& configurations, sdbusplus::asio::object_server& objServerIn, std::function&& callback) : _systemConfiguration(systemConfiguration), _missingConfigurations(missingConfigurations), _configurations(configurations), objServer(objServerIn), _callback(std::move(callback)) {} static void pruneRecordExposes(nlohmann::json& record) { auto findExposes = record.find("Exposes"); if (findExposes == record.end()) { return; } auto copy = nlohmann::json::array(); for (auto& expose : *findExposes) { if (!expose.is_null()) { copy.emplace_back(expose); } } *findExposes = copy; } static void recordDiscoveredIdentifiers(std::set& usedNames, std::list& indexes, const std::string& probeName, const nlohmann::json& record) { size_t indexIdx = probeName.find('$'); if (indexIdx == std::string::npos) { return; } auto nameIt = record.find("Name"); if (nameIt == record.end()) { std::cerr << "Last JSON Illegal\n"; return; } int index = 0; auto str = nameIt->get().substr(indexIdx); // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) const char* endPtr = str.data() + str.size(); auto [p, ec] = std::from_chars(str.data(), endPtr, index); if (ec != std::errc()) { return; // non-numeric replacement } usedNames.insert(nameIt.value()); auto usedIt = std::find(indexes.begin(), indexes.end(), index); if (usedIt != indexes.end()) { indexes.erase(usedIt); } } static bool extractExposeActionRecordNames(std::vector& matches, nlohmann::json::iterator& keyPair) { if (keyPair.value().is_string()) { matches.emplace_back(keyPair.value()); return true; } if (keyPair.value().is_array()) { for (const auto& value : keyPair.value()) { if (!value.is_string()) { std::cerr << "Value is invalid type " << value << "\n"; break; } matches.emplace_back(value); } return true; } std::cerr << "Value is invalid type " << keyPair.key() << "\n"; return false; } static std::optional::iterator> findExposeActionRecord(std::vector& matches, const nlohmann::json& record) { const auto& name = (record)["Name"].get_ref(); auto compare = [&name](const std::string& s) { return s == name; }; auto matchIt = std::find_if(matches.begin(), matches.end(), compare); if (matchIt == matches.end()) { return std::nullopt; } return matchIt; } static void applyBindExposeAction(nlohmann::json& exposedObject, nlohmann::json& expose, const std::string& propertyName) { if (boost::starts_with(propertyName, "Bind")) { std::string bind = propertyName.substr(sizeof("Bind") - 1); exposedObject["Status"] = "okay"; expose[bind] = exposedObject; } } static void applyDisableExposeAction(nlohmann::json& exposedObject, const std::string& propertyName) { if (propertyName == "DisableNode") { exposedObject["Status"] = "disabled"; } } static void applyConfigExposeActions(std::vector& matches, nlohmann::json& expose, const std::string& propertyName, nlohmann::json& configExposes) { for (auto& exposedObject : configExposes) { auto match = findExposeActionRecord(matches, exposedObject); if (match) { matches.erase(*match); applyBindExposeAction(exposedObject, expose, propertyName); applyDisableExposeAction(exposedObject, propertyName); } } } static void applyExposeActions(nlohmann::json& systemConfiguration, const std::string& recordName, nlohmann::json& expose, nlohmann::json::iterator& keyPair) { bool isBind = boost::starts_with(keyPair.key(), "Bind"); bool isDisable = keyPair.key() == "DisableNode"; bool isExposeAction = isBind || isDisable; if (!isExposeAction) { return; } std::vector matches; if (!extractExposeActionRecordNames(matches, keyPair)) { return; } for (const auto& [configId, config] : systemConfiguration.items()) { // don't disable ourselves if (isDisable && configId == recordName) { continue; } auto configListFind = config.find("Exposes"); if (configListFind == config.end()) { continue; } if (!configListFind->is_array()) { continue; } applyConfigExposeActions(matches, expose, keyPair.key(), *configListFind); } if (!matches.empty()) { std::cerr << "configuration file dependency error, could not find " << keyPair.key() << " " << keyPair.value() << "\n"; } } static std::string generateDeviceName(const std::set& usedNames, const DBusObject& dbusObject, size_t foundDeviceIdx, const std::string& nameTemplate, std::optional& replaceStr) { nlohmann::json copyForName = {{"Name", nameTemplate}}; nlohmann::json::iterator copyIt = copyForName.begin(); std::optional replaceVal = templateCharReplace(copyIt, dbusObject, foundDeviceIdx, replaceStr); if (!replaceStr && replaceVal) { if (usedNames.find(copyIt.value()) != usedNames.end()) { replaceStr = replaceVal; copyForName = {{"Name", nameTemplate}}; copyIt = copyForName.begin(); templateCharReplace(copyIt, dbusObject, foundDeviceIdx, replaceStr); } } if (replaceStr) { std::cerr << "Duplicates found, replacing " << *replaceStr << " with found device index.\n Consider " "fixing template to not have duplicates\n"; } return copyIt.value(); } void PerformScan::updateSystemConfiguration(const nlohmann::json& recordRef, const std::string& probeName, FoundDevices& foundDevices) { _passed = true; passedProbes.push_back(probeName); std::set usedNames; std::list indexes(foundDevices.size()); std::iota(indexes.begin(), indexes.end(), 1); // copy over persisted configurations and make sure we remove // indexes that are already used for (auto itr = foundDevices.begin(); itr != foundDevices.end();) { std::string recordName = getRecordName(itr->interface, probeName); auto record = _systemConfiguration.find(recordName); if (record == _systemConfiguration.end()) { record = lastJson.find(recordName); if (record == lastJson.end()) { itr++; continue; } pruneRecordExposes(*record); _systemConfiguration[recordName] = *record; } _missingConfigurations.erase(recordName); // We've processed the device, remove it and advance the // iterator itr = foundDevices.erase(itr); recordDiscoveredIdentifiers(usedNames, indexes, probeName, *record); } std::optional replaceStr; DBusObject emptyObject; DBusInterface emptyInterface; emptyObject.emplace(std::string{}, emptyInterface); for (const auto& [foundDevice, path] : foundDevices) { // Need all interfaces on this path so that template // substitutions can be done with any of the contained // properties. If the probe that passed didn't use an // interface, such as if it was just TRUE, then // templateCharReplace will just get passed in an empty // map. auto objectIt = dbusProbeObjects.find(path); const DBusObject& dbusObject = (objectIt == dbusProbeObjects.end()) ? emptyObject : objectIt->second; nlohmann::json record = recordRef; std::string recordName = getRecordName(foundDevice, probeName); size_t foundDeviceIdx = indexes.front(); indexes.pop_front(); // check name first so we have no duplicate names auto getName = record.find("Name"); if (getName == record.end()) { std::cerr << "Record Missing Name! " << record.dump(); continue; // this should be impossible at this level } std::string deviceName = generateDeviceName( usedNames, dbusObject, foundDeviceIdx, getName.value(), replaceStr); getName.value() = deviceName; usedNames.insert(deviceName); for (auto keyPair = record.begin(); keyPair != record.end(); keyPair++) { if (keyPair.key() != "Name") { templateCharReplace(keyPair, dbusObject, foundDeviceIdx, replaceStr); } } // insert into configuration temporarily to be able to // reference ourselves _systemConfiguration[recordName] = record; auto findExpose = record.find("Exposes"); if (findExpose == record.end()) { continue; } for (auto& expose : *findExpose) { for (auto keyPair = expose.begin(); keyPair != expose.end(); keyPair++) { templateCharReplace(keyPair, dbusObject, foundDeviceIdx, replaceStr); applyExposeActions(_systemConfiguration, recordName, expose, keyPair); } } // overwrite ourselves with cleaned up version _systemConfiguration[recordName] = record; _missingConfigurations.erase(recordName); } } void PerformScan::run() { boost::container::flat_set dbusProbeInterfaces; std::vector> dbusProbePointers; for (auto it = _configurations.begin(); it != _configurations.end();) { // check for poorly formatted fields, probe must be an array auto findProbe = it->find("Probe"); if (findProbe == it->end()) { std::cerr << "configuration file missing probe:\n " << *it << "\n"; it = _configurations.erase(it); continue; } auto findName = it->find("Name"); if (findName == it->end()) { std::cerr << "configuration file missing name:\n " << *it << "\n"; it = _configurations.erase(it); continue; } std::string probeName = *findName; if (std::find(passedProbes.begin(), passedProbes.end(), probeName) != passedProbes.end()) { it = _configurations.erase(it); continue; } nlohmann::json& recordRef = *it; nlohmann::json probeCommand; if ((*findProbe).type() != nlohmann::json::value_t::array) { probeCommand = nlohmann::json::array(); probeCommand.push_back(*findProbe); } else { probeCommand = *findProbe; } // store reference to this to children to makes sure we don't get // destroyed too early auto thisRef = shared_from_this(); auto probePointer = std::make_shared( recordRef, probeCommand, probeName, thisRef); // parse out dbus probes by discarding other probe types, store in a // map for (const nlohmann::json& probeJson : probeCommand) { const std::string* probe = probeJson.get_ptr(); if (probe == nullptr) { std::cerr << "Probe statement wasn't a string, can't parse"; continue; } if (findProbeType(*probe)) { continue; } // syntax requires probe before first open brace auto findStart = probe->find('('); std::string interface = probe->substr(0, findStart); dbusProbeInterfaces.emplace(interface); dbusProbePointers.emplace_back(probePointer); } it++; } // probe vector stores a shared_ptr to each PerformProbe that cares // about a dbus interface findDbusObjects(std::move(dbusProbePointers), std::move(dbusProbeInterfaces), shared_from_this()); if constexpr (debug) { std::cerr << __LINE__ << "\n"; } } PerformScan::~PerformScan() { if (_passed) { auto nextScan = std::make_shared( _systemConfiguration, _missingConfigurations, _configurations, objServer, std::move(_callback)); nextScan->passedProbes = std::move(passedProbes); nextScan->dbusProbeObjects = std::move(dbusProbeObjects); nextScan->run(); if constexpr (debug) { std::cerr << __LINE__ << "\n"; } } else { _callback(); if constexpr (debug) { std::cerr << __LINE__ << "\n"; } } }