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