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