diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2d23756..07e744d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -77,7 +77,7 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Install cargo-nextest - run: cargo install cargo-nextest + run: cargo install cargo-nextest --locked - name: Cache uses: actions/cache@v4 @@ -94,4 +94,4 @@ jobs: run: cargo build --release - name: Run cargo test regular features - run: cargo nextest run --release + run: cargo nextest run --release --no-tests=warn diff --git a/Cargo.lock b/Cargo.lock index 5bbecb9..29192c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr" @@ -1465,7 +1465,7 @@ dependencies = [ [[package]] name = "khost" -version = "0.4.0" +version = "0.5.0" dependencies = [ "addr", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 555cf70..cdd923d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "khost" -version = "0.4.0" +version = "0.5.0" edition = "2021" authors = ["Kaspa developers"] license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index ee503cd..52e0527 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Kaspa p2p node deployment automation tool for Linux. -kHOST was created to automate deployment of Kaspa nodes intended for use as a part of the Kaspa public RPC network as well as private network high-availability clusters. kHOST deploys Rusty-Kaspa nodes from sources, configures them to run as a `systemd` service as well as configures NGINX to act as a reverse proxy for the RPC. This tool exists to simplify and automate Kaspa node deployment as well as to standardize related system configuration. +kHOST was created to automate deployment of Kaspa nodes intended for use as a part of the Kaspa public RPC network as well as private network high-availability clusters. kHOST deploys Rusty-Kaspa nodes from sources, configures them to run as a `systemd` service as well as configures NGINX to act as a reverse proxy for the RPC. This tool exists to simplify and automate Kaspa node deployment as well as to standardize related system configuration. ## Deploying @@ -33,3 +33,7 @@ If you already have an existing user and rust installed, you can simply run `car Please note that the user needs to have root (sudo) privileges to run khost. IMPORTANT: This tool creates it's own configuration for the kaspad node, as such, any previous configurations should be disabled and removed. If kaspad was running before under the same username, the `~/.rusty-kaspa` data folders containing databases will be re-used. + +## Kaspad Default Origins + +They are set to `aspectron/*` origins, manually updated from `upstream`. This is a design choice that allows high velocity update coordination and controlled versioning, in practice, it should follow upstream release cycle. diff --git a/src/actions/advanced.rs b/src/actions/advanced.rs index 2e3fb40..40aafe0 100644 --- a/src/actions/advanced.rs +++ b/src/actions/advanced.rs @@ -84,7 +84,12 @@ impl Action for Advanced { match selector.interact() { Ok(BranchChange::Kaspad) => { let origin = git::create_origin("rusty-kaspa")?; - for config in ctx.config.kaspad.iter_mut() { + for config in ctx + .config + .kaspad + .iter_mut() + .filter(|config| config.network().is_supported()) + { config.set_origin(origin.clone()); } ctx.config.save()?; diff --git a/src/actions/bootstrap.rs b/src/actions/bootstrap.rs index d2408eb..178dec4 100644 --- a/src/actions/bootstrap.rs +++ b/src/actions/bootstrap.rs @@ -24,7 +24,7 @@ impl Action for Bootstrap { resolver::init_resolver_config(ctx).ok(); } - kaspad::select_networks(ctx)?; + kaspad::select_supported_networks(ctx)?; base::install(ctx, false)?; ctx.config.bootstrap = true; diff --git a/src/config.rs b/src/config.rs index 829eef5..91f95a1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use crate::imports::*; -const CONFIG_VERSION: u64 = 2; +const CONFIG_VERSION: u64 = 3; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { @@ -23,9 +23,18 @@ impl Config { .with_stats() .with_local_interface(8989); - let origin = Origin::try_new("https://github.com/aspectron/rusty-kaspa", Some("pnn-v1"))?; - let kaspad = Network::into_iter() - .map(|network| kaspad::Config::new(origin.clone(), network)) + let pnnv1_origin = Self::pnnv1_origin()?; + let tn12_origin = Self::pnnv1tn12_origin()?; + + let kaspad = SupportedNetwork::iter() + .copied() + .map(|network| { + let selected_origin = match network { + SupportedNetwork::Mainnet | SupportedNetwork::Testnet10 => pnnv1_origin.clone(), + SupportedNetwork::Testnet12 => tn12_origin.clone(), + }; + kaspad::Config::new(selected_origin, network.into()) + }) .collect::>(); let nginx = nginx::Config::default(); @@ -42,6 +51,17 @@ impl Config { resolver, }) } + + pub fn pnnv1_origin() -> Result { + Origin::try_new("https://github.com/aspectron/rusty-kaspa", Some("pnn-v1")) + } + + pub fn pnnv1tn12_origin() -> Result { + Origin::try_new( + "https://github.com/aspectron/rusty-kaspa", + Some("pnn-v1-tn12"), + ) + } } impl Config { @@ -51,26 +71,64 @@ impl Config { return Err(Error::custom("Config file not found")); } let mut config: Config = serde_json::from_str(&fs::read_to_string(config_path)?)?; + let last_version = config.version; let mut update = false; - // Migrate old config - if config.version == 1 { - config.kaspad.iter_mut().for_each(|config| { - if let Some(branch) = config.origin_mut().branch_mut() { - if branch == "omega" { - *branch = "pnn-v1".to_string(); - update = true; + // v1 -> v2 + if last_version <= 1 { + config.kaspad.iter_mut().for_each(|kaspad_config| { + if kaspad_config.network().is_supported() { + if let Some(branch) = kaspad_config.origin_mut().branch_mut() { + if branch == "omega" { + *branch = "pnn-v1".to_string(); + update = true; + } } } }); } + // v2 -> v3 + if last_version <= 2 { + // push tn12 + if !config + .kaspad + .iter() + .any(|config| config.network() == SupportedNetwork::Testnet12.into()) + { + config.kaspad.push(kaspad::Config::new( + Self::pnnv1tn12_origin()?, + SupportedNetwork::Testnet12.into(), + )); + update = true; + } + + // update rk origin + for kaspad_config in config.kaspad.iter_mut().filter(|kaspad_config| { + kaspad_config.is_supported_network() + && matches!( + kaspad_config.network(), + Network::Supported(SupportedNetwork::Testnet10 | SupportedNetwork::Mainnet) + ) + }) { + *kaspad_config.origin_mut() = Self::pnnv1_origin()?; + update = true; + } + } + + // keep until v3 is current, gate it to <= 3 on v4 + update |= config.remove_unused_legacy_network(DeprecatedNetwork::Testnet11); + + if config.version < CONFIG_VERSION { + config.version = CONFIG_VERSION; + update = true; + } + if update { log::success(format!( "Updated kHOST config to version {}", CONFIG_VERSION ))?; - config.version = CONFIG_VERSION; config.save()?; } @@ -83,6 +141,18 @@ impl Config { Ok(()) } + /// considered unused if no systemd service and no active in cfg + fn remove_unused_legacy_network(&mut self, network: DeprecatedNetwork) -> bool { + let network = Network::from(network); + let kaspad_len = self.kaspad.len(); + self.kaspad.retain(|kaspad_config| { + kaspad_config.network() != network + || kaspad_config.is_enabled() + || systemd::is_active(kaspad_config.service_name()).unwrap_or(true) + }); + self.kaspad.len() != kaspad_len + } + pub fn reset() { let path = data_folder().join("config.json"); if let Err(err) = fs::remove_file(&path) { diff --git a/src/git.rs b/src/git.rs index 1f909d5..4fcb487 100644 --- a/src/git.rs +++ b/src/git.rs @@ -180,12 +180,14 @@ where enum Preset { PNNv1, // Delta, + Tn12, Custom, } let preset = if name == "rusty-kaspa" { cliclack::select(format!("Select git origin for '{name}':")) .item(Preset::PNNv1, "pnn-v1 (aspectron/pnn-v1)", "") + .item(Preset::Tn12, "tn12 (kaspanet/tn12)", "") // .item( // Preset::Delta, // "Delta", @@ -198,9 +200,8 @@ where }; let origin = match preset { - Preset::PNNv1 => { - Origin::try_new("https://github.com/aspectron/rusty-kaspa", Some("pnn-v1"))? - } + Preset::PNNv1 => Config::pnnv1_origin()?, + Preset::Tn12 => Config::pnnv1tn12_origin()?, // Preset::Delta => { // Origin::try_new("https://github.com/aspectron/rusty-kaspa", Some("delta"))? // } diff --git a/src/imports.rs b/src/imports.rs index 2d89f71..9cf17b2 100644 --- a/src/imports.rs +++ b/src/imports.rs @@ -37,7 +37,7 @@ pub use crate::fqdn; pub use crate::git::{self, Origin}; pub use crate::kaspad; pub use crate::khost; -pub use crate::network::{Interface, Network}; +pub use crate::network::{DeprecatedNetwork, Interface, Network, SupportedNetwork}; pub use crate::nginx; pub use crate::nginx::ProxyConfig; pub use crate::resolver; diff --git a/src/kaspad.rs b/src/kaspad.rs index 2426603..a27793c 100644 --- a/src/kaspad.rs +++ b/src/kaspad.rs @@ -1,4 +1,7 @@ use crate::imports::*; +use crate::network::DeprecatedNetwork::*; +use crate::network::Network::*; +use crate::network::SupportedNetwork::*; use nginx::prelude::*; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -44,10 +47,14 @@ impl Service for Config { } fn managed(&self) -> bool { - true + self.is_supported_network() || self.is_enabled() } fn proxy_config(&self, _ctx: &Context) -> Option> { + if self.is_deprecated_network() { + return None; + } + let mut proxy_configs = Vec::new(); if let Some(iface) = self.wrpc_borsh.as_ref() { @@ -79,9 +86,10 @@ impl Service for Config { impl Config { pub fn new(origin: Origin, network: Network) -> Self { let (grpc, wrpc_borsh, wrpc_json) = match network { - Network::Mainnet => (16110, 17110, 18110), - Network::Testnet10 => (16210, 17210, 18210), - Network::Testnet11 => (16310, 17310, 18310), + Supported(Mainnet) => (16110, 17110, 18110), + Supported(Testnet10) => (16210, 17210, 18210), + Supported(Testnet12) => (16410, 17410, 18410), + Deprecated(Testnet11) => (16310, 17310, 18310), }; Self { @@ -114,6 +122,14 @@ impl Config { self.network } + pub fn is_supported_network(&self) -> bool { + self.network.is_supported() + } + + pub fn is_deprecated_network(&self) -> bool { + self.network.is_deprecated() + } + pub fn enable(&mut self) { self.enabled = true; } @@ -136,14 +152,18 @@ impl From<&Config> for Vec { let mut args = Arglist::default(); match config.network { - Network::Mainnet => { + Supported(Mainnet) => { // args.push("--connect=38.242.201.109"); } - Network::Testnet10 => { + Supported(Testnet10) => { args.push("--testnet"); args.push("--netsuffix=10"); } - Network::Testnet11 => { + Supported(Testnet12) => { + args.push("--testnet"); + args.push("--netsuffix=12"); + } + Deprecated(Testnet11) => { args.push("--testnet"); args.push("--netsuffix=11"); } @@ -191,9 +211,7 @@ impl From<&Config> for Vec { } pub fn unique_origins(ctx: &Context) -> HashSet { - ctx.config - .kaspad - .iter() + supported_configs(ctx) .map(|config| config.origin.clone()) .collect() } @@ -205,11 +223,49 @@ pub fn active_configs(ctx: &Context) -> impl Iterator { .filter(|config| config.is_enabled()) } -pub fn inactive_configs(ctx: &Context) -> impl Iterator { +pub fn supported_configs(ctx: &Context) -> impl Iterator { ctx.config .kaspad .iter() - .filter(|config| !config.is_enabled()) + .filter(|config| config.is_supported_network()) +} + +pub fn supported_configs_mut(ctx: &mut Context) -> impl Iterator { + ctx.config + .kaspad + .iter_mut() + .filter(|config| config.is_supported_network()) +} + +pub fn active_supported_configs(ctx: &Context) -> impl Iterator { + supported_configs(ctx).filter(|config| config.is_enabled()) +} + +pub fn inactive_supported_configs(ctx: &Context) -> impl Iterator { + supported_configs(ctx).filter(|config| !config.is_enabled()) +} + +pub fn legacy_network_service_exists() -> bool { + Network::deprecated().any(|network| { + let service_name = format!("kaspa-{network}"); + systemd::service_path(&service_name).exists() + }) +} + +pub fn has_legacy_network(ctx: &Context) -> bool { + ctx.config + .kaspad + .iter() + .any(|config| config.is_deprecated_network()) + || legacy_network_service_exists() +} + +pub fn warn_legacy_network(ctx: &Context) -> Result<()> { + if has_legacy_network(ctx) { + log::warning("testnet-11 is deprecated and no longer supported for new kHOST deployments. If kaspa-testnet-11 is installed or running, uninstall it and install/enable testnet-12 instead. testnet-11 compatibility is kept only to load legacy configs and will be removed in a future version.")?; + } + + Ok(()) } pub fn fetch(ctx: &Context) -> Result<()> { @@ -245,7 +301,7 @@ pub fn update(ctx: &Context) -> Result<()> { fetch(ctx)?; build(ctx)?; step("Restarting Kaspa p2p nodes...", || { - for config in active_configs(ctx) { + for config in active_supported_configs(ctx) { systemd::restart(config)?; } Ok(()) @@ -363,7 +419,7 @@ pub fn version(origin: &Origin) -> Option { .and_then(|s| { s.trim() .split(' ') - .last() + .next_back() .map(|version| format!("{version}-{hash}")) }) } @@ -396,10 +452,15 @@ pub fn supports_multiple_networks(ctx: &Context, networks: usize) -> bool { } pub fn configure_networks(ctx: &mut Context, networks: Vec) -> Result<()> { - let networks = networks.into_iter().collect::>(); + let selected_networks = networks.into_iter().collect::>(); + let supported_networks = selected_networks + .iter() + .copied() + .filter(|network| network.is_supported()) + .collect::>(); let limits = [(3, 42), (2, 32)].iter(); for (nodes, limit) in limits { - if networks.len() >= *nodes && ctx.system.ram_as_gb() <= (*limit - 2) { + if supported_networks.len() >= *nodes && ctx.system.ram_as_gb() <= (*limit - 2) { log::error(format!( "Detected RAM is {}, minimum required for {} networks is {} Gb. Aborting...", as_gb(ctx.system.total_memory as f64, false, false), @@ -411,7 +472,11 @@ pub fn configure_networks(ctx: &mut Context, networks: Vec) -> Result<( } for config in ctx.config.kaspad.iter_mut() { - config.enabled = networks.contains(&config.network); + if config.is_supported_network() { + config.enabled = supported_networks.contains(&config.network); + } else if config.is_enabled() { + config.enabled = selected_networks.contains(&config.network); + } } ctx.config.save()?; @@ -425,7 +490,12 @@ pub fn reconfigure(ctx: &Context, force: bool) -> Result<()> { log::remark("Updating Kaspa p2p node configuration...")?; - for config in inactive_configs(ctx) { + for config in ctx + .config + .kaspad + .iter() + .filter(|config| !config.is_enabled()) + { let service_name = config.service_name(); if systemd::exists(config) { if systemd::is_active(config.service_name())? { @@ -442,7 +512,7 @@ pub fn reconfigure(ctx: &Context, force: bool) -> Result<()> { } } - for config in active_configs(ctx) { + for config in active_supported_configs(ctx) { let service_name = config.service_name(); step(format!("Configuring '{}'", service_name), || { if force || !systemd::exists(config) { @@ -456,7 +526,7 @@ pub fn reconfigure(ctx: &Context, force: bool) -> Result<()> { if reconfigure_systemd { step("Reloading systemd daemon...", systemd::daemon_reload)?; - for config in active_configs(ctx) { + for config in active_supported_configs(ctx) { let service_name = config.service_name(); step(format!("Brining up '{}'", service_name), || { systemd::enable(config)?; @@ -470,20 +540,6 @@ pub fn reconfigure(ctx: &Context, force: bool) -> Result<()> { Ok(()) } -pub fn stop_all(ctx: &Context) -> Result<()> { - for config in active_configs(ctx) { - systemd::stop(config)?; - } - Ok(()) -} - -pub fn start_all(ctx: &Context) -> Result<()> { - for config in active_configs(ctx) { - systemd::start(config)?; - } - Ok(()) -} - pub fn restart_all(ctx: &Context) -> Result<()> { for config in active_configs(ctx) { step( @@ -577,7 +633,7 @@ pub fn find_config_by_service_detail<'a>( .find(|config| config.service_name() == detail.name) } -pub fn select_networks(ctx: &mut Context) -> Result<()> { +pub fn select_supported_networks(ctx: &mut Context) -> Result<()> { if ctx.system.ram_as_gb() < 24 { log::warning(format!( "Detected RAM is {}, minimum required for multiple networks is 32 Gb.", @@ -585,13 +641,12 @@ pub fn select_networks(ctx: &mut Context) -> Result<()> { ))?; let mut selector = cliclack::select("Select Kaspa p2p node network to enable"); - let details = ctx - .config - .kaspad - .iter() + let details = supported_configs(ctx) .map(Service::service_detail) .collect::>(); - let selected = active_configs(ctx).next().map(Service::service_detail); + let selected = active_supported_configs(ctx) + .next() + .map(Service::service_detail); if let Some(selected) = selected { selector = selector.initial_value(selected); } @@ -599,15 +654,12 @@ pub fn select_networks(ctx: &mut Context) -> Result<()> { selector = selector.item(detail.clone(), detail, ""); } let selected = selector.interact()?; - ctx.config.kaspad.iter_mut().for_each(Config::disable); + supported_configs_mut(ctx).for_each(Config::disable); find_config_by_service_detail(ctx, &selected) .unwrap() .enable(); } else { - let details = ctx - .config - .kaspad - .iter() + let details = supported_configs(ctx) .map(Service::service_detail) .collect::>(); let enabled = details @@ -635,7 +687,7 @@ pub fn select_networks(ctx: &mut Context) -> Result<()> { } } - ctx.config.kaspad.iter_mut().for_each(Config::disable); + supported_configs_mut(ctx).for_each(Config::disable); for detail in selected.iter() { find_config_by_service_detail(ctx, detail).unwrap().enable(); } diff --git a/src/main.rs b/src/main.rs index b7024ca..432424b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,6 +62,8 @@ fn main() { sudo::init(&mut ctx); + kaspad::warn_legacy_network(&ctx).ok(); + let first_run = !ctx.config.bootstrap; let status = status::detect(&ctx); diff --git a/src/network.rs b/src/network.rs index 0f29af8..156d274 100644 --- a/src/network.rs +++ b/src/network.rs @@ -28,19 +28,85 @@ impl Interface { #[derive(Default, Describe, Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(rename_all = "lowercase")] -pub enum Network { +pub enum SupportedNetwork { #[default] Mainnet, Testnet10, + Testnet12, +} + +#[derive(Describe, Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(rename_all = "lowercase")] +pub enum DeprecatedNetwork { Testnet11, } +impl Display for SupportedNetwork { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + SupportedNetwork::Mainnet => write!(f, "mainnet"), + SupportedNetwork::Testnet10 => write!(f, "testnet-10"), + SupportedNetwork::Testnet12 => write!(f, "testnet-12"), + } + } +} + +impl Display for DeprecatedNetwork { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + DeprecatedNetwork::Testnet11 => write!(f, "testnet-11"), + } + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum Network { + Supported(SupportedNetwork), + Deprecated(DeprecatedNetwork), +} + impl Display for Network { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - Network::Mainnet => write!(f, "mainnet"), - Network::Testnet10 => write!(f, "testnet-10"), - Network::Testnet11 => write!(f, "testnet-11"), + Network::Supported(network) => write!(f, "{network}"), + Network::Deprecated(network) => write!(f, "{network}"), } } } + +impl Network { + pub fn supported() -> impl Iterator { + SupportedNetwork::iter().copied().map(Self::from) + } + + pub fn deprecated() -> impl Iterator { + DeprecatedNetwork::iter().copied().map(Self::from) + } + + pub fn is_supported(self) -> bool { + matches!(self, Network::Supported(_)) + } + + pub fn is_deprecated(self) -> bool { + matches!(self, Network::Deprecated(_)) + } +} + +impl Default for Network { + fn default() -> Self { + SupportedNetwork::default().into() + } +} + +impl From for Network { + fn from(network: SupportedNetwork) -> Self { + Self::Supported(network) + } +} + +impl From for Network { + fn from(network: DeprecatedNetwork) -> Self { + Self::Deprecated(network) + } +} diff --git a/src/resolver.rs b/src/resolver.rs index 8c59b6c..dff03bc 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -249,7 +249,7 @@ pub fn version(origin: &Origin) -> Option { .and_then(|s| { s.trim() .split(' ') - .last() + .next_back() .map(|version| format!("{version}-{hash}")) }) }