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