1pub 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
37pub 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 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 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
346pub 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
429pub 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
493pub 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}