437 lines
18 KiB
Rust
437 lines
18 KiB
Rust
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
|
// 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, 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::{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;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::Duration;
|
|
use time::OffsetDateTime;
|
|
use tracing::{debug, info, warn};
|
|
use url::Url;
|
|
|
|
#[derive(clap::Args, Debug)]
|
|
pub(crate) struct Args {
|
|
/// Path to the daemon's executable.
|
|
daemon_binary: PathBuf,
|
|
|
|
/// ID specifies the human readable ID of this particular nymvisor instance.
|
|
/// Can be overridden with $NYMVISOR_ID environmental variable.
|
|
#[arg(long)]
|
|
id: Option<String>,
|
|
|
|
/// Sets the base url of the upstream source for obtaining upgrade information for the deaemon.
|
|
/// It will be used fo constructing the full url, i.e. $NYMVISOR_UPSTREAM_BASE_UPGRADE_URL/$DAEMON_NAME/upgrade-info.json
|
|
/// Can be overridden with $NYMVISOR_UPSTREAM_BASE_UPGRADE_URL environmental variable.
|
|
#[arg(long)]
|
|
upstream_base_upgrade_url: Option<Url>,
|
|
|
|
/// Specifies the rate of polling the upstream url for upgrade information.
|
|
/// default: 1h
|
|
/// Can be overridden with $NYMVISOR_UPSTREAM_POLLING_RATE
|
|
#[arg(long, value_parser = humantime::parse_duration)]
|
|
upstream_polling_rate: Option<Duration>,
|
|
|
|
/// If enabled, this will disable `nymvisor` logs (but not the underlying process)
|
|
/// Can be overridden with $NYMVISOR_DISABLE_LOGS environmental variable.
|
|
#[arg(long)]
|
|
disable_nymvisor_logs: bool,
|
|
|
|
/// Set custom directory for upgrade data - binaries and upgrade plans.
|
|
/// If not set, the global nymvisors' data directory will be used instead.
|
|
/// Can be overridden with $NYMVISOR_UPGRADE_DATA_DIRECTORY environmental variable.
|
|
#[arg(long)]
|
|
upgrade_data_directory: Option<PathBuf>,
|
|
|
|
/// The location where the `nymvisor/` directory is kept that contains the auxiliary files associated
|
|
/// with the underlying daemon, such as any backups or current version information.
|
|
/// (e.g. $HOME/.nym/nym-api/my-nym-api, $HOME/.nym/mixnodes/my-mixnode, etc.).
|
|
/// Can be overridden with $DAEMON_HOME environmental variable.
|
|
#[arg(long)]
|
|
daemon_home: Option<PathBuf>,
|
|
|
|
/// Override url to the upstream source for upgrade plans for this daeamon.
|
|
/// The Url has to point to an endpoint containing a valid [`UpgradeInfo`] json.
|
|
/// Note: if set this takes precedence over `upstream_base_upgrade_url`
|
|
/// Can be overridden with $DAEMON_ABSOLUTE_UPSTREAM_UPGRADE_URL environmental variable.
|
|
#[arg(long)]
|
|
daemon_absolute_upstream_upgrade_url: Option<Url>,
|
|
|
|
/// If set to true, this will enable auto-downloading of new binaries using the url provided in the `upgrade-info.json`
|
|
/// Can be overridden with $DAEMON_ALLOW_BINARIES_DOWNLOAD environmental variable.
|
|
#[arg(long)]
|
|
allow_download_upgrade_binaries: Option<bool>,
|
|
|
|
/// If enabled nymvisor will require that a checksum is provided in the upgrade plan for the binary to be downloaded.
|
|
/// If disabled, nymvisor will not require a checksum to be provided, but still check the checksum if one is provided.
|
|
/// Can be overridden with $DAEMON_ENFORCE_DOWNLOAD_CHECKSUM environmental variable.
|
|
#[arg(long)]
|
|
enforce_download_checksum: Option<bool>,
|
|
|
|
/// If enabled, nymvisor will restart the subprocess with the same command-line arguments and flags (but with the new binary) after a successful upgrade.
|
|
/// Otherwise (if disabled), nymvisor will stop running after an upgrade and will require the system administrator to manually restart it.
|
|
/// Note restart is only after the upgrade and does not auto-restart the subprocess after an error occurs.
|
|
/// Can be overridden with $DAEMON_RESTART_AFTER_UPGRADE environmental variable.
|
|
#[arg(long)]
|
|
restart_daemon_after_upgrade: Option<bool>,
|
|
|
|
/// If enabled, nymvisor will restart the subprocess with the same command-line arguments and flags after it has crashed
|
|
/// Can be overridden with $DAEMON_RESTART_ON_FAILURE environmental variable.
|
|
#[arg(long)]
|
|
restart_daemon_on_failure: bool,
|
|
|
|
/// If `restart_on_failure` is enabled, the following value defines the amount of time `nymvisor` shall wait before
|
|
/// restarting the subprocess.
|
|
/// Can be overridden with $DAEMON_FAILURE_RESTART_DELAY environmental variable.
|
|
#[arg(long, value_parser = humantime::parse_duration)]
|
|
on_failure_daemon_restart_delay: Option<Duration>,
|
|
|
|
/// Defines the maximum number of startup failures the subprocess can experience in a quick succession before
|
|
/// no further restarts will be attempted and `nymvisor` will exit.
|
|
/// Can be overridden with $DAEMON_MAX_STARTUP_FAILURES environmental variable.
|
|
#[arg(long)]
|
|
max_daemon_startup_failures: Option<usize>,
|
|
|
|
/// Defines the length of time during which the subprocess is still considered to be in the startup phase
|
|
/// when its failures are going to be considered in `max_startup_failures`.
|
|
/// Can be overridden with $DAEMON_STARTUP_PERIOD_DURATION environmental variable.
|
|
#[arg(long, value_parser = humantime::parse_duration)]
|
|
startup_period_duration: Option<Duration>,
|
|
|
|
/// Specifies the amount of time `nymvisor` is willing to wait for the subprocess to undergo graceful shutdown after receiving an interrupt
|
|
/// (for either an upgrade or shutdown of the `nymvisor` itself)
|
|
/// Once the time passes, a kill signal is going to be sent instead.
|
|
/// Can be overridden with $DAEMON_SHUTDOWN_GRACE_PERIOD environmental variable.
|
|
#[arg(long, value_parser = humantime::parse_duration)]
|
|
daemon_shutdown_grace_period: Option<Duration>,
|
|
|
|
/// Set custom backup directory for daemon data. If not set, the daemon's home directory will be used instead.
|
|
/// Can be overridden with $DAEMON_BACKUP_DATA_DIRECTORY environmental variable.
|
|
#[arg(long)]
|
|
daemon_backup_data_directory: Option<PathBuf>,
|
|
|
|
/// If enabled, `nymvisor` will perform upgrades directly without performing any backups.
|
|
/// default: false
|
|
/// Can be overridden with $DAEMON_UNSAFE_SKIP_BACKUP environmental variable.
|
|
#[arg(long)]
|
|
unsafe_skip_backup: bool,
|
|
|
|
#[arg(short, long, default_value_t = OutputFormat::default())]
|
|
output: OutputFormat,
|
|
}
|
|
|
|
impl Args {
|
|
pub(crate) fn override_config(&self, config: &mut Config) {
|
|
if let Some(nymvisor_id) = &self.id {
|
|
config.nymvisor.id.clone_from(nymvisor_id);
|
|
}
|
|
if let Some(upstream) = &self.upstream_base_upgrade_url {
|
|
config.nymvisor.debug.upstream_base_upgrade_url = upstream.clone()
|
|
}
|
|
if let Some(polling_rate) = self.upstream_polling_rate {
|
|
config.nymvisor.debug.upstream_polling_rate = polling_rate
|
|
}
|
|
if self.disable_nymvisor_logs {
|
|
config.nymvisor.debug.disable_logs = self.disable_nymvisor_logs;
|
|
}
|
|
if let Some(nymvisor_upgrade_data_directory) = &self.upgrade_data_directory {
|
|
config.nymvisor.debug.upgrade_data_directory =
|
|
Some(nymvisor_upgrade_data_directory.clone());
|
|
}
|
|
if let Some(daemon_home) = &self.daemon_home {
|
|
config.daemon.home.clone_from(daemon_home);
|
|
}
|
|
if let Some(upstream) = &self.daemon_absolute_upstream_upgrade_url {
|
|
config.daemon.debug.absolute_upstream_upgrade_url = Some(upstream.clone())
|
|
}
|
|
if let Some(daemon_allow_binaries_download) = self.allow_download_upgrade_binaries {
|
|
config.daemon.debug.allow_binaries_download = daemon_allow_binaries_download;
|
|
}
|
|
if let Some(enforce_download_checksum) = self.enforce_download_checksum {
|
|
config.daemon.debug.enforce_download_checksum = enforce_download_checksum;
|
|
}
|
|
if let Some(restart_daemon_after_upgrade) = self.restart_daemon_after_upgrade {
|
|
config.daemon.debug.restart_after_upgrade = restart_daemon_after_upgrade;
|
|
}
|
|
if self.restart_daemon_on_failure {
|
|
config.daemon.debug.restart_on_failure = self.restart_daemon_on_failure;
|
|
}
|
|
if let Some(on_failure_daemon_restart_delay) = self.on_failure_daemon_restart_delay {
|
|
config.daemon.debug.failure_restart_delay = on_failure_daemon_restart_delay;
|
|
}
|
|
if let Some(max_daemon_startup_failures) = self.max_daemon_startup_failures {
|
|
config.daemon.debug.max_startup_failures = max_daemon_startup_failures;
|
|
}
|
|
if let Some(startup_period_duration) = self.startup_period_duration {
|
|
config.daemon.debug.startup_period_duration = startup_period_duration;
|
|
}
|
|
if let Some(daemon_shutdown_grace_period) = self.daemon_shutdown_grace_period {
|
|
config.daemon.debug.shutdown_grace_period = daemon_shutdown_grace_period;
|
|
}
|
|
if let Some(daemon_backup_data_directory) = &self.daemon_backup_data_directory {
|
|
config.daemon.debug.backup_data_directory = Some(daemon_backup_data_directory.clone());
|
|
}
|
|
if self.unsafe_skip_backup {
|
|
config.daemon.debug.unsafe_skip_backup = self.unsafe_skip_backup;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn try_build_config(
|
|
args: &Args,
|
|
env: &Env,
|
|
daemon_info: &BinaryBuildInformationOwned,
|
|
) -> Result<Config, NymvisorError> {
|
|
let daemon_name = &daemon_info.binary_name;
|
|
let daemon_home = daemon_home(&args.daemon_home, env)?;
|
|
|
|
debug!(
|
|
"building config for '{daemon_name}' with home at {}",
|
|
daemon_home.display()
|
|
);
|
|
|
|
let mut config = Config::new(daemon_name, daemon_home);
|
|
|
|
// override config with environmental variables
|
|
debug!("overriding the config with command line arguments");
|
|
args.override_config(&mut config);
|
|
|
|
// and then override the result with the passed arguments
|
|
debug!("overriding the config with environmental variables");
|
|
env.override_config(&mut config);
|
|
|
|
Ok(config)
|
|
}
|
|
|
|
fn init_paths(config: &Config) -> Result<(), NymvisorError> {
|
|
info!("initialising the directory structure");
|
|
|
|
init_path(config.daemon_nymvisor_dir())?;
|
|
init_path(config.daemon_backup_dir())?;
|
|
init_path(config.upgrade_data_dir())?;
|
|
init_path(config.genesis_daemon_dir().join(BIN_DIR))?;
|
|
init_path(config.upgrades_dir())?;
|
|
|
|
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,
|
|
) -> Result<(), NymvisorError> {
|
|
info!("setting up the genesis binary");
|
|
let target = config.genesis_daemon_binary();
|
|
|
|
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 {
|
|
Err(NymvisorError::DuplicateDaemonGenesisBinary {
|
|
daemon_name: config.daemon.name.clone(),
|
|
existing_info: Box::new(existing_bin_info),
|
|
provided_genesis: Box::new(daemon_info.clone()),
|
|
})
|
|
} else {
|
|
debug!("there was already a genesis daemon binary present, but it was the same as the one provided");
|
|
Ok(())
|
|
};
|
|
}
|
|
|
|
let genesis_info = generate_and_save_genesis_upgrade_info(config, daemon_info)?;
|
|
setup_initial_upgrade_plan(config, genesis_info)?;
|
|
copy_binary(source, target)
|
|
}
|
|
|
|
fn create_current_symlink(config: &Config) -> Result<(), NymvisorError> {
|
|
info!("setting up the symlink to the genesis directory");
|
|
|
|
let original = config.genesis_daemon_dir();
|
|
let link = config.current_daemon_dir();
|
|
|
|
// check if a symlink already exists
|
|
if let Ok(existing_target) = fs::read_link(&link) {
|
|
return if existing_target != original {
|
|
Err(NymvisorError::ExistingCurrentSymlink {
|
|
daemon_name: config.daemon.name.clone(),
|
|
link: existing_target,
|
|
expected_link: original,
|
|
})
|
|
} else {
|
|
debug!(
|
|
"there already exist a symlink between {} and {}",
|
|
original.display(),
|
|
link.display()
|
|
);
|
|
Ok(())
|
|
};
|
|
}
|
|
|
|
std::os::unix::fs::symlink(&original, &link).map_err(|source| {
|
|
NymvisorError::SymlinkCreationFailure {
|
|
source_path: original,
|
|
target_path: link,
|
|
source,
|
|
}
|
|
})
|
|
}
|
|
|
|
fn generate_and_save_genesis_upgrade_info(
|
|
config: &Config,
|
|
genesis_info: &BinaryBuildInformationOwned,
|
|
) -> Result<UpgradeInfo, NymvisorError> {
|
|
info!("setting up the genesis upgrade-info.json");
|
|
|
|
let info = UpgradeInfo {
|
|
manual: true,
|
|
name: GENESIS_DIR.to_string(),
|
|
notes: "".to_string(),
|
|
publish_date: None,
|
|
version: genesis_info.build_version.clone(),
|
|
platforms: Default::default(),
|
|
upgrade_time: OffsetDateTime::UNIX_EPOCH,
|
|
binary_details: Some(genesis_info.clone()),
|
|
};
|
|
let save_path = config.upgrade_info_filepath(&info.name);
|
|
|
|
// if the upgrade info file already exists return an error since there is no associated binary
|
|
if save_path.exists() {
|
|
Err(NymvisorError::UpgradeInfoWithNoBinary {
|
|
name: info.name,
|
|
path: save_path,
|
|
})
|
|
} else {
|
|
info.save(save_path)?;
|
|
Ok(info)
|
|
}
|
|
}
|
|
|
|
fn setup_initial_upgrade_plan(
|
|
config: &Config,
|
|
genesis_info: UpgradeInfo,
|
|
) -> Result<(), NymvisorError> {
|
|
info!("setting up initial upgrade-plan.json");
|
|
|
|
let plan_path = config.upgrade_plan_filepath();
|
|
|
|
if plan_path.exists() {
|
|
warn!("there is already an upgrade-plan.json file present");
|
|
// if the file already exists, try to load it and see if the 'current' matches
|
|
let existing_plan = UpgradePlan::try_load(&plan_path)?;
|
|
if let (Some(current_info), Some(existing_info)) = (
|
|
&genesis_info.binary_details,
|
|
&existing_plan.current().binary_details,
|
|
) {
|
|
if current_info != existing_info {
|
|
// if possible, compare the actual full details
|
|
return Err(NymvisorError::PreexistingUpgradePlan {
|
|
path: plan_path,
|
|
current_name: genesis_info.name,
|
|
existing_name: existing_plan.current().name.clone(),
|
|
});
|
|
}
|
|
} else if genesis_info.name != existing_plan.current().name {
|
|
// otherwise just check the upgrade name
|
|
return Err(NymvisorError::PreexistingUpgradePlan {
|
|
path: plan_path,
|
|
current_name: genesis_info.name,
|
|
existing_name: existing_plan.current().name.clone(),
|
|
});
|
|
}
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
UpgradePlan::new(genesis_info).save_new(plan_path)
|
|
}
|
|
|
|
fn save_config(config: &Config, env: &Env) -> Result<(), NymvisorError> {
|
|
let id = &config.nymvisor.id;
|
|
let config_save_location = env
|
|
.nymvisor_config_path
|
|
.clone()
|
|
.unwrap_or(default_config_filepath(id));
|
|
|
|
info!(
|
|
"saving the config file to {}",
|
|
config_save_location.display()
|
|
);
|
|
|
|
config
|
|
.save_to_path(&config_save_location)
|
|
.map_err(|err| NymvisorError::ConfigSaveFailure {
|
|
path: config_save_location,
|
|
id: id.to_string(),
|
|
source: err,
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Initialise the nymvisor by performing the following:
|
|
/// - executing the `build-info` command on the daemon executable to check its validity and obtain its name
|
|
/// - creating `<DAEMON_HOME>/nymvisor` folder if it doesn't yet exist
|
|
/// - creating `<DAEMON_BACKUP_DATA_DIRECTORY>` folder if it doesn't yet exist
|
|
/// - creating `<NYMVISOR_UPGRADE_DATA_DIRECTORY>` folder if it doesn't yet exist
|
|
/// - creating `<NYMVISOR_UPGRADE_DATA_DIRECTORY>/<DAEMON_NAME>/genesis/bin` folder if it doesn't yet exist
|
|
/// - creating `<NYMVISOR_UPGRADE_DATA_DIRECTORY>/<DAEMON_NAME>/upgrades` folder if it doesn't yet exist
|
|
/// - copying the provided executable to `<NYMVISOR_UPGRADE_DATA_DIRECTORY>/<DAEMON_NAME>/genesis/bin/<DAEMON_NAME>`
|
|
/// - generating initial `<NYMVISOR_UPGRADE_DATA_DIRECTORY>/<DAEMON_NAME>/genesis/upgrade-info.json` file
|
|
/// - generating initial `<DAEMON_HOME>/nymvisor/current-version-info.json` file
|
|
/// - creating a `<NYMVISOR_UPGRADE_DATA_DIRECTORY>/<DAEMON_NAME>/current` symlink pointing to `<NYMVISOR_UPGRADE_DATA_DIRECTORY>/<DAEMON_NAME>/genesis`
|
|
/// - saving nymvisor's config file to `<NYMVISOR_CONFIG_PATH>` and creating the full directory structure.
|
|
///
|
|
/// note: it requires either passing `--daemon-home` flag or setting the `$DAEMON_HOME` environmental variable
|
|
pub(crate) fn execute(args: Args) -> Result<(), NymvisorError> {
|
|
let env = Env::try_read()?;
|
|
|
|
if use_logs(args.disable_nymvisor_logs, &env) {
|
|
setup_tracing_logger();
|
|
info!("enabled nymvisor logging");
|
|
}
|
|
|
|
info!("initialising the nymvisor");
|
|
|
|
// this serves two purposes:
|
|
// 1. we get daemon name if it wasn't provided via either a flag or env variable
|
|
// 2. we check if valid executable was provided
|
|
let daemon_info = Daemon::new(args.daemon_binary.clone()).get_build_information()?;
|
|
|
|
let config = try_build_config(&args, &env, &daemon_info)?;
|
|
|
|
init_paths(&config)?;
|
|
setup_genesis(&config, &args.daemon_binary, &daemon_info)?;
|
|
setup_daemon_current_version(&config, &daemon_info)?;
|
|
create_current_symlink(&config)?;
|
|
save_config(&config, &env)?;
|
|
|
|
println!("{}", args.output.format(&config));
|
|
Ok(())
|
|
}
|