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