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::{AddDaemonServiceOptions, AddNodeServiceOptions, InstallNodeServiceCtxBuilder};
13use crate::{
14    DAEMON_SERVICE_NAME, VerbosityLevel,
15    config::{create_owned_dir, get_user_antnode_data_dir},
16    helpers::{check_port_availability, get_start_port_if_applicable, increment_port_option},
17};
18use ant_service_management::{
19    DaemonServiceData, NatDetectionStatus, NodeRegistryManager, NodeServiceData, ServiceStatus,
20    control::ServiceControl, node::NODE_SERVICE_DATA_SCHEMA_LATEST,
21};
22use color_eyre::{Help, Result, eyre::eyre};
23use colored::Colorize;
24use service_manager::ServiceInstallCtx;
25use std::{
26    ffi::OsString,
27    net::{IpAddr, Ipv4Addr, SocketAddr},
28};
29
30/// Install antnode as a service.
31///
32/// This only defines the service; it does not start it.
33///
34/// There are several arguments that probably seem like they could be handled within the function,
35/// but they enable more controlled unit testing.
36///
37/// Returns the service names of the added services.
38pub async fn add_node(
39    mut options: AddNodeServiceOptions,
40    node_registry: NodeRegistryManager,
41    service_control: &dyn ServiceControl,
42    verbosity: VerbosityLevel,
43) -> Result<Vec<String>> {
44    if options.init_peers_config.first {
45        if let Some(count) = options.count
46            && count > 1
47        {
48            error!("A genesis node can only be added as a single node");
49            return Err(eyre!("A genesis node can only be added as a single node"));
50        }
51
52        let mut genesis_node_exists = false;
53        for node in node_registry.nodes.read().await.iter() {
54            if node.read().await.initial_peers_config.first {
55                genesis_node_exists = true;
56                break;
57            }
58        }
59
60        if genesis_node_exists {
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).await?;
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).await?;
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).await?;
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.environment_variables.write().await = options.env_variables.clone();
93        node_registry.save().await?;
94    }
95
96    let mut added_service_data = vec![];
97    let mut failed_service_data = vec![];
98
99    let current_node_count = node_registry.nodes.read().await.len() as u16;
100    let target_node_count = current_node_count + options.count.unwrap_or(1);
101
102    let mut node_number = current_node_count + 1;
103    let mut node_port = get_start_port_if_applicable(options.node_port);
104    let mut metrics_port = get_start_port_if_applicable(options.metrics_port);
105    let mut rpc_port = get_start_port_if_applicable(options.rpc_port);
106
107    while node_number <= target_node_count {
108        trace!("Adding node with node_number {node_number}");
109        let rpc_free_port = if let Some(port) = rpc_port {
110            port
111        } else {
112            service_control.get_available_port()?
113        };
114        let metrics_free_port = if let Some(port) = metrics_port {
115            Some(port)
116        } else if options.enable_metrics_server {
117            Some(service_control.get_available_port()?)
118        } else {
119            None
120        };
121
122        let rpc_socket_addr = if let Some(addr) = options.rpc_address {
123            SocketAddr::new(IpAddr::V4(addr), rpc_free_port)
124        } else {
125            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), rpc_free_port)
126        };
127
128        let service_name = format!("antnode{node_number}");
129        let service_data_dir_path = options.service_data_dir_path.join(service_name.clone());
130        let service_antnode_path = service_data_dir_path.join(antnode_file_name.clone());
131
132        // For a user mode service, if the user has *not* specified a custom directory and they are
133        // using the default, e.g., ~/.local/share/autonomi/node/<service-name>, an additional "logs"
134        // directory needs to be appended to the path, otherwise the log files will be output at
135        // the same directory where `secret-key` is, which is not what users expect.
136        let default_log_dir_path = get_user_antnode_data_dir()?;
137        let service_log_dir_path =
138            if options.user_mode && options.service_log_dir_path == default_log_dir_path {
139                options
140                    .service_log_dir_path
141                    .join(service_name.clone())
142                    .join("logs")
143            } else {
144                options.service_log_dir_path.join(service_name.clone())
145            };
146
147        if let Some(user) = &options.user {
148            debug!("Creating data_dir and log_dirs with user {user}");
149            create_owned_dir(service_data_dir_path.clone(), user)?;
150            create_owned_dir(service_log_dir_path.clone(), user)?;
151        } else {
152            debug!("Creating data_dir and log_dirs without user");
153            std::fs::create_dir_all(service_data_dir_path.clone())?;
154            std::fs::create_dir_all(service_log_dir_path.clone())?;
155        }
156
157        debug!("Copying antnode binary to {service_antnode_path:?}");
158        std::fs::copy(
159            options.antnode_src_path.clone(),
160            service_antnode_path.clone(),
161        )?;
162
163        if options.auto_set_nat_flags {
164            let nat_status = node_registry.nat_status.read().await;
165
166            match nat_status.as_ref() {
167                Some(NatDetectionStatus::Public) => {
168                    options.no_upnp = true; // UPnP not needed
169                    options.relay = false;
170                }
171                Some(NatDetectionStatus::UPnP) => {
172                    options.no_upnp = false;
173                    options.relay = false;
174                }
175                Some(NatDetectionStatus::Private) => {
176                    options.no_upnp = true;
177                    options.relay = true;
178                }
179                None => {
180                    // Fallback to private defaults
181                    options.no_upnp = true;
182                    options.relay = true;
183                    debug!("NAT status not set; defaulting to no_upnp=true and relay=true");
184                }
185            }
186
187            debug!(
188                "Auto-setting NAT flags: no_upnp={}, relay={}",
189                options.no_upnp, options.relay
190            );
191        }
192
193        let install_ctx = InstallNodeServiceCtxBuilder {
194            alpha: options.alpha,
195            autostart: options.auto_restart,
196            data_dir_path: service_data_dir_path.clone(),
197            env_variables: options.env_variables.clone(),
198            evm_network: options.evm_network.clone(),
199            relay: options.relay,
200            log_dir_path: service_log_dir_path.clone(),
201            log_format: options.log_format,
202            max_archived_log_files: options.max_archived_log_files,
203            max_log_files: options.max_log_files,
204            metrics_port: metrics_free_port,
205            name: service_name.clone(),
206            network_id: options.network_id,
207            node_ip: options.node_ip,
208            node_port,
209            init_peers_config: options.init_peers_config.clone(),
210            rewards_address: options.rewards_address,
211            rpc_socket_addr,
212            antnode_path: service_antnode_path.clone(),
213            service_user: options.user.clone(),
214            no_upnp: options.no_upnp,
215            write_older_cache_files: options.write_older_cache_files,
216        }
217        .build()?;
218
219        match service_control.install(install_ctx, options.user_mode) {
220            Ok(()) => {
221                info!("Successfully added service {service_name}");
222                added_service_data.push((
223                    service_name.clone(),
224                    service_antnode_path.to_string_lossy().into_owned(),
225                    service_data_dir_path.to_string_lossy().into_owned(),
226                    service_log_dir_path.to_string_lossy().into_owned(),
227                    rpc_socket_addr,
228                ));
229
230                node_registry
231                    .push_node(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                        write_older_cache_files: options.write_older_cache_files,
263                    })
264                    .await;
265                // We save the node registry for each service because it's possible any number of
266                // services could fail to be added.
267                node_registry.save().await?;
268            }
269            Err(e) => {
270                error!("Failed to add service {service_name}: {e}");
271                failed_service_data.push((service_name.clone(), e.to_string()));
272            }
273        }
274
275        node_number += 1;
276        node_port = increment_port_option(node_port);
277        metrics_port = increment_port_option(metrics_port);
278        rpc_port = increment_port_option(rpc_port);
279    }
280
281    if options.delete_antnode_src {
282        debug!("Deleting antnode binary file");
283        std::fs::remove_file(options.antnode_src_path)?;
284    }
285
286    if !added_service_data.is_empty() {
287        info!("Added {} services", added_service_data.len());
288    } else if !failed_service_data.is_empty() {
289        error!("Failed to add {} service(s)", failed_service_data.len());
290    }
291
292    if !added_service_data.is_empty() && verbosity != VerbosityLevel::Minimal {
293        println!("Services Added:");
294        for install in added_service_data.iter() {
295            println!(" {} {}", "✓".green(), install.0);
296            println!("    - Antnode path: {}", install.1);
297            println!("    - Data path: {}", install.2);
298            println!("    - Log path: {}", install.3);
299            println!("    - RPC port: {}", install.4);
300        }
301        println!("[!] Note: newly added services have not been started");
302    }
303
304    if !failed_service_data.is_empty() {
305        if verbosity != VerbosityLevel::Minimal {
306            println!("Failed to add {} service(s):", failed_service_data.len());
307            for failed in failed_service_data.iter() {
308                println!("{} {}: {}", "✕".red(), failed.0, failed.1);
309            }
310        }
311        return Err(eyre!("Failed to add one or more services")
312            .suggestion("However, any services that were successfully added will be usable."));
313    }
314
315    let added_services_names = added_service_data
316        .into_iter()
317        .map(|(name, ..)| name)
318        .collect();
319
320    Ok(added_services_names)
321}
322
323/// Install the daemon as a service.
324///
325/// This only defines the service; it does not start it.
326pub async fn add_daemon(
327    options: AddDaemonServiceOptions,
328    node_registry: NodeRegistryManager,
329    service_control: &dyn ServiceControl,
330) -> Result<()> {
331    if node_registry.daemon.read().await.is_some() {
332        error!("A antctld service has already been created");
333        return Err(eyre!("A antctld service has already been created"));
334    }
335
336    debug!(
337        "Copying daemon binary file to {:?}",
338        options.daemon_install_bin_path
339    );
340    std::fs::copy(
341        options.daemon_src_bin_path.clone(),
342        options.daemon_install_bin_path.clone(),
343    )?;
344
345    let install_ctx = ServiceInstallCtx {
346        args: vec![
347            OsString::from("--port"),
348            OsString::from(options.port.to_string()),
349            OsString::from("--address"),
350            OsString::from(options.address.to_string()),
351        ],
352        autostart: true,
353        contents: None,
354        environment: options.env_variables,
355        label: DAEMON_SERVICE_NAME.parse()?,
356        program: options.daemon_install_bin_path.clone(),
357        username: Some(options.user),
358        working_directory: None,
359        disable_restart_on_failure: false,
360    };
361
362    match service_control.install(install_ctx, false) {
363        Ok(()) => {
364            let daemon = DaemonServiceData {
365                daemon_path: options.daemon_install_bin_path.clone(),
366                endpoint: Some(SocketAddr::new(IpAddr::V4(options.address), options.port)),
367                pid: None,
368                service_name: DAEMON_SERVICE_NAME.to_string(),
369                status: ServiceStatus::Added,
370                version: options.version,
371            };
372
373            node_registry.insert_daemon(daemon).await;
374            info!("Daemon service has been added successfully");
375            println!("Daemon service added {}", "✓".green());
376            println!("[!] Note: the service has not been started");
377            node_registry.save().await?;
378            std::fs::remove_file(options.daemon_src_bin_path)?;
379            Ok(())
380        }
381        Err(e) => {
382            error!("Failed to add daemon service: {e}");
383            println!("Failed to add daemon service: {e}");
384            Err(e.into())
385        }
386    }
387}