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