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