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