diff --git a/src/sm/launcher/runtimes.cpp b/src/sm/launcher/runtimes.cpp index 4b204de6..120300b9 100644 --- a/src/sm/launcher/runtimes.cpp +++ b/src/sm/launcher/runtimes.cpp @@ -40,7 +40,9 @@ Error Runtimes::Init(const Config& config, iamclient::CurrentNodeInfoProviderItf } else if (runtimeConfig.mPlugin == cRuntimeBoot) { auto runtime = std::make_unique(); - if (auto err = runtime->Init(runtimeConfig, currentNodeInfoProvider); !err.IsNone()) { + if (auto err = runtime->Init( + runtimeConfig, currentNodeInfoProvider, itemInfoProvider, ociSpec, statusReceiver, systemdConn); + !err.IsNone()) { return AOS_ERROR_WRAP(err); } diff --git a/src/sm/launcher/runtimes/boot/CMakeLists.txt b/src/sm/launcher/runtimes/boot/CMakeLists.txt index 6a9264c0..75131a7d 100644 --- a/src/sm/launcher/runtimes/boot/CMakeLists.txt +++ b/src/sm/launcher/runtimes/boot/CMakeLists.txt @@ -14,13 +14,29 @@ set(TARGET_NAME boot) # Sources # ###################################################################################################################### -set(SOURCES boot.cpp) +set(SOURCES boot.cpp config.cpp efibootcontroller.cpp efivar.cpp partitionmanager.cpp) # ###################################################################################################################### # Libraries # ###################################################################################################################### -set(LIBRARIES Poco::JSON aos::common::utils aos::core::common::tools) +find_package(PkgConfig REQUIRED) + +pkg_check_modules(BLKID blkid REQUIRED) +pkg_check_modules(EFIVAR efivar REQUIRED) +pkg_check_modules(EFIBOOT efiboot REQUIRED) + +set(LIBRARIES + Poco::JSON + aos::common::utils + aos::sm::config + aos::sm::utils + aos::sm::runtimes::utils + aos::core::common::tools + ${BLKID_LIBRARIES} + ${EFIVAR_LIBRARIES} + ${EFIBOOT_LIBRARIES} +) # ###################################################################################################################### # Target diff --git a/src/sm/launcher/runtimes/boot/boot.cpp b/src/sm/launcher/runtimes/boot/boot.cpp index fc9cfc57..34f419ed 100644 --- a/src/sm/launcher/runtimes/boot/boot.cpp +++ b/src/sm/launcher/runtimes/boot/boot.cpp @@ -4,11 +4,20 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include +#include +#include +#include + #include +#include +#include #include #include "boot.hpp" +#include "efibootcontroller.hpp" +#include "partitionmanager.hpp" namespace aos::sm::launcher { @@ -16,33 +25,93 @@ namespace aos::sm::launcher { * Public **********************************************************************************************************************/ -// cppcheck-suppress constParameterReference -Error BootRuntime::Init(const RuntimeConfig& config, iamclient::CurrentNodeInfoProviderItf& currentNodeInfoProvider) +Error BootRuntime::Init(const RuntimeConfig& config, iamclient::CurrentNodeInfoProviderItf& currentNodeInfoProvider, + imagemanager::ItemInfoProviderItf& itemInfoProvider, oci::OCISpecItf& ociSpec, + InstanceStatusReceiverItf& statusReceiver, sm::utils::SystemdConnItf& systemdConn) { LOG_DBG() << "Init runtime" << Log::Field("type", config.mType.c_str()); - auto nodeInfo = std::make_unique(); + mConfig = config; + mCurrentNodeInfoProvider = ¤tNodeInfoProvider; + mItemInfoProvider = &itemInfoProvider; + mOCISpec = &ociSpec; + mStatusReceiver = &statusReceiver; - if (auto err = currentNodeInfoProvider.GetCurrentNodeInfo(*nodeInfo); !err.IsNone()) { + if (auto err = ParseConfig(config, mBootConfig); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - if (auto err = CreateRuntimeInfo(config.mType, *nodeInfo); !err.IsNone()) { + mPartitionManager = CreatePartitionManager(); + mBootController = CreateBootController(); + + if (auto err = CreateRuntimeInfo(); !err.IsNone()) { return err; } + if (auto err = mSystemdRebooter.Init(systemdConn); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mSystemdUpdateChecker.Init(mBootConfig.mHealthCheckServices, systemdConn); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + return ErrorEnum::eNone; } Error BootRuntime::Start() { + std::lock_guard lock {mMutex}; + LOG_DBG() << "Start runtime"; + if (auto err = mBootController->Init(mBootConfig); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mBootController->SetBootOK(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = InitBootPartitions(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = InitBootData(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = InitInstalledData(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = InitPendingData(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + auto instanceStatuses = std::make_unique>(); + + if (auto err = HandleUpdate(*instanceStatuses); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = instanceStatuses->EmplaceBack(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + ToInstanceStatus(mInstalled, instanceStatuses->Back()); + + if (auto err = mStatusReceiver->OnInstancesStatusesReceived(*instanceStatuses); !err.IsNone()) { + LOG_WRN() << "Failed to send instances statuses" << Log::Field(AOS_ERROR_WRAP(err)); + } + return ErrorEnum::eNone; } Error BootRuntime::Stop() { + std::lock_guard lock {mMutex}; + LOG_DBG() << "Stop runtime"; return ErrorEnum::eNone; @@ -50,6 +119,8 @@ Error BootRuntime::Stop() Error BootRuntime::GetRuntimeInfo(RuntimeInfo& runtimeInfo) const { + std::lock_guard lock {mMutex}; + LOG_DBG() << "Get runtime info"; runtimeInfo = mRuntimeInfo; @@ -59,15 +130,51 @@ Error BootRuntime::GetRuntimeInfo(RuntimeInfo& runtimeInfo) const Error BootRuntime::StartInstance(const InstanceInfo& instance, InstanceStatus& status) { - (void)status; + std::lock_guard lock {mMutex}; - LOG_DBG() << "Start instance" << Log::Field("instance", static_cast(instance)); + LOG_DBG() << "Start instance" << Log::Field("instance", static_cast(instance)) + << Log::Field("digest", instance.mManifestDigest); - return ErrorEnum::eNone; + if (instance.mManifestDigest == mInstalled.mManifestDigest) { + ToInstanceStatus(mInstalled, status); + + return ErrorEnum::eNone; + } + + if (mPending.HasValue()) { + LOG_DBG() << "Another update is already in progress" + << Log::Field("instance", static_cast(*mPending)) + << Log::Field("digest", mPending->mManifestDigest); + + return AOS_ERROR_WRAP(Error(ErrorEnum::eWrongState, "another update is already in progress")); + } + + mPending.EmplaceValue(); + + static_cast(*mPending) = static_cast(instance); + mPending->mManifestDigest = instance.mManifestDigest; + mPending->mState = InstanceStateEnum::eActivating; + mPending->mPartitionIndex = GetNextPartitionIndex(mInstalled.mPartitionIndex.GetValue()); + + if (auto err = StoreData(cPendingInstance, *mPending); !err.IsNone()) { + mPending->mError = AOS_ERROR_WRAP(err); + mPending->mState = InstanceStateEnum::eFailed; + } + + if (auto err = InstallPendingUpdate(); !err.IsNone()) { + mPending->mError = AOS_ERROR_WRAP(err); + mPending->mState = InstanceStateEnum::eFailed; + } + + ToInstanceStatus(*mPending, status); + + return mPending->mError; } Error BootRuntime::StopInstance(const InstanceIdent& instance, InstanceStatus& status) { + std::lock_guard lock {mMutex}; + (void)status; LOG_DBG() << "Stop instance" << Log::Field("instance", instance); @@ -77,14 +184,18 @@ Error BootRuntime::StopInstance(const InstanceIdent& instance, InstanceStatus& s Error BootRuntime::Reboot() { + std::lock_guard lock {mMutex}; + LOG_DBG() << "Reboot runtime"; - return ErrorEnum::eNotSupported; + return mSystemdRebooter.Reboot(); } Error BootRuntime::GetInstanceMonitoringData( const InstanceIdent& instanceIdent, monitoring::InstanceMonitoringData& monitoringData) { + std::lock_guard lock {mMutex}; + (void)monitoringData; LOG_DBG() << "Get instance monitoring data" << Log::Field("instance", instanceIdent); @@ -96,20 +207,118 @@ Error BootRuntime::GetInstanceMonitoringData( * Private **********************************************************************************************************************/ -Error BootRuntime::CreateRuntimeInfo(const std::string& runtimeType, const NodeInfo& nodeInfo) +std::shared_ptr BootRuntime::CreatePartitionManager() const +{ + return std::make_shared(); +} + +std::shared_ptr BootRuntime::CreateBootController() const +{ + return std::make_shared(); +} + +Error BootRuntime::InitBootPartitions() +{ + if (auto err = mBootController->GetPartitionDevices(mPartitionDevices); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + for (const auto& device : mPartitionDevices) { + LOG_DBG() << "Found partition device" << Log::Field("device", device.c_str()); + } + + if (mPartitionDevices.size() != cNumBootPartitions) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, "unexpected number of boot partitions")); + } + + return ErrorEnum::eNone; +} + +Error BootRuntime::InitBootData() { - auto runtimeID = runtimeType + "-" + nodeInfo.mNodeID.CStr(); + Error err = ErrorEnum::eNone; + + Tie(mCurrentPartition, err) = mBootController->GetCurrentBoot(); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + Tie(mMainPartition, err) = mBootController->GetMainBoot(); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + Tie(mCurrentPartitionVersion, err) = GetPartitionVersion(mCurrentPartition); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error BootRuntime::InitInstalledData() +{ + if (!std::filesystem::exists(GetPath(cInstalledInstance))) { + mInstalled.mPartitionIndex.SetValue(mCurrentPartition); + static_cast(mInstalled) = mDefaultInstanceIdent; + mInstalled.mVersion = mCurrentPartitionVersion.c_str(); + + if (auto err = StoreData(cInstalledInstance, mInstalled); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + } + + if (auto err = LoadData(cInstalledInstance, mInstalled); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error BootRuntime::InitPendingData() +{ + if (!std::filesystem::exists(GetPath(cPendingInstance))) { + return ErrorEnum::eNone; + } + + mPending.EmplaceValue(); + + if (auto err = LoadData(cPendingInstance, *mPending); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (mPending->mPartitionIndex.HasValue() && mPending->mPartitionIndex.GetValue() == mCurrentPartition) { + mPending->mVersion = mCurrentPartitionVersion.c_str(); + } + + return ErrorEnum::eNone; +} + +Error BootRuntime::CreateRuntimeInfo() +{ + auto nodeInfo = std::make_unique(); + + if (auto err = mCurrentNodeInfoProvider->GetCurrentNodeInfo(*nodeInfo); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + auto runtimeID = mConfig.mType + "-" + nodeInfo->mNodeID.CStr(); if (auto err = mRuntimeInfo.mRuntimeID.Assign(common::utils::NameUUID(runtimeID).c_str()); !err.IsNone()) { return AOS_ERROR_WRAP(err); } - if (auto err = mRuntimeInfo.mRuntimeType.Assign(runtimeType.c_str()); !err.IsNone()) { + if (auto err = mRuntimeInfo.mRuntimeType.Assign(mConfig.mType.c_str()); !err.IsNone()) { return AOS_ERROR_WRAP(err); } mRuntimeInfo.mMaxInstances = 1; + mDefaultInstanceIdent.mType = UpdateItemTypeEnum::eComponent; + mDefaultInstanceIdent.mInstance = 0; + mDefaultInstanceIdent.mItemID = mRuntimeInfo.mRuntimeType; + mDefaultInstanceIdent.mSubjectID = nodeInfo->mNodeType; + LOG_INF() << "Runtime info" << Log::Field("runtimeID", mRuntimeInfo.mRuntimeID) << Log::Field("runtimeType", mRuntimeInfo.mRuntimeType) << Log::Field("maxInstances", mRuntimeInfo.mMaxInstances); @@ -117,4 +326,374 @@ Error BootRuntime::CreateRuntimeInfo(const std::string& runtimeType, const NodeI return ErrorEnum::eNone; } +Error BootRuntime::HandleUpdate(Array& statuses) +{ + if (!mPending.HasValue()) { + LOG_DBG() << "No pending updates"; + + return ErrorEnum::eNone; + } + + LOG_DBG() << "Handle update"; + + statuses.EmplaceBack(); + + if (mPending->mState == InstanceStateEnum::eFailed) { + return HandleUpdateFailed(statuses.Back()); + } + + if (auto err = mSystemdUpdateChecker.Check(); !err.IsNone()) { + mPending->mError = AOS_ERROR_WRAP(err); + mPending->mState = InstanceStateEnum::eFailed; + + return HandleUpdateFailed(statuses.Back()); + } + + return HandleUpdateSucceeded(statuses.Back()); +} + +Error BootRuntime::HandleUpdateSucceeded(InstanceStatus& status) +{ + if (mCurrentPartition != *mPending->mPartitionIndex) { + ToInstanceStatus(*mPending, status); + + if (auto err = mBootController->SetMainBoot(*mPending->mPartitionIndex); !err.IsNone()) { + status.mError = AOS_ERROR_WRAP(err); + status.mState = InstanceStateEnum::eFailed; + + return status.mError; + } + + if (auto err = mStatusReceiver->RebootRequired(mRuntimeInfo.mRuntimeID); !err.IsNone()) { + status.mError = AOS_ERROR_WRAP(err); + status.mState = InstanceStateEnum::eFailed; + + return status.mError; + } + + return ErrorEnum::eNone; + } + + mInstalled.mState = InstanceStateEnum::eInactive; + mPending->mState = InstanceStateEnum::eActive; + + if (auto err = SyncPartition(*mPending->mPartitionIndex, *mInstalled.mPartitionIndex); !err.IsNone()) { + status.mError = AOS_ERROR_WRAP(err); + status.mState = InstanceStateEnum::eFailed; + } + + ToInstanceStatus(mInstalled, status); + + return CompletePendingUpdate(); +} + +Error BootRuntime::HandleUpdateFailed(InstanceStatus& status) +{ + ToInstanceStatus(*mPending, status); + + return CompletePendingUpdate(); +} + +Error BootRuntime::CompletePendingUpdate() +{ + Error err = ErrorEnum::eNone; + + if (mPending->mState == InstanceStateEnum::eActive) { + mInstalled = *mPending; + + if (auto storeErr = StoreData(cInstalledInstance, mInstalled); !storeErr.IsNone()) { + err = AOS_ERROR_WRAP(storeErr); + } + } + + if (!std::filesystem::remove(GetPath(cPendingInstance))) { + err = AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, "can't remove pending instance info")); + } + + if (auto syncErr = SyncPartition(*mInstalled.mPartitionIndex, *mPending->mPartitionIndex); + err.IsNone() && !syncErr.IsNone()) { + err = AOS_ERROR_WRAP(syncErr); + } + + mPending.Reset(); + + return err; +} + +RetWithError BootRuntime::GetPartitionVersion(size_t partitionIndex) const +{ + const auto mountDst = std::filesystem::path(mBootConfig.mWorkingDir) / cMountDirName; + const auto partition = mPartitionDevices.at(partitionIndex); + + LOG_DBG() << "Mount partition" << Log::Field("partition", partition.c_str()) + << Log::Field("mountDst", mountDst.c_str()); + + std::string version; + + if (auto err = fs::MakeDirAll(mountDst.c_str()); !err.IsNone()) { + return {version, AOS_ERROR_WRAP(err)}; + } + + auto cleanup = DeferRelease(&mountDst, [](const auto* path) { fs::RemoveAll(path->c_str()); }); + + PartInfo partInfo; + + if (auto err = mPartitionManager->GetPartInfo(partition, partInfo); !err.IsNone()) { + return {version, AOS_ERROR_WRAP(err)}; + } + + if (auto err = mPartitionManager->Mount(partInfo, mountDst.c_str(), MS_RDONLY); !err.IsNone()) { + return {version, AOS_ERROR_WRAP(err)}; + } + + auto umount = DeferRelease(&mountDst, [this](const auto* path) { + if (auto err = mPartitionManager->Unmount(*path); !err.IsNone()) { + LOG_ERR() << "Failed to unmount partition" << Log::Field(err); + } + }); + + const auto versionFilePath = mountDst / mBootConfig.mVersionFile; + + LOG_DBG() << "Read version file" << Log::Field("path", versionFilePath.c_str()); + + std::ifstream versionFile(versionFilePath); + if (!versionFile.is_open()) { + return {version, AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, "can't open version file"))}; + } + + std::string line; + + std::getline(versionFile, line); + + LOG_DBG() << "Version file content" << Log::Field("line", line.c_str()); + + std::regex versionRegex(R"(VERSION\s*=\s*\"(.+)\")"); + std::smatch match; + + if (std::regex_search(line, match, versionRegex) && match.size() == 2) { + version = match[1]; + } else { + return {version, AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, "invalid version file format"))}; + } + + return version; +} + +void BootRuntime::ToInstanceStatus(const BootData& data, InstanceStatus& status) const +{ + static_cast(status) = static_cast(data); + status.mManifestDigest = data.mManifestDigest; + status.mState = data.mState; + status.mVersion = data.mVersion; + status.mRuntimeID = mRuntimeInfo.mRuntimeID; + status.mType = UpdateItemTypeEnum::eComponent; + status.mPreinstalled = static_cast(data) == mDefaultInstanceIdent; +} + +Error BootRuntime::InstallPendingUpdate() +{ + LOG_DBG() << "Install pending update" << Log::Field("digest", mPending->mManifestDigest) + << Log::Field("partitionIndex", mPending->mPartitionIndex); + + auto manifest = std::make_unique(); + + if (auto err = GetImageManifest(mPending->mManifestDigest, *manifest); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = InstallImageOnPartition(*manifest, mPending->mPartitionIndex.GetValue()); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mBootController->SetMainBoot(mPending->mPartitionIndex.GetValue()); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mStatusReceiver->RebootRequired(mRuntimeInfo.mRuntimeID); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error BootRuntime::GetImageManifest(const String& digest, oci::ImageManifest& manifest) const +{ + LOG_DBG() << "Get image manifest" << Log::Field("digest", digest); + + StaticString blobPath; + + if (auto err = mItemInfoProvider->GetBlobPath(digest, blobPath); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mOCISpec->LoadImageManifest(blobPath, manifest); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error BootRuntime::InstallImageOnPartition(const oci::ImageManifest& manifest, size_t partitionIndex) +{ + LOG_DBG() << "Install image on partition" << Log::Field("partitionIndex", partitionIndex); + + if (manifest.mLayers.Size() == 0) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "image manifest has no layers")); + } + + const auto imagesDir = GetPath(cImagesDir); + const auto packedImagePath = imagesDir / "boot.img.gz"; + const auto unpackedImagePath = imagesDir / "boot.img"; + + if (auto err = fs::ClearDir(imagesDir.c_str()); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + StaticString imageArchivePath; + + if (auto err = mItemInfoProvider->GetBlobPath(manifest.mLayers[0].mDigest, imageArchivePath); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + std::error_code ec; + std::filesystem::copy_file(imageArchivePath.CStr(), packedImagePath, ec); + + if (ec) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, ec.message().c_str())); + } + + if (auto res = common::utils::ExecCommand({"gunzip", packedImagePath.c_str()}); !res.mError.IsNone()) { + return AOS_ERROR_WRAP(res.mError); + } + + auto cleanup = DeferRelease(&imagesDir, [](const auto* path) { fs::RemoveAll(path->c_str()); }); + + try { + const auto& toDevice = mPartitionDevices.at(partitionIndex); + + LOG_DBG() << "Install image" << Log::Field("image", unpackedImagePath.c_str()) + << Log::Field("toDevice", toDevice.c_str()); + + if (auto err = mPartitionManager->InstallImage(unpackedImagePath.c_str(), toDevice); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + } catch (const std::exception& e) { + return AOS_ERROR_WRAP(common::utils::ToAosError(e)); + } + + return ErrorEnum::eNone; +} + +Error BootRuntime::SyncPartition(size_t from, size_t to) +{ + if (from == to) { + return ErrorEnum::eNone; + } + + try { + const auto& fromDevice = mPartitionDevices.at(from); + const auto& toDevice = mPartitionDevices.at(to); + + LOG_DBG() << "Sync partition" << Log::Field("from", fromDevice.c_str()) << Log::Field("to", toDevice.c_str()); + + return mPartitionManager->CopyDevice(fromDevice, toDevice); + } catch (const std::exception& e) { + return AOS_ERROR_WRAP(common::utils::ToAosError(e)); + } + + return ErrorEnum::eNone; +} + +Error BootRuntime::StoreData(const std::string_view filename, const BootData& data) +{ + const auto path = GetPath(filename); + + LOG_DBG() << "Store data" << Log::Field("ident", static_cast(data)) + << Log::Field("digest", data.mManifestDigest) << Log::Field("state", data.mState) + << Log::Field("path", path.c_str()); + + std::ofstream file(path); + if (!file.is_open()) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, "can't open file")); + } + + auto json = Poco::makeShared(Poco::JSON_PRESERVE_KEY_ORDER); + + try { + json->set("itemId", data.mItemID.CStr()); + json->set("subjectId", data.mSubjectID.CStr()); + json->set("instance", data.mInstance); + json->set("manifestDigest", data.mManifestDigest.CStr()); + json->set("state", data.mState.ToString().CStr()); + json->set("type", data.mType.ToString().CStr()); + json->set("version", data.mVersion.CStr()); + + if (data.mPartitionIndex.HasValue()) { + json->set("partitionIndex", data.mPartitionIndex.GetValue()); + } + + json->stringify(file); + } catch (const std::exception& e) { + return AOS_ERROR_WRAP(common::utils::ToAosError(e)); + } + + return ErrorEnum::eNone; +} + +Error BootRuntime::LoadData(const std::string_view filename, BootData& data) +{ + const auto path = GetPath(filename); + + LOG_DBG() << "Load data" << Log::Field("path", path.c_str()); + + std::ifstream file(path); + if (!file.is_open()) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, "can't open file")); + } + + try { + auto parseResult = common::utils::ParseJson(file); + AOS_ERROR_CHECK_AND_THROW(parseResult.mError, "can't parse json"); + + const auto object = common::utils::CaseInsensitiveObjectWrapper(parseResult.mValue); + + auto err = data.mItemID.Assign(object.GetValue("itemId").c_str()); + AOS_ERROR_CHECK_AND_THROW(err, "can't parse itemID"); + + err = data.mSubjectID.Assign(object.GetValue("subjectId").c_str()); + AOS_ERROR_CHECK_AND_THROW(err, "can't parse subjectID"); + + data.mInstance = object.GetValue("instance"); + + err = data.mManifestDigest.Assign(object.GetValue("manifestDigest").c_str()); + AOS_ERROR_CHECK_AND_THROW(err, "can't parse manifestDigest"); + + err = data.mState.FromString(object.GetValue("state").c_str()); + AOS_ERROR_CHECK_AND_THROW(err, "can't parse instance state"); + + err = data.mVersion.Assign(object.GetValue("version").c_str()); + AOS_ERROR_CHECK_AND_THROW(err, "can't parse version"); + + if (object.Has("partitionIndex")) { + data.mPartitionIndex.SetValue(object.GetValue("partitionIndex")); + } + + data.mType = UpdateItemTypeEnum::eComponent; + } catch (const std::exception& e) { + return AOS_ERROR_WRAP(common::utils::ToAosError(e)); + } + + return ErrorEnum::eNone; +} + +std::filesystem::path BootRuntime::GetPath(const std::string_view relativePath) const +{ + return std::filesystem::absolute(mBootConfig.mWorkingDir) / relativePath; +} + +size_t BootRuntime::GetNextPartitionIndex(size_t currentPartition) const +{ + return (currentPartition + 1) % cNumBootPartitions; +} + } // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/boot/boot.hpp b/src/sm/launcher/runtimes/boot/boot.hpp index 25598832..08e044fa 100644 --- a/src/sm/launcher/runtimes/boot/boot.hpp +++ b/src/sm/launcher/runtimes/boot/boot.hpp @@ -7,10 +7,23 @@ #ifndef AOS_SM_LAUNCHER_RUNTIMES_BOOT_BOOT_HPP_ #define AOS_SM_LAUNCHER_RUNTIMES_BOOT_BOOT_HPP_ +#include +#include +#include + #include +#include +#include +#include #include #include +#include +#include + +#include "config.hpp" +#include "itf/bootcontroller.hpp" +#include "itf/partitionmanager.hpp" namespace aos::sm::launcher { @@ -18,6 +31,7 @@ namespace aos::sm::launcher { * Boot runtime name. */ constexpr auto cRuntimeBoot = "boot"; + /** * Boot runtime implementation. */ @@ -28,9 +42,15 @@ class BootRuntime : public RuntimeItf { * * @param config runtime config. * @param currentNodeInfoProvider current node info provider. + * @param itemInfoProvider item info provider. + * @param ociSpec OCI spec interface. + * @param statusReceiver instance status receiver. + * @param systemdConn systemd connection. * @return Error. */ - Error Init(const RuntimeConfig& config, iamclient::CurrentNodeInfoProviderItf& currentNodeInfoProvider); + Error Init(const RuntimeConfig& config, iamclient::CurrentNodeInfoProviderItf& currentNodeInfoProvider, + imagemanager::ItemInfoProviderItf& itemInfoProvider, oci::OCISpecItf& ociSpec, + InstanceStatusReceiverItf& statusReceiver, sm::utils::SystemdConnItf& systemdConn); /** * Starts runtime. @@ -90,9 +110,65 @@ class BootRuntime : public RuntimeItf { const InstanceIdent& instanceIdent, monitoring::InstanceMonitoringData& monitoringData) override; private: - Error CreateRuntimeInfo(const std::string& runtimeType, const NodeInfo& nodeInfo); - - RuntimeInfo mRuntimeInfo; + static constexpr auto cNumBootPartitions = 2; + static constexpr auto cInstalledInstance = "installed.json"; + static constexpr auto cPendingInstance = "pending.json"; + static constexpr auto cImagesDir = "images"; + static constexpr auto cMountDirName = "mnt"; + static constexpr auto cUpdateStateFile = "update.state"; + + struct BootData : public InstanceIdent { + StaticString mVersion; + InstanceState mState {InstanceStateEnum::eActive}; + StaticString mManifestDigest; + Error mError; + Optional mPartitionIndex; + }; + + virtual std::shared_ptr CreatePartitionManager() const; + virtual std::shared_ptr CreateBootController() const; + + Error InitBootPartitions(); + Error InitBootData(); + Error InitInstalledData(); + Error InitPendingData(); + Error CreateRuntimeInfo(); + Error HandleUpdate(Array& statuses); + Error HandleUpdateSucceeded(InstanceStatus& status); + Error HandleUpdateFailed(InstanceStatus& status); + Error CompletePendingUpdate(); + RetWithError GetPartitionVersion(size_t partitionIndex) const; + Error GetPartInfo(const std::string& partDevice, PartInfo& partInfo) const; + Error StoreUpdateState() const; + void ToInstanceStatus(const BootData& data, InstanceStatus& status) const; + Error InstallPendingUpdate(); + Error GetImageManifest(const String& digest, oci::ImageManifest& manifest) const; + Error InstallImageOnPartition(const oci::ImageManifest& manifest, size_t partitionIndex); + Error SyncPartition(size_t from, size_t to); + Error StoreData(const std::string_view filename, const BootData& data); + Error LoadData(const std::string_view filename, BootData& data); + std::filesystem::path GetPath(const std::string_view relativePath) const; + size_t GetNextPartitionIndex(size_t currentPartition) const; + + mutable std::mutex mMutex; + std::shared_ptr mPartitionManager; + std::shared_ptr mBootController; + iamclient::CurrentNodeInfoProviderItf* mCurrentNodeInfoProvider {}; + imagemanager::ItemInfoProviderItf* mItemInfoProvider {}; + oci::OCISpecItf* mOCISpec {}; + InstanceStatusReceiverItf* mStatusReceiver {}; + RuntimeConfig mConfig; + BootConfig mBootConfig; + InstanceIdent mDefaultInstanceIdent; + utils::SystemdRebooter mSystemdRebooter; + utils::SystemdUpdateChecker mSystemdUpdateChecker; + RuntimeInfo mRuntimeInfo; + size_t mMainPartition {}; + size_t mCurrentPartition {}; + std::string mCurrentPartitionVersion; + BootData mInstalled; + Optional mPending; + std::vector mPartitionDevices; }; } // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/boot/config.cpp b/src/sm/launcher/runtimes/boot/config.cpp new file mode 100644 index 00000000..b9287042 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/config.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "config.hpp" + +namespace aos::sm::launcher { + +namespace { + +/*********************************************************************************************************************** + * Consts + **********************************************************************************************************************/ + +constexpr auto cDefaultBootRuntimeDir = "runtimes/boot"; +constexpr auto cDefaultBootVersionFile = "aos/version"; + +} // namespace + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +Error ParseConfig(const RuntimeConfig& config, BootConfig& bootConfig) +{ + try { + const auto object = common::utils::CaseInsensitiveObjectWrapper(config.mConfig); + + bootConfig.mWorkingDir = object.GetValue( + "workingDir", common::utils::JoinPath(config.mWorkingDir, cDefaultBootRuntimeDir)); + bootConfig.mLoader = object.GetValue("loader"); + bootConfig.mVersionFile = object.GetValue("versionFile", cDefaultBootVersionFile); + + auto err = bootConfig.mDetectMode.FromString(object.GetValue("detectMode").c_str()); + AOS_ERROR_CHECK_AND_THROW(err, "invalid detect mode in boot runtime config"); + + bootConfig.mPartitions = common::utils::GetArrayValue( + object, "partitions", [](const Poco::Dynamic::Var& value) { return value.convert(); }); + bootConfig.mHealthCheckServices = common::utils::GetArrayValue(object, "healthCheckServices", + [](const Poco::Dynamic::Var& value) { return value.convert(); }); + } catch (const std::exception& e) { + return common::utils::ToAosError(e); + } + + return ErrorEnum::eNone; +} + +} // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/boot/config.hpp b/src/sm/launcher/runtimes/boot/config.hpp new file mode 100644 index 00000000..649d0f42 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/config.hpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_SM_LAUNCHER_RUNTIMES_BOOT_CONFIG_HPP_ +#define AOS_SM_LAUNCHER_RUNTIMES_BOOT_CONFIG_HPP_ + +#include +#include +#include + +#include +#include + +#include +#include + +namespace aos::sm::launcher { + +/** + * Boot detect mode type. + * + */ +class BootDetectModeType { +public: + enum class Enum { + eNone, + eAuto, + }; + + static const Array GetStrings() + { + static const char* const sStrings[] = { + "", + "auto", + }; + + return Array(sStrings, ArraySize(sStrings)); + }; +}; + +using BootDetectModeEnum = BootDetectModeType::Enum; +using BootDetectMode = EnumStringer; + +/** + * Boot runtime config. + */ +struct BootConfig { + std::string mWorkingDir; + std::string mLoader; + BootDetectMode mDetectMode; + std::string mVersionFile; + std::vector mPartitions; + std::vector mHealthCheckServices; +}; + +/** + * Parses boot runtime config. + * + * @param config runtime config. + * @param[out] bootConfig boot runtime config. + */ +Error ParseConfig(const RuntimeConfig& config, BootConfig& bootConfig); + +} // namespace aos::sm::launcher + +#endif diff --git a/src/sm/launcher/runtimes/boot/efibootcontroller.cpp b/src/sm/launcher/runtimes/boot/efibootcontroller.cpp new file mode 100644 index 00000000..2fddae81 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/efibootcontroller.cpp @@ -0,0 +1,433 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include + +#include + +#include "efibootcontroller.hpp" +#include "efivar.hpp" + +namespace aos::sm::launcher { + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +Error EFIBootController::Init(const BootConfig& config) +{ + std::lock_guard lock {mMutex}; + + LOG_DBG() << "Init EFI boot controller"; + + mConfig = config; + + mPartitionManager = CreatePartitionManager(); + mEFIVar = CreateEFIVar(); + + if (auto err = InitBootPartitions(config); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + for (const auto& bootItem : mBootItems) { + LOG_DBG() << "Configured boot item" << bootItem; + } + + return ErrorEnum::eNone; +} + +Error EFIBootController::GetPartitionDevices(std::vector& devices) const +{ + std::lock_guard lock {mMutex}; + + LOG_DBG() << "Get boot partition devices" << Log::Field("count", mBootItems.size()); + + std::transform(mBootItems.begin(), mBootItems.end(), std::back_inserter(devices), + [](const BootItem& item) { return item.mDevice; }); + + return ErrorEnum::eNone; +} + +RetWithError EFIBootController::GetCurrentBoot() const +{ + std::lock_guard lock {mMutex}; + + auto [efiCurrentBoot, err] = GetBootCurrent(); + if (!err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + LOG_DBG() << "Get EFI current boot" << Log::Field("bootID", efiCurrentBoot); + + auto itBootItem = std::find_if(mBootItems.begin(), mBootItems.end(), + [efiCurrentBoot](const BootItem& item) { return item.mID == efiCurrentBoot; }); + + if (itBootItem == mBootItems.end()) { + LOG_WRN() << "Boot from an unknown partition" << Log::Field("bootID", efiCurrentBoot); + + return 0; + } + + return std::distance(mBootItems.begin(), itBootItem); +} + +RetWithError EFIBootController::GetMainBoot() const +{ + std::lock_guard lock {mMutex}; + + LOG_DBG() << "Get main boot"; + + auto [currentBootOrder, err] = GetBootOrder(); + if (!err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + if (currentBootOrder.empty()) { + return {0, Error(ErrorEnum::eNotFound, "boot order is empty")}; + } + + auto itBootItem = std::find_if(mBootItems.begin(), mBootItems.end(), + [first = currentBootOrder[0]](const BootItem& item) { return item.mID == first; }); + + if (itBootItem == mBootItems.end()) { + return {0, AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "current boot entry not found"))}; + } + + return std::distance(mBootItems.begin(), itBootItem); +} + +Error EFIBootController::SetMainBoot(size_t index) +{ + std::lock_guard lock {mMutex}; + + if (mBootItems.size() <= index) { + LOG_DBG() << "Set main boot" << Log::Field("index", index); + + return Error(ErrorEnum::eOutOfRange, "wrong main boot index"); + } + + const auto bootID = mBootItems[index].mID; + + LOG_DBG() << "Set main boot" << Log::Field("index", index) << Log::Field("bootID", bootID); + + if (auto err = mEFIVar->WriteGlobalGuidVariable(cBootNextName, ToUint8({bootID})); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error EFIBootController::SetBootOK() +{ + std::lock_guard lock {mMutex}; + + LOG_DBG() << "Set boot OK"; + + auto [bootCurrent, err] = GetBootOrder(); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + uint16_t currentBootID = 0; + + Tie(currentBootID, err) = GetBootCurrent(); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (std::find_if(mBootItems.begin(), mBootItems.end(), + [currentBootID](const BootItem& item) { return item.mID == currentBootID; }) + == mBootItems.end()) { + LOG_DBG() << "Current boot partition is not in configured ones" << Log::Field("currentBootID", currentBootID); + + return ErrorEnum::eNone; + } + + if (std::find(bootCurrent.begin(), bootCurrent.end(), currentBootID) == bootCurrent.end()) { + LOG_WRN() << "Current boot ID not found in boot order, nothing to do" + << Log::Field("currentBootID", currentBootID); + + return ErrorEnum::eNone; + } + + err = UpdateBootOrder({currentBootID}, bootCurrent); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ + +std::shared_ptr EFIBootController::CreateEFIVar() const +{ + return std::make_shared(); +} + +std::shared_ptr EFIBootController::CreatePartitionManager() const +{ + return std::make_shared(); +} + +RetWithError> EFIBootController::ReadBootEntries() +{ + LOG_DBG() << "Read EFI boot entries"; + + const std::regex cRegEx(cBootItemPattern); + + std::vector bootItems; + + auto [efiVariables, err] = mEFIVar->GetAllVariables(); + if (!err.IsNone()) { + return {{}, AOS_ERROR_WRAP(err)}; + } + + for (const auto& efiVariable : efiVariables) { + std::smatch match; + if (!std::regex_search(efiVariable, match, cRegEx) || match.size() != 3) { + continue; + } + + const auto hexBootID = match[2].str(); + + BootItem item; + + Tie(item.mID, err) = ConvertHex(hexBootID); + + if (!err.IsNone()) { + LOG_DBG() << "Failed to convert EFI boot ID from hex string" << AOS_ERROR_WRAP(err); + + continue; + } + + Tie(item.mPartitionUUID, err) = mEFIVar->GetPartUUID(efiVariable); + if (!err.IsNone() && !err.Is(ErrorEnum::eNotFound)) { + LOG_ERR() << "EFI boot entry has no associated partition UUID" << Log::Field("bootID", item.mID); + + continue; + } + + bootItems.push_back(std::move(item)); + } + + std::sort(bootItems.begin(), bootItems.end(), [](const BootItem& a, const BootItem& b) { return a.mID < b.mID; }); + + return bootItems; +} + +RetWithError EFIBootController::ConvertHex(const std::string& hexStr) const +{ + uint16_t result = 0; + + const auto res = std::from_chars(hexStr.data(), hexStr.data() + hexStr.size(), result, 16); + if (res.ec != std::errc()) { + return {0, Error(ErrorEnum::eInvalidArgument, "invalid hex string")}; + } + + return result; +} + +Error EFIBootController::InitBootPartitions(const BootConfig& config) +{ + Error err = ErrorEnum::eNone; + std::string partitionPrefix; + + if (config.mDetectMode == BootDetectModeEnum::eAuto) { + Tie(partitionPrefix, err) = GetPartitionPrefix(); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + } + + for (const auto& partition : config.mPartitions) { + const auto device = partitionPrefix + partition; + + PartInfo partInfo; + + err = mPartitionManager->GetPartInfo(device, partInfo); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + BootItem item; + item.mDevice = device; + item.mParentDevice = partInfo.mParentDevice; + item.mPartitionNumber = partInfo.mPartitionNumber; + item.mPartitionUUID = partInfo.mPartUUID; + + mBootItems.push_back(std::move(item)); + } + + err = SetPartitionIDs(); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error EFIBootController::SetPartitionIDs() +{ + auto [efiBootItems, err] = ReadBootEntries(); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + size_t nextAvailableID = efiBootItems.empty() ? 0 : efiBootItems.back().mID + 1; + + std::vector newBootIDs; + + for (auto& bootItem : mBootItems) { + if (auto it = std::find_if(efiBootItems.begin(), efiBootItems.end(), + [&bootItem](const BootItem& item) { return item.mPartitionUUID == bootItem.mPartitionUUID; }); + it != efiBootItems.end()) { + bootItem.mID = it->mID; + + continue; + } + + bootItem.mID = nextAvailableID++; + + err = mEFIVar->CreateBootEntry( + bootItem.mParentDevice, bootItem.mPartitionNumber, GetLoaderPath(), bootItem.mID); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + newBootIDs.push_back(bootItem.mID); + + LOG_DBG() << "Created new boot entry" << Log::Field("item", bootItem); + } + + err = UpdateBootOrder(newBootIDs); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +RetWithError> EFIBootController::GetBootOrder() const +{ + auto [result, err] = ReadVariable(cBootOrderName); + if (!err.IsNone()) { + return {{}, AOS_ERROR_WRAP(err)}; + } + + return result; +} + +RetWithError EFIBootController::GetBootCurrent() const +{ + auto [result, err] = ReadVariable(cBootCurrentName); + if (!err.IsNone()) { + return {0, AOS_ERROR_WRAP(err)}; + } + + if (result.size() != 1) { + return {0, Error(ErrorEnum::eInvalidArgument, "invalid variable size")}; + } + + return result[0]; +} + +RetWithError EFIBootController::GetPartitionPrefix() const +{ + LOG_DBG() << "Get partition prefix from /proc/cmdline"; + + std::ifstream file("/proc/cmdline"); + if (!file.is_open()) { + return {"", Error(ErrorEnum::eFailed, "can't open /proc/cmdline")}; + } + + std::string cmdline; + std::getline(file, cmdline); + + std::regex rootRegex(R"(root=([^ \t]+))"); + std::smatch match; + + if (!std::regex_search(cmdline, match, rootRegex)) { + return {"", Error(ErrorEnum::eNotFound, "root device not found in /proc/cmdline")}; + } + + std::string device = match[1]; + + while (!device.empty() && std::isdigit(device.back())) { + device.pop_back(); + } + + return device; +} + +std::string EFIBootController::GetLoaderPath() const +{ + return mConfig.mLoader.empty() ? cDefaultLoader : mConfig.mLoader; +} + +RetWithError> EFIBootController::ReadVariable(const std::string& name) const +{ + std::vector data; + uint32_t attributes = 0; + + if (auto err = mEFIVar->ReadVariable(name, data, attributes); !err.IsNone()) { + return {{}, AOS_ERROR_WRAP(err)}; + } + + return ToUint16(data); +} + +std::vector EFIBootController::ToUint16(const std::vector& data) const +{ + return std::vector { + reinterpret_cast(data.data()), reinterpret_cast(data.data() + data.size())}; +} + +std::vector EFIBootController::ToUint8(const std::vector& data) const +{ + return std::vector {reinterpret_cast(data.data()), + reinterpret_cast(data.data()) + data.size() * sizeof(uint16_t)}; +} + +Error EFIBootController::UpdateBootOrder(const std::vector& newBootIDs) +{ + if (newBootIDs.empty()) { + return ErrorEnum::eNone; + } + + auto [oldBootOrder, err] = GetBootOrder(); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return UpdateBootOrder(newBootIDs, oldBootOrder); +} + +Error EFIBootController::UpdateBootOrder( + const std::vector& newBootIDs, const std::vector& oldBootOrder) +{ + std::vector newBootOrder = newBootIDs; + + std::copy_if(oldBootOrder.begin(), oldBootOrder.end(), std::back_inserter(newBootOrder), + [&newBootIDs](uint16_t id) { return std::find(newBootIDs.begin(), newBootIDs.end(), id) == newBootIDs.end(); }); + + if (newBootOrder == oldBootOrder) { + LOG_DBG() << "Boot order is up to date, nothing to do"; + + return ErrorEnum::eNone; + } + + return mEFIVar->WriteGlobalGuidVariable(cBootOrderName, ToUint8(newBootOrder)); +} + +}; // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/boot/efibootcontroller.hpp b/src/sm/launcher/runtimes/boot/efibootcontroller.hpp new file mode 100644 index 00000000..65fc7cb5 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/efibootcontroller.hpp @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_SM_LAUNCHER_RUNTIMES_BOOT_EFIBOOTCONTROLLER_HPP_ +#define AOS_SM_LAUNCHER_RUNTIMES_BOOT_EFIBOOTCONTROLLER_HPP_ + +#include +#include +#include + +#include + +#include "config.hpp" +#include "itf/bootcontroller.hpp" +#include "itf/efivar.hpp" +#include "partitionmanager.hpp" + +namespace aos::sm::launcher { + +/** + * EFI boot controller. + */ +class EFIBootController : public BootControllerItf { +public: + /** + * Initializes boot controller. + * + * @param config boot config. + * @return Error. + */ + Error Init(const BootConfig& config) override; + + /** + * Returns boot partition devices. + * + * @param[out] devices boot partition devices. + * @return Error. + */ + Error GetPartitionDevices(std::vector& devices) const override; + + /** + * Returns current boot index. + * + * @return RetWithError. + */ + RetWithError GetCurrentBoot() const override; + + /** + * Returns main boot index. + * + * @return RetWithError. + */ + RetWithError GetMainBoot() const override; + + /** + * Sets the main boot partition index. + * + * @param index main boot partition index. + * @return Error. + */ + Error SetMainBoot(size_t index) override; + + /** + * Sets boot successful flag. + * + * @return Error. + */ + Error SetBootOK() override; + +private: + static constexpr auto cDefaultLoader = "/EFI/BOOT/bootx64.efi"; + static constexpr auto cBootItemPattern = "(^Boot)([0-9A-Fa-f]{4})$"; + static constexpr auto cBootOrderName = "BootOrder"; + static constexpr auto cBootCurrentName = "BootCurrent"; + static constexpr auto cBootNextName = "BootNext"; + + struct BootItem { + uint16_t mID {}; + std::string mDevice; + std::string mParentDevice; + int mPartitionNumber {}; + std::string mPartitionUUID; + + friend Log& operator<<(Log& log, const BootItem& item) + { + log << Log::Field("id", item.mID) << Log::Field("device", item.mDevice.c_str()) + << Log::Field("parentDevice", item.mParentDevice.c_str()) + << Log::Field("partitionNumber", item.mPartitionNumber) + << Log::Field("partitionUUID", item.mPartitionUUID.c_str()); + + return log; + } + }; + + virtual std::shared_ptr CreateEFIVar() const; + virtual std::shared_ptr CreatePartitionManager() const; + RetWithError> ReadBootEntries(); + RetWithError ConvertHex(const std::string& hexStr) const; + Error InitBootPartitions(const BootConfig& config); + Error SetPartitionIDs(); + RetWithError GetPartitionPrefix() const; + RetWithError> GetBootOrder() const; + RetWithError GetBootCurrent() const; + std::string GetLoaderPath() const; + RetWithError> ReadVariable(const std::string& name) const; + std::vector ToUint16(const std::vector& data) const; + std::vector ToUint8(const std::vector& data) const; + Error UpdateBootOrder(const std::vector& newBootIDs); + Error UpdateBootOrder(const std::vector& newBootIDs, const std::vector& oldBootOrder); + + mutable std::mutex mMutex; + std::shared_ptr mPartitionManager; + std::shared_ptr mEFIVar; + BootConfig mConfig; + std::vector mBootItems; +}; + +} // namespace aos::sm::launcher + +#endif diff --git a/src/sm/launcher/runtimes/boot/efivar.cpp b/src/sm/launcher/runtimes/boot/efivar.cpp new file mode 100644 index 00000000..5d512e27 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/efivar.cpp @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +extern "C" { +#include +} + +#include + +#include + +#include "efivar.hpp" + +namespace aos::sm::launcher { + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +Error EFIVar::ReadVariable(const std::string& name, std::vector& data, uint32_t& attributes) const +{ + uint8_t* efiData = nullptr; + auto cleanup = DeferRelease(&efiData, [](uint8_t** ptr) { + if (ptr) { + free(*ptr); + } + }); + + size_t efiDataSize = 0; + + if (efi_get_variable(EFI_GLOBAL_GUID, name.c_str(), &efiData, &efiDataSize, &attributes) < 0) { + return Error(ErrorEnum::eFailed, strerror(errno)); + } + + data.assign(efiData, efiData + efiDataSize); + + return ErrorEnum::eNone; +} + +Error EFIVar::WriteGlobalGuidVariable(const std::string& name, const std::vector& data) +{ + auto out = data; + + if (efi_set_variable(EFI_GLOBAL_GUID, name.c_str(), out.data(), out.size(), cEFIVarAttributes, cWriteMode) < 0) { + uint32_t idx = 0; + char* file = nullptr; + char* func = nullptr; + int line = 0; + char* message = nullptr; + int error_num = 0; + + Error err = Error(ErrorEnum::eFailed, "can't write EFI variable"); + + while (efi_error_get(idx++, &file, &func, &line, &message, &error_num) == 1) { + LOG_DBG() << "Fetched EFI error" << Log::Field("file", file) << Log::Field("function", func) + << Log::Field("line", line) << Log::Field("message", message) + << Log::Field("errorNum", error_num); + + err = Error(ErrorEnum::eFailed, message); + } + + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +RetWithError EFIVar::GetPartUUID(const std::string& efiVarName) const +{ + LOG_DBG() << "Get partition UUID from EFI variable" << Log::Field("varName", efiVarName.c_str()); + + std::vector data; + uint32_t attributes = 0; + + if (auto err = ReadVariable(efiVarName, data, attributes); !err.IsNone()) { + return {{}, AOS_ERROR_WRAP(err)}; + } + + if (data.empty()) { + return {{}, AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound))}; + } + + const auto loadOpt = reinterpret_cast(static_cast(data.data())); + const auto len = efi_loadopt_pathlen(loadOpt, data.size()); + auto dpData = efi_loadopt_path(loadOpt, data.size()); + + if (!efidp_is_valid(dpData, len)) { + return {{}, AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, strerror(errno)))}; + } + + for (const_efidp next = dpData; next != nullptr;) { + const auto type = next->type; + const auto subType = next->subtype; + const auto length = next->length; + + if (length < sizeof(efidp_header)) { + return {{}, AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, strerror(errno)))}; + } + + if (type == EFIDP_END_TYPE && subType == EFIDP_END_ENTIRE) { + break; + } + + if (type == EFIDP_MEDIA_TYPE && subType == EFIDP_MEDIA_HD) { + const auto dpHD = reinterpret_cast(next); + + if (dpHD->signature_type != cHDSignatureGUIDType) { + continue; + } + + char* uuidStr = nullptr; + const auto guid = reinterpret_cast(&dpHD->signature[0]); + + if (!efi_guid_to_str(guid, &uuidStr)) { + return {{}, AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, strerror(errno)))}; + } + + return std::string(uuidStr); + } + + if (!efidp_next_node(next, &next)) { + return {{}, AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, strerror(errno)))}; + } + } + + return {{}, AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "partition UUID not found"))}; +} + +RetWithError> EFIVar::GetAllVariables() const +{ + std::vector result; + + efi_guid_t* guid = nullptr; + char* name = nullptr; + + while (efi_get_next_variable_name(&guid, &name) != 0) { + if (!guid || !name) { + return {{}, AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, "failed to get EFI variable name"))}; + } + + result.emplace_back(name); + } + + return result; +} + +Error EFIVar::CreateBootEntry( + const std::string& parentDevice, int partition, const std::string& loaderPath, uint16_t bootID) +{ + const auto efiDPSize = efi_generate_file_device_path_from_esp( + nullptr, 0, parentDevice.c_str(), partition, loaderPath.c_str(), cEFIBootAbbrevHD, cEDDDefaultDevice); + if (efiDPSize < 0) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, strerror(errno))); + } + + std::vector efiDP(static_cast(efiDPSize)); + + if (efi_generate_file_device_path_from_esp(efiDP.data(), efiDP.size(), parentDevice.c_str(), partition, + loaderPath.c_str(), cEFIBootAbbrevHD, cEDDDefaultDevice) + < 0) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, strerror(errno))); + } + + auto bootVarName = CreateBootVariableName(bootID); + + auto efiLoadOptSize = efi_loadopt_create(nullptr, 0, 1, reinterpret_cast(efiDP.data()), efiDP.size(), + reinterpret_cast(bootVarName.data()), nullptr, 0); + if (efiLoadOptSize < 0) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, strerror(errno))); + } + + std::vector efiLoadOpt(static_cast(efiLoadOptSize)); + + if (efi_loadopt_create(efiLoadOpt.data(), efiLoadOpt.size(), 1, reinterpret_cast(efiDP.data()), efiDP.size(), + reinterpret_cast(bootVarName.data()), nullptr, 0) + < 0) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, strerror(errno))); + } + + if (auto err = WriteGlobalGuidVariable(bootVarName, efiLoadOpt); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + LOG_DBG() << "Created EFI boot entry" << Log::Field("bootVarName", bootVarName.c_str()); + + return ErrorEnum::eNone; +} + +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ + +std::string EFIVar::CreateBootVariableName(uint16_t bootID) const +{ + std::stringstream ss; + + ss << cBootVarPrefix << std::hex << std::setw(4) << std::setfill('0') << bootID; + + return ss.str(); +} + +}; // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/boot/efivar.hpp b/src/sm/launcher/runtimes/boot/efivar.hpp new file mode 100644 index 00000000..8e851eb9 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/efivar.hpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_SM_LAUNCHER_RUNTIMES_BOOT_EFICONTROLLER_HPP_ +#define AOS_SM_LAUNCHER_RUNTIMES_BOOT_EFICONTROLLER_HPP_ + +#include + +#include "itf/efivar.hpp" + +namespace aos::sm::launcher { + +/** + * EFI variable. + */ +class EFIVar : public EFIVarItf { +public: + /** + * Reads EFI variable. + * + * @param name variable name. + * @param data[out] variable data. + * @param attributes[out] variable attributes. + * @return Error. + */ + virtual Error ReadVariable( + const std::string& name, std::vector& data, uint32_t& attributes) const override; + + /** + * Writes EFI global GUID variable. + * + * @param name variable name. + * @param data variable data. + * @return Error. + */ + Error WriteGlobalGuidVariable(const std::string& name, const std::vector& data) override; + + /** + * Returns partition UUID by EFI variable name. + * + * @param partitionUUID partition UUID. + * @return RetWithError. + */ + RetWithError GetPartUUID(const std::string& efiVarName) const override; + + /** + * Returns all EFI variable names. + * + * @return RetWithError>. + */ + RetWithError> GetAllVariables() const override; + + /** + * Creates a new EFI boot entry. + * + * @param parentDevice parent device path. + * @param partition partition number. + * @param loaderPath bootloader path. + * @param bootID desired boot entry ID. + * @return Error. + */ + Error CreateBootEntry( + const std::string& parentDevice, int partition, const std::string& loaderPath, uint16_t bootID) override; + +private: + static constexpr auto cDPHeaderSize = 4; + static constexpr auto cHDSignatureGUIDType = 2; + static constexpr auto cEFIBootAbbrevHD = 2; + static constexpr auto cEDDDefaultDevice = 0x80; + static constexpr auto cWriteMode = 0600; + static constexpr auto cBootVarPrefix = "Boot"; + static constexpr auto cEFIVarAttributes + = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS; + + std::string CreateBootVariableName(uint16_t bootID) const; +}; + +} // namespace aos::sm::launcher + +#endif diff --git a/src/sm/launcher/runtimes/boot/itf/bootcontroller.hpp b/src/sm/launcher/runtimes/boot/itf/bootcontroller.hpp new file mode 100644 index 00000000..3eb0fa99 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/itf/bootcontroller.hpp @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_SM_LAUNCHER_RUNTIMES_BOOT_ITF_BOOTCONTROLLER_HPP_ +#define AOS_SM_LAUNCHER_RUNTIMES_BOOT_ITF_BOOTCONTROLLER_HPP_ + +#include +#include + +#include + +#include + +namespace aos::sm::launcher { + +/** + * Boot controller interface. + */ +class BootControllerItf { +public: + /** + * Destructor. + */ + virtual ~BootControllerItf() = default; + + /** + * Initializes boot controller. + * + * @param config boot config. + * @return Error. + */ + virtual Error Init(const BootConfig& config) = 0; + + /** + * Returns boot partition devices. + * + * @param[out] devices boot partition devices. + * @return Error. + */ + virtual Error GetPartitionDevices(std::vector& devices) const = 0; + + /** + * Returns current boot index. + * + * @return RetWithError. + */ + virtual RetWithError GetCurrentBoot() const = 0; + + /** + * Returns main boot index. + * + * @return RetWithError. + */ + virtual RetWithError GetMainBoot() const = 0; + + /** + * Sets the main boot partition index. + * + * @param index main boot partition index. + * @return Error. + */ + virtual Error SetMainBoot(size_t index) = 0; + + /** + * Sets boot successful flag. + * + * @return Error. + */ + virtual Error SetBootOK() = 0; +}; + +} // namespace aos::sm::launcher + +#endif diff --git a/src/sm/launcher/runtimes/boot/itf/efivar.hpp b/src/sm/launcher/runtimes/boot/itf/efivar.hpp new file mode 100644 index 00000000..0ccb64c1 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/itf/efivar.hpp @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_SM_LAUNCHER_RUNTIMES_BOOT_ITF_EFIVAR_HPP_ +#define AOS_SM_LAUNCHER_RUNTIMES_BOOT_ITF_EFIVAR_HPP_ + +#include +#include + +#include + +namespace aos::sm::launcher { + +/** + * EFI variable interface. + */ +class EFIVarItf { +public: + /** + * Destructor. + */ + virtual ~EFIVarItf() = default; + + /** + * Reads EFI variable. + * + * @param name variable name. + * @param data[out] variable data. + * @param attributes[out] variable attributes. + * @return Error. + */ + virtual Error ReadVariable(const std::string& name, std::vector& data, uint32_t& attributes) const = 0; + + /** + * Writes EFI global GUID variable. + * + * @param name variable name. + * @param data variable data. + * @return Error. + */ + virtual Error WriteGlobalGuidVariable(const std::string& name, const std::vector& data) = 0; + + /** + * Returns partition UUID by EFI variable name. + * + * @param partitionUUID partition UUID. + * @return RetWithError. + */ + virtual RetWithError GetPartUUID(const std::string& efiVarName) const = 0; + + /** + * Returns all EFI variable names. + * + * @return RetWithError>. + */ + virtual RetWithError> GetAllVariables() const = 0; + + /** + * Creates a new EFI boot entry. + * + * @param parentDevice parent device path. + * @param partition partition number. + * @param loaderPath bootloader path. + * @param bootID desired boot entry ID. + * @return Error. + */ + virtual Error CreateBootEntry( + const std::string& parentDevice, int partition, const std::string& loaderPath, uint16_t bootID) + = 0; +}; + +} // namespace aos::sm::launcher + +#endif diff --git a/src/sm/launcher/runtimes/boot/itf/partitionmanager.hpp b/src/sm/launcher/runtimes/boot/itf/partitionmanager.hpp new file mode 100644 index 00000000..6c1ccfa7 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/itf/partitionmanager.hpp @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_SM_LAUNCHER_RUNTIMES_BOOT_ITF_PARTITIONMANAGER_HPP_ +#define AOS_SM_LAUNCHER_RUNTIMES_BOOT_ITF_PARTITIONMANAGER_HPP_ + +#include +#include + +#include + +#include + +namespace aos::sm::launcher { + +/** + * Partition info. + */ +struct PartInfo { + std::string mDevice; + std::string mLabel; + std::string mFSType; + std::string mPartUUID; + std::string mParentDevice; + size_t mPartitionNumber {}; + + /** + * Compares partition info. + * + * @param other other partition info. + * @return bool. + */ + bool operator==(const PartInfo& other) const + { + return mDevice == other.mDevice && mLabel == other.mLabel && mFSType == other.mFSType + && mPartUUID == other.mPartUUID && mParentDevice == other.mParentDevice + && mPartitionNumber == other.mPartitionNumber; + } + + /** + * Compares partition info. + * + * @param other other partition info. + * @return bool. + */ + bool operator!=(const PartInfo& other) const { return !(*this == other); } +}; + +/** + * Partition manager interface. + */ +class PartitionManagerItf { +public: + /** + * Destructor. + */ + virtual ~PartitionManagerItf() = default; + + /** + * Returns partition info. + * + * @param partDevice partition device. + * @param[out] partInfo partition info. + * @return Error. + */ + virtual Error GetPartInfo(const std::string& partDevice, PartInfo& partInfo) const = 0; + + /** + * Mounts partition. + * + * @param partInfo partition info. + * @param mountPoint mount point. + * @param flags mount flags. + * @return Error. + */ + virtual Error Mount(const PartInfo& partInfo, const std::string& mountPoint, int flags) const = 0; + + /** + * Unmounts partition. + * + * @param mountPoint mount point. + * @return Error. + */ + virtual Error Unmount(const std::string& mountPoint) const = 0; + + /** + * Copies device. + * + * @param src source device. + * @param dst destination device. + * @return Error. + */ + virtual Error CopyDevice(const std::string& src, const std::string& dst) const = 0; + + /** + * Copies image to device. + * + * @param image image path. + * @param device destination device. + * @return Error. + */ + virtual Error InstallImage(const std::string& image, const std::string& device) const = 0; +}; + +} // namespace aos::sm::launcher + +#endif diff --git a/src/sm/launcher/runtimes/boot/partitionmanager.cpp b/src/sm/launcher/runtimes/boot/partitionmanager.cpp new file mode 100644 index 00000000..5bc35b92 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/partitionmanager.cpp @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "partitionmanager.hpp" + +namespace aos::sm::launcher { + +namespace { + +/*********************************************************************************************************************** + * Consts + **********************************************************************************************************************/ + +constexpr auto cTagTypeLabel = "LABEL"; +constexpr auto cTagTypeFSType = "TYPE"; +constexpr auto cTagTypePartUUID = "PARTUUID"; +constexpr auto cUmountRetries = 3; +constexpr auto cUmountDelay = Time::cSeconds; +constexpr auto cUmountMaxDelay = 5 * Time::cSeconds; + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +RetWithError GetPartitionNumber(const std::string& block) +{ + std::ifstream f("/sys/class/block/" + block + "/partition"); + if (!f) { + return {{}, ErrorEnum::eNotFound}; + } + + size_t part = 0; + + f >> part; + + return part; +} + +std::string GetParentDevice(const std::string& block) +{ + std::filesystem::path p = std::filesystem::canonical("/sys/class/block/" + block); + + return (std::filesystem::path("/dev") / p.parent_path().filename()).string(); +} + +} // namespace + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +Error PartitionManager::GetPartInfo(const std::string& partDevice, PartInfo& partInfo) const +{ + blkid_cache blkcache = nullptr; + + // Initialize blkid cache + if (blkid_get_cache(&blkcache, "/dev/null") != 0) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, strerror(errno))); + } + + auto putCache = DeferRelease(blkcache, [](const blkid_cache cache) { blkid_put_cache(cache); }); + + auto blkdev = blkid_get_dev(blkcache, partDevice.c_str(), BLKID_DEV_NORMAL); + if (!blkdev) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, strerror(errno))); + } + + if (const char* devname = blkid_dev_devname(blkdev); devname) { + partInfo.mDevice = devname; + + const auto filename = std::filesystem::path(devname).filename().string(); + + auto [partNum, err] = GetPartitionNumber(filename); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + partInfo.mPartitionNumber = partNum; + partInfo.mParentDevice = GetParentDevice(filename); + } + + // Iterate over tags + blkid_tag_iterate iter = blkid_tag_iterate_begin(blkdev); + + const char* tagType = nullptr; + const char* tagValue = nullptr; + + while (blkid_tag_next(iter, &tagType, &tagValue) == 0) { + if (!tagType || !tagValue) + continue; + + if (std::string(tagType) == cTagTypeLabel) { + partInfo.mLabel = tagValue; + } else if (std::string(tagType) == cTagTypeFSType) { + partInfo.mFSType = tagValue; + } else if (std::string(tagType) == cTagTypePartUUID) { + partInfo.mPartUUID = tagValue; + } + } + + blkid_tag_iterate_end(iter); + + return ErrorEnum::eNone; +} + +Error PartitionManager::Mount(const PartInfo& partInfo, const std::string& mountPoint, int flags) const +{ + if (mount(partInfo.mDevice.c_str(), mountPoint.c_str(), partInfo.mFSType.c_str(), flags, nullptr) != 0) { + return Error(ErrorEnum::eFailed, strerror(errno)); + } + + return ErrorEnum::eNone; +} + +Error PartitionManager::Unmount(const std::string& mountPoint) const +{ + auto err = common::utils::Retry( + [mountPoint]() { + if (umount2(mountPoint.c_str(), 0) != 0) { + return ErrorEnum::eFailed; + } + + return ErrorEnum::eNone; + }, + {}, cUmountRetries, cUmountDelay, cUmountMaxDelay); + + if (!err.IsNone()) { + if (umount2(mountPoint.c_str(), MNT_FORCE) != 0) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, strerror(errno))); + } + } + + return ErrorEnum::eNone; +} + +Error PartitionManager::CopyDevice(const std::string& src, const std::string& dst) const +{ + if (src == dst) { + return ErrorEnum::eNone; + } + + auto result = common::utils::ExecCommand({"dd", "if=" + src, "of=" + dst, "bs=1M"}); + if (!result.mError.IsNone()) { + return AOS_ERROR_WRAP(result.mError); + } + + return ErrorEnum::eNone; +} + +Error PartitionManager::InstallImage(const std::string& image, const std::string& device) const +{ + auto result = common::utils::ExecCommand({"dd", "if=" + image, "of=" + device, "bs=1M"}); + if (!result.mError.IsNone()) { + return AOS_ERROR_WRAP(result.mError); + } + + return ErrorEnum::eNone; +} + +} // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/boot/partitionmanager.hpp b/src/sm/launcher/runtimes/boot/partitionmanager.hpp new file mode 100644 index 00000000..46f6d8ef --- /dev/null +++ b/src/sm/launcher/runtimes/boot/partitionmanager.hpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_SM_LAUNCHER_RUNTIMES_BOOT_PARTITIONMANAGER_HPP_ +#define AOS_SM_LAUNCHER_RUNTIMES_BOOT_PARTITIONMANAGER_HPP_ + +#include "itf/partitionmanager.hpp" + +namespace aos::sm::launcher { + +/** + * Partition manager. + */ +class PartitionManager : public PartitionManagerItf { +public: + /** + * Returns partition info. + * + * @param partDevice partition device. + * @param[out] partInfo partition info. + * @return Error. + */ + Error GetPartInfo(const std::string& partDevice, PartInfo& partInfo) const override; + + /** + * Mounts partition. + * + * @param partInfo partition info. + * @param mountPoint mount point. + * @param flags mount flags. + * @return Error. + */ + Error Mount(const PartInfo& partInfo, const std::string& mountPoint, int flags) const override; + + /** + * Unmounts partition. + * + * @param mountPoint mount point. + * @return Error. + */ + Error Unmount(const std::string& mountPoint) const override; + + /** + * Copies device. + * + * @param src source device. + * @param dst destination device. + * @return Error. + */ + Error CopyDevice(const std::string& src, const std::string& dst) const override; + + /** + * Copies image to device. + * + * @param image image path. + * @param device destination device. + * @return Error. + */ + Error InstallImage(const std::string& image, const std::string& device) const override; +}; + +} // namespace aos::sm::launcher + +#endif diff --git a/src/sm/launcher/runtimes/boot/tests/CMakeLists.txt b/src/sm/launcher/runtimes/boot/tests/CMakeLists.txt index 36ab897e..432e423e 100644 --- a/src/sm/launcher/runtimes/boot/tests/CMakeLists.txt +++ b/src/sm/launcher/runtimes/boot/tests/CMakeLists.txt @@ -10,13 +10,13 @@ set(TARGET_NAME boot_test) # Sources # ###################################################################################################################### -set(SOURCES boot.cpp) +set(SOURCES boot.cpp config.cpp efibootcontroller.cpp) # ###################################################################################################################### # Libraries # ###################################################################################################################### -set(LIBRARIES aos::core::common::tests::utils aos::sm::runtimes::boot GTest::gmock_main) +set(LIBRARIES aos::common::tests::utils aos::core::common::tests::utils aos::sm::runtimes::boot GTest::gmock_main) # ###################################################################################################################### # Target diff --git a/src/sm/launcher/runtimes/boot/tests/boot.cpp b/src/sm/launcher/runtimes/boot/tests/boot.cpp index 68cae99f..76a6a912 100644 --- a/src/sm/launcher/runtimes/boot/tests/boot.cpp +++ b/src/sm/launcher/runtimes/boot/tests/boot.cpp @@ -4,13 +4,35 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include +#include +#include +#include + #include +#include +#include #include #include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include #include +#include "partitionmanagermock.hpp" + using namespace testing; namespace aos::sm::launcher { @@ -21,10 +43,50 @@ namespace { * Consts **********************************************************************************************************************/ +const auto cTestDir = std::filesystem::absolute("testBoot"); +const auto cWorkingDir = cTestDir / "workdir"; +const auto cBootRuntimeWorkingDir = cWorkingDir / "runtimes/boot"; +const auto cInstalledInstance = cBootRuntimeWorkingDir / "installed.json"; +const auto cPendingInstance = cBootRuntimeWorkingDir / "pending.json"; +const auto cBootPartitionMountDir = cBootRuntimeWorkingDir / "mnt"; +const auto cTestDisk = cTestDir / "disk"; +const auto cPartition1 = cTestDisk / "1"; +const auto cPartition2 = cTestDisk / "2"; +const auto cUpdateImage = cBootRuntimeWorkingDir / "images" / "boot.img"; +const auto cUpdateImageArchivePath = cTestDir / "boot.img.gz"; +constexpr auto cRuntimeID = "ddb944db-faba-39d9-9982-8be46f10293b"; + /*********************************************************************************************************************** * Static **********************************************************************************************************************/ +class MockBootController : public BootControllerItf { +public: + MOCK_METHOD(Error, Init, (const BootConfig& config), (override)); + MOCK_METHOD(Error, GetPartitionDevices, (std::vector&), (const, override)); + MOCK_METHOD(RetWithError, GetCurrentBoot, (), (const, override)); + MOCK_METHOD(RetWithError, GetMainBoot, (), (const, override)); + MOCK_METHOD(Error, SetMainBoot, (size_t index), (override)); + MOCK_METHOD(Error, SetBootOK, (), (override)); +}; + +class TestBootRuntime : public BootRuntime { +public: + TestBootRuntime( + std::shared_ptr partitionManager, std::shared_ptr mockBootController) + : mPartitionManager(partitionManager) + , mMockBootController(mockBootController) + { + } + +private: + std::shared_ptr CreatePartitionManager() const override { return mPartitionManager; } + std::shared_ptr CreateBootController() const override { return mMockBootController; } + + std::shared_ptr mPartitionManager; + std::shared_ptr mMockBootController; +}; + } // namespace /*********************************************************************************************************************** @@ -35,19 +97,521 @@ class BootRuntimeTest : public Test { protected: static void SetUpTestSuite() { tests::utils::InitLog(); } - void SetUp() override { } + void SetUp() override + { + std::error_code ec; + std::filesystem::remove_all(cWorkingDir, ec); + std::filesystem::remove_all(cTestDir, ec); + + std::filesystem::create_directories(cBootRuntimeWorkingDir); + std::filesystem::create_directories(cTestDisk); + + WriteVersionFiles(); + InitConfig(); + InitNodeInfo(); + + mBootAPartition.mDevice = cPartition1.string(); + mBootBPartition.mDevice = cPartition2.string(); + + EXPECT_CALL(*mMockBootController, GetPartitionDevices) + .WillRepeatedly( + DoAll(SetArgReferee<0>(std::vector {cPartition1.string(), cPartition2.string()}), + Return(ErrorEnum::eNone))); + + EXPECT_CALL(*mMockBootController, Init).WillRepeatedly(Return(ErrorEnum::eNone)); + } void TearDown() override { } - BootRuntime mBootRuntime; + void WriteVersionFiles() + { + { + std::filesystem::create_directory(cPartition1); + + std::ofstream file(cPartition1 / "version.txt"); + file << R"(VERSION="1.0.0")" << std::endl; + } + + { + std::filesystem::create_directory(cPartition2); + + std::ofstream file(cPartition2 / "version.txt"); + file << R"(VERSION="1.0.1")" << std::endl; + } + } + + void CheckVersionFileContent(const std::filesystem::path& partitionPath, const std::string& expectedVersion) + { + std::string version; + + { + std::ifstream versionFile(partitionPath / "version.txt"); + ASSERT_TRUE(versionFile.is_open()) << "Can't open version file"; + + std::string line; + std::getline(versionFile, line); + version = line; + } + + EXPECT_EQ(version, R"(VERSION=")" + expectedVersion + R"(")"); + } + + void CreateUpdateImageArchive(const std::filesystem::path& partitionPath) + { + auto res = common::utils::ExecCommand({"gzip", "--keep", (partitionPath / "version.txt").string()}); + ASSERT_TRUE(res.mError.IsNone()) << tests::utils::ErrorToStr(res.mError); + + std::filesystem::copy_file(partitionPath / "version.txt.gz", cUpdateImageArchivePath, + std::filesystem::copy_options::overwrite_existing); + } + + void InitConfig() + { + mConfig.mWorkingDir = cWorkingDir.string(); + mConfig.mType = cRuntimeBoot; + mConfig.mConfig = Poco::makeShared(); + + mConfig.mConfig->set("versionFile", "version.txt"); + mConfig.mConfig->set("partitions", Poco::makeShared()); + + for (const auto& partition : {"a", "b"}) { + mConfig.mConfig->getArray("partitions")->add(partition); + } + } + + void InitNodeInfo() + { + mNodeInfo.mNodeID = "node1"; + mNodeInfo.mNodeType = "nodeType"; + + EXPECT_CALL(mCurrentNodeInfoProvider, GetCurrentNodeInfo) + .WillRepeatedly(DoAll(SetArgReferee<0>(mNodeInfo), Return(ErrorEnum::eNone))); + } + + PartInfo mBootAPartition; + PartInfo mBootBPartition; + NodeInfo mNodeInfo; + RuntimeConfig mConfig; + iamclient::CurrentNodeInfoProviderMock mCurrentNodeInfoProvider; + imagemanager::ItemInfoProviderMock mItemInfoProvider; + oci::OCISpecMock mOCISpec; + InstanceStatusReceiverStub mStatusReceiver; + sm::utils::SystemdConnMock mSystemdConn; + std::shared_ptr mPartitionManager {std::make_shared()}; + std::shared_ptr mMockBootController {std::make_shared()}; + TestBootRuntime mBootRuntime {mPartitionManager, mMockBootController}; }; /*********************************************************************************************************************** * Tests **********************************************************************************************************************/ +TEST_F(BootRuntimeTest, GetRuntimeInfo) +{ + EXPECT_CALL(*mMockBootController, GetCurrentBoot).WillOnce(Return(0u)); + EXPECT_CALL(*mMockBootController, GetMainBoot).WillOnce(Return(0u)); + EXPECT_CALL(*mMockBootController, SetBootOK).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(*mPartitionManager, GetPartInfo(cPartition1.string(), _)) + .WillOnce(DoAll(SetArgReferee<1>(mBootAPartition), Return(ErrorEnum::eNone))); + EXPECT_CALL(*mPartitionManager, Mount(mBootAPartition, cBootPartitionMountDir.string(), _)) + .WillOnce(Invoke([](const PartInfo&, const std::string&, int) { + std::filesystem::create_directory(cBootPartitionMountDir); + + std::filesystem::copy_file(cPartition1 / "version.txt", cBootPartitionMountDir / "version.txt", + std::filesystem::copy_options::overwrite_existing); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(*mPartitionManager, Unmount(cBootPartitionMountDir.string())).WillOnce(Return(ErrorEnum::eNone)); + + auto err = mBootRuntime.Init( + mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mBootRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + auto info = std::make_unique(); + + err = mBootRuntime.GetRuntimeInfo(*info); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_STREQ(info->mRuntimeType.CStr(), cRuntimeBoot); + EXPECT_EQ(info->mMaxInstances, 1u); + EXPECT_STREQ(info->mRuntimeID.CStr(), cRuntimeID); + + err = mBootRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(BootRuntimeTest, PreInstalledStatusIsSentOnStart) +{ + EXPECT_CALL(*mMockBootController, GetCurrentBoot).WillOnce(Return(0u)); + EXPECT_CALL(*mMockBootController, GetMainBoot).WillOnce(Return(0u)); + EXPECT_CALL(*mMockBootController, SetBootOK).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(*mPartitionManager, GetPartInfo(cPartition1.string(), _)) + .WillOnce(DoAll(SetArgReferee<1>(mBootAPartition), Return(ErrorEnum::eNone))); + EXPECT_CALL(*mPartitionManager, Mount(mBootAPartition, cBootPartitionMountDir.string(), _)) + .WillOnce(Invoke([](const PartInfo&, const std::string&, int) { + std::filesystem::create_directory(cBootPartitionMountDir); + + std::filesystem::copy_file(cPartition1 / "version.txt", cBootPartitionMountDir / "version.txt", + std::filesystem::copy_options::overwrite_existing); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(*mPartitionManager, Unmount(cBootPartitionMountDir.string())).WillOnce(Return(ErrorEnum::eNone)); + + auto err = mBootRuntime.Init( + mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mBootRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + std::vector statuses; + + err = mStatusReceiver.GetStatuses(statuses, std::chrono::seconds(1)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(statuses.size(), 1u); + + EXPECT_EQ(statuses[0].mState, InstanceStateEnum::eActive); + EXPECT_STREQ(statuses[0].mVersion.CStr(), "1.0.0"); + EXPECT_STREQ(statuses[0].mManifestDigest.CStr(), ""); + EXPECT_STREQ(statuses[0].mItemID.CStr(), "boot"); + EXPECT_STREQ(statuses[0].mSubjectID.CStr(), "nodeType"); + EXPECT_EQ(statuses[0].mInstance, 0u); + EXPECT_TRUE(statuses[0].mPreinstalled); + + err = mBootRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(BootRuntimeTest, InstalledStatusIsSentOnStart) +{ + constexpr auto cInstalled = R"({ + "itemId": "item1", + "subjectId": "subject1", + "instance": 1, + "manifestDigest": "digest", + "state": "active", + "version": "1.0.0", + "partitionIndex": 0 + })"; + + { + std::ofstream file(cInstalledInstance); + if (!file.is_open()) { + FAIL() << "Failed to create installed instance file"; + } + + file << cInstalled; + } + + EXPECT_CALL(*mMockBootController, GetCurrentBoot).WillOnce(Return(0u)); + EXPECT_CALL(*mMockBootController, GetMainBoot).WillOnce(Return(0u)); + EXPECT_CALL(*mMockBootController, SetBootOK).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(*mPartitionManager, GetPartInfo(cPartition1.string(), _)) + .WillOnce(DoAll(SetArgReferee<1>(mBootAPartition), Return(ErrorEnum::eNone))); + EXPECT_CALL(*mPartitionManager, Mount(mBootAPartition, cBootPartitionMountDir.string(), _)) + .WillOnce(Invoke([](const PartInfo&, const std::string&, int) { + std::filesystem::create_directory(cBootPartitionMountDir); + + std::filesystem::copy_file(cPartition1 / "version.txt", cBootPartitionMountDir / "version.txt", + std::filesystem::copy_options::overwrite_existing); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(*mPartitionManager, Unmount(cBootPartitionMountDir.string())).WillOnce(Return(ErrorEnum::eNone)); + + auto err = mBootRuntime.Init( + mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mBootRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + std::vector statuses; + + err = mStatusReceiver.GetStatuses(statuses, std::chrono::seconds(1)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(statuses.size(), 1u); + + EXPECT_EQ(statuses[0].mState, InstanceStateEnum::eActive); + EXPECT_STREQ(statuses[0].mVersion.CStr(), "1.0.0"); + EXPECT_STREQ(statuses[0].mManifestDigest.CStr(), "digest"); + EXPECT_STREQ(statuses[0].mItemID.CStr(), "item1"); + EXPECT_STREQ(statuses[0].mSubjectID.CStr(), "subject1"); + EXPECT_EQ(statuses[0].mInstance, 1u); + EXPECT_FALSE(statuses[0].mPreinstalled); + + err = mBootRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(BootRuntimeTest, UpdateSucceededOnStart) +{ + constexpr auto cInstalled = R"({ + "manifestDigest": "preinstalledDigest", + "itemId": "boot", + "subjectId": "nodeType", + "instance": 0, + "state": "active", + "version": "1.0.0", + "partitionIndex": 0 + })"; + constexpr auto cPending = R"({ + "itemId": "updateItem1", + "subjectId": "updateSubject1", + "instance": 1, + "manifestDigest": "updateDigest", + "state": "active", + "partitionIndex": 1 + })"; + + { + std::ofstream file(cInstalledInstance); + file << cInstalled; + } + + { + std::ofstream file(cPendingInstance); + file << cPending; + } + + EXPECT_CALL(*mMockBootController, GetCurrentBoot).WillOnce(Return(1u)); + EXPECT_CALL(*mMockBootController, GetMainBoot).WillOnce(Return(1u)); + EXPECT_CALL(*mMockBootController, SetBootOK).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(*mPartitionManager, GetPartInfo(cPartition2.string(), _)) + .WillOnce(DoAll(SetArgReferee<1>(mBootBPartition), Return(ErrorEnum::eNone))); + EXPECT_CALL(*mPartitionManager, Mount(mBootBPartition, cBootPartitionMountDir.string(), _)) + .WillOnce(Invoke([](const PartInfo&, const std::string&, int) { + std::filesystem::create_directory(cBootPartitionMountDir); + + std::filesystem::copy_file(cPartition2 / "version.txt", cBootPartitionMountDir / "version.txt", + std::filesystem::copy_options::overwrite_existing); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(*mPartitionManager, Unmount(cBootPartitionMountDir.string())).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(*mPartitionManager, CopyDevice(cPartition2.string(), cPartition1.string())) + .WillOnce(Invoke([](const std::string& from, const std::string& to) { + std::filesystem::copy_file( + from + "/version.txt", to + "/version.txt", std::filesystem::copy_options::overwrite_existing); + + return ErrorEnum::eNone; + })); + + auto err = mBootRuntime.Init( + mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mBootRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + std::vector statuses; + + err = mStatusReceiver.GetStatuses(statuses, std::chrono::seconds(1)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(statuses.size(), 2u); + + EXPECT_EQ(statuses[0].mState, InstanceStateEnum::eInactive); + EXPECT_STREQ(statuses[0].mManifestDigest.CStr(), "preinstalledDigest"); + EXPECT_STREQ(statuses[0].mItemID.CStr(), "boot"); + EXPECT_STREQ(statuses[0].mSubjectID.CStr(), "nodeType"); + EXPECT_EQ(statuses[0].mInstance, 0u); + EXPECT_EQ(statuses[0].mVersion, "1.0.0"); + EXPECT_TRUE(statuses[0].mPreinstalled); + + EXPECT_EQ(statuses[1].mState, InstanceStateEnum::eActive); + EXPECT_STREQ(statuses[1].mManifestDigest.CStr(), "updateDigest"); + EXPECT_STREQ(statuses[1].mItemID.CStr(), "updateItem1"); + EXPECT_STREQ(statuses[1].mSubjectID.CStr(), "updateSubject1"); + EXPECT_EQ(statuses[1].mInstance, 1u); + EXPECT_EQ(statuses[1].mVersion, "1.0.1"); + EXPECT_FALSE(statuses[1].mPreinstalled); + + for (const auto& partition : {cPartition1, cPartition2}) { + CheckVersionFileContent(partition, "1.0.1"); + } + + err = mBootRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(BootRuntimeTest, UpdateFailedOnStart) +{ + constexpr auto cInstalled = R"({ + "manifestDigest": "preinstalledDigest", + "itemId": "boot", + "subjectId": "nodeType", + "instance": 0, + "state": "active", + "version": "1.0.0", + "partitionIndex": 0 + })"; + constexpr auto cPending = R"({ + "itemId": "updateItem1", + "subjectId": "updateSubject1", + "instance": 0, + "manifestDigest": "updateDigest", + "state": "failed", + "version": "1.0.1", + "partitionIndex": 1 + })"; + + { + std::ofstream file(cInstalledInstance); + file << cInstalled; + } + + { + std::ofstream file(cPendingInstance); + file << cPending; + } + + EXPECT_CALL(*mMockBootController, GetCurrentBoot).WillOnce(Return(0u)); + EXPECT_CALL(*mMockBootController, GetMainBoot).WillOnce(Return(1u)); + EXPECT_CALL(*mMockBootController, SetBootOK).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(*mPartitionManager, GetPartInfo(cPartition1.string(), _)) + .WillOnce(DoAll(SetArgReferee<1>(mBootAPartition), Return(ErrorEnum::eNone))); + EXPECT_CALL(*mPartitionManager, Mount(mBootAPartition, cBootPartitionMountDir.string(), _)) + .WillOnce(Invoke([](const PartInfo&, const std::string&, int) { + std::filesystem::create_directory(cBootPartitionMountDir); + + std::filesystem::copy_file(cPartition1 / "version.txt", cBootPartitionMountDir / "version.txt", + std::filesystem::copy_options::overwrite_existing); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(*mPartitionManager, Unmount(cBootPartitionMountDir.string())).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(*mPartitionManager, CopyDevice(cPartition1.string(), cPartition2.string())) + .WillOnce(Invoke([](const std::string& from, const std::string& to) { + std::filesystem::copy_file( + from + "/version.txt", to + "/version.txt", std::filesystem::copy_options::overwrite_existing); + + return ErrorEnum::eNone; + })); + + auto err = mBootRuntime.Init( + mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mBootRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + std::vector statuses; + + err = mStatusReceiver.GetStatuses(statuses, std::chrono::seconds(1)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(statuses.size(), 2u); + + EXPECT_EQ(statuses[0].mState, InstanceStateEnum::eFailed); + EXPECT_STREQ(statuses[0].mManifestDigest.CStr(), "updateDigest"); + EXPECT_STREQ(statuses[0].mItemID.CStr(), "updateItem1"); + EXPECT_STREQ(statuses[0].mSubjectID.CStr(), "updateSubject1"); + EXPECT_EQ(statuses[0].mInstance, 0u); + EXPECT_STREQ(statuses[0].mVersion.CStr(), "1.0.1"); + EXPECT_FALSE(statuses[0].mPreinstalled); + + EXPECT_EQ(statuses[1].mState, InstanceStateEnum::eActive); + EXPECT_STREQ(statuses[1].mManifestDigest.CStr(), "preinstalledDigest"); + EXPECT_STREQ(statuses[1].mItemID.CStr(), "boot"); + EXPECT_STREQ(statuses[1].mSubjectID.CStr(), "nodeType"); + EXPECT_EQ(statuses[1].mInstance, 0u); + EXPECT_STREQ(statuses[1].mVersion.CStr(), "1.0.0"); + EXPECT_TRUE(statuses[1].mPreinstalled); + + for (const auto& partition : {cPartition1, cPartition2}) { + CheckVersionFileContent(partition, "1.0.0"); + } + + err = mBootRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + TEST_F(BootRuntimeTest, StartInstance) { + const auto cManifestPath = String("oci/manifest.json"); + const auto cLayerDigest = String("layerDigest"); + + EXPECT_CALL(*mMockBootController, GetCurrentBoot).WillOnce(Return(0u)); + EXPECT_CALL(*mMockBootController, GetMainBoot).WillOnce(Return(0u)); + EXPECT_CALL(*mMockBootController, SetBootOK).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(*mPartitionManager, GetPartInfo(cPartition1.string(), _)) + .WillOnce(DoAll(SetArgReferee<1>(mBootAPartition), Return(ErrorEnum::eNone))); + EXPECT_CALL(*mPartitionManager, Mount(mBootAPartition, cBootPartitionMountDir.string(), _)) + .WillOnce(Invoke([](const PartInfo&, const std::string&, int) { + std::filesystem::create_directory(cBootPartitionMountDir); + + std::filesystem::copy_file(cPartition1 / "version.txt", cBootPartitionMountDir / "version.txt", + std::filesystem::copy_options::overwrite_existing); + + return ErrorEnum::eNone; + })); + EXPECT_CALL(*mPartitionManager, Unmount(cBootPartitionMountDir.string())).WillOnce(Return(ErrorEnum::eNone)); + + auto err = mBootRuntime.Init( + mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mBootRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + auto instance = std::make_unique(); + instance->mManifestDigest = "updateDigest"; + instance->mItemID = "item1"; + instance->mSubjectID = "subject1"; + instance->mInstance = 1; + instance->mType = UpdateItemTypeEnum::eComponent; + + EXPECT_CALL(mItemInfoProvider, GetBlobPath(instance->mManifestDigest, _)) + .WillOnce(DoAll(SetArgReferee<1>(cManifestPath), Return(ErrorEnum::eNone))); + EXPECT_CALL(mOCISpec, LoadImageManifest(cManifestPath, _)) + .WillOnce(Invoke([cLayerDigest](const String&, aos::oci::ImageManifest& manifest) { + manifest.mLayers.EmplaceBack(); + manifest.mLayers.Back().mDigest = cLayerDigest; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(mItemInfoProvider, GetBlobPath(cLayerDigest, _)).WillOnce(Invoke([&](const String&, String& path) { + CreateUpdateImageArchive(cPartition2); + + path = cUpdateImageArchivePath.c_str(); + return ErrorEnum::eNone; + })); + EXPECT_CALL(*mMockBootController, SetMainBoot(1)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(*mPartitionManager, InstallImage(cUpdateImage.c_str(), mBootBPartition.mDevice)) + .WillOnce(Invoke([](const std::string& from, const std::string& to) { + LOG_DBG() << "Installing image from " << from.c_str() << " to " << to.c_str(); + + std::filesystem::copy_file(from, to + "/version.txt", std::filesystem::copy_options::overwrite_existing); + + return ErrorEnum::eNone; + })); + + auto status = std::make_unique(); + + err = mBootRuntime.StartInstance(*instance, *status); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(status->mState, InstanceStateEnum::eActivating); + EXPECT_TRUE(static_cast(*instance) == static_cast(*status)); + + std::vector> runtimesToReboot; + + err = mStatusReceiver.GetRuntimesToReboot(runtimesToReboot, std::chrono::seconds(1)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(runtimesToReboot.size(), 1u); + EXPECT_STREQ(runtimesToReboot[0].CStr(), cRuntimeID); + + err = mBootRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); } } // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/boot/tests/config.cpp b/src/sm/launcher/runtimes/boot/tests/config.cpp new file mode 100644 index 00000000..914ac7a6 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/tests/config.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +#include +#include + +#include +#include + +#include + +using namespace testing; + +namespace aos::sm::launcher { + +/*********************************************************************************************************************** + * Suite + **********************************************************************************************************************/ + +class BootRuntimeConfigTest : public Test { +protected: + static void SetUpTestSuite() { tests::utils::InitLog(); } + + void SetUp() override + { + mRuntimeConfig.mPlugin = "boot"; + mRuntimeConfig.mType = "boot"; + mRuntimeConfig.mWorkingDir = std::filesystem::current_path().string(); + mRuntimeConfig.mConfig = Poco::makeShared(Poco::JSON_PRESERVE_KEY_ORDER); + } + + RuntimeConfig mRuntimeConfig; +}; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +TEST_F(BootRuntimeConfigTest, ParseEmptyConfig) +{ + BootConfig bootConfig; + + auto err = ParseConfig(mRuntimeConfig, bootConfig); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(bootConfig.mWorkingDir, std::filesystem::current_path() / "runtimes" / "boot"); + EXPECT_TRUE(bootConfig.mLoader.empty()); + EXPECT_EQ(bootConfig.mDetectMode, BootDetectModeEnum::eNone); + EXPECT_TRUE(bootConfig.mPartitions.empty()); + EXPECT_TRUE(bootConfig.mHealthCheckServices.empty()); +} + +TEST_F(BootRuntimeConfigTest, ParseConfig) +{ + mRuntimeConfig.mConfig->set("workingDir", "/custom/working/dir"); + mRuntimeConfig.mConfig->set("loader", "/custom/loader/path"); + mRuntimeConfig.mConfig->set("detectMode", "auto"); + + mRuntimeConfig.mConfig->set("partitions", Poco::JSON::Array::Ptr(new Poco::JSON::Array)); + mRuntimeConfig.mConfig->getArray("partitions")->add("part1"); + mRuntimeConfig.mConfig->getArray("partitions")->add("part2"); + + mRuntimeConfig.mConfig->set("healthCheckServices", Poco::JSON::Array::Ptr(new Poco::JSON::Array)); + mRuntimeConfig.mConfig->getArray("healthCheckServices")->add("service1"); + mRuntimeConfig.mConfig->getArray("healthCheckServices")->add("service2"); + + mRuntimeConfig.mConfig->set("versionFile", "/custom/version/file"); + + BootConfig bootConfig; + + auto err = ParseConfig(mRuntimeConfig, bootConfig); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(bootConfig.mWorkingDir, "/custom/working/dir"); + EXPECT_EQ(bootConfig.mLoader, "/custom/loader/path"); + EXPECT_EQ(bootConfig.mDetectMode, BootDetectModeEnum::eAuto); + + ASSERT_EQ(bootConfig.mPartitions.size(), 2); + EXPECT_EQ(bootConfig.mPartitions[0], "part1"); + EXPECT_EQ(bootConfig.mPartitions[1], "part2"); + + ASSERT_EQ(bootConfig.mHealthCheckServices.size(), 2); + EXPECT_EQ(bootConfig.mHealthCheckServices[0], "service1"); + EXPECT_EQ(bootConfig.mHealthCheckServices[1], "service2"); + + EXPECT_EQ(bootConfig.mVersionFile, "/custom/version/file"); +} + +} // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/boot/tests/efibootcontroller.cpp b/src/sm/launcher/runtimes/boot/tests/efibootcontroller.cpp new file mode 100644 index 00000000..26d17806 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/tests/efibootcontroller.cpp @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +#include + +#include +#include + +#include +#include + +#include +#include + +#include "partitionmanagermock.hpp" + +using namespace testing; + +namespace aos::sm::launcher { + +namespace { + +/*********************************************************************************************************************** + * Consts + **********************************************************************************************************************/ + +/*********************************************************************************************************************** + * Static + **********************************************************************************************************************/ + +class MockEFIVar : public EFIVarItf { +public: + MOCK_METHOD( + Error, ReadVariable, (const std::string&, std::vector&, uint32_t& attributes), (const, override)); + MOCK_METHOD(Error, WriteGlobalGuidVariable, (const std::string&, const std::vector&), (override)); + MOCK_METHOD(RetWithError, GetPartUUID, (const std::string& efiVarName), (const, override)); + MOCK_METHOD(RetWithError>, GetAllVariables, (), (const, override)); + MOCK_METHOD(Error, CreateBootEntry, (const std::string&, int, const std::string&, uint16_t), (override)); +}; + +class TestEFIBootController : public EFIBootController { +public: + TestEFIBootController( + std::shared_ptr efiVar, std::shared_ptr partitionManagerMock) + : mEFIVar(efiVar) + , mPartitionManagerMock(partitionManagerMock) + { + } + +private: + std::shared_ptr CreateEFIVar() const override { return mEFIVar; } + std::shared_ptr CreatePartitionManager() const override { return mPartitionManagerMock; } + + std::shared_ptr mEFIVar; + std::shared_ptr mPartitionManagerMock; +}; + +struct TestParams { + std::string mVarName; + std::optional mBootID; +}; + +} // namespace + +/*********************************************************************************************************************** + * Suite + **********************************************************************************************************************/ + +class EFIBootControllerTest : public Test { +protected: + static void SetUpTestSuite() { tests::utils::InitLog(); } + + void SetUp() override + { + mBootConfig.mDetectMode = BootDetectModeEnum::eNone; + mBootConfig.mPartitions = {"/dev/sda1", "/dev/sda2"}; + } + + void TearDown() override { } + + void SetGetPartitionInfoExpectation(const std::vector& efiVars = {"Boot000A", "Boot000B"}) + { + EXPECT_CALL(*mEFIVar, GetAllVariables).WillOnce(Return(efiVars)); + + EXPECT_CALL(*mMockPartitionManager, GetPartInfo("/dev/sda1", _)) + .WillOnce(Invoke([&](const std::string&, PartInfo& partInfo) { + partInfo.mPartUUID = "Boot000A-UUID"; + partInfo.mParentDevice = "/dev/sda"; + partInfo.mPartitionNumber = 1; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(*mEFIVar, GetPartUUID("Boot000A")).WillOnce(Return(RetWithError {"Boot000A-UUID"})); + + EXPECT_CALL(*mMockPartitionManager, GetPartInfo("/dev/sda2", _)) + .WillOnce(Invoke([&](const std::string&, PartInfo& partInfo) { + partInfo.mPartUUID = "Boot000B-UUID"; + partInfo.mParentDevice = "/dev/sda"; + partInfo.mPartitionNumber = 2; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(*mEFIVar, GetPartUUID("Boot000B")).WillOnce(Return(RetWithError {"Boot000B-UUID"})); + } + + BootConfig mBootConfig; + std::shared_ptr mEFIVar = std::make_shared>(); + std::shared_ptr mMockPartitionManager = std::make_shared>(); + TestEFIBootController mBootController {mEFIVar, mMockPartitionManager}; + + const std::vector cExpectedDevices = {"/dev/sda1", "/dev/sda2"}; + const std::vector cBootVars = { + {"Boot0001", 1}, + {"Boot0002", 2}, + {"Boot000A", 10}, + {"NotABootVar", std::nullopt}, + {"BootZZZZ", std::nullopt}, + }; +}; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +TEST_F(EFIBootControllerTest, GetPartitionDevices) +{ + SetGetPartitionInfoExpectation(); + + auto err = mBootController.Init(mBootConfig); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + std::vector devices; + + err = mBootController.GetPartitionDevices(devices); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(cExpectedDevices, devices); +} + +TEST_F(EFIBootControllerTest, GetCurrentBoot) +{ + SetGetPartitionInfoExpectation(); + + auto err = mBootController.Init(mBootConfig); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(*mEFIVar, ReadVariable("BootCurrent", _, _)) + .WillOnce(Invoke([](const std::string&, std::vector& data, uint32_t& attributes) { + data = {0x0B, 0x00}; + attributes = 0; + + return ErrorEnum::eNone; + })); + + size_t currentBoot = 0; + + Tie(currentBoot, err) = mBootController.GetCurrentBoot(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(1, currentBoot); +} + +TEST_F(EFIBootControllerTest, EFIBootEntriesAreCreatedOnInit) +{ + EXPECT_CALL(*mEFIVar, GetAllVariables).WillOnce(Return(std::vector {"Boot0009"})); + + EXPECT_CALL(*mMockPartitionManager, GetPartInfo("/dev/sda1", _)) + .WillOnce(Invoke([&](const std::string&, PartInfo& partInfo) { + partInfo.mPartUUID = "Boot000A-UUID"; + partInfo.mParentDevice = "/dev/sda"; + partInfo.mPartitionNumber = 1; + + return ErrorEnum::eNone; + })); + EXPECT_CALL(*mEFIVar, GetPartUUID("Boot0009")).WillOnce(Return(RetWithError {"Boot0009-UUID"})); + + EXPECT_CALL(*mMockPartitionManager, GetPartInfo("/dev/sda2", _)) + .WillOnce(Invoke([&](const std::string&, PartInfo& partInfo) { + partInfo.mPartUUID = "Boot000B-UUID"; + partInfo.mParentDevice = "/dev/sda"; + partInfo.mPartitionNumber = 2; + + return ErrorEnum::eNone; + })); + + EXPECT_CALL(*mEFIVar, CreateBootEntry(_, 1, "/EFI/BOOT/bootx64.efi", 10)).WillOnce(Return(ErrorEnum::eNone)); + EXPECT_CALL(*mEFIVar, CreateBootEntry(_, 2, "/EFI/BOOT/bootx64.efi", 11)).WillOnce(Return(ErrorEnum::eNone)); + + EXPECT_CALL(*mEFIVar, ReadVariable("BootOrder", _, _)) + .WillOnce(Invoke([](const std::string&, std::vector& data, uint32_t& attributes) { + data = {0x09, 0x00}; // Boot Order: 9 + attributes = 0; + return ErrorEnum::eNone; + })); + + EXPECT_CALL( + *mEFIVar, WriteGlobalGuidVariable("BootOrder", std::vector {0x0A, 0x00, 0x0B, 0x00, 0x09, 0x00})) + .WillOnce(Return(ErrorEnum::eNone)); + + auto err = mBootController.Init(mBootConfig); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(EFIBootControllerTest, GetMainBootReturnsErrorIfFirstBootEntryIsUnknown) +{ + SetGetPartitionInfoExpectation(); + + auto err = mBootController.Init(mBootConfig); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(*mEFIVar, ReadVariable("BootOrder", _, _)) + .WillOnce(Invoke([](const std::string&, std::vector& data, uint32_t& attributes) { + data = {0x01, 0x00, 0x0A, 0x00, 0x0B, 0x00}; // Boot Order: 1, 10, 11 + attributes = 0; + + return ErrorEnum::eNone; + })); + + size_t mainBoot = 0; + + Tie(mainBoot, err) = mBootController.GetMainBoot(); + EXPECT_TRUE(err.Is(ErrorEnum::eNotFound)) << tests::utils::ErrorToStr(err); +} + +TEST_F(EFIBootControllerTest, GetMainBoot) +{ + SetGetPartitionInfoExpectation(); + + auto err = mBootController.Init(mBootConfig); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(*mEFIVar, ReadVariable("BootOrder", _, _)) + .WillOnce(Invoke([](const std::string&, std::vector& data, uint32_t& attributes) { + data = {0x0B, 0x00, 0x0A, 0x00, 0x01, 0x00}; // Boot Order: 11, 10, 1 + attributes = 0; + + return ErrorEnum::eNone; + })); + + size_t mainBoot = 0; + + Tie(mainBoot, err) = mBootController.GetMainBoot(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(1, mainBoot); +} + +TEST_F(EFIBootControllerTest, SetMainBootReturnsErrorOnInvalidIndex) +{ + SetGetPartitionInfoExpectation(); + + auto err = mBootController.Init(mBootConfig); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mBootController.SetMainBoot(111); + EXPECT_TRUE(err.Is(ErrorEnum::eOutOfRange)) << tests::utils::ErrorToStr(err); +} + +TEST_F(EFIBootControllerTest, SetMainBoot) +{ + SetGetPartitionInfoExpectation(); + + auto err = mBootController.Init(mBootConfig); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + const uint16_t bootID = 11; + + EXPECT_CALL(*mEFIVar, + WriteGlobalGuidVariable("BootNext", + std::vector { + reinterpret_cast(&bootID), reinterpret_cast(&bootID) + sizeof(bootID)})) + .WillOnce(Return(ErrorEnum::eNone)); + + err = mBootController.SetMainBoot(1); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(EFIBootControllerTest, SetMainBootInvalidIndex) +{ + SetGetPartitionInfoExpectation(); + + auto err = mBootController.Init(mBootConfig); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mBootController.SetMainBoot(100); + EXPECT_TRUE(err.Is(ErrorEnum::eOutOfRange)) << tests::utils::ErrorToStr(err); +} + +TEST_F(EFIBootControllerTest, SetBootOK) +{ + SetGetPartitionInfoExpectation(); + + auto err = mBootController.Init(mBootConfig); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(*mEFIVar, ReadVariable("BootOrder", _, _)) + .WillOnce(Invoke([](const std::string&, std::vector& data, uint32_t& attributes) { + data = {0x01, 0x00, 0x0A, 0x00, 0x02, 0x00}; // Boot Order: 1, 10, 2 + attributes = 0; + return ErrorEnum::eNone; + })); + + EXPECT_CALL(*mEFIVar, ReadVariable("BootCurrent", _, _)) + .WillOnce(Invoke([](const std::string&, std::vector& data, uint32_t& attributes) { + data = {0x0A, 0x00}; // Boot ID 10 + attributes = 0; + return ErrorEnum::eNone; + })); + + std::vector uint16Order {10, 1, 2}; + std::vector newBootOrder {reinterpret_cast(uint16Order.data()), + reinterpret_cast(uint16Order.data()) + sizeof(uint16_t) * uint16Order.size()}; + + EXPECT_CALL(*mEFIVar, WriteGlobalGuidVariable("BootOrder", newBootOrder)).WillOnce(Return(ErrorEnum::eNone)); + + err = mBootController.SetBootOK(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(EFIBootControllerTest, SetBootOKAlreadyHasCorrectOrder) +{ + SetGetPartitionInfoExpectation(); + + auto err = mBootController.Init(mBootConfig); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(*mEFIVar, ReadVariable("BootOrder", _, _)) + .WillOnce(Invoke([](const std::string&, std::vector& data, uint32_t& attributes) { + data = {0x01, 0x00, 0x0A, 0x00, 0x02, 0x00}; // Boot Order: 1, 10, 2 + attributes = 0; + return ErrorEnum::eNone; + })); + + EXPECT_CALL(*mEFIVar, ReadVariable("BootCurrent", _, _)) + .WillOnce(Invoke([](const std::string&, std::vector& data, uint32_t& attributes) { + data = {0x01, 0x00}; // Boot ID 1 + attributes = 0; + return ErrorEnum::eNone; + })); + + std::vector newBootOrder; + + EXPECT_CALL(*mEFIVar, WriteGlobalGuidVariable).Times(0); + + err = mBootController.SetBootOK(); + EXPECT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +} // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/boot/tests/partitionmanagermock.hpp b/src/sm/launcher/runtimes/boot/tests/partitionmanagermock.hpp new file mode 100644 index 00000000..c3dc7f92 --- /dev/null +++ b/src/sm/launcher/runtimes/boot/tests/partitionmanagermock.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_SM_LAUNCHER_RUNTIMES_BOOT_TESTS_PARTITIONMANAGERMOCK_HPP_ +#define AOS_SM_LAUNCHER_RUNTIMES_BOOT_TESTS_PARTITIONMANAGERMOCK_HPP_ + +#include + +namespace aos::sm::launcher { + +/** + * Partition manager interface. + */ +class PartitionManagerMock : public PartitionManagerItf { +public: + MOCK_METHOD(Error, GetPartInfo, (const std::string&, PartInfo&), (const, override)); + MOCK_METHOD(Error, Mount, (const PartInfo&, const std::string&, int), (const, override)); + MOCK_METHOD(Error, Unmount, (const std::string&), (const, override)); + MOCK_METHOD(Error, CopyDevice, (const std::string&, const std::string&), (const, override)); + MOCK_METHOD(Error, InstallImage, (const std::string&, const std::string&), (const, override)); +}; + +} // namespace aos::sm::launcher + +#endif