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