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