| #include "install/installer.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <cstdlib> |
| #include <filesystem> // NOLINT |
| #include <fstream> |
| #include <ios> |
| #include <string> |
| #include <system_error> // NOLINT |
| #include <vector> |
| |
| #include "absl/cleanup/cleanup.h" |
| #include "absl/log/log.h" |
| #include "absl/strings/substitute.h" |
| #include "absl/time/clock.h" |
| #include "absl/time/time.h" |
| #include "nlohmann/json.hpp" |
| |
| 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; |
| } |
| |
| log_file_ << log_entry.dump() << "\n"; |
| 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); |
| } |
| } |
| |
| } // 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."; |
| } |
| } |
| |
| 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 check script to the activation check directory. |
| std::filesystem::path activation_check_script_path = |
| options_.activation_check_dir; |
| activation_check_script_path /= package.name; |
| std::error_code ec; |
| if (std::filesystem::create_directories(activation_check_script_path, ec); |
| ec) { |
| const std::string failure_msg = absl::Substitute( |
| "Could not create activation check script directory: $0 with error: " |
| "$1", |
| activation_check_script_path.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; |
| } |
| activation_check_script_path /= "activation_check.sh"; |
| ec.clear(); |
| if (std::filesystem::copy( |
| package.activation_script_path, activation_check_script_path, |
| std::filesystem::copy_options::overwrite_existing, ec); |
| ec) { |
| const std::string failure_msg = absl::Substitute( |
| "Could not copy activation check script: $0 to: $1 with error: $2", |
| package.activation_script_path, activation_check_script_path.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; |
| } |
| |
| 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 = |
| std::system(package.activation_script_path.c_str()); |
| 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 = std::system(package.install_script_path.c_str()); |
| 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. |
| std::system(package.activation_script_path.c_str()); |
| } |
| } |
| |
| 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; |
| } |
| |
| std::vector<Package> ParsePostInstallPackages( |
| const std::string& tar_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; |
| std::error_code ec; |
| for (const auto& entry : std::filesystem::directory_iterator(untar_dir, ec)) { |
| if (!ec && entry.is_directory() && |
| std::filesystem::exists(entry.path() / "post_install.sh")) { |
| 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); |
| } |
| } |
| 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) { |
| std::vector<Package> packages = |
| ParsePostInstallPackages(bundle_tar_file_path); |
| 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 bundle_tar_file = bundle_tar_file_path; |
| std::vector<Package> packages_to_install = |
| ParsePostInstallPackages(bundle_tar_file); |
| 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; |
| } |
| |
| } // namespace bloom_install |