diff --git a/.bazelrc b/.bazelrc index af3751b66..4db0597cf 100644 --- a/.bazelrc +++ b/.bazelrc @@ -26,7 +26,7 @@ build:lint --config=clippy build:ci --progress_report_interval=30 -build --nosandbox_default_allow_network +build --sandbox_default_allow_network build --incompatible_strict_action_env # use an environment with a static value for PATH and do not inherit LD_LIBRARY_PATH # default to optimized and unstripped binaries. diff --git a/.vscode/settings.json b/.vscode/settings.json index 01b46c29e..1d398b267 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "yaml.schemas": { "release-index-shema.json": "release-index.yaml" - } + }, + "rust-analyzer.showUnlinkedFileNotification": false } \ No newline at end of file diff --git a/Cargo.Bazel.lock b/Cargo.Bazel.lock index 07ca7e604..fdcadd3f3 100644 --- a/Cargo.Bazel.lock +++ b/Cargo.Bazel.lock @@ -1,5 +1,5 @@ { - "checksum": "31058f9f66529f2b114e4c4508e3cc6dbf7243335ca17851bca859ae720bcccd", + "checksum": "b6737f3f6bdb970486578a3c4c615afe6fd16e0bf32c3930b649d568c6be06c5", "crates": { "actix-codec 0.5.2": { "name": "actix-codec", @@ -22116,6 +22116,10 @@ "id": "chrono 0.4.37", "target": "chrono" }, + { + "id": "futures 0.3.30", + "target": "futures" + }, { "id": "ic-base-types 0.9.0", "target": "ic_base_types" @@ -22152,6 +22156,10 @@ "id": "strum 0.26.2", "target": "strum" }, + { + "id": "tokio 1.36.0", + "target": "tokio" + }, { "id": "url 2.5.0", "target": "url" @@ -22159,6 +22167,15 @@ ], "selects": {} }, + "deps_dev": { + "common": [ + { + "id": "wiremock 0.6.0", + "target": "wiremock" + } + ], + "selects": {} + }, "edition": "2021", "proc_macro_deps": { "common": [ diff --git a/Cargo.lock b/Cargo.lock index b55e1d07a..b5131af8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4432,6 +4432,7 @@ dependencies = [ "anyhow", "candid", "chrono", + "futures", "ic-base-types", "ic-nns-governance", "ic-registry-subnet-type", @@ -4442,7 +4443,9 @@ dependencies = [ "serde_json", "strum 0.26.2", "strum_macros 0.26.2", + "tokio", "url", + "wiremock", ] [[package]] @@ -7968,6 +7971,7 @@ dependencies = [ "env_logger 0.11.3", "futures", "ic-agent", + "ic-management-types", "ic-nns-common", "ic-nns-constants", "ic-nns-governance", diff --git a/Cargo.toml b/Cargo.toml index 9664c19fe..bad5b9e10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -175,6 +175,7 @@ tokio = { version = "1.2.0", features = ["full"] } url = "2.5.0" urlencoding = "2.1.0" warp = "0.3" +wiremock = "0.6.0" [profile.release] diff --git a/dashboard/package.json b/dashboard/package.json index 2e9639c5b..82d1d3ee5 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -7,9 +7,9 @@ }, "scripts": { "dev-staging": "concurrently \"yarn start\" \"yarn start-backend\" \"yarn start-rust-backend-staging\"", - "start-rust-backend-staging": "cd .. && BACKEND_PORT=8081 RUST_BACKTRACE=1 NETWORK=staging NNS_URL=http://[2600:3000:6100:200:5000:b0ff:fe8e:6b7b]:8080 cargo watch -C rs/ic-management-backend -x 'run --release --color=always'", + "start-rust-backend-staging": "cd .. && BACKEND_PORT=8081 RUST_BACKTRACE=1 cargo watch -C rs/ic-management-backend -x 'run --release --color=always -- --network staging'", "dev-mainnet": "concurrently \"yarn start\" \"yarn start-backend\" \"yarn start-rust-backend-mainnet\"", - "start-rust-backend-mainnet": "cd .. && RUST_BACKTRACE=1 NETWORK=mainnet NNS_URL=https://ic0.app cargo watch -C rs/ic-management-backend -x 'run --release --color=always'", + "start-rust-backend-mainnet": "cd .. && RUST_BACKTRACE=1 cargo watch -C rs/ic-management-backend -x 'run --release --color=always -- --network mainnet'", "dev": "concurrently \"yarn start\" \"yarn start-backend\" \"yarn start-rust-backend-staging\" \"yarn start-rust-backend-mainnet\"", "start": "yarn workspace app start", "start-backend": "yarn workspace backend start", @@ -71,4 +71,4 @@ ] }, "dependencies": {} -} +} \ No newline at end of file diff --git a/rs/cli/Cargo.toml b/rs/cli/Cargo.toml index 1a4e90dd6..aacec17da 100644 --- a/rs/cli/Cargo.toml +++ b/rs/cli/Cargo.toml @@ -60,7 +60,7 @@ url = { workspace = true } tempfile = "3.10.0" [dev-dependencies] -wiremock = "0.6.0" +wiremock = { workspace = true } [[bin]] name = "dre" diff --git a/rs/cli/src/cli.rs b/rs/cli/src/cli.rs index 2ac0b9873..fbd7a98d5 100644 --- a/rs/cli/src/cli.rs +++ b/rs/cli/src/cli.rs @@ -4,9 +4,9 @@ use clap::{Parser, Subcommand}; use clap_num::maybe_hex; use ic_base_types::PrincipalId; use ic_management_types::{Artifact, Network}; -use log::error; +use url::Url; -use crate::detect_neuron::{detect_hsm_auth, detect_neuron, Auth, Neuron}; +use crate::detect_neuron::Neuron; // For more info about the version setup, look at https://docs.rs/clap/latest/clap/struct.Command.html#method.version #[derive(Parser, Clone, Default)] @@ -38,9 +38,14 @@ pub struct Opts { #[clap(long, env = "VERBOSE", global = true)] pub verbose: bool, - // Specify the target network: "mainnet" (default), "staging", or NNS URL + // Specify the target network: "mainnet" (default), "staging", or a testnet name #[clap(long, env = "NETWORK", default_value = "mainnet")] - pub network: Network, + pub network: String, + + // NNS_URLs for the target network, comma separated. + // The argument is mandatory for testnets, and is optional for mainnet and staging + #[clap(long, env = "NNS_URLS", aliases = &["registry-url", "nns-url"], value_delimiter = ',')] + pub nns_urls: Vec, #[clap(subcommand)] pub subcommand: Commands, @@ -409,11 +414,11 @@ pub mod nodes { } #[derive(Clone)] -pub struct Cli { - pub ic_admin: Option, - pub nns_url: url::Url, +pub struct ParsedCli { + pub network: Network, + pub ic_admin_bin_path: Option, pub yes: bool, - pub neuron: Option, + pub neuron: Neuron, } #[derive(Clone)] @@ -427,15 +432,11 @@ pub struct UpdateVersion { pub versions_to_retire: Option>, } -impl Cli { - pub fn get_neuron(&self) -> &Option { +impl ParsedCli { + pub fn get_neuron(&self) -> &Neuron { &self.neuron } - pub fn get_nns_url(&self) -> &url::Url { - &self.nns_url - } - pub fn get_update_cmd_args(update_version: &UpdateVersion) -> Vec { [ [ @@ -462,39 +463,29 @@ impl Cli { } pub async fn from_opts(opts: &Opts, require_authentication: bool) -> anyhow::Result { - let nns_url = opts.network.get_url(); - let neuron = if let Some(id) = opts.neuron_id { - Some(Neuron { - id, - auth: if let Some(path) = opts.private_key_pem.clone() { - Auth::Keyfile { path } - } else if let (Some(slot), Some(pin), Some(key_id)) = - (opts.hsm_slot, opts.hsm_pin.clone(), opts.hsm_key_id.clone()) - { - Auth::Hsm { pin, slot, key_id } - } else { - detect_hsm_auth()? - .ok_or_else(|| anyhow::anyhow!("No valid authentication method found for neuron: {id}"))? - }, - }) - } else if require_authentication { - // Early warn if there will be a problem because a neuron was not detected. - match detect_neuron(nns_url.clone()).await { - Ok(Some(n)) => Some(n), - Ok(None) => { - error!("No neuron detected. Your HSM device is not detectable (or override variables HSM_PIN, HSM_SLOT, HSM_KEY_ID are incorrectly set); your variables NEURON_ID, PRIVATE_KEY_PEM might not be defined either."); - None - }, - Err(e) => return Err(anyhow::anyhow!("Failed to detect neuron: {}. Your HSM device is not detectable (or override variables HSM_PIN, HSM_SLOT, HSM_KEY_ID are incorrectly set); your variables NEURON_ID, PRIVATE_KEY_PEM might not be defined either.", e)), - } - } else { - None - }; - Ok(Cli { + let network = Network::new(&opts.network, &opts.nns_urls).await.map_err(|e| { + anyhow::anyhow!( + "Failed to parse network from name {} and NNS urls {:?}. Error: {}", + opts.network, + opts.nns_urls, + e + ) + })?; + let neuron = Neuron::new( + &network, + require_authentication, + opts.neuron_id, + opts.private_key_pem.clone(), + opts.hsm_slot.clone(), + opts.hsm_pin.clone(), + opts.hsm_key_id.clone(), + ) + .await?; + Ok(ParsedCli { + network: network, yes: opts.yes, neuron, - ic_admin: opts.ic_admin.clone(), - nns_url, + ic_admin_bin_path: opts.ic_admin.clone(), }) } } diff --git a/rs/cli/src/clients.rs b/rs/cli/src/clients.rs index b21680918..ff000225c 100644 --- a/rs/cli/src/clients.rs +++ b/rs/cli/src/clients.rs @@ -18,7 +18,7 @@ pub struct DashboardBackendClient { impl DashboardBackendClient { // Only used in tests, which should be cleaned up together with this code. #[allow(dead_code)] - pub fn new(network: Network, dev: bool) -> DashboardBackendClient { + pub fn new(network: &Network, dev: bool) -> DashboardBackendClient { Self { url: reqwest::Url::parse(if !dev { "https://dashboard.internal.dfinity.network/" @@ -28,16 +28,12 @@ impl DashboardBackendClient { .expect("invalid base url") .join("api/proxy/registry/") .expect("failed to join url") - .join(match network { - Network::Mainnet => "mainnet/", - Network::Staging => "staging/", - Network::Url(_) => "/", - }) + .join(&network.name) .expect("failed to join url"), } } - pub fn new_with_network_url(url: String) -> Self { + pub fn new_with_backend_url(url: String) -> Self { Self { url: reqwest::Url::parse(&url).unwrap(), } @@ -154,22 +150,28 @@ impl RESTRequestBuilder for reqwest::RequestBuilder { mod tests { use super::*; - #[test] - fn dashboard_backend_client_url() { + #[tokio::test] + async fn dashboard_backend_client_url() { + let mainnet = Network::new("mainnet", &vec![]) + .await + .expect("failed to create mainnet network"); + let staging = Network::new("staging", &vec![]) + .await + .expect("failed to create staging network"); assert_eq!( - DashboardBackendClient::new(Network::Mainnet, false).url.to_string(), + DashboardBackendClient::new(&mainnet, false).url.to_string(), "https://dashboard.internal.dfinity.network/api/proxy/registry/mainnet/" ); assert_eq!( - DashboardBackendClient::new(Network::Staging, false).url.to_string(), + DashboardBackendClient::new(&staging, false).url.to_string(), "https://dashboard.internal.dfinity.network/api/proxy/registry/staging/" ); assert_eq!( - DashboardBackendClient::new(Network::Mainnet, true).url.to_string(), + DashboardBackendClient::new(&mainnet, true).url.to_string(), "http://localhost:17000/api/proxy/registry/mainnet/" ); assert_eq!( - DashboardBackendClient::new(Network::Staging, true).url.to_string(), + DashboardBackendClient::new(&staging, true).url.to_string(), "http://localhost:17000/api/proxy/registry/staging/" ); } diff --git a/rs/cli/src/detect_neuron.rs b/rs/cli/src/detect_neuron.rs index 45fc41c8b..8baf68835 100644 --- a/rs/cli/src/detect_neuron.rs +++ b/rs/cli/src/detect_neuron.rs @@ -1,5 +1,6 @@ use std::{path::PathBuf, str::FromStr}; +use anyhow::Context; use candid::{Decode, Encode}; use cryptoki::{ context::{CInitializeArgs, Pkcs11}, @@ -23,6 +24,69 @@ impl Neuron { pub fn as_arg_vec(&self) -> Vec { vec!["--proposer".to_string(), self.id.to_string()] } + + // FIXME: make this auth lazy + pub async fn new( + network: &ic_management_types::Network, + require_authentication: bool, + neuron_id: Option, + private_key_pem: Option, + hsm_slot: Option, + hsm_pin: Option, + hsm_key_id: Option, + ) -> anyhow::Result { + match require_authentication { + // Auth required, try to find valid neuron id using HSM or with the private key + true => { + // If private key is provided, use it without checking + if let Some(path) = private_key_pem { + Ok(Self { + id: neuron_id.context("Neuron ID is required when using a private key")?, + auth: Auth::Keyfile { path }, + }) + // If HSM slot, pin and key id are provided, use them without checking + } else if let (Some(slot), Some(pin), Some(key_id)) = (hsm_slot, hsm_pin, hsm_key_id) { + Ok(Self { + id: neuron_id.context("Neuron ID is required when using HSM")?, + auth: Auth::Hsm { pin, slot, key_id }, + }) + } else { + // Fully automatic detection of the neuron id using HSM + let auth = match detect_hsm_auth()? { + Some(auth) => auth, + None => return Err(anyhow::anyhow!("No HSM detected")), + }; + match auto_detect_neuron(&network.get_nns_urls(), auth).await { + Ok(Some(n)) => Ok(n), + Ok(None) => anyhow::bail!("No HSM detected. Please provide HSM slot, pin, and key id."), + Err(e) => anyhow::bail!("Error while detectin neuron: {}", e), + } + } + } + // Auth not required, don't attempt to talk to HSM and the NNS, but accept values provided by the user. + false => { + if let Some(path) = private_key_pem { + Ok(Self { + id: neuron_id.unwrap_or_default(), + auth: Auth::Keyfile { path }, + }) + // If HSM slot, pin and key id are provided, use them without checking + } else if let (Some(slot), Some(pin), Some(key_id)) = (hsm_slot, hsm_pin, hsm_key_id) { + Ok(Self { + id: neuron_id.unwrap_or_default(), + auth: Auth::Hsm { pin, slot, key_id }, + }) + } else { + Ok(Self { + id: neuron_id.unwrap_or_default(), + auth: Auth::Keyfile { + path: "/fake/path/to/private_key.pem".to_string(), + }, + }) + } + } + } + } } #[derive(Clone)] @@ -64,6 +128,19 @@ impl Auth { Auth::Keyfile { path } => vec!["--secret-key-pem".to_string(), path.clone()], } } + + pub fn from_cli_args( + private_key_pem: Option, + hsm_slot: Option, + hsm_pin: Option, + hsm_key_id: Option, + ) -> anyhow::Result { + match (private_key_pem, hsm_slot, hsm_pin, hsm_key_id) { + (Some(path), _, _, _) => Ok(Auth::Keyfile { path }), + (None, Some(slot), Some(pin), Some(key_id)) => Ok(Auth::Hsm { pin, slot, key_id }), + _ => Err(anyhow::anyhow!("Invalid auth arguments")), + } + } } pub fn detect_hsm_auth() -> anyhow::Result> { @@ -96,8 +173,9 @@ pub fn detect_hsm_auth() -> anyhow::Result> { Ok(None) } -pub async fn detect_neuron(url: url::Url) -> anyhow::Result> { - if let Some(Auth::Hsm { pin, slot, key_id }) = detect_hsm_auth()? { +// FIXME: This function should use either the HSM or the private key, instead of assuming the HSM +pub async fn auto_detect_neuron(nns_urls: &Vec, auth: Auth) -> anyhow::Result> { + if let Auth::Hsm { pin, slot, key_id } = auth { let auth = Auth::Hsm { pin: pin.clone(), slot, @@ -112,7 +190,7 @@ pub async fn detect_neuron(url: url::Url) -> anyhow::Result> { ) }), ); - let agent = Agent::new(url, sender); + let agent = Agent::new(nns_urls[0].clone(), sender); let neuron_id = if let Some(response) = agent .execute_query( &GOVERNANCE_CANISTER_ID, diff --git a/rs/cli/src/general.rs b/rs/cli/src/general.rs index ba7ea5b4b..e4ddf684b 100644 --- a/rs/cli/src/general.rs +++ b/rs/cli/src/general.rs @@ -19,16 +19,16 @@ use crate::detect_neuron::{Auth, Neuron}; pub async fn vote_on_proposals( neuron: &Neuron, - nns_url: &Url, + nns_urls: &Vec, accepted_proposers: &[u64], accepted_topics: &[i32], simulate: bool, ) -> anyhow::Result<()> { let client: GovernanceCanisterWrapper = match &neuron.auth { Auth::Hsm { pin, slot, key_id } => { - CanisterClient::from_hsm(pin.to_string(), *slot, key_id.to_string(), nns_url)?.into() + CanisterClient::from_hsm(pin.to_string(), *slot, key_id.to_string(), &nns_urls[0])?.into() } - Auth::Keyfile { path } => CanisterClient::from_key_file(path.into(), nns_url)?.into(), + Auth::Keyfile { path } => CanisterClient::from_key_file(path.into(), &nns_urls[0])?.into(), }; // In case of incorrectly set voting following, or in case of some other errors, @@ -93,15 +93,19 @@ pub async fn get_node_metrics_history( wallet: CanisterId, subnets: Vec, start_at_nanos: u64, - neuron: &Neuron, - nns_url: &Url, + auth: &Auth, + nns_urls: &Vec, ) -> anyhow::Result<()> { let lock = Mutex::new(()); - let canister_agent = match &neuron.auth { - Auth::Hsm { pin, slot, key_id } => { - IcAgentCanisterClient::from_hsm(pin.to_string(), *slot, key_id.to_string(), nns_url.clone(), Some(lock))? - } - Auth::Keyfile { path } => IcAgentCanisterClient::from_key_file(path.into(), nns_url.clone())?, + let canister_agent = match auth { + Auth::Hsm { pin, slot, key_id } => IcAgentCanisterClient::from_hsm( + pin.to_string(), + *slot, + key_id.to_string(), + nns_urls[0].clone(), + Some(lock), + )?, + Auth::Keyfile { path } => IcAgentCanisterClient::from_key_file(path.into(), nns_urls[0].clone())?, }; info!("Started action..."); let wallet_client = WalletCanisterWrapper::new(canister_agent.agent.clone()); diff --git a/rs/cli/src/ic_admin.rs b/rs/cli/src/ic_admin.rs index 3d79695ac..474b8db3d 100644 --- a/rs/cli/src/ic_admin.rs +++ b/rs/cli/src/ic_admin.rs @@ -31,7 +31,7 @@ use std::{fmt::Display, path::Path, process::Command}; use strum::Display; use tempfile::NamedTempFile; -use crate::cli::Cli; +use crate::cli::ParsedCli; use crate::detect_neuron::{Auth, Neuron}; use crate::{cli, defaults}; @@ -138,24 +138,36 @@ impl FirewallRuleModifications { #[derive(Clone)] pub struct IcAdminWrapper { - ic_admin: Option, - nns_url: url::Url, - yes: bool, - neuron: Option, + network: Network, + ic_admin_bin_path: Option, + proceed_without_confirmation: bool, + neuron: Neuron, } -impl From for IcAdminWrapper { - fn from(cli: Cli) -> Self { +impl IcAdminWrapper { + pub fn new( + network: Network, + ic_admin_bin_path: Option, + proceed_without_confirmation: bool, + neuron: Neuron, + ) -> Self { + Self { + network, + ic_admin_bin_path, + proceed_without_confirmation, + neuron, + } + } + + pub fn from_cli(cli: ParsedCli) -> Self { Self { - ic_admin: cli.ic_admin, - nns_url: cli.nns_url, - yes: cli.yes, + network: cli.network, + ic_admin_bin_path: cli.ic_admin_bin_path, + proceed_without_confirmation: cli.yes, neuron: cli.neuron, } } -} -impl IcAdminWrapper { fn print_ic_admin_command_line(&self, cmd: &Command) { info!( "running ic-admin: \n$ {}{}", @@ -164,19 +176,12 @@ impl IcAdminWrapper { .map(|s| s.to_str().unwrap().to_string()) .fold("".to_string(), |acc, s| { let s = if s.contains('\n') { format!(r#""{}""#, s) } else { s }; - if self - .neuron - .as_ref() - .and_then(|n| { - if let Auth::Hsm { pin, .. } = &n.auth { - Some(pin.clone()) - } else { - None - } - }) - .unwrap_or_default() - == s - { + let hsm_pin = if let Auth::Hsm { pin, .. } = &self.neuron.auth { + &pin + } else { + "" + }; + if hsm_pin == s { format!("{acc} ") } else if s.starts_with("--") { format!("{acc} \\\n {s}") @@ -228,7 +233,7 @@ impl IcAdminWrapper { ] }) .unwrap_or_default(), - cli.neuron.as_ref().map(|n| n.as_arg_vec()).unwrap_or_default(), + cli.neuron.as_arg_vec(), cmd.args(), ] .concat() @@ -243,15 +248,10 @@ impl IcAdminWrapper { } // If --yes was not specified, ask the user if they want to proceed - if !self.yes { + if !self.proceed_without_confirmation { exec(self, cmd.clone(), opts.clone(), true)?; } - // User wants to proceed but does not have neuron configuration. Bail out. - if self.neuron.is_none() { - return Err(anyhow::anyhow!("Submitting this proposal requires a neuron, which was not detected -- and would cause ic-admin to fail during submition. Please look through your scroll buffer for specific error messages about your HSM and address the issue that prevents your neuron from being detected.")); - } - if Confirm::new() .with_prompt("Do you want to continue?") .default(false) @@ -266,14 +266,18 @@ impl IcAdminWrapper { } fn _run_ic_admin_with_args(&self, ic_admin_args: &[String], with_auth: bool) -> anyhow::Result { - let ic_admin_path = self.ic_admin.clone().unwrap_or_else(|| "ic-admin".to_string()); + let ic_admin_path = self.ic_admin_bin_path.clone().unwrap_or_else(|| "ic-admin".to_string()); let mut cmd = Command::new(ic_admin_path); let auth_options = if with_auth { - self.neuron.as_ref().map(|n| n.auth.as_arg_vec()).unwrap_or_default() + self.neuron.auth.as_arg_vec() } else { vec![] }; - let root_options = [auth_options, vec!["--nns-url".to_string(), self.nns_url.to_string()]].concat(); + let root_options = [ + auth_options, + vec!["--nns-urls".to_string(), self.network.get_nns_urls_string()], + ] + .concat(); let cmd = cmd.args([&root_options, ic_admin_args].concat()); self.print_ic_admin_command_line(cmd); @@ -315,7 +319,7 @@ impl IcAdminWrapper { /// extract the ones matching `needle_regex` and return them as a /// `Vec` fn grep_subcommands(&self, needle_regex: &str) -> Vec { - let ic_admin_path = self.ic_admin.clone().unwrap_or_else(|| "ic-admin".to_string()); + let ic_admin_path = self.ic_admin_bin_path.clone().unwrap_or_else(|| "ic-admin".to_string()); let cmd_result = Command::new(ic_admin_path).args(["--help"]).output(); match cmd_result.map_err(|e| e.to_string()) { Ok(output) => { @@ -668,10 +672,10 @@ must be identical, and must match the SHA256 from the payload of the NNS proposa pub async fn update_unassigned_nodes( &self, nns_subned_id: &String, - network: Network, + network: &Network, simulate: bool, ) -> Result<(), Error> { - let local_registry_path = local_registry_path(network.clone()); + let local_registry_path = local_registry_path(network); let local_registry = LocalRegistry::new(local_registry_path, Duration::from_secs(10)) .map_err(|e| anyhow::anyhow!("Error in creating local registry instance: {:?}", e))?; @@ -718,11 +722,11 @@ must be identical, and must match the SHA256 from the payload of the NNS proposa pub async fn update_replica_nodes_firewall( &self, - network: Network, + network: &Network, propose_options: ProposeOptions, simulate: bool, ) -> Result<(), Error> { - let local_registry_path = local_registry_path(network.clone()); + let local_registry_path = local_registry_path(network); let local_registry = LocalRegistry::new(local_registry_path, Duration::from_secs(10)) .map_err(|e| anyhow::anyhow!("Error in creating local registry instance: {:?}", e))?; @@ -1109,11 +1113,14 @@ oSMDIQBa2NLmSmaqjDXej4rrJEuEhKIz7/pGXpxztViWhB+X9Q== // Start a background HTTP server on a random local port let mock_server = MockServer::start().await; + let network = Network::new("testnet", &vec![url::Url::from_str(&mock_server.uri()).unwrap()]) + .await + .expect("Failed to create network"); for cmd in test_cases { let cli = IcAdminWrapper { - nns_url: url::Url::from_str(&mock_server.uri()).unwrap(), - yes: false, + network: network.clone(), + proceed_without_confirmation: false, neuron: Neuron { id: 3, auth: Auth::Keyfile { @@ -1125,7 +1132,7 @@ oSMDIQBa2NLmSmaqjDXej4rrJEuEhKIz7/pGXpxztViWhB+X9Q== }, } .into(), - ic_admin: None, + ic_admin_bin_path: None, }; let cmd_name = cmd.to_string(); @@ -1136,7 +1143,7 @@ oSMDIQBa2NLmSmaqjDXej4rrJEuEhKIz7/pGXpxztViWhB+X9Q== }; let vector = vec![ - if !cli.yes { + if !cli.proceed_without_confirmation { vec!["--dry-run".to_string()] } else { Default::default() @@ -1158,7 +1165,7 @@ oSMDIQBa2NLmSmaqjDXej4rrJEuEhKIz7/pGXpxztViWhB+X9Q== ] }) .unwrap_or_default(), - cli.neuron.as_ref().map(|n| n.as_arg_vec()).unwrap_or_default(), + cli.neuron.auth.as_arg_vec(), cmd.args(), ] .concat() diff --git a/rs/cli/src/main.rs b/rs/cli/src/main.rs index 7e7accdd1..9482abefa 100644 --- a/rs/cli/src/main.rs +++ b/rs/cli/src/main.rs @@ -1,6 +1,7 @@ use crate::ic_admin::IcAdminWrapper; use clap::{error::ErrorKind, CommandFactory, Parser}; use dotenv::dotenv; +use dre::detect_neuron::Auth; use dre::general::{get_node_metrics_history, vote_on_proposals}; use dre::operations::hostos_rollout::{NodeGroupUpdate, NumberOfNodes}; use dre::{cli, ic_admin, local_unused_port, registry_dump, runner}; @@ -8,7 +9,7 @@ use ic_base_types::CanisterId; use ic_canisters::governance::governance_canister_version; use ic_management_backend::endpoints; use ic_management_types::requests::NodesRemoveRequest; -use ic_management_types::{Artifact, MinNakamotoCoefficients, Network, NodeFeature}; +use ic_management_types::{Artifact, MinNakamotoCoefficients, NodeFeature}; use log::info; use std::collections::BTreeMap; use std::str::FromStr; @@ -26,7 +27,24 @@ async fn main() -> Result<(), anyhow::Error> { let mut cli_opts = cli::Opts::parse(); let mut cmd = cli::Opts::command(); - let governance_canister_v = match governance_canister_version(cli_opts.network.get_url()).await { + let target_network = ic_management_types::Network::new(cli_opts.network.clone(), &cli_opts.nns_urls) + .await + .expect("Failed to create network"); + let nns_urls = target_network.get_nns_urls(); + + // Start of actually doing stuff with commands. + if target_network.name == "staging" { + if cli_opts.private_key_pem.is_none() { + cli_opts.private_key_pem = Some( + std::env::var("HOME").expect("Please set HOME env var") + + "/.config/dfx/identity/bootstrap-super-leader/identity.pem", + ); + } + if cli_opts.neuron_id.is_none() { + cli_opts.neuron_id = Some(STAGING_NEURON_ID); + } + } + let governance_canister_v = match governance_canister_version(&nns_urls).await { Ok(c) => c, Err(e) => { return Err(anyhow::anyhow!( @@ -38,7 +56,6 @@ async fn main() -> Result<(), anyhow::Error> { let governance_canister_version = governance_canister_v.stringified_hash; - let target_network = cli_opts.network.clone(); let (tx, rx) = mpsc::channel(); let backend_port = local_unused_port(); @@ -46,7 +63,7 @@ async fn main() -> Result<(), anyhow::Error> { thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { - endpoints::run_backend(target_network_backend, "127.0.0.1", backend_port, true, Some(tx)) + endpoints::run_backend(&target_network_backend, "127.0.0.1", backend_port, true, Some(tx)) .await .expect("failed") }); @@ -56,14 +73,20 @@ async fn main() -> Result<(), anyhow::Error> { let r = ic_admin::with_ic_admin(governance_canister_version.into(), async { - // Start of actually doing stuff with commands. - if cli_opts.network == Network::Staging { - cli_opts.private_key_pem = Some(std::env::var("HOME").expect("Please set HOME env var") + "/.config/dfx/identity/bootstrap-super-leader/identity.pem"); - cli_opts.neuron_id = Some(STAGING_NEURON_ID); - } - let simulate = cli_opts.simulate; + let runner_unauth = || async { + let cli = cli::ParsedCli::from_opts(&cli_opts, false).await.expect("Failed to create unauthenticated CLI"); + let ic_admin_wrapper = IcAdminWrapper::from_cli(cli); + runner::Runner::new_with_network_and_backend_port(ic_admin_wrapper, &target_network, backend_port).await.expect("Failed to create unauthenticated runner") + }; + + let runner_auth = || async { + let cli = cli::ParsedCli::from_opts(&cli_opts, true).await.expect("Failed to create authenticated CLI"); + let ic_admin_wrapper = IcAdminWrapper::from_cli(cli); + runner::Runner::new_with_network_and_backend_port(ic_admin_wrapper, &target_network, backend_port).await.expect("Failed to create authenticated runner") + }; + match &cli_opts.subcommand { cli::Commands::DerToPrincipal { path } => { let principal = ic_base_types::PrincipalId::new_self_authenticating(&std::fs::read(path)?); @@ -102,8 +125,7 @@ async fn main() -> Result<(), anyhow::Error> { match &subnet.subcommand { cli::subnet::Commands::Deploy { version } => { - let runner = runner::Runner::new_with_network_url(cli::Cli::from_opts(&cli_opts, false).await?.into(), backend_port).await?; - runner.deploy(&subnet.id.unwrap(), version, simulate) + runner_auth().await.deploy(&subnet.id.unwrap(), version, simulate) }, cli::subnet::Commands::Replace { nodes, @@ -116,8 +138,7 @@ async fn main() -> Result<(), anyhow::Error> { min_nakamoto_coefficients, } => { let min_nakamoto_coefficients = parse_min_nakamoto_coefficients(&mut cmd, min_nakamoto_coefficients); - let runner = runner::Runner::new_with_network_url(cli::Cli::from_opts(&cli_opts, true).await?.into(), backend_port).await?; - runner + runner_auth().await .membership_replace(ic_management_types::requests::MembershipReplaceRequest { target: match &subnet.id { Some(subnet) => { @@ -149,8 +170,7 @@ async fn main() -> Result<(), anyhow::Error> { } cli::subnet::Commands::Resize { add, remove, include, only, exclude, motivation, } => { if let Some(motivation) = motivation.clone() { - let runner = runner::Runner::new_with_network_url(cli::Cli::from_opts(&cli_opts, true).await?.into(), backend_port).await?; - runner.subnet_resize(ic_management_types::requests::SubnetResizeRequest { + runner_auth().await.subnet_resize(ic_management_types::requests::SubnetResizeRequest { subnet: subnet.id.unwrap(), add: *add, remove: *remove, @@ -169,8 +189,7 @@ async fn main() -> Result<(), anyhow::Error> { cli::subnet::Commands::Create { size, min_nakamoto_coefficients, exclude, only, include, motivation, replica_version } => { let min_nakamoto_coefficients = parse_min_nakamoto_coefficients(&mut cmd, min_nakamoto_coefficients); if let Some(motivation) = motivation.clone() { - let runner = runner::Runner::new_with_network_url(cli::Cli::from_opts(&cli_opts, true).await?.into(), backend_port).await?; - runner.subnet_create(ic_management_types::requests::SubnetCreateRequest { + runner_auth().await.subnet_create(ic_management_types::requests::SubnetCreateRequest { size: *size, min_nakamoto_coefficients, only: only.clone().into(), @@ -189,27 +208,24 @@ async fn main() -> Result<(), anyhow::Error> { } cli::Commands::Get { args } => { - let ic_admin: IcAdminWrapper = cli::Cli::from_opts(&cli_opts, false).await?.into(); - ic_admin.run_passthrough_get(args) + runner_unauth().await.ic_admin.run_passthrough_get(args) }, cli::Commands::Propose { args } => { - let ic_admin: IcAdminWrapper = cli::Cli::from_opts(&cli_opts, true).await?.into(); - ic_admin.run_passthrough_propose(args, simulate) + runner_auth().await.ic_admin.run_passthrough_propose(args, simulate) }, cli::Commands::UpdateUnassignedNodes { nns_subnet_id } => { - let ic_admin: IcAdminWrapper = cli::Cli::from_opts(&cli_opts, true).await?.into(); - ic_admin.update_unassigned_nodes( nns_subnet_id, cli_opts.network, simulate).await + runner_auth().await.ic_admin.update_unassigned_nodes( nns_subnet_id, &target_network, simulate).await }, cli::Commands::Version(version_command) => { match &version_command { cli::version::Cmd::Update(update_command) => { - let runner = runner::Runner::new_with_network_url(cli::Cli::from_opts(&cli_opts, true).await?.into(), backend_port).await?; - let ic_admin: IcAdminWrapper = cli::Cli::from_opts(&cli_opts, true).await?.into(); let release_artifact: &Artifact = &update_command.subcommand.clone().into(); + let runner_auth = runner_auth().await; + let update_version = match &update_command.subcommand { cli::version::UpdateCommands::Replica { version, release_tag, force} | cli::version::UpdateCommands::HostOS { version, release_tag, force} => { ic_admin::IcAdminWrapper::prepare_to_propose_to_update_elected_versions( @@ -217,14 +233,14 @@ async fn main() -> Result<(), anyhow::Error> { version, release_tag, *force, - runner.prepare_versions_to_retire(release_artifact, false).await.map(|res| res.1)?, + runner_auth.prepare_versions_to_retire(release_artifact, false).await.map(|res| res.1)?, ) } }.await?; - ic_admin.propose_run(ic_admin::ProposeCommand::UpdateElectedVersions { + runner_auth.ic_admin.propose_run(ic_admin::ProposeCommand::UpdateElectedVersions { release_artifact: update_version.release_artifact.clone(), - args: cli::Cli::get_update_cmd_args(&update_version) + args: cli::ParsedCli::get_update_cmd_args(&update_version) }, ic_admin::ProposeOptions{ title: Some(update_version.title), @@ -239,14 +255,12 @@ async fn main() -> Result<(), anyhow::Error> { cli::Commands::Hostos(nodes) => { match &nodes.subcommand { cli::hostos::Commands::Rollout { version,nodes} => { - let runner = runner::Runner::new_with_network_url(cli::Cli::from_opts(&cli_opts, true).await?.into(), backend_port).await?; - runner.hostos_rollout(nodes.clone(), version, simulate, None).await + runner_auth().await.hostos_rollout(nodes.clone(), version, simulate, None).await }, cli::hostos::Commands::RolloutFromNodeGroup {version, assignment, owner, nodes_in_group, exclude } => { let update_group = NodeGroupUpdate::new(*assignment, *owner, NumberOfNodes::from_str(nodes_in_group)?); - let runner = runner::Runner::new(cli::Cli::from_opts(&cli_opts, true).await?.into(), target_network.clone()).await?; - if let Some((nodes_to_update, summary)) = runner.hostos_rollout_nodes(update_group, version, exclude).await? { - return runner.hostos_rollout(nodes_to_update, version, simulate, Some(summary)).await + if let Some((nodes_to_update, summary)) = runner_auth().await.hostos_rollout_nodes(update_group, version, exclude).await? { + return runner_auth().await.hostos_rollout(nodes_to_update, version, simulate, Some(summary)).await } Ok(()) } @@ -262,8 +276,7 @@ async fn main() -> Result<(), anyhow::Error> { ) .exit(); } - let runner = runner::Runner::new_with_network_url(cli::Cli::from_opts(&cli_opts, true).await?.into(), backend_port).await?; - runner.remove_nodes(NodesRemoveRequest { + runner_auth().await.remove_nodes(NodesRemoveRequest { extra_nodes_filter: extra_nodes_filter.clone(), no_auto: *no_auto, remove_degraded: *remove_degraded, @@ -275,32 +288,23 @@ async fn main() -> Result<(), anyhow::Error> { }, cli::Commands::Vote {accepted_neurons, accepted_topics}=> { - let cli = cli::Cli::from_opts(&cli_opts, true).await?; - vote_on_proposals(match cli.get_neuron() { - Some(neuron) => neuron, - None => return Err(anyhow::anyhow!("Neuron required for this command")), - }, cli.get_nns_url(), accepted_neurons, accepted_topics, simulate).await + let cli = cli::ParsedCli::from_opts(&cli_opts, true).await?; + vote_on_proposals(cli.get_neuron(), &target_network.get_nns_urls(), accepted_neurons, accepted_topics, simulate).await }, cli::Commands::TrustworthyMetrics { wallet, start_at_timestamp, subnet_ids } => { - // Neuron is not actually needed for this operation, we only need other authentication params - // Unfortunately, we need to create a Cli object to get the NNS URL and this object at the moment - // requires a neuron to be set. We set a dummy neuron here to get around this. - let cli_opts = cli::Opts { neuron_id: Some(0), ..cli_opts.clone() }; - let cli = cli::Cli::from_opts(&cli_opts, true).await?; - get_node_metrics_history(CanisterId::from_str(wallet)?, subnet_ids.clone(), *start_at_timestamp, match cli.get_neuron() { - Some(neuron) => neuron, - None => return Err(anyhow::anyhow!("Neuron required for this command")), - }, cli.get_nns_url()).await + let auth = Auth::from_cli_args(cli_opts.private_key_pem, cli_opts.hsm_slot, cli_opts.hsm_pin, cli_opts.hsm_key_id)?; + get_node_metrics_history(CanisterId::from_str(wallet)?, subnet_ids.clone(), *start_at_timestamp, &auth, &target_network.get_nns_urls()).await }, cli::Commands::DumpRegistry { version, path } => { - registry_dump::dump_registry(path, cli_opts.network, version).await + registry_dump::dump_registry(path, &target_network, version).await } cli::Commands::Firewall{title, summary, motivation} => { - let ic_admin: IcAdminWrapper = cli::Cli::from_opts(&cli_opts, true).await?.into(); - ic_admin.update_replica_nodes_firewall(cli_opts.network, ic_admin::ProposeOptions{ + let runner_auth = runner_auth().await; + + runner_auth.ic_admin.update_replica_nodes_firewall(&target_network, ic_admin::ProposeOptions{ title: title.clone(), summary: summary.clone(), motivation: motivation.clone(), diff --git a/rs/cli/src/operations/hostos_rollout.rs b/rs/cli/src/operations/hostos_rollout.rs index ce3097aaa..33bbf5e16 100644 --- a/rs/cli/src/operations/hostos_rollout.rs +++ b/rs/cli/src/operations/hostos_rollout.rs @@ -178,7 +178,7 @@ impl HostosRollout { pub fn new( nodes: BTreeMap, subnets: BTreeMap, - network: Network, + network: &Network, proposal_agent: ProposalAgent, rollout_version: &str, nodes_filter: &Option>, @@ -213,7 +213,7 @@ impl HostosRollout { HostosRollout { grouped_nodes, subnets, - network, + network: network.clone(), proposal_agent, exclude: nodes_filter.clone(), version: rollout_version.to_string(), @@ -578,11 +578,13 @@ pub mod test { let open_proposals: Vec = vec![]; + let network = Network::new("mainnet", &vec![]).await.unwrap(); + let nns_urls = network.get_nns_urls(); let hostos_rollout = HostosRollout::new( union.clone(), subnet.clone(), - Network::Mainnet, - ProposalAgent::new("https://ic0.app".to_string()), + &network, + ProposalAgent::new(nns_urls), version_one.clone().as_str(), &None, ); @@ -613,8 +615,8 @@ pub mod test { let hostos_rollout = HostosRollout::new( union.clone(), subnet.clone(), - Network::Mainnet, - ProposalAgent::new("https://ic0.app".to_string()), + &network, + ProposalAgent::new(nns_urls), version_one.clone().as_str(), &Some(nodes_to_exclude), ); @@ -637,8 +639,8 @@ pub mod test { let hostos_rollout = HostosRollout::new( union.clone(), subnet.clone(), - Network::Mainnet, - ProposalAgent::new("https://ic0.app".to_string()), + &network, + ProposalAgent::new(nns_urls), version_two.clone().as_str(), &None, ); diff --git a/rs/cli/src/registry_dump.rs b/rs/cli/src/registry_dump.rs index 00300bd52..e2b14735f 100644 --- a/rs/cli/src/registry_dump.rs +++ b/rs/cli/src/registry_dump.rs @@ -18,11 +18,11 @@ use itertools::Itertools; use registry_canister::mutations::common::decode_registry_value; use serde::Serialize; -pub async fn dump_registry(path: &Option, network: Network, version: &i64) -> Result<(), Error> { +pub async fn dump_registry(path: &Option, network: &Network, version: &i64) -> Result<(), Error> { if let Some(path) = path { std::env::set_var("LOCAL_REGISTRY_PATH", path) } - sync_local_store(network.clone()).await?; + sync_local_store(network).await?; let local_registry = LocalRegistry::new(local_registry_path(network), Duration::from_secs(10)) .map_err(|e| anyhow::anyhow!("Couldn't create local registry client instance: {:?}", e))?; diff --git a/rs/cli/src/runner.rs b/rs/cli/src/runner.rs index 921486d0a..79a86728c 100644 --- a/rs/cli/src/runner.rs +++ b/rs/cli/src/runner.rs @@ -17,7 +17,7 @@ use tabled::builder::Builder; use tabled::settings::Style; pub struct Runner { - ic_admin: ic_admin::IcAdminWrapper, + pub ic_admin: ic_admin::IcAdminWrapper, dashboard_backend_client: DashboardBackendClient, registry: RegistryState, } @@ -181,22 +181,28 @@ impl Runner { Ok(()) } - pub async fn new_with_network_url(ic_admin: ic_admin::IcAdminWrapper, backend_port: u16) -> anyhow::Result { - let dashboard_backend_client = - DashboardBackendClient::new_with_network_url(format!("http://localhost:{}/", backend_port)); + pub async fn new_with_network_and_backend_port( + ic_admin: ic_admin::IcAdminWrapper, + network: &Network, + backend_port: u16, + ) -> anyhow::Result { + let backend_url = format!("http://localhost:{}/", backend_port); + + let dashboard_backend_client = DashboardBackendClient::new_with_backend_url(backend_url); Ok(Self { ic_admin, dashboard_backend_client, // TODO: Remove once DREL-118 completed. - // Fake registry not used for methods that still rely on backend. - registry: registry::RegistryState::new(Network::Mainnet, true).await, + // Fake registry that is not used, but some methods still rely on backend. + registry: registry::RegistryState::new(&network, true).await, }) } - pub async fn new(ic_admin: ic_admin::IcAdminWrapper, network: Network) -> anyhow::Result { + pub async fn new(ic_admin: ic_admin::IcAdminWrapper, network: &Network) -> anyhow::Result { // TODO: Remove once DREL-118 completed. - let dashboard_backend_client = - DashboardBackendClient::new_with_network_url(format!("http://localhost:{}/", local_unused_port())); + let backend_port = local_unused_port(); + let backend_url = format!("http://localhost:{}/", backend_port); + let dashboard_backend_client = DashboardBackendClient::new_with_backend_url(backend_url); let mut registry = registry::RegistryState::new(network, true).await; let node_providers = query_ic_dashboard_list::("v3/node-providers") @@ -298,8 +304,8 @@ impl Runner { let hostos_rollout = HostosRollout::new( self.registry.nodes(), self.registry.subnets(), - self.registry.network(), - ProposalAgent::new(self.registry.nns_url()), + &self.registry.network(), + ProposalAgent::new(self.registry.get_nns_urls()), &version, &exclude, ); diff --git a/rs/ic-canisters/src/governance.rs b/rs/ic-canisters/src/governance.rs index ca291b069..6eee08580 100644 --- a/rs/ic-canisters/src/governance.rs +++ b/rs/ic-canisters/src/governance.rs @@ -18,11 +18,14 @@ pub struct GovernanceCanisterVersion { pub stringified_hash: String, } -pub async fn governance_canister_version(nns_url: Url) -> Result { +pub async fn governance_canister_version(nns_urls: &Vec) -> Result { let canister_agent = Agent::builder() .with_transport( - ic_agent::agent::http_transport::reqwest_transport::ReqwestHttpReplicaV2Transport::create(nns_url)?, + ic_agent::agent::http_transport::reqwest_transport::ReqwestHttpReplicaV2Transport::create( + nns_urls[0].clone(), + )?, ) + .with_verify_query_signatures(false) .build()?; canister_agent.fetch_root_key().await?; diff --git a/rs/ic-canisters/src/lib.rs b/rs/ic-canisters/src/lib.rs index 503490f44..502187c0a 100644 --- a/rs/ic-canisters/src/lib.rs +++ b/rs/ic-canisters/src/lib.rs @@ -70,6 +70,7 @@ impl IcAgentCanisterClient { agent: Agent::builder() .with_identity(identity) .with_transport(ReqwestTransport::create(url)?) + .with_verify_query_signatures(false) .build()?, }) } @@ -81,6 +82,7 @@ impl IcAgentCanisterClient { agent: Agent::builder() .with_identity(identity) .with_transport(ReqwestTransport::create(url)?) + .with_verify_query_signatures(false) .build()?, }) } diff --git a/rs/ic-management-backend/Cargo.toml b/rs/ic-management-backend/Cargo.toml index 02beb8861..2b93b7fe7 100644 --- a/rs/ic-management-backend/Cargo.toml +++ b/rs/ic-management-backend/Cargo.toml @@ -13,6 +13,7 @@ async-trait = { workspace = true } backon = { workspace = true } candid = { workspace = true } chrono = { workspace = true } +clap = { workspace = true } csv = { workspace = true } custom_error = { workspace = true } decentralization = { path = "../decentralization" } @@ -58,7 +59,6 @@ strum = { workspace = true } strum_macros = { workspace = true } tokio = { workspace = true } url = { workspace = true } -clap = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/rs/ic-management-backend/src/config.rs b/rs/ic-management-backend/src/config.rs deleted file mode 100644 index a844f8f9d..000000000 --- a/rs/ic-management-backend/src/config.rs +++ /dev/null @@ -1,29 +0,0 @@ -use ic_management_types::Network; -use std::str::FromStr; -use url::Url; - -pub fn target_network() -> Network { - Network::from_str(&std::env::var("NETWORK").expect("Missing NETWORK environment variable")) - .expect("Invalid network") -} - -pub fn get_nns_url_string_from_target_network(target_network: &Network) -> String { - match std::env::var("NNS_URL") { - Ok(nns_url) => nns_url, - Err(_) => match target_network { - Network::Mainnet => "https://ic0.app".to_string(), - Network::Staging => "http://[2600:3004:1200:1200:5000:11ff:fe37:c55d]:8080".to_string(), - _ => panic!( - "Cannot get NNS URL for target network {}. Please set NNS_URL environment variable", - target_network - ), - }, - } -} - -pub fn get_nns_url_vec_from_target_network(target_network: &Network) -> Vec { - get_nns_url_string_from_target_network(target_network) - .split(',') - .map(|s| Url::parse(s).unwrap_or_else(|_| panic!("Cannot parse {} as a valid NNS URL", s))) - .collect() -} diff --git a/rs/ic-management-backend/src/endpoints/governance_canister.rs b/rs/ic-management-backend/src/endpoints/governance_canister.rs index 9bf073e42..c7696389f 100644 --- a/rs/ic-management-backend/src/endpoints/governance_canister.rs +++ b/rs/ic-management-backend/src/endpoints/governance_canister.rs @@ -1,11 +1,11 @@ use super::*; -use crate::config::{get_nns_url_vec_from_target_network, target_network}; use ic_canisters::governance::governance_canister_version; #[get("/canisters/governance/version")] -async fn governance_canister_version_endpoint() -> Result { - let network = target_network(); - let u = get_nns_url_vec_from_target_network(&network)[0].clone(); - let g = governance_canister_version(u).await; +async fn governance_canister_version_endpoint( + registry: web::Data>>, +) -> Result { + let registry = registry.read().await; + let g = governance_canister_version(®istry.network().get_nns_urls()).await; response_from_result(g) } diff --git a/rs/ic-management-backend/src/endpoints/mod.rs b/rs/ic-management-backend/src/endpoints/mod.rs index 3a318daab..3dd41a28a 100644 --- a/rs/ic-management-backend/src/endpoints/mod.rs +++ b/rs/ic-management-backend/src/endpoints/mod.rs @@ -5,8 +5,8 @@ pub mod release; pub mod subnet; use crate::{ - config::get_nns_url_vec_from_target_network, gitlab_dfinity, health, prometheus, proposal, registry, - registry::RegistryState, release::list_subnets_release_statuses, release::RolloutBuilder, + gitlab_dfinity, health, prometheus, proposal, registry, registry::RegistryState, + release::list_subnets_release_statuses, release::RolloutBuilder, }; use actix_web::dev::Service; use actix_web::{get, post, web, App, Error, HttpResponse, HttpServer, Responder, Result}; @@ -28,7 +28,7 @@ const GITLAB_TOKEN_RELEASE_ENV: &str = "GITLAB_API_TOKEN_RELEASE"; const GITLAB_API_TOKEN_FALLBACK: &str = "GITLAB_API_TOKEN"; pub async fn run_backend( - target_network: Network, + target_network: &Network, listen_ip: &str, listen_port: u16, run_from_cli: bool, @@ -36,7 +36,7 @@ pub async fn run_backend( ) -> std::io::Result<()> { debug!("Starting backend"); let registry_state = Arc::new(RwLock::new( - registry::RegistryState::new(target_network.clone(), run_from_cli).await, + registry::RegistryState::new(target_network, run_from_cli).await, )); if run_from_cli { @@ -63,8 +63,9 @@ pub async fn run_backend( let num_workers = if run_from_cli { 1 } else { 8 }; + let closure_target_network = target_network.clone(); let mut srv = HttpServer::new(move || { - let network = target_network.clone(); + let network = closure_target_network.clone(); // For `dre` cli invocations we don't need more than one worker let middleware_registry_state = registry_state.clone(); @@ -75,8 +76,8 @@ pub async fn run_backend( let registry_state = middleware_registry_state.clone(); let network = network.clone(); async move { - let nns_urls = get_nns_url_vec_from_target_network(&network); - let registry_canister = RegistryCanister::new(nns_urls.clone()); + let nns_urls = network.get_nns_urls().clone(); + let registry_canister = RegistryCanister::new(nns_urls); let registry_reader = registry_state.read().await; let registry_version = registry_reader.version(); if registry_canister @@ -180,7 +181,7 @@ async fn get_subnet( #[get("/rollout")] async fn rollout(registry: web::Data>>) -> Result { let registry = registry.read().await; - let proposal_agent = proposal::ProposalAgent::new(registry.nns_url()); + let proposal_agent = proposal::ProposalAgent::new(®istry.get_nns_urls()); let network = registry.network(); let prometheus_client = prometheus::client(&network); let service = RolloutBuilder { @@ -196,7 +197,7 @@ async fn rollout(registry: web::Data>>) -> R #[get("/subnets/versions")] async fn subnets_release(registry: web::Data>>) -> Result { let registry = registry.read().await; - let proposal_agent = proposal::ProposalAgent::new(registry.nns_url()); + let proposal_agent = proposal::ProposalAgent::new(®istry.get_nns_urls()); let network = registry.network(); let prometheus_client = prometheus::client(&network); response_from_result( diff --git a/rs/ic-management-backend/src/factsdb.rs b/rs/ic-management-backend/src/factsdb.rs index 39aede6ff..eaff7f458 100644 --- a/rs/ic-management-backend/src/factsdb.rs +++ b/rs/ic-management-backend/src/factsdb.rs @@ -3,7 +3,7 @@ use hyper::StatusCode; use ic_management_types::{FactsDBGuest, Guest}; use log::warn; -pub async fn query_guests(gitlab_client: AsyncGitlab, network: String) -> anyhow::Result> { +pub async fn query_guests(gitlab_client: AsyncGitlab, network: &String) -> anyhow::Result> { ::gitlab::api::raw( ::gitlab::api::projects::repository::files::FileRaw::builder() .ref_("refs/heads/main") diff --git a/rs/ic-management-backend/src/lib.rs b/rs/ic-management-backend/src/lib.rs index ae1e1e964..3e2cf6b8d 100644 --- a/rs/ic-management-backend/src/lib.rs +++ b/rs/ic-management-backend/src/lib.rs @@ -1,4 +1,3 @@ -pub mod config; pub mod endpoints; pub mod factsdb; pub mod git_ic_repo; diff --git a/rs/ic-management-backend/src/main.rs b/rs/ic-management-backend/src/main.rs index 0adebe674..28885819d 100644 --- a/rs/ic-management-backend/src/main.rs +++ b/rs/ic-management-backend/src/main.rs @@ -1,4 +1,3 @@ -mod config; mod endpoints; mod factsdb; mod git_ic_repo; @@ -13,24 +12,36 @@ mod subnets; use clap::Parser; use dotenv::dotenv; - -#[derive(Parser)] -struct Args {} +use url::Url; #[actix_web::main] async fn main() -> std::io::Result<()> { - let _args = Args::parse(); - dotenv().ok(); std::env::set_var("RUST_LOG", "info"); env_logger::init(); + let args = Cli::parse(); + let target_network = ic_management_types::Network::new(args.network.clone(), &args.nns_urls) + .await + .expect("Failed to create network"); - let target_network = config::target_network(); let listen_port = std::env::var("BACKEND_PORT") .map(|p| { p.parse() .expect("Unable to parse BACKEND_PORT environment variable as a valid port") }) .unwrap_or(8080); - endpoints::run_backend(target_network, "0.0.0.0", listen_port, false, None).await + endpoints::run_backend(&target_network, "0.0.0.0", listen_port, false, None).await +} + +#[derive(Parser, Debug)] +#[clap(about, version)] +struct Cli { + // Target network. Can be one of: "mainnet", "staging", or an arbitrary "" name + #[clap(long, env = "NETWORK", default_value = "mainnet")] + network: String, + + // NNS_URLs for the target network, comma separated. + // The argument is mandatory for testnets, and is optional for mainnet and staging + #[clap(long, env = "NNS_URLS", aliases = &["registry-url", "nns-url"], value_delimiter = ',')] + pub nns_urls: Vec, } diff --git a/rs/ic-management-backend/src/prometheus.rs b/rs/ic-management-backend/src/prometheus.rs index 0aba75134..b3aa24be6 100644 --- a/rs/ic-management-backend/src/prometheus.rs +++ b/rs/ic-management-backend/src/prometheus.rs @@ -2,8 +2,5 @@ use ic_management_types::Network; use prometheus_http_query::Client; pub fn client(network: &Network) -> Client { - match network { - Network::Mainnet => Client::try_from("https://victoria.mainnet.dfinity.network/select/0/prometheus/").unwrap(), - _ => Client::try_from("https://victoria.testnet.dfinity.network/select/0/prometheus").unwrap(), - } + Client::try_from(network.get_prometheus_endpoint().as_str()).unwrap() } diff --git a/rs/ic-management-backend/src/proposal.rs b/rs/ic-management-backend/src/proposal.rs index 033a89d3a..2496003d2 100644 --- a/rs/ic-management-backend/src/proposal.rs +++ b/rs/ic-management-backend/src/proposal.rs @@ -24,6 +24,7 @@ use registry_canister::mutations::do_update_subnet_replica::UpdateSubnetReplicaV use registry_canister::mutations::do_update_unassigned_nodes_config::UpdateUnassignedNodesConfigPayload; use registry_canister::mutations::node_management::do_remove_nodes::RemoveNodesPayload; use serde::Serialize; +use url::Url; #[derive(Clone)] pub struct ProposalAgent { @@ -82,9 +83,12 @@ pub struct UpdateUnassignedNodesProposal { } impl ProposalAgent { - pub fn new(url: String) -> Self { + pub fn new(nns_urls: &Vec) -> Self { let agent = Agent::builder() - .with_transport(ReqwestHttpReplicaV2Transport::create(url).expect("failed to create transport")) + .with_transport( + ReqwestHttpReplicaV2Transport::create(nns_urls[0].clone()).expect("failed to create transport"), + ) + .with_verify_query_signatures(false) .build() .expect("failed to build the agent"); Self { agent } diff --git a/rs/ic-management-backend/src/registry.rs b/rs/ic-management-backend/src/registry.rs index 60f864fce..9fb87855b 100644 --- a/rs/ic-management-backend/src/registry.rs +++ b/rs/ic-management-backend/src/registry.rs @@ -1,4 +1,3 @@ -use crate::config::get_nns_url_vec_from_target_network; use crate::factsdb; use crate::git_ic_repo::IcRepo; use crate::proposal::{self, SubnetUpdateProposal, UpdateUnassignedNodesProposal}; @@ -57,6 +56,7 @@ use std::{ }; use tokio::sync::RwLock; use tokio::time::{sleep, Duration}; +use url::Url; extern crate env_logger; use anyhow::Result; @@ -67,7 +67,6 @@ pub const NNS_SUBNET_NAME: &str = "NNS"; pub const DFINITY_DCS: &str = "zh2 mr1 bo1 sh1"; pub struct RegistryState { - nns_url: String, network: Network, local_registry: Arc, @@ -194,18 +193,14 @@ impl ReleasesOps for ArtifactReleases { } impl RegistryState { - pub async fn new(network: Network, without_update_loop: bool) -> Self { - let nns_url = network.get_url().to_string(); - - sync_local_store(network.clone()) - .await - .expect("failed to init local store"); + pub async fn new(network: &Network, without_update_loop: bool) -> Self { + sync_local_store(&network).await.expect("failed to init local store"); if !without_update_loop { let closure_network = network.clone(); tokio::spawn(async move { loop { - if let Err(e) = sync_local_store(closure_network.clone()).await { + if let Err(e) = sync_local_store(&closure_network).await { error!("Failed to update local registry: {}", e); } tokio::time::sleep(std::time::Duration::from_secs(5)).await; @@ -213,10 +208,10 @@ impl RegistryState { }); } - let local_registry_path = local_registry_path(network.clone()); + let local_registry_path = local_registry_path(&network.clone()); info!( "Using local registry path for network {}: {}", - network.to_string(), + network.name, local_registry_path.display() ); let local_registry: Arc = Arc::new( @@ -225,8 +220,7 @@ impl RegistryState { ); Self { - nns_url, - network, + network: network.clone(), local_registry, version: 0, subnets: BTreeMap::::new(), @@ -276,7 +270,7 @@ impl RegistryState { pub fn update_factsdb_guests(&mut self, factsdb_guests: Vec) { self.factsdb_guests = factsdb_guests; - if !matches!(self.network, Network::Mainnet) { + if self.network.name != "mainnet" { for g in &mut self.factsdb_guests { g.dfinity_owned = true; } @@ -657,7 +651,7 @@ impl RegistryState { pub async fn nodes_with_proposals(&self) -> Result> { let nodes = self.nodes.clone(); - let proposal_agent = proposal::ProposalAgent::new(self.nns_url.clone()); + let proposal_agent = proposal::ProposalAgent::new(&self.network.get_nns_urls()); let topology_proposals = proposal_agent.list_open_topology_proposals().await?; @@ -675,18 +669,18 @@ impl RegistryState { } pub async fn open_elect_replica_proposals(&self) -> Result> { - let proposal_agent = proposal::ProposalAgent::new(self.nns_url.clone()); + let proposal_agent = proposal::ProposalAgent::new(&self.network.get_nns_urls()); proposal_agent.list_open_elect_replica_proposals().await } pub async fn open_elect_hostos_proposals(&self) -> Result> { - let proposal_agent = proposal::ProposalAgent::new(self.nns_url.clone()); + let proposal_agent = proposal::ProposalAgent::new(&self.network.get_nns_urls()); proposal_agent.list_open_elect_hostos_proposals().await } pub async fn subnets_with_proposals(&self) -> Result> { let subnets = self.subnets.clone(); - let proposal_agent = proposal::ProposalAgent::new(self.nns_url.clone()); + let proposal_agent = proposal::ProposalAgent::new(&self.network.get_nns_urls()); let topology_proposals = proposal_agent.list_open_topology_proposals().await?; @@ -723,13 +717,13 @@ impl RegistryState { } pub async fn open_subnet_upgrade_proposals(&self) -> Result> { - let proposal_agent = proposal::ProposalAgent::new(self.nns_url.clone()); + let proposal_agent = proposal::ProposalAgent::new(self.get_nns_urls()); proposal_agent.list_update_subnet_version_proposals().await } pub async fn open_upgrade_unassigned_nodes_proposals(&self) -> Result> { - let proposal_agent = proposal::ProposalAgent::new(self.nns_url.clone()); + let proposal_agent = proposal::ProposalAgent::new(self.get_nns_urls()); proposal_agent.list_update_unassigned_nodes_version_proposals().await } @@ -833,8 +827,8 @@ impl RegistryState { self.replica_releases.releases.clone() } - pub fn nns_url(&self) -> String { - self.nns_url.clone() + pub fn get_nns_urls(&self) -> &Vec { + self.network.get_nns_urls() } pub async fn get_unassigned_nodes_replica_version(&self) -> Result { @@ -961,7 +955,7 @@ fn node_ip_addr(nr: &NodeRecord) -> Ipv6Addr { Ipv6Addr::from_str(&nr.http.clone().expect("missing ipv6 address").ip_addr).expect("invalid ipv6 address") } -pub fn local_registry_path(network: Network) -> PathBuf { +pub fn local_registry_path(network: &Network) -> PathBuf { match std::env::var("LOCAL_REGISTRY_PATH") { Ok(path) => PathBuf::from(path), Err(_) => match dirs::cache_dir() { @@ -970,7 +964,7 @@ pub fn local_registry_path(network: Network) -> PathBuf { }, } .join("ic-registry-cache") - .join(Path::new(network.to_string().as_str())) + .join(Path::new(network.name.as_str())) .join("local_registry") } @@ -998,10 +992,11 @@ pub async fn nns_public_key(registry_canister: &RegistryCanister) -> anyhow::Res } /// Sync all versions of the registry, up to the latest one. -pub async fn sync_local_store(target_network: Network) -> anyhow::Result<()> { - let local_registry_path = local_registry_path(target_network.clone()); +pub async fn sync_local_store(target_network: &Network) -> anyhow::Result<()> { + let local_registry_path = local_registry_path(target_network); let local_store = Arc::new(LocalStoreImpl::new(local_registry_path.clone())); - let registry_canister = RegistryCanister::new(get_nns_url_vec_from_target_network(&target_network)); + let nns_urls = target_network.get_nns_urls().clone(); + let registry_canister = RegistryCanister::new(nns_urls); let mut local_latest_version = if !Path::new(&local_registry_path).exists() { ZERO_REGISTRY_VERSION } else { @@ -1030,7 +1025,7 @@ pub async fn sync_local_store(target_network: Network) -> anyhow::Result<()> { Ordering::Greater => { warn!( "Removing faulty local copy of the registry for the IC network {}: {}", - target_network, + target_network.name, local_registry_path.display() ); std::fs::remove_dir_all(&local_registry_path)?; @@ -1131,7 +1126,8 @@ pub async fn poll( registry_state: Arc>, target_network: Network, ) { - let registry_canister = RegistryCanister::new(get_nns_url_vec_from_target_network(&target_network)); + let nns_urls = target_network.get_nns_urls().clone(); + let registry_canister = RegistryCanister::new(nns_urls); loop { sleep(Duration::from_secs(1)).await; let latest_version = if let Ok(v) = registry_canister.get_latest_version().await { @@ -1158,7 +1154,7 @@ async fn fetch_and_add_factsdb_guests_to_registry( target_network: &Network, registry_state: &Arc>, ) { - let guests_result = factsdb::query_guests(gitlab_client_release_repo.clone(), target_network.to_string()).await; + let guests_result = factsdb::query_guests(gitlab_client_release_repo.clone(), &target_network.name).await; match guests_result { Ok(factsdb_guests) => { diff --git a/rs/ic-management-types/Cargo.toml b/rs/ic-management-types/Cargo.toml index 08d636c0d..8c1fb95b3 100644 --- a/rs/ic-management-types/Cargo.toml +++ b/rs/ic-management-types/Cargo.toml @@ -9,6 +9,7 @@ documentation.workspace = true [dependencies] actix-web = { workspace = true } chrono = { workspace = true } +futures = { workspace = true } ic-base-types = { workspace = true } ic-nns-governance = { workspace = true } ic-registry-subnet-type = { workspace = true } @@ -19,9 +20,13 @@ serde = { workspace = true } serde_json = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } +tokio = { workspace = true } url = { workspace = true } anyhow = { workspace = true } candid = { workspace = true } +[dev-dependencies] +wiremock = { workspace = true } + [lib] path = "src/lib.rs" diff --git a/rs/ic-management-types/src/lib.rs b/rs/ic-management-types/src/lib.rs index 970e5c9d8..072ad2624 100644 --- a/rs/ic-management-types/src/lib.rs +++ b/rs/ic-management-types/src/lib.rs @@ -29,7 +29,7 @@ use std::net::Ipv6Addr; use std::ops::Deref; use std::str::FromStr; use strum::VariantNames; -use strum_macros::{Display, EnumString}; +use strum_macros::EnumString; use url::Url; pub trait NnsFunctionProposal: CandidType + serde::de::DeserializeOwned { @@ -244,7 +244,18 @@ pub struct Node { } #[derive( - Display, EnumString, VariantNames, Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize, Deserialize, Debug, + strum_macros::Display, + EnumString, + VariantNames, + Hash, + Eq, + PartialEq, + Ord, + PartialOrd, + Clone, + Serialize, + Deserialize, + Debug, )] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] @@ -430,7 +441,9 @@ pub enum Health { Unknown, } -#[derive(PartialOrd, Ord, Eq, PartialEq, EnumString, Serialize, Display, Deserialize, Debug, Clone, Hash)] +#[derive( + PartialOrd, Ord, Eq, PartialEq, EnumString, Serialize, strum_macros::Display, Deserialize, Debug, Clone, Hash, +)] pub enum Status { Healthy, Degraded, @@ -538,7 +551,7 @@ impl ArtifactReleases { } } -#[derive(Display, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] +#[derive(strum_macros::Display, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] #[strum(serialize_all = "lowercase")] #[serde(rename_all = "lowercase")] pub enum Artifact { @@ -561,53 +574,284 @@ impl Artifact { } } -#[derive(Clone, PartialEq, Eq, strum_macros::Display)] -#[strum(serialize_all = "lowercase")] -pub enum Network { - Staging, - Mainnet, - Url(url::Url), +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Network { + pub name: String, + pub nns_urls: Vec, } -impl Default for Network { - fn default() -> Self { - Network::Mainnet +impl Network { + pub async fn new>(name: S, nns_urls: &Vec) -> Result { + let (name, nns_urls) = match name.as_ref() { + "mainnet" => ( + "mainnet".to_string(), + if nns_urls.is_empty() { + vec![Url::from_str("https://ic0.app").unwrap()] + } else { + nns_urls.clone() + }, + ), + "staging" => ( + "staging".to_string(), + if nns_urls.is_empty() { + vec![Url::from_str("http://[2600:3000:6100:200:5000:b0ff:fe8e:6b7b]:8080").unwrap()] + } else { + nns_urls.clone() + }, + ), + _ => ( + name.as_ref().to_string(), + if nns_urls.is_empty() { + return Err("No NNS URLs provided".to_string()); + } else { + nns_urls.clone() + }, + ), + }; + let nns_urls = find_reachable_nns_urls(nns_urls).await; + if nns_urls.is_empty() { + return Err("No reachable NNS URLs provided".to_string()); + } + Ok(Network { name, nns_urls }) + } + + pub fn get_nns_urls(&self) -> &Vec { + &self.nns_urls + } + + pub fn get_nns_urls_string(&self) -> String { + self.nns_urls + .iter() + .map(|url| url.to_string()) + .collect::>() + .join(",") + } + + pub fn get_prometheus_endpoint(&self) -> Url { + match self.name.as_str() { + "mainnet" => "https://victoria.mainnet.dfinity.network/select/0/prometheus/", + _ => "https://victoria.testnet.dfinity.network/select/0/prometheus/", + } + .parse() + .expect("Couldn't parse url") + } + + pub fn legacy_name(&self) -> String { + match self.name.as_str() { + "mainnet" => "mercury".to_string(), + _ => self.name.clone(), + } } } -impl Debug for Network { +impl std::fmt::Display for Network { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self) + write!(f, "{} ({})", self.name, self.get_nns_urls_string()) } } -impl FromStr for Network { - type Err = String; +/// Utility function to convert a Url to a host:port string. +fn url_to_host_with_port(url: Url) -> String { + let host = url.host_str().unwrap_or(""); + let host = if host.contains(':') && !host.starts_with('[') && !host.ends_with(']') { + // Likely an IPv6 address, enclose in brackets + format!("[{}]", host) + } else { + // IPv4 or hostname + host.to_string() + }; + let port = url.port_or_known_default().unwrap_or(8080); - fn from_str(s: &str) -> Result { - Ok(match s { - "mainnet" => Self::Mainnet, - "staging" => Self::Staging, - _ => Self::Url(url::Url::from_str(s).map_err(|e| format!("{}", e))?), - }) - } + format!("{}:{}", host, port) } -impl Network { - pub fn get_url(&self) -> Url { - match self { - Network::Mainnet => Url::from_str("https://ic0.app").unwrap(), - // Workaround for staging boundary node not working properly (503 Service unavailable) - Network::Staging => Url::from_str("http://[2600:3000:6100:200:5000:b0ff:fe8e:6b7b]:8080").unwrap(), - Self::Url(url) => url.clone(), - } +/// Utility function to find NNS URLs that the local machine can connect to. +async fn find_reachable_nns_urls(nns_urls: Vec) -> Vec { + // Early return, otherwise `futures::future::select_all` will panic without a good error + // message. + if nns_urls.is_empty() { + return Vec::new(); } - pub fn legacy_name(&self) -> String { - match self { - Network::Mainnet => "mercury".to_string(), - Network::Staging => "staging".to_string(), - Self::Url(url) => format!("testnet-{url}"), + let retries_max = 3; + let timeout_duration = tokio::time::Duration::from_secs(10); + + for i in 1..=retries_max { + let tasks: Vec<_> = nns_urls + .iter() + .map(|url| { + Box::pin(async move { + let host_with_port = url_to_host_with_port(url.clone()); + + match tokio::net::lookup_host(host_with_port.clone()).await { + Ok(ips) => { + for ip in ips { + match tokio::time::timeout(timeout_duration, tokio::net::TcpStream::connect(ip)).await { + Ok(connection) => match connection { + Ok(_) => return Some(url.clone()), + Err(err) => { + eprintln!("WARNING: Failed to connect to {}: {:?}", ip, err); + } + }, + Err(err) => { + eprintln!("WARNING: Failed to connect to {}: {:?}", ip, err); + } + } + } + } + Err(err) => { + eprintln!("WARNING: Failed to lookup {}: {:?}", host_with_port, err); + } + } + None + }) + }) + .collect(); + + // Wait for the first task to complete ==> until we have a reachable NNS URL. + // select_all returns the completed future at position 0, and the remaining futures at position 2. + let (completed_task, _, remaining_tasks) = futures::future::select_all(tasks).await; + match completed_task { + Some(url) => return vec![url], + None => { + for task in remaining_tasks { + if let Some(url) = task.await { + return vec![url]; + } + } + eprintln!( + "WARNING: None of the provided NNS urls are reachable. Retrying in 5 seconds... ({}/{})", + i, retries_max + ); + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + } } } + + Vec::new() +} + +#[cfg(test)] +mod tests { + use super::*; + use wiremock::MockServer; + + #[tokio::test] + async fn test_network_new_mainnet() { + let network = Network::new("mainnet", &vec![]).await.unwrap(); + + assert_eq!(network.name, "mainnet"); + assert_eq!(network.get_nns_urls(), &vec![Url::from_str("https://ic0.app").unwrap()]); + } + + #[tokio::test] + async fn test_network_new_mainnet_custom_url() { + let mock_server = MockServer::start().await; + let mock_server_url: Url = mock_server.uri().parse().unwrap(); + let network = Network::new("mainnet", &vec![mock_server_url.clone()]).await.unwrap(); + + assert_eq!(network.name, "mainnet"); + assert_eq!(network.get_nns_urls(), &vec![mock_server_url]); + } + + #[tokio::test] + async fn test_network_new_mainnet_custom_and_invalid_url() { + let mock_server = MockServer::start().await; + let mock_server_url: Url = mock_server.uri().parse().unwrap(); + let invalid_url1 = Url::from_str("https://unreachable.url1").unwrap(); + let invalid_url2 = Url::from_str("https://unreachable.url2").unwrap(); + + let expected_nns_urls = vec![mock_server_url.clone()]; + + // Test with the invalid URL last + let network = Network::new("mainnet", &vec![mock_server_url.clone(), invalid_url1.clone()]) + .await + .unwrap(); + + assert_eq!(network.name, "mainnet"); + assert_eq!(network.get_nns_urls(), &expected_nns_urls); + + // Test with the invalid URL first + let network = Network::new("mainnet", &vec![invalid_url1.clone(), mock_server_url.clone()]) + .await + .unwrap(); + + assert_eq!(network.name, "mainnet"); + assert_eq!(network.get_nns_urls(), &expected_nns_urls); + + // Test with the valid URL in the middle + let network = Network::new("mainnet", &vec![invalid_url1, mock_server_url.clone(), invalid_url2]) + .await + .unwrap(); + + assert_eq!(network.name, "mainnet"); + assert_eq!(network.get_nns_urls(), &expected_nns_urls); + } + + #[ignore] // Ignore failures since staging IC is not accessible from GitHub actions + #[tokio::test] + async fn test_network_new_staging() { + let network = Network::new("staging", &vec![]).await.unwrap(); + + assert_eq!(network.name, "staging"); + assert_eq!( + network.get_nns_urls(), + &vec![Url::from_str("http://[2600:3000:6100:200:5000:b0ff:fe8e:6b7b]:8080").unwrap()] + ); + } + + #[tokio::test] + async fn test_network_new_all_unreachable() { + let name = "custom"; + let nns_urls = vec![Url::from_str("https://unreachable.url").unwrap()]; + let network = Network::new(name, &nns_urls).await; + + assert_eq!(network, Err("No reachable NNS URLs provided".to_string())); + } + + #[test] + fn test_network_get_nns_urls_string() { + let nns_urls = vec![ + Url::from_str("https://ic0.app").unwrap(), + Url::from_str("https://custom.nns").unwrap(), + ]; + let network = Network { + name: "mainnet".to_string(), + nns_urls, + }; + + assert_eq!(network.get_nns_urls_string(), "https://ic0.app/,https://custom.nns/"); + } + + #[test] + fn test_network_get_prometheus_endpoint() { + let network = Network { + name: "mainnet".to_string(), + nns_urls: vec![], + }; + + assert_eq!( + network.get_prometheus_endpoint(), + Url::parse("https://victoria.mainnet.dfinity.network/select/0/prometheus/").unwrap() + ); + + let network = Network { + name: "some_testnet".to_string(), + nns_urls: vec![], + }; + assert_eq!( + network.get_prometheus_endpoint(), + Url::parse("https://victoria.testnet.dfinity.network/select/0/prometheus/").unwrap() + ); + } + + #[test] + fn test_network_legacy_name() { + let network = Network { + name: "mainnet".to_string(), + nns_urls: vec![], + }; + + assert_eq!(network.legacy_name(), "mercury"); + } } diff --git a/rs/ic-observability/multiservice-discovery/src/definition.rs b/rs/ic-observability/multiservice-discovery/src/definition.rs index ca0286016..155943fbc 100644 --- a/rs/ic-observability/multiservice-discovery/src/definition.rs +++ b/rs/ic-observability/multiservice-discovery/src/definition.rs @@ -1,7 +1,6 @@ use crossbeam_channel::Receiver; use crossbeam_channel::Sender; use futures_util::future::join_all; -use ic_management_types::Network; use ic_registry_client::client::ThresholdSigPublicKey; use serde::Deserialize; use serde::Serialize; @@ -85,7 +84,10 @@ impl From for Definition { public_key: fs_definition.public_key, poll_interval: fs_definition.poll_interval, registry_query_timeout: fs_definition.registry_query_timeout.clone(), - ic_discovery: Arc::new(IcServiceDiscoveryImpl::new(log, fs_definition.registry_path, fs_definition.registry_query_timeout).unwrap()), + ic_discovery: Arc::new( + IcServiceDiscoveryImpl::new(log, fs_definition.registry_path, fs_definition.registry_query_timeout) + .unwrap(), + ), boundary_nodes: vec![], } } @@ -282,7 +284,7 @@ impl RunningDefinition { let r = sync_local_registry( self.definition.log.clone(), self.definition.registry_path.join("targets"), - self.definition.nns_urls.clone(), + &self.definition.nns_urls.clone(), use_current_version, self.definition.public_key, &self.stop_signal, @@ -570,12 +572,12 @@ impl DefinitionsSupervisor { } if !self.allow_mercury_deletion - && !ic_names_to_add.contains(&Network::Mainnet.legacy_name()) + && !ic_names_to_add.contains("mercury") && start_mode == StartMode::ReplaceExistingDefinitions { error .errors - .push(StartDefinitionError::DeletionDisallowed(Network::Mainnet.legacy_name())) + .push(StartDefinitionError::DeletionDisallowed("mercury".to_string())) } if !error.errors.is_empty() { @@ -654,7 +656,7 @@ impl DefinitionsSupervisor { errors.extend( definition_names .iter() - .filter(|n| **n == Network::Mainnet.legacy_name() && !self.allow_mercury_deletion) + .filter(|n| *n == "mercury" && !self.allow_mercury_deletion) .map(|n| StopDefinitionError::DeletionDisallowed(n.clone())), ); if !errors.is_empty() { diff --git a/rs/ic-observability/multiservice-discovery/src/main.rs b/rs/ic-observability/multiservice-discovery/src/main.rs index 57b6725cc..9c2bc3149 100644 --- a/rs/ic-observability/multiservice-discovery/src/main.rs +++ b/rs/ic-observability/multiservice-discovery/src/main.rs @@ -13,7 +13,6 @@ use url::Url; use definition::{Definition, DefinitionsSupervisor, StartMode}; use ic_async_utils::shutdown_signal; -use ic_management_types::Network; use crate::definition::{RunningDefinition, TestDefinition}; use crate::metrics::{MSDMetrics, RunningDefinitionsMetrics}; @@ -34,7 +33,7 @@ fn main() { Definition::new( vec![cli_args.nns_url.clone()], cli_args.targets_dir.clone(), - Network::Mainnet.legacy_name(), + "mercury".to_string(), log.clone(), None, cli_args.poll_interval, diff --git a/rs/ic-observability/node-status-updater/src/main.rs b/rs/ic-observability/node-status-updater/src/main.rs index 6607ea310..7b8ba0beb 100644 --- a/rs/ic-observability/node-status-updater/src/main.rs +++ b/rs/ic-observability/node-status-updater/src/main.rs @@ -39,7 +39,7 @@ fn main() -> Result<()> { .block_on(sync_local_registry( log.clone(), mercury_target_dir, - nns_url, + &nns_url, cli_args.skip_sync, None, &stop_signal_rcv, diff --git a/rs/ic-observability/obs-canister-clients/src/node_status_canister_client.rs b/rs/ic-observability/obs-canister-clients/src/node_status_canister_client.rs index 6e9dfa128..0ee996e09 100644 --- a/rs/ic-observability/obs-canister-clients/src/node_status_canister_client.rs +++ b/rs/ic-observability/obs-canister-clients/src/node_status_canister_client.rs @@ -51,6 +51,7 @@ impl NodeStatusCanister { Agent::builder() .with_url(url.as_str()) .with_identity(AnonymousIdentity) + .with_verify_query_signatures(false) .build() .unwrap() }) diff --git a/rs/ic-observability/prometheus-config-updater/src/main.rs b/rs/ic-observability/prometheus-config-updater/src/main.rs index 950562f95..14e5cf39e 100644 --- a/rs/ic-observability/prometheus-config-updater/src/main.rs +++ b/rs/ic-observability/prometheus-config-updater/src/main.rs @@ -61,7 +61,7 @@ fn main() -> Result<()> { .block_on(sync_local_registry( log.clone(), mercury_target_dir, - nns_urls, + &nns_urls, cli_args.skip_sync, public_key, &stop_signal_rcv, diff --git a/rs/ic-observability/service-discovery/src/registry_sync.rs b/rs/ic-observability/service-discovery/src/registry_sync.rs index 704f79f1a..280bbb550 100644 --- a/rs/ic-observability/service-discovery/src/registry_sync.rs +++ b/rs/ic-observability/service-discovery/src/registry_sync.rs @@ -41,14 +41,14 @@ impl Display for SyncError { pub async fn sync_local_registry( log: Logger, local_path: PathBuf, - nns_urls: Vec, + nns_urls: &Vec, use_current_version: bool, public_key: Option, stop_signal: &Receiver<()>, ) -> Result<(), SyncError> { let start = Instant::now(); let local_store = Arc::new(LocalStoreImpl::new(local_path.clone())); - let registry_canister = RegistryCanister::new(nns_urls); + let registry_canister = RegistryCanister::new(nns_urls.to_vec()); let mut latest_version = if !Path::new(&local_path).exists() { ZERO_REGISTRY_VERSION diff --git a/rs/np-notifications/Cargo.toml b/rs/np-notifications/Cargo.toml index 10d3b674d..34983ac9e 100644 --- a/rs/np-notifications/Cargo.toml +++ b/rs/np-notifications/Cargo.toml @@ -11,6 +11,7 @@ documentation.workspace = true [dependencies] actix-web = { workspace = true } anyhow = { workspace = true } +clap = { workspace = true } ic-management-backend = { path = "../ic-management-backend" } ic-management-types = { path = "../ic-management-types" } ic-types = { workspace = true } @@ -25,7 +26,6 @@ tracing = { version = "0.1.37", features = ["log"] } tracing-log = { version = "0.2.0", features = ["log-tracer"] } tracing-subscriber = "0.3.17" url = { workspace = true } -clap = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/rs/np-notifications/src/health_check.rs b/rs/np-notifications/src/health_check.rs index 8206abf98..80e8b9fe1 100644 --- a/rs/np-notifications/src/health_check.rs +++ b/rs/np-notifications/src/health_check.rs @@ -27,7 +27,10 @@ impl std::fmt::Debug for HealthCheckLoopConfig { #[tracing::instrument] pub async fn start_health_check_loop(config: HealthCheckLoopConfig) { info!("Starting health check loop"); - let hc = HealthClient::new(ic_management_types::Network::Mainnet); + let network = ic_management_types::Network::new("mainnet", &vec![]) + .await + .expect("failed to create mainnet network"); + let hc = HealthClient::new(network); let mut nodes_status = NodesStatus::from(hc.nodes().await.unwrap()); let mut rs = config.registry_state; diff --git a/rs/np-notifications/src/main.rs b/rs/np-notifications/src/main.rs index 749657f53..3d2fbc4cf 100644 --- a/rs/np-notifications/src/main.rs +++ b/rs/np-notifications/src/main.rs @@ -29,8 +29,8 @@ use std::sync::mpsc; use actix_web::rt::signal; use actix_web::{web, App, HttpServer}; +use clap::Parser; use health_check::HealthCheckLoopConfig; -use ic_management_backend::config::target_network; use notification::NotificationSenderLoopConfig; @@ -38,6 +38,7 @@ use tokio_util::sync::CancellationToken; use tracing::{debug, info, Level}; use tracing_log::LogTracer; use tracing_subscriber::FmtSubscriber; +use url::Url; use crate::health_check::start_health_check_loop; use crate::notification::start_notification_sender_loop; @@ -45,7 +46,6 @@ use crate::registry::{start_registry_updater_loop, RegistryLoopConfig}; use crate::router::Router; use crate::service_health::ServiceHealth; use crate::sink::{LogSink, Sink}; -use clap::Parser; mod health_check; mod nodes_status; @@ -55,12 +55,12 @@ mod router; mod service_health; mod sink; -#[derive(Parser)] -struct Args {} - #[actix_web::main] async fn main() { - let _args = Args::parse(); + let cli_opts = Cli::parse(); + let target_network = ic_management_types::Network::new(cli_opts.network.clone(), &cli_opts.nns_urls) + .await + .expect("Failed to create network"); let subscriber = FmtSubscriber::builder().with_max_level(Level::INFO).compact().finish(); LogTracer::builder().init().expect("can create a log tracer"); @@ -76,14 +76,14 @@ async fn main() { actix_web::rt::spawn(start_registry_updater_loop(RegistryLoopConfig { cancellation_token: cancellation_token.clone(), - target_network: target_network(), + target_network: target_network.clone(), service_health: service_health.clone(), })); actix_web::rt::spawn(start_health_check_loop(HealthCheckLoopConfig { notification_sender: notif_sender.clone(), cancellation_token: cancellation_token.clone(), - registry_state: registry::create_registry_state().await, + registry_state: registry::create_registry_state(target_network).await, service_health: service_health.clone(), })); @@ -126,3 +126,16 @@ async fn main() { debug!("Stopping server"); srv_handle.stop(true).await } + +#[derive(Parser, Debug)] +#[clap(about, version)] +struct Cli { + // Target network. Can be one of: "mainnet", "staging", or an arbitrary "" name + #[clap(long, env = "NETWORK", default_value = "mainnet")] + network: String, + + // NNS_URLs for the target network, comma separated. + // The argument is mandatory for testnets, and is optional for mainnet and staging + #[clap(long, env = "NNS_URLS", aliases = &["registry-url", "nns-url"], value_delimiter = ',')] + pub nns_urls: Vec, +} diff --git a/rs/np-notifications/src/registry.rs b/rs/np-notifications/src/registry.rs index fcd39d9b3..296e6a19e 100644 --- a/rs/np-notifications/src/registry.rs +++ b/rs/np-notifications/src/registry.rs @@ -32,18 +32,17 @@ pub async fn start_registry_updater_loop(config: RegistryLoopConfig) { break; } config.service_health.set_registry_updater_loop_readiness(true); - if let Err(e) = registry::sync_local_store(config.target_network.clone()).await { + if let Err(e) = registry::sync_local_store(&config.target_network).await { error!(message = "Failed to update local registry", error = e.to_string()); } tokio::time::sleep(std::time::Duration::from_secs(5)).await; } } -pub async fn create_registry_state() -> RegistryState { - let target_network = ic_management_backend::config::target_network(); - ic_management_backend::registry::sync_local_store(target_network.clone()) +pub async fn create_registry_state(target_network: Network) -> RegistryState { + ic_management_backend::registry::sync_local_store(&target_network) .await .expect("failed to init local store"); - RegistryState::new(ic_management_types::Network::Mainnet, true).await + RegistryState::new(&target_network, true).await } diff --git a/rs/rollout-controller/src/actions/mod.rs b/rs/rollout-controller/src/actions/mod.rs index bdd66f3df..61bba7c49 100644 --- a/rs/rollout-controller/src/actions/mod.rs +++ b/rs/rollout-controller/src/actions/mod.rs @@ -1,7 +1,7 @@ use std::time::Duration; use dre::{ - cli::Opts, + detect_neuron::Neuron, ic_admin::{IcAdminWrapper, ProposeCommand, ProposeOptions}, }; use ic_base_types::PrincipalId; @@ -107,7 +107,9 @@ impl<'a> SubnetAction { ..Default::default() }; - executor.ic_admin.propose_run(proposal, opts, executor.simulate)?; + executor + .ic_admin_wrapper + .propose_run(proposal, opts, executor.simulate)?; } Ok(()) @@ -115,7 +117,7 @@ impl<'a> SubnetAction { } pub struct ActionExecutor<'a> { - ic_admin: IcAdminWrapper, + ic_admin_wrapper: IcAdminWrapper, simulate: bool, logger: Option<&'a Logger>, } @@ -128,36 +130,18 @@ impl<'a> ActionExecutor<'a> { simulate: bool, logger: Option<&'a Logger>, ) -> anyhow::Result { + let neuron = Neuron::new(&network, true, Some(neuron_id), Some(private_key_pem), None, None, None).await?; Ok(Self { - ic_admin: dre::cli::Cli::from_opts( - &Opts { - neuron_id: Some(neuron_id), - private_key_pem: Some(private_key_pem), - yes: true, - network, - ..Default::default() - }, - true, - ) - .await? - .into(), + ic_admin_wrapper: IcAdminWrapper::new(network, None, true, neuron), simulate, logger, }) } pub async fn test(network: Network, logger: Option<&'a Logger>) -> anyhow::Result { + let neuron = Neuron::new(&network, false, None, None, None, None, None).await?; Ok(Self { - ic_admin: dre::cli::Cli::from_opts( - &Opts { - yes: true, - network, - ..Default::default() - }, - false, - ) - .await? - .into(), + ic_admin_wrapper: IcAdminWrapper::new(network, None, true, neuron), simulate: true, logger, }) diff --git a/rs/rollout-controller/src/main.rs b/rs/rollout-controller/src/main.rs index c018190f0..eb20db589 100644 --- a/rs/rollout-controller/src/main.rs +++ b/rs/rollout-controller/src/main.rs @@ -1,9 +1,8 @@ -use std::{path::PathBuf, str::FromStr, time::Duration}; +use std::{path::PathBuf, time::Duration}; use clap::{Parser, Subcommand}; use fetching::{curl_fetcher::CurlFetcherConfig, sparse_checkout_fetcher::SparseCheckoutFetcherConfig}; use humantime::parse_duration; -use ic_management_types::Network; use prometheus_http_query::Client; use slog::{info, o, warn, Drain, Level, Logger}; use tokio::select; @@ -20,17 +19,11 @@ mod registry_wrappers; #[tokio::main] async fn main() -> anyhow::Result<()> { let args = Cli::parse(); + let target_network = ic_management_types::Network::new(args.network.clone(), &args.nns_urls) + .await + .expect("Failed to create network"); let logger = make_logger(args.log_level.clone().into()); - let prometheus_endpoint = match &args.network { - Network::Mainnet => Url::from_str("https://victoria.ch1-obs1.dfinity.network") - .map_err(|e| anyhow::anyhow!("Couldn't parse url: {:?}", e))?, - Network::Staging => Url::from_str("https://victoria.ch1-obsstage1.dfinity.network") - .map_err(|e| anyhow::anyhow!("Couldn't parse url: {:?}", e))?, - Network::Url(url) => url.clone(), - }; - let prometheus_endpoint = prometheus_endpoint - .join("select/0/prometheus") - .map_err(|e| anyhow::anyhow!("Couldn't append victoria prometheus endpoint: {:?}", e))?; + let prometheus_endpoint = target_network.get_prometheus_endpoint(); let client = Client::try_from(prometheus_endpoint.to_string()) .map_err(|e| anyhow::anyhow!("Couldn't create prometheus client: {:?}", e))?; @@ -52,8 +45,8 @@ async fn main() -> anyhow::Result<()> { let fetcher = fetching::resolve(args.subcommand, logger.clone()).await?; let executor = match args.private_key_pem { - Some(path) => ActionExecutor::new(args.neuron_id, path, args.network.clone(), false, Some(&logger)).await?, - None => ActionExecutor::test(args.network.clone(), Some(&logger)).await?, + Some(path) => ActionExecutor::new(args.neuron_id, path, target_network.clone(), false, Some(&logger)).await?, + None => ActionExecutor::test(target_network.clone(), Some(&logger)).await?, }; let mut interval = tokio::time::interval(args.poll_interval); @@ -69,9 +62,9 @@ async fn main() -> anyhow::Result<()> { } should_sleep = true; - info!(logger, "Syncing registry for network '{:?}'", args.network); + info!(logger, "Syncing registry for network '{}'", target_network); let maybe_registry_state = select! { - res = sync_wrap(logger.clone(), args.targets_dir.clone(), args.network.clone()) => res, + res = sync_wrap(logger.clone(), args.targets_dir.clone(), target_network.clone()) => res, _ = token.cancelled() => break, }; let registry_state = match maybe_registry_state { @@ -160,19 +153,14 @@ instances are stored. )] targets_dir: PathBuf, - #[clap( - long, - default_value = "mainnet", - help = r#" -Target network to observe and update with the controller. -Can be one of: - 1. mainnet, - 2. staging, - 3. arbitrary nns url + // Target network. Can be one of: "mainnet", "staging", or an arbitrary "" name + #[clap(long, env = "NETWORK", default_value = "mainnet")] + network: String, -"# - )] - network: Network, + // NNS_URLs for the target network, comma separated. + // The argument is mandatory for testnets, and is optional for mainnet and staging + #[clap(long, env = "NNS_URLS", aliases = &["registry-url", "nns-url"], value_delimiter = ',')] + pub nns_urls: Vec, #[clap( long, @@ -191,7 +179,7 @@ Log level to use for running. You can use standard log levels 'info', value_parser = parse_duration, help = r#" The interval at which ICs are polled for updates. - + "# )] poll_interval: Duration, diff --git a/rs/rollout-controller/src/registry_wrappers.rs b/rs/rollout-controller/src/registry_wrappers.rs index 1a10bde65..0b2cde166 100644 --- a/rs/rollout-controller/src/registry_wrappers.rs +++ b/rs/rollout-controller/src/registry_wrappers.rs @@ -11,7 +11,7 @@ pub async fn sync_wrap(logger: Logger, targets_dir: PathBuf, network: Network) - sync_local_registry( logger.clone(), targets_dir, - vec![network.get_url()], + network.get_nns_urls(), false, None, &stop_signal, @@ -21,7 +21,7 @@ pub async fn sync_wrap(logger: Logger, targets_dir: PathBuf, network: Network) - // Check if the desired rollout version is elected debug!(logger, "Creating registry"); - let mut registry_state = RegistryState::new(network.clone(), true).await; + let mut registry_state = RegistryState::new(&network, true).await; debug!(logger, "Updating registry with data"); let node_provider_data = vec![]; diff --git a/rs/slack-notifications/BUILD.bazel b/rs/slack-notifications/BUILD.bazel index 5d72be245..748de94f1 100644 --- a/rs/slack-notifications/BUILD.bazel +++ b/rs/slack-notifications/BUILD.bazel @@ -4,7 +4,9 @@ load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test") package(default_visibility = ["//visibility:public"]) -DEPS = [] +DEPS = [ + "//rs/ic-management-types", +] rust_binary( name = "slack-notifications", diff --git a/rs/slack-notifications/Cargo.toml b/rs/slack-notifications/Cargo.toml index 091f1441e..00daf8b60 100644 --- a/rs/slack-notifications/Cargo.toml +++ b/rs/slack-notifications/Cargo.toml @@ -15,6 +15,7 @@ dotenv = { workspace = true } env_logger = { workspace = true } futures = { workspace = true } ic-agent = { workspace = true } +ic-management-types = { workspace = true } ic-nns-common = { workspace = true } ic-nns-constants = { workspace = true } ic-nns-governance = { workspace = true } diff --git a/rs/slack-notifications/src/main.rs b/rs/slack-notifications/src/main.rs index 73ccf64d1..3490a76ce 100644 --- a/rs/slack-notifications/src/main.rs +++ b/rs/slack-notifications/src/main.rs @@ -1,3 +1,4 @@ +use ic_management_types::Network; use ic_nns_governance::pb::v1::{ListProposalInfo, ListProposalInfoResponse, ProposalInfo, ProposalStatus}; use anyhow::Result; @@ -12,16 +13,11 @@ use std::time::SystemTime; use tokio::time::{sleep, Duration}; mod slack; use clap::Parser; +use reqwest::Url; #[macro_use] extern crate lazy_static; -#[derive(Deserialize)] -struct Config {} - -#[derive(Parser)] -struct Args {} - // Time to wait for a new proposal after the last one was created before sending // out the Slack notification. const COOLING_PERIOD_SECS: u64 = 60; @@ -30,17 +26,34 @@ const SLACK_URL_ENV: &str = "SLACK_URL"; #[tokio::main] async fn main() { - let _args = Args::parse(); std::env::set_var("RUST_LOG", "info"); env_logger::init(); dotenv::dotenv().ok(); - let failed_proposals_handle = tokio::spawn(notify_for_failed_proposals()); - let new_proposals_handle = tokio::spawn(notify_for_new_proposals()); + let args = Cli::parse(); + let target_network = ic_management_types::Network::new(args.network.clone(), &args.nns_urls) + .await + .expect("Failed to create network"); + + let failed_proposals_handle = tokio::spawn(notify_for_failed_proposals(target_network.clone())); + let new_proposals_handle = tokio::spawn(notify_for_new_proposals(target_network)); futures::future::join_all(vec![failed_proposals_handle, new_proposals_handle]).await; } +#[derive(Parser, Debug)] +#[clap(about, version)] +struct Cli { + // Target network. Can be one of: "mainnet", "staging", or an arbitrary "" name + #[clap(long, env = "NETWORK", default_value = "mainnet")] + network: String, + + // NNS_URLs for the target network, comma separated. + // The argument is mandatory for testnets, and is optional for mainnet and staging + #[clap(long, env = "NNS_URLS", aliases = &["registry-url", "nns-url"], value_delimiter = ',')] + pub nns_urls: Vec, +} + #[derive(Default)] pub struct ProposalCheckpointStore { file_path: String, @@ -96,11 +109,10 @@ struct ProposalPoller { } impl ProposalPoller { - fn new() -> Self { + fn new(target_network: Network) -> Self { + let nns_url = target_network.get_nns_urls()[0].clone(); let agent = Agent::builder() - .with_transport( - ReqwestHttpReplicaV2Transport::create("https://ic0.app").expect("failed to create transport"), - ) + .with_transport(ReqwestHttpReplicaV2Transport::create(nns_url).expect("failed to create transport")) .build() .expect("failed to build the agent"); Self { agent } @@ -146,10 +158,10 @@ impl ProposalPoller { } } -async fn notify_for_new_proposals() { +async fn notify_for_new_proposals(target_network: Network) { let mut last_notified_proposal = ProposalCheckpointStore::new("new").expect("failed to initialize last notified proposal tracking"); - let proposal_poller = ProposalPoller::new(); + let proposal_poller = ProposalPoller::new(target_network); loop { info!("sleeping"); sleep(Duration::from_secs(10)).await; @@ -224,10 +236,10 @@ async fn notify_for_new_proposals() { } } -async fn notify_for_failed_proposals() { +async fn notify_for_failed_proposals(target_network: Network) { let mut checkpoint = ProposalCheckpointStore::new("failed").expect("failed to initialize last notified proposal tracking"); - let proposal_poller = ProposalPoller::new(); + let proposal_poller = ProposalPoller::new(target_network); loop { info!("checking for failed proposals"); if let Ok(mut proposals) = proposal_poller.poll_not_executed_once().await {