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
115        let metrics_free_port = if let Some(port) = metrics_port {
116            Some(port)
117        } else if options.enable_metrics_server {
118            Some(service_control.get_available_port()?)
119        } else {
120            None
121        };
122
123        let node_free_port = if let Some(port) = node_port {
124            Some(port)
125        } else {
126            Some(service_control.get_available_port()?)
127        };
128
129        let rpc_socket_addr = if let Some(addr) = options.rpc_address {
130            SocketAddr::new(IpAddr::V4(addr), rpc_free_port)
131        } else {
132            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), rpc_free_port)
133        };
134
135        let service_name = format!("antnode{node_number}");
136        let service_data_dir_path = options.service_data_dir_path.join(service_name.clone());
137        let service_antnode_path = service_data_dir_path.join(antnode_file_name.clone());
138
139        // For a user mode service, if the user has *not* specified a custom directory and they are
140        // using the default, e.g., ~/.local/share/autonomi/node/<service-name>, an additional "logs"
141        // directory needs to be appended to the path, otherwise the log files will be output at
142        // the same directory where `secret-key` is, which is not what users expect.
143        let default_log_dir_path = get_user_antnode_data_dir()?;
144        let service_log_dir_path =
145            if options.user_mode && options.service_log_dir_path == default_log_dir_path {
146                options
147                    .service_log_dir_path
148                    .join(service_name.clone())
149                    .join("logs")
150            } else {
151                options.service_log_dir_path.join(service_name.clone())
152            };
153
154        if let Some(user) = &options.user {
155            debug!("Creating data_dir and log_dirs with user {user}");
156            create_owned_dir(service_data_dir_path.clone(), user)?;
157            create_owned_dir(service_log_dir_path.clone(), user)?;
158        } else {
159            debug!("Creating data_dir and log_dirs without user");
160            std::fs::create_dir_all(service_data_dir_path.clone())?;
161            std::fs::create_dir_all(service_log_dir_path.clone())?;
162        }
163
164        debug!("Copying antnode binary to {service_antnode_path:?}");
165        std::fs::copy(
166            options.antnode_src_path.clone(),
167            service_antnode_path.clone(),
168        )?;
169
170        if options.auto_set_nat_flags {
171            let nat_status = node_registry.nat_status.read().await;
172
173            match nat_status.as_ref() {
174                Some(NatDetectionStatus::Public) => {
175                    options.no_upnp = true; // UPnP not needed
176                    options.relay = false;
177                }
178                Some(NatDetectionStatus::UPnP) => {
179                    options.no_upnp = false;
180                    options.relay = false;
181                }
182                Some(NatDetectionStatus::Private) => {
183                    options.no_upnp = true;
184                    options.relay = true;
185                }
186                None => {
187                    // Fallback to private defaults
188                    options.no_upnp = true;
189                    options.relay = true;
190                    debug!("NAT status not set; defaulting to no_upnp=true and relay=true");
191                }
192            }
193
194            debug!(
195                "Auto-setting NAT flags: no_upnp={}, relay={}",
196                options.no_upnp, options.relay
197            );
198        }
199
200        let install_ctx = InstallNodeServiceCtxBuilder {
201            alpha: options.alpha,
202            antnode_path: service_antnode_path.clone(),
203            autostart: options.auto_restart,
204            data_dir_path: service_data_dir_path.clone(),
205            env_variables: options.env_variables.clone(),
206            evm_network: options.evm_network.clone(),
207            init_peers_config: options.init_peers_config.clone(),
208            log_dir_path: service_log_dir_path.clone(),
209            log_format: options.log_format,
210            max_archived_log_files: options.max_archived_log_files,
211            max_log_files: options.max_log_files,
212            metrics_port: metrics_free_port,
213            name: service_name.clone(),
214            network_id: options.network_id,
215            no_upnp: options.no_upnp,
216            node_ip: options.node_ip,
217            node_port: node_free_port,
218            relay: options.relay,
219            restart_policy: options.restart_policy,
220            rewards_address: options.rewards_address,
221            rpc_socket_addr,
222            service_user: options.user.clone(),
223            stop_on_upgrade: true,
224            write_older_cache_files: options.write_older_cache_files,
225        }
226        .build()?;
227
228        match service_control.install(install_ctx, options.user_mode) {
229            Ok(()) => {
230                info!("Successfully added service {service_name}");
231                added_service_data.push((
232                    service_name.clone(),
233                    service_antnode_path.to_string_lossy().into_owned(),
234                    service_data_dir_path.to_string_lossy().into_owned(),
235                    service_log_dir_path.to_string_lossy().into_owned(),
236                    rpc_socket_addr,
237                ));
238
239                node_registry
240                    .push_node(NodeServiceData {
241                        alpha: options.alpha,
242                        antnode_path: service_antnode_path,
243                        auto_restart: options.auto_restart,
244                        connected_peers: None,
245                        data_dir_path: service_data_dir_path.clone(),
246                        evm_network: options.evm_network.clone(),
247                        relay: options.relay,
248                        initial_peers_config: options.init_peers_config.clone(),
249                        listen_addr: None,
250                        log_dir_path: service_log_dir_path.clone(),
251                        log_format: options.log_format,
252                        max_archived_log_files: options.max_archived_log_files,
253                        max_log_files: options.max_log_files,
254                        metrics_port: metrics_free_port,
255                        network_id: options.network_id,
256                        node_ip: options.node_ip,
257                        node_port: node_free_port,
258                        number: node_number,
259                        rewards_address: options.rewards_address,
260                        reward_balance: None,
261                        rpc_socket_addr,
262                        peer_id: None,
263                        pid: None,
264                        schema_version: NODE_SERVICE_DATA_SCHEMA_LATEST,
265                        service_name,
266                        status: ServiceStatus::Added,
267                        no_upnp: options.no_upnp,
268                        user: options.user.clone(),
269                        user_mode: options.user_mode,
270                        version: options.version.clone(),
271                        write_older_cache_files: options.write_older_cache_files,
272                    })
273                    .await;
274                // We save the node registry for each service because it's possible any number of
275                // services could fail to be added.
276                node_registry.save().await?;
277            }
278            Err(e) => {
279                error!("Failed to add service {service_name}: {e}");
280                failed_service_data.push((service_name.clone(), e.to_string()));
281            }
282        }
283
284        node_number += 1;
285        node_port = increment_port_option(node_port);
286        metrics_port = increment_port_option(metrics_port);
287        rpc_port = increment_port_option(rpc_port);
288    }
289
290    if options.delete_antnode_src {
291        debug!("Deleting antnode binary file");
292        std::fs::remove_file(options.antnode_src_path)?;
293    }
294
295    if !added_service_data.is_empty() {
296        info!("Added {} services", added_service_data.len());
297    } else if !failed_service_data.is_empty() {
298        error!("Failed to add {} service(s)", failed_service_data.len());
299    }
300
301    if !added_service_data.is_empty() && verbosity != VerbosityLevel::Minimal {
302        println!("Services Added:");
303        for install in added_service_data.iter() {
304            println!(" {} {}", "✓".green(), install.0);
305            println!("    - Antnode path: {}", install.1);
306            println!("    - Data path: {}", install.2);
307            println!("    - Log path: {}", install.3);
308            println!("    - RPC port: {}", install.4);
309        }
310        println!("[!] Note: newly added services have not been started");
311    }
312
313    if !failed_service_data.is_empty() {
314        if verbosity != VerbosityLevel::Minimal {
315            println!("Failed to add {} service(s):", failed_service_data.len());
316            for failed in failed_service_data.iter() {
317                println!("{} {}: {}", "✕".red(), failed.0, failed.1);
318            }
319        }
320        return Err(eyre!("Failed to add one or more services")
321            .suggestion("However, any services that were successfully added will be usable."));
322    }
323
324    let added_services_names = added_service_data
325        .into_iter()
326        .map(|(name, ..)| name)
327        .collect();
328
329    Ok(added_services_names)
330}
331
332/// Install the daemon as a service.
333///
334/// This only defines the service; it does not start it.
335pub async fn add_daemon(
336    options: AddDaemonServiceOptions,
337    node_registry: NodeRegistryManager,
338    service_control: &dyn ServiceControl,
339) -> Result<()> {
340    if node_registry.daemon.read().await.is_some() {
341        error!("A antctld service has already been created");
342        return Err(eyre!("A antctld service has already been created"));
343    }
344
345    debug!(
346        "Copying daemon binary file to {:?}",
347        options.daemon_install_bin_path
348    );
349    std::fs::copy(
350        options.daemon_src_bin_path.clone(),
351        options.daemon_install_bin_path.clone(),
352    )?;
353
354    let install_ctx = ServiceInstallCtx {
355        args: vec![
356            OsString::from("--port"),
357            OsString::from(options.port.to_string()),
358            OsString::from("--address"),
359            OsString::from(options.address.to_string()),
360        ],
361        autostart: true,
362        contents: None,
363        environment: options.env_variables,
364        label: DAEMON_SERVICE_NAME.parse()?,
365        program: options.daemon_install_bin_path.clone(),
366        restart_policy: service_manager::RestartPolicy::OnSuccess { delay_secs: None },
367        username: Some(options.user),
368        working_directory: None,
369    };
370
371    match service_control.install(install_ctx, false) {
372        Ok(()) => {
373            let daemon = DaemonServiceData {
374                daemon_path: options.daemon_install_bin_path.clone(),
375                endpoint: Some(SocketAddr::new(IpAddr::V4(options.address), options.port)),
376                pid: None,
377                service_name: DAEMON_SERVICE_NAME.to_string(),
378                status: ServiceStatus::Added,
379                version: options.version,
380            };
381
382            node_registry.insert_daemon(daemon).await;
383            info!("Daemon service has been added successfully");
384            println!("Daemon service added {}", "✓".green());
385            println!("[!] Note: the service has not been started");
386            node_registry.save().await?;
387            std::fs::remove_file(options.daemon_src_bin_path)?;
388            Ok(())
389        }
390        Err(e) => {
391            error!("Failed to add daemon service: {e}");
392            println!("Failed to add daemon service: {e}");
393            Err(e.into())
394        }
395    }
396}