From 34e4da31b91bae4610c2275817678b800e9ee8bc Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Fri, 27 Oct 2023 10:05:58 +0300 Subject: [PATCH] Use `clap` to parse args in `executor` binary --- node/Cargo.toml | 2 +- node/tools/src/bin/localnet_config.rs | 2 +- node/tools/src/config.rs | 125 ++++++++++++++++++++++++++ node/tools/src/config/config_paths.rs | 111 ----------------------- node/tools/src/config/mod.rs | 67 -------------- node/tools/src/lib.rs | 2 +- node/tools/src/main.rs | 63 +++++++++---- 7 files changed, 175 insertions(+), 197 deletions(-) create mode 100644 node/tools/src/config.rs delete mode 100644 node/tools/src/config/config_paths.rs delete mode 100644 node/tools/src/config/mod.rs diff --git a/node/Cargo.toml b/node/Cargo.toml index d560d722..38551c14 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -26,7 +26,7 @@ assert_matches = "1.5.0" async-trait = "0.1.71" bit-vec = "0.6" blst = "0.3.10" -clap = { version = "4.3.3", features = ["derive"] } +clap = { version = "4.3.3", features = ["derive", "env"] } ed25519-dalek = { version = "2.0.0", features = ["serde", "rand_core"] } futures = "0.3.28" hex = "0.4.3" diff --git a/node/tools/src/bin/localnet_config.rs b/node/tools/src/bin/localnet_config.rs index 374ce19e..20862ef5 100644 --- a/node/tools/src/bin/localnet_config.rs +++ b/node/tools/src/bin/localnet_config.rs @@ -21,7 +21,7 @@ fn with_unspecified_ip(addr: SocketAddr) -> SocketAddr { } /// Command line arguments. -#[derive(Parser, Debug)] +#[derive(Debug, Parser)] struct Args { /// Path to a file with newline separated IP:port addrs of the nodes to configure. /// Binary will generate a config for each IP in this file. diff --git a/node/tools/src/config.rs b/node/tools/src/config.rs new file mode 100644 index 00000000..0776659d --- /dev/null +++ b/node/tools/src/config.rs @@ -0,0 +1,125 @@ +//! Node configuration. + +use anyhow::Context as _; +use crypto::{read_optional_text, Text, TextFmt}; +use executor::{ConsensusConfig, ExecutorConfig}; +use roles::{node, validator}; +use schema::{proto::executor::config as proto, read_optional, read_required, ProtoFmt}; +use std::{fs, net, path::Path}; + +/// This struct holds the file path to each of the config files. +#[derive(Debug)] +pub struct ConfigPaths<'a> { + /// Path to a JSON file with node configuration. + pub config: &'a Path, + /// Path to a validator key file. + pub validator_key: Option<&'a Path>, + /// Path to a node key file. + pub node_key: &'a Path, +} + +/// Node configuration including executor configuration, optional validator configuration, +/// and application-specific settings (e.g. metrics scraping). +#[derive(Debug)] +pub struct NodeConfig { + /// Executor configuration. + pub executor: ExecutorConfig, + /// IP:port to serve metrics data for scraping. + pub metrics_server_addr: Option, + /// Consensus network config. + pub consensus: Option, +} + +impl ProtoFmt for NodeConfig { + type Proto = proto::NodeConfig; + + fn read(r: &Self::Proto) -> anyhow::Result { + Ok(Self { + executor: read_required(&r.executor).context("executor")?, + metrics_server_addr: read_optional_text(&r.metrics_server_addr) + .context("metrics_server_addr")?, + consensus: read_optional(&r.consensus).context("consensus")?, + }) + } + + fn build(&self) -> Self::Proto { + Self::Proto { + executor: Some(self.executor.build()), + metrics_server_addr: self.metrics_server_addr.as_ref().map(TextFmt::encode), + consensus: self.consensus.as_ref().map(ProtoFmt::build), + } + } +} + +/// Main struct that holds the config options for the node. +#[derive(Debug)] +pub struct Configs { + /// Executor configuration of the node. + pub executor: ExecutorConfig, + /// IP:port to serve metrics data for scraping. + pub metrics_server_addr: Option, + /// Consensus-specific config extensions. Only set for validators. + pub consensus: Option<(ConsensusConfig, validator::SecretKey)>, + /// The validator secret key for this node. + /// The node secret key. This key is used by both full nodes and validators to identify themselves + /// in the P2P network. + pub node_key: node::SecretKey, +} + +impl Configs { + /// Method to fetch the node config. + #[tracing::instrument(level = "trace", ret)] + pub fn read(args: ConfigPaths<'_>) -> anyhow::Result { + let node_config = fs::read_to_string(args.config).with_context(|| { + format!( + "failed reading node config from `{}`", + args.config.display() + ) + })?; + let node_config: NodeConfig = schema::decode_json(&node_config).with_context(|| { + format!( + "failed decoding JSON node config at `{}`", + args.config.display() + ) + })?; + + let validator_key: Option = args + .validator_key + .as_ref() + .map(|validator_key| { + let read_key = fs::read_to_string(validator_key).with_context(|| { + format!( + "failed reading validator key from `{}`", + validator_key.display() + ) + })?; + Text::new(&read_key).decode().with_context(|| { + format!( + "failed decoding validator key at `{}`", + validator_key.display() + ) + }) + }) + .transpose()?; + let read_key = fs::read_to_string(args.node_key).with_context(|| { + format!("failed reading node key from `{}`", args.node_key.display()) + })?; + let node_key = Text::new(&read_key).decode().with_context(|| { + format!("failed decoding node key at `{}`", args.node_key.display()) + })?; + + anyhow::ensure!( + validator_key.is_some() == node_config.consensus.is_some(), + "Validator key and consensus config must be specified at the same time" + ); + let consensus = validator_key.and_then(|key| Some((node_config.consensus?, key))); + + let cfg = Configs { + executor: node_config.executor, + metrics_server_addr: node_config.metrics_server_addr, + consensus, + node_key, + }; + Ok(cfg) + } +} diff --git a/node/tools/src/config/config_paths.rs b/node/tools/src/config/config_paths.rs deleted file mode 100644 index 805b0997..00000000 --- a/node/tools/src/config/config_paths.rs +++ /dev/null @@ -1,111 +0,0 @@ -use super::{Configs, NodeConfig}; -use anyhow::Context as _; -use crypto::Text; -use roles::validator; -use std::fs; -use tracing::instrument; - -/// This struct holds the file path to each of the config files. -#[derive(Debug)] -pub(crate) struct ConfigPaths { - config: String, - validator_key: Option, - node_key: String, -} - -impl ConfigPaths { - /// This function gets the paths for the config file and the key files. - /// First, we try to get the path from the command line arguments. If that fails, we try to get - /// it as an environment variable. If that also fails, we just use a default value. - #[instrument(level = "trace")] - pub(crate) fn resolve(args: &[String]) -> Self { - let validator_key = PathSpec { - name: "Validator key", - flag: "--validator-key", - env: "VALIDATOR_KEY", - default: "validator_key", - } - .resolve(args); - let validator_key = (!validator_key.is_empty()).then_some(validator_key); - - Self { - config: PathSpec { - name: "Config file", - flag: "--config-file", - env: "CONFIG_FILE", - default: "config.json", - } - .resolve(args), - validator_key, - node_key: PathSpec { - name: "Node key", - flag: "--node-key", - env: "NODE_KEY", - default: "node_key", - } - .resolve(args), - } - } - - /// This function parses the config files from the paths given as command line arguments. - #[instrument(level = "trace", ret)] - pub(crate) fn read(self) -> anyhow::Result { - let node_config: NodeConfig = - schema::decode_json(&fs::read_to_string(&self.config).context(self.config)?)?; - let validator_key: Option = self - .validator_key - .as_ref() - .map(|validator_key| { - let read_key = fs::read_to_string(validator_key).context(validator_key.clone())?; - Text::new(&read_key).decode() - }) - .transpose()?; - let node_key = - Text::new(&fs::read_to_string(&self.node_key).context(self.node_key)?).decode()?; - - anyhow::ensure!( - validator_key.is_some() == node_config.consensus.is_some(), - "Validator key and consensus config must be specified at the same time" - ); - let consensus = validator_key.and_then(|key| Some((node_config.consensus?, key))); - - let cfg = Configs { - executor: node_config.executor, - metrics_server_addr: node_config.metrics_server_addr, - consensus, - node_key, - }; - Ok(cfg) - } -} - -#[derive(Debug)] -struct PathSpec<'a> { - name: &'a str, - flag: &'a str, - env: &'a str, - default: &'a str, -} - -impl<'a> PathSpec<'a> { - #[instrument(level = "trace", ret)] - fn resolve(&self, args: &[String]) -> String { - if let Some(path) = find_flag(args, self.flag) { - tracing::debug!("{} path found in command line arguments.", self.name); - return path.clone(); - } - - if let Ok(path) = std::env::var(self.env) { - tracing::debug!("{} path found in environment variable.", self.name); - return path; - } - - tracing::debug!("Using default {} path.", self.name); - self.default.to_string() - } -} - -fn find_flag<'a>(args: &'a [String], flag: &'a str) -> Option<&'a String> { - let pos = args.iter().position(|x| x == flag)?; - args.get(pos + 1) -} diff --git a/node/tools/src/config/mod.rs b/node/tools/src/config/mod.rs deleted file mode 100644 index 9394375f..00000000 --- a/node/tools/src/config/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Node configuration. - -use self::config_paths::ConfigPaths; -use anyhow::Context as _; -use crypto::{read_optional_text, TextFmt}; -use executor::{ConsensusConfig, ExecutorConfig}; -use roles::{node, validator}; -use schema::{proto::executor::config as proto, read_optional, read_required, ProtoFmt}; -use std::net; - -mod config_paths; - -/// Node configuration including executor configuration, optional validator configuration, -/// and application-specific settings (e.g. metrics scraping). -#[derive(Debug)] -pub struct NodeConfig { - /// Executor configuration. - pub executor: ExecutorConfig, - /// IP:port to serve metrics data for scraping. - pub metrics_server_addr: Option, - /// Consensus network config. - pub consensus: Option, -} - -impl ProtoFmt for NodeConfig { - type Proto = proto::NodeConfig; - - fn read(r: &Self::Proto) -> anyhow::Result { - Ok(Self { - executor: read_required(&r.executor).context("executor")?, - metrics_server_addr: read_optional_text(&r.metrics_server_addr) - .context("metrics_server_addr")?, - consensus: read_optional(&r.consensus).context("consensus")?, - }) - } - - fn build(&self) -> Self::Proto { - Self::Proto { - executor: Some(self.executor.build()), - metrics_server_addr: self.metrics_server_addr.as_ref().map(TextFmt::encode), - consensus: self.consensus.as_ref().map(ProtoFmt::build), - } - } -} - -/// Main struct that holds the config options for the node. -#[derive(Debug)] -pub struct Configs { - /// Executor configuration of the node. - pub executor: ExecutorConfig, - /// IP:port to serve metrics data for scraping. - pub metrics_server_addr: Option, - /// Consensus-specific config extensions. Only set for validators. - pub consensus: Option<(ConsensusConfig, validator::SecretKey)>, - /// The validator secret key for this node. - /// The node secret key. This key is used by both full nodes and validators to identify themselves - /// in the P2P network. - pub node_key: node::SecretKey, -} - -impl Configs { - /// Method to fetch the node config. - #[tracing::instrument(level = "trace", ret)] - pub fn read(args: &[String]) -> anyhow::Result { - ConfigPaths::resolve(args).read() - } -} diff --git a/node/tools/src/lib.rs b/node/tools/src/lib.rs index 917fcdea..6b818859 100644 --- a/node/tools/src/lib.rs +++ b/node/tools/src/lib.rs @@ -2,4 +2,4 @@ mod config; -pub use self::config::{Configs, NodeConfig}; +pub use self::config::{ConfigPaths, Configs, NodeConfig}; diff --git a/node/tools/src/main.rs b/node/tools/src/main.rs index 6757adb3..d7c14a86 100644 --- a/node/tools/src/main.rs +++ b/node/tools/src/main.rs @@ -2,34 +2,65 @@ //! manages communication between the actors. It is the main executable in this workspace. use anyhow::Context as _; +use clap::Parser; use concurrency::{ ctx::{self, channel}, scope, time, }; use executor::Executor; -use std::{fs, io::IsTerminal as _, path::Path, sync::Arc}; +use std::{ + fs, + io::IsTerminal as _, + path::{Path, PathBuf}, + sync::Arc, +}; use storage::{BlockStore, RocksdbStorage}; -use tools::Configs; +use tools::{ConfigPaths, Configs}; use tracing::metadata::LevelFilter; use tracing_subscriber::{prelude::*, Registry}; use utils::no_copy::NoCopy; use vise_exporter::MetricsExporter; +/// Command-line application launching a node executor. +#[derive(Debug, Parser)] +struct Args { + /// Verify configuration instead of launching a node. + #[arg(long, conflicts_with_all = ["ci_mode", "validator_key", "config_file", "node_key"])] + verify_config: bool, + /// Exit after finalizing 100 blocks. + #[arg(long)] + ci_mode: bool, + /// Path to a validator key file. If set to an empty string, validator key will not be read + /// (i.e., a node will be initialized as a non-validator node). + #[arg(long, env = "VALIDATOR_KEY", default_value = "validator_key")] + validator_key: PathBuf, + /// Path to a JSON file with node configuration. + #[arg(long, env = "CONFIG_FILE", default_value = "config.json")] + config_file: PathBuf, + /// Path to a node key file. + #[arg(long, env = "NODE_KEY", default_value = "node_key")] + node_key: PathBuf, +} + +impl Args { + /// Extracts configuration paths from these args. + fn config_paths(&self) -> ConfigPaths<'_> { + ConfigPaths { + config: &self.config_file, + node_key: &self.node_key, + validator_key: (!self.validator_key.as_os_str().is_empty()) + .then_some(&self.validator_key), + } + } +} + #[tokio::main] async fn main() -> anyhow::Result<()> { + let args: Args = Args::parse(); + tracing::trace!(?args, "Starting node"); let ctx = &ctx::root(); - // Get the command line arguments. - let args: Vec<_> = std::env::args().collect(); - - // Check if we are in config mode. - let config_mode = args.iter().any(|x| x == "--verify-config"); - - // Check if we are in CI mode. - // If we are in CI mode, we will exit after finalizing more than 100 blocks. - let ci_mode = args.iter().any(|x| x == "--ci-mode"); - - if !config_mode { + if !args.verify_config { // Create log file. fs::create_dir_all("logs/")?; let log_file = fs::File::create("logs/output.log")?; @@ -63,9 +94,9 @@ async fn main() -> anyhow::Result<()> { // Load the config files. tracing::debug!("Loading config files."); - let configs = Configs::read(&args).context("configs.read()")?; + let configs = Configs::read(args.config_paths()).context("configs.read()")?; - if config_mode { + if args.verify_config { tracing::info!("Configuration verified."); return Ok(()); } @@ -109,7 +140,7 @@ async fn main() -> anyhow::Result<()> { s.spawn(executor.run(ctx)); // if we are in CI mode, we wait for the node to finalize 100 blocks and then we stop it - if ci_mode { + if args.ci_mode { let storage = storage.clone(); loop { let block_finalized = storage.head_block(ctx).await.context("head_block")?;