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