1 #include "config.h" 2 3 #include "item_updater.hpp" 4 5 #include "runtime_warning.hpp" 6 #include "utils.hpp" 7 8 #include <phosphor-logging/elog-errors.hpp> 9 #include <phosphor-logging/lg2.hpp> 10 #include <xyz/openbmc_project/Common/error.hpp> 11 12 #include <exception> 13 #include <filesystem> 14 #include <format> 15 #include <set> 16 #include <stdexcept> 17 18 namespace 19 { 20 constexpr auto MANIFEST_VERSION = "version"; 21 constexpr auto MANIFEST_EXTENDED_VERSION = "extended_version"; 22 } // namespace 23 24 namespace phosphor 25 { 26 namespace software 27 { 28 namespace updater 29 { 30 namespace server = sdbusplus::xyz::openbmc_project::Software::server; 31 32 using namespace sdbusplus::xyz::openbmc_project::Common::Error; 33 using namespace phosphor::logging; 34 using SVersion = server::Version; 35 using VersionPurpose = SVersion::VersionPurpose; 36 37 void ItemUpdater::onVersionInterfacesAddedMsg(sdbusplus::message_t& msg) 38 { 39 try 40 { 41 sdbusplus::message::object_path objPath; 42 InterfacesAddedMap interfaces; 43 msg.read(objPath, interfaces); 44 45 std::string path(std::move(objPath)); 46 onVersionInterfacesAdded(path, interfaces); 47 } 48 catch (const std::exception& e) 49 { 50 lg2::error("Unable to handle version InterfacesAdded event: {ERROR}", 51 "ERROR", e); 52 } 53 } 54 55 void ItemUpdater::onVersionInterfacesAdded(const std::string& path, 56 const InterfacesAddedMap& interfaces) 57 { 58 std::string filePath; 59 auto purpose = VersionPurpose::Unknown; 60 std::string version; 61 62 for (const auto& [interfaceName, propertyMap] : interfaces) 63 { 64 if (interfaceName == VERSION_IFACE) 65 { 66 for (const auto& [propertyName, propertyValue] : propertyMap) 67 { 68 if (propertyName == "Purpose") 69 { 70 // Only process the PSU images 71 auto value = SVersion::convertVersionPurposeFromString( 72 std::get<std::string>(propertyValue)); 73 74 if (value == VersionPurpose::PSU) 75 { 76 purpose = value; 77 } 78 } 79 else if (propertyName == VERSION) 80 { 81 version = std::get<std::string>(propertyValue); 82 } 83 } 84 } 85 else if (interfaceName == FILEPATH_IFACE) 86 { 87 const auto& it = propertyMap.find("Path"); 88 if (it != propertyMap.end()) 89 { 90 filePath = std::get<std::string>(it->second); 91 } 92 } 93 } 94 if ((filePath.empty()) || (purpose == VersionPurpose::Unknown)) 95 { 96 return; 97 } 98 99 // If we are only installing PSU images from the built-in directory, ignore 100 // PSU images from other directories 101 if (ALWAYS_USE_BUILTIN_IMG_DIR && !filePath.starts_with(IMG_DIR_BUILTIN)) 102 { 103 return; 104 } 105 106 // Version id is the last item in the path 107 auto pos = path.rfind('/'); 108 if (pos == std::string::npos) 109 { 110 lg2::error("No version id found in object path {OBJPATH}", "OBJPATH", 111 path); 112 return; 113 } 114 115 auto versionId = path.substr(pos + 1); 116 117 if (activations.find(versionId) == activations.end()) 118 { 119 // Determine the Activation state by processing the given image dir. 120 AssociationList associations; 121 auto activationState = Activation::Status::Ready; 122 123 associations.emplace_back(std::make_tuple( 124 ACTIVATION_FWD_ASSOCIATION, ACTIVATION_REV_ASSOCIATION, 125 PSU_INVENTORY_PATH_BASE)); 126 127 fs::path manifestPath(filePath); 128 manifestPath /= MANIFEST_FILE; 129 std::string extendedVersion = 130 Version::getValue(manifestPath, {MANIFEST_EXTENDED_VERSION}); 131 132 auto activation = 133 createActivationObject(path, versionId, extendedVersion, 134 activationState, associations, filePath); 135 activations.emplace(versionId, std::move(activation)); 136 137 auto versionPtr = 138 createVersionObject(path, versionId, version, purpose); 139 versions.emplace(versionId, std::move(versionPtr)); 140 } 141 } 142 143 void ItemUpdater::erase(const std::string& versionId) 144 { 145 auto it = versions.find(versionId); 146 if (it == versions.end()) 147 { 148 lg2::error("Error: Failed to find version {VERSION_ID} in " 149 "item updater versions map. Unable to remove.", 150 "VERSION_ID", versionId); 151 } 152 else 153 { 154 versionStrings.erase(it->second->getVersionString()); 155 versions.erase(it); 156 } 157 158 // Removing entry in activations map 159 auto ita = activations.find(versionId); 160 if (ita == activations.end()) 161 { 162 lg2::error("Error: Failed to find version {VERSION_ID} in " 163 "item updater activations map. Unable to remove.", 164 "VERSION_ID", versionId); 165 } 166 else 167 { 168 activations.erase(versionId); 169 } 170 } 171 172 void ItemUpdater::createActiveAssociation(const std::string& path) 173 { 174 assocs.emplace_back( 175 std::make_tuple(ACTIVE_FWD_ASSOCIATION, ACTIVE_REV_ASSOCIATION, path)); 176 associations(assocs); 177 } 178 179 void ItemUpdater::addFunctionalAssociation(const std::string& path) 180 { 181 assocs.emplace_back(std::make_tuple(FUNCTIONAL_FWD_ASSOCIATION, 182 FUNCTIONAL_REV_ASSOCIATION, path)); 183 associations(assocs); 184 } 185 186 void ItemUpdater::addUpdateableAssociation(const std::string& path) 187 { 188 assocs.emplace_back(std::make_tuple(UPDATEABLE_FWD_ASSOCIATION, 189 UPDATEABLE_REV_ASSOCIATION, path)); 190 associations(assocs); 191 } 192 193 void ItemUpdater::removeAssociation(const std::string& path) 194 { 195 for (auto iter = assocs.begin(); iter != assocs.end();) 196 { 197 if ((std::get<2>(*iter)) == path) 198 { 199 iter = assocs.erase(iter); 200 associations(assocs); 201 } 202 else 203 { 204 ++iter; 205 } 206 } 207 } 208 209 void ItemUpdater::onUpdateDone(const std::string& versionId, 210 const std::string& psuInventoryPath) 211 { 212 // After update is done, remove old activation objects 213 for (auto it = activations.begin(); it != activations.end(); ++it) 214 { 215 if (it->second->getVersionId() != versionId && 216 utils::isAssociated(psuInventoryPath, it->second->associations())) 217 { 218 removePsuObject(psuInventoryPath); 219 break; 220 } 221 } 222 223 auto it = activations.find(versionId); 224 if (it == activations.end()) 225 { 226 lg2::error("Unable to find Activation for version ID {VERSION_ID}", 227 "VERSION_ID", versionId); 228 } 229 else 230 { 231 psuPathActivationMap.emplace(psuInventoryPath, it->second); 232 } 233 } 234 235 std::unique_ptr<Activation> ItemUpdater::createActivationObject( 236 const std::string& path, const std::string& versionId, 237 const std::string& extVersion, Activation::Status activationStatus, 238 const AssociationList& assocs, const std::string& filePath) 239 { 240 return std::make_unique<Activation>(bus, path, versionId, extVersion, 241 activationStatus, assocs, filePath, 242 this, this); 243 } 244 245 void ItemUpdater::createPsuObject(const std::string& psuInventoryPath, 246 const std::string& psuVersion) 247 { 248 auto versionId = utils::getVersionId(psuVersion); 249 auto path = std::string(SOFTWARE_OBJPATH) + "/" + versionId; 250 251 auto it = activations.find(versionId); 252 if (it != activations.end()) 253 { 254 // The versionId is already created, associate the path 255 auto associations = it->second->associations(); 256 associations.emplace_back( 257 std::make_tuple(ACTIVATION_FWD_ASSOCIATION, 258 ACTIVATION_REV_ASSOCIATION, psuInventoryPath)); 259 it->second->associations(associations); 260 psuPathActivationMap.emplace(psuInventoryPath, it->second); 261 } 262 else 263 { 264 // Create a new object for running PSU inventory 265 AssociationList associations; 266 auto activationState = Activation::Status::Active; 267 268 associations.emplace_back( 269 std::make_tuple(ACTIVATION_FWD_ASSOCIATION, 270 ACTIVATION_REV_ASSOCIATION, psuInventoryPath)); 271 272 auto activation = createActivationObject( 273 path, versionId, "", activationState, associations, ""); 274 activations.emplace(versionId, std::move(activation)); 275 psuPathActivationMap.emplace(psuInventoryPath, activations[versionId]); 276 277 auto versionPtr = createVersionObject(path, versionId, psuVersion, 278 VersionPurpose::PSU); 279 versions.emplace(versionId, std::move(versionPtr)); 280 281 createActiveAssociation(path); 282 addFunctionalAssociation(path); 283 addUpdateableAssociation(path); 284 } 285 } 286 287 void ItemUpdater::removePsuObject(const std::string& psuInventoryPath) 288 { 289 auto it = psuPathActivationMap.find(psuInventoryPath); 290 if (it == psuPathActivationMap.end()) 291 { 292 lg2::error("No Activation found for PSU {PSUPATH}", "PSUPATH", 293 psuInventoryPath); 294 return; 295 } 296 const auto& activationPtr = it->second; 297 psuPathActivationMap.erase(psuInventoryPath); 298 299 auto associations = activationPtr->associations(); 300 for (auto iter = associations.begin(); iter != associations.end();) 301 { 302 if ((std::get<2>(*iter)) == psuInventoryPath) 303 { 304 iter = associations.erase(iter); 305 } 306 else 307 { 308 ++iter; 309 } 310 } 311 if (associations.empty()) 312 { 313 // Remove the activation 314 erase(activationPtr->getVersionId()); 315 } 316 else 317 { 318 // Update association 319 activationPtr->associations(associations); 320 } 321 } 322 323 void ItemUpdater::addPsuToStatusMap(const std::string& psuPath) 324 { 325 if (!psuStatusMap.contains(psuPath)) 326 { 327 psuStatusMap[psuPath] = {false, ""}; 328 329 // Add PropertiesChanged listener for Item interface so we are notified 330 // when Present property changes 331 psuMatches.emplace_back( 332 bus, MatchRules::propertiesChanged(psuPath, ITEM_IFACE), 333 std::bind(&ItemUpdater::onPsuInventoryChangedMsg, this, 334 std::placeholders::_1)); 335 } 336 } 337 338 void ItemUpdater::handlePSUPresenceChanged(const std::string& psuPath) 339 { 340 if (psuStatusMap.contains(psuPath)) 341 { 342 if (psuStatusMap[psuPath].present) 343 { 344 // PSU is now present 345 psuStatusMap[psuPath].model = utils::getModel(psuPath); 346 auto version = utils::getVersion(psuPath); 347 if (!version.empty() && !psuPathActivationMap.contains(psuPath)) 348 { 349 createPsuObject(psuPath, version); 350 } 351 } 352 else 353 { 354 // PSU is now missing 355 psuStatusMap[psuPath].model.clear(); 356 if (psuPathActivationMap.contains(psuPath)) 357 { 358 removePsuObject(psuPath); 359 } 360 } 361 } 362 } 363 364 std::unique_ptr<Version> ItemUpdater::createVersionObject( 365 const std::string& objPath, const std::string& versionId, 366 const std::string& versionString, 367 sdbusplus::xyz::openbmc_project::Software::server::Version::VersionPurpose 368 versionPurpose) 369 { 370 versionStrings.insert(versionString); 371 auto version = std::make_unique<Version>( 372 bus, objPath, versionId, versionString, versionPurpose, 373 std::bind(&ItemUpdater::erase, this, std::placeholders::_1)); 374 return version; 375 } 376 377 void ItemUpdater::onPsuInventoryChangedMsg(sdbusplus::message_t& msg) 378 { 379 try 380 { 381 using Interface = std::string; 382 Interface interface; 383 Properties properties; 384 std::string psuPath = msg.get_path(); 385 386 msg.read(interface, properties); 387 onPsuInventoryChanged(psuPath, properties); 388 } 389 catch (const std::exception& e) 390 { 391 lg2::error( 392 "Unable to handle inventory PropertiesChanged event: {ERROR}", 393 "ERROR", e); 394 } 395 } 396 397 void ItemUpdater::onPsuInventoryChanged(const std::string& psuPath, 398 const Properties& properties) 399 { 400 if (psuStatusMap.contains(psuPath) && properties.contains(PRESENT)) 401 { 402 psuStatusMap[psuPath].present = std::get<bool>(properties.at(PRESENT)); 403 handlePSUPresenceChanged(psuPath); 404 if (psuStatusMap[psuPath].present) 405 { 406 // Check if there are new PSU images to update 407 processStoredImage(); 408 syncToLatestImage(); 409 } 410 } 411 } 412 413 void ItemUpdater::processPSUImage() 414 { 415 try 416 { 417 auto paths = utils::getPSUInventoryPaths(bus); 418 for (const auto& p : paths) 419 { 420 try 421 { 422 addPsuToStatusMap(p); 423 auto service = utils::getService(bus, p.c_str(), ITEM_IFACE); 424 psuStatusMap[p].present = utils::getProperty<bool>( 425 bus, service.c_str(), p.c_str(), ITEM_IFACE, PRESENT); 426 handlePSUPresenceChanged(p); 427 } 428 catch (const std::exception& e) 429 { 430 // Ignore errors; the information might not be available yet 431 } 432 } 433 } 434 catch (const std::exception& e) 435 { 436 // Ignore errors; the information might not be available yet 437 } 438 } 439 440 void ItemUpdater::processStoredImage() 441 { 442 // Build list of directories to scan 443 std::vector<fs::path> paths; 444 paths.emplace_back(IMG_DIR_BUILTIN); 445 if (!ALWAYS_USE_BUILTIN_IMG_DIR) 446 { 447 paths.emplace_back(IMG_DIR_PERSIST); 448 } 449 450 // Scan directories 451 auto logMsg = "Unable to find PSU firmware in directory {PATH}: {ERROR}"; 452 for (const auto& path : paths) 453 { 454 try 455 { 456 scanDirectory(path); 457 } 458 catch (const RuntimeWarning& r) 459 { 460 lg2::warning(logMsg, "PATH", path, "ERROR", r); 461 } 462 catch (const std::exception& e) 463 { 464 lg2::error(logMsg, "PATH", path, "ERROR", e); 465 } 466 } 467 } 468 469 void ItemUpdater::scanDirectory(const fs::path& dir) 470 { 471 // Find the model subdirectory within the specified directory 472 auto modelDir = findModelDirectory(dir); 473 if (modelDir.empty()) 474 { 475 return; 476 } 477 478 // Verify a manifest file exists within the model subdirectory 479 auto manifest = modelDir / MANIFEST_FILE; 480 if (!fs::exists(manifest)) 481 { 482 throw std::runtime_error{ 483 std::format("Manifest file does not exist: {}", manifest.c_str())}; 484 } 485 if (!fs::is_regular_file(manifest)) 486 { 487 throw std::runtime_error{ 488 std::format("Path is not a file: {}", manifest.c_str())}; 489 } 490 491 // Get version, extVersion, and model from manifest file 492 auto ret = Version::getValues( 493 manifest.string(), {MANIFEST_VERSION, MANIFEST_EXTENDED_VERSION}); 494 auto version = ret[MANIFEST_VERSION]; 495 auto extVersion = ret[MANIFEST_EXTENDED_VERSION]; 496 auto info = Version::getExtVersionInfo(extVersion); 497 auto model = info["model"]; 498 499 // Verify version and model are valid 500 if (version.empty() || model.empty()) 501 { 502 throw std::runtime_error{std::format( 503 "Invalid information in manifest: path={}, version={}, model={}", 504 manifest.c_str(), version, model)}; 505 } 506 507 // Verify model from manifest matches the subdirectory name 508 if (modelDir.stem() != model) 509 { 510 throw std::runtime_error{std::format( 511 "Model in manifest does not match path: model={}, path={}", model, 512 modelDir.c_str())}; 513 } 514 515 // Found a valid PSU image directory; write path to journal 516 lg2::info("Found PSU firmware image directory: {PATH}", "PATH", modelDir); 517 518 // Calculate version ID and check if an Activation for it exists 519 auto versionId = utils::getVersionId(version); 520 auto it = activations.find(versionId); 521 if (it == activations.end()) 522 { 523 // This is a version that is different than the running PSUs 524 auto activationState = Activation::Status::Ready; 525 auto purpose = VersionPurpose::PSU; 526 auto objPath = std::string(SOFTWARE_OBJPATH) + "/" + versionId; 527 528 auto activation = createActivationObject(objPath, versionId, extVersion, 529 activationState, {}, modelDir); 530 activations.emplace(versionId, std::move(activation)); 531 532 auto versionPtr = 533 createVersionObject(objPath, versionId, version, purpose); 534 versions.emplace(versionId, std::move(versionPtr)); 535 } 536 else 537 { 538 // Activation already exists. It may have been created for code that is 539 // running on one or more PSUs. Set Path and ExtendedVersion properties. 540 // The properties are not set when the Activation is created for code 541 // running on a PSU. The properties are needed to update other PSUs. 542 it->second->path(modelDir); 543 it->second->extendedVersion(extVersion); 544 } 545 } 546 547 fs::path ItemUpdater::findModelDirectory(const fs::path& dir) 548 { 549 fs::path modelDir; 550 551 // Verify directory path exists and is a directory 552 if (!fs::exists(dir)) 553 { 554 // Warning condition. IMG_DIR_BUILTIN might not be used. IMG_DIR_PERSIST 555 // might not exist if an image from IMG_DIR has not been stored. 556 throw RuntimeWarning{ 557 std::format("Directory does not exist: {}", dir.c_str())}; 558 } 559 if (!fs::is_directory(dir)) 560 { 561 throw std::runtime_error{ 562 std::format("Path is not a directory: {}", dir.c_str())}; 563 } 564 565 // Get the model name of the PSUs that have been found. Note that we 566 // might not have found the PSU information yet on D-Bus. 567 std::string model; 568 for (const auto& [key, item] : psuStatusMap) 569 { 570 if (!item.model.empty()) 571 { 572 model = item.model; 573 break; 574 } 575 } 576 if (!model.empty()) 577 { 578 // Verify model subdirectory path exists and is a directory 579 auto subDir = dir / model; 580 if (!fs::exists(subDir)) 581 { 582 // Warning condition. Subdirectory may not exist in IMG_DIR_PERSIST 583 // if no image has been stored there. May also not exist if 584 // firmware update is not supported for this PSU model. 585 throw RuntimeWarning{ 586 std::format("Directory does not exist: {}", subDir.c_str())}; 587 } 588 if (!fs::is_directory(subDir)) 589 { 590 throw std::runtime_error{ 591 std::format("Path is not a directory: {}", subDir.c_str())}; 592 } 593 modelDir = subDir; 594 } 595 596 return modelDir; 597 } 598 599 std::optional<std::string> ItemUpdater::getLatestVersionId() 600 { 601 std::string latestVersion; 602 if (ALWAYS_USE_BUILTIN_IMG_DIR) 603 { 604 latestVersion = getFWVersionFromBuiltinDir(); 605 } 606 else 607 { 608 latestVersion = utils::getLatestVersion(versionStrings); 609 } 610 if (latestVersion.empty()) 611 { 612 return {}; 613 } 614 615 std::optional<std::string> versionId; 616 for (const auto& v : versions) 617 { 618 if (v.second->version() == latestVersion) 619 { 620 versionId = v.first; 621 break; 622 } 623 } 624 if (!versionId.has_value()) 625 { 626 lg2::error("Unable to find versionId for latest version {VERSION}", 627 "VERSION", latestVersion); 628 } 629 return versionId; 630 } 631 632 void ItemUpdater::syncToLatestImage() 633 { 634 auto latestVersionId = getLatestVersionId(); 635 if (!latestVersionId) 636 { 637 return; 638 } 639 const auto& it = activations.find(*latestVersionId); 640 if (it == activations.end()) 641 642 { 643 lg2::error("Unable to find Activation for versionId {VERSION_ID}", 644 "VERSION_ID", *latestVersionId); 645 return; 646 } 647 const auto& activation = it->second; 648 const auto& assocs = activation->associations(); 649 650 auto paths = utils::getPSUInventoryPaths(bus); 651 for (const auto& p : paths) 652 { 653 // If there is a present PSU that is not associated with the latest 654 // image, run the activation so that all PSUs are running the same 655 // latest image. 656 if (psuStatusMap.contains(p) && psuStatusMap[p].present) 657 { 658 if (!utils::isAssociated(p, assocs)) 659 { 660 lg2::info("Automatically update PSUs to versionId {VERSION_ID}", 661 "VERSION_ID", *latestVersionId); 662 invokeActivation(activation); 663 break; 664 } 665 } 666 } 667 } 668 669 void ItemUpdater::invokeActivation( 670 const std::unique_ptr<Activation>& activation) 671 { 672 activation->requestedActivation(Activation::RequestedActivations::Active); 673 } 674 675 void ItemUpdater::onPSUInterfacesAdded(sdbusplus::message_t& msg) 676 { 677 // Maintain static set of valid PSU paths. This is needed if PSU interface 678 // comes in a separate InterfacesAdded message from Item interface. 679 static std::set<std::string> psuPaths{}; 680 681 try 682 { 683 sdbusplus::message::object_path objPath; 684 InterfacesAddedMap interfaces; 685 msg.read(objPath, interfaces); 686 std::string path = objPath.str; 687 688 if (interfaces.contains(PSU_INVENTORY_IFACE)) 689 { 690 psuPaths.insert(path); 691 } 692 693 if (interfaces.contains(ITEM_IFACE) && psuPaths.contains(path) && 694 !psuStatusMap.contains(path)) 695 { 696 auto interface = interfaces[ITEM_IFACE]; 697 if (interface.contains(PRESENT)) 698 { 699 addPsuToStatusMap(path); 700 psuStatusMap[path].present = std::get<bool>(interface[PRESENT]); 701 handlePSUPresenceChanged(path); 702 if (psuStatusMap[path].present) 703 { 704 // Check if there are new PSU images to update 705 processStoredImage(); 706 syncToLatestImage(); 707 } 708 } 709 } 710 } 711 catch (const std::exception& e) 712 { 713 lg2::error("Unable to handle inventory InterfacesAdded event: {ERROR}", 714 "ERROR", e); 715 } 716 } 717 718 void ItemUpdater::processPSUImageAndSyncToLatest() 719 { 720 processPSUImage(); 721 processStoredImage(); 722 syncToLatestImage(); 723 } 724 725 std::string ItemUpdater::getFWVersionFromBuiltinDir() 726 { 727 std::string version; 728 for (const auto& activation : activations) 729 { 730 if (activation.second->path().starts_with(IMG_DIR_BUILTIN)) 731 { 732 std::string versionId = activation.second->getVersionId(); 733 auto it = versions.find(versionId); 734 if (it != versions.end()) 735 { 736 const auto& versionPtr = it->second; 737 version = versionPtr->version(); 738 break; 739 } 740 } 741 } 742 return version; 743 } 744 745 } // namespace updater 746 } // namespace software 747 } // namespace phosphor 748