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