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 (isCompatible(p)) 180 { 181 if (utils::isAssociated(p, associations())) 182 { 183 lg2::notice("PSU {PSU} is already running the image, skipping", 184 "PSU", p); 185 continue; 186 } 187 psuQueue.push(p); 188 } 189 else 190 { 191 lg2::notice("PSU {PSU} is not compatible", "PSU", p); 192 } 193 } 194 195 if (psuQueue.empty()) 196 { 197 lg2::warning("No PSU compatible with the software"); 198 return activation(); // Return the previous activation status 199 } 200 201 if (!activationProgress) 202 { 203 activationProgress = std::make_unique<ActivationProgress>(bus, objPath); 204 } 205 if (!activationBlocksTransition) 206 { 207 activationBlocksTransition = 208 std::make_unique<ActivationBlocksTransition>(bus, objPath); 209 } 210 211 // The progress to be increased for each successful update of PSU 212 // E.g. in case we have 4 PSUs: 213 // 1. Initial progress is 10 214 // 2. Add 20 after each update is done, so we will see progress to be 30, 215 // 50, 70, 90 216 // 3. When all PSUs are updated, it will be 100 and the interface is 217 // removed. 218 progressStep = 80 / psuQueue.size(); 219 if (doUpdate()) 220 { 221 activationProgress->progress(10); 222 return Status::Activating; 223 } 224 else 225 { 226 return Status::Failed; 227 } 228 } 229 230 void Activation::finishActivation() 231 { 232 storeImage(); 233 activationProgress->progress(100); 234 235 deleteImageManagerObject(); 236 237 associationInterface->createActiveAssociation(objPath); 238 associationInterface->addFunctionalAssociation(objPath); 239 associationInterface->addUpdateableAssociation(objPath); 240 241 // Reset RequestedActivations to none so that it could be activated in 242 // future 243 requestedActivation(SoftwareActivation::RequestedActivations::None); 244 activation(Status::Active); 245 } 246 247 void Activation::deleteImageManagerObject() 248 { 249 // Get the Delete object for <versionID> inside image_manager 250 std::vector<std::string> services; 251 constexpr auto deleteInterface = "xyz.openbmc_project.Object.Delete"; 252 try 253 { 254 services = utils::getServices(bus, objPath.c_str(), deleteInterface); 255 } 256 catch (const std::exception& e) 257 { 258 lg2::error( 259 "Unable to find services to Delete object path {PATH}: {ERROR}", 260 "PATH", objPath, "ERROR", e); 261 } 262 263 // We need to find the phosphor-version-software-manager's version service 264 // to invoke the delete interface 265 constexpr auto versionServiceStr = "xyz.openbmc_project.Software.Version"; 266 std::string versionService; 267 for (const auto& service : services) 268 { 269 if (service.find(versionServiceStr) != std::string::npos) 270 { 271 versionService = service; 272 break; 273 } 274 } 275 if (versionService.empty()) 276 { 277 // When updating a stored image, there is no version object created by 278 // "xyz.openbmc_project.Software.Version" service, so skip it. 279 return; 280 } 281 282 // Call the Delete object for <versionID> inside image_manager 283 try 284 { 285 auto method = bus.new_method_call( 286 versionService.c_str(), objPath.c_str(), deleteInterface, "Delete"); 287 bus.call(method); 288 } 289 catch (const std::exception& e) 290 { 291 lg2::error("Unable to Delete object path {PATH}: {ERROR}", "PATH", 292 objPath, "ERROR", e); 293 } 294 } 295 296 bool Activation::isCompatible(const std::string& psuInventoryPath) 297 { 298 bool isCompat{false}; 299 try 300 { 301 auto service = 302 utils::getService(bus, psuInventoryPath.c_str(), ASSET_IFACE); 303 auto psuManufacturer = utils::getProperty<std::string>( 304 bus, service.c_str(), psuInventoryPath.c_str(), ASSET_IFACE, 305 MANUFACTURER); 306 auto psuModel = utils::getModel(psuInventoryPath); 307 // The model shall match 308 if (psuModel == model) 309 { 310 // If PSU inventory has manufacturer property, it shall match 311 if (psuManufacturer.empty() || (psuManufacturer == manufacturer)) 312 { 313 isCompat = true; 314 } 315 } 316 } 317 catch (const std::exception& e) 318 { 319 lg2::error( 320 "Unable to determine if PSU {PSU} is compatible with firmware " 321 "versionId {VERSION_ID}: {ERROR}", 322 "PSU", psuInventoryPath, "VERSION_ID", versionId, "ERROR", e); 323 } 324 return isCompat; 325 } 326 327 void Activation::storeImage() 328 { 329 // If image is not in IMG_DIR (temporary storage) then exit. We don't want 330 // to copy from IMG_DIR_PERSIST or IMG_DIR_BUILTIN. 331 auto src = path(); 332 if (!src.starts_with(IMG_DIR)) 333 { 334 return; 335 } 336 337 // Store image in persistent dir separated by model 338 // and only store the latest one by removing old ones 339 auto dst = fs::path(IMG_DIR_PERSIST) / model; 340 try 341 { 342 fs::remove_all(dst); 343 fs::create_directories(dst); 344 fs::copy(src, dst); 345 path(dst.string()); // Update the FilePath interface 346 } 347 catch (const fs::filesystem_error& e) 348 { 349 lg2::error("Error storing PSU image: src={SRC}, dst={DST}: {ERROR}", 350 "SRC", src, "DST", dst, "ERROR", e); 351 } 352 } 353 354 std::string Activation::getUpdateService(const std::string& psuInventoryPath) 355 { 356 fs::path imagePath(path()); 357 358 // The systemd unit shall be escaped 359 std::string args = psuInventoryPath; 360 args += "\\x20"; 361 args += imagePath; 362 std::replace(args.begin(), args.end(), '/', '-'); 363 364 std::string service = PSU_UPDATE_SERVICE; 365 auto p = service.find('@'); 366 if (p == std::string::npos) 367 { 368 throw std::runtime_error{std::format( 369 "Invalid PSU update service name: {}", PSU_UPDATE_SERVICE)}; 370 } 371 service.insert(p + 1, args); 372 return service; 373 } 374 375 void ActivationBlocksTransition::enableRebootGuard() 376 { 377 lg2::info("PSU image activating - BMC reboots are disabled."); 378 379 auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH, 380 SYSTEMD_INTERFACE, "StartUnit"); 381 method.append("reboot-guard-enable.service", "replace"); 382 bus.call_noreply_noerror(method); 383 } 384 385 void ActivationBlocksTransition::disableRebootGuard() 386 { 387 lg2::info("PSU activation has ended - BMC reboots are re-enabled."); 388 389 auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH, 390 SYSTEMD_INTERFACE, "StartUnit"); 391 method.append("reboot-guard-disable.service", "replace"); 392 bus.call_noreply_noerror(method); 393 } 394 395 } // namespace updater 396 } // namespace software 397 } // namespace phosphor 398