#include "config.h" #include "item_updater.hpp" #include "runtime_warning.hpp" #include "utils.hpp" #include #include #include #include #include #include #include #include namespace { constexpr auto MANIFEST_VERSION = "version"; constexpr auto MANIFEST_EXTENDED_VERSION = "extended_version"; } // namespace namespace phosphor { namespace software { namespace updater { namespace server = sdbusplus::xyz::openbmc_project::Software::server; using namespace sdbusplus::xyz::openbmc_project::Common::Error; using namespace phosphor::logging; using SVersion = server::Version; using VersionPurpose = SVersion::VersionPurpose; void ItemUpdater::onVersionInterfacesAddedMsg(sdbusplus::message_t& msg) { try { sdbusplus::message::object_path objPath; InterfacesAddedMap interfaces; msg.read(objPath, interfaces); std::string path(std::move(objPath)); onVersionInterfacesAdded(path, interfaces); } catch (const std::exception& e) { lg2::error("Unable to handle version InterfacesAdded event: {ERROR}", "ERROR", e); } } void ItemUpdater::onVersionInterfacesAdded(const std::string& path, const InterfacesAddedMap& interfaces) { std::string filePath; auto purpose = VersionPurpose::Unknown; std::string version; for (const auto& [interfaceName, propertyMap] : interfaces) { if (interfaceName == VERSION_IFACE) { for (const auto& [propertyName, propertyValue] : propertyMap) { if (propertyName == "Purpose") { // Only process the PSU images auto value = SVersion::convertVersionPurposeFromString( std::get(propertyValue)); if (value == VersionPurpose::PSU) { purpose = value; } } else if (propertyName == VERSION) { version = std::get(propertyValue); } } } else if (interfaceName == FILEPATH_IFACE) { const auto& it = propertyMap.find("Path"); if (it != propertyMap.end()) { filePath = std::get(it->second); } } } if ((filePath.empty()) || (purpose == VersionPurpose::Unknown)) { return; } // If we are only installing PSU images from the built-in directory, ignore // PSU images from other directories if (ALWAYS_USE_BUILTIN_IMG_DIR && !filePath.starts_with(IMG_DIR_BUILTIN)) { return; } // Version id is the last item in the path auto pos = path.rfind('/'); if (pos == std::string::npos) { lg2::error("No version id found in object path {OBJPATH}", "OBJPATH", path); return; } auto versionId = path.substr(pos + 1); if (activations.find(versionId) == activations.end()) { // Determine the Activation state by processing the given image dir. AssociationList associations; auto activationState = Activation::Status::Ready; associations.emplace_back(std::make_tuple( ACTIVATION_FWD_ASSOCIATION, ACTIVATION_REV_ASSOCIATION, PSU_INVENTORY_PATH_BASE)); fs::path manifestPath(filePath); manifestPath /= MANIFEST_FILE; std::string extendedVersion = Version::getValue(manifestPath, {MANIFEST_EXTENDED_VERSION}); auto activation = createActivationObject(path, versionId, extendedVersion, activationState, associations, filePath); activations.emplace(versionId, std::move(activation)); auto versionPtr = createVersionObject(path, versionId, version, purpose); versions.emplace(versionId, std::move(versionPtr)); } } void ItemUpdater::erase(const std::string& versionId) { auto it = versions.find(versionId); if (it == versions.end()) { lg2::error("Error: Failed to find version {VERSION_ID} in " "item updater versions map. Unable to remove.", "VERSION_ID", versionId); } else { versionStrings.erase(it->second->getVersionString()); versions.erase(it); } // Removing entry in activations map auto ita = activations.find(versionId); if (ita == activations.end()) { lg2::error("Error: Failed to find version {VERSION_ID} in " "item updater activations map. Unable to remove.", "VERSION_ID", versionId); } else { activations.erase(versionId); } } void ItemUpdater::createActiveAssociation(const std::string& path) { assocs.emplace_back( std::make_tuple(ACTIVE_FWD_ASSOCIATION, ACTIVE_REV_ASSOCIATION, path)); associations(assocs); } void ItemUpdater::addFunctionalAssociation(const std::string& path) { assocs.emplace_back(std::make_tuple(FUNCTIONAL_FWD_ASSOCIATION, FUNCTIONAL_REV_ASSOCIATION, path)); associations(assocs); } void ItemUpdater::addUpdateableAssociation(const std::string& path) { assocs.emplace_back(std::make_tuple(UPDATEABLE_FWD_ASSOCIATION, UPDATEABLE_REV_ASSOCIATION, path)); associations(assocs); } void ItemUpdater::removeAssociation(const std::string& path) { for (auto iter = assocs.begin(); iter != assocs.end();) { if ((std::get<2>(*iter)) == path) { iter = assocs.erase(iter); associations(assocs); } else { ++iter; } } } void ItemUpdater::onUpdateDone(const std::string& versionId, const std::string& psuInventoryPath) { // After update is done, remove old activation objects for (auto it = activations.begin(); it != activations.end(); ++it) { if (it->second->getVersionId() != versionId && utils::isAssociated(psuInventoryPath, it->second->associations())) { removePsuObject(psuInventoryPath); break; } } auto it = activations.find(versionId); if (it == activations.end()) { lg2::error("Unable to find Activation for version ID {VERSION_ID}", "VERSION_ID", versionId); } else { psuPathActivationMap.emplace(psuInventoryPath, it->second); } } std::unique_ptr ItemUpdater::createActivationObject( const std::string& path, const std::string& versionId, const std::string& extVersion, Activation::Status activationStatus, const AssociationList& assocs, const std::string& filePath) { return std::make_unique(bus, path, versionId, extVersion, activationStatus, assocs, filePath, this, this); } void ItemUpdater::createPsuObject(const std::string& psuInventoryPath, const std::string& psuVersion) { auto versionId = utils::getVersionId(psuVersion); auto path = std::string(SOFTWARE_OBJPATH) + "/" + versionId; auto it = activations.find(versionId); if (it != activations.end()) { // The versionId is already created, associate the path auto associations = it->second->associations(); associations.emplace_back( std::make_tuple(ACTIVATION_FWD_ASSOCIATION, ACTIVATION_REV_ASSOCIATION, psuInventoryPath)); it->second->associations(associations); psuPathActivationMap.emplace(psuInventoryPath, it->second); } else { // Create a new object for running PSU inventory AssociationList associations; auto activationState = Activation::Status::Active; associations.emplace_back( std::make_tuple(ACTIVATION_FWD_ASSOCIATION, ACTIVATION_REV_ASSOCIATION, psuInventoryPath)); auto activation = createActivationObject( path, versionId, "", activationState, associations, ""); activations.emplace(versionId, std::move(activation)); psuPathActivationMap.emplace(psuInventoryPath, activations[versionId]); auto versionPtr = createVersionObject(path, versionId, psuVersion, VersionPurpose::PSU); versions.emplace(versionId, std::move(versionPtr)); createActiveAssociation(path); addFunctionalAssociation(path); addUpdateableAssociation(path); } } void ItemUpdater::removePsuObject(const std::string& psuInventoryPath) { auto it = psuPathActivationMap.find(psuInventoryPath); if (it == psuPathActivationMap.end()) { lg2::error("No Activation found for PSU {PSUPATH}", "PSUPATH", psuInventoryPath); return; } const auto& activationPtr = it->second; psuPathActivationMap.erase(psuInventoryPath); auto associations = activationPtr->associations(); for (auto iter = associations.begin(); iter != associations.end();) { if ((std::get<2>(*iter)) == psuInventoryPath) { iter = associations.erase(iter); } else { ++iter; } } if (associations.empty()) { // Remove the activation erase(activationPtr->getVersionId()); } else { // Update association activationPtr->associations(associations); } } void ItemUpdater::addPsuToStatusMap(const std::string& psuPath) { if (!psuStatusMap.contains(psuPath)) { psuStatusMap[psuPath] = {false, ""}; // Add PropertiesChanged listener for Item interface so we are notified // when Present property changes psuMatches.emplace_back( bus, MatchRules::propertiesChanged(psuPath, ITEM_IFACE), std::bind(&ItemUpdater::onPsuInventoryChangedMsg, this, std::placeholders::_1)); } } void ItemUpdater::handlePSUPresenceChanged(const std::string& psuPath) { if (psuStatusMap.contains(psuPath)) { if (psuStatusMap[psuPath].present) { // PSU is now present psuStatusMap[psuPath].model = utils::getModel(psuPath); auto version = utils::getVersion(psuPath); if (!version.empty() && !psuPathActivationMap.contains(psuPath)) { createPsuObject(psuPath, version); } } else { // PSU is now missing psuStatusMap[psuPath].model.clear(); if (psuPathActivationMap.contains(psuPath)) { removePsuObject(psuPath); } } } } std::unique_ptr ItemUpdater::createVersionObject( const std::string& objPath, const std::string& versionId, const std::string& versionString, sdbusplus::xyz::openbmc_project::Software::server::Version::VersionPurpose versionPurpose) { versionStrings.insert(versionString); auto version = std::make_unique( bus, objPath, versionId, versionString, versionPurpose, std::bind(&ItemUpdater::erase, this, std::placeholders::_1)); return version; } void ItemUpdater::onPsuInventoryChangedMsg(sdbusplus::message_t& msg) { try { using Interface = std::string; Interface interface; Properties properties; std::string psuPath = msg.get_path(); msg.read(interface, properties); onPsuInventoryChanged(psuPath, properties); } catch (const std::exception& e) { lg2::error( "Unable to handle inventory PropertiesChanged event: {ERROR}", "ERROR", e); } } void ItemUpdater::onPsuInventoryChanged(const std::string& psuPath, const Properties& properties) { if (psuStatusMap.contains(psuPath) && properties.contains(PRESENT)) { psuStatusMap[psuPath].present = std::get(properties.at(PRESENT)); handlePSUPresenceChanged(psuPath); if (psuStatusMap[psuPath].present) { // Check if there are new PSU images to update processStoredImage(); syncToLatestImage(); } } } void ItemUpdater::processPSUImage() { try { auto paths = utils::getPSUInventoryPaths(bus); for (const auto& p : paths) { try { addPsuToStatusMap(p); auto service = utils::getService(bus, p.c_str(), ITEM_IFACE); psuStatusMap[p].present = utils::getProperty( bus, service.c_str(), p.c_str(), ITEM_IFACE, PRESENT); handlePSUPresenceChanged(p); } catch (const std::exception& e) { // Ignore errors; the information might not be available yet } } } catch (const std::exception& e) { // Ignore errors; the information might not be available yet } } void ItemUpdater::processStoredImage() { // Build list of directories to scan std::vector paths; paths.emplace_back(IMG_DIR_BUILTIN); if (!ALWAYS_USE_BUILTIN_IMG_DIR) { paths.emplace_back(IMG_DIR_PERSIST); } // Scan directories auto logMsg = "Unable to find PSU firmware in directory {PATH}: {ERROR}"; for (const auto& path : paths) { try { scanDirectory(path); } catch (const RuntimeWarning& r) { lg2::warning(logMsg, "PATH", path, "ERROR", r); } catch (const std::exception& e) { lg2::error(logMsg, "PATH", path, "ERROR", e); } } } void ItemUpdater::scanDirectory(const fs::path& dir) { // Find the model subdirectory within the specified directory auto modelDir = findModelDirectory(dir); if (modelDir.empty()) { return; } // Verify a manifest file exists within the model subdirectory auto manifest = modelDir / MANIFEST_FILE; if (!fs::exists(manifest)) { throw std::runtime_error{ std::format("Manifest file does not exist: {}", manifest.c_str())}; } if (!fs::is_regular_file(manifest)) { throw std::runtime_error{ std::format("Path is not a file: {}", manifest.c_str())}; } // Get version, extVersion, and model from manifest file auto ret = Version::getValues( manifest.string(), {MANIFEST_VERSION, MANIFEST_EXTENDED_VERSION}); auto version = ret[MANIFEST_VERSION]; auto extVersion = ret[MANIFEST_EXTENDED_VERSION]; auto info = Version::getExtVersionInfo(extVersion); auto model = info["model"]; // Verify version and model are valid if (version.empty() || model.empty()) { throw std::runtime_error{std::format( "Invalid information in manifest: path={}, version={}, model={}", manifest.c_str(), version, model)}; } // Verify model from manifest matches the subdirectory name if (modelDir.stem() != model) { throw std::runtime_error{std::format( "Model in manifest does not match path: model={}, path={}", model, modelDir.c_str())}; } // Found a valid PSU image directory; write path to journal lg2::info("Found PSU firmware image directory: {PATH}", "PATH", modelDir); // Calculate version ID and check if an Activation for it exists auto versionId = utils::getVersionId(version); auto it = activations.find(versionId); if (it == activations.end()) { // This is a version that is different than the running PSUs auto activationState = Activation::Status::Ready; auto purpose = VersionPurpose::PSU; auto objPath = std::string(SOFTWARE_OBJPATH) + "/" + versionId; auto activation = createActivationObject(objPath, versionId, extVersion, activationState, {}, modelDir); activations.emplace(versionId, std::move(activation)); auto versionPtr = createVersionObject(objPath, versionId, version, purpose); versions.emplace(versionId, std::move(versionPtr)); } else { // Activation already exists. It may have been created for code that is // running on one or more PSUs. Set Path and ExtendedVersion properties. // The properties are not set when the Activation is created for code // running on a PSU. The properties are needed to update other PSUs. it->second->path(modelDir); it->second->extendedVersion(extVersion); } } fs::path ItemUpdater::findModelDirectory(const fs::path& dir) { fs::path modelDir; // Verify directory path exists and is a directory if (!fs::exists(dir)) { // Warning condition. IMG_DIR_BUILTIN might not be used. IMG_DIR_PERSIST // might not exist if an image from IMG_DIR has not been stored. throw RuntimeWarning{ std::format("Directory does not exist: {}", dir.c_str())}; } if (!fs::is_directory(dir)) { throw std::runtime_error{ std::format("Path is not a directory: {}", dir.c_str())}; } // Get the model name of the PSUs that have been found. Note that we // might not have found the PSU information yet on D-Bus. std::string model; for (const auto& [key, item] : psuStatusMap) { if (!item.model.empty()) { model = item.model; break; } } if (!model.empty()) { // Verify model subdirectory path exists and is a directory auto subDir = dir / model; if (!fs::exists(subDir)) { // Warning condition. Subdirectory may not exist in IMG_DIR_PERSIST // if no image has been stored there. May also not exist if // firmware update is not supported for this PSU model. throw RuntimeWarning{ std::format("Directory does not exist: {}", subDir.c_str())}; } if (!fs::is_directory(subDir)) { throw std::runtime_error{ std::format("Path is not a directory: {}", subDir.c_str())}; } modelDir = subDir; } return modelDir; } std::optional ItemUpdater::getLatestVersionId() { std::string latestVersion; if (ALWAYS_USE_BUILTIN_IMG_DIR) { latestVersion = getFWVersionFromBuiltinDir(); } else { latestVersion = utils::getLatestVersion(versionStrings); } if (latestVersion.empty()) { return {}; } std::optional versionId; for (const auto& v : versions) { if (v.second->version() == latestVersion) { versionId = v.first; break; } } if (!versionId.has_value()) { lg2::error("Unable to find versionId for latest version {VERSION}", "VERSION", latestVersion); } return versionId; } void ItemUpdater::syncToLatestImage() { auto latestVersionId = getLatestVersionId(); if (!latestVersionId) { return; } const auto& it = activations.find(*latestVersionId); if (it == activations.end()) { lg2::error("Unable to find Activation for versionId {VERSION_ID}", "VERSION_ID", *latestVersionId); return; } const auto& activation = it->second; const auto& assocs = activation->associations(); auto paths = utils::getPSUInventoryPaths(bus); for (const auto& p : paths) { // If there is a present PSU that is not associated with the latest // image, run the activation so that all PSUs are running the same // latest image. if (psuStatusMap.contains(p) && psuStatusMap[p].present) { if (!utils::isAssociated(p, assocs)) { lg2::info("Automatically update PSUs to versionId {VERSION_ID}", "VERSION_ID", *latestVersionId); invokeActivation(activation); break; } } } } void ItemUpdater::invokeActivation(const std::unique_ptr& activation) { activation->requestedActivation(Activation::RequestedActivations::Active); } void ItemUpdater::onPSUInterfacesAdded(sdbusplus::message_t& msg) { // Maintain static set of valid PSU paths. This is needed if PSU interface // comes in a separate InterfacesAdded message from Item interface. static std::set psuPaths{}; try { sdbusplus::message::object_path objPath; InterfacesAddedMap interfaces; msg.read(objPath, interfaces); std::string path = objPath.str; if (interfaces.contains(PSU_INVENTORY_IFACE)) { psuPaths.insert(path); } if (interfaces.contains(ITEM_IFACE) && psuPaths.contains(path) && !psuStatusMap.contains(path)) { auto interface = interfaces[ITEM_IFACE]; if (interface.contains(PRESENT)) { addPsuToStatusMap(path); psuStatusMap[path].present = std::get(interface[PRESENT]); handlePSUPresenceChanged(path); if (psuStatusMap[path].present) { // Check if there are new PSU images to update processStoredImage(); syncToLatestImage(); } } } } catch (const std::exception& e) { lg2::error("Unable to handle inventory InterfacesAdded event: {ERROR}", "ERROR", e); } } void ItemUpdater::processPSUImageAndSyncToLatest() { processPSUImage(); processStoredImage(); syncToLatestImage(); } std::string ItemUpdater::getFWVersionFromBuiltinDir() { std::string version; for (const auto& activation : activations) { if (activation.second->path().starts_with(IMG_DIR_BUILTIN)) { std::string versionId = activation.second->getVersionId(); auto it = versions.find(versionId); if (it != versions.end()) { const auto& versionPtr = it->second; version = versionPtr->version(); break; } } } return version; } } // namespace updater } // namespace software } // namespace phosphor