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