diff --git a/Cargo.toml b/Cargo.toml index f447260417509377666fb4a95b00ef2cde071b93..f873155c44d93df91b9dcc9811c6b7e151a5088b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,3 +52,4 @@ configuration = { package = "zombienet-configuration", version = "0.1.0-alpha.0" orchestrator = { package = "zombienet-orchestrator", version = "0.1.0-alpha.0", path = "crates/orchestrator" } provider = { package = "zombienet-provider", version = "0.1.0-alpha.0", path = "crates/provider" } prom-metrics-parser = { package = "zombienet-prom-metrics-parser", version = "0.1.0-alpha.0", path = "crates/prom-metrics-parser" } +sdk = { package = "zombienet-sdk", version = "0.1.0-alpha.0", path = "crates/sdk" } diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index ca65ee13c0e7f78b08b22caf9391454464cb23b9..0dd7f343b211fc1cae31666735c50c75406f156c 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -10,3 +10,4 @@ zombienet-sdk = { path = "../sdk" } tokio = { workspace = true } futures = { workspace = true } subxt = { workspace = true } +tracing-subscriber = "0.3" diff --git a/crates/examples/examples/simple_network_example.rs b/crates/examples/examples/simple_network_example.rs index 5464c990d3ee86bfcd20751da31a50094adf5ae8..0e6cf9306b70340966eb082d1946e06d8c99dde8 100644 --- a/crates/examples/examples/simple_network_example.rs +++ b/crates/examples/examples/simple_network_example.rs @@ -3,6 +3,7 @@ use zombienet_sdk::{NetworkConfig, NetworkConfigExt}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { + tracing_subscriber::fmt::init(); let network = NetworkConfig::load_from_toml("./crates/examples/examples/0001-simple.toml") .expect("errored?") .spawn_native() diff --git a/crates/examples/examples/small_network_with_default.rs b/crates/examples/examples/small_network_with_default.rs index 5a32d764f1074b3ffcde43c9dc330fd13c6eb239..93fdeac141333b7249f693f7bdbfd03114a77c88 100644 --- a/crates/examples/examples/small_network_with_default.rs +++ b/crates/examples/examples/small_network_with_default.rs @@ -1,7 +1,8 @@ -use zombienet_sdk::{AddNodeOpts, NetworkConfigBuilder, NetworkConfigExt}; +use zombienet_sdk::{AddCollatorOptions, AddNodeOptions, NetworkConfigBuilder, NetworkConfigExt}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { + tracing_subscriber::fmt::init(); let mut network = NetworkConfigBuilder::new() .with_relaychain(|r| { r.with_chain("rococo-local") @@ -20,15 +21,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { .await?; println!("🚀🚀🚀🚀 network deployed"); - // add a new node - let opts = AddNodeOpts { + + // Add a new node to the running network. + let opts = AddNodeOptions { rpc_port: Some(9444), is_validator: true, ..Default::default() }; - // TODO: add check to ensure if unique - network.add_node("new1", opts, None).await?; + network.add_node("new1", opts).await?; // Example of some operations that you can do // with `nodes` (e.g pause, resume, restart) @@ -49,11 +50,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { // node.resume().await?; // println!("node new1 resumed!"); - let col_opts = AddNodeOpts { + let col_opts = AddCollatorOptions { command: Some("polkadot-parachain".try_into()?), ..Default::default() }; - network.add_node("new-col-1", col_opts, Some(100)).await?; + network.add_collator("new-col-1", col_opts, 100).await?; println!("new collator deployed!"); // For now let just loop.... diff --git a/crates/examples/examples/small_network_with_para.rs b/crates/examples/examples/small_network_with_para.rs index d11b8a36cb2eeff85f364146c4e851ea25a54af4..5c332728d71bed9eb38c47f66fb7126d77edfb86 100644 --- a/crates/examples/examples/small_network_with_para.rs +++ b/crates/examples/examples/small_network_with_para.rs @@ -1,9 +1,13 @@ use std::time::Duration; -use zombienet_sdk::{AddNodeOpts, NetworkConfigBuilder, NetworkConfigExt, RegistrationStrategy}; +use zombienet_sdk::{ + AddCollatorOptions, AddNodeOptions, NetworkConfigBuilder, NetworkConfigExt, + RegistrationStrategy, +}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { + tracing_subscriber::fmt::init(); let mut network = NetworkConfigBuilder::new() .with_relaychain(|r| { r.with_chain("rococo-local") @@ -24,14 +28,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { println!("🚀🚀🚀🚀 network deployed"); // add a new node - let opts = AddNodeOpts { + let opts = AddNodeOptions { rpc_port: Some(9444), is_validator: true, ..Default::default() }; // TODO: add check to ensure if unique - network.add_node("new1", opts, None).await?; + network.add_node("new1", opts).await?; tokio::time::sleep(Duration::from_secs(2)).await; @@ -58,11 +62,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { // node.resume().await?; // println!("node new1 resumed!"); - let col_opts = AddNodeOpts { + let col_opts = AddCollatorOptions { command: Some("polkadot-parachain".try_into()?), ..Default::default() }; - network.add_node("new-col-1", col_opts, Some(100)).await?; + network.add_collator("new-col-1", col_opts, 100).await?; println!("new collator deployed!"); // For now let just loop.... diff --git a/crates/orchestrator/src/generators/chain_spec.rs b/crates/orchestrator/src/generators/chain_spec.rs index 27e96c6bf82dfca2f2a336c839ef4e3fe8d22903..d44dab28a29db0f7c1511c2f3e211954becacdf3 100644 --- a/crates/orchestrator/src/generators/chain_spec.rs +++ b/crates/orchestrator/src/generators/chain_spec.rs @@ -11,7 +11,7 @@ use provider::{ }; use serde_json::json; use support::fs::FileSystem; -use tracing::warn; +use tracing::{debug, warn}; use super::errors::GeneratorError; use crate::{ @@ -811,7 +811,7 @@ fn add_collator_selection( *invulnerables = json!(keys); } else { // TODO: add a nice warning here. - println!("warn!! can't customize the invulnerables key"); + debug!("âš ï¸ 'invulnerables' not present in spec, will not be customized"); } } else { unreachable!("pointer to runtime config should be valid!") diff --git a/crates/orchestrator/src/lib.rs b/crates/orchestrator/src/lib.rs index 992112c3ad7523e220bfb1cd9ec6037ac47d4bce..b2d42c3b3dacd0580607fadb2ffa68a552ab4ee9 100644 --- a/crates/orchestrator/src/lib.rs +++ b/crates/orchestrator/src/lib.rs @@ -458,4 +458,4 @@ pub enum ZombieRole { } // re-export -pub use network::AddNodeOpts; +pub use network::{AddCollatorOptions, AddNodeOptions}; diff --git a/crates/orchestrator/src/network.rs b/crates/orchestrator/src/network.rs index 20edbd8315f88d6740838e8124b9f0b66cd88c07..0b981e44c20345b377d7f10e6e4e4b196540eaef 100644 --- a/crates/orchestrator/src/network.rs +++ b/crates/orchestrator/src/network.rs @@ -14,7 +14,7 @@ use support::fs::FileSystem; use self::{node::NetworkNode, parachain::Parachain, relaychain::Relaychain}; use crate::{ network_spec::{self, NetworkSpec}, - shared::types::ChainDefaultContext, + shared::{macros, types::ChainDefaultContext}, spawner::{self, SpawnNodeCtx}, ScopedFilesystem, ZombieRole, }; @@ -40,17 +40,14 @@ impl<T: FileSystem> std::fmt::Debug for Network<T> { } } -#[derive(Default, Debug, Clone)] -pub struct AddNodeOpts { - pub image: Option<Image>, - pub command: Option<Command>, - pub args: Vec<Arg>, - pub env: Vec<EnvVar>, - pub is_validator: bool, - pub rpc_port: Option<Port>, - pub prometheus_port: Option<Port>, - pub p2p_port: Option<Port>, -} +macros::create_add_options!(AddNodeOptions { + chain_spec: Option<PathBuf> +}); + +macros::create_add_options!(AddCollatorOptions { + chain_spec: Option<PathBuf>, + chain_spec_relay: Option<PathBuf> +}); impl<T: FileSystem> Network<T> { pub(crate) fn new_with_relay( @@ -69,108 +66,225 @@ impl<T: FileSystem> Network<T> { } } - // Pub API + // Pubic API + + pub fn relaychain(&self) -> &Relaychain { + &self.relay + } // Teardown the network // destroy() + /// Add a node to the relaychain + /// + /// NOTE: name must be unique in the whole network. The new node is added to the + /// running network instance. + /// + /// # Example: + /// ```rust + /// # use provider::NativeProvider; + /// # use support::{fs::local::LocalFileSystem, process::os::OsProcessManager}; + /// # use zombienet_orchestrator::{errors, AddNodeOptions, Orchestrator}; + /// # use configuration::NetworkConfig; + /// # async fn example() -> Result<(), errors::OrchestratorError> { + /// # let provider = NativeProvider::new(LocalFileSystem {}, OsProcessManager {}); + /// # let orchestrator = Orchestrator::new(LocalFileSystem {}, provider); + /// # let config = NetworkConfig::load_from_toml("config.toml")?; + /// let mut network = orchestrator.spawn(config).await?; + /// + /// // Create the options to add the new node + /// let opts = AddNodeOptions { + /// rpc_port: Some(9444), + /// is_validator: true, + /// ..Default::default() + /// }; + /// + /// network.add_node("new-node", opts).await?; + /// # Ok(()) + /// # } + /// ``` pub async fn add_node( &mut self, name: impl Into<String>, - options: AddNodeOpts, - para_id: Option<u32>, + options: AddNodeOptions, ) -> Result<(), anyhow::Error> { - // build context - // let (maybe_para_chain_id, chain_context, para_spec, role) = - let (chain_context, role, maybe_para_chain_id, para_spec, maybe_para_chain_spec_path) = - if let Some(para_id) = para_id { - let spec = self - .initial_spec - .parachains - .iter() - .find(|para| para.id == para_id) - .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?; - let role = if spec.is_cumulus_based { - ZombieRole::CumulusCollator - } else { - ZombieRole::Collator - }; - let chain_context = ChainDefaultContext { - default_command: spec.default_command.as_ref(), - default_image: spec.default_image.as_ref(), - default_resources: spec.default_resources.as_ref(), - default_db_snapshot: spec.default_db_snapshot.as_ref(), - default_args: spec.default_args.iter().collect(), - }; - let parachain = self - .parachains - .get(¶_id) - .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?; - - // (parachain.chain_id.clone(), chain_context, Some(spec), role) - ( - chain_context, - role, - parachain.chain_id.clone(), - Some(spec), - parachain.chain_spec_path.clone(), - ) - } else { - let spec = &self.initial_spec.relaychain; - let chain_context = ChainDefaultContext { - default_command: spec.default_command.as_ref(), - default_image: spec.default_image.as_ref(), - default_resources: spec.default_resources.as_ref(), - default_db_snapshot: spec.default_db_snapshot.as_ref(), - default_args: spec.default_args.iter().collect(), - }; - (chain_context, ZombieRole::Node, None, None, None) - }; + let name = name.into(); + let relaychain = self.relaychain(); + + if self.nodes_by_name.contains_key(&name) { + return Err(anyhow::anyhow!("Name: {} is already used.", name)); + } + + let chain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec { + chain_spec_custom_path.clone() + } else { + PathBuf::from(format!( + "{}/{}.json", + self.ns.base_dir().to_string_lossy(), + relaychain.chain + )) + }; + + let chain_context = ChainDefaultContext { + default_command: self.initial_spec.relaychain.default_command.as_ref(), + default_image: self.initial_spec.relaychain.default_image.as_ref(), + default_resources: self.initial_spec.relaychain.default_resources.as_ref(), + default_db_snapshot: self.initial_spec.relaychain.default_db_snapshot.as_ref(), + default_args: self.initial_spec.relaychain.default_args.iter().collect(), + }; let node_spec = - network_spec::node::NodeSpec::from_ad_hoc(name.into(), options, &chain_context)?; + network_spec::node::NodeSpec::from_ad_hoc(&name, options.into(), &chain_context)?; + let base_dir = self.ns.base_dir().to_string_lossy(); + let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir); + + let ctx = SpawnNodeCtx { + chain_id: &relaychain.chain_id, + parachain_id: None, + chain: &relaychain.chain, + role: ZombieRole::Node, + ns: &self.ns, + scoped_fs: &scoped_fs, + parachain: None, + bootnodes_addr: &vec![], + wait_ready: true, + }; + + let global_files_to_inject = vec![TransferedFile { + local_path: chain_spec_path, + remote_path: PathBuf::from(format!("/cfg/{}.json", relaychain.chain)), + }]; + + let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?; + + // TODO: register the new node as validator in the relaychain + // STEPS: + // - check balance of `stash` derivation for validator account + // - call rotate_keys on the new validator + // - call setKeys on the new validator + // if node_spec.is_validator { + // let running_node = self.relay.nodes.first().unwrap(); + // // tx_helper::validator_actions::register(vec![&node], &running_node.ws_uri, None).await?; + // } + + // Add node to the global hash + self.add_running_node(node.clone(), None); + // add node to relay + self.relay.nodes.push(node); + + Ok(()) + } + + /// Add a new collator to a parachain + /// + /// NOTE: name must be unique in the whole network. + /// + /// # Example: + /// ```rust + /// # use provider::NativeProvider; + /// # use support::{fs::local::LocalFileSystem, process::os::OsProcessManager}; + /// # use zombienet_orchestrator::{errors, AddCollatorOptions, Orchestrator}; + /// # use configuration::NetworkConfig; + /// # async fn example() -> Result<(), anyhow::Error> { + /// # let provider = NativeProvider::new(LocalFileSystem {}, OsProcessManager {}); + /// # let orchestrator = Orchestrator::new(LocalFileSystem {}, provider); + /// # let config = NetworkConfig::load_from_toml("config.toml")?; + /// let mut network = orchestrator.spawn(config).await?; + /// + /// let col_opts = AddCollatorOptions { + /// command: Some("polkadot-parachain".try_into()?), + /// ..Default::default() + /// }; + /// + /// network.add_collator("new-col-1", col_opts, 100).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn add_collator( + &mut self, + name: impl Into<String>, + options: AddCollatorOptions, + para_id: u32, + ) -> Result<(), anyhow::Error> { + let spec = self + .initial_spec + .parachains + .iter() + .find(|para| para.id == para_id) + .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?; + let role = if spec.is_cumulus_based { + ZombieRole::CumulusCollator + } else { + ZombieRole::Collator + }; + let chain_context = ChainDefaultContext { + default_command: spec.default_command.as_ref(), + default_image: spec.default_image.as_ref(), + default_resources: spec.default_resources.as_ref(), + default_db_snapshot: spec.default_db_snapshot.as_ref(), + default_args: spec.default_args.iter().collect(), + }; + let parachain = self + .parachains + .get(¶_id) + .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?; + let base_dir = self.ns.base_dir().to_string_lossy(); let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir); // TODO: we want to still supporting spawn a dedicated bootnode?? let ctx = SpawnNodeCtx { chain_id: &self.relay.chain_id, - parachain_id: maybe_para_chain_id.as_deref(), + parachain_id: parachain.chain_id.as_deref(), chain: &self.relay.chain, role, ns: &self.ns, scoped_fs: &scoped_fs, - parachain: para_spec, + parachain: Some(spec), bootnodes_addr: &vec![], wait_ready: true, }; - let mut global_files_to_inject = vec![TransferedFile { - local_path: PathBuf::from(format!( + let relaychain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec_relay { + chain_spec_custom_path.clone() + } else { + PathBuf::from(format!( "{}/{}.json", self.ns.base_dir().to_string_lossy(), self.relay.chain - )), + )) + }; + + let mut global_files_to_inject = vec![TransferedFile { + local_path: relaychain_spec_path, remote_path: PathBuf::from(format!("/cfg/{}.json", self.relay.chain)), }]; - if let Some(para_spec_path) = maybe_para_chain_spec_path { + let para_chain_spec_local_path = if let Some(para_chain_spec_custom) = &options.chain_spec { + Some(para_chain_spec_custom.clone()) + } else if let Some(para_spec_path) = ¶chain.chain_spec_path { + Some(PathBuf::from(format!( + "{}/{}", + self.ns.base_dir().to_string_lossy(), + para_spec_path.to_string_lossy() + ))) + } else { + None + }; + + if let Some(para_spec_path) = para_chain_spec_local_path { global_files_to_inject.push(TransferedFile { - local_path: PathBuf::from(format!( - "{}/{}", - self.ns.base_dir().to_string_lossy(), - para_spec_path.to_string_lossy() - )), - remote_path: PathBuf::from(format!( - "/cfg/{}.json", - para_id.ok_or(anyhow::anyhow!( - "para_id should be valid here, this is a bug!" - ))? - )), + local_path: para_spec_path, + remote_path: PathBuf::from(format!("/cfg/{}.json", para_id)), }); } + let node_spec = + network_spec::node::NodeSpec::from_ad_hoc(name.into(), options.into(), &chain_context)?; + let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?; + let para = self.parachains.get_mut(¶_id).unwrap(); + para.collators.push(node.clone()); self.add_running_node(node, None); Ok(()) @@ -228,10 +342,6 @@ impl<T: FileSystem> Network<T> { self.ns.name() } - pub(crate) fn relaychain(&self) -> &Relaychain { - &self.relay - } - pub(crate) fn parachain(&self, para_id: u32) -> Option<&Parachain> { self.parachains.get(¶_id) } diff --git a/crates/orchestrator/src/network/node.rs b/crates/orchestrator/src/network/node.rs index 904f96f1ecc9c1e99a79437c6f8e37598994a2e2..63ffaf36f80f67e7d941e59e5baddccec2043f02 100644 --- a/crates/orchestrator/src/network/node.rs +++ b/crates/orchestrator/src/network/node.rs @@ -39,6 +39,14 @@ impl NetworkNode { } } + pub fn spec(&self) -> &NodeSpec { + &self.spec + } + + pub fn ws_uri(&self) -> &str { + &self.ws_uri + } + /// Pause the node, this is implemented by pausing the /// actual process (e.g polkadot) with sendig `SIGSTOP` signal pub async fn pause(&self) -> Result<(), anyhow::Error> { diff --git a/crates/orchestrator/src/network/relaychain.rs b/crates/orchestrator/src/network/relaychain.rs index 74886165298f408d616e410e8b004609f8dc52d8..356f84c2a060f06b1c4ad0792260ebad7ee165e4 100644 --- a/crates/orchestrator/src/network/relaychain.rs +++ b/crates/orchestrator/src/network/relaychain.rs @@ -19,4 +19,9 @@ impl Relaychain { nodes: Default::default(), } } + + // Public API + pub fn chain(&self) -> &str { + &self.chain + } } diff --git a/crates/orchestrator/src/network_spec/node.rs b/crates/orchestrator/src/network_spec/node.rs index 15d9580b250590478cb61b354d45f35cdab3452f..b49885e521c319b122b1594f89b9bebcab5515ec 100644 --- a/crates/orchestrator/src/network_spec/node.rs +++ b/crates/orchestrator/src/network_spec/node.rs @@ -4,14 +4,43 @@ use configuration::shared::{ types::{Arg, AssetLocation, Command, Image}, }; use multiaddr::Multiaddr; +use provider::types::Port; use crate::{ errors::OrchestratorError, generators, - network::AddNodeOpts, - shared::types::{ChainDefaultContext, NodeAccounts, ParkedPort}, + network::AddNodeOptions, + shared::{ + macros, + types::{ChainDefaultContext, NodeAccounts, ParkedPort}, + }, + AddCollatorOptions, }; +macros::create_add_options!(AddNodeSpecOpts {}); + +macro_rules! impl_from_for_add_node_opts { + ($struct:ident) => { + impl From<$struct> for AddNodeSpecOpts { + fn from(value: $struct) -> Self { + Self { + image: value.image, + command: value.command, + args: value.args, + env: value.env, + is_validator: value.is_validator, + rpc_port: value.rpc_port, + prometheus_port: value.prometheus_port, + p2p_port: value.p2p_port, + } + } + } + }; +} + +impl_from_for_add_node_opts!(AddNodeOptions); +impl_from_for_add_node_opts!(AddCollatorOptions); + /// A node configuration, with fine-grained configuration options. #[derive(Debug, Clone)] pub struct NodeSpec { @@ -145,7 +174,7 @@ impl NodeSpec { pub fn from_ad_hoc( name: impl Into<String>, - options: AddNodeOpts, + options: AddNodeSpecOpts, chain_context: &ChainDefaultContext, ) -> Result<Self, OrchestratorError> { // Check first if the image is set at node level, then try with the default diff --git a/crates/orchestrator/src/shared.rs b/crates/orchestrator/src/shared.rs index cd408564ea089141dd830d4524e027b1064b33d7..9bedb0e5fabb56ff7c5b852f78cbfa517a932ef4 100644 --- a/crates/orchestrator/src/shared.rs +++ b/crates/orchestrator/src/shared.rs @@ -1 +1,2 @@ +pub mod macros; pub mod types; diff --git a/crates/orchestrator/src/shared/macros.rs b/crates/orchestrator/src/shared/macros.rs new file mode 100644 index 0000000000000000000000000000000000000000..f79b3a1aa6ecb225605ee147725c69fd6836ad30 --- /dev/null +++ b/crates/orchestrator/src/shared/macros.rs @@ -0,0 +1,30 @@ +macro_rules! create_add_options { + ($struct:ident {$( $field:ident:$type:ty ),*}) =>{ + #[derive(Default, Debug, Clone)] + pub struct $struct { + /// Image to run the node + pub image: Option<Image>, + /// Command to run the node + pub command: Option<Command>, + /// Arguments to pass to the node + pub args: Vec<Arg>, + /// Env vars to set + pub env: Vec<EnvVar>, + /// Make the node a validator + /// + /// This implies `--validator` or `--collator` + pub is_validator: bool, + /// RPC port to use, if None a random one will be set + pub rpc_port: Option<Port>, + /// Prometheus port to use, if None a random one will be set + pub prometheus_port: Option<Port>, + /// P2P port to use, if None a random one will be set + pub p2p_port: Option<Port>, + $( + pub $field: $type, + )* + } + }; +} + +pub(crate) use create_add_options; diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index f113b2a15e47acd991b115116dd16c06b2a7c5e5..867e483f84195ee2d80085c429573ab154ec0ae1 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1,6 +1,8 @@ use async_trait::async_trait; pub use configuration::{NetworkConfig, NetworkConfigBuilder, RegistrationStrategy}; -pub use orchestrator::{errors::OrchestratorError, network::Network, AddNodeOpts, Orchestrator}; +pub use orchestrator::{ + errors::OrchestratorError, network::Network, AddCollatorOptions, AddNodeOptions, Orchestrator, +}; use provider::NativeProvider; use support::{fs::local::LocalFileSystem, process::os::OsProcessManager};