diff --git a/tools/nymvisor/src/cli/init.rs b/tools/nymvisor/src/cli/init.rs index ec8d282d8c..40003eff36 100644 --- a/tools/nymvisor/src/cli/init.rs +++ b/tools/nymvisor/src/cli/init.rs @@ -2,12 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 use crate::cli::helpers::{copy_binary, daemon_home, use_logs}; -use crate::config::{default_config_filepath, Config, BIN_DIR, GENESIS_DIR}; +use crate::config::{ + default_config_filepath, Config, BIN_DIR, CURRENT_VERSION_FILENAME, GENESIS_DIR, +}; use crate::daemon::Daemon; use crate::env::Env; use crate::error::NymvisorError; use crate::helpers::init_path; -use crate::upgrades::types::{UpgradeInfo, UpgradePlan}; +use crate::upgrades::types::{CurrentVersionInfo, UpgradeInfo, UpgradePlan}; use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_bin_common::logging::setup_tracing_logger; use nym_bin_common::output_format::OutputFormat; @@ -15,7 +17,7 @@ use std::fs; use std::path::{Path, PathBuf}; use std::time::Duration; use time::OffsetDateTime; -use tracing::{debug, info, trace, warn}; +use tracing::{debug, info, warn}; use url::Url; #[derive(clap::Args, Debug)] @@ -223,10 +225,27 @@ fn init_paths(config: &Config) -> Result<(), NymvisorError> { Ok(()) } +fn setup_daemon_current_version( + config: &Config, + daemon_info: &BinaryBuildInformationOwned, +) -> Result<(), NymvisorError> { + info!("setting up initial {}", CURRENT_VERSION_FILENAME); + let path = config.current_daemon_version_filepath(); + + let initial = CurrentVersionInfo { + name: GENESIS_DIR.to_string(), + version: daemon_info.build_version.clone(), + upgrade_time: OffsetDateTime::now_utc(), + binary_details: daemon_info.clone(), + }; + + initial.save(path) +} + fn setup_genesis( config: &Config, source: &Path, - daemon_info: BinaryBuildInformationOwned, + daemon_info: &BinaryBuildInformationOwned, ) -> Result<(), NymvisorError> { info!("setting up the genesis binary"); let target = config.genesis_daemon_binary(); @@ -234,7 +253,7 @@ fn setup_genesis( if target.exists() { // if there already exists a binary at the genesis location, see if it's the same one let existing_bin_info = Daemon::new(target).get_build_information()?; - return if existing_bin_info != daemon_info { + return if &existing_bin_info != daemon_info { Err(NymvisorError::DuplicateDaemonGenesisBinary { daemon_name: config.daemon.name.clone(), existing_info: Box::new(existing_bin_info), @@ -286,7 +305,7 @@ fn create_current_symlink(config: &Config) -> Result<(), NymvisorError> { fn generate_and_save_genesis_upgrade_info( config: &Config, - genesis_info: BinaryBuildInformationOwned, + genesis_info: &BinaryBuildInformationOwned, ) -> Result { info!("setting up the genesis upgrade-info.json"); @@ -298,7 +317,7 @@ fn generate_and_save_genesis_upgrade_info( version: genesis_info.build_version.clone(), platforms: Default::default(), upgrade_time: OffsetDateTime::UNIX_EPOCH, - binary_details: Some(genesis_info), + binary_details: Some(genesis_info.clone()), }; let save_path = config.upgrade_info_filepath(&info.name); @@ -384,6 +403,7 @@ fn save_config(config: &Config, env: &Env) -> Result<(), NymvisorError> { /// - creating `//upgrades` folder if it doesn't yet exist /// - copying the provided executable to `//genesis/bin/` /// - generating initial `//genesis/upgrade-info.json` file +/// - generating initial `/nymvisor/current-version-info.json` file /// - creating a `//current` symlink pointing to `//genesis` /// - saving nymvisor's config file to `` and creating the full directory structure. /// @@ -406,7 +426,8 @@ pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> { let config = try_build_config(&args, &env, &daemon_info)?; init_paths(&config)?; - setup_genesis(&config, &args.daemon_binary, daemon_info)?; + setup_genesis(&config, &args.daemon_binary, &daemon_info)?; + setup_daemon_current_version(&config, &daemon_info)?; create_current_symlink(&config)?; save_config(&config, &env)?; diff --git a/tools/nymvisor/src/daemon/mod.rs b/tools/nymvisor/src/daemon/mod.rs index a8fdac25ff..22428226e4 100644 --- a/tools/nymvisor/src/daemon/mod.rs +++ b/tools/nymvisor/src/daemon/mod.rs @@ -56,12 +56,6 @@ impl Daemon { } } - #[must_use] - pub(crate) fn with_kill_timeout(mut self, kill_timeout: Duration) -> Self { - self.kill_timeout = kill_timeout; - self - } - #[instrument(skip(self), fields(self.executable_path = ?self.executable_path))] pub(crate) fn get_build_information( &self, diff --git a/tools/nymvisor/src/error.rs b/tools/nymvisor/src/error.rs index 975f1e6678..dffa7f02b2 100644 --- a/tools/nymvisor/src/error.rs +++ b/tools/nymvisor/src/error.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::upgrades::types::DigestAlgorithm; +use crate::upgrades::types::{CurrentVersionInfo, DigestAlgorithm, UpgradeInfo}; use async_file_watcher::NotifyError; use nix::errno::Errno; use nix::sys::signal::Signal; @@ -101,6 +101,51 @@ pub(crate) enum NymvisorError { source: io::Error, }, + #[error( + "failed to load current version information using path '{}'. detailed message: {source}", path.display() + )] + CurrentVersionInfoLoadFailure { + path: PathBuf, + #[source] + source: io::Error, + }, + + #[error( + "failed to save current version information using path '{}'. detailed message: {source}", path.display() + )] + CurrentVersionInfoSaveFailure { + path: PathBuf, + #[source] + source: io::Error, + }, + + #[error( + "the current version information does not match the expected `current/upgrade-info.json`.\n\ +The daemon version is:\n{current_version_info:#?}\n\ +While the stored info point to:\n{current_info:#?}" + )] + UnexpectedCurrentVersionInfo { + current_info: Box, + current_version_info: Box, + }, + + #[error( + "the current daemon build does not match the expected value in `current-version.info.json`.\n\ +The daemon version is:\n{daemon_version:#?}\n\ +While the stored info point to:\n{current_version_info:#?}" + )] + UnexpectedDaemonBuild { + daemon_version: Box, + current_version_info: Box, + }, + + #[error("the daemon for upgrade '{upgrade_name}' has version {daemon_version} while {expected} was expected instead")] + UnexpectedUpgradeDaemonVersion { + upgrade_name: String, + daemon_version: String, + expected: String, + }, + #[error("could not acquire the lock at {} to perform binary upgrade with error code {libc_code}. It is either held by another process or this nymvisor has experienced a critical failure during previous upgrade attempt", lock_path.display())] UnableToAcquireUpgradePlanLock { lock_path: PathBuf, diff --git a/tools/nymvisor/src/main.rs b/tools/nymvisor/src/main.rs index 0e566d00da..50d703a2da 100644 --- a/tools/nymvisor/src/main.rs +++ b/tools/nymvisor/src/main.rs @@ -3,6 +3,8 @@ #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] use crate::cli::Cli; use clap::Parser; diff --git a/tools/nymvisor/src/tasks/launcher/mod.rs b/tools/nymvisor/src/tasks/launcher/mod.rs index e9c416d624..9089dcb09b 100644 --- a/tools/nymvisor/src/tasks/launcher/mod.rs +++ b/tools/nymvisor/src/tasks/launcher/mod.rs @@ -4,7 +4,9 @@ use crate::config::Config; use crate::daemon::Daemon; use crate::error::NymvisorError; -use crate::upgrades::{types::UpgradePlan, upgrade_binary, UpgradeResult}; +use crate::tasks::launcher::backup::BackupBuilder; +use crate::upgrades::types::{CurrentVersionInfo, UpgradeInfo}; +use crate::upgrades::{perform_upgrade, types::UpgradePlan, UpgradeResult}; use async_file_watcher::FileWatcherEventReceiver; use futures::future::{FusedFuture, OptionFuture}; use futures::{FutureExt, StreamExt}; @@ -103,7 +105,7 @@ impl DaemonLauncher { self.perform_backup()?; } - upgrade_binary(&self.config).await + perform_upgrade(&self.config).await // if we ever wanted to introduce any pre-upgrade scripts like cosmovisor, they'd go here } @@ -136,8 +138,18 @@ impl DaemonLauncher { ) -> Result { let daemon = Daemon::from_config(&self.config); - let unused_variable = 42; - // TODO: check whether the daemon's version matches the expected value + let current_info = UpgradeInfo::try_load(self.config.current_upgrade_info_filepath())?; + let expected_version = + CurrentVersionInfo::try_load(self.config.current_daemon_version_filepath())?; + let daemon_info = daemon.get_build_information()?; + + current_info.ensure_matches(&expected_version)?; + if expected_version.binary_details != daemon_info { + return Err(NymvisorError::UnexpectedDaemonBuild { + daemon_version: Box::new(daemon_info), + current_version_info: Box::new(expected_version.binary_details), + }); + } let mut running_daemon = daemon.execute_async(args)?; let interrupt_handle = running_daemon.interrupt_handle(); @@ -210,8 +222,15 @@ impl DaemonLauncher { } fn perform_backup(&self) -> Result<(), NymvisorError> { - let upgrade_name = todo!(); - // BackupBuilder::new(self.config.daemon_upgrade_backup_dir(upgrade_name))? - // .backup_daemon_home(&self.config.daemon.home) + let plan = UpgradePlan::try_load(self.config.upgrade_plan_filepath())?; + + let Some(upgrade_name) = plan.next_upgrade().map(|u| &u.name) else { + // this should NEVER be possible, but because those famous last words have been said before, + // let's just return an error when it inevitably happens + return Err(NymvisorError::NoQueuedUpgrades); + }; + + BackupBuilder::new(self.config.daemon_upgrade_backup_dir(upgrade_name))? + .backup_daemon_home(&self.config.daemon.home) } } diff --git a/tools/nymvisor/src/upgrades/mod.rs b/tools/nymvisor/src/upgrades/mod.rs index a4028bce3f..30cb8302fa 100644 --- a/tools/nymvisor/src/upgrades/mod.rs +++ b/tools/nymvisor/src/upgrades/mod.rs @@ -5,12 +5,13 @@ use crate::config::Config; use crate::daemon::Daemon; use crate::error::NymvisorError; use crate::upgrades::download::download_upgrade_binary; -use crate::upgrades::types::{UpgradeHistory, UpgradePlan}; +use crate::upgrades::types::{CurrentVersionInfo, UpgradeHistory, UpgradePlan}; use nix::fcntl::{flock, FlockArg}; use std::fs; use std::fs::File; use std::os::fd::AsRawFd; use std::path::PathBuf; +use time::OffsetDateTime; use tracing::{debug, info}; pub(crate) mod download; @@ -32,7 +33,7 @@ impl UpgradeResult { } } -pub(crate) async fn upgrade_binary(config: &Config) -> Result { +pub(crate) async fn perform_upgrade(config: &Config) -> Result { info!("attempting to perform binary upgrade"); let mut plan = UpgradePlan::try_load(config.upgrade_plan_filepath())?; @@ -88,14 +89,29 @@ pub(crate) async fn upgrade_binary(config: &Config) -> Result>(path: P) -> Result { let path = path.as_ref(); - std::fs::File::open(path) + fs::File::open(path) .and_then(|file| { serde_json::from_reader(file) .map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err)) @@ -295,6 +295,30 @@ impl UpgradeInfo { arch: os_arch(), }) } + + /// Check whether the loaded (presumably `current`) upgrade-info matches the provided current version information. + pub(crate) fn ensure_matches( + &self, + current_info: &CurrentVersionInfo, + ) -> Result<(), NymvisorError> { + if self.name != current_info.name || self.version != current_info.version { + return Err(NymvisorError::UnexpectedCurrentVersionInfo { + current_info: Box::new(self.clone()), + current_version_info: Box::new(current_info.clone()), + }); + } + + if let Some(bin_info) = &self.binary_details { + if bin_info != ¤t_info.binary_details { + return Err(NymvisorError::UnexpectedCurrentVersionInfo { + current_info: Box::new(self.clone()), + current_version_info: Box::new(current_info.clone()), + }); + } + } + + Ok(()) + } } #[derive(Serialize, Deserialize, Debug)] @@ -373,7 +397,7 @@ impl UpgradeHistory { pub(crate) fn try_load>(path: P) -> Result { let path = path.as_ref(); - std::fs::File::open(path) + fs::File::open(path) .and_then(|file| { serde_json::from_reader(file) .map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err)) @@ -385,6 +409,54 @@ impl UpgradeHistory { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "lowercase")] -pub struct CurrentVersionInfo {} +pub struct CurrentVersionInfo { + /// Name of the current version, for example `2023.4-galaxy` + pub name: String, + + /// Version of this upgrade, for example `1.1.69` + pub version: String, + + /// Time when the upgrade has happened. + #[serde(with = "time::serde::rfc3339")] + pub upgrade_time: OffsetDateTime, + + /// Build information of the expected current binary for additional verification + pub binary_details: BinaryBuildInformationOwned, +} + +impl CurrentVersionInfo { + pub(crate) fn save>(&self, path: P) -> Result<(), NymvisorError> { + let path = path.as_ref(); + + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path) + .map_err(|source| NymvisorError::CurrentVersionInfoSaveFailure { + path: path.to_path_buf(), + source, + })?; + + // we're not using any non-standard serializer and thus the serialization should not ever fail + #[allow(clippy::expect_used)] + serde_json::to_writer_pretty(file, self) + .expect("unexpected CurrentVersionInfo serialization failure"); + Ok(()) + } + + pub(crate) fn try_load>(path: P) -> Result { + let path = path.as_ref(); + fs::File::open(path) + .and_then(|file| { + serde_json::from_reader(file) + .map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err)) + }) + .map_err(|source| NymvisorError::CurrentVersionInfoLoadFailure { + path: path.to_path_buf(), + source, + }) + } +}