1 #include "config.h"
2 
3 #include "item_updater.hpp"
4 
5 #include "images.hpp"
6 #include "serialize.hpp"
7 #include "version.hpp"
8 #include "xyz/openbmc_project/Software/Version/server.hpp"
9 
10 #include <filesystem>
11 #include <fstream>
12 #include <phosphor-logging/elog-errors.hpp>
13 #include <phosphor-logging/elog.hpp>
14 #include <phosphor-logging/log.hpp>
15 #include <queue>
16 #include <set>
17 #include <string>
18 #include <thread>
19 #include <xyz/openbmc_project/Common/error.hpp>
20 #include <xyz/openbmc_project/Software/Image/error.hpp>
21 
22 namespace phosphor
23 {
24 namespace software
25 {
26 namespace updater
27 {
28 
29 // When you see server:: you know we're referencing our base class
30 namespace server = sdbusplus::xyz::openbmc_project::Software::server;
31 namespace control = sdbusplus::xyz::openbmc_project::Control::server;
32 
33 using namespace phosphor::logging;
34 using namespace sdbusplus::xyz::openbmc_project::Software::Image::Error;
35 using namespace phosphor::software::image;
36 namespace fs = std::filesystem;
37 using NotAllowed = sdbusplus::xyz::openbmc_project::Common::Error::NotAllowed;
38 
39 void ItemUpdater::createActivation(sdbusplus::message::message& msg)
40 {
41 
42     using SVersion = server::Version;
43     using VersionPurpose = SVersion::VersionPurpose;
44     using VersionClass = phosphor::software::manager::Version;
45 
46     sdbusplus::message::object_path objPath;
47     auto purpose = VersionPurpose::Unknown;
48     std::string version;
49     std::map<std::string, std::map<std::string, std::variant<std::string>>>
50         interfaces;
51     msg.read(objPath, interfaces);
52     std::string path(std::move(objPath));
53     std::string filePath;
54 
55     for (const auto& intf : interfaces)
56     {
57         if (intf.first == VERSION_IFACE)
58         {
59             for (const auto& property : intf.second)
60             {
61                 if (property.first == "Purpose")
62                 {
63                     auto value = SVersion::convertVersionPurposeFromString(
64                         std::get<std::string>(property.second));
65                     if (value == VersionPurpose::BMC ||
66 #ifdef HOST_BIOS_UPGRADE
67                         value == VersionPurpose::Host ||
68 #endif
69                         value == VersionPurpose::System)
70                     {
71                         purpose = value;
72                     }
73                 }
74                 else if (property.first == "Version")
75                 {
76                     version = std::get<std::string>(property.second);
77                 }
78             }
79         }
80         else if (intf.first == FILEPATH_IFACE)
81         {
82             for (const auto& property : intf.second)
83             {
84                 if (property.first == "Path")
85                 {
86                     filePath = std::get<std::string>(property.second);
87                 }
88             }
89         }
90     }
91     if (version.empty() || filePath.empty() ||
92         purpose == VersionPurpose::Unknown)
93     {
94         return;
95     }
96 
97     // Version id is the last item in the path
98     auto pos = path.rfind("/");
99     if (pos == std::string::npos)
100     {
101         log<level::ERR>("No version id found in object path",
102                         entry("OBJPATH=%s", path.c_str()));
103         return;
104     }
105 
106     auto versionId = path.substr(pos + 1);
107 
108     if (activations.find(versionId) == activations.end())
109     {
110         // Determine the Activation state by processing the given image dir.
111         auto activationState = server::Activation::Activations::Invalid;
112         ItemUpdater::ActivationStatus result;
113         if (purpose == VersionPurpose::BMC || purpose == VersionPurpose::System)
114             result = ItemUpdater::validateSquashFSImage(filePath);
115         else
116             result = ItemUpdater::ActivationStatus::ready;
117 
118         AssociationList associations = {};
119 
120         if (result == ItemUpdater::ActivationStatus::ready)
121         {
122             activationState = server::Activation::Activations::Ready;
123             // Create an association to the BMC inventory item
124             associations.emplace_back(
125                 std::make_tuple(ACTIVATION_FWD_ASSOCIATION,
126                                 ACTIVATION_REV_ASSOCIATION, bmcInventoryPath));
127         }
128 
129         activations.insert(std::make_pair(
130             versionId,
131             std::make_unique<Activation>(bus, path, *this, versionId,
132                                          activationState, associations)));
133 
134         auto versionPtr = std::make_unique<VersionClass>(
135             bus, path, version, purpose, filePath,
136             std::bind(&ItemUpdater::erase, this, std::placeholders::_1));
137         versionPtr->deleteObject =
138             std::make_unique<phosphor::software::manager::Delete>(bus, path,
139                                                                   *versionPtr);
140         versions.insert(std::make_pair(versionId, std::move(versionPtr)));
141     }
142     return;
143 }
144 
145 void ItemUpdater::processBMCImage()
146 {
147     using VersionClass = phosphor::software::manager::Version;
148 
149     // Check MEDIA_DIR and create if it does not exist
150     try
151     {
152         if (!fs::is_directory(MEDIA_DIR))
153         {
154             fs::create_directory(MEDIA_DIR);
155         }
156     }
157     catch (const fs::filesystem_error& e)
158     {
159         log<level::ERR>("Failed to prepare dir", entry("ERR=%s", e.what()));
160         return;
161     }
162 
163     // Read os-release from /etc/ to get the functional BMC version
164     auto functionalVersion = VersionClass::getBMCVersion(OS_RELEASE_FILE);
165 
166     // Read os-release from folders under /media/ to get
167     // BMC Software Versions.
168     for (const auto& iter : fs::directory_iterator(MEDIA_DIR))
169     {
170         auto activationState = server::Activation::Activations::Active;
171         static const auto BMC_RO_PREFIX_LEN = strlen(BMC_ROFS_PREFIX);
172 
173         // Check if the BMC_RO_PREFIXis the prefix of the iter.path
174         if (0 ==
175             iter.path().native().compare(0, BMC_RO_PREFIX_LEN, BMC_ROFS_PREFIX))
176         {
177             // The versionId is extracted from the path
178             // for example /media/ro-2a1022fe.
179             auto id = iter.path().native().substr(BMC_RO_PREFIX_LEN);
180             auto osRelease = iter.path() / OS_RELEASE_FILE;
181             if (!fs::is_regular_file(osRelease))
182             {
183                 log<level::ERR>(
184                     "Failed to read osRelease",
185                     entry("FILENAME=%s", osRelease.string().c_str()));
186                 ItemUpdater::erase(id);
187                 continue;
188             }
189             auto version = VersionClass::getBMCVersion(osRelease);
190             if (version.empty())
191             {
192                 log<level::ERR>(
193                     "Failed to read version from osRelease",
194                     entry("FILENAME=%s", osRelease.string().c_str()));
195                 activationState = server::Activation::Activations::Invalid;
196             }
197 
198             auto purpose = server::Version::VersionPurpose::BMC;
199             restorePurpose(id, purpose);
200 
201             auto path = fs::path(SOFTWARE_OBJPATH) / id;
202 
203             // Create functional association if this is the functional
204             // version
205             if (version.compare(functionalVersion) == 0)
206             {
207                 createFunctionalAssociation(path);
208             }
209 
210             AssociationList associations = {};
211 
212             if (activationState == server::Activation::Activations::Active)
213             {
214                 // Create an association to the BMC inventory item
215                 associations.emplace_back(std::make_tuple(
216                     ACTIVATION_FWD_ASSOCIATION, ACTIVATION_REV_ASSOCIATION,
217                     bmcInventoryPath));
218 
219                 // Create an active association since this image is active
220                 createActiveAssociation(path);
221             }
222 
223             // All updateable firmware components must expose the updateable
224             // association.
225             createUpdateableAssociation(path);
226 
227             // Create Version instance for this version.
228             auto versionPtr = std::make_unique<VersionClass>(
229                 bus, path, version, purpose, "",
230                 std::bind(&ItemUpdater::erase, this, std::placeholders::_1));
231             auto isVersionFunctional = versionPtr->isFunctional();
232             if (!isVersionFunctional)
233             {
234                 versionPtr->deleteObject =
235                     std::make_unique<phosphor::software::manager::Delete>(
236                         bus, path, *versionPtr);
237             }
238             versions.insert(std::make_pair(id, std::move(versionPtr)));
239 
240             // Create Activation instance for this version.
241             activations.insert(std::make_pair(
242                 id, std::make_unique<Activation>(
243                         bus, path, *this, id, activationState, associations)));
244 
245             // If Active, create RedundancyPriority instance for this
246             // version.
247             if (activationState == server::Activation::Activations::Active)
248             {
249                 uint8_t priority = std::numeric_limits<uint8_t>::max();
250                 if (!restorePriority(id, priority))
251                 {
252                     if (isVersionFunctional)
253                     {
254                         priority = 0;
255                     }
256                     else
257                     {
258                         log<level::ERR>("Unable to restore priority from file.",
259                                         entry("VERSIONID=%s", id.c_str()));
260                     }
261                 }
262                 activations.find(id)->second->redundancyPriority =
263                     std::make_unique<RedundancyPriority>(
264                         bus, path, *(activations.find(id)->second), priority,
265                         false);
266             }
267         }
268     }
269 
270     // If there is no ubi volume for bmc version then read the /etc/os-release
271     // and create rofs-<versionId> under /media
272     if (activations.size() == 0)
273     {
274         auto version = VersionClass::getBMCVersion(OS_RELEASE_FILE);
275         auto id = phosphor::software::manager::Version::getId(version);
276         auto versionFileDir = BMC_ROFS_PREFIX + id + "/etc/";
277         try
278         {
279             if (!fs::is_directory(versionFileDir))
280             {
281                 fs::create_directories(versionFileDir);
282             }
283             auto versionFilePath = BMC_ROFS_PREFIX + id + OS_RELEASE_FILE;
284             fs::create_directory_symlink(OS_RELEASE_FILE, versionFilePath);
285             ItemUpdater::processBMCImage();
286         }
287         catch (const std::exception& e)
288         {
289             log<level::ERR>(e.what());
290         }
291     }
292 
293     mirrorUbootToAlt();
294     return;
295 }
296 
297 void ItemUpdater::erase(std::string entryId)
298 {
299     // Find entry in versions map
300     auto it = versions.find(entryId);
301     if (it != versions.end())
302     {
303         if (it->second->isFunctional() && ACTIVE_BMC_MAX_ALLOWED > 1)
304         {
305             log<level::ERR>("Error: Version is currently running on the BMC. "
306                             "Unable to remove.",
307                             entry("VERSIONID=%s", entryId.c_str()));
308             return;
309         }
310 
311         // Delete ReadOnly partitions if it's not active
312         removeReadOnlyPartition(entryId);
313         removePersistDataDirectory(entryId);
314 
315         // Removing entry in versions map
316         this->versions.erase(entryId);
317     }
318     else
319     {
320         // Delete ReadOnly partitions even if we can't find the version
321         removeReadOnlyPartition(entryId);
322         removePersistDataDirectory(entryId);
323 
324         log<level::ERR>("Error: Failed to find version in item updater "
325                         "versions map. Unable to remove.",
326                         entry("VERSIONID=%s", entryId.c_str()));
327     }
328 
329     helper.clearEntry(entryId);
330 
331     // Removing entry in activations map
332     auto ita = activations.find(entryId);
333     if (ita == activations.end())
334     {
335         log<level::ERR>("Error: Failed to find version in item updater "
336                         "activations map. Unable to remove.",
337                         entry("VERSIONID=%s", entryId.c_str()));
338     }
339     else
340     {
341         removeAssociations(ita->second->path);
342         this->activations.erase(entryId);
343     }
344     ItemUpdater::resetUbootEnvVars();
345     return;
346 }
347 
348 void ItemUpdater::deleteAll()
349 {
350     std::vector<std::string> deletableVersions;
351 
352     for (const auto& versionIt : versions)
353     {
354         if (!versionIt.second->isFunctional())
355         {
356             deletableVersions.push_back(versionIt.first);
357         }
358     }
359 
360     for (const auto& deletableIt : deletableVersions)
361     {
362         ItemUpdater::erase(deletableIt);
363     }
364 
365     helper.cleanup();
366 }
367 
368 ItemUpdater::ActivationStatus
369     ItemUpdater::validateSquashFSImage(const std::string& filePath)
370 {
371     bool invalid = false;
372 
373     for (auto& bmcImage : bmcImages)
374     {
375         fs::path file(filePath);
376         file /= bmcImage;
377         std::ifstream efile(file.c_str());
378         if (efile.good() != 1)
379         {
380             log<level::ERR>("Failed to find the BMC image.",
381                             entry("IMAGE=%s", bmcImage.c_str()));
382             invalid = true;
383         }
384     }
385 
386     if (invalid)
387     {
388         return ItemUpdater::ActivationStatus::invalid;
389     }
390 
391     return ItemUpdater::ActivationStatus::ready;
392 }
393 
394 void ItemUpdater::savePriority(const std::string& versionId, uint8_t value)
395 {
396     storePriority(versionId, value);
397     helper.setEntry(versionId, value);
398 }
399 
400 void ItemUpdater::freePriority(uint8_t value, const std::string& versionId)
401 {
402     std::map<std::string, uint8_t> priorityMap;
403 
404     // Insert the requested version and priority, it may not exist yet.
405     priorityMap.insert(std::make_pair(versionId, value));
406 
407     for (const auto& intf : activations)
408     {
409         if (intf.second->redundancyPriority)
410         {
411             priorityMap.insert(std::make_pair(
412                 intf.first, intf.second->redundancyPriority.get()->priority()));
413         }
414     }
415 
416     // Lambda function to compare 2 priority values, use <= to allow duplicates
417     typedef std::function<bool(std::pair<std::string, uint8_t>,
418                                std::pair<std::string, uint8_t>)>
419         cmpPriority;
420     cmpPriority cmpPriorityFunc =
421         [](std::pair<std::string, uint8_t> priority1,
422            std::pair<std::string, uint8_t> priority2) {
423             return priority1.second <= priority2.second;
424         };
425 
426     // Sort versions by ascending priority
427     std::set<std::pair<std::string, uint8_t>, cmpPriority> prioritySet(
428         priorityMap.begin(), priorityMap.end(), cmpPriorityFunc);
429 
430     auto freePriorityValue = value;
431     for (auto& element : prioritySet)
432     {
433         if (element.first == versionId)
434         {
435             continue;
436         }
437         if (element.second == freePriorityValue)
438         {
439             ++freePriorityValue;
440             auto it = activations.find(element.first);
441             it->second->redundancyPriority.get()->sdbusPriority(
442                 freePriorityValue);
443         }
444     }
445 
446     auto lowestVersion = prioritySet.begin()->first;
447     if (value == prioritySet.begin()->second)
448     {
449         lowestVersion = versionId;
450     }
451     updateUbootEnvVars(lowestVersion);
452 }
453 
454 void ItemUpdater::reset()
455 {
456     constexpr auto setFactoryResetWait = std::chrono::seconds(3);
457     helper.factoryReset();
458 
459     // Need to wait for env variables to complete, otherwise an immediate reboot
460     // will not factory reset.
461     std::this_thread::sleep_for(setFactoryResetWait);
462 
463     log<level::INFO>("BMC factory reset will take effect upon reboot.");
464 }
465 
466 void ItemUpdater::removeReadOnlyPartition(std::string versionId)
467 {
468     helper.removeVersion(versionId);
469 }
470 
471 bool ItemUpdater::fieldModeEnabled(bool value)
472 {
473     // enabling field mode is intended to be one way: false -> true
474     if (value && !control::FieldMode::fieldModeEnabled())
475     {
476         control::FieldMode::fieldModeEnabled(value);
477 
478         auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH,
479                                           SYSTEMD_INTERFACE, "StartUnit");
480         method.append("obmc-flash-bmc-setenv@fieldmode\\x3dtrue.service",
481                       "replace");
482         bus.call_noreply(method);
483 
484         method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH,
485                                      SYSTEMD_INTERFACE, "StopUnit");
486         method.append("usr-local.mount", "replace");
487         bus.call_noreply(method);
488 
489         std::vector<std::string> usrLocal = {"usr-local.mount"};
490 
491         method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH,
492                                      SYSTEMD_INTERFACE, "MaskUnitFiles");
493         method.append(usrLocal, false, true);
494         bus.call_noreply(method);
495     }
496     else if (!value && control::FieldMode::fieldModeEnabled())
497     {
498         elog<NotAllowed>(xyz::openbmc_project::Common::NotAllowed::REASON(
499             "FieldMode is not allowed to be cleared"));
500     }
501 
502     return control::FieldMode::fieldModeEnabled();
503 }
504 
505 void ItemUpdater::restoreFieldModeStatus()
506 {
507     std::ifstream input("/dev/mtd/u-boot-env");
508     std::string envVar;
509     std::getline(input, envVar);
510 
511     if (envVar.find("fieldmode=true") != std::string::npos)
512     {
513         ItemUpdater::fieldModeEnabled(true);
514     }
515 }
516 
517 void ItemUpdater::setBMCInventoryPath()
518 {
519     auto depth = 0;
520     auto mapperCall = bus.new_method_call(MAPPER_BUSNAME, MAPPER_PATH,
521                                           MAPPER_INTERFACE, "GetSubTreePaths");
522 
523     mapperCall.append(INVENTORY_PATH);
524     mapperCall.append(depth);
525     std::vector<std::string> filter = {BMC_INVENTORY_INTERFACE};
526     mapperCall.append(filter);
527 
528     try
529     {
530         auto response = bus.call(mapperCall);
531 
532         using ObjectPaths = std::vector<std::string>;
533         ObjectPaths result;
534         response.read(result);
535 
536         if (!result.empty())
537         {
538             bmcInventoryPath = result.front();
539         }
540     }
541     catch (const sdbusplus::exception::SdBusError& e)
542     {
543         log<level::ERR>("Error in mapper GetSubTreePath");
544         return;
545     }
546 
547     return;
548 }
549 
550 void ItemUpdater::createActiveAssociation(const std::string& path)
551 {
552     assocs.emplace_back(
553         std::make_tuple(ACTIVE_FWD_ASSOCIATION, ACTIVE_REV_ASSOCIATION, path));
554     associations(assocs);
555 }
556 
557 void ItemUpdater::createFunctionalAssociation(const std::string& path)
558 {
559     assocs.emplace_back(std::make_tuple(FUNCTIONAL_FWD_ASSOCIATION,
560                                         FUNCTIONAL_REV_ASSOCIATION, path));
561     associations(assocs);
562 }
563 
564 void ItemUpdater::createUpdateableAssociation(const std::string& path)
565 {
566     assocs.emplace_back(std::make_tuple(UPDATEABLE_FWD_ASSOCIATION,
567                                         UPDATEABLE_REV_ASSOCIATION, path));
568     associations(assocs);
569 }
570 
571 void ItemUpdater::removeAssociations(const std::string& path)
572 {
573     for (auto iter = assocs.begin(); iter != assocs.end();)
574     {
575         if ((std::get<2>(*iter)).compare(path) == 0)
576         {
577             iter = assocs.erase(iter);
578             associations(assocs);
579         }
580         else
581         {
582             ++iter;
583         }
584     }
585 }
586 
587 bool ItemUpdater::isLowestPriority(uint8_t value)
588 {
589     for (const auto& intf : activations)
590     {
591         if (intf.second->redundancyPriority)
592         {
593             if (intf.second->redundancyPriority.get()->priority() < value)
594             {
595                 return false;
596             }
597         }
598     }
599     return true;
600 }
601 
602 void ItemUpdater::updateUbootEnvVars(const std::string& versionId)
603 {
604     helper.updateUbootVersionId(versionId);
605 }
606 
607 void ItemUpdater::resetUbootEnvVars()
608 {
609     decltype(activations.begin()->second->redundancyPriority.get()->priority())
610         lowestPriority = std::numeric_limits<uint8_t>::max();
611     decltype(activations.begin()->second->versionId) lowestPriorityVersion;
612     for (const auto& intf : activations)
613     {
614         if (!intf.second->redundancyPriority.get())
615         {
616             // Skip this version if the redundancyPriority is not initialized.
617             continue;
618         }
619 
620         if (intf.second->redundancyPriority.get()->priority() <= lowestPriority)
621         {
622             lowestPriority = intf.second->redundancyPriority.get()->priority();
623             lowestPriorityVersion = intf.second->versionId;
624         }
625     }
626 
627     // Update the U-boot environment variable to point to the lowest priority
628     updateUbootEnvVars(lowestPriorityVersion);
629 }
630 
631 void ItemUpdater::freeSpace(Activation& caller)
632 {
633     //  Versions with the highest priority in front
634     std::priority_queue<std::pair<int, std::string>,
635                         std::vector<std::pair<int, std::string>>,
636                         std::less<std::pair<int, std::string>>>
637         versionsPQ;
638 
639     std::size_t count = 0;
640     for (const auto& iter : activations)
641     {
642         if ((iter.second.get()->activation() ==
643              server::Activation::Activations::Active) ||
644             (iter.second.get()->activation() ==
645              server::Activation::Activations::Failed))
646         {
647             count++;
648             // Don't put the functional version on the queue since we can't
649             // remove the "running" BMC version.
650             // If ACTIVE_BMC_MAX_ALLOWED <= 1, there is only one active BMC,
651             // so remove functional version as well.
652             // Don't delete the the Activation object that called this function.
653             if ((versions.find(iter.second->versionId)
654                      ->second->isFunctional() &&
655                  ACTIVE_BMC_MAX_ALLOWED > 1) ||
656                 (iter.second->versionId == caller.versionId))
657             {
658                 continue;
659             }
660 
661             // Failed activations don't have priority, assign them a large value
662             // for sorting purposes.
663             auto priority = 999;
664             if (iter.second.get()->activation() ==
665                 server::Activation::Activations::Active)
666             {
667                 priority = iter.second->redundancyPriority.get()->priority();
668             }
669 
670             versionsPQ.push(std::make_pair(priority, iter.second->versionId));
671         }
672     }
673 
674     // If the number of BMC versions is over ACTIVE_BMC_MAX_ALLOWED -1,
675     // remove the highest priority one(s).
676     while ((count >= ACTIVE_BMC_MAX_ALLOWED) && (!versionsPQ.empty()))
677     {
678         erase(versionsPQ.top().second);
679         versionsPQ.pop();
680         count--;
681     }
682 }
683 
684 void ItemUpdater::mirrorUbootToAlt()
685 {
686     helper.mirrorAlt();
687 }
688 
689 } // namespace updater
690 } // namespace software
691 } // namespace phosphor
692