zombienet_configuration/
global_settings.rs

1use std::{
2    error::Error,
3    fmt::Display,
4    net::IpAddr,
5    path::{Path, PathBuf},
6    str::FromStr,
7};
8
9use multiaddr::Multiaddr;
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    shared::{
14        errors::{ConfigError, FieldError},
15        helpers::{merge_errors, merge_errors_vecs},
16        types::Duration,
17    },
18    utils::{default_as_true, default_node_spawn_timeout, default_timeout},
19};
20
21/// Global settings applied to an entire network.
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct GlobalSettings {
24    /// Global bootnodes to use (we will then add more)
25    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
26    bootnodes_addresses: Vec<Multiaddr>,
27    // TODO: parse both case in zombienet node version to avoid renamed ?
28    /// Global spawn timeout
29    #[serde(rename = "timeout", default = "default_timeout")]
30    network_spawn_timeout: Duration,
31    // TODO: not used yet
32    /// Node spawn timeout
33    #[serde(default = "default_node_spawn_timeout")]
34    node_spawn_timeout: Duration,
35    // TODO: not used yet
36    /// Local ip to use for construct the direct links
37    local_ip: Option<IpAddr>,
38    /// Directory to use as base dir
39    /// Used to reuse the same files (database) from a previous run,
40    /// also note that we will override the content of some of those files.
41    base_dir: Option<PathBuf>,
42    /// Number of concurrent spawning process to launch, None means try to spawn all at the same time.
43    spawn_concurrency: Option<usize>,
44    /// If enabled, will launch a task to monitor nodes' liveness and tear down the network if there are any.
45    #[serde(default = "default_as_true")]
46    tear_down_on_failure: bool,
47}
48
49impl GlobalSettings {
50    /// External bootnode address.
51    pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
52        self.bootnodes_addresses.iter().collect()
53    }
54
55    /// Global spawn timeout in seconds.
56    pub fn network_spawn_timeout(&self) -> Duration {
57        self.network_spawn_timeout
58    }
59
60    /// Individual node spawn timeout in seconds.
61    pub fn node_spawn_timeout(&self) -> Duration {
62        self.node_spawn_timeout
63    }
64
65    /// Local IP used to expose local services (including RPC, metrics and monitoring).
66    pub fn local_ip(&self) -> Option<&IpAddr> {
67        self.local_ip.as_ref()
68    }
69
70    /// Base directory to use (instead a random tmp one)
71    /// All the artifacts will be created in this directory.
72    pub fn base_dir(&self) -> Option<&Path> {
73        self.base_dir.as_deref()
74    }
75
76    /// Number of concurrent spawning process to launch
77    pub fn spawn_concurrency(&self) -> Option<usize> {
78        self.spawn_concurrency
79    }
80
81    /// A flag to tear down the network if there are any unresponsive nodes detected.
82    pub fn tear_down_on_failure(&self) -> bool {
83        self.tear_down_on_failure
84    }
85}
86
87impl Default for GlobalSettings {
88    fn default() -> Self {
89        Self {
90            bootnodes_addresses: Default::default(),
91            network_spawn_timeout: default_timeout(),
92            node_spawn_timeout: default_node_spawn_timeout(),
93            local_ip: Default::default(),
94            base_dir: Default::default(),
95            spawn_concurrency: Default::default(),
96            tear_down_on_failure: true,
97        }
98    }
99}
100
101/// A global settings builder, used to build [`GlobalSettings`] declaratively with fields validation.
102#[derive(Default)]
103pub struct GlobalSettingsBuilder {
104    config: GlobalSettings,
105    errors: Vec<anyhow::Error>,
106}
107
108impl GlobalSettingsBuilder {
109    pub fn new() -> Self {
110        Self::default()
111    }
112
113    // Transition to the next state of the builder.
114    fn transition(config: GlobalSettings, errors: Vec<anyhow::Error>) -> Self {
115        Self { config, errors }
116    }
117
118    /// Set the external bootnode address.
119    ///
120    /// Note: Bootnode address replacements are NOT supported here.
121    /// Only arguments (`args`) support dynamic replacements. Bootnode addresses must be a valid address.
122    pub fn with_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
123    where
124        T: TryInto<Multiaddr> + Display + Copy,
125        T::Error: Error + Send + Sync + 'static,
126    {
127        let mut addrs = vec![];
128        let mut errors = vec![];
129
130        for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
131            match addr.try_into() {
132                Ok(addr) => addrs.push(addr),
133                Err(error) => errors.push(
134                    FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
135                ),
136            }
137        }
138
139        Self::transition(
140            GlobalSettings {
141                bootnodes_addresses: addrs,
142                ..self.config
143            },
144            merge_errors_vecs(self.errors, errors),
145        )
146    }
147
148    /// Set global spawn timeout in seconds.
149    pub fn with_network_spawn_timeout(self, timeout: Duration) -> Self {
150        Self::transition(
151            GlobalSettings {
152                network_spawn_timeout: timeout,
153                ..self.config
154            },
155            self.errors,
156        )
157    }
158
159    /// Set individual node spawn timeout in seconds.
160    pub fn with_node_spawn_timeout(self, timeout: Duration) -> Self {
161        Self::transition(
162            GlobalSettings {
163                node_spawn_timeout: timeout,
164                ..self.config
165            },
166            self.errors,
167        )
168    }
169
170    /// Set local IP used to expose local services (including RPC, metrics and monitoring).
171    pub fn with_local_ip(self, local_ip: &str) -> Self {
172        match IpAddr::from_str(local_ip) {
173            Ok(local_ip) => Self::transition(
174                GlobalSettings {
175                    local_ip: Some(local_ip),
176                    ..self.config
177                },
178                self.errors,
179            ),
180            Err(error) => Self::transition(
181                self.config,
182                merge_errors(self.errors, FieldError::LocalIp(error.into()).into()),
183            ),
184        }
185    }
186
187    /// Set the directory to use as base (instead of a random tmp one).
188    pub fn with_base_dir(self, base_dir: impl Into<PathBuf>) -> Self {
189        Self::transition(
190            GlobalSettings {
191                base_dir: Some(base_dir.into()),
192                ..self.config
193            },
194            self.errors,
195        )
196    }
197
198    /// Set the spawn concurrency
199    pub fn with_spawn_concurrency(self, spawn_concurrency: usize) -> Self {
200        Self::transition(
201            GlobalSettings {
202                spawn_concurrency: Some(spawn_concurrency),
203                ..self.config
204            },
205            self.errors,
206        )
207    }
208
209    /// Set the `tear_down_on_failure` flag
210    pub fn with_tear_down_on_failure(self, tear_down_on_failure: bool) -> Self {
211        Self::transition(
212            GlobalSettings {
213                tear_down_on_failure,
214                ..self.config
215            },
216            self.errors,
217        )
218    }
219
220    /// Seals the builder and returns a [`GlobalSettings`] if there are no validation errors, else returns errors.
221    pub fn build(self) -> Result<GlobalSettings, Vec<anyhow::Error>> {
222        if !self.errors.is_empty() {
223            return Err(self
224                .errors
225                .into_iter()
226                .map(|error| ConfigError::GlobalSettings(error).into())
227                .collect::<Vec<_>>());
228        }
229
230        Ok(self.config)
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn global_settings_config_builder_should_succeeds_and_returns_a_global_settings_config() {
240        let global_settings_config = GlobalSettingsBuilder::new()
241            .with_raw_bootnodes_addresses(vec![
242                "/ip4/10.41.122.55/tcp/45421",
243                "/ip4/51.144.222.10/tcp/2333",
244            ])
245            .with_network_spawn_timeout(600)
246            .with_node_spawn_timeout(120)
247            .with_local_ip("10.0.0.1")
248            .with_base_dir("/home/nonroot/mynetwork")
249            .with_spawn_concurrency(5)
250            .with_tear_down_on_failure(true)
251            .build()
252            .unwrap();
253
254        let bootnodes_addresses: Vec<Multiaddr> = vec![
255            "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
256            "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
257        ];
258        assert_eq!(
259            global_settings_config.bootnodes_addresses(),
260            bootnodes_addresses.iter().collect::<Vec<_>>()
261        );
262        assert_eq!(global_settings_config.network_spawn_timeout(), 600);
263        assert_eq!(global_settings_config.node_spawn_timeout(), 120);
264        assert_eq!(
265            global_settings_config
266                .local_ip()
267                .unwrap()
268                .to_string()
269                .as_str(),
270            "10.0.0.1"
271        );
272        assert_eq!(
273            global_settings_config.base_dir().unwrap(),
274            Path::new("/home/nonroot/mynetwork")
275        );
276        assert_eq!(global_settings_config.spawn_concurrency().unwrap(), 5);
277        assert!(global_settings_config.tear_down_on_failure());
278    }
279
280    #[test]
281    fn global_settings_config_builder_should_succeeds_when_node_spawn_timeout_is_missing() {
282        let global_settings_config = GlobalSettingsBuilder::new()
283            .with_raw_bootnodes_addresses(vec![
284                "/ip4/10.41.122.55/tcp/45421",
285                "/ip4/51.144.222.10/tcp/2333",
286            ])
287            .with_network_spawn_timeout(600)
288            .with_local_ip("10.0.0.1")
289            .build()
290            .unwrap();
291
292        let bootnodes_addresses: Vec<Multiaddr> = vec![
293            "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
294            "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
295        ];
296        assert_eq!(
297            global_settings_config.bootnodes_addresses(),
298            bootnodes_addresses.iter().collect::<Vec<_>>()
299        );
300        assert_eq!(global_settings_config.network_spawn_timeout(), 600);
301        assert_eq!(global_settings_config.node_spawn_timeout(), 600);
302        assert_eq!(
303            global_settings_config
304                .local_ip()
305                .unwrap()
306                .to_string()
307                .as_str(),
308            "10.0.0.1"
309        );
310    }
311
312    #[test]
313    fn global_settings_builder_should_fails_and_returns_an_error_if_one_bootnode_address_is_invalid(
314    ) {
315        let errors = GlobalSettingsBuilder::new()
316            .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
317            .build()
318            .unwrap_err();
319
320        assert_eq!(errors.len(), 1);
321        assert_eq!(
322            errors.first().unwrap().to_string(),
323            "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
324        );
325    }
326
327    #[test]
328    fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_bootnodes_addresses_are_invalid(
329    ) {
330        let errors = GlobalSettingsBuilder::new()
331            .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
332            .build()
333            .unwrap_err();
334
335        assert_eq!(errors.len(), 2);
336        assert_eq!(
337            errors.first().unwrap().to_string(),
338            "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
339        );
340        assert_eq!(
341            errors.get(1).unwrap().to_string(),
342            "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
343        );
344    }
345
346    #[test]
347    fn global_settings_builder_should_fails_and_returns_an_error_if_local_ip_is_invalid() {
348        let errors = GlobalSettingsBuilder::new()
349            .with_local_ip("invalid")
350            .build()
351            .unwrap_err();
352
353        assert_eq!(errors.len(), 1);
354        assert_eq!(
355            errors.first().unwrap().to_string(),
356            "global_settings.local_ip: invalid IP address syntax"
357        );
358    }
359
360    #[test]
361    fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
362    ) {
363        let errors = GlobalSettingsBuilder::new()
364            .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
365            .with_local_ip("invalid")
366            .build()
367            .unwrap_err();
368
369        assert_eq!(errors.len(), 3);
370        assert_eq!(
371            errors.first().unwrap().to_string(),
372            "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
373        );
374        assert_eq!(
375            errors.get(1).unwrap().to_string(),
376            "global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
377        );
378        assert_eq!(
379            errors.get(2).unwrap().to_string(),
380            "global_settings.local_ip: invalid IP address syntax"
381        );
382    }
383}