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