sn_node_manager/add_services/
mod.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.
8pub mod config;
9#[cfg(test)]
10mod tests;
11
12use self::config::{
13    AddAuditorServiceOptions, AddDaemonServiceOptions, AddFaucetServiceOptions,
14    AddNodeServiceOptions, InstallAuditorServiceCtxBuilder, InstallFaucetServiceCtxBuilder,
15    InstallNodeServiceCtxBuilder,
16};
17use crate::{
18    config::{create_owned_dir, get_user_safenode_data_dir},
19    helpers::{check_port_availability, get_start_port_if_applicable, increment_port_option},
20    VerbosityLevel, DAEMON_SERVICE_NAME,
21};
22use color_eyre::{
23    eyre::{eyre, OptionExt},
24    Help, Result,
25};
26use colored::Colorize;
27use service_manager::ServiceInstallCtx;
28use sn_service_management::{
29    auditor::AuditorServiceData, control::ServiceControl, DaemonServiceData, FaucetServiceData,
30    NatDetectionStatus, NodeRegistry, NodeServiceData, ServiceStatus,
31};
32use std::{
33    ffi::OsString,
34    net::{IpAddr, Ipv4Addr, SocketAddr},
35};
36
37/// Install safenode as a service.
38///
39/// This only defines the service; it does not start it.
40///
41/// There are several arguments that probably seem like they could be handled within the function,
42/// but they enable more controlled unit testing.
43///
44/// Returns the service names of the added services.
45pub async fn add_node(
46    mut options: AddNodeServiceOptions,
47    node_registry: &mut NodeRegistry,
48    service_control: &dyn ServiceControl,
49    verbosity: VerbosityLevel,
50) -> Result<Vec<String>> {
51    if options.genesis {
52        if let Some(count) = options.count {
53            if count > 1 {
54                error!("A genesis node can only be added as a single node");
55                return Err(eyre!("A genesis node can only be added as a single node"));
56            }
57        }
58
59        let genesis_node = node_registry.nodes.iter().find(|n| n.genesis);
60        if genesis_node.is_some() {
61            error!("A genesis node already exists");
62            return Err(eyre!("A genesis node already exists"));
63        }
64    }
65
66    if let Some(port_option) = &options.node_port {
67        port_option.validate(options.count.unwrap_or(1))?;
68        check_port_availability(port_option, &node_registry.nodes)?;
69    }
70
71    if let Some(port_option) = &options.metrics_port {
72        port_option.validate(options.count.unwrap_or(1))?;
73        check_port_availability(port_option, &node_registry.nodes)?;
74    }
75
76    if let Some(port_option) = &options.rpc_port {
77        port_option.validate(options.count.unwrap_or(1))?;
78        check_port_availability(port_option, &node_registry.nodes)?;
79    }
80
81    let owner = match &options.owner {
82        Some(owner) => {
83            if owner.chars().any(|c| c.is_uppercase()) {
84                warn!("Owner name ({owner}) contains uppercase characters and will be converted to lowercase");
85            }
86            Some(owner.to_lowercase())
87        }
88        None => None,
89    };
90
91    let safenode_file_name = options
92        .safenode_src_path
93        .file_name()
94        .ok_or_else(|| {
95            error!("Could not get filename from the safenode download path");
96            eyre!("Could not get filename from the safenode download path")
97        })?
98        .to_string_lossy()
99        .to_string();
100
101    {
102        let mut should_save = false;
103        let new_bootstrap_peers: Vec<_> = options
104            .bootstrap_peers
105            .iter()
106            .filter(|peer| !node_registry.bootstrap_peers.contains(peer))
107            .collect();
108        if !new_bootstrap_peers.is_empty() {
109            node_registry
110                .bootstrap_peers
111                .extend(new_bootstrap_peers.into_iter().cloned());
112            should_save = true;
113        }
114
115        if options.env_variables.is_some() {
116            node_registry
117                .environment_variables
118                .clone_from(&options.env_variables);
119            should_save = true;
120        }
121
122        if should_save {
123            node_registry.save()?;
124        }
125    }
126
127    let mut added_service_data = vec![];
128    let mut failed_service_data = vec![];
129
130    let current_node_count = node_registry.nodes.len() as u16;
131    let target_node_count = current_node_count + options.count.unwrap_or(1);
132
133    let mut node_number = current_node_count + 1;
134    let mut node_port = get_start_port_if_applicable(options.node_port);
135    let mut metrics_port = get_start_port_if_applicable(options.metrics_port);
136    let mut rpc_port = get_start_port_if_applicable(options.rpc_port);
137
138    while node_number <= target_node_count {
139        trace!("Adding node with node_number {node_number}");
140        let rpc_free_port = if let Some(port) = rpc_port {
141            port
142        } else {
143            service_control.get_available_port()?
144        };
145        let metrics_free_port = if let Some(port) = metrics_port {
146            Some(port)
147        } else if options.enable_metrics_server {
148            Some(service_control.get_available_port()?)
149        } else {
150            None
151        };
152
153        let rpc_socket_addr = if let Some(addr) = options.rpc_address {
154            SocketAddr::new(IpAddr::V4(addr), rpc_free_port)
155        } else {
156            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), rpc_free_port)
157        };
158
159        let service_name = format!("safenode{node_number}");
160        let service_data_dir_path = options.service_data_dir_path.join(service_name.clone());
161        let service_safenode_path = service_data_dir_path.join(safenode_file_name.clone());
162
163        // For a user mode service, if the user has *not* specified a custom directory and they are
164        // using the default, e.g., ~/.local/share/safe/node/<service-name>, an additional "logs"
165        // directory needs to be appended to the path, otherwise the log files will be output at
166        // the same directory where `secret-key` is, which is not what users expect.
167        let default_log_dir_path = get_user_safenode_data_dir()?;
168        let service_log_dir_path =
169            if options.user_mode && options.service_log_dir_path == default_log_dir_path {
170                options
171                    .service_log_dir_path
172                    .join(service_name.clone())
173                    .join("logs")
174            } else {
175                options.service_log_dir_path.join(service_name.clone())
176            };
177
178        if let Some(user) = &options.user {
179            debug!("Creating data_dir and log_dirs with user {user}");
180            create_owned_dir(service_data_dir_path.clone(), user)?;
181            create_owned_dir(service_log_dir_path.clone(), user)?;
182        } else {
183            debug!("Creating data_dir and log_dirs without user");
184            std::fs::create_dir_all(service_data_dir_path.clone())?;
185            std::fs::create_dir_all(service_log_dir_path.clone())?;
186        }
187
188        debug!("Copying safenode binary to {service_safenode_path:?}");
189        std::fs::copy(
190            options.safenode_src_path.clone(),
191            service_safenode_path.clone(),
192        )?;
193
194        if options.auto_set_nat_flags {
195            let nat_status = node_registry
196                .nat_status
197                .clone()
198                .ok_or_eyre("NAT status has not been set. Run 'nat-detection' first")?;
199
200            match nat_status {
201                NatDetectionStatus::Public => {
202                    options.upnp = false;
203                    options.home_network = false;
204                }
205                NatDetectionStatus::UPnP => {
206                    options.upnp = true;
207                    options.home_network = false;
208                }
209                NatDetectionStatus::Private => {
210                    options.upnp = false;
211                    options.home_network = true;
212                }
213            }
214            debug!(
215                "Auto-setting NAT flags: upnp={}, home_network={}",
216                options.upnp, options.home_network
217            );
218        }
219
220        let install_ctx = InstallNodeServiceCtxBuilder {
221            autostart: options.auto_restart,
222            bootstrap_peers: options.bootstrap_peers.clone(),
223            data_dir_path: service_data_dir_path.clone(),
224            env_variables: options.env_variables.clone(),
225            evm_network: options.evm_network.clone(),
226            genesis: options.genesis,
227            home_network: options.home_network,
228            local: options.local,
229            log_dir_path: service_log_dir_path.clone(),
230            log_format: options.log_format,
231            max_archived_log_files: options.max_archived_log_files,
232            max_log_files: options.max_log_files,
233            metrics_port: metrics_free_port,
234            name: service_name.clone(),
235            node_ip: options.node_ip,
236            node_port,
237            owner: owner.clone(),
238            rewards_address: options.rewards_address,
239            rpc_socket_addr,
240            safenode_path: service_safenode_path.clone(),
241            service_user: options.user.clone(),
242            upnp: options.upnp,
243        }
244        .build()?;
245
246        match service_control.install(install_ctx, options.user_mode) {
247            Ok(()) => {
248                info!("Successfully added service {service_name}");
249                added_service_data.push((
250                    service_name.clone(),
251                    service_safenode_path.to_string_lossy().into_owned(),
252                    service_data_dir_path.to_string_lossy().into_owned(),
253                    service_log_dir_path.to_string_lossy().into_owned(),
254                    rpc_socket_addr,
255                ));
256
257                node_registry.nodes.push(NodeServiceData {
258                    auto_restart: options.auto_restart,
259                    connected_peers: None,
260                    data_dir_path: service_data_dir_path.clone(),
261                    evm_network: options.evm_network.clone(),
262                    genesis: options.genesis,
263                    home_network: options.home_network,
264                    listen_addr: None,
265                    local: options.local,
266                    log_dir_path: service_log_dir_path.clone(),
267                    log_format: options.log_format,
268                    max_archived_log_files: options.max_archived_log_files,
269                    max_log_files: options.max_log_files,
270                    metrics_port: metrics_free_port,
271                    node_ip: options.node_ip,
272                    node_port,
273                    number: node_number,
274                    rewards_address: options.rewards_address,
275                    reward_balance: None,
276                    rpc_socket_addr,
277                    owner: owner.clone(),
278                    peer_id: None,
279                    pid: None,
280                    safenode_path: service_safenode_path,
281                    service_name,
282                    status: ServiceStatus::Added,
283                    upnp: options.upnp,
284                    user: options.user.clone(),
285                    user_mode: options.user_mode,
286                    version: options.version.clone(),
287                });
288                // We save the node registry for each service because it's possible any number of
289                // services could fail to be added.
290                node_registry.save()?;
291            }
292            Err(e) => {
293                error!("Failed to add service {service_name}: {e}");
294                failed_service_data.push((service_name.clone(), e.to_string()));
295            }
296        }
297
298        node_number += 1;
299        node_port = increment_port_option(node_port);
300        metrics_port = increment_port_option(metrics_port);
301        rpc_port = increment_port_option(rpc_port);
302    }
303
304    if options.delete_safenode_src {
305        debug!("Deleting safenode binary file");
306        std::fs::remove_file(options.safenode_src_path)?;
307    }
308
309    if !added_service_data.is_empty() {
310        info!("Added {} services", added_service_data.len());
311    } else if !failed_service_data.is_empty() {
312        error!("Failed to add {} service(s)", failed_service_data.len());
313    }
314
315    if !added_service_data.is_empty() && verbosity != VerbosityLevel::Minimal {
316        println!("Services Added:");
317        for install in added_service_data.iter() {
318            println!(" {} {}", "✓".green(), install.0);
319            println!("    - Safenode path: {}", install.1);
320            println!("    - Data path: {}", install.2);
321            println!("    - Log path: {}", install.3);
322            println!("    - RPC port: {}", install.4);
323        }
324        println!("[!] Note: newly added services have not been started");
325    }
326
327    if !failed_service_data.is_empty() {
328        if verbosity != VerbosityLevel::Minimal {
329            println!("Failed to add {} service(s):", failed_service_data.len());
330            for failed in failed_service_data.iter() {
331                println!("{} {}: {}", "✕".red(), failed.0, failed.1);
332            }
333        }
334        return Err(eyre!("Failed to add one or more services")
335            .suggestion("However, any services that were successfully added will be usable."));
336    }
337
338    let added_services_names = added_service_data
339        .into_iter()
340        .map(|(name, ..)| name)
341        .collect();
342
343    Ok(added_services_names)
344}
345
346/// Install the auditor as a service.
347///
348/// This only defines the service; it does not start it.
349///
350/// There are several arguments that probably seem like they could be handled within the function,
351/// but they enable more controlled unit testing.
352pub fn add_auditor(
353    install_options: AddAuditorServiceOptions,
354    node_registry: &mut NodeRegistry,
355    service_control: &dyn ServiceControl,
356    verbosity: VerbosityLevel,
357) -> Result<()> {
358    if node_registry.auditor.is_some() {
359        error!("An Auditor service has already been created");
360        return Err(eyre!("An Auditor service has already been created"));
361    }
362
363    debug!(
364        "Creating log directory at {:?} as user {:?}",
365        install_options.service_log_dir_path, install_options.user
366    );
367    create_owned_dir(
368        install_options.service_log_dir_path.clone(),
369        &install_options.user,
370    )?;
371
372    debug!(
373        "Copying auditor binary file to {:?}",
374        install_options.auditor_install_bin_path
375    );
376    std::fs::copy(
377        install_options.auditor_src_bin_path.clone(),
378        install_options.auditor_install_bin_path.clone(),
379    )?;
380
381    let install_ctx = InstallAuditorServiceCtxBuilder {
382        auditor_path: install_options.auditor_install_bin_path.clone(),
383        beta_encryption_key: install_options.beta_encryption_key.clone(),
384        bootstrap_peers: install_options.bootstrap_peers.clone(),
385        env_variables: install_options.env_variables.clone(),
386        log_dir_path: install_options.service_log_dir_path.clone(),
387        name: "auditor".to_string(),
388        service_user: install_options.user.clone(),
389    }
390    .build()?;
391
392    match service_control.install(install_ctx, false) {
393        Ok(()) => {
394            node_registry.auditor = Some(AuditorServiceData {
395                auditor_path: install_options.auditor_install_bin_path.clone(),
396                log_dir_path: install_options.service_log_dir_path.clone(),
397                pid: None,
398                service_name: "auditor".to_string(),
399                status: ServiceStatus::Added,
400                user: install_options.user.clone(),
401                version: install_options.version,
402            });
403            info!("Auditor service has been added successfully");
404            println!("Auditor service added {}", "✓".green());
405            if verbosity != VerbosityLevel::Minimal {
406                println!(
407                    "  - Bin path: {}",
408                    install_options.auditor_install_bin_path.to_string_lossy()
409                );
410                println!(
411                    "  - Log path: {}",
412                    install_options.service_log_dir_path.to_string_lossy()
413                );
414            }
415            println!("[!] Note: the service has not been started");
416            debug!("Removing auditor binary file");
417            std::fs::remove_file(install_options.auditor_src_bin_path)?;
418            node_registry.save()?;
419            Ok(())
420        }
421        Err(e) => {
422            error!("Failed to add auditor service: {e}");
423            println!("Failed to add auditor service: {e}");
424            Err(e.into())
425        }
426    }
427}
428
429/// Install the daemon as a service.
430///
431/// This only defines the service; it does not start it.
432pub fn add_daemon(
433    options: AddDaemonServiceOptions,
434    node_registry: &mut NodeRegistry,
435    service_control: &dyn ServiceControl,
436) -> Result<()> {
437    if node_registry.daemon.is_some() {
438        error!("A safenodemand service has already been created");
439        return Err(eyre!("A safenodemand service has already been created"));
440    }
441
442    debug!(
443        "Copying daemon binary file to {:?}",
444        options.daemon_install_bin_path
445    );
446    std::fs::copy(
447        options.daemon_src_bin_path.clone(),
448        options.daemon_install_bin_path.clone(),
449    )?;
450
451    let install_ctx = ServiceInstallCtx {
452        args: vec![
453            OsString::from("--port"),
454            OsString::from(options.port.to_string()),
455            OsString::from("--address"),
456            OsString::from(options.address.to_string()),
457        ],
458        autostart: true,
459        contents: None,
460        environment: options.env_variables,
461        label: DAEMON_SERVICE_NAME.parse()?,
462        program: options.daemon_install_bin_path.clone(),
463        username: Some(options.user),
464        working_directory: None,
465    };
466
467    match service_control.install(install_ctx, false) {
468        Ok(()) => {
469            let daemon = DaemonServiceData {
470                daemon_path: options.daemon_install_bin_path.clone(),
471                endpoint: Some(SocketAddr::new(IpAddr::V4(options.address), options.port)),
472                pid: None,
473                service_name: DAEMON_SERVICE_NAME.to_string(),
474                status: ServiceStatus::Added,
475                version: options.version,
476            };
477            node_registry.daemon = Some(daemon);
478            info!("Daemon service has been added successfully");
479            println!("Daemon service added {}", "✓".green());
480            println!("[!] Note: the service has not been started");
481            node_registry.save()?;
482            std::fs::remove_file(options.daemon_src_bin_path)?;
483            Ok(())
484        }
485        Err(e) => {
486            error!("Failed to add daemon service: {e}");
487            println!("Failed to add daemon service: {e}");
488            Err(e.into())
489        }
490    }
491}
492
493/// Install the faucet as a service.
494///
495/// This only defines the service; it does not start it.
496///
497/// There are several arguments that probably seem like they could be handled within the function,
498/// but they enable more controlled unit testing.
499pub fn add_faucet(
500    install_options: AddFaucetServiceOptions,
501    node_registry: &mut NodeRegistry,
502    service_control: &dyn ServiceControl,
503    verbosity: VerbosityLevel,
504) -> Result<()> {
505    if node_registry.faucet.is_some() {
506        error!("A faucet service has already been created");
507        return Err(eyre!("A faucet service has already been created"));
508    }
509
510    debug!(
511        "Creating log directory at {:?} as user {:?}",
512        install_options.service_log_dir_path, install_options.user
513    );
514    create_owned_dir(
515        install_options.service_log_dir_path.clone(),
516        &install_options.user,
517    )?;
518    debug!(
519        "Copying faucet binary file to {:?}",
520        install_options.faucet_install_bin_path
521    );
522    std::fs::copy(
523        install_options.faucet_src_bin_path.clone(),
524        install_options.faucet_install_bin_path.clone(),
525    )?;
526
527    let install_ctx = InstallFaucetServiceCtxBuilder {
528        bootstrap_peers: install_options.bootstrap_peers.clone(),
529        env_variables: install_options.env_variables.clone(),
530        faucet_path: install_options.faucet_install_bin_path.clone(),
531        local: install_options.local,
532        log_dir_path: install_options.service_log_dir_path.clone(),
533        name: "faucet".to_string(),
534        service_user: install_options.user.clone(),
535    }
536    .build()?;
537
538    match service_control.install(install_ctx, false) {
539        Ok(()) => {
540            node_registry.faucet = Some(FaucetServiceData {
541                faucet_path: install_options.faucet_install_bin_path.clone(),
542                local: false,
543                log_dir_path: install_options.service_log_dir_path.clone(),
544                pid: None,
545                service_name: "faucet".to_string(),
546                status: ServiceStatus::Added,
547                user: install_options.user.clone(),
548                version: install_options.version,
549            });
550            info!("Faucet service has been added successfully");
551            println!("Faucet service added {}", "✓".green());
552            if verbosity != VerbosityLevel::Minimal {
553                println!(
554                    "  - Bin path: {}",
555                    install_options.faucet_install_bin_path.to_string_lossy()
556                );
557                println!(
558                    "  - Data path: {}",
559                    install_options.service_data_dir_path.to_string_lossy()
560                );
561                println!(
562                    "  - Log path: {}",
563                    install_options.service_log_dir_path.to_string_lossy()
564                );
565            }
566            println!("[!] Note: the service has not been started");
567            std::fs::remove_file(install_options.faucet_src_bin_path)?;
568            node_registry.save()?;
569            Ok(())
570        }
571        Err(e) => {
572            error!("Failed to add faucet service: {e}");
573            println!("Failed to add faucet service: {e}");
574            Err(e.into())
575        }
576    }
577}