sn_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 color_eyre::{eyre::eyre, Result};
10use libp2p::Multiaddr;
11use service_manager::{ServiceInstallCtx, ServiceLabel};
12use sn_evm::{EvmNetwork, RewardsAddress};
13use sn_logging::LogFormat;
14use std::{
15    ffi::OsString,
16    net::{Ipv4Addr, SocketAddr},
17    path::PathBuf,
18    str::FromStr,
19};
20
21#[derive(Clone, Debug)]
22pub enum PortRange {
23    Single(u16),
24    Range(u16, u16),
25}
26
27impl PortRange {
28    pub fn parse(s: &str) -> Result<Self> {
29        if let Ok(port) = u16::from_str(s) {
30            Ok(Self::Single(port))
31        } else {
32            let parts: Vec<&str> = s.split('-').collect();
33            if parts.len() != 2 {
34                return Err(eyre!("Port range must be in the format 'start-end'"));
35            }
36            let start = parts[0].parse::<u16>()?;
37            let end = parts[1].parse::<u16>()?;
38            if start >= end {
39                return Err(eyre!("End port must be greater than start port"));
40            }
41            Ok(Self::Range(start, end))
42        }
43    }
44
45    /// Validate the port range against a count to make sure the correct number of ports are provided.
46    pub fn validate(&self, count: u16) -> Result<()> {
47        match self {
48            Self::Single(_) => {
49                if count != 1 {
50                    error!("The count ({count}) does not match the number of ports (1)");
51                    return Err(eyre!(
52                        "The count ({count}) does not match the number of ports (1)"
53                    ));
54                }
55            }
56            Self::Range(start, end) => {
57                let port_count = end - start + 1;
58                if count != port_count {
59                    error!("The count ({count}) does not match the number of ports ({port_count})");
60                    return Err(eyre!(
61                        "The count ({count}) does not match the number of ports ({port_count})"
62                    ));
63                }
64            }
65        }
66        Ok(())
67    }
68}
69
70#[derive(Debug, PartialEq)]
71pub struct InstallNodeServiceCtxBuilder {
72    pub autostart: bool,
73    pub bootstrap_peers: Vec<Multiaddr>,
74    pub data_dir_path: PathBuf,
75    pub env_variables: Option<Vec<(String, String)>>,
76    pub evm_network: EvmNetwork,
77    pub genesis: bool,
78    pub home_network: bool,
79    pub local: bool,
80    pub log_dir_path: PathBuf,
81    pub log_format: Option<LogFormat>,
82    pub name: String,
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 owner: Option<String>,
89    pub rewards_address: RewardsAddress,
90    pub rpc_socket_addr: SocketAddr,
91    pub safenode_path: PathBuf,
92    pub service_user: Option<String>,
93    pub upnp: bool,
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        if self.genesis {
109            args.push(OsString::from("--first"));
110        }
111        if self.home_network {
112            args.push(OsString::from("--home-network"));
113        }
114        if self.local {
115            args.push(OsString::from("--local"));
116        }
117        if let Some(log_format) = self.log_format {
118            args.push(OsString::from("--log-format"));
119            args.push(OsString::from(log_format.as_str()));
120        }
121        if self.upnp {
122            args.push(OsString::from("--upnp"));
123        }
124        if let Some(node_ip) = self.node_ip {
125            args.push(OsString::from("--ip"));
126            args.push(OsString::from(node_ip.to_string()));
127        }
128        if let Some(node_port) = self.node_port {
129            args.push(OsString::from("--port"));
130            args.push(OsString::from(node_port.to_string()));
131        }
132        if let Some(metrics_port) = self.metrics_port {
133            args.push(OsString::from("--metrics-server-port"));
134            args.push(OsString::from(metrics_port.to_string()));
135        }
136        if let Some(owner) = self.owner {
137            args.push(OsString::from("--owner"));
138            args.push(OsString::from(owner));
139        }
140        if let Some(log_files) = self.max_archived_log_files {
141            args.push(OsString::from("--max-archived-log-files"));
142            args.push(OsString::from(log_files.to_string()));
143        }
144        if let Some(log_files) = self.max_log_files {
145            args.push(OsString::from("--max-log-files"));
146            args.push(OsString::from(log_files.to_string()));
147        }
148
149        if !self.bootstrap_peers.is_empty() {
150            let peers_str = self
151                .bootstrap_peers
152                .iter()
153                .map(|peer| peer.to_string())
154                .collect::<Vec<_>>()
155                .join(",");
156            args.push(OsString::from("--peer"));
157            args.push(OsString::from(peers_str));
158        }
159
160        args.push(OsString::from("--rewards-address"));
161        args.push(OsString::from(self.rewards_address.to_string()));
162
163        args.push(OsString::from(self.evm_network.to_string()));
164        if let EvmNetwork::Custom(custom_network) = &self.evm_network {
165            args.push(OsString::from("--rpc-url"));
166            args.push(OsString::from(custom_network.rpc_url_http.to_string()));
167            args.push(OsString::from("--payment-token-address"));
168            args.push(OsString::from(
169                custom_network.payment_token_address.to_string(),
170            ));
171            args.push(OsString::from("--data-payments-address"));
172            args.push(OsString::from(
173                custom_network.data_payments_address.to_string(),
174            ));
175        }
176
177        Ok(ServiceInstallCtx {
178            args,
179            autostart: self.autostart,
180            contents: None,
181            environment: self.env_variables,
182            label: label.clone(),
183            program: self.safenode_path.to_path_buf(),
184            username: self.service_user.clone(),
185            working_directory: None,
186        })
187    }
188}
189
190pub struct AddNodeServiceOptions {
191    pub auto_restart: bool,
192    pub auto_set_nat_flags: bool,
193    pub bootstrap_peers: Vec<Multiaddr>,
194    pub count: Option<u16>,
195    pub delete_safenode_src: bool,
196    pub enable_metrics_server: bool,
197    pub env_variables: Option<Vec<(String, String)>>,
198    pub evm_network: EvmNetwork,
199    pub genesis: bool,
200    pub home_network: bool,
201    pub local: bool,
202    pub log_format: Option<LogFormat>,
203    pub max_archived_log_files: Option<usize>,
204    pub max_log_files: Option<usize>,
205    pub metrics_port: Option<PortRange>,
206    pub node_ip: Option<Ipv4Addr>,
207    pub node_port: Option<PortRange>,
208    pub owner: Option<String>,
209    pub rewards_address: RewardsAddress,
210    pub rpc_address: Option<Ipv4Addr>,
211    pub rpc_port: Option<PortRange>,
212    pub safenode_src_path: PathBuf,
213    pub safenode_dir_path: PathBuf,
214    pub service_data_dir_path: PathBuf,
215    pub service_log_dir_path: PathBuf,
216    pub upnp: bool,
217    pub user: Option<String>,
218    pub user_mode: bool,
219    pub version: String,
220}
221
222#[derive(Debug, PartialEq)]
223pub struct InstallAuditorServiceCtxBuilder {
224    pub auditor_path: PathBuf,
225    pub beta_encryption_key: Option<String>,
226    pub bootstrap_peers: Vec<Multiaddr>,
227    pub env_variables: Option<Vec<(String, String)>>,
228    pub log_dir_path: PathBuf,
229    pub name: String,
230    pub service_user: String,
231}
232
233impl InstallAuditorServiceCtxBuilder {
234    pub fn build(self) -> Result<ServiceInstallCtx> {
235        let mut args = vec![
236            OsString::from("--log-output-dest"),
237            OsString::from(self.log_dir_path.to_string_lossy().to_string()),
238        ];
239
240        if !self.bootstrap_peers.is_empty() {
241            let peers_str = self
242                .bootstrap_peers
243                .iter()
244                .map(|peer| peer.to_string())
245                .collect::<Vec<_>>()
246                .join(",");
247            args.push(OsString::from("--peer"));
248            args.push(OsString::from(peers_str));
249        }
250        if let Some(beta_encryption_key) = self.beta_encryption_key {
251            args.push(OsString::from("--beta-encryption-key"));
252            args.push(OsString::from(beta_encryption_key));
253        }
254
255        Ok(ServiceInstallCtx {
256            args,
257            autostart: true,
258            contents: None,
259            environment: self.env_variables,
260            label: self.name.parse()?,
261            program: self.auditor_path.to_path_buf(),
262            username: Some(self.service_user.to_string()),
263            working_directory: None,
264        })
265    }
266}
267
268#[derive(Debug, PartialEq)]
269pub struct InstallFaucetServiceCtxBuilder {
270    pub bootstrap_peers: Vec<Multiaddr>,
271    pub env_variables: Option<Vec<(String, String)>>,
272    pub faucet_path: PathBuf,
273    pub local: bool,
274    pub log_dir_path: PathBuf,
275    pub name: String,
276    pub service_user: String,
277}
278
279impl InstallFaucetServiceCtxBuilder {
280    pub fn build(self) -> Result<ServiceInstallCtx> {
281        let mut args = vec![
282            OsString::from("--log-output-dest"),
283            OsString::from(self.log_dir_path.to_string_lossy().to_string()),
284        ];
285
286        if !self.bootstrap_peers.is_empty() {
287            let peers_str = self
288                .bootstrap_peers
289                .iter()
290                .map(|peer| peer.to_string())
291                .collect::<Vec<_>>()
292                .join(",");
293            args.push(OsString::from("--peer"));
294            args.push(OsString::from(peers_str));
295        }
296
297        args.push(OsString::from("server"));
298
299        Ok(ServiceInstallCtx {
300            args,
301            autostart: true,
302            contents: None,
303            environment: self.env_variables,
304            label: self.name.parse()?,
305            program: self.faucet_path.to_path_buf(),
306            username: Some(self.service_user.to_string()),
307            working_directory: None,
308        })
309    }
310}
311
312pub struct AddAuditorServiceOptions {
313    pub auditor_install_bin_path: PathBuf,
314    pub auditor_src_bin_path: PathBuf,
315    pub beta_encryption_key: Option<String>,
316    pub bootstrap_peers: Vec<Multiaddr>,
317    pub env_variables: Option<Vec<(String, String)>>,
318    pub service_log_dir_path: PathBuf,
319    pub user: String,
320    pub version: String,
321}
322
323pub struct AddFaucetServiceOptions {
324    pub bootstrap_peers: Vec<Multiaddr>,
325    pub env_variables: Option<Vec<(String, String)>>,
326    pub faucet_install_bin_path: PathBuf,
327    pub faucet_src_bin_path: PathBuf,
328    pub local: bool,
329    pub service_data_dir_path: PathBuf,
330    pub service_log_dir_path: PathBuf,
331    pub user: String,
332    pub version: String,
333}
334
335pub struct AddDaemonServiceOptions {
336    pub address: Ipv4Addr,
337    pub env_variables: Option<Vec<(String, String)>>,
338    pub daemon_install_bin_path: PathBuf,
339    pub daemon_src_bin_path: PathBuf,
340    pub port: u16,
341    pub user: String,
342    pub version: String,
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use sn_evm::{CustomNetwork, RewardsAddress};
349    use std::net::{IpAddr, Ipv4Addr};
350
351    fn create_default_builder() -> InstallNodeServiceCtxBuilder {
352        InstallNodeServiceCtxBuilder {
353            autostart: true,
354            bootstrap_peers: vec![],
355            data_dir_path: PathBuf::from("/data"),
356            env_variables: None,
357            evm_network: EvmNetwork::ArbitrumOne,
358            genesis: false,
359            home_network: false,
360            local: false,
361            log_dir_path: PathBuf::from("/logs"),
362            log_format: None,
363            name: "test-node".to_string(),
364            max_archived_log_files: None,
365            max_log_files: None,
366            metrics_port: None,
367            node_ip: None,
368            node_port: None,
369            owner: None,
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            safenode_path: PathBuf::from("/bin/safenode"),
374            service_user: None,
375            upnp: false,
376        }
377    }
378
379    fn create_custom_evm_network_builder() -> InstallNodeServiceCtxBuilder {
380        InstallNodeServiceCtxBuilder {
381            autostart: true,
382            bootstrap_peers: vec![],
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            genesis: false,
397            home_network: false,
398            local: false,
399            log_dir_path: PathBuf::from("/logs"),
400            log_format: None,
401            name: "test-node".to_string(),
402            max_archived_log_files: None,
403            max_log_files: None,
404            metrics_port: None,
405            node_ip: None,
406            node_port: None,
407            owner: None,
408            rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
409                .unwrap(),
410            rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
411            safenode_path: PathBuf::from("/bin/safenode"),
412            service_user: None,
413            upnp: false,
414        }
415    }
416
417    fn create_builder_with_all_options_enabled() -> InstallNodeServiceCtxBuilder {
418        InstallNodeServiceCtxBuilder {
419            autostart: true,
420            bootstrap_peers: vec![],
421            data_dir_path: PathBuf::from("/data"),
422            env_variables: None,
423            evm_network: EvmNetwork::Custom(CustomNetwork {
424                rpc_url_http: "http://localhost:8545".parse().unwrap(),
425                payment_token_address: RewardsAddress::from_str(
426                    "0x5FbDB2315678afecb367f032d93F642f64180aa3",
427                )
428                .unwrap(),
429                data_payments_address: RewardsAddress::from_str(
430                    "0x8464135c8F25Da09e49BC8782676a84730C318bC",
431                )
432                .unwrap(),
433            }),
434            genesis: false,
435            home_network: false,
436            local: false,
437            log_dir_path: PathBuf::from("/logs"),
438            log_format: None,
439            name: "test-node".to_string(),
440            max_archived_log_files: Some(10),
441            max_log_files: Some(10),
442            metrics_port: None,
443            node_ip: None,
444            node_port: None,
445            owner: None,
446            rewards_address: RewardsAddress::from_str("0x03B770D9cD32077cC0bF330c13C114a87643B124")
447                .unwrap(),
448            rpc_socket_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
449            safenode_path: PathBuf::from("/bin/safenode"),
450            service_user: None,
451            upnp: false,
452        }
453    }
454
455    #[test]
456    fn build_should_assign_expected_values_when_mandatory_options_are_provided() {
457        let builder = create_default_builder();
458        let result = builder.build().unwrap();
459
460        assert_eq!(result.label.to_string(), "test-node");
461        assert_eq!(result.program, PathBuf::from("/bin/safenode"));
462        assert!(result.autostart);
463        assert_eq!(result.username, None);
464        assert_eq!(result.working_directory, None);
465
466        let expected_args = vec![
467            "--rpc",
468            "127.0.0.1:8080",
469            "--root-dir",
470            "/data",
471            "--log-output-dest",
472            "/logs",
473            "--rewards-address",
474            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
475            "evm-arbitrum-one",
476        ];
477        assert_eq!(
478            result
479                .args
480                .iter()
481                .map(|os| os.to_str().unwrap())
482                .collect::<Vec<_>>(),
483            expected_args
484        );
485    }
486
487    #[test]
488    fn build_should_assign_expected_values_when_a_custom_evm_network_is_provided() {
489        let builder = create_custom_evm_network_builder();
490        let result = builder.build().unwrap();
491
492        assert_eq!(result.label.to_string(), "test-node");
493        assert_eq!(result.program, PathBuf::from("/bin/safenode"));
494        assert!(result.autostart);
495        assert_eq!(result.username, None);
496        assert_eq!(result.working_directory, None);
497
498        let expected_args = vec![
499            "--rpc",
500            "127.0.0.1:8080",
501            "--root-dir",
502            "/data",
503            "--log-output-dest",
504            "/logs",
505            "--rewards-address",
506            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
507            "evm-custom",
508            "--rpc-url",
509            "http://localhost:8545/",
510            "--payment-token-address",
511            "0x5FbDB2315678afecb367f032d93F642f64180aa3",
512            "--data-payments-address",
513            "0x8464135c8F25Da09e49BC8782676a84730C318bC",
514        ];
515        assert_eq!(
516            result
517                .args
518                .iter()
519                .map(|os| os.to_str().unwrap())
520                .collect::<Vec<_>>(),
521            expected_args
522        );
523    }
524
525    #[test]
526    fn build_should_assign_expected_values_when_all_options_are_enabled() {
527        let mut builder = create_builder_with_all_options_enabled();
528        builder.genesis = true;
529        builder.home_network = true;
530        builder.local = true;
531        builder.log_format = Some(LogFormat::Json);
532        builder.upnp = true;
533        builder.node_ip = Some(Ipv4Addr::new(192, 168, 1, 1));
534        builder.node_port = Some(12345);
535        builder.metrics_port = Some(9090);
536        builder.owner = Some("test-owner".to_string());
537        builder.bootstrap_peers = vec![
538            "/ip4/127.0.0.1/tcp/8080".parse().unwrap(),
539            "/ip4/192.168.1.1/tcp/8081".parse().unwrap(),
540        ];
541        builder.service_user = Some("safenode-user".to_string());
542
543        let result = builder.build().unwrap();
544
545        let expected_args = vec![
546            "--rpc",
547            "127.0.0.1:8080",
548            "--root-dir",
549            "/data",
550            "--log-output-dest",
551            "/logs",
552            "--first",
553            "--home-network",
554            "--local",
555            "--log-format",
556            "json",
557            "--upnp",
558            "--ip",
559            "192.168.1.1",
560            "--port",
561            "12345",
562            "--metrics-server-port",
563            "9090",
564            "--owner",
565            "test-owner",
566            "--max-archived-log-files",
567            "10",
568            "--max-log-files",
569            "10",
570            "--peer",
571            "/ip4/127.0.0.1/tcp/8080,/ip4/192.168.1.1/tcp/8081",
572            "--rewards-address",
573            "0x03B770D9cD32077cC0bF330c13C114a87643B124",
574            "evm-custom",
575            "--rpc-url",
576            "http://localhost:8545/",
577            "--payment-token-address",
578            "0x5FbDB2315678afecb367f032d93F642f64180aa3",
579            "--data-payments-address",
580            "0x8464135c8F25Da09e49BC8782676a84730C318bC",
581        ];
582        assert_eq!(
583            result
584                .args
585                .iter()
586                .map(|os| os.to_str().unwrap())
587                .collect::<Vec<_>>(),
588            expected_args
589        );
590        assert_eq!(result.username, Some("safenode-user".to_string()));
591    }
592
593    #[test]
594    fn build_should_assign_expected_values_when_environment_variables_are_provided() {
595        let mut builder = create_default_builder();
596        builder.env_variables = Some(vec![
597            ("VAR1".to_string(), "value1".to_string()),
598            ("VAR2".to_string(), "value2".to_string()),
599        ]);
600
601        let result = builder.build().unwrap();
602
603        assert_eq!(
604            result.environment,
605            Some(vec![
606                ("VAR1".to_string(), "value1".to_string()),
607                ("VAR2".to_string(), "value2".to_string()),
608            ])
609        );
610    }
611}