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