1 #include "config.h"
2 
3 #include "activation.hpp"
4 
5 #include "utils.hpp"
6 
7 #include <cassert>
8 #include <filesystem>
9 #include <phosphor-logging/elog-errors.hpp>
10 #include <phosphor-logging/log.hpp>
11 
12 namespace phosphor
13 {
14 namespace software
15 {
16 namespace updater
17 {
18 
19 constexpr auto SYSTEMD_BUSNAME = "org.freedesktop.systemd1";
20 constexpr auto SYSTEMD_PATH = "/org/freedesktop/systemd1";
21 constexpr auto SYSTEMD_INTERFACE = "org.freedesktop.systemd1.Manager";
22 
23 namespace fs = std::filesystem;
24 namespace softwareServer = sdbusplus::xyz::openbmc_project::Software::server;
25 
26 using namespace phosphor::logging;
27 using SoftwareActivation = softwareServer::Activation;
28 
29 auto Activation::activation(Activations value) -> Activations
30 {
31     if (value == Status::Activating)
32     {
33         value = startActivation();
34     }
35     else
36     {
37         activationBlocksTransition.reset();
38         activationProgress.reset();
39     }
40 
41     return SoftwareActivation::activation(value);
42 }
43 
44 auto Activation::requestedActivation(RequestedActivations value)
45     -> RequestedActivations
46 {
47     if ((value == SoftwareActivation::RequestedActivations::Active) &&
48         (SoftwareActivation::requestedActivation() !=
49          SoftwareActivation::RequestedActivations::Active))
50     {
51         // PSU image could be activated even when it's in active,
52         // e.g. in case a PSU is replaced and has a older image, it will be
53         // updated with the running PSU image that is stored in BMC.
54         if ((activation() == Status::Ready) ||
55             (activation() == Status::Failed) || activation() == Status::Active)
56         {
57             activation(Status::Activating);
58         }
59     }
60     return SoftwareActivation::requestedActivation(value);
61 }
62 
63 void Activation::unitStateChange(sdbusplus::message::message& msg)
64 {
65     uint32_t newStateID{};
66     sdbusplus::message::object_path newStateObjPath;
67     std::string newStateUnit{};
68     std::string newStateResult{};
69 
70     // Read the msg and populate each variable
71     msg.read(newStateID, newStateObjPath, newStateUnit, newStateResult);
72 
73     if (newStateUnit == psuUpdateUnit)
74     {
75         if (newStateResult == "done")
76         {
77             onUpdateDone();
78         }
79         if (newStateResult == "failed" || newStateResult == "dependency")
80         {
81             onUpdateFailed();
82         }
83     }
84 }
85 
86 bool Activation::doUpdate(const std::string& psuInventoryPath)
87 {
88     currentUpdatingPsu = psuInventoryPath;
89     psuUpdateUnit = getUpdateService(currentUpdatingPsu);
90     try
91     {
92         auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH,
93                                           SYSTEMD_INTERFACE, "StartUnit");
94         method.append(psuUpdateUnit, "replace");
95         bus.call_noreply(method);
96         return true;
97     }
98     catch (const sdbusplus::exception::exception& e)
99     {
100         log<level::ERR>("Error staring service", entry("ERROR=%s", e.what()));
101         onUpdateFailed();
102         return false;
103     }
104 }
105 
106 bool Activation::doUpdate()
107 {
108     // When the queue is empty, all updates are done
109     if (psuQueue.empty())
110     {
111         finishActivation();
112         return true;
113     }
114 
115     // Do the update on a PSU
116     const auto& psu = psuQueue.front();
117     return doUpdate(psu);
118 }
119 
120 void Activation::onUpdateDone()
121 {
122     auto progress = activationProgress->progress() + progressStep;
123     activationProgress->progress(progress);
124 
125     // Update the activation association
126     auto assocs = associations();
127     assocs.emplace_back(ACTIVATION_FWD_ASSOCIATION, ACTIVATION_REV_ASSOCIATION,
128                         currentUpdatingPsu);
129     associations(assocs);
130 
131     activationListener->onUpdateDone(versionId, currentUpdatingPsu);
132     currentUpdatingPsu.clear();
133 
134     psuQueue.pop();
135     doUpdate(); // Update the next psu
136 }
137 
138 void Activation::onUpdateFailed()
139 {
140     // TODO: report an event
141     log<level::ERR>("Failed to udpate PSU",
142                     entry("PSU=%s", psuQueue.front().c_str()));
143     std::queue<std::string>().swap(psuQueue); // Clear the queue
144     activation(Status::Failed);
145 }
146 
147 Activation::Status Activation::startActivation()
148 {
149     // Check if the activation has file path
150     if (path().empty())
151     {
152         log<level::WARNING>("No image for the activation, skipped",
153                             entry("VERSION_ID=%s", versionId.c_str()));
154         return activation(); // Return the previous activation status
155     }
156 
157     auto psuPaths = utils::getPSUInventoryPath(bus);
158     if (psuPaths.empty())
159     {
160         log<level::WARNING>("No PSU inventory found");
161         return Status::Failed;
162     }
163 
164     for (const auto& p : psuPaths)
165     {
166         if (isCompatible(p))
167         {
168             if (utils::isAssociated(p, associations()))
169             {
170                 log<level::NOTICE>("PSU already running the image, skipping",
171                                    entry("PSU=%s", p.c_str()));
172                 continue;
173             }
174             psuQueue.push(p);
175         }
176         else
177         {
178             log<level::NOTICE>("PSU not compatible",
179                                entry("PSU=%s", p.c_str()));
180         }
181     }
182 
183     if (psuQueue.empty())
184     {
185         log<level::WARNING>("No PSU compatible with the software");
186         return activation(); // Return the previous activation status
187     }
188 
189     if (!activationProgress)
190     {
191         activationProgress = std::make_unique<ActivationProgress>(bus, objPath);
192     }
193     if (!activationBlocksTransition)
194     {
195         activationBlocksTransition =
196             std::make_unique<ActivationBlocksTransition>(bus, objPath);
197     }
198 
199     // The progress to be increased for each successful update of PSU
200     // E.g. in case we have 4 PSUs:
201     //   1. Initial progrss is 10
202     //   2. Add 20 after each update is done, so we will see progress to be 30,
203     //      50, 70, 90
204     //   3. When all PSUs are updated, it will be 100 and the interface is
205     //   removed.
206     progressStep = 80 / psuQueue.size();
207     if (doUpdate())
208     {
209         activationProgress->progress(10);
210         return Status::Activating;
211     }
212     else
213     {
214         return Status::Failed;
215     }
216 }
217 
218 void Activation::finishActivation()
219 {
220     storeImage();
221     activationProgress->progress(100);
222 
223     deleteImageManagerObject();
224 
225     associationInterface->createActiveAssociation(objPath);
226     associationInterface->addFunctionalAssociation(objPath);
227     associationInterface->addUpdateableAssociation(objPath);
228 
229     // Reset RequestedActivations to none so that it could be activated in
230     // future
231     requestedActivation(SoftwareActivation::RequestedActivations::None);
232     activation(Status::Active);
233 }
234 
235 void Activation::deleteImageManagerObject()
236 {
237     // Get the Delete object for <versionID> inside image_manager
238     constexpr auto versionServiceStr = "xyz.openbmc_project.Software.Version";
239     constexpr auto deleteInterface = "xyz.openbmc_project.Object.Delete";
240     std::string versionService;
241     auto services = utils::getServices(bus, objPath.c_str(), deleteInterface);
242 
243     // We need to find the phosphor-version-software-manager's version service
244     // to invoke the delete interface
245     for (const auto& service : services)
246     {
247         if (service.find(versionServiceStr) != std::string::npos)
248         {
249             versionService = service;
250             break;
251         }
252     }
253     if (versionService.empty())
254     {
255         // When updating a stored image, there is no version object created by
256         // "xyz.openbmc_project.Software.Version" service, so skip it.
257         return;
258     }
259 
260     // Call the Delete object for <versionID> inside image_manager
261     auto method = bus.new_method_call(versionService.c_str(), objPath.c_str(),
262                                       deleteInterface, "Delete");
263     try
264     {
265         bus.call(method);
266     }
267     catch (const sdbusplus::exception::exception& e)
268     {
269         log<level::ERR>("Error performing call to Delete object path",
270                         entry("ERROR=%s", e.what()),
271                         entry("PATH=%s", objPath.c_str()));
272     }
273 }
274 
275 bool Activation::isCompatible(const std::string& psuInventoryPath)
276 {
277     auto service =
278         utils::getService(bus, psuInventoryPath.c_str(), ASSET_IFACE);
279     auto psuManufacturer = utils::getProperty<std::string>(
280         bus, service.c_str(), psuInventoryPath.c_str(), ASSET_IFACE,
281         MANUFACTURER);
282     auto psuModel = utils::getProperty<std::string>(
283         bus, service.c_str(), psuInventoryPath.c_str(), ASSET_IFACE, MODEL);
284     if (psuModel != model)
285     {
286         // The model shall match
287         return false;
288     }
289     if (!psuManufacturer.empty())
290     {
291         // If PSU inventory has manufacturer property, it shall match
292         return psuManufacturer == manufacturer;
293     }
294     return true;
295 }
296 
297 void Activation::storeImage()
298 {
299     // Store image in persistent dir separated by model
300     // and only store the latest one by removing old ones
301     auto src = path();
302     auto dst = fs::path(IMG_DIR_PERSIST) / model;
303     if (src == dst)
304     {
305         // This happens when updating an stored image, no need to store it again
306         return;
307     }
308     try
309     {
310         fs::remove_all(dst);
311         fs::create_directories(dst);
312         fs::copy(src, dst);
313         path(dst.string()); // Update the FilePath interface
314     }
315     catch (const fs::filesystem_error& e)
316     {
317         log<level::ERR>("Error storing PSU image", entry("ERROR=%s", e.what()),
318                         entry("SRC=%s", src.c_str()),
319                         entry("DST=%s", dst.c_str()));
320     }
321 }
322 
323 std::string Activation::getUpdateService(const std::string& psuInventoryPath)
324 {
325     fs::path imagePath(path());
326 
327     // The systemd unit shall be escaped
328     std::string args = psuInventoryPath;
329     args += "\\x20";
330     args += imagePath;
331     std::replace(args.begin(), args.end(), '/', '-');
332 
333     std::string service = PSU_UPDATE_SERVICE;
334     auto p = service.find('@');
335     assert(p != std::string::npos);
336     service.insert(p + 1, args);
337     return service;
338 }
339 
340 void ActivationBlocksTransition::enableRebootGuard()
341 {
342     log<level::INFO>("PSU image activating - BMC reboots are disabled.");
343 
344     auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH,
345                                       SYSTEMD_INTERFACE, "StartUnit");
346     method.append("reboot-guard-enable.service", "replace");
347     bus.call_noreply_noerror(method);
348 }
349 
350 void ActivationBlocksTransition::disableRebootGuard()
351 {
352     log<level::INFO>("PSU activation has ended - BMC reboots are re-enabled.");
353 
354     auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH,
355                                       SYSTEMD_INTERFACE, "StartUnit");
356     method.append("reboot-guard-disable.service", "replace");
357     bus.call_noreply_noerror(method);
358 }
359 
360 } // namespace updater
361 } // namespace software
362 } // namespace phosphor
363