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 scanDirectory(IMG_DIR_PERSIST); 424 } 425 426 void ItemUpdater::scanDirectory(const fs::path& dir) 427 { 428 auto manifest = dir; 429 auto path = dir; 430 // The directory shall put PSU images in directories named with model 431 if (!fs::exists(dir)) 432 { 433 // Skip 434 return; 435 } 436 if (!fs::is_directory(dir)) 437 { 438 log<level::ERR>("The path is not a directory", 439 entry("PATH=%s", dir.c_str())); 440 return; 441 } 442 443 for (const auto& [key, item] : psuStatusMap) 444 { 445 if (!item.model.empty()) 446 { 447 path = path / item.model; 448 manifest = dir / item.model / MANIFEST_FILE; 449 break; 450 } 451 } 452 if (path == dir) 453 { 454 log<level::ERR>("Model directory not found"); 455 return; 456 } 457 458 if (!fs::is_directory(path)) 459 { 460 log<level::ERR>("The path is not a directory", 461 entry("PATH=%s", path.c_str())); 462 return; 463 } 464 465 if (!fs::exists(manifest)) 466 { 467 log<level::ERR>("No MANIFEST found", 468 entry("PATH=%s", manifest.c_str())); 469 return; 470 } 471 // If the model in manifest does not match the dir name 472 // Log a warning 473 if (fs::is_regular_file(manifest)) 474 { 475 auto ret = Version::getValues( 476 manifest.string(), {MANIFEST_VERSION, MANIFEST_EXTENDED_VERSION}); 477 auto version = ret[MANIFEST_VERSION]; 478 auto extVersion = ret[MANIFEST_EXTENDED_VERSION]; 479 auto info = Version::getExtVersionInfo(extVersion); 480 auto model = info["model"]; 481 if (path.stem() != model) 482 { 483 log<level::ERR>("Unmatched model", entry("PATH=%s", path.c_str()), 484 entry("MODEL=%s", model.c_str())); 485 } 486 else 487 { 488 auto versionId = utils::getVersionId(version); 489 auto it = activations.find(versionId); 490 if (it == activations.end()) 491 { 492 // This is a version that is different than the running PSUs 493 auto activationState = Activation::Status::Ready; 494 auto purpose = VersionPurpose::PSU; 495 auto objPath = std::string(SOFTWARE_OBJPATH) + "/" + versionId; 496 497 auto activation = createActivationObject( 498 objPath, versionId, extVersion, activationState, {}, path); 499 activations.emplace(versionId, std::move(activation)); 500 501 auto versionPtr = 502 createVersionObject(objPath, versionId, version, purpose); 503 versions.emplace(versionId, std::move(versionPtr)); 504 } 505 else 506 { 507 // This is a version that a running PSU is using, set the path 508 // on the version object 509 it->second->path(path); 510 } 511 } 512 } 513 else 514 { 515 log<level::ERR>("MANIFEST is not a file", 516 entry("PATH=%s", manifest.c_str())); 517 } 518 } 519 520 std::optional<std::string> ItemUpdater::getLatestVersionId() 521 { 522 auto latestVersion = utils::getLatestVersion(versionStrings); 523 if (latestVersion.empty()) 524 { 525 return {}; 526 } 527 528 std::optional<std::string> versionId; 529 for (const auto& v : versions) 530 { 531 if (v.second->version() == latestVersion) 532 { 533 versionId = v.first; 534 break; 535 } 536 } 537 assert(versionId.has_value()); 538 return versionId; 539 } 540 541 void ItemUpdater::syncToLatestImage() 542 { 543 auto latestVersionId = getLatestVersionId(); 544 if (!latestVersionId) 545 { 546 return; 547 } 548 const auto& it = activations.find(*latestVersionId); 549 assert(it != activations.end()); 550 const auto& activation = it->second; 551 const auto& assocs = activation->associations(); 552 553 auto paths = utils::getPSUInventoryPath(bus); 554 for (const auto& p : paths) 555 { 556 // As long as there is a PSU is not associated with the latest 557 // image, run the activation so that all PSUs are running the same 558 // latest image. 559 if (!utils::isAssociated(p, assocs)) 560 { 561 log<level::INFO>("Automatically update PSU", 562 entry("VERSION_ID=%s", latestVersionId->c_str())); 563 invokeActivation(activation); 564 break; 565 } 566 } 567 } 568 569 void ItemUpdater::invokeActivation( 570 const std::unique_ptr<Activation>& activation) 571 { 572 activation->requestedActivation(Activation::RequestedActivations::Active); 573 } 574 575 void ItemUpdater::onPSUInterfaceAdded(sdbusplus::message_t& msg) 576 { 577 sdbusplus::message::object_path objPath; 578 std::map<std::string, 579 std::map<std::string, std::variant<bool, std::string>>> 580 interfaces; 581 msg.read(objPath, interfaces); 582 std::string path = objPath.str; 583 584 if (interfaces.find(PSU_INVENTORY_IFACE) == interfaces.end() || 585 (psuStatusMap[path].present && !psuStatusMap[path].model.empty())) 586 { 587 return; 588 } 589 590 auto timeout = std::chrono::steady_clock::now() + 591 std::chrono::seconds(TIMEOUT); 592 593 // Poll the inventory item until it gets the present property 594 // or the timeout is reached 595 while (std::chrono::steady_clock::now() < timeout) 596 { 597 try 598 { 599 psuStatusMap[path].present = utils::getProperty<bool>( 600 bus, msg.get_sender(), path.c_str(), ITEM_IFACE, PRESENT); 601 break; 602 } 603 catch (const std::exception& e) 604 { 605 auto err = errno; 606 log<level::INFO>( 607 std::format("Failed to get Inventory Item Present. errno={}", 608 err) 609 .c_str()); 610 sleep(1); 611 } 612 } 613 614 // Poll the inventory item until it retrieves model or the timeout is 615 // reached. The model is the path trail of the firmware's and manifest 616 // subdirectory. If the model not found the firmware and manifest 617 // cannot be located. 618 timeout = std::chrono::steady_clock::now() + std::chrono::seconds(TIMEOUT); 619 while (std::chrono::steady_clock::now() < timeout && 620 psuStatusMap[path].present) 621 { 622 try 623 { 624 psuStatusMap[path].model = utils::getProperty<std::string>( 625 bus, msg.get_sender(), path.c_str(), ASSET_IFACE, MODEL); 626 processPSUImageAndSyncToLatest(); 627 break; 628 } 629 catch (const std::exception& e) 630 { 631 auto err = errno; 632 log<level::INFO>( 633 std::format( 634 "Failed to get Inventory Decorator Asset model. errno={}", 635 err) 636 .c_str()); 637 sleep(1); 638 } 639 } 640 } 641 642 void ItemUpdater::processPSUImageAndSyncToLatest() 643 { 644 processPSUImage(); 645 processStoredImage(); 646 syncToLatestImage(); 647 } 648 649 } // namespace updater 650 } // namespace software 651 } // namespace phosphor 652