1 #include "config.h"
2
3 #include "item_updater.hpp"
4
5 #include "runtime_warning.hpp"
6 #include "utils.hpp"
7
8 #include <phosphor-logging/elog-errors.hpp>
9 #include <phosphor-logging/lg2.hpp>
10 #include <xyz/openbmc_project/Common/error.hpp>
11
12 #include <exception>
13 #include <filesystem>
14 #include <format>
15 #include <set>
16 #include <stdexcept>
17
18 namespace
19 {
20 constexpr auto MANIFEST_VERSION = "version";
21 constexpr auto MANIFEST_EXTENDED_VERSION = "extended_version";
22 } // namespace
23
24 namespace phosphor
25 {
26 namespace software
27 {
28 namespace updater
29 {
30 namespace server = sdbusplus::xyz::openbmc_project::Software::server;
31
32 using namespace sdbusplus::xyz::openbmc_project::Common::Error;
33 using namespace phosphor::logging;
34 using SVersion = server::Version;
35 using VersionPurpose = SVersion::VersionPurpose;
36
onVersionInterfacesAddedMsg(sdbusplus::message_t & msg)37 void ItemUpdater::onVersionInterfacesAddedMsg(sdbusplus::message_t& msg)
38 {
39 try
40 {
41 sdbusplus::message::object_path objPath;
42 InterfacesAddedMap interfaces;
43 msg.read(objPath, interfaces);
44
45 std::string path(std::move(objPath));
46 onVersionInterfacesAdded(path, interfaces);
47 }
48 catch (const std::exception& e)
49 {
50 lg2::error("Unable to handle version InterfacesAdded event: {ERROR}",
51 "ERROR", e);
52 }
53 }
54
onVersionInterfacesAdded(const std::string & path,const InterfacesAddedMap & interfaces)55 void ItemUpdater::onVersionInterfacesAdded(const std::string& path,
56 const InterfacesAddedMap& interfaces)
57 {
58 std::string filePath;
59 auto purpose = VersionPurpose::Unknown;
60 std::string version;
61
62 for (const auto& [interfaceName, propertyMap] : interfaces)
63 {
64 if (interfaceName == VERSION_IFACE)
65 {
66 for (const auto& [propertyName, propertyValue] : propertyMap)
67 {
68 if (propertyName == "Purpose")
69 {
70 // Only process the PSU images
71 auto value = SVersion::convertVersionPurposeFromString(
72 std::get<std::string>(propertyValue));
73
74 if (value == VersionPurpose::PSU)
75 {
76 purpose = value;
77 }
78 }
79 else if (propertyName == VERSION)
80 {
81 version = std::get<std::string>(propertyValue);
82 }
83 }
84 }
85 else if (interfaceName == FILEPATH_IFACE)
86 {
87 const auto& it = propertyMap.find("Path");
88 if (it != propertyMap.end())
89 {
90 filePath = std::get<std::string>(it->second);
91 }
92 }
93 }
94 if ((filePath.empty()) || (purpose == VersionPurpose::Unknown))
95 {
96 return;
97 }
98
99 // If we are only installing PSU images from the built-in directory, ignore
100 // PSU images from other directories
101 if (ALWAYS_USE_BUILTIN_IMG_DIR && !filePath.starts_with(IMG_DIR_BUILTIN))
102 {
103 return;
104 }
105
106 // Version id is the last item in the path
107 auto pos = path.rfind('/');
108 if (pos == std::string::npos)
109 {
110 lg2::error("No version id found in object path {OBJPATH}", "OBJPATH",
111 path);
112 return;
113 }
114
115 auto versionId = path.substr(pos + 1);
116
117 if (activations.find(versionId) == activations.end())
118 {
119 // Determine the Activation state by processing the given image dir.
120 AssociationList associations;
121 auto activationState = Activation::Status::Ready;
122
123 associations.emplace_back(std::make_tuple(
124 ACTIVATION_FWD_ASSOCIATION, ACTIVATION_REV_ASSOCIATION,
125 PSU_INVENTORY_PATH_BASE));
126
127 fs::path manifestPath(filePath);
128 manifestPath /= MANIFEST_FILE;
129 std::string extendedVersion =
130 Version::getValue(manifestPath, {MANIFEST_EXTENDED_VERSION});
131
132 auto activation =
133 createActivationObject(path, versionId, extendedVersion,
134 activationState, associations, filePath);
135 activations.emplace(versionId, std::move(activation));
136
137 auto versionPtr =
138 createVersionObject(path, versionId, version, purpose);
139 versions.emplace(versionId, std::move(versionPtr));
140 }
141 }
142
erase(const std::string & versionId)143 void ItemUpdater::erase(const std::string& versionId)
144 {
145 auto it = versions.find(versionId);
146 if (it == versions.end())
147 {
148 lg2::error("Error: Failed to find version {VERSION_ID} in "
149 "item updater versions map. Unable to remove.",
150 "VERSION_ID", versionId);
151 }
152 else
153 {
154 versionStrings.erase(it->second->getVersionString());
155 versions.erase(it);
156 }
157
158 // Removing entry in activations map
159 auto ita = activations.find(versionId);
160 if (ita == activations.end())
161 {
162 lg2::error("Error: Failed to find version {VERSION_ID} in "
163 "item updater activations map. Unable to remove.",
164 "VERSION_ID", versionId);
165 }
166 else
167 {
168 activations.erase(versionId);
169 }
170 }
171
createActiveAssociation(const std::string & path)172 void ItemUpdater::createActiveAssociation(const std::string& path)
173 {
174 assocs.emplace_back(
175 std::make_tuple(ACTIVE_FWD_ASSOCIATION, ACTIVE_REV_ASSOCIATION, path));
176 associations(assocs);
177 }
178
addFunctionalAssociation(const std::string & path)179 void ItemUpdater::addFunctionalAssociation(const std::string& path)
180 {
181 assocs.emplace_back(std::make_tuple(FUNCTIONAL_FWD_ASSOCIATION,
182 FUNCTIONAL_REV_ASSOCIATION, path));
183 associations(assocs);
184 }
185
addUpdateableAssociation(const std::string & path)186 void ItemUpdater::addUpdateableAssociation(const std::string& path)
187 {
188 assocs.emplace_back(std::make_tuple(UPDATEABLE_FWD_ASSOCIATION,
189 UPDATEABLE_REV_ASSOCIATION, path));
190 associations(assocs);
191 }
192
removeAssociation(const std::string & path)193 void ItemUpdater::removeAssociation(const std::string& path)
194 {
195 for (auto iter = assocs.begin(); iter != assocs.end();)
196 {
197 if ((std::get<2>(*iter)) == path)
198 {
199 iter = assocs.erase(iter);
200 associations(assocs);
201 }
202 else
203 {
204 ++iter;
205 }
206 }
207 }
208
onUpdateDone(const std::string & versionId,const std::string & psuInventoryPath)209 void ItemUpdater::onUpdateDone(const std::string& versionId,
210 const std::string& psuInventoryPath)
211 {
212 // After update is done, remove old activation objects
213 for (auto it = activations.begin(); it != activations.end(); ++it)
214 {
215 if (it->second->getVersionId() != versionId &&
216 utils::isAssociated(psuInventoryPath, it->second->associations()))
217 {
218 removePsuObject(psuInventoryPath);
219 break;
220 }
221 }
222
223 auto it = activations.find(versionId);
224 if (it == activations.end())
225 {
226 lg2::error("Unable to find Activation for version ID {VERSION_ID}",
227 "VERSION_ID", versionId);
228 }
229 else
230 {
231 psuPathActivationMap.emplace(psuInventoryPath, it->second);
232 }
233 }
234
createActivationObject(const std::string & path,const std::string & versionId,const std::string & extVersion,Activation::Status activationStatus,const AssociationList & assocs,const std::string & filePath)235 std::unique_ptr<Activation> ItemUpdater::createActivationObject(
236 const std::string& path, const std::string& versionId,
237 const std::string& extVersion, Activation::Status activationStatus,
238 const AssociationList& assocs, const std::string& filePath)
239 {
240 return std::make_unique<Activation>(bus, path, versionId, extVersion,
241 activationStatus, assocs, filePath,
242 this, this);
243 }
244
createPsuObject(const std::string & psuInventoryPath,const std::string & psuVersion)245 void ItemUpdater::createPsuObject(const std::string& psuInventoryPath,
246 const std::string& psuVersion)
247 {
248 auto versionId = utils::getVersionId(psuVersion);
249 auto path = std::string(SOFTWARE_OBJPATH) + "/" + versionId;
250
251 auto it = activations.find(versionId);
252 if (it != activations.end())
253 {
254 // The versionId is already created, associate the path
255 auto associations = it->second->associations();
256 associations.emplace_back(
257 std::make_tuple(ACTIVATION_FWD_ASSOCIATION,
258 ACTIVATION_REV_ASSOCIATION, psuInventoryPath));
259 it->second->associations(associations);
260 psuPathActivationMap.emplace(psuInventoryPath, it->second);
261 }
262 else
263 {
264 // Create a new object for running PSU inventory
265 AssociationList associations;
266 auto activationState = Activation::Status::Active;
267
268 associations.emplace_back(
269 std::make_tuple(ACTIVATION_FWD_ASSOCIATION,
270 ACTIVATION_REV_ASSOCIATION, psuInventoryPath));
271
272 auto activation = createActivationObject(
273 path, versionId, "", activationState, associations, "");
274 activations.emplace(versionId, std::move(activation));
275 psuPathActivationMap.emplace(psuInventoryPath, activations[versionId]);
276
277 auto versionPtr = createVersionObject(path, versionId, psuVersion,
278 VersionPurpose::PSU);
279 versions.emplace(versionId, std::move(versionPtr));
280
281 createActiveAssociation(path);
282 addFunctionalAssociation(path);
283 addUpdateableAssociation(path);
284 }
285 }
286
removePsuObject(const std::string & psuInventoryPath)287 void ItemUpdater::removePsuObject(const std::string& psuInventoryPath)
288 {
289 auto it = psuPathActivationMap.find(psuInventoryPath);
290 if (it == psuPathActivationMap.end())
291 {
292 lg2::error("No Activation found for PSU {PSUPATH}", "PSUPATH",
293 psuInventoryPath);
294 return;
295 }
296 const auto& activationPtr = it->second;
297 psuPathActivationMap.erase(psuInventoryPath);
298
299 auto associations = activationPtr->associations();
300 for (auto iter = associations.begin(); iter != associations.end();)
301 {
302 if ((std::get<2>(*iter)) == psuInventoryPath)
303 {
304 iter = associations.erase(iter);
305 }
306 else
307 {
308 ++iter;
309 }
310 }
311 if (associations.empty())
312 {
313 // Remove the activation
314 erase(activationPtr->getVersionId());
315 }
316 else
317 {
318 // Update association
319 activationPtr->associations(associations);
320 }
321 }
322
addPsuToStatusMap(const std::string & psuPath)323 void ItemUpdater::addPsuToStatusMap(const std::string& psuPath)
324 {
325 if (!psuStatusMap.contains(psuPath))
326 {
327 psuStatusMap[psuPath] = {false, ""};
328
329 // Add PropertiesChanged listener for Item interface so we are notified
330 // when Present property changes
331 psuMatches.emplace_back(
332 bus, MatchRules::propertiesChanged(psuPath, ITEM_IFACE),
333 std::bind(&ItemUpdater::onPsuInventoryChangedMsg, this,
334 std::placeholders::_1));
335 }
336 }
337
handlePSUPresenceChanged(const std::string & psuPath)338 void ItemUpdater::handlePSUPresenceChanged(const std::string& psuPath)
339 {
340 if (psuStatusMap.contains(psuPath))
341 {
342 if (psuStatusMap[psuPath].present)
343 {
344 // PSU is now present
345 psuStatusMap[psuPath].model = utils::getModel(psuPath);
346 auto version = utils::getVersion(psuPath);
347 if (!version.empty() && !psuPathActivationMap.contains(psuPath))
348 {
349 createPsuObject(psuPath, version);
350 }
351 }
352 else
353 {
354 // PSU is now missing
355 psuStatusMap[psuPath].model.clear();
356 if (psuPathActivationMap.contains(psuPath))
357 {
358 removePsuObject(psuPath);
359 }
360 }
361 }
362 }
363
createVersionObject(const std::string & objPath,const std::string & versionId,const std::string & versionString,sdbusplus::xyz::openbmc_project::Software::server::Version::VersionPurpose versionPurpose)364 std::unique_ptr<Version> ItemUpdater::createVersionObject(
365 const std::string& objPath, const std::string& versionId,
366 const std::string& versionString,
367 sdbusplus::xyz::openbmc_project::Software::server::Version::VersionPurpose
368 versionPurpose)
369 {
370 versionStrings.insert(versionString);
371 auto version = std::make_unique<Version>(
372 bus, objPath, versionId, versionString, versionPurpose,
373 std::bind(&ItemUpdater::erase, this, std::placeholders::_1));
374 return version;
375 }
376
onPsuInventoryChangedMsg(sdbusplus::message_t & msg)377 void ItemUpdater::onPsuInventoryChangedMsg(sdbusplus::message_t& msg)
378 {
379 try
380 {
381 using Interface = std::string;
382 Interface interface;
383 Properties properties;
384 std::string psuPath = msg.get_path();
385
386 msg.read(interface, properties);
387 onPsuInventoryChanged(psuPath, properties);
388 }
389 catch (const std::exception& e)
390 {
391 lg2::error(
392 "Unable to handle inventory PropertiesChanged event: {ERROR}",
393 "ERROR", e);
394 }
395 }
396
onPsuInventoryChanged(const std::string & psuPath,const Properties & properties)397 void ItemUpdater::onPsuInventoryChanged(const std::string& psuPath,
398 const Properties& properties)
399 {
400 if (psuStatusMap.contains(psuPath) && properties.contains(PRESENT))
401 {
402 psuStatusMap[psuPath].present = std::get<bool>(properties.at(PRESENT));
403 handlePSUPresenceChanged(psuPath);
404 if (psuStatusMap[psuPath].present)
405 {
406 // Check if there are new PSU images to update
407 processStoredImage();
408 syncToLatestImage();
409 }
410 }
411 }
412
processPSUImage()413 void ItemUpdater::processPSUImage()
414 {
415 try
416 {
417 auto paths = utils::getPSUInventoryPaths(bus);
418 for (const auto& p : paths)
419 {
420 try
421 {
422 addPsuToStatusMap(p);
423 auto service = utils::getService(bus, p.c_str(), ITEM_IFACE);
424 psuStatusMap[p].present = utils::getProperty<bool>(
425 bus, service.c_str(), p.c_str(), ITEM_IFACE, PRESENT);
426 handlePSUPresenceChanged(p);
427 }
428 catch (const std::exception& e)
429 {
430 // Ignore errors; the information might not be available yet
431 }
432 }
433 }
434 catch (const std::exception& e)
435 {
436 // Ignore errors; the information might not be available yet
437 }
438 }
439
processStoredImage()440 void ItemUpdater::processStoredImage()
441 {
442 // Build list of directories to scan
443 std::vector<fs::path> paths;
444 paths.emplace_back(IMG_DIR_BUILTIN);
445 if (!ALWAYS_USE_BUILTIN_IMG_DIR)
446 {
447 paths.emplace_back(IMG_DIR_PERSIST);
448 }
449
450 // Scan directories
451 auto logMsg = "Unable to find PSU firmware in directory {PATH}: {ERROR}";
452 for (const auto& path : paths)
453 {
454 try
455 {
456 scanDirectory(path);
457 }
458 catch (const RuntimeWarning& r)
459 {
460 lg2::warning(logMsg, "PATH", path, "ERROR", r);
461 }
462 catch (const std::exception& e)
463 {
464 lg2::error(logMsg, "PATH", path, "ERROR", e);
465 }
466 }
467 }
468
scanDirectory(const fs::path & dir)469 void ItemUpdater::scanDirectory(const fs::path& dir)
470 {
471 // Find the model subdirectory within the specified directory
472 auto modelDir = findModelDirectory(dir);
473 if (modelDir.empty())
474 {
475 return;
476 }
477
478 // Verify a manifest file exists within the model subdirectory
479 auto manifest = modelDir / MANIFEST_FILE;
480 if (!fs::exists(manifest))
481 {
482 throw std::runtime_error{
483 std::format("Manifest file does not exist: {}", manifest.c_str())};
484 }
485 if (!fs::is_regular_file(manifest))
486 {
487 throw std::runtime_error{
488 std::format("Path is not a file: {}", manifest.c_str())};
489 }
490
491 // Get version, extVersion, and model from manifest file
492 auto ret = Version::getValues(
493 manifest.string(), {MANIFEST_VERSION, MANIFEST_EXTENDED_VERSION});
494 auto version = ret[MANIFEST_VERSION];
495 auto extVersion = ret[MANIFEST_EXTENDED_VERSION];
496 auto info = Version::getExtVersionInfo(extVersion);
497 auto model = info["model"];
498
499 // Verify version and model are valid
500 if (version.empty() || model.empty())
501 {
502 throw std::runtime_error{std::format(
503 "Invalid information in manifest: path={}, version={}, model={}",
504 manifest.c_str(), version, model)};
505 }
506
507 // Verify model from manifest matches the subdirectory name
508 if (modelDir.stem() != model)
509 {
510 throw std::runtime_error{std::format(
511 "Model in manifest does not match path: model={}, path={}", model,
512 modelDir.c_str())};
513 }
514
515 // Found a valid PSU image directory; write path to journal
516 lg2::info("Found PSU firmware image directory: {PATH}", "PATH", modelDir);
517
518 // Calculate version ID and check if an Activation for it exists
519 auto versionId = utils::getVersionId(version);
520 auto it = activations.find(versionId);
521 if (it == activations.end())
522 {
523 // This is a version that is different than the running PSUs
524 auto activationState = Activation::Status::Ready;
525 auto purpose = VersionPurpose::PSU;
526 auto objPath = std::string(SOFTWARE_OBJPATH) + "/" + versionId;
527
528 auto activation = createActivationObject(objPath, versionId, extVersion,
529 activationState, {}, modelDir);
530 activations.emplace(versionId, std::move(activation));
531
532 auto versionPtr =
533 createVersionObject(objPath, versionId, version, purpose);
534 versions.emplace(versionId, std::move(versionPtr));
535 }
536 else
537 {
538 // Activation already exists. It may have been created for code that is
539 // running on one or more PSUs. Set Path and ExtendedVersion properties.
540 // The properties are not set when the Activation is created for code
541 // running on a PSU. The properties are needed to update other PSUs.
542 it->second->path(modelDir);
543 it->second->extendedVersion(extVersion);
544 }
545 }
546
findModelDirectory(const fs::path & dir)547 fs::path ItemUpdater::findModelDirectory(const fs::path& dir)
548 {
549 fs::path modelDir;
550
551 // Verify directory path exists and is a directory
552 if (!fs::exists(dir))
553 {
554 // Warning condition. IMG_DIR_BUILTIN might not be used. IMG_DIR_PERSIST
555 // might not exist if an image from IMG_DIR has not been stored.
556 throw RuntimeWarning{
557 std::format("Directory does not exist: {}", dir.c_str())};
558 }
559 if (!fs::is_directory(dir))
560 {
561 throw std::runtime_error{
562 std::format("Path is not a directory: {}", dir.c_str())};
563 }
564
565 // Get the model name of the PSUs that have been found. Note that we
566 // might not have found the PSU information yet on D-Bus.
567 std::string model;
568 for (const auto& [key, item] : psuStatusMap)
569 {
570 if (!item.model.empty())
571 {
572 model = item.model;
573 break;
574 }
575 }
576 if (!model.empty())
577 {
578 // Verify model subdirectory path exists and is a directory
579 auto subDir = dir / model;
580 if (!fs::exists(subDir))
581 {
582 // Warning condition. Subdirectory may not exist in IMG_DIR_PERSIST
583 // if no image has been stored there. May also not exist if
584 // firmware update is not supported for this PSU model.
585 throw RuntimeWarning{
586 std::format("Directory does not exist: {}", subDir.c_str())};
587 }
588 if (!fs::is_directory(subDir))
589 {
590 throw std::runtime_error{
591 std::format("Path is not a directory: {}", subDir.c_str())};
592 }
593 modelDir = subDir;
594 }
595
596 return modelDir;
597 }
598
getLatestVersionId()599 std::optional<std::string> ItemUpdater::getLatestVersionId()
600 {
601 std::string latestVersion;
602 if (ALWAYS_USE_BUILTIN_IMG_DIR)
603 {
604 latestVersion = getFWVersionFromBuiltinDir();
605 }
606 else
607 {
608 latestVersion = utils::getLatestVersion(versionStrings);
609 }
610 if (latestVersion.empty())
611 {
612 return {};
613 }
614
615 std::optional<std::string> versionId;
616 for (const auto& v : versions)
617 {
618 if (v.second->version() == latestVersion)
619 {
620 versionId = v.first;
621 break;
622 }
623 }
624 if (!versionId.has_value())
625 {
626 lg2::error("Unable to find versionId for latest version {VERSION}",
627 "VERSION", latestVersion);
628 }
629 return versionId;
630 }
631
syncToLatestImage()632 void ItemUpdater::syncToLatestImage()
633 {
634 auto latestVersionId = getLatestVersionId();
635 if (!latestVersionId)
636 {
637 return;
638 }
639 const auto& it = activations.find(*latestVersionId);
640 if (it == activations.end())
641
642 {
643 lg2::error("Unable to find Activation for versionId {VERSION_ID}",
644 "VERSION_ID", *latestVersionId);
645 return;
646 }
647 const auto& activation = it->second;
648 const auto& assocs = activation->associations();
649
650 auto paths = utils::getPSUInventoryPaths(bus);
651 for (const auto& p : paths)
652 {
653 // If there is a present PSU that is not associated with the latest
654 // image, run the activation so that all PSUs are running the same
655 // latest image.
656 if (psuStatusMap.contains(p) && psuStatusMap[p].present)
657 {
658 if (!utils::isAssociated(p, assocs))
659 {
660 lg2::info("Automatically update PSUs to versionId {VERSION_ID}",
661 "VERSION_ID", *latestVersionId);
662 invokeActivation(activation);
663 break;
664 }
665 }
666 }
667 }
668
669 void
invokeActivation(const std::unique_ptr<Activation> & activation)670 ItemUpdater::invokeActivation(const std::unique_ptr<Activation>& activation)
671 {
672 activation->requestedActivation(Activation::RequestedActivations::Active);
673 }
674
onPSUInterfacesAdded(sdbusplus::message_t & msg)675 void ItemUpdater::onPSUInterfacesAdded(sdbusplus::message_t& msg)
676 {
677 // Maintain static set of valid PSU paths. This is needed if PSU interface
678 // comes in a separate InterfacesAdded message from Item interface.
679 static std::set<std::string> psuPaths{};
680
681 try
682 {
683 sdbusplus::message::object_path objPath;
684 InterfacesAddedMap interfaces;
685 msg.read(objPath, interfaces);
686 std::string path = objPath.str;
687
688 if (interfaces.contains(PSU_INVENTORY_IFACE))
689 {
690 psuPaths.insert(path);
691 }
692
693 if (interfaces.contains(ITEM_IFACE) && psuPaths.contains(path) &&
694 !psuStatusMap.contains(path))
695 {
696 auto interface = interfaces[ITEM_IFACE];
697 if (interface.contains(PRESENT))
698 {
699 addPsuToStatusMap(path);
700 psuStatusMap[path].present = std::get<bool>(interface[PRESENT]);
701 handlePSUPresenceChanged(path);
702 if (psuStatusMap[path].present)
703 {
704 // Check if there are new PSU images to update
705 processStoredImage();
706 syncToLatestImage();
707 }
708 }
709 }
710 }
711 catch (const std::exception& e)
712 {
713 lg2::error("Unable to handle inventory InterfacesAdded event: {ERROR}",
714 "ERROR", e);
715 }
716 }
717
processPSUImageAndSyncToLatest()718 void ItemUpdater::processPSUImageAndSyncToLatest()
719 {
720 processPSUImage();
721 processStoredImage();
722 syncToLatestImage();
723 }
724
getFWVersionFromBuiltinDir()725 std::string ItemUpdater::getFWVersionFromBuiltinDir()
726 {
727 std::string version;
728 for (const auto& activation : activations)
729 {
730 if (activation.second->path().starts_with(IMG_DIR_BUILTIN))
731 {
732 std::string versionId = activation.second->getVersionId();
733 auto it = versions.find(versionId);
734 if (it != versions.end())
735 {
736 const auto& versionPtr = it->second;
737 version = versionPtr->version();
738 break;
739 }
740 }
741 }
742 return version;
743 }
744
745 } // namespace updater
746 } // namespace software
747 } // namespace phosphor
748