diff --git a/tools/nymvisor/src/cli/add_upgrade.rs b/tools/nymvisor/src/cli/add_upgrade.rs index 4fcca805da..3504faf59f 100644 --- a/tools/nymvisor/src/cli/add_upgrade.rs +++ b/tools/nymvisor/src/cli/add_upgrade.rs @@ -35,26 +35,25 @@ pub(crate) struct Args { /// Indicate that this command should only add binary to an *existing* scheduled upgrade #[arg(long)] - #[deprecated(note = "need to implement")] add_binary: bool, /// Force the upgrade to happen immediately - #[arg(long, group = "time")] + #[arg(long, group = "time", conflicts_with = "add_binary")] now: bool, /// Specifies the publish date metadata field of this upgrade. /// If unset, the current time will be used. - #[arg(long, value_parser = parse_rfc3339_upgrade_time)] + #[arg(long, value_parser = parse_rfc3339_upgrade_time, conflicts_with = "add_binary")] publish_date: Option, /// Specifies the time at which the provided upgrade will be performed (RFC3339 formatted). /// If left unset, the upgrade will be performed in 15min - #[arg(long, value_parser = parse_rfc3339_upgrade_time, group = "time")] + #[arg(long, value_parser = parse_rfc3339_upgrade_time, group = "time", conflicts_with = "add_binary")] upgrade_time: Option, /// Specifies delay until the provided upgrade is going to get performed. /// If let unset, the upgrade will be performed in 15min - #[arg(long, value_parser = humantime::parse_duration, group = "time")] + #[arg(long, value_parser = humantime::parse_duration, group = "time", conflicts_with = "add_binary")] upgrade_delay: Option, #[clap(short, long, default_value_t = OutputFormat::default())] @@ -87,52 +86,54 @@ pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> { if env.daemon_name.is_none() { env.daemon_name = Some(bin_info.binary_name.clone()); } - let config = try_load_current_config(&env)?; - let mut current_upgrade_plan = UpgradePlan::try_load(config.upgrade_plan_filepath())?; - - let upgrade_time = args.determine_upgrade_time(); - let upgrade_info = UpgradeInfo { - manual: false, - name: args.upgrade_name, - notes: "manually added via 'add-upgrade' command".to_string(), - publish_date: Some(args.publish_date.unwrap_or(OffsetDateTime::now_utc())), - version: bin_info.build_version.clone(), - platforms: Default::default(), - upgrade_time, - binary_details: Some(bin_info), - }; - - let upgrade_info_path = config.upgrade_dir(&upgrade_info.name); - if upgrade_info_path.exists() { - // TODO: maybe just copy binary? - todo!() - } - - // TODO: check if upgrade-plan already contains this upgrade - - // if upgrade_info_path.exists() && !args.force { - // return Err(NymvisorError::ExistingUpgrade { - // name: upgrade_info.name, - // path: upgrade_info_path, - // }); - // } - let upgrade_binary_path = config.upgrade_binary_dir(&upgrade_info.name); - if upgrade_binary_path.exists() && !args.force { + let upgrade_info_path = config.upgrade_info_filepath(&args.upgrade_name); + let bin_path = config.upgrade_binary(&args.upgrade_name); + if bin_path.exists() && !args.force { return Err(NymvisorError::ExistingUpgrade { - name: upgrade_info.name, - path: upgrade_binary_path, + name: args.upgrade_name, + path: bin_path, }); } - init_path(upgrade_binary_path)?; - copy_binary( - &args.daemon_binary, - config.upgrade_binary(&upgrade_info.name), - )?; - upgrade_info.save(config.upgrade_info_filepath(&upgrade_info.name))?; - current_upgrade_plan.insert_new_upgrade(upgrade_info)?; + // if we're just adding the binary, the upgrade plan MUST already exist, + // otherwise it MUSTN'T exist (unless --force is used) + if args.add_binary { + let upgrade_info = UpgradeInfo::try_load(upgrade_info_path)?; + upgrade_info.ensure_matches_bin_info(&bin_info)?; + } else { + if upgrade_info_path.exists() && !args.force { + return Err(NymvisorError::ExistingUpgradeInfo { + name: args.upgrade_name, + path: upgrade_info_path, + }); + } + + let mut current_upgrade_plan = UpgradePlan::try_load(config.upgrade_plan_filepath())?; + let upgrade_info = UpgradeInfo { + manual: false, + name: args.upgrade_name.clone(), + notes: "manually added via 'add-upgrade' command".to_string(), + publish_date: Some(args.publish_date.unwrap_or(OffsetDateTime::now_utc())), + version: bin_info.build_version.clone(), + platforms: Default::default(), + upgrade_time: args.determine_upgrade_time(), + binary_details: Some(bin_info), + }; + + if current_upgrade_plan.has_planned_by_name(&args.upgrade_name) { + return Err(NymvisorError::UpgradePlanWithNoInfo { + name: args.upgrade_name, + }); + } + + init_path(config.upgrade_binary_dir(&args.upgrade_name))?; + upgrade_info.save(config.upgrade_info_filepath(&upgrade_info.name))?; + current_upgrade_plan.insert_new_upgrade(upgrade_info)?; + } + + copy_binary(&args.daemon_binary, bin_path)?; Ok(()) } diff --git a/tools/nymvisor/src/cli/mod.rs b/tools/nymvisor/src/cli/mod.rs index b50c798fc0..bfe175739f 100644 --- a/tools/nymvisor/src/cli/mod.rs +++ b/tools/nymvisor/src/cli/mod.rs @@ -43,7 +43,7 @@ impl Cli { setup_env(&self.config_env_file)?; match self.command { - Commands::Init(args) => init::execute(args), + Commands::Init(args) => init::execute(*args), Commands::Run(args) => run::execute(args), Commands::BuildInfo(args) => build_info::execute(args), Commands::DaemonBuildInfo(args) => daemon_build_info::execute(args), @@ -56,7 +56,7 @@ impl Cli { #[derive(Subcommand, Debug)] pub(crate) enum Commands { /// Initialise a nymvisor instance with persistent Config.toml file. - Init(init::Args), + Init(Box), /// Run the associated daemon with the preconfigured settings. Run(run::Args), diff --git a/tools/nymvisor/src/cli/run.rs b/tools/nymvisor/src/cli/run.rs index 9673ee5ca2..fc698f93e8 100644 --- a/tools/nymvisor/src/cli/run.rs +++ b/tools/nymvisor/src/cli/run.rs @@ -8,7 +8,10 @@ use crate::tasks::launcher::DaemonLauncher; use crate::tasks::upgrade_plan_watcher::start_upgrade_plan_watcher; use crate::tasks::upstream_poller::UpstreamPoller; use nym_bin_common::logging::setup_tracing_logger; +use std::future::Future; +use std::time::Duration; use tokio::runtime; +use tokio::time::timeout; use tracing::{error, info}; #[derive(clap::Args, Debug)] @@ -27,8 +30,8 @@ pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> { info!("starting nymvisor for {}", config.daemon.name); - // TODO: experiment with the minimal runtime - // look at futures::executor::LocalPool + // TODO: experiment with other minimal runtimes, maybe futures::executor::LocalPool + // // well, if the creation of the runtime failed, there isn't much we could do #[allow(clippy::expect_used)] let rt = runtime::Builder::new_current_thread() @@ -62,11 +65,18 @@ pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> { upstream_poller_handle.abort(); } - // TODO: add timeouts and error handling here - // TODO2: maybe we need to make those fuse futures? - watcher_handle.await; - upstream_poller_handle.await; + wait_for_task_termination(watcher_handle, "Upgrade plan watcher").await; + wait_for_task_termination(upstream_poller_handle, "Upstream poller").await; Ok(()) }) } + +async fn wait_for_task_termination(task: F, name: &str) { + match timeout(Duration::from_secs(2), task).await { + Ok(_) => info!("{name} has finished execution"), + Err(_timeout) => { + error!("{name} task has timed out and has not shutdown gracefully") + } + } +} diff --git a/tools/nymvisor/src/config/mod.rs b/tools/nymvisor/src/config/mod.rs index 479aac95a4..213c27a5f2 100644 --- a/tools/nymvisor/src/config/mod.rs +++ b/tools/nymvisor/src/config/mod.rs @@ -206,10 +206,6 @@ impl Config { Self::read_from_path(path) } - pub fn read_from_default_path>(id: P) -> io::Result { - Self::read_from_path(default_config_filepath(id)) - } - pub fn default_location(&self) -> PathBuf { default_config_filepath(&self.nymvisor.id) } @@ -223,6 +219,8 @@ impl Config { save_formatted_config_to_file(self, path) } + // this code will be needed for config upgrades + #[allow(dead_code)] pub fn try_save(&self) -> io::Result<()> { if let Some(save_location) = &self.save_path { save_formatted_config_to_file(self, save_location) @@ -253,7 +251,7 @@ impl Config { // e.g. $HOME/.nym/nym-api//nymvisor/current-version.json pub fn current_daemon_version_filepath(&self) -> PathBuf { - todo!() + self.daemon_nymvisor_dir().join(CURRENT_VERSION_FILENAME) } // e.g. $HOME/.nym/nymvisors/data/nym-api/ diff --git a/tools/nymvisor/src/daemon/mod.rs b/tools/nymvisor/src/daemon/mod.rs index 22428226e4..6c552e8238 100644 --- a/tools/nymvisor/src/daemon/mod.rs +++ b/tools/nymvisor/src/daemon/mod.rs @@ -84,10 +84,17 @@ impl Daemon { #[instrument(skip(self), fields(self.executable_path = ?self.executable_path))] pub(crate) fn verify_binary(&self) -> Result<(), NymvisorError> { - let metadata = fs::metadata(&self.executable_path).expect("error handling"); + let metadata = fs::metadata(&self.executable_path).map_err(|source| { + NymvisorError::MetadataReadFailure { + path: self.executable_path.clone(), + source, + } + })?; if !metadata.is_file() { - todo!("error not a file") + return Err(NymvisorError::DaemonNotAFile { + path: self.executable_path.clone(), + }); } let mut permissions = metadata.permissions(); @@ -100,7 +107,12 @@ impl Daemon { let new_mode = mode | 0o111; // Set the three execute bits to on (a+x). permissions.set_mode(new_mode); - fs::set_permissions(&self.executable_path, permissions).expect("error handling"); + fs::set_permissions(&self.executable_path, permissions).map_err(|source| { + NymvisorError::DaemonPermissionFailure { + path: self.executable_path.clone(), + source, + } + })?; } Ok(()) diff --git a/tools/nymvisor/src/error.rs b/tools/nymvisor/src/error.rs index dffa7f02b2..71345793ae 100644 --- a/tools/nymvisor/src/error.rs +++ b/tools/nymvisor/src/error.rs @@ -130,13 +130,13 @@ While the stored info point to:\n{current_info:#?}" }, #[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:#?}" + "the current daemon build information does not match the expected stored value.\n\ +The daemon build is:\n{daemon_info:#?}\n\ +While the stored info point to:\n{stored_info:#?}" )] UnexpectedDaemonBuild { - daemon_version: Box, - current_version_info: Box, + daemon_info: Box, + stored_info: Box, }, #[error("the daemon for upgrade '{upgrade_name}' has version {daemon_version} while {expected} was expected instead")] @@ -146,6 +146,23 @@ While the stored info point to:\n{current_version_info:#?}" expected: String, }, + #[error("the provided daemon at {} is not a file", path.display())] + DaemonNotAFile { path: PathBuf }, + + #[error("could not read daemon's metadata at {}: {source}", path.display())] + MetadataReadFailure { + path: PathBuf, + #[source] + source: io::Error, + }, + + #[error("could not adjust permission of the daemon at: {}: {source}", path.display())] + DaemonPermissionFailure { + path: PathBuf, + #[source] + source: io::Error, + }, + #[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, @@ -304,9 +321,15 @@ While the stored info point to:\n{current_version_info:#?}" provided_genesis: Box, }, - #[error("there already exist upgrade binary for '{name}' at: {}. if you want to ovewrite its content, use --force flag", path.display())] + #[error("there already exist upgrade binary for '{name}' at: {}. if you want to overwrite its content, use --force flag", path.display())] ExistingUpgrade { name: String, path: PathBuf }, + #[error("there already exist upgrade information for '{name}' at: {}. if you want to overwrite its content, use --force flag", path.display())] + ExistingUpgradeInfo { name: String, path: PathBuf }, + + #[error("the current upgrade-plan.json has planned upgrade for '{name}', but no corresponding upgrade-info.json file could be found")] + UpgradePlanWithNoInfo { name: String }, + #[error("there was already a symlink for the 'current' binary of {daemon_name}. it's pointing to {} while we needed to create one to {}", link.display(), expected_link.display())] ExistingCurrentSymlink { daemon_name: String, diff --git a/tools/nymvisor/src/tasks/launcher/mod.rs b/tools/nymvisor/src/tasks/launcher/mod.rs index 9089dcb09b..bf5911215e 100644 --- a/tools/nymvisor/src/tasks/launcher/mod.rs +++ b/tools/nymvisor/src/tasks/launcher/mod.rs @@ -146,8 +146,8 @@ impl DaemonLauncher { 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), + daemon_info: Box::new(daemon_info), + stored_info: Box::new(expected_version.binary_details), }); } diff --git a/tools/nymvisor/src/upgrades/mod.rs b/tools/nymvisor/src/upgrades/mod.rs index 30cb8302fa..8964b68314 100644 --- a/tools/nymvisor/src/upgrades/mod.rs +++ b/tools/nymvisor/src/upgrades/mod.rs @@ -88,13 +88,7 @@ pub(crate) async fn perform_upgrade(config: &Config) -> Result Result Result<(), NymvisorError> { - // - // } + pub(crate) fn has_planned_by_name(&self, upgrade_name: &str) -> bool { + for planned in &self.queued_up { + if planned.name == upgrade_name { + return true; + } + } + false + } pub(crate) fn save_new>(&self, path: P) -> Result<(), NymvisorError> { debug_assert!(self._save_path.is_none()); @@ -280,9 +284,9 @@ impl UpgradeInfo { }) } - pub(crate) fn is_genesis(&self) -> bool { - self.name == GENESIS_DIR - } + // pub(crate) fn is_genesis(&self) -> bool { + // self.name == GENESIS_DIR + // } pub(crate) fn get_download_url(&self) -> Result<&DownloadUrl, NymvisorError> { if let Some(download_url) = self.platforms.get(&os_arch()) { @@ -319,6 +323,29 @@ impl UpgradeInfo { Ok(()) } + + pub(crate) fn ensure_matches_bin_info( + &self, + info: &BinaryBuildInformationOwned, + ) -> Result<(), NymvisorError> { + if let Some(self_info) = &self.binary_details { + if self_info != info { + return Err(NymvisorError::UnexpectedDaemonBuild { + daemon_info: Box::new(info.clone()), + stored_info: Box::new(self_info.clone()), + }); + } + } + if self.version != info.build_version { + return Err(NymvisorError::UnexpectedUpgradeDaemonVersion { + upgrade_name: self.name.clone(), + daemon_version: info.build_version.clone(), + expected: self.version.clone(), + }); + } + + Ok(()) + } } #[derive(Serialize, Deserialize, Debug)]