1 #include "config.h" 2 3 #include "activation.hpp" 4 5 #include "utils.hpp" 6 7 #include <phosphor-logging/elog-errors.hpp> 8 #include <phosphor-logging/lg2.hpp> 9 10 #include <exception> 11 #include <filesystem> 12 #include <format> 13 #include <stdexcept> 14 #include <vector> 15 16 namespace phosphor 17 { 18 namespace software 19 { 20 namespace updater 21 { 22 23 constexpr auto SYSTEMD_BUSNAME = "org.freedesktop.systemd1"; 24 constexpr auto SYSTEMD_PATH = "/org/freedesktop/systemd1"; 25 constexpr auto SYSTEMD_INTERFACE = "org.freedesktop.systemd1.Manager"; 26 27 namespace fs = std::filesystem; 28 29 using namespace phosphor::logging; 30 using SoftwareActivation = 31 sdbusplus::server::xyz::openbmc_project::software::Activation; 32 using ExtendedVersion = 33 sdbusplus::server::xyz::openbmc_project::software::ExtendedVersion; 34 35 auto Activation::activation(Activations value) -> Activations 36 { 37 if (value == Status::Activating) 38 { 39 value = startActivation(); 40 } 41 else 42 { 43 activationBlocksTransition.reset(); 44 activationProgress.reset(); 45 } 46 47 return SoftwareActivation::activation(value); 48 } 49 50 auto Activation::requestedActivation(RequestedActivations value) 51 -> RequestedActivations 52 { 53 if ((value == SoftwareActivation::RequestedActivations::Active) && 54 (SoftwareActivation::requestedActivation() != 55 SoftwareActivation::RequestedActivations::Active)) 56 { 57 // PSU image could be activated even when it's in active, 58 // e.g. in case a PSU is replaced and has a older image, it will be 59 // updated with the running PSU image that is stored in BMC. 60 if ((activation() == Status::Ready) || 61 (activation() == Status::Failed) || activation() == Status::Active) 62 { 63 activation(Status::Activating); 64 } 65 } 66 return SoftwareActivation::requestedActivation(value); 67 } 68 69 auto Activation::extendedVersion(std::string value) -> std::string 70 { 71 auto info = Version::getExtVersionInfo(value); 72 manufacturer = info["manufacturer"]; 73 model = info["model"]; 74 75 return ExtendedVersion::extendedVersion(value); 76 } 77 78 void Activation::unitStateChange(sdbusplus::message_t& msg) 79 { 80 uint32_t newStateID{}; 81 sdbusplus::message::object_path newStateObjPath; 82 std::string newStateUnit{}; 83 std::string newStateResult{}; 84 85 try 86 { 87 // Read the msg and populate each variable 88 msg.read(newStateID, newStateObjPath, newStateUnit, newStateResult); 89 90 if (newStateUnit == psuUpdateUnit) 91 { 92 if (newStateResult == "done") 93 { 94 onUpdateDone(); 95 } 96 if (newStateResult == "failed" || newStateResult == "dependency") 97 { 98 onUpdateFailed(); 99 } 100 } 101 } 102 catch (const std::exception& e) 103 { 104 lg2::error("Unable to handle unit state change event: {ERROR}", "ERROR", 105 e); 106 } 107 } 108 109 bool Activation::doUpdate(const std::string& psuInventoryPath) 110 { 111 currentUpdatingPsu = psuInventoryPath; 112 try 113 { 114 psuUpdateUnit = getUpdateService(currentUpdatingPsu); 115 auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH, 116 SYSTEMD_INTERFACE, "StartUnit"); 117 method.append(psuUpdateUnit, "replace"); 118 bus.call_noreply(method); 119 return true; 120 } 121 catch (const std::exception& e) 122 { 123 lg2::error("Error starting update service for PSU {PSU}: {ERROR}", 124 "PSU", psuInventoryPath, "ERROR", e); 125 onUpdateFailed(); 126 return false; 127 } 128 } 129 130 bool Activation::doUpdate() 131 { 132 // When the queue is empty, all updates are done 133 if (psuQueue.empty()) 134 { 135 finishActivation(); 136 return true; 137 } 138 139 // Do the update on a PSU 140 const auto& psu = psuQueue.front(); 141 return doUpdate(psu); 142 } 143 144 void Activation::onUpdateDone() 145 { 146 auto progress = activationProgress->progress() + progressStep; 147 activationProgress->progress(progress); 148 149 // Update the activation association 150 auto assocs = associations(); 151 assocs.emplace_back(ACTIVATION_FWD_ASSOCIATION, ACTIVATION_REV_ASSOCIATION, 152 currentUpdatingPsu); 153 associations(assocs); 154 155 activationListener->onUpdateDone(versionId, currentUpdatingPsu); 156 currentUpdatingPsu.clear(); 157 158 psuQueue.pop(); 159 doUpdate(); // Update the next psu 160 } 161 162 void Activation::onUpdateFailed() 163 { 164 // TODO: report an event 165 lg2::error("Failed to update PSU {PSU}", "PSU", psuQueue.front()); 166 std::queue<std::string>().swap(psuQueue); // Clear the queue 167 activation(Status::Failed); 168 } 169 170 Activation::Status Activation::startActivation() 171 { 172 // Check if the activation has file path 173 if (path().empty()) 174 { 175 lg2::warning( 176 "No image for the activation, skipped version {VERSION_ID}", 177 "VERSION_ID", versionId); 178 return activation(); // Return the previous activation status 179 } 180 181 auto psuPaths = utils::getPSUInventoryPaths(bus); 182 if (psuPaths.empty()) 183 { 184 lg2::warning("No PSU inventory found"); 185 return Status::Failed; 186 } 187 188 for (const auto& p : psuPaths) 189 { 190 if (!isPresent(p)) 191 { 192 continue; 193 } 194 if (isCompatible(p)) 195 { 196 if (utils::isAssociated(p, associations())) 197 { 198 lg2::notice("PSU {PSU} is already running the image, skipping", 199 "PSU", p); 200 continue; 201 } 202 psuQueue.push(p); 203 } 204 else 205 { 206 lg2::notice("PSU {PSU} is not compatible", "PSU", p); 207 } 208 } 209 210 if (psuQueue.empty()) 211 { 212 lg2::warning("No PSU compatible with the software"); 213 return activation(); // Return the previous activation status 214 } 215 216 if (!activationProgress) 217 { 218 activationProgress = std::make_unique<ActivationProgress>(bus, objPath); 219 } 220 if (!activationBlocksTransition) 221 { 222 activationBlocksTransition = 223 std::make_unique<ActivationBlocksTransition>(bus, objPath); 224 } 225 226 // The progress to be increased for each successful update of PSU 227 // E.g. in case we have 4 PSUs: 228 // 1. Initial progress is 10 229 // 2. Add 20 after each update is done, so we will see progress to be 30, 230 // 50, 70, 90 231 // 3. When all PSUs are updated, it will be 100 and the interface is 232 // removed. 233 progressStep = 80 / psuQueue.size(); 234 if (doUpdate()) 235 { 236 activationProgress->progress(10); 237 return Status::Activating; 238 } 239 else 240 { 241 return Status::Failed; 242 } 243 } 244 245 void Activation::finishActivation() 246 { 247 storeImage(); 248 activationProgress->progress(100); 249 250 deleteImageManagerObject(); 251 252 associationInterface->createActiveAssociation(objPath); 253 associationInterface->addFunctionalAssociation(objPath); 254 associationInterface->addUpdateableAssociation(objPath); 255 256 // Reset RequestedActivations to none so that it could be activated in 257 // future 258 requestedActivation(SoftwareActivation::RequestedActivations::None); 259 activation(Status::Active); 260 } 261 262 void Activation::deleteImageManagerObject() 263 { 264 // Get the Delete object for <versionID> inside image_manager 265 std::vector<std::string> services; 266 constexpr auto deleteInterface = "xyz.openbmc_project.Object.Delete"; 267 try 268 { 269 services = utils::getServices(bus, objPath.c_str(), deleteInterface); 270 } 271 catch (const std::exception& e) 272 { 273 lg2::error( 274 "Unable to find services to Delete object path {PATH}: {ERROR}", 275 "PATH", objPath, "ERROR", e); 276 } 277 278 // We need to find the phosphor-version-software-manager's version service 279 // to invoke the delete interface 280 constexpr auto versionServiceStr = "xyz.openbmc_project.Software.Version"; 281 std::string versionService; 282 for (const auto& service : services) 283 { 284 if (service.find(versionServiceStr) != std::string::npos) 285 { 286 versionService = service; 287 break; 288 } 289 } 290 if (versionService.empty()) 291 { 292 // When updating a stored image, there is no version object created by 293 // "xyz.openbmc_project.Software.Version" service, so skip it. 294 return; 295 } 296 297 // Call the Delete object for <versionID> inside image_manager 298 try 299 { 300 auto method = bus.new_method_call( 301 versionService.c_str(), objPath.c_str(), deleteInterface, "Delete"); 302 bus.call(method); 303 } 304 catch (const std::exception& e) 305 { 306 lg2::error("Unable to Delete object path {PATH}: {ERROR}", "PATH", 307 objPath, "ERROR", e); 308 } 309 } 310 311 bool Activation::isPresent(const std::string& psuInventoryPath) 312 { 313 bool isPres{false}; 314 try 315 { 316 auto service = 317 utils::getService(bus, psuInventoryPath.c_str(), ITEM_IFACE); 318 isPres = utils::getProperty<bool>(bus, service.c_str(), 319 psuInventoryPath.c_str(), ITEM_IFACE, 320 PRESENT); 321 } 322 catch (const std::exception& e) 323 { 324 // Treat as a warning condition and assume the PSU is missing. The 325 // D-Bus information might not be available if the PSU is missing. 326 lg2::warning("Unable to determine if PSU {PSU} is present: {ERROR}", 327 "PSU", psuInventoryPath, "ERROR", e); 328 } 329 return isPres; 330 } 331 332 bool Activation::isCompatible(const std::string& psuInventoryPath) 333 { 334 bool isCompat{false}; 335 try 336 { 337 auto service = 338 utils::getService(bus, psuInventoryPath.c_str(), ASSET_IFACE); 339 auto psuManufacturer = utils::getProperty<std::string>( 340 bus, service.c_str(), psuInventoryPath.c_str(), ASSET_IFACE, 341 MANUFACTURER); 342 auto psuModel = utils::getModel(psuInventoryPath); 343 // The model shall match 344 if (psuModel == model) 345 { 346 // If PSU inventory has manufacturer property, it shall match 347 if (psuManufacturer.empty() || (psuManufacturer == manufacturer)) 348 { 349 isCompat = true; 350 } 351 } 352 } 353 catch (const std::exception& e) 354 { 355 lg2::error( 356 "Unable to determine if PSU {PSU} is compatible with firmware " 357 "versionId {VERSION_ID}: {ERROR}", 358 "PSU", psuInventoryPath, "VERSION_ID", versionId, "ERROR", e); 359 } 360 return isCompat; 361 } 362 363 void Activation::storeImage() 364 { 365 // If image is not in IMG_DIR (temporary storage) then exit. We don't want 366 // to copy from IMG_DIR_PERSIST or IMG_DIR_BUILTIN. 367 auto src = path(); 368 if (!src.starts_with(IMG_DIR)) 369 { 370 return; 371 } 372 373 // Store image in persistent dir separated by model 374 // and only store the latest one by removing old ones 375 auto dst = fs::path(IMG_DIR_PERSIST) / model; 376 try 377 { 378 fs::remove_all(dst); 379 fs::create_directories(dst); 380 fs::copy(src, dst); 381 path(dst.string()); // Update the FilePath interface 382 } 383 catch (const fs::filesystem_error& e) 384 { 385 lg2::error("Error storing PSU image: src={SRC}, dst={DST}: {ERROR}", 386 "SRC", src, "DST", dst, "ERROR", e); 387 } 388 } 389 390 std::string Activation::getUpdateService(const std::string& psuInventoryPath) 391 { 392 fs::path imagePath(path()); 393 394 // The systemd unit shall be escaped 395 std::string args = psuInventoryPath; 396 args += "\\x20"; 397 args += imagePath; 398 std::replace(args.begin(), args.end(), '/', '-'); 399 400 std::string service = PSU_UPDATE_SERVICE; 401 auto p = service.find('@'); 402 if (p == std::string::npos) 403 { 404 throw std::runtime_error{std::format( 405 "Invalid PSU update service name: {}", PSU_UPDATE_SERVICE)}; 406 } 407 service.insert(p + 1, args); 408 return service; 409 } 410 411 void ActivationBlocksTransition::enableRebootGuard() 412 { 413 lg2::info("PSU image activating - BMC reboots are disabled."); 414 415 auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH, 416 SYSTEMD_INTERFACE, "StartUnit"); 417 method.append("reboot-guard-enable.service", "replace"); 418 bus.call_noreply_noerror(method); 419 } 420 421 void ActivationBlocksTransition::disableRebootGuard() 422 { 423 lg2::info("PSU activation has ended - BMC reboots are re-enabled."); 424 425 auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH, 426 SYSTEMD_INTERFACE, "StartUnit"); 427 method.append("reboot-guard-disable.service", "replace"); 428 bus.call_noreply_noerror(method); 429 } 430 431 } // namespace updater 432 } // namespace software 433 } // namespace phosphor 434