Skip to main content

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 {
265                delay_secs: None,
266                max_retries: None,
267                reset_after_secs: None,
268            },
269            rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
270            service_user: None,
271            stop_on_upgrade: true,
272            write_older_cache_files: false,
273        }
274    }
275
276    fn create_custom_evm_network_builder() -> InstallNodeServiceCtxBuilder {
277        InstallNodeServiceCtxBuilder {
278            alpha: false,
279            antnode_path: PathBuf::from("/bin/antnode"),
280            autostart: true,
281            data_dir_path: PathBuf::from("/data"),
282            env_variables: None,
283            evm_network: EvmNetwork::Custom(CustomNetwork {
284                rpc_url_http: "http://localhost:8545".parse().unwrap(),
285                payment_token_address: RewardsAddress::from_str(
286                    "0x5FbDB2315678afecb367f032d93F642f64180aa3",
287                )
288                .unwrap(),
289                data_payments_address: RewardsAddress::from_str(
290                    "0x8464135c8F25Da09e49BC8782676a84730C318bC",
291                )
292                .unwrap(),
293                merkle_payments_address: None,
294            }),
295            init_peers_config: InitialPeersConfig::default(),
296            log_dir_path: PathBuf::from("/logs"),
297            log_format: None,
298            max_archived_log_files: None,
299            max_log_files: None,
300            metrics_port: None,
301            name: "test-node".to_string(),
302            network_id: None,
303            no_upnp: false,
304            node_ip: None,
305            node_port: None,
306            relay: false,
307            restart_policy: RestartPolicy::OnSuccess { delay_secs: None },
308            rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
309                .unwrap(),
310            rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
311            service_user: None,
312            stop_on_upgrade: false,
313            write_older_cache_files: false,
314        }
315    }
316
317    fn create_builder_with_all_options_enabled() -> InstallNodeServiceCtxBuilder {
318        InstallNodeServiceCtxBuilder {
319            alpha: true,
320            antnode_path: PathBuf::from("/bin/antnode"),
321            autostart: true,
322            data_dir_path: PathBuf::from("/data"),
323            env_variables: None,
324            evm_network: EvmNetwork::Custom(CustomNetwork {
325                rpc_url_http: "http://localhost:8545".parse().unwrap(),
326                payment_token_address: RewardsAddress::from_str(
327                    "0x5FbDB2315678afecb367f032d93F642f64180aa3",
328                )
329                .unwrap(),
330                data_payments_address: RewardsAddress::from_str(
331                    "0x8464135c8F25Da09e49BC8782676a84730C318bC",
332                )
333                .unwrap(),
334                merkle_payments_address: Some(
335                    RewardsAddress::from_str("0x742D35CC6634C0532925A3B844BC9E7595F0BE3A").unwrap(),
336                ),
337            }),
338            log_dir_path: PathBuf::from("/logs"),
339            log_format: None,
340            init_peers_config: InitialPeersConfig::default(),
341            max_archived_log_files: Some(10),
342            max_log_files: Some(10),
343            metrics_port: None,
344            name: "test-node".to_string(),
345            network_id: Some(5),
346            no_upnp: false,
347            node_ip: None,
348            node_port: None,
349            relay: false,
350            restart_policy: RestartPolicy::OnSuccess { delay_secs: None },
351            rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
352                .unwrap(),
353            rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
354            service_user: None,
355            stop_on_upgrade: true,
356            write_older_cache_files: false,
357        }
358    }
359
360    #[test]
361    fn build_should_assign_expected_values_when_mandatory_options_are_provided() {
362        let builder = create_default_builder();
363        let result = builder.build().unwrap();
364
365        assert_eq!(result.label.to_string(), "test-node");
366        assert_eq!(result.program, PathBuf::from("/bin/antnode"));
367        assert!(result.autostart);
368        assert_eq!(result.username, None);
369        assert_eq!(result.working_directory, None);
370
371        let expected_args = vec![
372            "--rpc",
373            "127.0.0.1:8080",
374            "--root-dir",
375            "/data",
376            "--log-output-dest",
377            "/logs",
378            "--rewards-address",
379            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
380            "--stop-on-upgrade",
381            "evm-arbitrum-one",
382        ];
383        assert_eq!(
384            result
385                .args
386                .iter()
387                .map(|os| os.to_str().unwrap())
388                .collect::<Vec<_>>(),
389            expected_args
390        );
391    }
392
393    #[test]
394    fn build_should_assign_expected_values_when_a_custom_evm_network_is_provided() {
395        let builder = create_custom_evm_network_builder();
396        let result = builder.build().unwrap();
397
398        assert_eq!(result.label.to_string(), "test-node");
399        assert_eq!(result.program, PathBuf::from("/bin/antnode"));
400        assert!(result.autostart);
401        assert_eq!(result.username, None);
402        assert_eq!(result.working_directory, None);
403
404        let expected_args = vec![
405            "--rpc",
406            "127.0.0.1:8080",
407            "--root-dir",
408            "/data",
409            "--log-output-dest",
410            "/logs",
411            "--rewards-address",
412            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
413            "evm-custom",
414            "--rpc-url",
415            "http://localhost:8545/",
416            "--payment-token-address",
417            "0x5FbDB2315678afecb367f032d93F642f64180aa3",
418            "--data-payments-address",
419            "0x8464135c8F25Da09e49BC8782676a84730C318bC",
420        ];
421        assert_eq!(
422            result
423                .args
424                .iter()
425                .map(|os| os.to_str().unwrap())
426                .collect::<Vec<_>>(),
427            expected_args
428        );
429    }
430
431    #[test]
432    fn build_should_assign_expected_values_when_all_options_are_enabled() {
433        let mut builder = create_builder_with_all_options_enabled();
434        builder.alpha = true;
435        builder.relay = true;
436        builder.log_format = Some(LogFormat::Json);
437        builder.no_upnp = true;
438        builder.node_ip = Some(Ipv4Addr::new(192, 168, 1, 1));
439        builder.node_port = Some(12345);
440        builder.metrics_port = Some(9090);
441        builder.init_peers_config.addrs = vec![
442            "/ip4/127.0.0.1/tcp/8080".parse().unwrap(),
443            "/ip4/192.168.1.1/tcp/8081".parse().unwrap(),
444        ];
445        builder.init_peers_config.first = true;
446        builder.init_peers_config.local = true;
447        builder.init_peers_config.network_contacts_url =
448            vec!["http://localhost:8080".parse().unwrap()];
449        builder.init_peers_config.ignore_cache = true;
450        builder.service_user = Some("antnode-user".to_string());
451        builder.write_older_cache_files = true;
452
453        let result = builder.build().unwrap();
454
455        let expected_args = vec![
456            "--rpc",
457            "127.0.0.1:8080",
458            "--root-dir",
459            "/data",
460            "--log-output-dest",
461            "/logs",
462            "--first",
463            "--local",
464            "--peer",
465            "/ip4/127.0.0.1/tcp/8080,/ip4/192.168.1.1/tcp/8081",
466            "--network-contacts-url",
467            "http://localhost:8080",
468            "--ignore-cache",
469            "--alpha",
470            "--network-id",
471            "5",
472            "--relay",
473            "--log-format",
474            "json",
475            "--no-upnp",
476            "--ip",
477            "192.168.1.1",
478            "--port",
479            "12345",
480            "--metrics-server-port",
481            "9090",
482            "--max-archived-log-files",
483            "10",
484            "--max-log-files",
485            "10",
486            "--rewards-address",
487            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
488            "--write-older-cache-files",
489            "--stop-on-upgrade",
490            "evm-custom",
491            "--rpc-url",
492            "http://localhost:8545/",
493            "--payment-token-address",
494            "0x5FbDB2315678afecb367f032d93F642f64180aa3",
495            "--data-payments-address",
496            "0x8464135c8F25Da09e49BC8782676a84730C318bC",
497            "--merkle-payments-address",
498            "0x742d35cC6634C0532925A3b844bC9E7595F0be3A",
499        ];
500        assert_eq!(
501            result
502                .args
503                .iter()
504                .map(|os| os.to_str().unwrap())
505                .collect::<Vec<_>>(),
506            expected_args
507        );
508        assert_eq!(result.username, Some("antnode-user".to_string()));
509    }
510
511    #[test]
512    fn build_should_assign_expected_values_when_environment_variables_are_provided() {
513        let mut builder = create_default_builder();
514        builder.env_variables = Some(vec![
515            ("VAR1".to_string(), "value1".to_string()),
516            ("VAR2".to_string(), "value2".to_string()),
517        ]);
518
519        let result = builder.build().unwrap();
520
521        assert_eq!(
522            result.environment,
523            Some(vec![
524                ("VAR1".to_string(), "value1".to_string()),
525                ("VAR2".to_string(), "value2".to_string()),
526            ])
527        );
528    }
529}