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