diff --git a/Cargo.lock b/Cargo.lock index 2166190574..ae3ec06761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1550,6 +1550,7 @@ dependencies = [ "tokio-tungstenite", "topology", "url", + "version-checker", "websocket-requests", ] @@ -1601,6 +1602,7 @@ dependencies = [ "tokio-tungstenite", "tokio-util", "tungstenite", + "version-checker", ] [[package]] @@ -1627,6 +1629,7 @@ dependencies = [ "tokio", "tokio-util", "topology", + "version-checker", ] [[package]] @@ -1656,6 +1659,7 @@ dependencies = [ "tempfile", "tokio", "topology", + "version-checker", ] [[package]] diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index 1e04d21964..5c865fa312 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -21,6 +21,8 @@ use std::time::Duration; pub mod persistence; +pub const MISSING_VALUE: &str = "MISSING VALUE"; + // 'CLIENT' const DEFAULT_DIRECTORY_SERVER: &str = "https://directory.nymtech.net"; // 'DEBUG' @@ -39,6 +41,10 @@ const DEFAULT_VPN_KEY_REUSE_LIMIT: usize = 1000; const ZERO_DELAY: Duration = Duration::from_nanos(0); +pub fn missing_string_value() -> String { + MISSING_VALUE.to_string() +} + #[derive(Debug, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { @@ -140,6 +146,10 @@ impl Config { self.debug.vpn_key_reuse_limit = Some(reuse_limit) } + pub fn set_custom_version(&mut self, version: &str) { + self.client.version = version.to_string(); + } + pub fn get_id(&self) -> String { self.client.id.clone() } @@ -251,6 +261,10 @@ impl Config { ), } } + + pub fn get_version(&self) -> &str { + &self.client.version + } } impl Default for Config { @@ -267,6 +281,7 @@ impl Default for Config { #[serde(deny_unknown_fields)] pub struct Client { /// Version of the client for which this configuration was created. + #[serde(default = "missing_string_value")] version: String, /// ID specifies the human readable ID of this particular client. @@ -278,6 +293,7 @@ pub struct Client { /// Special mode of the system such that all messages are sent as soon as they are received /// and no cover traffic is generated. If set all message delays are set to 0 and overwriting /// 'Debug' values will have no effect. + #[serde(default)] vpn_mode: bool, /// Path to file containing private identity key. diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index f8d24a72a0..54e55f4c55 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -40,6 +40,7 @@ nymsphinx = { path = "../../common/nymsphinx" } pemstore = { path = "../../common/pemstore" } topology = { path = "../../common/topology" } websocket-requests = { path = "websocket-requests" } +version-checker = { path = "../../common/version-checker" } [dev-dependencies] tempfile = "3.1.0" diff --git a/clients/native/src/client/config/mod.rs b/clients/native/src/client/config/mod.rs index 9de9b8ee90..d100ce72dd 100644 --- a/clients/native/src/client/config/mod.rs +++ b/clients/native/src/client/config/mod.rs @@ -14,6 +14,7 @@ use crate::client::config::template::config_template; use client_core::config::Config as BaseConfig; +pub use client_core::config::MISSING_VALUE; use config::NymConfig; use serde::{Deserialize, Serialize}; use std::path::PathBuf; diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index 927ff08736..f7792206c3 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -15,8 +15,9 @@ use crate::client::config::{Config, SocketType}; use clap::ArgMatches; -pub mod init; -pub mod run; +pub(crate) mod init; +pub(crate) mod run; +pub(crate) mod upgrade; pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Config { if let Some(directory) = matches.value_of("directory") { diff --git a/clients/native/src/commands/upgrade.rs b/clients/native/src/commands/upgrade.rs new file mode 100644 index 0000000000..47802b1eab --- /dev/null +++ b/clients/native/src/commands/upgrade.rs @@ -0,0 +1,171 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::client::config::{Config, MISSING_VALUE}; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; +use std::fmt::Display; +use std::process; +use version_checker::{parse_version, Version}; + +fn print_start_upgrade(from: D1, to: D2) { + println!( + "\n==================\nTrying to upgrade mixnode from {} to {} ...", + from, to + ); +} + +fn print_failed_upgrade(from: D1, to: D2) { + eprintln!( + "Upgrade from {} to {} failed!\n==================\n", + from, to + ); +} + +fn print_successful_upgrade(from: D1, to: D2) { + println!( + "Upgrade from {} to {} was successful!\n==================\n", + from, to + ); +} + +fn pre_090_upgrade(from: &str, mut config: Config) -> Config { + // this is not extracted to separate function as you only have to manually pass version + // if upgrading from pre090 version + let from = match from.strip_prefix("v") { + Some(stripped) => stripped, + None => from, + }; + + let from = match from.strip_prefix("V") { + Some(stripped) => stripped, + None => from, + }; + + let from_version = parse_version(from).expect("invalid version provided!"); + if from_version.major == 0 && from_version.minor < 8 { + // technically this could be implemented, but is there any point in that? + eprintln!("upgrading node from before v0.8.0 is not supported. Please run `init` with new binary instead"); + process::exit(1) + } + + if (from_version.major == 0 && from_version.minor >= 9) || from_version.major >= 1 { + eprintln!("self reported version is higher than 0.9.0. Those releases should have already contained version numbers in config! Make sure you provided correct version"); + process::exit(1) + } + + // note: current is guaranteed to not have any `build` information suffix (nor pre-release + // information), as this was asserted at the beginning of this command) + // + // upgrade to current (if it's a 0.9.X) or try to upgrade to 0.9.0 as an intermediate + // step in future upgrades (so, for example, we might go 0.8.0 -> 0.9.0 -> 0.10.0) + // this way we don't need to have all the crazy paths on how to upgrade from any version to any + // other version. We just upgrade one minor version at a time. + let current = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + let to_version = if current.major == 0 && current.minor == 9 { + current + } else { + Version::new(0, 9, 0) + }; + + print_start_upgrade(&from_version, &to_version); + + config + .get_base_mut() + .set_custom_version(to_version.to_string().as_ref()); + + config.save_to_file(None).unwrap_or_else(|err| { + eprintln!("failed to overwrite config file! - {:?}", err); + print_failed_upgrade(&from_version, &to_version); + process::exit(1); + }); + + print_successful_upgrade(from_version, to_version); + + config +} + +pub fn command_args<'a, 'b>() -> App<'a, 'b> { + App::new("upgrade").about("Try to upgrade the mixnode") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the nym-client we want to upgrade") + .takes_value(true) + .required(true), + ) + // the rest of arguments depend on the upgrade path + .arg(Arg::with_name("current version") + .long("current-version") + .help("REQUIRED FOR PRE-0.9.0 UPGRADES. Self provided version of the nym-client if none is available in the config. NOTE: if provided incorrectly, results may be catastrophic.") + .takes_value(true) + ) +} + +pub fn execute(matches: &ArgMatches) { + let current = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + + // technically this is not a correct way of checking it as a released version might contain valid build identifiers + // however, we are not using them ourselves at the moment and hence it should be fine. + // if we change our mind, we could easily tweak this code + if current.is_prerelease() || !current.build.is_empty() { + eprintln!( + "Trying to upgrade to a non-released version {}. This is not supported!", + current + ); + process::exit(1) + } + + let id = matches.value_of("id").unwrap(); + + let mut existing_config = Config::load_from_file(None, Some(id)).unwrap_or_else(|err| { + eprintln!("failed to load existing config file! - {:?}", err); + process::exit(1) + }); + + // versions fields were added in 0.9.0 + if existing_config.get_base().get_version() == MISSING_VALUE { + let self_reported_version = matches.value_of("current version").unwrap_or_else(|| { + eprintln!( + "trying to upgrade from pre v0.9.0 without providing current system version!" + ); + process::exit(1) + }); + + // upgrades up to 0.9.0 + existing_config = pre_090_upgrade(self_reported_version, existing_config); + } + + let config_version = + Version::parse(existing_config.get_base().get_version()).unwrap_or_else(|err| { + eprintln!("failed to parse node version! - {:?}", err); + process::exit(1) + }); + + if config_version.is_prerelease() || !config_version.build.is_empty() { + eprintln!( + "Trying to upgrade to from non-released version {}. This is not supported!", + current + ); + process::exit(1) + } + + // here be upgrade path to 0.10.0 and beyond based on version number from config + if config_version == current { + println!("You're using the most recent version!"); + } else { + eprintln!("Cannot perform upgrade from {} to {}. Please let the developers know about this issue!", config_version, current); + process::exit(1) + } +} diff --git a/clients/native/src/main.rs b/clients/native/src/main.rs index 21f5cfc601..a59fd48239 100644 --- a/clients/native/src/main.rs +++ b/clients/native/src/main.rs @@ -29,6 +29,7 @@ fn main() { .about("Implementation of the Nym Client") .subcommand(commands::init::command_args()) .subcommand(commands::run::command_args()) + .subcommand(commands::upgrade::command_args()) .get_matches(); execute(arg_matches); @@ -38,6 +39,7 @@ fn execute(matches: ArgMatches) { match matches.subcommand() { ("init", Some(m)) => commands::init::execute(m), ("run", Some(m)) => commands::run::execute(m), + ("upgrade", Some(m)) => commands::upgrade::execute(m), _ => println!("{}", usage()), } } diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index ceed6abfce..e81ba3a875 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -33,6 +33,7 @@ ordered-buffer = {path = "../../common/socks5/ordered-buffer"} socks5-requests = { path = "../../common/socks5/requests" } topology = { path = "../../common/topology" } proxy-helpers = { path = "../../common/socks5/proxy-helpers" } +version-checker = { path = "../../common/version-checker" } [dev-dependencies] tempfile = "3.1.0" \ No newline at end of file diff --git a/clients/socks5/src/client/config/mod.rs b/clients/socks5/src/client/config/mod.rs index 6779ade230..d49b5b7d1b 100644 --- a/clients/socks5/src/client/config/mod.rs +++ b/clients/socks5/src/client/config/mod.rs @@ -14,6 +14,7 @@ use crate::client::config::template::config_template; use client_core::config::Config as BaseConfig; +pub use client_core::config::MISSING_VALUE; use config::NymConfig; use nymsphinx::addressing::clients::Recipient; use serde::{Deserialize, Serialize}; diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index dabb25bdf8..4e72c17f40 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -15,8 +15,9 @@ use crate::client::config::Config; use clap::ArgMatches; -pub mod init; -pub mod run; +pub(crate) mod init; +pub(crate) mod run; +pub(crate) mod upgrade; pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Config { if let Some(directory) = matches.value_of("directory") { diff --git a/clients/socks5/src/commands/upgrade.rs b/clients/socks5/src/commands/upgrade.rs new file mode 100644 index 0000000000..3e70b0b319 --- /dev/null +++ b/clients/socks5/src/commands/upgrade.rs @@ -0,0 +1,171 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::client::config::{Config, MISSING_VALUE}; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; +use std::fmt::Display; +use std::process; +use version_checker::{parse_version, Version}; + +fn print_start_upgrade(from: D1, to: D2) { + println!( + "\n==================\nTrying to upgrade mixnode from {} to {} ...", + from, to + ); +} + +fn print_failed_upgrade(from: D1, to: D2) { + eprintln!( + "Upgrade from {} to {} failed!\n==================\n", + from, to + ); +} + +fn print_successful_upgrade(from: D1, to: D2) { + println!( + "Upgrade from {} to {} was successful!\n==================\n", + from, to + ); +} + +fn pre_090_upgrade(from: &str, mut config: Config) -> Config { + // this is not extracted to separate function as you only have to manually pass version + // if upgrading from pre090 version + let from = match from.strip_prefix("v") { + Some(stripped) => stripped, + None => from, + }; + + let from = match from.strip_prefix("V") { + Some(stripped) => stripped, + None => from, + }; + + let from_version = parse_version(from).expect("invalid version provided!"); + if from_version.major == 0 && from_version.minor < 8 { + // technically this could be implemented, but is there any point in that? + eprintln!("upgrading node from before v0.8.0 is not supported. Please run `init` with new binary instead"); + process::exit(1) + } + + if (from_version.major == 0 && from_version.minor >= 9) || from_version.major >= 1 { + eprintln!("self reported version is higher than 0.9.0. Those releases should have already contained version numbers in config! Make sure you provided correct version"); + process::exit(1) + } + + // note: current is guaranteed to not have any `build` information suffix (nor pre-release + // information), as this was asserted at the beginning of this command) + // + // upgrade to current (if it's a 0.9.X) or try to upgrade to 0.9.0 as an intermediate + // step in future upgrades (so, for example, we might go 0.8.0 -> 0.9.0 -> 0.10.0) + // this way we don't need to have all the crazy paths on how to upgrade from any version to any + // other version. We just upgrade one minor version at a time. + let current = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + let to_version = if current.major == 0 && current.minor == 9 { + current + } else { + Version::new(0, 9, 0) + }; + + print_start_upgrade(&from_version, &to_version); + + config + .get_base_mut() + .set_custom_version(to_version.to_string().as_ref()); + + config.save_to_file(None).unwrap_or_else(|err| { + eprintln!("failed to overwrite config file! - {:?}", err); + print_failed_upgrade(&from_version, &to_version); + process::exit(1); + }); + + print_successful_upgrade(from_version, to_version); + + config +} + +pub fn command_args<'a, 'b>() -> App<'a, 'b> { + App::new("upgrade").about("Try to upgrade the mixnode") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the nym-socks5-client we want to upgrade") + .takes_value(true) + .required(true), + ) + // the rest of arguments depend on the upgrade path + .arg(Arg::with_name("current version") + .long("current-version") + .help("REQUIRED FOR PRE-0.9.0 UPGRADES. Self provided version of the nym-socks5-client if none is available in the config. NOTE: if provided incorrectly, results may be catastrophic.") + .takes_value(true) + ) +} + +pub fn execute(matches: &ArgMatches) { + let current = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + + // technically this is not a correct way of checking it as a released version might contain valid build identifiers + // however, we are not using them ourselves at the moment and hence it should be fine. + // if we change our mind, we could easily tweak this code + if current.is_prerelease() || !current.build.is_empty() { + eprintln!( + "Trying to upgrade to a non-released version {}. This is not supported!", + current + ); + process::exit(1) + } + + let id = matches.value_of("id").unwrap(); + + let mut existing_config = Config::load_from_file(None, Some(id)).unwrap_or_else(|err| { + eprintln!("failed to load existing config file! - {:?}", err); + process::exit(1) + }); + + // versions fields were added in 0.9.0 + if existing_config.get_base().get_version() == MISSING_VALUE { + let self_reported_version = matches.value_of("current version").unwrap_or_else(|| { + eprintln!( + "trying to upgrade from pre v0.9.0 without providing current system version!" + ); + process::exit(1) + }); + + // upgrades up to 0.9.0 + existing_config = pre_090_upgrade(self_reported_version, existing_config); + } + + let config_version = + Version::parse(existing_config.get_base().get_version()).unwrap_or_else(|err| { + eprintln!("failed to parse node version! - {:?}", err); + process::exit(1) + }); + + if config_version.is_prerelease() || !config_version.build.is_empty() { + eprintln!( + "Trying to upgrade to from non-released version {}. This is not supported!", + current + ); + process::exit(1) + } + + // here be upgrade path to 0.10.0 and beyond based on version number from config + if config_version == current { + println!("You're using the most recent version!"); + } else { + eprintln!("Cannot perform upgrade from {} to {}. Please let the developers know about this issue!", config_version, current); + process::exit(1) + } +} diff --git a/clients/socks5/src/main.rs b/clients/socks5/src/main.rs index 76bc756136..a04f8ec90d 100644 --- a/clients/socks5/src/main.rs +++ b/clients/socks5/src/main.rs @@ -29,6 +29,7 @@ fn main() { .about("A Socks5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address") .subcommand(commands::init::command_args()) .subcommand(commands::run::command_args()) + .subcommand(commands::upgrade::command_args()) .get_matches(); execute(arg_matches); @@ -38,6 +39,7 @@ fn execute(matches: ArgMatches) { match matches.subcommand() { ("init", Some(m)) => commands::init::execute(m), ("run", Some(m)) => commands::run::execute(m), + ("upgrade", Some(m)) => commands::upgrade::execute(m), _ => println!("{}", usage()), } } diff --git a/common/version-checker/src/lib.rs b/common/version-checker/src/lib.rs index 3503c89a7b..46aafc2c80 100644 --- a/common/version-checker/src/lib.rs +++ b/common/version-checker/src/lib.rs @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use semver::Version; +use semver::SemVerError; +pub use semver::Version; /// Checks whether given `version` is compatible with a given semantic version requirement `req` /// according to major-minor semver rules. The semantic version requirement can be passed as a full, @@ -32,6 +33,10 @@ pub fn is_minor_version_compatible(version: &str, req: &str) -> bool { expected_version.major == req_version.major && expected_version.minor == req_version.minor } +pub fn parse_version(raw_version: &str) -> Result { + Version::parse(raw_version) +} + #[cfg(test)] mod tests { use super::*; diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 1c58b7c036..ea4a9ea0c7 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -31,6 +31,7 @@ mixnet-client = { path = "../common/client-libs/mixnet-client" } mixnode-common = { path = "../common/mixnode-common" } nymsphinx = { path = "../common/nymsphinx" } pemstore = { path = "../common/pemstore" } +version-checker = { path = "../common/version-checker" } [dependencies.tungstenite] version = "0.11" diff --git a/gateway/src/commands/mod.rs b/gateway/src/commands/mod.rs index 4a8c3eeff8..2fd55cffbb 100644 --- a/gateway/src/commands/mod.rs +++ b/gateway/src/commands/mod.rs @@ -15,8 +15,9 @@ use crate::config::Config; use clap::ArgMatches; -pub mod init; -pub mod run; +pub(crate) mod init; +pub(crate) mod run; +pub(crate) mod upgrade; pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Config { let mut was_mix_host_overridden = false; diff --git a/gateway/src/commands/upgrade.rs b/gateway/src/commands/upgrade.rs new file mode 100644 index 0000000000..8085a7c8e3 --- /dev/null +++ b/gateway/src/commands/upgrade.rs @@ -0,0 +1,170 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::config::{Config, MISSING_VALUE}; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; +use std::fmt::Display; +use std::process; +use version_checker::{parse_version, Version}; + +fn print_start_upgrade(from: D1, to: D2) { + println!( + "\n==================\nTrying to upgrade gateway from {} to {} ...", + from, to + ); +} + +fn print_failed_upgrade(from: D1, to: D2) { + eprintln!( + "Upgrade from {} to {} failed!\n==================\n", + from, to + ); +} + +fn print_successful_upgrade(from: D1, to: D2) { + println!( + "Upgrade from {} to {} was successful!\n==================\n", + from, to + ); +} + +fn pre_090_upgrade(from: &str, config: Config) -> Config { + // this is not extracted to separate function as you only have to manually pass version + // if upgrading from pre090 version + let from = match from.strip_prefix("v") { + Some(stripped) => stripped, + None => from, + }; + + let from = match from.strip_prefix("V") { + Some(stripped) => stripped, + None => from, + }; + + let from_version = parse_version(from).expect("invalid version provided!"); + if from_version.major == 0 && from_version.minor < 8 { + // technically this could be implemented, but is there any point in that? + eprintln!("upgrading node from before v0.8.0 is not supported. Please run `init` with new binary instead"); + process::exit(1) + } + + if (from_version.major == 0 && from_version.minor >= 9) || from_version.major >= 1 { + eprintln!("self reported version is higher than 0.9.0. Those releases should have already contained version numbers in config! Make sure you provided correct version"); + process::exit(1) + } + + // note: current is guaranteed to not have any `build` information suffix (nor pre-release + // information), as this was asserted at the beginning of this command) + // + // upgrade to current (if it's a 0.9.X) or try to upgrade to 0.9.0 as an intermediate + // step in future upgrades (so, for example, we might go 0.8.0 -> 0.9.0 -> 0.10.0) + // this way we don't need to have all the crazy paths on how to upgrade from any version to any + // other version. We just upgrade one minor version at a time. + let current = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + let to_version = if current.major == 0 && current.minor == 9 { + current + } else { + Version::new(0, 9, 0) + }; + + print_start_upgrade(&from_version, &to_version); + + let upgraded_config = config.with_custom_version(to_version.to_string().as_ref()); + // TODO: THIS IS INCOMPLETE AS ONCE PRESENCE IS REMOVED IN 0.9.0 IT WILL ALSO NEED + // TO BE PURGED FROM CONFIG + + upgraded_config.save_to_file(None).unwrap_or_else(|err| { + eprintln!("failed to overwrite config file! - {:?}", err); + print_failed_upgrade(&from_version, &to_version); + process::exit(1); + }); + + print_successful_upgrade(from_version, to_version); + + upgraded_config +} + +pub fn command_args<'a, 'b>() -> App<'a, 'b> { + App::new("upgrade").about("Try to upgrade the gateway") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the nym-gateway we want to upgrade") + .takes_value(true) + .required(true), + ) + // the rest of arguments depend on the upgrade path + .arg(Arg::with_name("current version") + .long("current-version") + .help("REQUIRED FOR PRE-0.9.0 UPGRADES. Self provided version of the nym-gateway if none is available in the config. NOTE: if provided incorrectly, results may be catastrophic.") + .takes_value(true) + ) +} + +pub fn execute(matches: &ArgMatches) { + let current = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + + // technically this is not a correct way of checking it as a released version might contain valid build identifiers + // however, we are not using them ourselves at the moment and hence it should be fine. + // if we change our mind, we could easily tweak this code + if current.is_prerelease() || !current.build.is_empty() { + eprintln!( + "Trying to upgrade to a non-released version {}. This is not supported!", + current + ); + process::exit(1) + } + + let id = matches.value_of("id").unwrap(); + + let mut existing_config = Config::load_from_file(None, Some(id)).unwrap_or_else(|err| { + eprintln!("failed to load existing config file! - {:?}", err); + process::exit(1) + }); + + // versions fields were added in 0.9.0 + if existing_config.get_version() == MISSING_VALUE { + let self_reported_version = matches.value_of("current version").unwrap_or_else(|| { + eprintln!( + "trying to upgrade from pre v0.9.0 without providing current system version!" + ); + process::exit(1) + }); + + // upgrades up to 0.9.0 + existing_config = pre_090_upgrade(self_reported_version, existing_config); + } + + let config_version = Version::parse(existing_config.get_version()).unwrap_or_else(|err| { + eprintln!("failed to parse node version! - {:?}", err); + process::exit(1) + }); + + if config_version.is_prerelease() || !config_version.build.is_empty() { + eprintln!( + "Trying to upgrade to from non-released version {}. This is not supported!", + current + ); + process::exit(1) + } + + // here be upgrade path to 0.10.0 and beyond based on version number from config + if config_version == current { + println!("You're using the most recent version!"); + } else { + eprintln!("Cannot perform upgrade from {} to {}. Please let the developers know about this issue!", config_version, current); + process::exit(1) + } +} diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index 276c5ae6df..cd59b82aa8 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -24,6 +24,8 @@ use std::time; pub mod persistence; mod template; +pub(crate) const MISSING_VALUE: &str = "MISSING VALUE"; + // 'GATEWAY' const DEFAULT_MIX_LISTENING_PORT: u16 = 1789; const DEFAULT_CLIENT_LISTENING_PORT: u16 = 9000; @@ -90,6 +92,10 @@ impl NymConfig for Config { } } +pub fn missing_string_value() -> String { + MISSING_VALUE.to_string() +} + impl Config { pub fn new>(id: S) -> Self { Config::default().with_id(id) @@ -322,6 +328,11 @@ impl Config { self } + pub fn with_custom_version(mut self, version: &str) -> Self { + self.gateway.version = version.to_string(); + self + } + // getters pub fn get_config_file_save_location(&self) -> PathBuf { self.config_directory().join(Self::config_file_name()) @@ -402,12 +413,17 @@ impl Config { pub fn get_cache_entry_ttl(&self) -> time::Duration { time::Duration::from_millis(self.debug.cache_entry_ttl) } + + pub fn get_version(&self) -> &str { + &self.gateway.version + } } #[derive(Debug, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Gateway { /// Version of the gateway for which this configuration was created. + #[serde(default = "missing_string_value")] version: String, /// ID specifies the human readable ID of this particular gateway. diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 3adbae7295..e9bdf86443 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -29,6 +29,7 @@ fn main() { .about("Implementation of the Nym Mixnet Gateway") .subcommand(commands::init::command_args()) .subcommand(commands::run::command_args()) + .subcommand(commands::upgrade::command_args()) .get_matches(); execute(arg_matches); @@ -38,6 +39,7 @@ fn execute(matches: ArgMatches) { match matches.subcommand() { ("init", Some(m)) => commands::init::execute(m), ("run", Some(m)) => commands::run::execute(m), + ("upgrade", Some(m)) => commands::upgrade::execute(m), _ => println!("{}", usage()), } } diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index 13bae1ad74..d9e4c5bae9 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -28,6 +28,7 @@ mixnode-common = { path = "../common/mixnode-common" } nymsphinx = {path = "../common/nymsphinx" } pemstore = {path = "../common/pemstore"} topology = {path = "../common/topology"} +version-checker = { path = "../common/version-checker" } [dev-dependencies] tempfile = "3.1.0" \ No newline at end of file diff --git a/mixnode/src/commands/mod.rs b/mixnode/src/commands/mod.rs index d22c1215e1..ef5ec82a8e 100644 --- a/mixnode/src/commands/mod.rs +++ b/mixnode/src/commands/mod.rs @@ -16,8 +16,9 @@ use crate::config::Config; use clap::ArgMatches; use nymsphinx::params::DEFAULT_NUM_MIX_HOPS; -pub mod init; -pub mod run; +pub(crate) mod init; +pub(crate) mod run; +pub(crate) mod upgrade; pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Config { let max_layer = DEFAULT_NUM_MIX_HOPS; diff --git a/mixnode/src/commands/upgrade.rs b/mixnode/src/commands/upgrade.rs new file mode 100644 index 0000000000..b074d2f1ba --- /dev/null +++ b/mixnode/src/commands/upgrade.rs @@ -0,0 +1,170 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::config::{Config, MISSING_VALUE}; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; +use std::fmt::Display; +use std::process; +use version_checker::{parse_version, Version}; + +fn print_start_upgrade(from: D1, to: D2) { + println!( + "\n==================\nTrying to upgrade mixnode from {} to {} ...", + from, to + ); +} + +fn print_failed_upgrade(from: D1, to: D2) { + eprintln!( + "Upgrade from {} to {} failed!\n==================\n", + from, to + ); +} + +fn print_successful_upgrade(from: D1, to: D2) { + println!( + "Upgrade from {} to {} was successful!\n==================\n", + from, to + ); +} + +fn pre_090_upgrade(from: &str, config: Config) -> Config { + // this is not extracted to separate function as you only have to manually pass version + // if upgrading from pre090 version + let from = match from.strip_prefix("v") { + Some(stripped) => stripped, + None => from, + }; + + let from = match from.strip_prefix("V") { + Some(stripped) => stripped, + None => from, + }; + + let from_version = parse_version(from).expect("invalid version provided!"); + if from_version.major == 0 && from_version.minor < 8 { + // technically this could be implemented, but is there any point in that? + eprintln!("upgrading node from before v0.8.0 is not supported. Please run `init` with new binary instead"); + process::exit(1) + } + + if (from_version.major == 0 && from_version.minor >= 9) || from_version.major >= 1 { + eprintln!("self reported version is higher than 0.9.0. Those releases should have already contained version numbers in config! Make sure you provided correct version"); + process::exit(1) + } + + // note: current is guaranteed to not have any `build` information suffix (nor pre-release + // information), as this was asserted at the beginning of this command) + // + // upgrade to current (if it's a 0.9.X) or try to upgrade to 0.9.0 as an intermediate + // step in future upgrades (so, for example, we might go 0.8.0 -> 0.9.0 -> 0.10.0) + // this way we don't need to have all the crazy paths on how to upgrade from any version to any + // other version. We just upgrade one minor version at a time. + let current = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + let to_version = if current.major == 0 && current.minor == 9 { + current + } else { + Version::new(0, 9, 0) + }; + + print_start_upgrade(&from_version, &to_version); + + let upgraded_config = config.with_custom_version(to_version.to_string().as_ref()); + // TODO: THIS IS INCOMPLETE AS ONCE PRESENCE IS REMOVED IN 0.9.0 IT WILL ALSO NEED + // TO BE PURGED FROM CONFIG + + upgraded_config.save_to_file(None).unwrap_or_else(|err| { + eprintln!("failed to overwrite config file! - {:?}", err); + print_failed_upgrade(&from_version, &to_version); + process::exit(1); + }); + + print_successful_upgrade(from_version, to_version); + + upgraded_config +} + +pub fn command_args<'a, 'b>() -> App<'a, 'b> { + App::new("upgrade").about("Try to upgrade the mixnode") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the nym-mixnode we want to upgrade") + .takes_value(true) + .required(true), + ) + // the rest of arguments depend on the upgrade path + .arg(Arg::with_name("current version") + .long("current-version") + .help("REQUIRED FOR PRE-0.9.0 UPGRADES. Self provided version of the nym-mixnode if none is available in the config. NOTE: if provided incorrectly, results may be catastrophic.") + .takes_value(true) + ) +} + +pub fn execute(matches: &ArgMatches) { + let current = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + + // technically this is not a correct way of checking it as a released version might contain valid build identifiers + // however, we are not using them ourselves at the moment and hence it should be fine. + // if we change our mind, we could easily tweak this code + if current.is_prerelease() || !current.build.is_empty() { + eprintln!( + "Trying to upgrade to a non-released version {}. This is not supported!", + current + ); + process::exit(1) + } + + let id = matches.value_of("id").unwrap(); + + let mut existing_config = Config::load_from_file(None, Some(id)).unwrap_or_else(|err| { + eprintln!("failed to load existing config file! - {:?}", err); + process::exit(1) + }); + + // versions fields were added in 0.9.0 + if existing_config.get_version() == MISSING_VALUE { + let self_reported_version = matches.value_of("current version").unwrap_or_else(|| { + eprintln!( + "trying to upgrade from pre v0.9.0 without providing current system version!" + ); + process::exit(1) + }); + + // upgrades up to 0.9.0 + existing_config = pre_090_upgrade(self_reported_version, existing_config); + } + + let config_version = Version::parse(existing_config.get_version()).unwrap_or_else(|err| { + eprintln!("failed to parse node version! - {:?}", err); + process::exit(1) + }); + + if config_version.is_prerelease() || !config_version.build.is_empty() { + eprintln!( + "Trying to upgrade to from non-released version {}. This is not supported!", + current + ); + process::exit(1) + } + + // here be upgrade path to 0.10.0 and beyond based on version number from config + if config_version == current { + println!("You're using the most recent version!"); + } else { + eprintln!("Cannot perform upgrade from {} to {}. Please let the developers know about this issue!", config_version, current); + process::exit(1) + } +} diff --git a/mixnode/src/config/mod.rs b/mixnode/src/config/mod.rs index 0b088507e4..925d9b4c01 100644 --- a/mixnode/src/config/mod.rs +++ b/mixnode/src/config/mod.rs @@ -24,6 +24,8 @@ use std::time; pub mod persistence; mod template; +pub(crate) const MISSING_VALUE: &str = "MISSING VALUE"; + // 'MIXNODE' const DEFAULT_LISTENING_PORT: u16 = 1789; const DEFAULT_DIRECTORY_SERVER: &str = "https://directory.nymtech.net"; @@ -84,6 +86,10 @@ impl NymConfig for Config { } } +pub fn missing_string_value() -> String { + MISSING_VALUE.to_string() +} + impl Config { pub fn new>(id: S) -> Self { Config::default().with_id(id) @@ -198,6 +204,11 @@ impl Config { self } + pub fn with_custom_version(mut self, version: &str) -> Self { + self.mixnode.version = version.to_string(); + self + } + // getters pub fn get_config_file_save_location(&self) -> PathBuf { self.config_directory().join(Self::config_file_name()) @@ -262,12 +273,17 @@ impl Config { pub fn get_cache_entry_ttl(&self) -> time::Duration { time::Duration::from_millis(self.debug.cache_entry_ttl) } + + pub fn get_version(&self) -> &str { + &self.mixnode.version + } } #[derive(Debug, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct MixNode { /// Version of the mixnode for which this configuration was created. + #[serde(default = "missing_string_value")] version: String, /// ID specifies the human readable ID of this particular mixnode. diff --git a/mixnode/src/main.rs b/mixnode/src/main.rs index 7b5dc96631..31373f6fe7 100644 --- a/mixnode/src/main.rs +++ b/mixnode/src/main.rs @@ -29,6 +29,7 @@ fn main() { .about("Implementation of the Loopix-based Mixnode") .subcommand(commands::init::command_args()) .subcommand(commands::run::command_args()) + .subcommand(commands::upgrade::command_args()) .get_matches(); execute(arg_matches); @@ -38,6 +39,7 @@ fn execute(matches: ArgMatches) { match matches.subcommand() { ("init", Some(m)) => commands::init::execute(m), ("run", Some(m)) => commands::run::execute(m), + ("upgrade", Some(m)) => commands::upgrade::execute(m), _ => println!("{}", usage()), } }