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