// SPDX-License-Identifier: Apache-2.0 /**@file functions.cpp*/ #include "config.h" #include "functions.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace functions { namespace process_hostfirmware { using namespace phosphor::logging; using InterfacesPropertiesMap = std::map>>>; using ManagedObjectType = std::map; /** * @brief Returns the managed objects for a given service */ ManagedObjectType getManagedObjects(sdbusplus::bus_t& bus, const std::string& service, const std::string& managerPath) { auto method = bus.new_method_call(service.c_str(), managerPath.c_str(), "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); ManagedObjectType objects; try { auto reply = bus.call(method); reply.read(objects); } catch (const sdbusplus::exception_t& e) { return ManagedObjectType{}; } return objects; } /** * @brief Issue callbacks safely * * std::function can be empty, so this wrapper method checks for that prior to * calling it to avoid std::bad_function_call * * @tparam Sig the types of the std::function arguments * @tparam Args the deduced argument types * @param[in] callback the callback being wrapped * @param[in] args the callback arguments */ template void makeCallback(const std::function& callback, Args&&... args) { if (callback) { callback(std::forward(args)...); } } /** * @brief Get file extensions for Compatible * * IBM host firmware can be deployed as blobs (files) in a filesystem. Host * firmware blobs for different values of * xyz.openbmc_project.Inventory.Decorator.Compatible are packaged with * different filename extensions. getExtensionsForIbmCompatibleSystem maintains * the mapping from a given value of * xyz.openbmc_project.Inventory.Decorator.Compatible to an array of filename * extensions. * * If a mapping is found getExtensionsForIbmCompatibleSystem returns true and * the extensions parameter is reset with the map entry. If no mapping is found * getExtensionsForIbmCompatibleSystem returns false and extensions is * unmodified. * * @param[in] extensionMap a map of * xyz.openbmc_project.Inventory.Decorator.Compatible to host firmware blob file * extensions. * @param[in] ibmCompatibleSystem The names property of an instance of * xyz.openbmc_project.Inventory.Decorator.Compatible * @param[out] extensions the host firmware blob file extensions * @return true if an entry was found, otherwise false */ bool getExtensionsForIbmCompatibleSystem( const std::map>& extensionMap, const std::vector& ibmCompatibleSystem, std::vector& extensions) { for (const auto& system : ibmCompatibleSystem) { auto extensionMapIterator = extensionMap.find(system); if (extensionMapIterator != extensionMap.end()) { extensions = extensionMapIterator->second; return true; } } return false; } /** * @brief Write host firmware well-known name * * A wrapper around std::filesystem::create_symlink that avoids EEXIST by * deleting any pre-existing file. * * @param[in] linkTarget The link target argument to * std::filesystem::create_symlink * @param[in] linkPath The link path argument to std::filesystem::create_symlink * @param[in] errorCallback A callback made in the event of filesystem errors. */ void writeLink(const std::filesystem::path& linkTarget, const std::filesystem::path& linkPath, const ErrorCallbackType& errorCallback) { std::error_code ec; // remove files with the same name as the symlink to be created, // otherwise symlink will fail with EEXIST. if (!std::filesystem::remove(linkPath, ec)) { if (ec) { makeCallback(errorCallback, linkPath, ec); return; } } std::filesystem::create_symlink(linkTarget, linkPath, ec); if (ec) { makeCallback(errorCallback, linkPath, ec); return; } } /** * @brief Find host firmware blob files that need well-known names * * The IBM host firmware runtime looks for data and/or additional code while * bootstrapping in files with well-known names. findLinks uses the provided * extensions argument to find host firmware blob files that require a * well-known name. When a blob is found, issue the provided callback * (typically a function that will write a symlink). * * @param[in] hostFirmwareDirectory The directory in which findLinks should * look for host firmware blob files that need well-known names. * @param[in] extensions The extensions of the firmware blob files denote a * host firmware blob file requires a well-known name. * @param[in] errorCallback A callback made in the event of filesystem errors. * @param[in] linkCallback A callback made when host firmware blob files * needing a well known name are found. */ void findLinks(const std::filesystem::path& hostFirmwareDirectory, const std::vector& extensions, const ErrorCallbackType& errorCallback, const LinkCallbackType& linkCallback) { std::error_code ec; std::filesystem::directory_iterator directoryIterator(hostFirmwareDirectory, ec); if (ec) { makeCallback(errorCallback, hostFirmwareDirectory, ec); return; } // Create a symlink for pnor.toc static const auto tocLid = "81e00994.lid"; auto tocLidPath = hostFirmwareDirectory / tocLid; if (std::filesystem::exists(tocLidPath)) { static const auto tocName = "pnor.toc"; auto tocLinkPath = hostFirmwareDirectory / tocName; makeCallback(linkCallback, tocLid, tocLinkPath, errorCallback); } for (; directoryIterator != std::filesystem::end(directoryIterator); directoryIterator.increment(ec)) { const auto& file = directoryIterator->path(); if (ec) { makeCallback(errorCallback, file, ec); // quit here if the increment call failed otherwise the loop may // never finish break; } if (std::find(extensions.begin(), extensions.end(), file.extension()) == extensions.end()) { // this file doesn't have an extension or doesn't match any of the // provided extensions. continue; } auto linkPath(file.parent_path().append( static_cast(file.stem()))); makeCallback(linkCallback, file.filename(), linkPath, errorCallback); } } /** * @brief Parse the elements json file and construct a string with the data to * be used to update the bios attribute table. * * @param[in] elementsJsonFilePath - The path to the host firmware json file. * @param[in] extensions - The extensions of the firmware blob files. */ std::string getBiosAttrStr(const std::filesystem::path& elementsJsonFilePath, const std::vector& extensions) { std::string biosAttrStr{}; std::ifstream jsonFile(elementsJsonFilePath.c_str()); if (!jsonFile) { return {}; } std::map attr; auto data = nlohmann::json::parse(jsonFile, nullptr, false); if (data.is_discarded()) { log("Error parsing JSON file", entry("FILE=%s", elementsJsonFilePath.c_str())); return {}; } // .get requires a non-const iterator for (auto& iter : data["lids"]) { std::string name{}; std::string lid{}; try { name = iter["element_name"].get(); lid = iter["short_lid_name"].get(); } catch (const std::exception& e) { // Possibly the element or lid name field was not found log("Error reading JSON field", entry("FILE=%s", elementsJsonFilePath.c_str()), entry("ERROR=%s", e.what())); continue; } // The elements with the ipl extension have higher priority. Therefore // Use operator[] to overwrite value if an entry for it already exists, // and create a second entry with key name element_RT to specify it as // a runtime element. // Ex: if the JSON contains an entry A.P10 with lid name X, it'll create // and try A=X. If the JSON also contained an entry A.P10.iplTime with // lid name Y, the A entry would be overwritten to be A=Y and a second // entry A_RT=X would be created. constexpr auto iplExtension = ".iplTime"; constexpr auto runtimeSuffix = "_RT"; std::filesystem::path path(name); if (path.extension() == iplExtension) { // Some elements have an additional extension, ex: .P10.iplTime // Strip off the ipl extension with stem(), then check if there is // an additional extension with extension(). if (!path.stem().extension().empty()) { // Check if the extension matches the extensions for this system if (std::find(extensions.begin(), extensions.end(), path.stem().extension()) == extensions.end()) { continue; } } // Get the element name without extensions by calling stem() twice // since stem() returns the base name if no periods are found. // Therefore both "element.P10" and "element.P10.iplTime" would // become "element". auto keyName = path.stem().stem(); auto attrIt = attr.find(keyName); if (attrIt != attr.end()) { // Copy the existing entry to a runtime entry auto runtimeKeyName = keyName.string() + runtimeSuffix; attr.insert({runtimeKeyName, attrIt->second}); } // Overwrite the existing element with the ipl entry attr[keyName] = lid; continue; } // Process all other extensions. The extension should match the list of // supported extensions for this system. Use .insert() to only add // entries that do not exist, so to not overwrite the values that may // had been added that had the ipl extension. if (std::find(extensions.begin(), extensions.end(), path.extension()) != extensions.end()) { auto keyName = path.stem(); auto attrIt = attr.find(keyName); if (attrIt != attr.end()) { // The existing entry is an ipl entry, therefore create this // entry as a runtime one. auto runtimeKeyName = keyName.string() + runtimeSuffix; attr.insert({runtimeKeyName, lid}); } else { attr.insert({path.stem(), lid}); } } } for (const auto& a : attr) { // Build the bios attribute string with format: // "element1=lid1,element2=lid2,elementN=lidN," biosAttrStr += a.first + "=" + a.second + ","; // Create symlinks from the hostfw elements to their corresponding // lid files if they don't exist auto elementFilePath = std::filesystem::path("/media/hostfw/running") / a.first; if (!std::filesystem::exists(elementFilePath)) { std::error_code ec; auto lidName = a.second + ".lid"; std::filesystem::create_symlink(lidName, elementFilePath, ec); if (ec) { log("Error creating symlink", entry("TARGET=%s", lidName.c_str()), entry("LINK=%s", elementFilePath.c_str())); } } } // Delete the last comma of the bios attribute string if (biosAttrStr.back() == ',') { return biosAttrStr.substr(0, biosAttrStr.length() - 1); } return biosAttrStr; } /** * @brief Set the bios attribute table with details of the host firmware data * for this system. * * @param[in] elementsJsonFilePath - The path to the host firmware json file. * @param[in] extensions - The extensions of the firmware blob files. */ void setBiosAttr(const std::filesystem::path& elementsJsonFilePath, const std::vector& extensions) { auto biosAttrStr = getBiosAttrStr(elementsJsonFilePath, extensions); constexpr auto biosConfigPath = "/xyz/openbmc_project/bios_config/manager"; constexpr auto biosConfigIntf = "xyz.openbmc_project.BIOSConfig.Manager"; constexpr auto dbusAttrName = "hb_lid_ids"; constexpr auto dbusAttrType = "xyz.openbmc_project.BIOSConfig.Manager.AttributeType.String"; using PendingAttributesType = std::vector>>>; PendingAttributesType pendingAttributes; pendingAttributes.emplace_back(std::make_pair( dbusAttrName, std::make_tuple(dbusAttrType, biosAttrStr))); auto bus = sdbusplus::bus::new_default(); auto method = bus.new_method_call(MAPPER_BUSNAME, MAPPER_PATH, MAPPER_INTERFACE, "GetObject"); method.append(biosConfigPath, std::vector({biosConfigIntf})); std::vector>> response; try { auto reply = bus.call(method); reply.read(response); if (response.empty()) { log("Error reading mapper response", entry("PATH=%s", biosConfigPath), entry("INTERFACE=%s", biosConfigIntf)); throw sdbusplus::xyz::openbmc_project::Common::Error:: InternalFailure(); } auto method = bus.new_method_call((response.begin()->first).c_str(), biosConfigPath, SYSTEMD_PROPERTY_INTERFACE, "Set"); method.append(biosConfigIntf, "PendingAttributes", std::variant(pendingAttributes)); bus.call(method); } catch (const sdbusplus::exception_t& e) { log("Error setting the bios attribute", entry("ERROR=%s", e.what()), entry("ATTRIBUTE=%s", dbusAttrName)); throw; } } /** * @brief Make callbacks on * xyz.openbmc_project.Inventory.Decorator.Compatible instances. * * Look for an instance of xyz.openbmc_project.Inventory.Decorator.Compatible in * the provided argument and if found, issue the provided callback. * * @param[in] interfacesAndProperties the interfaces in which to look for an * instance of xyz.openbmc_project.Inventory.Decorator.Compatible * @param[in] callback the user callback to make if * xyz.openbmc_project.Inventory.Decorator.Compatible is found in * interfacesAndProperties * @return true if interfacesAndProperties contained an instance of * xyz.openbmc_project.Inventory.Decorator.Compatible, false otherwise */ bool maybeCall(const std::map>>>& interfacesAndProperties, const MaybeCallCallbackType& callback) { using namespace std::string_literals; static const auto interfaceName = "xyz.openbmc_project.Inventory.Decorator.Compatible"s; auto interfaceIterator = interfacesAndProperties.find(interfaceName); if (interfaceIterator == interfacesAndProperties.cend()) { // Compatible interface not found, so instruct the caller to keep // waiting or try again later. return false; } auto propertyIterator = interfaceIterator->second.find("Names"s); if (propertyIterator == interfaceIterator->second.cend()) { // The interface exists but the property doesn't. This is a bug in the // Compatible implementation. The caller should not try again. std::cerr << "Names property not implemented on " << interfaceName << "\n"; return true; } const auto& ibmCompatibleSystem = std::get>(propertyIterator->second); if (callback) { try { callback(ibmCompatibleSystem); } catch (const sdbusplus::exception_t& e) { return false; } } // Compatible found and callback issued. return true; } /** * @brief Make callbacks on * xyz.openbmc_project.Inventory.Decorator.Compatible instances. * * Look for an instance ofxyz.openbmc_project.Inventory.Decorator.Compatible in * the provided argument and if found, issue the provided callback. * * @param[in] message the DBus message in which to look for an instance of * xyz.openbmc_project.Inventory.Decorator.Compatible * @param[in] callback the user callback to make if * xyz.openbmc_project.Inventory.Decorator.Compatible is found in message * @return true if message contained an instance of * xyz.openbmc_project.Inventory.Decorator.Compatible, false otherwise */ bool maybeCallMessage(sdbusplus::message_t& message, const MaybeCallCallbackType& callback) { std::map>>> interfacesAndProperties; sdbusplus::message::object_path _; message.read(_, interfacesAndProperties); return maybeCall(interfacesAndProperties, callback); } /** * @brief Determine system support for host firmware well-known names. * * Using the provided extensionMap and * xyz.openbmc_project.Inventory.Decorator.Compatible, determine if well-known * names for host firmware blob files are necessary and if so, create them. * * @param[in] extensionMap a map of * xyz.openbmc_project.Inventory.Decorator.Compatible to host firmware blob file * extensions. * @param[in] hostFirmwareDirectory The directory in which findLinks should look * for host firmware blob files that need well-known names. * @param[in] ibmCompatibleSystem The names property of an instance of * xyz.openbmc_project.Inventory.Decorator.Compatible * @param[in] errorCallback A callback made in the event of filesystem errors. */ void maybeMakeLinks( const std::map>& extensionMap, const std::filesystem::path& hostFirmwareDirectory, const std::vector& ibmCompatibleSystem, const ErrorCallbackType& errorCallback) { std::vector extensions; if (getExtensionsForIbmCompatibleSystem(extensionMap, ibmCompatibleSystem, extensions)) { findLinks(hostFirmwareDirectory, extensions, errorCallback, writeLink); } } /** * @brief Determine system support for updating the bios attribute table. * * Using the provided extensionMap and * xyz.openbmc_project.Inventory.Decorator.Compatible, determine if the bios * attribute table needs to be updated. * * @param[in] extensionMap a map of * xyz.openbmc_project.Inventory.Decorator.Compatible to host firmware blob file * extensions. * @param[in] elementsJsonFilePath The file path to the json file * @param[in] ibmCompatibleSystem The names property of an instance of * xyz.openbmc_project.Inventory.Decorator.Compatible */ void maybeSetBiosAttr( const std::map>& extensionMap, const std::filesystem::path& elementsJsonFilePath, const std::vector& ibmCompatibleSystem) { std::vector extensions; if (getExtensionsForIbmCompatibleSystem(extensionMap, ibmCompatibleSystem, extensions)) { try { setBiosAttr(elementsJsonFilePath, extensions); } catch (const sdbusplus::exception_t& e) { throw; } } } /** * @brief process host firmware * * Allocate a callback context and register for DBus.ObjectManager Interfaces * added signals from entity manager. * * Check the current entity manager object tree for a * xyz.openbmc_project.Inventory.Decorator.Compatible instance (entity manager * will be dbus activated if it is not running). If one is found, determine if * symlinks need to be created and create them. Instruct the program event loop * to exit. * * If no instance of xyz.openbmc_project.Inventory.Decorator.Compatible is found * return the callback context to main, where the program will sleep until the * callback is invoked one or more times and instructs the program event loop to * exit when xyz.openbmc_project.Inventory.Decorator.Compatible is added. * * @param[in] bus a DBus client connection * @param[in] extensionMap a map of * xyz.openbmc_project.Inventory.Decorator.Compatible to host firmware blob file * extensions. * @param[in] hostFirmwareDirectory The directory in which processHostFirmware * should look for blob files. * @param[in] errorCallback A callback made in the event of filesystem errors. * @param[in] loop a program event loop * @return nullptr if an instance of * xyz.openbmc_project.Inventory.Decorator.Compatible is found, otherwise a * pointer to an sdbusplus match object. */ std::shared_ptr processHostFirmware( sdbusplus::bus_t& bus, std::map> extensionMap, std::filesystem::path hostFirmwareDirectory, ErrorCallbackType errorCallback, sdeventplus::Event& loop) { // ownership of extensionMap, hostFirmwareDirectory and errorCallback can't // be transferred to the match callback because they are needed in the non // async part of this function below, so they need to be moved to the heap. auto pExtensionMap = std::make_shared(std::move(extensionMap)); auto pHostFirmwareDirectory = std::make_shared( std::move(hostFirmwareDirectory)); auto pErrorCallback = std::make_shared(std::move(errorCallback)); // register for a callback in case the Compatible interface has not yet been // published by entity manager. auto interfacesAddedMatch = std::make_shared( bus, sdbusplus::bus::match::rules::interfacesAdded() + sdbusplus::bus::match::rules::sender( "xyz.openbmc_project.EntityManager"), [pExtensionMap, pHostFirmwareDirectory, pErrorCallback, &loop](auto& message) { // bind the extension map, host firmware directory, and error // callback to the maybeMakeLinks function. auto maybeMakeLinksWithArgsBound = std::bind(maybeMakeLinks, std::cref(*pExtensionMap), std::cref(*pHostFirmwareDirectory), std::placeholders::_1, std::cref(*pErrorCallback)); // if the InterfacesAdded message contains an an instance of // xyz.openbmc_project.Inventory.Decorator.Compatible, check to see if // links are necessary on this system and if so, create them. if (maybeCallMessage(message, maybeMakeLinksWithArgsBound)) { // The Compatible interface was found and the links were created if // applicable. Instruct the event loop / subcommand to exit. loop.exit(0); } }); // now that we'll get a callback in the event of an InterfacesAdded signal // (potentially containing // xyz.openbmc_project.Inventory.Decorator.Compatible), activate entity // manager if it isn't running and enumerate its objects auto getManagedObjects = bus.new_method_call( "xyz.openbmc_project.EntityManager", "/xyz/openbmc_project/inventory", "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); std::map>>> interfacesAndProperties; std::map objects; try { auto reply = bus.call(getManagedObjects); reply.read(objects); } catch (const sdbusplus::exception_t& e) { // Error querying the EntityManager interface. Return the match to have // the callback run if/when the interface appears in D-Bus. return interfacesAddedMatch; } // bind the extension map, host firmware directory, and error callback to // the maybeMakeLinks function. auto maybeMakeLinksWithArgsBound = std::bind(maybeMakeLinks, std::cref(*pExtensionMap), std::cref(*pHostFirmwareDirectory), std::placeholders::_1, std::cref(*pErrorCallback)); for (const auto& pair : objects) { std::tie(std::ignore, interfacesAndProperties) = pair; // if interfacesAndProperties contains an an instance of // xyz.openbmc_project.Inventory.Decorator.Compatible, check to see if // links are necessary on this system and if so, create them if (maybeCall(interfacesAndProperties, maybeMakeLinksWithArgsBound)) { // The Compatible interface is already on the bus and the links were // created if applicable. Instruct the event loop to exit. loop.exit(0); // The match object isn't needed anymore, so destroy it on return. return nullptr; } } // The Compatible interface has not yet been published. Move ownership of // the match callback to the caller. return interfacesAddedMatch; } /** * @brief Update the Bios Attribute Table * * If an instance of xyz.openbmc_project.Inventory.Decorator.Compatible is * found, update the Bios Attribute Table with the appropriate host firmware * data. * * @param[in] bus - D-Bus client connection. * @param[in] extensionMap - Map of Compatible names and host firmware file extensions. * @param[in] elementsJsonFilePath - The Path to the json file * @param[in] loop - Program event loop. * @return nullptr */ std::vector> updateBiosAttrTable( sdbusplus::bus_t& bus, std::map> extensionMap, std::filesystem::path elementsJsonFilePath, sdeventplus::Event& loop) { constexpr auto pldmPath = "/xyz/openbmc_project/pldm"; constexpr auto entityManagerServiceName = "xyz.openbmc_project.EntityManager"; auto pExtensionMap = std::make_shared(std::move(extensionMap)); auto pElementsJsonFilePath = std::make_shared( std::move(elementsJsonFilePath)); auto maybeSetAttrWithArgsBound = std::bind(maybeSetBiosAttr, std::cref(*pExtensionMap), std::cref(*pElementsJsonFilePath), std::placeholders::_1); std::vector> matches; // Entity Manager is needed to get the list of supported extensions. Add a // match to monitor interfaces added in case it's not running yet. matches.emplace_back(std::make_shared( bus, sdbusplus::bus::match::rules::interfacesAdded() + sdbusplus::bus::match::rules::sender( "xyz.openbmc_project.EntityManager"), [pldmPath, pExtensionMap, pElementsJsonFilePath, maybeSetAttrWithArgsBound, &loop](auto& message) { if (maybeCallMessage(message, maybeSetAttrWithArgsBound)) { loop.exit(0); } })); // The BIOS attribute table can only be updated if PLDM is running because // PLDM is the one that exposes this property. Add a match to monitor when // the PLDM service starts. matches.emplace_back(std::make_shared( bus, sdbusplus::bus::match::rules::nameOwnerChanged() + sdbusplus::bus::match::rules::arg0namespace( "xyz.openbmc_project.PLDM"), [pExtensionMap, pElementsJsonFilePath, maybeSetAttrWithArgsBound, &loop](auto& message) { std::string name; std::string oldOwner; std::string newOwner; message.read(name, oldOwner, newOwner); if (newOwner.empty()) { return; } auto bus = sdbusplus::bus::new_default(); InterfacesPropertiesMap interfacesAndProperties; auto objects = getManagedObjects(bus, entityManagerServiceName, "/xyz/openbmc_project/inventory"); for (const auto& pair : objects) { std::tie(std::ignore, interfacesAndProperties) = pair; if (maybeCall(interfacesAndProperties, maybeSetAttrWithArgsBound)) { loop.exit(0); } } })); InterfacesPropertiesMap interfacesAndProperties; auto objects = getManagedObjects(bus, entityManagerServiceName, "/xyz/openbmc_project/inventory"); for (const auto& pair : objects) { std::tie(std::ignore, interfacesAndProperties) = pair; if (maybeCall(interfacesAndProperties, maybeSetAttrWithArgsBound)) { loop.exit(0); return {}; } } return matches; } } // namespace process_hostfirmware } // namespace functions