casper_node/
cli.rs

1//! Command-line option parsing.
2//!
3//! Most configuration is done via config files (see [`config`](../config/index.html) for details).
4
5pub mod arglang;
6
7use std::{
8    alloc::System,
9    fs,
10    path::{Path, PathBuf},
11    str::FromStr,
12    sync::Arc,
13};
14
15use anyhow::{self, bail, Context};
16use prometheus::Registry;
17use regex::Regex;
18use stats_alloc::{StatsAlloc, INSTRUMENTED_SYSTEM};
19use structopt::StructOpt;
20use toml::{value::Table, Value};
21use tracing::{error, info};
22
23use casper_types::{Chainspec, ChainspecRawBytes};
24
25use crate::{
26    components::network::Identity as NetworkIdentity,
27    logging,
28    reactor::{main_reactor, Runner},
29    setup_signal_hooks,
30    types::ExitCode,
31    utils::{
32        chain_specification::validate_chainspec, config_specification::validate_config, Loadable,
33        WithDir,
34    },
35};
36
37// We override the standard allocator to gather metrics and tune the allocator via the MALLOC_CONF
38// env var.
39#[global_allocator]
40static ALLOC: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;
41
42// Note: The docstring on `Cli` is the help shown when calling the binary with `--help`.
43#[derive(Debug, StructOpt)]
44#[structopt(version = crate::VERSION_STRING_COLOR.as_str())]
45#[allow(rustdoc::invalid_html_tags)]
46/// Casper blockchain node.
47pub enum Cli {
48    /// Run the node in standard mode.
49    ///
50    /// Loads the configuration values from the given configuration file or uses defaults if not
51    /// given, then runs the reactor.
52    #[structopt(alias = "validator")]
53    Standard {
54        /// Path to configuration file.
55        config: PathBuf,
56
57        #[structopt(
58            short = "C",
59            long,
60            env = "NODE_CONFIG",
61            use_delimiter(true),
62            value_delimiter(";")
63        )]
64        /// Overrides and extensions for configuration file entries in the form
65        /// <SECTION>.<KEY>=<VALUE>.  For example, '-C=node.chainspec_config_path=chainspec.toml'
66        config_ext: Vec<ConfigExt>,
67    },
68    /// Migrate modified values from the old config as required after an upgrade.
69    MigrateConfig {
70        /// Path to configuration file of previous version of node.
71        #[structopt(long)]
72        old_config: PathBuf,
73        /// Path to configuration file of this version of node.
74        #[structopt(long)]
75        new_config: PathBuf,
76    },
77    /// Migrate any stored data as required after an upgrade.
78    MigrateData {
79        /// Path to configuration file of previous version of node.
80        #[structopt(long)]
81        old_config: PathBuf,
82        /// Path to configuration file of this version of node.
83        #[structopt(long)]
84        new_config: PathBuf,
85    },
86    /// Verify that a given config file can be parsed.
87    ValidateConfig {
88        /// Path to configuration file.
89        config: PathBuf,
90    },
91}
92
93#[derive(Debug)]
94/// Command line extension to be applied to TOML-based config file values.
95pub struct ConfigExt {
96    section: String,
97    key: String,
98    value: String,
99}
100
101impl ConfigExt {
102    /// Updates TOML table with updated or extended key value pairs.
103    ///
104    /// Returns errors if the respective sections to be updated are not TOML tables or if parsing
105    /// the command line options failed.
106    fn update_toml_table(&self, toml_value: &mut Value) -> anyhow::Result<()> {
107        let table = toml_value
108            .as_table_mut()
109            .ok_or_else(|| anyhow::anyhow!("configuration table is not a table"))?;
110
111        if !table.contains_key(&self.section) {
112            table.insert(self.section.clone(), Value::Table(Table::new()));
113        }
114        let val = arglang::parse(&self.value)?;
115        table[&self.section]
116            .as_table_mut()
117            .ok_or_else(|| {
118                anyhow::anyhow!("configuration section {} is not a table", self.section)
119            })?
120            .insert(self.key.clone(), val);
121        Ok(())
122    }
123}
124
125impl FromStr for ConfigExt {
126    type Err = anyhow::Error;
127
128    /// Attempts to create a ConfigExt from a str patterned as `section.key=value`
129    fn from_str(input: &str) -> Result<Self, Self::Err> {
130        let re = Regex::new(r"^([^.]+)\.([^=]+)=(.+)$").unwrap();
131        let captures = re
132            .captures(input)
133            .context("could not parse config_ext (see README.md)")?;
134        Ok(ConfigExt {
135            section: captures
136                .get(1)
137                .context("failed to find section")?
138                .as_str()
139                .to_owned(),
140            key: captures
141                .get(2)
142                .context("failed to find key")?
143                .as_str()
144                .to_owned(),
145            value: captures
146                .get(3)
147                .context("failed to find value")?
148                .as_str()
149                .to_owned(),
150        })
151    }
152}
153
154impl Cli {
155    /// Executes selected CLI command.
156    pub async fn run(self) -> anyhow::Result<i32> {
157        match self {
158            Cli::Standard { config, config_ext } => {
159                // Setup UNIX signal hooks.
160                setup_signal_hooks();
161
162                let mut reactor_config = Self::init(&config, config_ext)?;
163
164                // We use a `ChaCha20Rng` for the production node. For one, we want to completely
165                // eliminate any chance of runtime failures, regardless of how small (these
166                // exist with `OsRng`). Additionally, we want to limit the number of syscalls for
167                // performance reasons.
168                let mut rng = crate::new_rng();
169
170                let registry = Registry::new();
171
172                let (chainspec, chainspec_raw_bytes) =
173                    <(Chainspec, ChainspecRawBytes)>::from_path(reactor_config.dir())?;
174
175                info!(
176                    protocol_version = %chainspec.protocol_version(),
177                    build_version = %crate::VERSION_STRING.as_str(),
178                    "node starting up"
179                );
180
181                if !validate_chainspec(&chainspec) {
182                    bail!("invalid chainspec");
183                }
184
185                if !validate_config(reactor_config.value()) {
186                    bail!("invalid config");
187                }
188
189                reactor_config.value_mut().ensure_valid(&chainspec);
190
191                let network_identity = NetworkIdentity::from_config(WithDir::new(
192                    reactor_config.dir(),
193                    reactor_config.value().network.clone(),
194                ))
195                .context("failed to create a network identity")?;
196
197                let mut main_runner = Runner::<main_reactor::MainReactor>::with_metrics(
198                    reactor_config,
199                    Arc::new(chainspec),
200                    Arc::new(chainspec_raw_bytes),
201                    network_identity,
202                    &mut rng,
203                    &registry,
204                )
205                .await?;
206
207                let exit_code = main_runner.run(&mut rng).await;
208                Ok(exit_code as i32)
209            }
210            Cli::MigrateConfig {
211                old_config,
212                new_config,
213            } => {
214                let new_config = Self::init(&new_config, vec![])?;
215
216                let old_root = old_config
217                    .parent()
218                    .map_or_else(|| "/".into(), Path::to_path_buf);
219                let encoded_old_config = fs::read_to_string(&old_config)
220                    .context("could not read old configuration file")
221                    .with_context(|| old_config.display().to_string())?;
222                let old_config = toml::from_str(&encoded_old_config)?;
223
224                info!(build_version = %crate::VERSION_STRING.as_str(), "migrating config");
225                crate::config_migration::migrate_config(
226                    WithDir::new(old_root, old_config),
227                    new_config,
228                )?;
229                Ok(ExitCode::Success as i32)
230            }
231            Cli::MigrateData {
232                old_config,
233                new_config,
234            } => {
235                let new_config = Self::init(&new_config, vec![])?;
236
237                let old_root = old_config
238                    .parent()
239                    .map_or_else(|| "/".into(), Path::to_path_buf);
240                let encoded_old_config = fs::read_to_string(&old_config)
241                    .context("could not read old configuration file")
242                    .with_context(|| old_config.display().to_string())?;
243                let old_config = toml::from_str(&encoded_old_config)?;
244
245                info!(build_version = %crate::VERSION_STRING.as_str(), "migrating data");
246                crate::data_migration::migrate_data(
247                    WithDir::new(old_root, old_config),
248                    new_config,
249                )?;
250                Ok(ExitCode::Success as i32)
251            }
252            Cli::ValidateConfig { config } => {
253                info!(build_version = %crate::VERSION_STRING.as_str(), config_file = ?config, "validating config file");
254                match Self::init(&config, vec![]) {
255                    Ok(_config) => {
256                        info!(build_version = %crate::VERSION_STRING.as_str(), config_file = ?config, "config file is valid");
257                        Ok(ExitCode::Success as i32)
258                    }
259                    Err(err) => {
260                        // initialize manually in case of error to avoid double initialization
261                        logging::init_with_config(&Default::default())?;
262                        error!(build_version = %crate::VERSION_STRING.as_str(), config_file = ?config, "config file is not valid");
263                        Err(err)
264                    }
265                }
266            }
267        }
268    }
269
270    /// Parses the config file for the current version of casper-node, and initializes logging.
271    fn init(
272        config: &Path,
273        config_ext: Vec<ConfigExt>,
274    ) -> anyhow::Result<WithDir<main_reactor::Config>> {
275        // Determine the parent directory of the configuration file, if any.
276        // Otherwise, we default to `/`.
277        let root = config
278            .parent()
279            .map_or_else(|| "/".into(), Path::to_path_buf);
280
281        // The app supports running without a config file, using default values.
282        let encoded_config = fs::read_to_string(config)
283            .context("could not read configuration file")
284            .with_context(|| config.display().to_string())?;
285
286        // Get the TOML table version of the config indicated from CLI args, or from a new
287        // defaulted config instance if one is not provided.
288        let mut config_table: Value = toml::from_str(&encoded_config)?;
289
290        // If any command line overrides to the config values are passed, apply them.
291        for item in config_ext {
292            item.update_toml_table(&mut config_table)?;
293        }
294
295        // Create main config, including any overridden values.
296        let main_config: main_reactor::Config = config_table.try_into()?;
297        logging::init_with_config(&main_config.logging)?;
298
299        Ok(WithDir::new(root, main_config))
300    }
301}