xref: /openbmc/phosphor-psu-code-mgmt/src/activation.cpp (revision 56760ee2d42710153a9d8d9d73f537a326cb0e7c)
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