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