blob: 1d16a347fafefb0b640f91d78b78f3275c283d09 [file] [edit]
#include "install/installer.h"
#include <sys/wait.h>
#include <unistd.h>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <filesystem> // NOLINT
#include <fstream>
#include <ios>
#include <sstream>
#include <string>
#include <system_error> // NOLINT
#include <vector>
#include "owner_certificate/owner_verification_cert_configuration.pb.h"
#include "absl/cleanup/cleanup.h"
#include "absl/container/flat_hash_set.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/strings/ascii.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include <nlohmann/json.hpp>
#include "google/protobuf/text_format.h"
namespace bloom_install {
using ::bloom_install::internal::StatusReporter;
// LINT.IfChange
constexpr char kStatusKeyStatus[] = "status";
constexpr char kStatusKeyPercentComplete[] = "percent_complete";
constexpr char kStatusKeyMessage[] = "message";
constexpr char kStatusKeyStartTime[] = "start_time";
constexpr char kStatusKeyLastUpdateTime[] = "last_update_time";
// LINT.ThenChange(//depot/google3/third_party/milotic/external/cc/tlbmc/redfish/routes/task_service.cc)
ProgressLogger::ProgressLogger(const std::string& log_file_path)
: log_file_path_(log_file_path),
log_file_(log_file_path_, std::ios_base::app),
is_healthy_(log_file_.is_open()) {}
bool ProgressLogger::IsHealthy() const { return is_healthy_; }
void ProgressLogger::Log(State state, absl::Duration elapsed_time,
const std::string& message, LogCode code,
int retries) {
if (!is_healthy_) {
return;
}
nlohmann::json log_entry;
log_entry["state"] = StateToString(state);
log_entry["time"] = absl::ToDoubleSeconds(elapsed_time);
log_entry["message"] = message;
log_entry["code"] = LogCodeToString(code);
if (state == State::kFailureRetry) {
log_entry["retries"] = retries;
}
// TODO(nanzhou): Remove dump. It's not very useful as we have separate
// journal logs. I kept them because of the test was checking the files.
const std::string log_str = log_entry.dump();
log_file_ << log_str << "\n";
LOG(INFO) << log_str;
log_file_.flush();
}
namespace internal {
StatusReporter StatusReporter::Create(const std::string& status_path,
int total_packages) {
return StatusReporter(status_path, total_packages);
}
StatusReporter::StatusReporter(const std::string& status_path,
int total_packages)
: status_path_(status_path), total_packages_(std::max(total_packages, 1)) {}
void StatusReporter::Start(const std::string& message) {
status_json_[kStatusKeyStatus] = "New";
status_json_[kStatusKeyPercentComplete] = 0;
status_json_[kStatusKeyMessage] = message;
const std::string now_str = absl::FormatTime(absl::Now());
status_json_[kStatusKeyStartTime] = now_str;
status_json_[kStatusKeyLastUpdateTime] = now_str;
WriteStatus();
}
void StatusReporter::RecordSuccess(const std::string& message) {
if (packages_completed_ < total_packages_) {
packages_completed_++;
}
UpdateProgress(message);
}
void StatusReporter::UpdateProgress(const std::string& message) {
status_json_[kStatusKeyPercentComplete] =
std::round((packages_completed_ * 100.0) / total_packages_);
status_json_[kStatusKeyMessage] = message;
status_json_[kStatusKeyLastUpdateTime] = absl::FormatTime(absl::Now());
WriteStatus();
}
void StatusReporter::Complete(bool success, const std::string& message) {
status_json_[kStatusKeyStatus] = success ? "Completed" : "Exception";
if (success) {
status_json_[kStatusKeyPercentComplete] = 100;
}
status_json_[kStatusKeyMessage] = message;
status_json_[kStatusKeyLastUpdateTime] = absl::FormatTime(absl::Now());
WriteStatus();
}
void StatusReporter::WriteStatus() {
const std::string temp_path = status_path_ + ".tmp";
{
std::ofstream temp_file(temp_path);
if (!temp_file.is_open()) {
return;
}
temp_file << status_json_.dump(4);
}
std::error_code ec;
if (std::filesystem::rename(temp_path, status_path_, ec); ec) {
std::filesystem::remove(temp_path);
}
}
// Helper to get the set of files listed in the manifest.
absl::flat_hash_set<std::string> GetManifestFiles(
const std::string& sha256sums_file_path) {
absl::flat_hash_set<std::string> manifest_files;
std::ifstream sums_file(sha256sums_file_path);
if (!sums_file.is_open()) {
LOG(ERROR) << "Could not open manifest file: " << sha256sums_file_path;
return manifest_files;
}
std::string line;
while (std::getline(sums_file, line)) {
// sha256sum format is: <hash> <mode><file_path>
// mode is ' ' for text and '*' for binary.
size_t first_space = line.find(' ');
if (first_space == std::string::npos || first_space + 2 > line.length()) {
continue;
}
absl::string_view path(line);
// Remove the hash, the space and the 1-character mode flag (* or [space]).
path.remove_prefix(first_space + 2);
manifest_files.insert(
std::filesystem::path(path).lexically_normal().string());
}
return manifest_files;
}
// Returns true if the directory entry represents a valid package.
// A valid package must be a directory containing both post_install.sh and
// activation_check.sh, and both files must be present in the manifest.
bool IsPackageValid(const std::string& package_path_str,
const absl::flat_hash_set<std::string>& manifest_files) {
std::error_code ec;
std::filesystem::path package_path(package_path_str);
if (!std::filesystem::is_directory(package_path, ec)) {
return false;
}
if (!std::filesystem::exists(package_path / "post_install.sh", ec) ||
!std::filesystem::exists(package_path / "activation_check.sh", ec)) {
return false;
}
std::string post_install_path = (std::filesystem::path("firmware_bundle") /
package_path.filename() / "post_install.sh")
.string();
std::string activation_check_path =
(std::filesystem::path("firmware_bundle") / package_path.filename() /
"activation_check.sh")
.string();
return manifest_files.contains(post_install_path) &&
manifest_files.contains(activation_check_path);
}
} // namespace internal
Installer::Installer(const InstallerOptions& options)
: options_(options),
logger_(options.log_path),
status_reporter_(StatusReporter::Create(
options.status_path, static_cast<int>(options.packages.size()))) {
if (options.packages.empty()) {
LOG(WARNING) << "No packages to install. Post install or activation "
"check will succeed with no-op.";
}
}
// Helper function to verify a detached signature using OpenSSL.
bool Installer::VerifyDetachedSignature(const std::string& file_to_verify,
const std::string& signature_file,
const std::string& public_key_file) {
absl::Time check_start_time = absl::Now();
std::vector<std::string> args = {
"openssl", "dgst", "-sha256", "-verify",
public_key_file, "-signature", signature_file, file_to_verify};
int result = ExecuteCommand(args);
if (result != 0) {
const std::string error_msg = absl::Substitute(
"Detached signature verification failed for $0 with exit code: $1",
file_to_verify, result);
logger_.Log(State::kFailureFatal, absl::Now() - check_start_time, error_msg,
LogCode::kBundleSignatureFail);
return false;
}
const std::string success_msg = absl::Substitute(
"Successfully verified detached signature for $0", file_to_verify);
logger_.Log(State::kSuccess, absl::Now() - check_start_time, success_msg,
LogCode::kBundleSignatureSuccess);
return true;
}
bool Installer::CheckBundleSignature(const std::string& sha256sums_file_path,
const std::string& signature_file_path,
const std::string& public_key_path) {
absl::Time check_start_time = absl::Now();
const std::string start_msg = "Starting bundle signature check";
logger_.Log(State::kCheckingBundleSignature, absl::ZeroDuration(), start_msg,
LogCode::kBundleSignatureCheckStart);
status_reporter_.Start(start_msg);
// First, verify signature over the sums file
if (!VerifyDetachedSignature(sha256sums_file_path, signature_file_path,
public_key_path)) {
const std::string error_msg = "Detached signature verification failed.";
logger_.Log(State::kFailureFatal, absl::Now() - check_start_time, error_msg,
LogCode::kBundleSignatureFail);
status_reporter_.Complete(false, error_msg);
return false;
}
// Now verify that file hashes match the sums file
std::filesystem::path sums_file_parent_path =
std::filesystem::path(sha256sums_file_path).parent_path();
std::vector<std::string> args = {"sha256sum", "-c", sha256sums_file_path};
int result = ExecuteCommand(args, sums_file_parent_path.string());
if (result != 0) {
const std::string error_msg = absl::Substitute(
"Bundle file hash verification failed for command 'sha256sum -c $0'. "
"Exit code: $1",
sha256sums_file_path, result);
logger_.Log(State::kFailureFatal, absl::Now() - check_start_time, error_msg,
LogCode::kBundleHashFail);
status_reporter_.Complete(false, error_msg);
return false;
}
const std::string success_msg = "Bundle signature check successful.";
logger_.Log(State::kSuccess, absl::Now() - check_start_time, success_msg,
LogCode::kBundleSignatureSuccess);
return true;
}
absl::StatusOr<owner_certificate::OwnerVerificationCertConfiguration>
Installer::GetOwnerConfiguration(const std::string& config_file_path) {
absl::Time start_time = absl::Now();
const std::string start_msg = absl::Substitute(
"Starting to retrieve owner configuration from $0", config_file_path);
logger_.Log(State::kGettingOwnerConfiguration, absl::ZeroDuration(),
start_msg, LogCode::kGetOwnerConfigStart);
std::ifstream config_file(config_file_path);
if (!config_file.is_open()) {
std::string msg = absl::Substitute(
"Could not open owner configuration file: $0", config_file_path);
LOG(ERROR) << msg;
logger_.Log(State::kFailureFatal, absl::Now() - start_time, msg,
LogCode::kGetOwnerConfigFail);
status_reporter_.Complete(false, msg);
return absl::NotFoundError(msg);
}
std::stringstream buffer;
buffer << config_file.rdbuf();
if (config_file.bad()) {
std::string msg = absl::Substitute(
"Failed to read owner configuration file from $0", config_file_path);
LOG(ERROR) << msg;
logger_.Log(State::kFailureFatal, absl::Now() - start_time, msg,
LogCode::kGetOwnerConfigFail);
status_reporter_.Complete(false, msg);
return absl::InternalError(msg);
}
std::string config_str = buffer.str();
if (absl::StripAsciiWhitespace(config_str).empty()) {
std::string msg = absl::Substitute(
"Owner configuration file $0 is empty or contains only whitespace.",
config_file_path);
LOG(ERROR) << msg;
logger_.Log(State::kFailureFatal, absl::Now() - start_time, msg,
LogCode::kGetOwnerConfigFail);
status_reporter_.Complete(false, msg);
return absl::InternalError(msg);
}
owner_certificate::OwnerVerificationCertConfiguration config;
// TODO(b/489183188): Remove this temporary default once all test machines
// have the field deployed.
config.set_bundle_dev_verification_enabled(true);
if (!::google::protobuf::TextFormat::MergeFromString(config_str, &config)) {
std::string msg =
absl::Substitute("Failed to parse $0 as a textproto", config_file_path);
LOG(ERROR) << msg;
logger_.Log(State::kFailureFatal, absl::Now() - start_time, msg,
LogCode::kGetOwnerConfigFail);
status_reporter_.Complete(false, msg);
return absl::InvalidArgumentError(msg);
}
const std::string success_msg = "Successfully retrieved owner configuration";
logger_.Log(State::kSuccess, absl::Now() - start_time, success_msg,
LogCode::kGetOwnerConfigSuccess);
return config;
}
bool Installer::PostInstall() {
absl::Time total_start_time = absl::Now();
if (options_.packages.empty()) {
const std::string complete_msg =
"No packages to install. Process complete.";
logger_.Log(State::kSuccess, absl::Now() - total_start_time, complete_msg,
LogCode::kInstallComplete);
return true;
}
const std::string start_msg = "Starting installation process.";
logger_.Log(State::kStarting, absl::ZeroDuration(), start_msg,
LogCode::kInitStart);
status_reporter_.Start(start_msg);
int packages_completed_for_logging = 0;
for (const auto& package : options_.packages) {
if (!InstallPackage(package)) {
const std::string failure_msg =
absl::Substitute("Installation failed on package: $0", package.name);
absl::Duration elapsed = absl::Now() - total_start_time;
logger_.Log(
State::kFailureFatal, elapsed,
absl::Substitute("Installation halted due to failure in package: $0",
package.name),
LogCode::kInstallHalt);
status_reporter_.Complete(false, failure_msg);
return false;
}
// Persist the activation scripts togather with other scripts to the
// activation check directory.
std::filesystem::path activation_package_dir =
options_.activation_check_dir;
activation_package_dir /= package.name;
std::error_code ec;
if (std::filesystem::create_directories(activation_package_dir, ec); ec) {
const std::string failure_msg = absl::Substitute(
"Could not create activation check script directory: $0 with error: "
"$1",
activation_package_dir.c_str(), ec.message());
logger_.Log(State::kFailureFatal, absl::Now() - total_start_time,
failure_msg, LogCode::kInstallHalt);
status_reporter_.Complete(false, failure_msg);
return false;
}
const std::filesystem::path package_dir =
std::filesystem::path(package.activation_script_path).parent_path();
ec.clear();
for (auto it = std::filesystem::directory_iterator(package_dir, ec);
it != std::filesystem::directory_iterator(); it.increment(ec)) {
if (ec) {
break;
}
std::filesystem::directory_entry file_entry = *it;
if (!file_entry.is_regular_file()) {
continue;
}
if (file_entry.path().extension() == ".sh" ||
file_entry.path().filename() == "VERSION") {
std::filesystem::path dest_path = activation_package_dir;
dest_path /= file_entry.path().filename();
std::error_code copy_ec;
if (std::filesystem::copy(
file_entry.path(), dest_path,
std::filesystem::copy_options::overwrite_existing, copy_ec);
copy_ec) {
const std::string failure_msg = absl::Substitute(
"Could not copy file: $0 to: $1 with error: $2",
file_entry.path().c_str(), dest_path.c_str(), copy_ec.message());
logger_.Log(State::kFailureFatal, absl::Now() - total_start_time,
failure_msg, LogCode::kInstallHalt);
status_reporter_.Complete(false, failure_msg);
return false;
}
}
}
packages_completed_for_logging++;
const std::string progress_msg = absl::Substitute(
"Successfully completed package $0 ($1 of $2).", package.name,
packages_completed_for_logging, options_.packages.size());
status_reporter_.RecordSuccess(progress_msg);
}
const std::string success_msg = "All packages installed successfully.";
absl::Duration elapsed = absl::Now() - total_start_time;
logger_.Log(State::kSuccess, elapsed, success_msg, LogCode::kInstallComplete);
status_reporter_.Complete(true, success_msg);
return true;
}
bool Installer::InstallPackage(const Package& package) {
absl::Time check_start_time = absl::Now();
logger_.Log(
State::kCheckingActivation, absl::ZeroDuration(),
absl::Substitute("Checking activation for package: $0", package.name),
LogCode::kPkgActivationCheckStart);
int activation_return_code = ExecuteCommand(package.activation_script_path);
absl::Duration check_elapsed = absl::Now() - check_start_time;
if (activation_return_code == 0) {
logger_.Log(
State::kSkipped, check_elapsed,
absl::Substitute("Package already active, skipping installation: $0",
package.name),
LogCode::kPkgActivationSkip);
return true;
}
logger_.Log(State::kInstalling, absl::ZeroDuration(),
absl::Substitute(
"Package not active, proceeding with installation for: $0",
package.name),
LogCode::kPkgInstallProceed);
for (int attempt = 0; attempt <= options_.max_retries; ++attempt) {
absl::Time attempt_start_time = absl::Now();
logger_.Log(
State::kInstalling, absl::ZeroDuration(),
absl::Substitute("Attempting to install package: $0", package.name),
LogCode::kPkgInstallStart);
int return_code = ExecuteCommand(package.install_script_path);
absl::Duration elapsed = absl::Now() - attempt_start_time;
if (return_code == 0) {
logger_.Log(
State::kSuccess, elapsed,
absl::Substitute("Successfully installed package: $0", package.name),
LogCode::kPkgInstallSuccess);
return true;
}
if (attempt < options_.max_retries) {
logger_.Log(State::kFailureRetry, elapsed,
absl::Substitute("Failed to install package: $0. Retrying.",
package.name),
LogCode::kPkgInstallFailRetry, attempt + 1);
absl::SleepFor(absl::Seconds(2));
} else {
logger_.Log(
State::kFailureFatal, elapsed,
absl::Substitute("Fatal error installing package: $0", package.name),
LogCode::kPkgInstallFailFatal);
return false;
}
}
return false;
}
void Installer::ActivationCheck() {
for (const auto& package : options_.packages) {
// Ignore the return code of the activation script so that all packages are
// checked.
ExecuteCommand(package.activation_script_path);
}
}
int Installer::ExecuteCommand(const std::vector<std::string>& args,
const std::string& working_dir) {
if (args.empty()) {
return -1;
}
pid_t pid = fork();
if (pid == -1) {
return -1;
}
if (pid == 0) {
// Child process
if (!working_dir.empty()) {
if (chdir(working_dir.c_str()) != 0) {
// Exit with 127 to mimic std::system behavior.
// Use _exit() to avoid libc process cleanup behavior.
_exit(127);
}
}
std::vector<char*> argv;
argv.reserve(args.size() + 1);
for (const auto& arg : args) {
argv.push_back(const_cast<char*>(arg.c_str()));
}
argv.push_back(nullptr);
execvp(argv[0], argv.data());
// execvp() should not return on success. If it returns, exit child with
// 127 to mimic std::system behavior.
// Use _exit() to avoid libc process cleanup behavior..
_exit(127);
}
// Parent process
int status;
if (waitpid(pid, &status, 0) == -1) {
return -1;
}
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}
return -1;
}
int Installer::ExecuteCommand(const std::string& command,
const std::string& working_dir) {
return ExecuteCommand(absl::StrSplit(command, ' ', absl::SkipWhitespace()),
working_dir);
}
int ActivationMain(const std::string& activation_check_dir) {
std::vector<Package> packages;
std::error_code ec;
for (const auto& entry :
std::filesystem::directory_iterator(activation_check_dir, ec)) {
if (!ec && entry.is_directory() &&
std::filesystem::exists(entry.path() / "activation_check.sh")) {
Package package;
package.name = entry.path().filename().string();
package.activation_script_path = entry.path() / "activation_check.sh";
packages.push_back(package);
}
}
InstallerOptions options;
options.packages = packages;
Installer installer(options);
installer.ActivationCheck();
LOG(INFO) << "Activation check completed successfully.";
return 0;
}
int SignatureCheckMain(const std::string& bundle_tar_file_path,
const std::string& public_key_path) {
InstallerOptions options;
Installer installer(options);
std::filesystem::path sha256sums_file = bundle_tar_file_path;
sha256sums_file.remove_filename();
sha256sums_file /= "bundle_files.SHA256SUM";
std::filesystem::path signature_file = sha256sums_file.string() + ".sig";
if (!installer.CheckBundleSignature(
sha256sums_file.string(), signature_file.string(), public_key_path)) {
LOG(ERROR) << "Bundle signature check failed";
return 1; // Exit if signature verification fails
}
LOG(INFO) << "Bundle signature check successful.";
return 0;
}
// Parses the firmware bundle to identify and validate software packages.
// Each package must contain 'post_install.sh' and 'activation_check.sh', and
// these files must be present in the manifest.
// Returns a vector of valid Package objects, sorted alphabetically by package
// name.
std::vector<Package> ParsePostInstallPackages(
const std::string& tar_file_path, const std::string& sha256sums_file_path) {
std::vector<Package> packages;
std::filesystem::path untar_dir = tar_file_path;
untar_dir.remove_filename();
untar_dir /= "firmware_bundle";
LOG(INFO) << "Firmware bundle directory: " << untar_dir;
absl::flat_hash_set<std::string> manifest_files =
internal::GetManifestFiles(sha256sums_file_path);
std::error_code ec;
for (const auto& entry : std::filesystem::directory_iterator(untar_dir, ec)) {
if (!ec &&
internal::IsPackageValid(entry.path().string(), manifest_files)) {
LOG(INFO) << "Found package: " << entry.path().filename().string();
Package package;
package.name = entry.path().filename().string();
package.activation_script_path = entry.path() / "activation_check.sh";
package.install_script_path = entry.path() / "post_install.sh";
packages.push_back(package);
} else if (!ec) {
LOG(INFO) << "Not a valid package or missing from the manifest: "
<< entry.path().filename().string();
}
}
std::sort(packages.begin(), packages.end(),
[](const Package& a, const Package& b) { return a.name < b.name; });
return packages;
}
int PostInstallMain(const std::string& inventory_dir,
const std::string& install_lock_file_path,
const std::string& bundle_tar_file_path,
const std::string& activation_check_dir) {
InstallerOptions options;
// Check if an installation is already running.
std::filesystem::path install_lock_file = install_lock_file_path;
if (std::error_code ec; std::filesystem::exists(install_lock_file, ec)) {
LOG(ERROR) << "Installation is already in progress. Lock file found: "
<< install_lock_file;
return 1;
}
std::error_code ec;
if (std::filesystem::create_directories(inventory_dir, ec); ec) {
LOG(ERROR) << "Error: Could not create installation directory: "
<< inventory_dir << " with error: " << ec.message();
return 1;
}
ec.clear();
if (std::filesystem::create_directories(install_lock_file.parent_path(), ec);
ec) {
LOG(ERROR) << "Error: Could not create installation lock file directory: "
<< install_lock_file.parent_path()
<< " with error: " << ec.message();
return 1;
}
// Create the installation lock file to signal that an install is starting.
std::ofstream lock_file_stream(install_lock_file);
if (!lock_file_stream) {
LOG(ERROR) << "Error: Could not create lock file: " << install_lock_file
<< " with error: " << lock_file_stream.rdstate();
return 1;
}
absl::Cleanup lock_file_cleanup = [install_lock_file] {
std::error_code ec;
if (std::filesystem::remove(install_lock_file, ec); ec) {
LOG(ERROR) << "Error: Could not remove lock file: " << install_lock_file;
}
LOG(INFO) << "Successfully removed lock file: " << install_lock_file;
};
LOG(INFO) << "Successfully created lock file: " << install_lock_file;
// Now run install
std::filesystem::path sha256sums_file = bundle_tar_file_path;
sha256sums_file.remove_filename();
sha256sums_file /= "bundle_files.SHA256SUM";
std::vector<Package> packages_to_install =
ParsePostInstallPackages(bundle_tar_file_path, sha256sums_file.string());
options.packages = packages_to_install;
options.max_retries = 2;
std::filesystem::path log_path = inventory_dir;
log_path.remove_filename();
log_path /= "progress_internal.log";
options.log_path = log_path.c_str();
options.activation_check_dir = activation_check_dir;
Installer installer(options);
LOG(INFO) << "Starting installation process...";
if (!installer.PostInstall()) {
LOG(ERROR) << "Post install failed. Check log at " << log_path
<< " for details.";
return 1;
}
LOG(INFO) << "Post install completed successfully.";
return 0;
}
int CheckDevKeysEnabledMain(const std::string& config_file_path) {
InstallerOptions options;
Installer installer(options);
absl::StatusOr<owner_certificate::OwnerVerificationCertConfiguration> status =
installer.GetOwnerConfiguration(config_file_path);
if (!status.ok()) {
LOG(ERROR) << "Failed to get owner configuration: " << status.status();
LOG(ERROR) << "Dev verification keys use will be disabled by default.";
return kDevKeysError;
}
owner_certificate::OwnerVerificationCertConfiguration config = status.value();
if (config.bundle_dev_verification_enabled()) {
LOG(INFO) << "Dev verification keys are enabled via " << config_file_path;
return kDevKeysEnabled;
}
LOG(INFO) << "Dev verification keys are explicitly disabled in "
<< config_file_path;
return kDevKeysDisabled;
}
} // namespace bloom_install