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::{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 log_dir_path: PathBuf,
80    pub log_format: Option<LogFormat>,
81    pub name: String,
82    pub network_id: Option<u8>,
83    pub no_upnp: bool,
84    pub max_archived_log_files: Option<usize>,
85    pub max_log_files: Option<usize>,
86    pub metrics_port: Option<u16>,
87    pub node_ip: Option<Ipv4Addr>,
88    pub node_port: Option<u16>,
89    pub init_peers_config: InitialPeersConfig,
90    pub rewards_address: RewardsAddress,
91    pub relay: bool,
92    pub rpc_socket_addr: SocketAddr,
93    pub service_user: Option<String>,
94    pub write_older_cache_files: bool,
95}
96
97impl InstallNodeServiceCtxBuilder {
98    pub fn build(self) -> Result<ServiceInstallCtx> {
99        let label: ServiceLabel = self.name.parse()?;
100        let mut args = vec![
101            OsString::from("--rpc"),
102            OsString::from(self.rpc_socket_addr.to_string()),
103            OsString::from("--root-dir"),
104            OsString::from(self.data_dir_path.to_string_lossy().to_string()),
105            OsString::from("--log-output-dest"),
106            OsString::from(self.log_dir_path.to_string_lossy().to_string()),
107        ];
108
109        push_arguments_from_initial_peers_config(&self.init_peers_config, &mut args);
110        if self.alpha {
111            args.push(OsString::from("--alpha"));
112        }
113        if let Some(id) = self.network_id {
114            args.push(OsString::from("--network-id"));
115            args.push(OsString::from(id.to_string()));
116        }
117        if self.relay {
118            args.push(OsString::from("--relay"));
119        }
120        if let Some(log_format) = self.log_format {
121            args.push(OsString::from("--log-format"));
122            args.push(OsString::from(log_format.as_str()));
123        }
124        if self.no_upnp {
125            args.push(OsString::from("--no-upnp"));
126        }
127        if let Some(node_ip) = self.node_ip {
128            args.push(OsString::from("--ip"));
129            args.push(OsString::from(node_ip.to_string()));
130        }
131        if let Some(node_port) = self.node_port {
132            args.push(OsString::from("--port"));
133            args.push(OsString::from(node_port.to_string()));
134        }
135        if let Some(metrics_port) = self.metrics_port {
136            args.push(OsString::from("--metrics-server-port"));
137            args.push(OsString::from(metrics_port.to_string()));
138        }
139        if let Some(log_files) = self.max_archived_log_files {
140            args.push(OsString::from("--max-archived-log-files"));
141            args.push(OsString::from(log_files.to_string()));
142        }
143        if let Some(log_files) = self.max_log_files {
144            args.push(OsString::from("--max-log-files"));
145            args.push(OsString::from(log_files.to_string()));
146        }
147
148        args.push(OsString::from("--rewards-address"));
149        args.push(OsString::from(self.rewards_address.to_string()));
150        if self.write_older_cache_files {
151            args.push(OsString::from("--write-older-cache-files"));
152        }
153
154        args.push(OsString::from(self.evm_network.to_string()));
155        if let EvmNetwork::Custom(custom_network) = &self.evm_network {
156            args.push(OsString::from("--rpc-url"));
157            args.push(OsString::from(custom_network.rpc_url_http.to_string()));
158            args.push(OsString::from("--payment-token-address"));
159            args.push(OsString::from(
160                custom_network.payment_token_address.to_string(),
161            ));
162            args.push(OsString::from("--data-payments-address"));
163            args.push(OsString::from(
164                custom_network.data_payments_address.to_string(),
165            ));
166        }
167
168        Ok(ServiceInstallCtx {
169            args,
170            autostart: self.autostart,
171            contents: None,
172            environment: self.env_variables,
173            label: label.clone(),
174            program: self.antnode_path.to_path_buf(),
175            username: self.service_user.clone(),
176            working_directory: None,
177            disable_restart_on_failure: true,
178        })
179    }
180}
181
182pub struct AddNodeServiceOptions {
183    pub alpha: bool,
184    pub antnode_dir_path: PathBuf,
185    pub antnode_src_path: PathBuf,
186    pub auto_restart: bool,
187    pub auto_set_nat_flags: bool,
188    pub count: Option<u16>,
189    pub delete_antnode_src: bool,
190    pub enable_metrics_server: bool,
191    pub env_variables: Option<Vec<(String, String)>>,
192    pub evm_network: EvmNetwork,
193    pub init_peers_config: InitialPeersConfig,
194    pub log_format: Option<LogFormat>,
195    pub max_archived_log_files: Option<usize>,
196    pub max_log_files: Option<usize>,
197    pub metrics_port: Option<PortRange>,
198    pub network_id: Option<u8>,
199    pub node_ip: Option<Ipv4Addr>,
200    pub node_port: Option<PortRange>,
201    pub no_upnp: bool,
202    pub relay: bool,
203    pub rewards_address: RewardsAddress,
204    pub rpc_address: Option<Ipv4Addr>,
205    pub rpc_port: Option<PortRange>,
206    pub service_data_dir_path: PathBuf,
207    pub service_log_dir_path: PathBuf,
208    pub user: Option<String>,
209    pub user_mode: bool,
210    pub version: String,
211    pub write_older_cache_files: bool,
212}
213
214pub struct AddDaemonServiceOptions {
215    pub address: Ipv4Addr,
216    pub env_variables: Option<Vec<(String, String)>>,
217    pub daemon_install_bin_path: PathBuf,
218    pub daemon_src_bin_path: PathBuf,
219    pub port: u16,
220    pub user: String,
221    pub version: String,
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use ant_evm::{CustomNetwork, RewardsAddress};
228    use std::net::{IpAddr, Ipv4Addr};
229
230    fn create_default_builder() -> InstallNodeServiceCtxBuilder {
231        InstallNodeServiceCtxBuilder {
232            alpha: false,
233            antnode_path: PathBuf::from("/bin/antnode"),
234            autostart: true,
235            data_dir_path: PathBuf::from("/data"),
236            env_variables: None,
237            evm_network: EvmNetwork::ArbitrumOne,
238            relay: false,
239            log_dir_path: PathBuf::from("/logs"),
240            log_format: None,
241            max_archived_log_files: None,
242            max_log_files: None,
243            metrics_port: None,
244            name: "test-node".to_string(),
245            network_id: None,
246            no_upnp: false,
247            node_ip: None,
248            node_port: None,
249            init_peers_config: InitialPeersConfig::default(),
250            rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
251                .unwrap(),
252            rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
253            service_user: None,
254            write_older_cache_files: false,
255        }
256    }
257
258    fn create_custom_evm_network_builder() -> InstallNodeServiceCtxBuilder {
259        InstallNodeServiceCtxBuilder {
260            alpha: false,
261            autostart: true,
262            data_dir_path: PathBuf::from("/data"),
263            env_variables: None,
264            evm_network: EvmNetwork::Custom(CustomNetwork {
265                rpc_url_http: "http://localhost:8545".parse().unwrap(),
266                payment_token_address: RewardsAddress::from_str(
267                    "0x5FbDB2315678afecb367f032d93F642f64180aa3",
268                )
269                .unwrap(),
270                data_payments_address: RewardsAddress::from_str(
271                    "0x8464135c8F25Da09e49BC8782676a84730C318bC",
272                )
273                .unwrap(),
274            }),
275            relay: false,
276            log_dir_path: PathBuf::from("/logs"),
277            log_format: None,
278            max_archived_log_files: None,
279            max_log_files: None,
280            metrics_port: None,
281            name: "test-node".to_string(),
282            network_id: None,
283            node_ip: None,
284            node_port: None,
285            init_peers_config: InitialPeersConfig::default(),
286            rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
287                .unwrap(),
288            rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
289            antnode_path: PathBuf::from("/bin/antnode"),
290            service_user: None,
291            no_upnp: false,
292            write_older_cache_files: false,
293        }
294    }
295
296    fn create_builder_with_all_options_enabled() -> InstallNodeServiceCtxBuilder {
297        InstallNodeServiceCtxBuilder {
298            alpha: true,
299            autostart: true,
300            data_dir_path: PathBuf::from("/data"),
301            env_variables: None,
302            evm_network: EvmNetwork::Custom(CustomNetwork {
303                rpc_url_http: "http://localhost:8545".parse().unwrap(),
304                payment_token_address: RewardsAddress::from_str(
305                    "0x5FbDB2315678afecb367f032d93F642f64180aa3",
306                )
307                .unwrap(),
308                data_payments_address: RewardsAddress::from_str(
309                    "0x8464135c8F25Da09e49BC8782676a84730C318bC",
310                )
311                .unwrap(),
312            }),
313            relay: false,
314            log_dir_path: PathBuf::from("/logs"),
315            log_format: None,
316            max_archived_log_files: Some(10),
317            max_log_files: Some(10),
318            metrics_port: None,
319            name: "test-node".to_string(),
320            network_id: Some(5),
321            node_ip: None,
322            node_port: None,
323            init_peers_config: InitialPeersConfig::default(),
324            rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
325                .unwrap(),
326            rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
327            antnode_path: PathBuf::from("/bin/antnode"),
328            service_user: None,
329            no_upnp: false,
330            write_older_cache_files: false,
331        }
332    }
333
334    #[test]
335    fn build_should_assign_expected_values_when_mandatory_options_are_provided() {
336        let builder = create_default_builder();
337        let result = builder.build().unwrap();
338
339        assert_eq!(result.label.to_string(), "test-node");
340        assert_eq!(result.program, PathBuf::from("/bin/antnode"));
341        assert!(result.autostart);
342        assert_eq!(result.username, None);
343        assert_eq!(result.working_directory, None);
344
345        let expected_args = vec![
346            "--rpc",
347            "127.0.0.1:8080",
348            "--root-dir",
349            "/data",
350            "--log-output-dest",
351            "/logs",
352            "--rewards-address",
353            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
354            "evm-arbitrum-one",
355        ];
356        assert_eq!(
357            result
358                .args
359                .iter()
360                .map(|os| os.to_str().unwrap())
361                .collect::<Vec<_>>(),
362            expected_args
363        );
364    }
365
366    #[test]
367    fn build_should_assign_expected_values_when_a_custom_evm_network_is_provided() {
368        let builder = create_custom_evm_network_builder();
369        let result = builder.build().unwrap();
370
371        assert_eq!(result.label.to_string(), "test-node");
372        assert_eq!(result.program, PathBuf::from("/bin/antnode"));
373        assert!(result.autostart);
374        assert_eq!(result.username, None);
375        assert_eq!(result.working_directory, None);
376
377        let expected_args = vec![
378            "--rpc",
379            "127.0.0.1:8080",
380            "--root-dir",
381            "/data",
382            "--log-output-dest",
383            "/logs",
384            "--rewards-address",
385            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
386            "evm-custom",
387            "--rpc-url",
388            "http://localhost:8545/",
389            "--payment-token-address",
390            "0x5FbDB2315678afecb367f032d93F642f64180aa3",
391            "--data-payments-address",
392            "0x8464135c8F25Da09e49BC8782676a84730C318bC",
393        ];
394        assert_eq!(
395            result
396                .args
397                .iter()
398                .map(|os| os.to_str().unwrap())
399                .collect::<Vec<_>>(),
400            expected_args
401        );
402    }
403
404    #[test]
405    fn build_should_assign_expected_values_when_all_options_are_enabled() {
406        let mut builder = create_builder_with_all_options_enabled();
407        builder.alpha = true;
408        builder.relay = true;
409        builder.log_format = Some(LogFormat::Json);
410        builder.no_upnp = true;
411        builder.node_ip = Some(Ipv4Addr::new(192, 168, 1, 1));
412        builder.node_port = Some(12345);
413        builder.metrics_port = Some(9090);
414        builder.init_peers_config.addrs = vec![
415            "/ip4/127.0.0.1/tcp/8080".parse().unwrap(),
416            "/ip4/192.168.1.1/tcp/8081".parse().unwrap(),
417        ];
418        builder.init_peers_config.first = true;
419        builder.init_peers_config.local = true;
420        builder.init_peers_config.network_contacts_url =
421            vec!["http://localhost:8080".parse().unwrap()];
422        builder.init_peers_config.ignore_cache = true;
423        builder.service_user = Some("antnode-user".to_string());
424        builder.write_older_cache_files = true;
425
426        let result = builder.build().unwrap();
427
428        let expected_args = vec![
429            "--rpc",
430            "127.0.0.1:8080",
431            "--root-dir",
432            "/data",
433            "--log-output-dest",
434            "/logs",
435            "--first",
436            "--local",
437            "--peer",
438            "/ip4/127.0.0.1/tcp/8080,/ip4/192.168.1.1/tcp/8081",
439            "--network-contacts-url",
440            "http://localhost:8080",
441            "--ignore-cache",
442            "--alpha",
443            "--network-id",
444            "5",
445            "--relay",
446            "--log-format",
447            "json",
448            "--no-upnp",
449            "--ip",
450            "192.168.1.1",
451            "--port",
452            "12345",
453            "--metrics-server-port",
454            "9090",
455            "--max-archived-log-files",
456            "10",
457            "--max-log-files",
458            "10",
459            "--rewards-address",
460            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
461            "--write-older-cache-files",
462            "evm-custom",
463            "--rpc-url",
464            "http://localhost:8545/",
465            "--payment-token-address",
466            "0x5FbDB2315678afecb367f032d93F642f64180aa3",
467            "--data-payments-address",
468            "0x8464135c8F25Da09e49BC8782676a84730C318bC",
469        ];
470        assert_eq!(
471            result
472                .args
473                .iter()
474                .map(|os| os.to_str().unwrap())
475                .collect::<Vec<_>>(),
476            expected_args
477        );
478        assert_eq!(result.username, Some("antnode-user".to_string()));
479    }
480
481    #[test]
482    fn build_should_assign_expected_values_when_environment_variables_are_provided() {
483        let mut builder = create_default_builder();
484        builder.env_variables = Some(vec![
485            ("VAR1".to_string(), "value1".to_string()),
486            ("VAR2".to_string(), "value2".to_string()),
487        ]);
488
489        let result = builder.build().unwrap();
490
491        assert_eq!(
492            result.environment,
493            Some(vec![
494                ("VAR1".to_string(), "value1".to_string()),
495                ("VAR2".to_string(), "value2".to_string()),
496            ])
497        );
498    }
499}