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