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