ant_node_manager/add_services/
config.rs

1// Copyright (C) 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use ant_bootstrap::InitialPeersConfig;
10use ant_evm::{EvmNetwork, RewardsAddress};
11use ant_logging::LogFormat;
12use ant_service_management::node::push_arguments_from_initial_peers_config;
13use color_eyre::{Result, eyre::eyre};
14use service_manager::{RestartPolicy, ServiceInstallCtx, ServiceLabel};
15use std::{
16    ffi::OsString,
17    net::{Ipv4Addr, SocketAddr},
18    path::PathBuf,
19    str::FromStr,
20};
21
22#[derive(Clone, Debug)]
23pub enum PortRange {
24    Single(u16),
25    Range(u16, u16),
26}
27
28impl PortRange {
29    pub fn parse(s: &str) -> Result<Self> {
30        if let Ok(port) = u16::from_str(s) {
31            Ok(Self::Single(port))
32        } else {
33            let parts: Vec<&str> = s.split('-').collect();
34            if parts.len() != 2 {
35                return Err(eyre!("Port range must be in the format 'start-end'"));
36            }
37            let start = parts[0].parse::<u16>()?;
38            let end = parts[1].parse::<u16>()?;
39            if start >= end {
40                return Err(eyre!("End port must be greater than start port"));
41            }
42            Ok(Self::Range(start, end))
43        }
44    }
45
46    /// Validate the port range against a count to make sure the correct number of ports are provided.
47    pub fn validate(&self, count: u16) -> Result<()> {
48        match self {
49            Self::Single(_) => {
50                if count != 1 {
51                    error!("The count ({count}) does not match the number of ports (1)");
52                    return Err(eyre!(
53                        "The count ({count}) does not match the number of ports (1)"
54                    ));
55                }
56            }
57            Self::Range(start, end) => {
58                let port_count = end - start + 1;
59                if count != port_count {
60                    error!("The count ({count}) does not match the number of ports ({port_count})");
61                    return Err(eyre!(
62                        "The count ({count}) does not match the number of ports ({port_count})"
63                    ));
64                }
65            }
66        }
67        Ok(())
68    }
69}
70
71#[derive(Debug, PartialEq)]
72pub struct InstallNodeServiceCtxBuilder {
73    pub alpha: bool,
74    pub antnode_path: PathBuf,
75    pub autostart: bool,
76    pub data_dir_path: PathBuf,
77    pub env_variables: Option<Vec<(String, String)>>,
78    pub evm_network: EvmNetwork,
79    pub init_peers_config: InitialPeersConfig,
80    pub log_dir_path: PathBuf,
81    pub log_format: Option<LogFormat>,
82    pub max_archived_log_files: Option<usize>,
83    pub max_log_files: Option<usize>,
84    pub metrics_port: Option<u16>,
85    pub name: String,
86    pub network_id: Option<u8>,
87    pub no_upnp: bool,
88    pub node_ip: Option<Ipv4Addr>,
89    pub node_port: Option<u16>,
90    pub relay: bool,
91    pub restart_policy: RestartPolicy,
92    pub rewards_address: RewardsAddress,
93    pub rpc_socket_addr: SocketAddr,
94    pub service_user: Option<String>,
95    pub stop_on_upgrade: bool,
96    pub write_older_cache_files: bool,
97}
98
99impl InstallNodeServiceCtxBuilder {
100    pub fn build(self) -> Result<ServiceInstallCtx> {
101        let label: ServiceLabel = self.name.parse()?;
102        let mut args = vec![
103            OsString::from("--rpc"),
104            OsString::from(self.rpc_socket_addr.to_string()),
105            OsString::from("--root-dir"),
106            OsString::from(self.data_dir_path.to_string_lossy().to_string()),
107            OsString::from("--log-output-dest"),
108            OsString::from(self.log_dir_path.to_string_lossy().to_string()),
109        ];
110
111        push_arguments_from_initial_peers_config(&self.init_peers_config, &mut args);
112        if self.alpha {
113            args.push(OsString::from("--alpha"));
114        }
115        if let Some(id) = self.network_id {
116            args.push(OsString::from("--network-id"));
117            args.push(OsString::from(id.to_string()));
118        }
119        if self.relay {
120            args.push(OsString::from("--relay"));
121        }
122        if let Some(log_format) = self.log_format {
123            args.push(OsString::from("--log-format"));
124            args.push(OsString::from(log_format.as_str()));
125        }
126        if self.no_upnp {
127            args.push(OsString::from("--no-upnp"));
128        }
129        if let Some(node_ip) = self.node_ip {
130            args.push(OsString::from("--ip"));
131            args.push(OsString::from(node_ip.to_string()));
132        }
133        if let Some(node_port) = self.node_port {
134            args.push(OsString::from("--port"));
135            args.push(OsString::from(node_port.to_string()));
136        }
137        if let Some(metrics_port) = self.metrics_port {
138            args.push(OsString::from("--metrics-server-port"));
139            args.push(OsString::from(metrics_port.to_string()));
140        }
141        if let Some(log_files) = self.max_archived_log_files {
142            args.push(OsString::from("--max-archived-log-files"));
143            args.push(OsString::from(log_files.to_string()));
144        }
145        if let Some(log_files) = self.max_log_files {
146            args.push(OsString::from("--max-log-files"));
147            args.push(OsString::from(log_files.to_string()));
148        }
149
150        args.push(OsString::from("--rewards-address"));
151        args.push(OsString::from(self.rewards_address.to_string()));
152        if self.write_older_cache_files {
153            args.push(OsString::from("--write-older-cache-files"));
154        }
155
156        if self.stop_on_upgrade {
157            args.push(OsString::from("--stop-on-upgrade"));
158        }
159
160        // The EVM details must always be the last arguments.
161        args.push(OsString::from(self.evm_network.to_string()));
162        if let EvmNetwork::Custom(custom_network) = &self.evm_network {
163            args.push(OsString::from("--rpc-url"));
164            args.push(OsString::from(custom_network.rpc_url_http.to_string()));
165            args.push(OsString::from("--payment-token-address"));
166            args.push(OsString::from(
167                custom_network.payment_token_address.to_string(),
168            ));
169            args.push(OsString::from("--data-payments-address"));
170            args.push(OsString::from(
171                custom_network.data_payments_address.to_string(),
172            ));
173            if let Some(merkle_payments_address) = custom_network.merkle_payments_address {
174                args.push(OsString::from("--merkle-payments-address"));
175                args.push(OsString::from(merkle_payments_address.to_string()));
176            }
177        }
178
179        Ok(ServiceInstallCtx {
180            args,
181            autostart: self.autostart,
182            contents: None,
183            environment: self.env_variables,
184            label: label.clone(),
185            program: self.antnode_path.to_path_buf(),
186            restart_policy: self.restart_policy,
187            username: self.service_user.clone(),
188            working_directory: None,
189        })
190    }
191}
192
193pub struct AddNodeServiceOptions {
194    pub alpha: bool,
195    pub antnode_dir_path: PathBuf,
196    pub antnode_src_path: PathBuf,
197    pub auto_restart: bool,
198    pub auto_set_nat_flags: bool,
199    pub count: Option<u16>,
200    pub delete_antnode_src: bool,
201    pub enable_metrics_server: bool,
202    pub env_variables: Option<Vec<(String, String)>>,
203    pub evm_network: EvmNetwork,
204    pub init_peers_config: InitialPeersConfig,
205    pub log_format: Option<LogFormat>,
206    pub max_archived_log_files: Option<usize>,
207    pub max_log_files: Option<usize>,
208    pub metrics_port: Option<PortRange>,
209    pub network_id: Option<u8>,
210    pub node_ip: Option<Ipv4Addr>,
211    pub node_port: Option<PortRange>,
212    pub no_upnp: bool,
213    pub relay: bool,
214    pub restart_policy: RestartPolicy,
215    pub rewards_address: RewardsAddress,
216    pub rpc_address: Option<Ipv4Addr>,
217    pub rpc_port: Option<PortRange>,
218    pub service_data_dir_path: PathBuf,
219    pub service_log_dir_path: PathBuf,
220    pub user: Option<String>,
221    pub user_mode: bool,
222    pub version: String,
223    pub write_older_cache_files: bool,
224}
225
226pub struct AddDaemonServiceOptions {
227    pub address: Ipv4Addr,
228    pub env_variables: Option<Vec<(String, String)>>,
229    pub daemon_install_bin_path: PathBuf,
230    pub daemon_src_bin_path: PathBuf,
231    pub port: u16,
232    pub user: String,
233    pub version: String,
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use ant_evm::{CustomNetwork, RewardsAddress};
240    use std::net::{IpAddr, Ipv4Addr};
241
242    fn create_default_builder() -> InstallNodeServiceCtxBuilder {
243        InstallNodeServiceCtxBuilder {
244            alpha: false,
245            antnode_path: PathBuf::from("/bin/antnode"),
246            autostart: true,
247            data_dir_path: PathBuf::from("/data"),
248            env_variables: None,
249            evm_network: EvmNetwork::ArbitrumOne,
250            relay: false,
251            log_dir_path: PathBuf::from("/logs"),
252            log_format: None,
253            max_archived_log_files: None,
254            max_log_files: None,
255            metrics_port: None,
256            name: "test-node".to_string(),
257            network_id: None,
258            no_upnp: false,
259            node_ip: None,
260            node_port: None,
261            init_peers_config: InitialPeersConfig::default(),
262            rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
263                .unwrap(),
264            restart_policy: RestartPolicy::OnFailure { delay_secs: None },
265            rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
266            service_user: None,
267            stop_on_upgrade: true,
268            write_older_cache_files: false,
269        }
270    }
271
272    fn create_custom_evm_network_builder() -> InstallNodeServiceCtxBuilder {
273        InstallNodeServiceCtxBuilder {
274            alpha: false,
275            antnode_path: PathBuf::from("/bin/antnode"),
276            autostart: true,
277            data_dir_path: PathBuf::from("/data"),
278            env_variables: None,
279            evm_network: EvmNetwork::Custom(CustomNetwork {
280                rpc_url_http: "http://localhost:8545".parse().unwrap(),
281                payment_token_address: RewardsAddress::from_str(
282                    "0x5FbDB2315678afecb367f032d93F642f64180aa3",
283                )
284                .unwrap(),
285                data_payments_address: RewardsAddress::from_str(
286                    "0x8464135c8F25Da09e49BC8782676a84730C318bC",
287                )
288                .unwrap(),
289                merkle_payments_address: None,
290            }),
291            init_peers_config: InitialPeersConfig::default(),
292            log_dir_path: PathBuf::from("/logs"),
293            log_format: None,
294            max_archived_log_files: None,
295            max_log_files: None,
296            metrics_port: None,
297            name: "test-node".to_string(),
298            network_id: None,
299            no_upnp: false,
300            node_ip: None,
301            node_port: None,
302            relay: false,
303            restart_policy: RestartPolicy::OnSuccess { delay_secs: None },
304            rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
305                .unwrap(),
306            rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
307            service_user: None,
308            stop_on_upgrade: false,
309            write_older_cache_files: false,
310        }
311    }
312
313    fn create_builder_with_all_options_enabled() -> InstallNodeServiceCtxBuilder {
314        InstallNodeServiceCtxBuilder {
315            alpha: true,
316            antnode_path: PathBuf::from("/bin/antnode"),
317            autostart: true,
318            data_dir_path: PathBuf::from("/data"),
319            env_variables: None,
320            evm_network: EvmNetwork::Custom(CustomNetwork {
321                rpc_url_http: "http://localhost:8545".parse().unwrap(),
322                payment_token_address: RewardsAddress::from_str(
323                    "0x5FbDB2315678afecb367f032d93F642f64180aa3",
324                )
325                .unwrap(),
326                data_payments_address: RewardsAddress::from_str(
327                    "0x8464135c8F25Da09e49BC8782676a84730C318bC",
328                )
329                .unwrap(),
330                merkle_payments_address: Some(
331                    RewardsAddress::from_str("0x742D35CC6634C0532925A3B844BC9E7595F0BE3A").unwrap(),
332                ),
333            }),
334            log_dir_path: PathBuf::from("/logs"),
335            log_format: None,
336            init_peers_config: InitialPeersConfig::default(),
337            max_archived_log_files: Some(10),
338            max_log_files: Some(10),
339            metrics_port: None,
340            name: "test-node".to_string(),
341            network_id: Some(5),
342            no_upnp: false,
343            node_ip: None,
344            node_port: None,
345            relay: false,
346            restart_policy: RestartPolicy::OnSuccess { delay_secs: None },
347            rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
348                .unwrap(),
349            rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
350            service_user: None,
351            stop_on_upgrade: true,
352            write_older_cache_files: false,
353        }
354    }
355
356    #[test]
357    fn build_should_assign_expected_values_when_mandatory_options_are_provided() {
358        let builder = create_default_builder();
359        let result = builder.build().unwrap();
360
361        assert_eq!(result.label.to_string(), "test-node");
362        assert_eq!(result.program, PathBuf::from("/bin/antnode"));
363        assert!(result.autostart);
364        assert_eq!(result.username, None);
365        assert_eq!(result.working_directory, None);
366
367        let expected_args = vec![
368            "--rpc",
369            "127.0.0.1:8080",
370            "--root-dir",
371            "/data",
372            "--log-output-dest",
373            "/logs",
374            "--rewards-address",
375            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
376            "--stop-on-upgrade",
377            "evm-arbitrum-one",
378        ];
379        assert_eq!(
380            result
381                .args
382                .iter()
383                .map(|os| os.to_str().unwrap())
384                .collect::<Vec<_>>(),
385            expected_args
386        );
387    }
388
389    #[test]
390    fn build_should_assign_expected_values_when_a_custom_evm_network_is_provided() {
391        let builder = create_custom_evm_network_builder();
392        let result = builder.build().unwrap();
393
394        assert_eq!(result.label.to_string(), "test-node");
395        assert_eq!(result.program, PathBuf::from("/bin/antnode"));
396        assert!(result.autostart);
397        assert_eq!(result.username, None);
398        assert_eq!(result.working_directory, None);
399
400        let expected_args = vec![
401            "--rpc",
402            "127.0.0.1:8080",
403            "--root-dir",
404            "/data",
405            "--log-output-dest",
406            "/logs",
407            "--rewards-address",
408            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
409            "evm-custom",
410            "--rpc-url",
411            "http://localhost:8545/",
412            "--payment-token-address",
413            "0x5FbDB2315678afecb367f032d93F642f64180aa3",
414            "--data-payments-address",
415            "0x8464135c8F25Da09e49BC8782676a84730C318bC",
416        ];
417        assert_eq!(
418            result
419                .args
420                .iter()
421                .map(|os| os.to_str().unwrap())
422                .collect::<Vec<_>>(),
423            expected_args
424        );
425    }
426
427    #[test]
428    fn build_should_assign_expected_values_when_all_options_are_enabled() {
429        let mut builder = create_builder_with_all_options_enabled();
430        builder.alpha = true;
431        builder.relay = true;
432        builder.log_format = Some(LogFormat::Json);
433        builder.no_upnp = true;
434        builder.node_ip = Some(Ipv4Addr::new(192, 168, 1, 1));
435        builder.node_port = Some(12345);
436        builder.metrics_port = Some(9090);
437        builder.init_peers_config.addrs = vec![
438            "/ip4/127.0.0.1/tcp/8080".parse().unwrap(),
439            "/ip4/192.168.1.1/tcp/8081".parse().unwrap(),
440        ];
441        builder.init_peers_config.first = true;
442        builder.init_peers_config.local = true;
443        builder.init_peers_config.network_contacts_url =
444            vec!["http://localhost:8080".parse().unwrap()];
445        builder.init_peers_config.ignore_cache = true;
446        builder.service_user = Some("antnode-user".to_string());
447        builder.write_older_cache_files = true;
448
449        let result = builder.build().unwrap();
450
451        let expected_args = vec![
452            "--rpc",
453            "127.0.0.1:8080",
454            "--root-dir",
455            "/data",
456            "--log-output-dest",
457            "/logs",
458            "--first",
459            "--local",
460            "--peer",
461            "/ip4/127.0.0.1/tcp/8080,/ip4/192.168.1.1/tcp/8081",
462            "--network-contacts-url",
463            "http://localhost:8080",
464            "--ignore-cache",
465            "--alpha",
466            "--network-id",
467            "5",
468            "--relay",
469            "--log-format",
470            "json",
471            "--no-upnp",
472            "--ip",
473            "192.168.1.1",
474            "--port",
475            "12345",
476            "--metrics-server-port",
477            "9090",
478            "--max-archived-log-files",
479            "10",
480            "--max-log-files",
481            "10",
482            "--rewards-address",
483            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
484            "--write-older-cache-files",
485            "--stop-on-upgrade",
486            "evm-custom",
487            "--rpc-url",
488            "http://localhost:8545/",
489            "--payment-token-address",
490            "0x5FbDB2315678afecb367f032d93F642f64180aa3",
491            "--data-payments-address",
492            "0x8464135c8F25Da09e49BC8782676a84730C318bC",
493            "--merkle-payments-address",
494            "0x742d35cC6634C0532925A3b844bC9E7595F0be3A",
495        ];
496        assert_eq!(
497            result
498                .args
499                .iter()
500                .map(|os| os.to_str().unwrap())
501                .collect::<Vec<_>>(),
502            expected_args
503        );
504        assert_eq!(result.username, Some("antnode-user".to_string()));
505    }
506
507    #[test]
508    fn build_should_assign_expected_values_when_environment_variables_are_provided() {
509        let mut builder = create_default_builder();
510        builder.env_variables = Some(vec![
511            ("VAR1".to_string(), "value1".to_string()),
512            ("VAR2".to_string(), "value2".to_string()),
513        ]);
514
515        let result = builder.build().unwrap();
516
517        assert_eq!(
518            result.environment,
519            Some(vec![
520                ("VAR1".to_string(), "value1".to_string()),
521                ("VAR2".to_string(), "value2".to_string()),
522            ])
523        );
524    }
525}