#include "config.h" #include "activation.hpp" #include "utils.hpp" #include #include #include #include #include #include #include namespace phosphor { namespace software { namespace updater { constexpr auto SYSTEMD_BUSNAME = "org.freedesktop.systemd1"; constexpr auto SYSTEMD_PATH = "/org/freedesktop/systemd1"; constexpr auto SYSTEMD_INTERFACE = "org.freedesktop.systemd1.Manager"; namespace fs = std::filesystem; using namespace phosphor::logging; using SoftwareActivation = sdbusplus::server::xyz::openbmc_project::software::Activation; using ExtendedVersion = sdbusplus::server::xyz::openbmc_project::software::ExtendedVersion; auto Activation::activation(Activations value) -> Activations { if (value == Status::Activating) { value = startActivation(); } else { activationBlocksTransition.reset(); activationProgress.reset(); } return SoftwareActivation::activation(value); } auto Activation::requestedActivation(RequestedActivations value) -> RequestedActivations { if ((value == SoftwareActivation::RequestedActivations::Active) && (SoftwareActivation::requestedActivation() != SoftwareActivation::RequestedActivations::Active)) { // PSU image could be activated even when it's in active, // e.g. in case a PSU is replaced and has a older image, it will be // updated with the running PSU image that is stored in BMC. if ((activation() == Status::Ready) || (activation() == Status::Failed) || activation() == Status::Active) { activation(Status::Activating); } } return SoftwareActivation::requestedActivation(value); } auto Activation::extendedVersion(std::string value) -> std::string { auto info = Version::getExtVersionInfo(value); manufacturer = info["manufacturer"]; model = info["model"]; return ExtendedVersion::extendedVersion(value); } void Activation::unitStateChange(sdbusplus::message_t& msg) { uint32_t newStateID{}; sdbusplus::message::object_path newStateObjPath; std::string newStateUnit{}; std::string newStateResult{}; try { // Read the msg and populate each variable msg.read(newStateID, newStateObjPath, newStateUnit, newStateResult); if (newStateUnit == psuUpdateUnit) { if (newStateResult == "done") { onUpdateDone(); } if (newStateResult == "failed" || newStateResult == "dependency") { onUpdateFailed(); } } } catch (const std::exception& e) { lg2::error("Unable to handle unit state change event: {ERROR}", "ERROR", e); } } bool Activation::doUpdate(const std::string& psuInventoryPath) { currentUpdatingPsu = psuInventoryPath; try { psuUpdateUnit = getUpdateService(currentUpdatingPsu); auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH, SYSTEMD_INTERFACE, "StartUnit"); method.append(psuUpdateUnit, "replace"); bus.call_noreply(method); return true; } catch (const std::exception& e) { lg2::error("Error starting update service for PSU {PSU}: {ERROR}", "PSU", psuInventoryPath, "ERROR", e); onUpdateFailed(); return false; } } bool Activation::doUpdate() { // When the queue is empty, all updates are done if (psuQueue.empty()) { finishActivation(); return true; } // Do the update on a PSU const auto& psu = psuQueue.front(); return doUpdate(psu); } void Activation::onUpdateDone() { auto progress = activationProgress->progress() + progressStep; activationProgress->progress(progress); // Update the activation association auto assocs = associations(); assocs.emplace_back(ACTIVATION_FWD_ASSOCIATION, ACTIVATION_REV_ASSOCIATION, currentUpdatingPsu); associations(assocs); activationListener->onUpdateDone(versionId, currentUpdatingPsu); currentUpdatingPsu.clear(); psuQueue.pop(); doUpdate(); // Update the next psu } void Activation::onUpdateFailed() { // TODO: report an event lg2::error("Failed to update PSU {PSU}", "PSU", psuQueue.front()); std::queue().swap(psuQueue); // Clear the queue activation(Status::Failed); } Activation::Status Activation::startActivation() { // Check if the activation has file path if (path().empty()) { lg2::warning( "No image for the activation, skipped version {VERSION_ID}", "VERSION_ID", versionId); return activation(); // Return the previous activation status } auto psuPaths = utils::getPSUInventoryPaths(bus); if (psuPaths.empty()) { lg2::warning("No PSU inventory found"); return Status::Failed; } for (const auto& p : psuPaths) { if (!isPresent(p)) { continue; } if (isCompatible(p)) { if (utils::isAssociated(p, associations())) { lg2::notice("PSU {PSU} is already running the image, skipping", "PSU", p); continue; } psuQueue.push(p); } else { lg2::notice("PSU {PSU} is not compatible", "PSU", p); } } if (psuQueue.empty()) { lg2::warning("No PSU compatible with the software"); return activation(); // Return the previous activation status } if (!activationProgress) { activationProgress = std::make_unique(bus, objPath); } if (!activationBlocksTransition) { activationBlocksTransition = std::make_unique(bus, objPath); } // The progress to be increased for each successful update of PSU // E.g. in case we have 4 PSUs: // 1. Initial progress is 10 // 2. Add 20 after each update is done, so we will see progress to be 30, // 50, 70, 90 // 3. When all PSUs are updated, it will be 100 and the interface is // removed. progressStep = 80 / psuQueue.size(); if (doUpdate()) { activationProgress->progress(10); return Status::Activating; } else { return Status::Failed; } } void Activation::finishActivation() { storeImage(); activationProgress->progress(100); deleteImageManagerObject(); associationInterface->createActiveAssociation(objPath); associationInterface->addFunctionalAssociation(objPath); associationInterface->addUpdateableAssociation(objPath); // Reset RequestedActivations to none so that it could be activated in // future requestedActivation(SoftwareActivation::RequestedActivations::None); activation(Status::Active); } void Activation::deleteImageManagerObject() { // Get the Delete object for inside image_manager std::vector services; constexpr auto deleteInterface = "xyz.openbmc_project.Object.Delete"; try { services = utils::getServices(bus, objPath.c_str(), deleteInterface); } catch (const std::exception& e) { lg2::error( "Unable to find services to Delete object path {PATH}: {ERROR}", "PATH", objPath, "ERROR", e); } // We need to find the phosphor-version-software-manager's version service // to invoke the delete interface constexpr auto versionServiceStr = "xyz.openbmc_project.Software.Version"; std::string versionService; for (const auto& service : services) { if (service.find(versionServiceStr) != std::string::npos) { versionService = service; break; } } if (versionService.empty()) { // When updating a stored image, there is no version object created by // "xyz.openbmc_project.Software.Version" service, so skip it. return; } // Call the Delete object for inside image_manager try { auto method = bus.new_method_call( versionService.c_str(), objPath.c_str(), deleteInterface, "Delete"); bus.call(method); } catch (const std::exception& e) { lg2::error("Unable to Delete object path {PATH}: {ERROR}", "PATH", objPath, "ERROR", e); } } bool Activation::isPresent(const std::string& psuInventoryPath) { bool isPres{false}; try { auto service = utils::getService(bus, psuInventoryPath.c_str(), ITEM_IFACE); isPres = utils::getProperty(bus, service.c_str(), psuInventoryPath.c_str(), ITEM_IFACE, PRESENT); } catch (const std::exception& e) { // Treat as a warning condition and assume the PSU is missing. The // D-Bus information might not be available if the PSU is missing. lg2::warning("Unable to determine if PSU {PSU} is present: {ERROR}", "PSU", psuInventoryPath, "ERROR", e); } return isPres; } bool Activation::isCompatible(const std::string& psuInventoryPath) { bool isCompat{false}; try { auto service = utils::getService(bus, psuInventoryPath.c_str(), ASSET_IFACE); auto psuManufacturer = utils::getProperty( bus, service.c_str(), psuInventoryPath.c_str(), ASSET_IFACE, MANUFACTURER); auto psuModel = utils::getModel(psuInventoryPath); // The model shall match if (psuModel == model) { // If PSU inventory has manufacturer property, it shall match if (psuManufacturer.empty() || (psuManufacturer == manufacturer)) { isCompat = true; } } } catch (const std::exception& e) { lg2::error( "Unable to determine if PSU {PSU} is compatible with firmware " "versionId {VERSION_ID}: {ERROR}", "PSU", psuInventoryPath, "VERSION_ID", versionId, "ERROR", e); } return isCompat; } void Activation::storeImage() { // If image is not in IMG_DIR (temporary storage) then exit. We don't want // to copy from IMG_DIR_PERSIST or IMG_DIR_BUILTIN. auto src = path(); if (!src.starts_with(IMG_DIR)) { return; } // Store image in persistent dir separated by model // and only store the latest one by removing old ones auto dst = fs::path(IMG_DIR_PERSIST) / model; try { fs::remove_all(dst); fs::create_directories(dst); fs::copy(src, dst); path(dst.string()); // Update the FilePath interface } catch (const fs::filesystem_error& e) { lg2::error("Error storing PSU image: src={SRC}, dst={DST}: {ERROR}", "SRC", src, "DST", dst, "ERROR", e); } } std::string Activation::getUpdateService(const std::string& psuInventoryPath) { fs::path imagePath(path()); // The systemd unit shall be escaped std::string args = psuInventoryPath; args += "\\x20"; args += imagePath; std::replace(args.begin(), args.end(), '/', '-'); std::string service = PSU_UPDATE_SERVICE; auto p = service.find('@'); if (p == std::string::npos) { throw std::runtime_error{std::format( "Invalid PSU update service name: {}", PSU_UPDATE_SERVICE)}; } service.insert(p + 1, args); return service; } void ActivationBlocksTransition::enableRebootGuard() { lg2::info("PSU image activating - BMC reboots are disabled."); auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH, SYSTEMD_INTERFACE, "StartUnit"); method.append("reboot-guard-enable.service", "replace"); bus.call_noreply_noerror(method); } void ActivationBlocksTransition::disableRebootGuard() { lg2::info("PSU activation has ended - BMC reboots are re-enabled."); auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH, SYSTEMD_INTERFACE, "StartUnit"); method.append("reboot-guard-disable.service", "replace"); bus.call_noreply_noerror(method); } } // namespace updater } // namespace software } // namespace phosphor