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/log.hpp>
9 #include <xyz/openbmc_project/Common/error.hpp>
10 
11 #include <cassert>
12 #include <filesystem>
13 
14 namespace
15 {
16 constexpr auto MANIFEST_VERSION = "version";
17 constexpr auto MANIFEST_EXTENDED_VERSION = "extended_version";
18 } // namespace
19 
20 namespace phosphor
21 {
22 namespace software
23 {
24 namespace updater
25 {
26 namespace server = sdbusplus::xyz::openbmc_project::Software::server;
27 
28 using namespace sdbusplus::xyz::openbmc_project::Common::Error;
29 using namespace phosphor::logging;
30 using SVersion = server::Version;
31 using VersionPurpose = SVersion::VersionPurpose;
32 
33 void ItemUpdater::createActivation(sdbusplus::message_t& m)
34 {
35     sdbusplus::message::object_path objPath;
36     std::map<std::string, std::map<std::string, std::variant<std::string>>>
37         interfaces;
38     m.read(objPath, interfaces);
39 
40     std::string path(std::move(objPath));
41     std::string filePath;
42     auto purpose = VersionPurpose::Unknown;
43     std::string version;
44 
45     for (const auto& [interfaceName, propertyMap] : interfaces)
46     {
47         if (interfaceName == VERSION_IFACE)
48         {
49             for (const auto& [propertyName, propertyValue] : propertyMap)
50             {
51                 if (propertyName == "Purpose")
52                 {
53                     // Only process the PSU images
54                     auto value = SVersion::convertVersionPurposeFromString(
55                         std::get<std::string>(propertyValue));
56 
57                     if (value == VersionPurpose::PSU)
58                     {
59                         purpose = value;
60                     }
61                 }
62                 else if (propertyName == VERSION)
63                 {
64                     version = std::get<std::string>(propertyValue);
65                 }
66             }
67         }
68         else if (interfaceName == FILEPATH_IFACE)
69         {
70             const auto& it = propertyMap.find("Path");
71             if (it != propertyMap.end())
72             {
73                 filePath = std::get<std::string>(it->second);
74             }
75         }
76     }
77     if ((filePath.empty()) || (purpose == VersionPurpose::Unknown))
78     {
79         return;
80     }
81 
82     // Version id is the last item in the path
83     auto pos = path.rfind("/");
84     if (pos == std::string::npos)
85     {
86         log<level::ERR>("No version id found in object path",
87                         entry("OBJPATH=%s", path.c_str()));
88         return;
89     }
90 
91     auto versionId = path.substr(pos + 1);
92 
93     if (activations.find(versionId) == activations.end())
94     {
95         // Determine the Activation state by processing the given image dir.
96         AssociationList associations;
97         auto activationState = Activation::Status::Ready;
98 
99         associations.emplace_back(std::make_tuple(ACTIVATION_FWD_ASSOCIATION,
100                                                   ACTIVATION_REV_ASSOCIATION,
101                                                   PSU_INVENTORY_PATH_BASE));
102 
103         fs::path manifestPath(filePath);
104         manifestPath /= MANIFEST_FILE;
105         std::string extendedVersion =
106             Version::getValue(manifestPath, {MANIFEST_EXTENDED_VERSION});
107 
108         auto activation =
109             createActivationObject(path, versionId, extendedVersion,
110                                    activationState, associations, filePath);
111         activations.emplace(versionId, std::move(activation));
112 
113         auto versionPtr = createVersionObject(path, versionId, version,
114                                               purpose);
115         versions.emplace(versionId, std::move(versionPtr));
116     }
117     return;
118 }
119 
120 void ItemUpdater::erase(const std::string& versionId)
121 {
122     auto it = versions.find(versionId);
123     if (it == versions.end())
124     {
125         log<level::ERR>(("Error: Failed to find version " + versionId +
126                          " in item updater versions map."
127                          " Unable to remove.")
128                             .c_str());
129     }
130     else
131     {
132         versionStrings.erase(it->second->getVersionString());
133         versions.erase(it);
134     }
135 
136     // Removing entry in activations map
137     auto ita = activations.find(versionId);
138     if (ita == activations.end())
139     {
140         log<level::ERR>(("Error: Failed to find version " + versionId +
141                          " in item updater activations map."
142                          " Unable to remove.")
143                             .c_str());
144     }
145     else
146     {
147         activations.erase(versionId);
148     }
149 }
150 
151 void ItemUpdater::createActiveAssociation(const std::string& path)
152 {
153     assocs.emplace_back(
154         std::make_tuple(ACTIVE_FWD_ASSOCIATION, ACTIVE_REV_ASSOCIATION, path));
155     associations(assocs);
156 }
157 
158 void ItemUpdater::addFunctionalAssociation(const std::string& path)
159 {
160     assocs.emplace_back(std::make_tuple(FUNCTIONAL_FWD_ASSOCIATION,
161                                         FUNCTIONAL_REV_ASSOCIATION, path));
162     associations(assocs);
163 }
164 
165 void ItemUpdater::addUpdateableAssociation(const std::string& path)
166 {
167     assocs.emplace_back(std::make_tuple(UPDATEABLE_FWD_ASSOCIATION,
168                                         UPDATEABLE_REV_ASSOCIATION, path));
169     associations(assocs);
170 }
171 
172 void ItemUpdater::removeAssociation(const std::string& path)
173 {
174     for (auto iter = assocs.begin(); iter != assocs.end();)
175     {
176         if ((std::get<2>(*iter)).compare(path) == 0)
177         {
178             iter = assocs.erase(iter);
179             associations(assocs);
180         }
181         else
182         {
183             ++iter;
184         }
185     }
186 }
187 
188 void ItemUpdater::onUpdateDone(const std::string& versionId,
189                                const std::string& psuInventoryPath)
190 {
191     // After update is done, remove old activation objects
192     for (auto it = activations.begin(); it != activations.end(); ++it)
193     {
194         if (it->second->getVersionId() != versionId &&
195             utils::isAssociated(psuInventoryPath, it->second->associations()))
196         {
197             removePsuObject(psuInventoryPath);
198             break;
199         }
200     }
201 
202     auto it = activations.find(versionId);
203     assert(it != activations.end());
204     psuPathActivationMap.emplace(psuInventoryPath, it->second);
205 }
206 
207 std::unique_ptr<Activation> ItemUpdater::createActivationObject(
208     const std::string& path, const std::string& versionId,
209     const std::string& extVersion, Activation::Status activationStatus,
210     const AssociationList& assocs, const std::string& filePath)
211 {
212     return std::make_unique<Activation>(bus, path, versionId, extVersion,
213                                         activationStatus, assocs, filePath,
214                                         this, this);
215 }
216 
217 void ItemUpdater::createPsuObject(const std::string& psuInventoryPath,
218                                   const std::string& psuVersion)
219 {
220     auto versionId = utils::getVersionId(psuVersion);
221     auto path = std::string(SOFTWARE_OBJPATH) + "/" + versionId;
222 
223     auto it = activations.find(versionId);
224     if (it != activations.end())
225     {
226         // The versionId is already created, associate the path
227         auto associations = it->second->associations();
228         associations.emplace_back(std::make_tuple(ACTIVATION_FWD_ASSOCIATION,
229                                                   ACTIVATION_REV_ASSOCIATION,
230                                                   psuInventoryPath));
231         it->second->associations(associations);
232         psuPathActivationMap.emplace(psuInventoryPath, it->second);
233     }
234     else
235     {
236         // Create a new object for running PSU inventory
237         AssociationList associations;
238         auto activationState = Activation::Status::Active;
239 
240         associations.emplace_back(std::make_tuple(ACTIVATION_FWD_ASSOCIATION,
241                                                   ACTIVATION_REV_ASSOCIATION,
242                                                   psuInventoryPath));
243 
244         auto activation = createActivationObject(
245             path, versionId, "", activationState, associations, "");
246         activations.emplace(versionId, std::move(activation));
247         psuPathActivationMap.emplace(psuInventoryPath, activations[versionId]);
248 
249         auto versionPtr = createVersionObject(path, versionId, psuVersion,
250                                               VersionPurpose::PSU);
251         versions.emplace(versionId, std::move(versionPtr));
252 
253         createActiveAssociation(path);
254         addFunctionalAssociation(path);
255         addUpdateableAssociation(path);
256     }
257 }
258 
259 void ItemUpdater::removePsuObject(const std::string& psuInventoryPath)
260 {
261     psuStatusMap[psuInventoryPath] = {false, ""};
262 
263     auto it = psuPathActivationMap.find(psuInventoryPath);
264     if (it == psuPathActivationMap.end())
265     {
266         log<level::ERR>("No Activation found for PSU",
267                         entry("PSUPATH=%s", psuInventoryPath.c_str()));
268         return;
269     }
270     const auto& activationPtr = it->second;
271     psuPathActivationMap.erase(psuInventoryPath);
272 
273     auto associations = activationPtr->associations();
274     for (auto iter = associations.begin(); iter != associations.end();)
275     {
276         if ((std::get<2>(*iter)).compare(psuInventoryPath) == 0)
277         {
278             iter = associations.erase(iter);
279         }
280         else
281         {
282             ++iter;
283         }
284     }
285     if (associations.empty())
286     {
287         // Remove the activation
288         erase(activationPtr->getVersionId());
289     }
290     else
291     {
292         // Update association
293         activationPtr->associations(associations);
294     }
295 }
296 
297 std::unique_ptr<Version> ItemUpdater::createVersionObject(
298     const std::string& objPath, const std::string& versionId,
299     const std::string& versionString,
300     sdbusplus::xyz::openbmc_project::Software::server::Version::VersionPurpose
301         versionPurpose)
302 {
303     versionStrings.insert(versionString);
304     auto version = std::make_unique<Version>(
305         bus, objPath, versionId, versionString, versionPurpose,
306         std::bind(&ItemUpdater::erase, this, std::placeholders::_1));
307     return version;
308 }
309 
310 void ItemUpdater::onPsuInventoryChangedMsg(sdbusplus::message_t& msg)
311 {
312     using Interface = std::string;
313     Interface interface;
314     Properties properties;
315     std::string psuPath = msg.get_path();
316 
317     msg.read(interface, properties);
318     onPsuInventoryChanged(psuPath, properties);
319 }
320 
321 void ItemUpdater::onPsuInventoryChanged(const std::string& psuPath,
322                                         const Properties& properties)
323 {
324     std::optional<bool> present;
325     std::optional<std::string> model;
326 
327     // The code was expecting to get callback on multiple properties changed.
328     // But in practice, the callback is received one-by-one for each property.
329     // So it has to handle Present and Version property separately.
330     auto p = properties.find(PRESENT);
331     if (p != properties.end())
332     {
333         present = std::get<bool>(p->second);
334         psuStatusMap[psuPath].present = *present;
335     }
336     p = properties.find(MODEL);
337     if (p != properties.end())
338     {
339         model = std::get<std::string>(p->second);
340         psuStatusMap[psuPath].model = *model;
341     }
342 
343     // If present or model is not changed, ignore
344     if (!present.has_value() && !model.has_value())
345     {
346         return;
347     }
348 
349     if (psuStatusMap[psuPath].present)
350     {
351         // If model is not updated, let's wait for it
352         if (psuStatusMap[psuPath].model.empty())
353         {
354             log<level::DEBUG>("Waiting for model to be updated");
355             return;
356         }
357 
358         auto version = utils::getVersion(psuPath);
359         if (!version.empty())
360         {
361             createPsuObject(psuPath, version);
362             // Check if there is new PSU images to update
363             syncToLatestImage();
364         }
365         else
366         {
367             // TODO: log an event
368             log<level::ERR>("Failed to get PSU version",
369                             entry("PSU=%s", psuPath.c_str()));
370         }
371     }
372     else
373     {
374         if (!present.has_value())
375         {
376             // If a PSU is plugged out, model property is update to empty as
377             // well, and we get callback here, but ignore that because it is
378             // handled by "Present" callback.
379             return;
380         }
381         // Remove object or association
382         removePsuObject(psuPath);
383     }
384 }
385 
386 void ItemUpdater::processPSUImage()
387 {
388     auto paths = utils::getPSUInventoryPath(bus);
389     for (const auto& p : paths)
390     {
391         auto service = utils::getService(bus, p.c_str(), ITEM_IFACE);
392         auto present = utils::getProperty<bool>(bus, service.c_str(), p.c_str(),
393                                                 ITEM_IFACE, PRESENT);
394         auto version = utils::getVersion(p);
395         if (present && !version.empty())
396         {
397             createPsuObject(p, version);
398         }
399         // Add matches for PSU Inventory's property changes
400         psuMatches.emplace_back(
401             bus, MatchRules::propertiesChanged(p, ITEM_IFACE),
402             std::bind(&ItemUpdater::onPsuInventoryChangedMsg, this,
403                       std::placeholders::_1)); // For present
404         psuMatches.emplace_back(
405             bus, MatchRules::propertiesChanged(p, ASSET_IFACE),
406             std::bind(&ItemUpdater::onPsuInventoryChangedMsg, this,
407                       std::placeholders::_1)); // For model
408     }
409 }
410 
411 void ItemUpdater::processStoredImage()
412 {
413     scanDirectory(IMG_DIR_BUILTIN);
414     scanDirectory(IMG_DIR_PERSIST);
415 }
416 
417 void ItemUpdater::scanDirectory(const fs::path& dir)
418 {
419     // The directory shall put PSU images in directories named with model
420     if (!fs::exists(dir))
421     {
422         // Skip
423         return;
424     }
425     if (!fs::is_directory(dir))
426     {
427         log<level::ERR>("The path is not a directory",
428                         entry("PATH=%s", dir.c_str()));
429         return;
430     }
431     for (const auto& d : fs::directory_iterator(dir))
432     {
433         // If the model in manifest does not match the dir name
434         // Log a warning and skip it
435         auto path = d.path();
436         auto manifest = path / MANIFEST_FILE;
437         if (fs::exists(manifest))
438         {
439             auto ret = Version::getValues(
440                 manifest.string(),
441                 {MANIFEST_VERSION, MANIFEST_EXTENDED_VERSION});
442             auto version = ret[MANIFEST_VERSION];
443             auto extVersion = ret[MANIFEST_EXTENDED_VERSION];
444             auto info = Version::getExtVersionInfo(extVersion);
445             auto model = info["model"];
446             if (path.stem() != model)
447             {
448                 log<level::ERR>("Unmatched model",
449                                 entry("PATH=%s", path.c_str()),
450                                 entry("MODEL=%s", model.c_str()));
451                 continue;
452             }
453             auto versionId = utils::getVersionId(version);
454             auto it = activations.find(versionId);
455             if (it == activations.end())
456             {
457                 // This is a version that is different than the running PSUs
458                 auto activationState = Activation::Status::Ready;
459                 auto purpose = VersionPurpose::PSU;
460                 auto objPath = std::string(SOFTWARE_OBJPATH) + "/" + versionId;
461 
462                 auto activation = createActivationObject(
463                     objPath, versionId, extVersion, activationState, {}, path);
464                 activations.emplace(versionId, std::move(activation));
465 
466                 auto versionPtr = createVersionObject(objPath, versionId,
467                                                       version, purpose);
468                 versions.emplace(versionId, std::move(versionPtr));
469             }
470             else
471             {
472                 // This is a version that a running PSU is using, set the path
473                 // on the version object
474                 it->second->path(path);
475             }
476         }
477         else
478         {
479             log<level::ERR>("No MANIFEST found",
480                             entry("PATH=%s", path.c_str()));
481         }
482     }
483 }
484 
485 std::optional<std::string> ItemUpdater::getLatestVersionId()
486 {
487     auto latestVersion = utils::getLatestVersion(versionStrings);
488     if (latestVersion.empty())
489     {
490         return {};
491     }
492 
493     std::optional<std::string> versionId;
494     for (const auto& v : versions)
495     {
496         if (v.second->version() == latestVersion)
497         {
498             versionId = v.first;
499             break;
500         }
501     }
502     assert(versionId.has_value());
503     return versionId;
504 }
505 
506 void ItemUpdater::syncToLatestImage()
507 {
508     auto latestVersionId = getLatestVersionId();
509     if (!latestVersionId)
510     {
511         return;
512     }
513     const auto& it = activations.find(*latestVersionId);
514     assert(it != activations.end());
515     const auto& activation = it->second;
516     const auto& assocs = activation->associations();
517 
518     auto paths = utils::getPSUInventoryPath(bus);
519     for (const auto& p : paths)
520     {
521         // As long as there is a PSU is not associated with the latest image,
522         // run the activation so that all PSUs are running the same latest
523         // image.
524         if (!utils::isAssociated(p, assocs))
525         {
526             log<level::INFO>("Automatically update PSU",
527                              entry("VERSION_ID=%s", latestVersionId->c_str()));
528             invokeActivation(activation);
529             break;
530         }
531     }
532 }
533 
534 void ItemUpdater::invokeActivation(
535     const std::unique_ptr<Activation>& activation)
536 {
537     activation->requestedActivation(Activation::RequestedActivations::Active);
538 }
539 
540 } // namespace updater
541 } // namespace software
542 } // namespace phosphor
543