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