1use crate::{
8 ansible::{
9 inventory::{
10 generate_environment_inventory,
11 generate_full_cone_private_node_static_environment_inventory,
12 generate_symmetric_private_node_static_environment_inventory, AnsibleInventoryType,
13 },
14 provisioning::{AnsibleProvisioner, PrivateNodeProvisionInventory},
15 AnsibleRunner,
16 },
17 get_bootstrap_cache_url, get_environment_details, get_genesis_multiaddr,
18 s3::S3Repository,
19 ssh::SshClient,
20 terraform::TerraformRunner,
21 BinaryOption, CloudProvider, DeploymentType, EnvironmentDetails, Error, TestnetDeployer,
22};
23use alloy::hex::ToHexExt;
24use ant_service_management::{NodeRegistry, ServiceStatus};
25use color_eyre::{eyre::eyre, Result};
26use log::debug;
27use rand::seq::{IteratorRandom, SliceRandom};
28use semver::Version;
29use serde::{Deserialize, Serialize};
30use std::{
31 collections::{HashMap, HashSet},
32 convert::From,
33 fs::File,
34 io::Write,
35 net::{IpAddr, SocketAddr},
36 path::PathBuf,
37};
38
39const DEFAULT_CONTACTS_COUNT: usize = 100;
40const UNAVAILABLE_NODE: &str = "-";
41const TESTNET_BUCKET_NAME: &str = "sn-testnet";
42
43pub struct DeploymentInventoryService {
44 pub ansible_runner: AnsibleRunner,
45 pub ansible_provisioner: AnsibleProvisioner,
49 pub cloud_provider: CloudProvider,
50 pub inventory_file_path: PathBuf,
51 pub s3_repository: S3Repository,
52 pub ssh_client: SshClient,
53 pub terraform_runner: TerraformRunner,
54 pub working_directory_path: PathBuf,
55}
56
57impl From<&TestnetDeployer> for DeploymentInventoryService {
58 fn from(item: &TestnetDeployer) -> Self {
59 let provider = match item.cloud_provider {
60 CloudProvider::Aws => "aws",
61 CloudProvider::DigitalOcean => "digital_ocean",
62 };
63 DeploymentInventoryService {
64 ansible_runner: item.ansible_provisioner.ansible_runner.clone(),
65 ansible_provisioner: item.ansible_provisioner.clone(),
66 cloud_provider: item.cloud_provider,
67 inventory_file_path: item
68 .working_directory_path
69 .join("ansible")
70 .join("inventory")
71 .join(format!("dev_inventory_{}.yml", provider)),
72 s3_repository: item.s3_repository.clone(),
73 ssh_client: item.ssh_client.clone(),
74 terraform_runner: item.terraform_runner.clone(),
75 working_directory_path: item.working_directory_path.clone(),
76 }
77 }
78}
79
80impl DeploymentInventoryService {
81 pub async fn generate_or_retrieve_inventory(
96 &self,
97 name: &str,
98 force: bool,
99 binary_option: Option<BinaryOption>,
100 ) -> Result<DeploymentInventory> {
101 println!("======================================");
102 println!(" Generating or Retrieving Inventory ");
103 println!("======================================");
104 let inventory_path = get_data_directory()?.join(format!("{name}-inventory.json"));
105 if inventory_path.exists() && !force {
106 let inventory = DeploymentInventory::read(&inventory_path)?;
107 return Ok(inventory);
108 }
109
110 if !force {
113 let environments = self.terraform_runner.workspace_list()?;
114 if !environments.contains(&name.to_string()) {
115 return Err(eyre!("The '{}' environment does not exist", name));
116 }
117 }
118
119 let output_inventory_dir_path = self
124 .working_directory_path
125 .join("ansible")
126 .join("inventory");
127 generate_environment_inventory(
128 name,
129 &self.inventory_file_path,
130 &output_inventory_dir_path,
131 )?;
132
133 let environment_details = match get_environment_details(name, &self.s3_repository).await {
134 Ok(details) => details,
135 Err(Error::EnvironmentDetailsNotFound(_)) => {
136 println!("Environment details not found: treating this as a new deployment");
137 return Ok(DeploymentInventory::empty(
138 name,
139 binary_option.ok_or_else(|| {
140 eyre!("For a new deployment the binary option must be set")
141 })?,
142 ));
143 }
144 Err(e) => return Err(e.into()),
145 };
146
147 let genesis_vm = self
148 .ansible_runner
149 .get_inventory(AnsibleInventoryType::Genesis, false)?;
150
151 let mut misc_vms = Vec::new();
152 let build_vm = self
153 .ansible_runner
154 .get_inventory(AnsibleInventoryType::Build, false)?;
155 misc_vms.extend(build_vm);
156
157 let full_cone_nat_gateway_vms = self
158 .ansible_runner
159 .get_inventory(AnsibleInventoryType::FullConeNatGateway, false)?;
160 let full_cone_private_node_vms = self
161 .ansible_runner
162 .get_inventory(AnsibleInventoryType::FullConePrivateNodes, false)?;
163
164 let symmetric_nat_gateway_vms = self
165 .ansible_runner
166 .get_inventory(AnsibleInventoryType::SymmetricNatGateway, false)?;
167 let symmetric_private_node_vms = self
168 .ansible_runner
169 .get_inventory(AnsibleInventoryType::SymmetricPrivateNodes, false)?;
170
171 let generic_node_vms = self
172 .ansible_runner
173 .get_inventory(AnsibleInventoryType::Nodes, false)?;
174
175 generate_full_cone_private_node_static_environment_inventory(
177 name,
178 &output_inventory_dir_path,
179 &full_cone_private_node_vms,
180 &full_cone_nat_gateway_vms,
181 &self.ssh_client.private_key_path,
182 )?;
183 generate_symmetric_private_node_static_environment_inventory(
184 name,
185 &output_inventory_dir_path,
186 &symmetric_private_node_vms,
187 &symmetric_nat_gateway_vms,
188 &self.ssh_client.private_key_path,
189 )?;
190
191 if !symmetric_nat_gateway_vms.is_empty() {
193 self.ssh_client.set_symmetric_nat_routed_vms(
194 &symmetric_private_node_vms,
195 &symmetric_nat_gateway_vms,
196 )?;
197 }
198 if !full_cone_nat_gateway_vms.is_empty() {
199 self.ssh_client.set_full_cone_nat_routed_vms(
200 &full_cone_private_node_vms,
201 &full_cone_nat_gateway_vms,
202 )?;
203 }
204
205 let peer_cache_node_vms = self
206 .ansible_runner
207 .get_inventory(AnsibleInventoryType::PeerCacheNodes, false)?;
208
209 let uploader_vms = if environment_details.deployment_type != DeploymentType::Bootstrap {
210 let uploader_and_sks = self.ansible_provisioner.get_uploader_secret_keys()?;
211 uploader_and_sks
212 .iter()
213 .map(|(vm, sks)| UploaderVirtualMachine {
214 vm: vm.clone(),
215 wallet_public_key: sks
216 .iter()
217 .enumerate()
218 .map(|(user, sk)| (format!("safe{}", user + 1), sk.address().encode_hex()))
219 .collect(),
220 })
221 .collect()
222 } else {
223 Vec::new()
224 };
225
226 println!("Retrieving node registries from all VMs...");
227 let mut failed_node_registry_vms = Vec::new();
228
229 let peer_cache_node_registries = self
230 .ansible_provisioner
231 .get_node_registries(&AnsibleInventoryType::PeerCacheNodes)?;
232 let peer_cache_node_vms =
233 NodeVirtualMachine::from_list(&peer_cache_node_vms, &peer_cache_node_registries);
234
235 let generic_node_registries = self
236 .ansible_provisioner
237 .get_node_registries(&AnsibleInventoryType::Nodes)?;
238 let generic_node_vms =
239 NodeVirtualMachine::from_list(&generic_node_vms, &generic_node_registries);
240
241 let symmetric_private_node_registries = self
242 .ansible_provisioner
243 .get_node_registries(&AnsibleInventoryType::SymmetricPrivateNodes)?;
244 let symmetric_private_node_vms = NodeVirtualMachine::from_list(
245 &symmetric_private_node_vms,
246 &symmetric_private_node_registries,
247 );
248 let full_cone_private_node_registries = self
249 .ansible_provisioner
250 .get_node_registries(&AnsibleInventoryType::FullConePrivateNodes)?;
251 debug!("full_cone_private_node_vms: {full_cone_private_node_vms:?}");
252 let full_cone_private_node_gateway_vm_map =
253 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
254 &full_cone_private_node_vms,
255 &full_cone_nat_gateway_vms,
256 )?;
257 debug!("full_cone_private_node_gateway_vm_map: {full_cone_private_node_gateway_vm_map:?}");
258 let full_cone_private_node_vms = NodeVirtualMachine::from_list(
259 &full_cone_private_node_vms,
260 &full_cone_private_node_registries,
261 );
262
263 let genesis_node_registry = self
264 .ansible_provisioner
265 .get_node_registries(&AnsibleInventoryType::Genesis)?;
266 let genesis_vm = NodeVirtualMachine::from_list(&genesis_vm, &genesis_node_registry);
267 let genesis_vm = if !genesis_vm.is_empty() {
268 Some(genesis_vm[0].clone())
269 } else {
270 None
271 };
272
273 failed_node_registry_vms.extend(peer_cache_node_registries.failed_vms);
274 failed_node_registry_vms.extend(generic_node_registries.failed_vms);
275 failed_node_registry_vms.extend(full_cone_private_node_registries.failed_vms);
276 failed_node_registry_vms.extend(symmetric_private_node_registries.failed_vms);
277 failed_node_registry_vms.extend(genesis_node_registry.failed_vms);
278
279 let binary_option = if let Some(binary_option) = binary_option {
280 binary_option
281 } else {
282 let (antnode_version, antctl_version) = {
283 let mut random_vm = None;
284 if !generic_node_vms.is_empty() {
285 random_vm = generic_node_vms.first().cloned();
286 } else if !peer_cache_node_vms.is_empty() {
287 random_vm = peer_cache_node_vms.first().cloned();
288 } else if genesis_vm.is_some() {
289 random_vm = genesis_vm.clone()
290 };
291
292 let Some(random_vm) = random_vm else {
293 return Err(eyre!("Unable to obtain a VM to retrieve versions"));
294 };
295
296 let antnode_version = self.get_bin_version(
298 &random_vm.vm,
299 "/mnt/antnode-storage/data/antnode1/antnode --version",
300 "Autonomi Node v",
301 )?;
302 let antctl_version = self.get_bin_version(
303 &random_vm.vm,
304 "antctl --version",
305 "Autonomi Node Manager v",
306 )?;
307 (antnode_version, antctl_version)
308 };
309
310 let ant_version = if environment_details.deployment_type != DeploymentType::Bootstrap {
311 let random_uploader_vm = uploader_vms
312 .choose(&mut rand::thread_rng())
313 .ok_or_else(|| eyre!("No uploader VMs available to retrieve ant version"))?;
314 Some(self.get_bin_version(
315 &random_uploader_vm.vm,
316 "ant --version",
317 "Autonomi Client v",
318 )?)
319 } else {
320 None
321 };
322
323 println!("Retrieved binary versions from previous deployment:");
324 println!(" antnode: {}", antnode_version);
325 println!(" antctl: {}", antctl_version);
326 if let Some(version) = &ant_version {
327 println!(" ant: {}", version);
328 }
329
330 BinaryOption::Versioned {
331 ant_version,
332 antnode_version,
333 antctl_version,
334 }
335 };
336
337 let (genesis_multiaddr, genesis_ip) =
338 if environment_details.deployment_type == DeploymentType::New {
339 match get_genesis_multiaddr(&self.ansible_runner, &self.ssh_client) {
340 Ok((multiaddr, ip)) => (Some(multiaddr), Some(ip)),
341 Err(_) => (None, None),
342 }
343 } else {
344 (None, None)
345 };
346 let inventory = DeploymentInventory {
347 binary_option,
348 environment_details,
349 failed_node_registry_vms,
350 faucet_address: genesis_ip.map(|ip| format!("{ip}:8000")),
351 full_cone_nat_gateway_vms,
352 full_cone_private_node_vms,
353 genesis_multiaddr,
354 genesis_vm,
355 name: name.to_string(),
356 misc_vms,
357 node_vms: generic_node_vms,
358 peer_cache_node_vms,
359 ssh_user: self.cloud_provider.get_ssh_user(),
360 ssh_private_key_path: self.ssh_client.private_key_path.clone(),
361 symmetric_nat_gateway_vms,
362 symmetric_private_node_vms,
363 uploaded_files: Vec::new(),
364 uploader_vms,
365 };
366 debug!("Inventory: {inventory:?}");
367 Ok(inventory)
368 }
369
370 pub fn setup_environment_inventory(&self, name: &str) -> Result<()> {
375 let output_inventory_dir_path = self
376 .working_directory_path
377 .join("ansible")
378 .join("inventory");
379 generate_environment_inventory(
380 name,
381 &self.inventory_file_path,
382 &output_inventory_dir_path,
383 )?;
384
385 let full_cone_nat_gateway_vms = self
386 .ansible_runner
387 .get_inventory(AnsibleInventoryType::FullConeNatGateway, false)?;
388 let full_cone_private_node_vms = self
389 .ansible_runner
390 .get_inventory(AnsibleInventoryType::FullConePrivateNodes, false)?;
391
392 let symmetric_nat_gateway_vms = self
393 .ansible_runner
394 .get_inventory(AnsibleInventoryType::SymmetricNatGateway, false)?;
395 let symmetric_private_node_vms = self
396 .ansible_runner
397 .get_inventory(AnsibleInventoryType::SymmetricPrivateNodes, false)?;
398
399 generate_symmetric_private_node_static_environment_inventory(
401 name,
402 &output_inventory_dir_path,
403 &symmetric_private_node_vms,
404 &symmetric_nat_gateway_vms,
405 &self.ssh_client.private_key_path,
406 )?;
407
408 generate_full_cone_private_node_static_environment_inventory(
409 name,
410 &output_inventory_dir_path,
411 &full_cone_private_node_vms,
412 &full_cone_nat_gateway_vms,
413 &self.ssh_client.private_key_path,
414 )?;
415
416 if !full_cone_nat_gateway_vms.is_empty() {
418 self.ssh_client.set_full_cone_nat_routed_vms(
419 &full_cone_private_node_vms,
420 &full_cone_nat_gateway_vms,
421 )?;
422 }
423
424 if !symmetric_nat_gateway_vms.is_empty() {
425 self.ssh_client.set_symmetric_nat_routed_vms(
426 &symmetric_private_node_vms,
427 &symmetric_nat_gateway_vms,
428 )?;
429 }
430
431 Ok(())
432 }
433
434 pub async fn upload_network_contacts(
435 &self,
436 inventory: &DeploymentInventory,
437 contacts_file_name: Option<String>,
438 ) -> Result<()> {
439 let temp_dir_path = tempfile::tempdir()?.into_path();
440 let temp_file_path = if let Some(file_name) = contacts_file_name {
441 temp_dir_path.join(file_name)
442 } else {
443 temp_dir_path.join(inventory.name.clone())
444 };
445
446 let mut file = std::fs::File::create(&temp_file_path)?;
447 let mut rng = rand::thread_rng();
448
449 let peer_cache_peers = inventory
450 .peer_cache_node_vms
451 .iter()
452 .flat_map(|vm| vm.get_quic_addresses())
453 .collect::<Vec<_>>();
454 let peer_cache_peers_len = peer_cache_peers.len();
455 for peer in peer_cache_peers
456 .iter()
457 .filter(|&peer| peer != UNAVAILABLE_NODE)
458 .cloned()
459 .choose_multiple(&mut rng, DEFAULT_CONTACTS_COUNT)
460 {
461 writeln!(file, "{peer}",)?;
462 }
463
464 if DEFAULT_CONTACTS_COUNT > peer_cache_peers_len {
465 let node_peers = inventory
466 .node_vms
467 .iter()
468 .flat_map(|vm| vm.get_quic_addresses())
469 .collect::<Vec<_>>();
470 for peer in node_peers
471 .iter()
472 .filter(|&peer| peer != UNAVAILABLE_NODE)
473 .cloned()
474 .choose_multiple(&mut rng, DEFAULT_CONTACTS_COUNT - peer_cache_peers_len)
475 {
476 writeln!(file, "{peer}",)?;
477 }
478 }
479
480 self.s3_repository
481 .upload_file(TESTNET_BUCKET_NAME, &temp_file_path, true)
482 .await?;
483
484 Ok(())
485 }
486
487 fn get_bin_version(&self, vm: &VirtualMachine, command: &str, prefix: &str) -> Result<Version> {
489 let output = self.ssh_client.run_command(
490 &vm.public_ip_addr,
491 &self.cloud_provider.get_ssh_user(),
492 command,
493 true,
494 )?;
495 let version_line = output
496 .first()
497 .ok_or_else(|| eyre!("No output from {} command", command))?;
498 let version_str = version_line
499 .strip_prefix(prefix)
500 .ok_or_else(|| eyre!("Unexpected output format from {} command", command))?;
501 Version::parse(version_str).map_err(|e| eyre!("Failed to parse {} version: {}", command, e))
502 }
503}
504
505impl NodeVirtualMachine {
506 pub fn from_list(
507 vms: &[VirtualMachine],
508 node_registries: &DeploymentNodeRegistries,
509 ) -> Vec<Self> {
510 let mut node_vms = Vec::new();
511 for vm in vms {
512 let node_registry = node_registries
513 .retrieved_registries
514 .iter()
515 .find(|(name, _)| {
516 if vm.name.contains("private") {
517 let result = name == &vm.private_ip_addr.to_string();
518 debug!(
519 "Vm name: {name} is a private node with result {result}. Vm: {vm:?}"
520 );
521 result
522 } else {
523 name == &vm.name
524 }
525 })
526 .map(|(_, reg)| reg);
527 let Some(node_registry) = node_registry else {
528 debug!("No node registry found for vm: {vm:?}. Skipping");
529 continue;
530 };
531
532 let node_vm = Self {
533 node_count: node_registry.nodes.len(),
534 node_listen_addresses: node_registry
535 .nodes
536 .iter()
537 .map(|node| {
538 if let Some(listen_addresses) = &node.listen_addr {
539 listen_addresses
540 .iter()
541 .map(|addr| addr.to_string())
542 .collect()
543 } else {
544 vec![UNAVAILABLE_NODE.to_string()]
545 }
546 })
547 .collect(),
548 rpc_endpoint: node_registry
549 .nodes
550 .iter()
551 .map(|node| {
552 let id = if let Some(peer_id) = node.peer_id {
553 peer_id.to_string().clone()
554 } else {
555 UNAVAILABLE_NODE.to_string()
556 };
557 (id, node.rpc_socket_addr)
558 })
559 .collect(),
560 safenodemand_endpoint: node_registry
561 .daemon
562 .as_ref()
563 .and_then(|daemon| daemon.endpoint),
564 vm: vm.clone(),
565 };
566 node_vms.push(node_vm);
567 }
568 debug!("Node VMs generated from NodeRegistries: {node_vms:?}");
569 node_vms
570 }
571
572 pub fn get_quic_addresses(&self) -> Vec<String> {
573 self.node_listen_addresses
574 .iter()
575 .map(|addresses| {
576 addresses
577 .iter()
578 .find(|addr| {
579 addr.contains("/quic-v1")
580 && !addr.starts_with("/ip4/127.0.0.1")
581 && !addr.starts_with("/ip4/10.")
582 })
583 .map(|s| s.to_string())
584 .unwrap_or_else(|| UNAVAILABLE_NODE.to_string())
585 })
586 .collect()
587 }
588}
589
590pub type OsUser = String;
592
593#[derive(Clone, Debug, Serialize, Deserialize)]
594pub struct UploaderVirtualMachine {
595 pub vm: VirtualMachine,
596 pub wallet_public_key: HashMap<OsUser, String>,
598}
599
600#[derive(Clone, Debug, Serialize, Deserialize)]
601pub struct NodeVirtualMachine {
602 pub vm: VirtualMachine,
603 pub node_count: usize,
604 pub node_listen_addresses: Vec<Vec<String>>,
605 pub rpc_endpoint: HashMap<String, SocketAddr>,
606 pub safenodemand_endpoint: Option<SocketAddr>,
607}
608
609#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
610pub struct VirtualMachine {
611 pub id: u64,
612 pub name: String,
613 pub public_ip_addr: IpAddr,
614 pub private_ip_addr: IpAddr,
615}
616
617#[derive(Clone)]
618pub struct DeploymentNodeRegistries {
619 pub inventory_type: AnsibleInventoryType,
620 pub retrieved_registries: Vec<(String, NodeRegistry)>,
623 pub failed_vms: Vec<String>,
624}
625
626impl DeploymentNodeRegistries {
627 pub fn print(&self) {
628 Self::print_banner(&self.inventory_type.to_string());
629 for (vm_name, registry) in self.retrieved_registries.iter() {
630 println!("{vm_name}:");
631 for node in registry.nodes.iter() {
632 println!(
633 " {}: {} {}",
634 node.service_name,
635 node.version,
636 Self::format_status(&node.status)
637 );
638 }
639 }
640 if !self.failed_vms.is_empty() {
641 println!(
642 "Failed to retrieve node registries for {}:",
643 self.inventory_type
644 );
645 for vm_name in self.failed_vms.iter() {
646 println!("- {}", vm_name);
647 }
648 }
649 }
650
651 fn format_status(status: &ServiceStatus) -> String {
652 match status {
653 ServiceStatus::Running => "RUNNING".to_string(),
654 ServiceStatus::Stopped => "STOPPED".to_string(),
655 ServiceStatus::Added => "ADDED".to_string(),
656 ServiceStatus::Removed => "REMOVED".to_string(),
657 }
658 }
659
660 fn print_banner(text: &str) {
661 let padding = 2;
662 let text_width = text.len() + padding * 2;
663 let border_chars = 2;
664 let total_width = text_width + border_chars;
665 let top_bottom = "═".repeat(total_width);
666
667 println!("╔{}╗", top_bottom);
668 println!("║ {:^width$} ║", text, width = text_width);
669 println!("╚{}╝", top_bottom);
670 }
671}
672
673#[derive(Clone, Debug, Serialize, Deserialize)]
674pub struct DeploymentInventory {
675 pub binary_option: BinaryOption,
676 pub environment_details: EnvironmentDetails,
677 pub failed_node_registry_vms: Vec<String>,
678 pub faucet_address: Option<String>,
679 pub full_cone_nat_gateway_vms: Vec<VirtualMachine>,
680 pub full_cone_private_node_vms: Vec<NodeVirtualMachine>,
681 pub genesis_vm: Option<NodeVirtualMachine>,
682 pub genesis_multiaddr: Option<String>,
683 pub misc_vms: Vec<VirtualMachine>,
684 pub name: String,
685 pub node_vms: Vec<NodeVirtualMachine>,
686 pub peer_cache_node_vms: Vec<NodeVirtualMachine>,
687 pub ssh_user: String,
688 pub ssh_private_key_path: PathBuf,
689 pub symmetric_nat_gateway_vms: Vec<VirtualMachine>,
690 pub symmetric_private_node_vms: Vec<NodeVirtualMachine>,
691 pub uploaded_files: Vec<(String, String)>,
692 pub uploader_vms: Vec<UploaderVirtualMachine>,
693}
694
695impl DeploymentInventory {
696 pub fn empty(name: &str, binary_option: BinaryOption) -> DeploymentInventory {
699 Self {
700 binary_option,
701 environment_details: EnvironmentDetails::default(),
702 genesis_vm: Default::default(),
703 genesis_multiaddr: Default::default(),
704 failed_node_registry_vms: Default::default(),
705 faucet_address: Default::default(),
706 full_cone_nat_gateway_vms: Default::default(),
707 full_cone_private_node_vms: Default::default(),
708 misc_vms: Default::default(),
709 name: name.to_string(),
710 node_vms: Default::default(),
711 peer_cache_node_vms: Default::default(),
712 ssh_user: "root".to_string(),
713 ssh_private_key_path: Default::default(),
714 symmetric_nat_gateway_vms: Default::default(),
715 symmetric_private_node_vms: Default::default(),
716 uploaded_files: Default::default(),
717 uploader_vms: Default::default(),
718 }
719 }
720
721 pub fn get_tfvars_filename(&self) -> String {
722 let filename = self
723 .environment_details
724 .environment_type
725 .get_tfvars_filename(&self.name);
726 debug!("Using tfvars file {filename}",);
727 filename
728 }
729
730 pub fn is_empty(&self) -> bool {
731 self.peer_cache_node_vms.is_empty() && self.node_vms.is_empty()
732 }
733
734 pub fn vm_list(&self) -> Vec<VirtualMachine> {
735 let mut list = Vec::new();
736 list.extend(self.symmetric_nat_gateway_vms.clone());
737 list.extend(self.full_cone_nat_gateway_vms.clone());
738 list.extend(
739 self.peer_cache_node_vms
740 .iter()
741 .map(|node_vm| node_vm.vm.clone()),
742 );
743 list.extend(self.genesis_vm.iter().map(|node_vm| node_vm.vm.clone()));
744 list.extend(self.node_vms.iter().map(|node_vm| node_vm.vm.clone()));
745 list.extend(self.misc_vms.clone());
746 list.extend(
747 self.symmetric_private_node_vms
748 .iter()
749 .map(|node_vm| node_vm.vm.clone()),
750 );
751 list.extend(
752 self.full_cone_private_node_vms
753 .iter()
754 .map(|node_vm| node_vm.vm.clone()),
755 );
756 list.extend(
757 self.uploader_vms
758 .iter()
759 .map(|uploader_vm| uploader_vm.vm.clone()),
760 );
761 list
762 }
763
764 pub fn node_vm_list(&self) -> Vec<NodeVirtualMachine> {
765 let mut list = Vec::new();
766 list.extend(self.peer_cache_node_vms.iter().cloned());
767 list.extend(self.genesis_vm.iter().cloned());
768 list.extend(self.node_vms.iter().cloned());
769 list.extend(self.full_cone_private_node_vms.iter().cloned());
770 list.extend(self.symmetric_private_node_vms.iter().cloned());
771
772 list
773 }
774
775 pub fn peers(&self) -> HashSet<String> {
776 let mut list = HashSet::new();
777 list.extend(
778 self.peer_cache_node_vms
779 .iter()
780 .flat_map(|node_vm| node_vm.get_quic_addresses()),
781 );
782 list.extend(
783 self.genesis_vm
784 .iter()
785 .flat_map(|node_vm| node_vm.get_quic_addresses()),
786 );
787 list.extend(
788 self.node_vms
789 .iter()
790 .flat_map(|node_vm| node_vm.get_quic_addresses()),
791 );
792 list.extend(
793 self.full_cone_private_node_vms
794 .iter()
795 .flat_map(|node_vm| node_vm.get_quic_addresses()),
796 );
797 list.extend(
798 self.symmetric_private_node_vms
799 .iter()
800 .flat_map(|node_vm| node_vm.get_quic_addresses()),
801 );
802 list
803 }
804
805 pub fn save(&self) -> Result<()> {
806 let path = get_data_directory()?.join(format!("{}-inventory.json", self.name));
807 let serialized_data = serde_json::to_string_pretty(self)?;
808 let mut file = File::create(path)?;
809 file.write_all(serialized_data.as_bytes())?;
810 Ok(())
811 }
812
813 pub fn read(file_path: &PathBuf) -> Result<Self> {
814 let data = std::fs::read_to_string(file_path)?;
815 let deserialized_data: DeploymentInventory = serde_json::from_str(&data)?;
816 Ok(deserialized_data)
817 }
818
819 pub fn add_uploaded_files(&mut self, uploaded_files: Vec<(String, String)>) {
820 self.uploaded_files.extend_from_slice(&uploaded_files);
821 }
822
823 pub fn get_random_peer(&self) -> Option<String> {
824 let mut rng = rand::thread_rng();
825 self.peers().into_iter().choose(&mut rng)
826 }
827
828 pub fn peer_cache_node_count(&self) -> usize {
829 if let Some(first_vm) = self.peer_cache_node_vms.first() {
830 first_vm.node_count
831 } else {
832 0
833 }
834 }
835
836 pub fn genesis_node_count(&self) -> usize {
837 if let Some(genesis_vm) = &self.genesis_vm {
838 genesis_vm.node_count
839 } else {
840 0
841 }
842 }
843
844 pub fn node_count(&self) -> usize {
845 if let Some(first_vm) = self.node_vms.first() {
846 first_vm.node_count
847 } else {
848 0
849 }
850 }
851
852 pub fn full_cone_private_node_count(&self) -> usize {
853 if let Some(first_vm) = self.full_cone_private_node_vms.first() {
854 first_vm.node_count
855 } else {
856 0
857 }
858 }
859
860 pub fn symmetric_private_node_count(&self) -> usize {
861 if let Some(first_vm) = self.symmetric_private_node_vms.first() {
862 first_vm.node_count
863 } else {
864 0
865 }
866 }
867
868 pub fn print_report(&self, full: bool) -> Result<()> {
869 println!("**************************************");
870 println!("* *");
871 println!("* Inventory Report *");
872 println!("* *");
873 println!("**************************************");
874
875 println!("Environment Name: {}", self.name);
876 println!();
877 match &self.binary_option {
878 BinaryOption::BuildFromSource {
879 repo_owner, branch, ..
880 } => {
881 println!("==============");
882 println!("Branch Details");
883 println!("==============");
884 println!("Repo owner: {repo_owner}");
885 println!("Branch name: {branch}");
886 println!();
887 }
888 BinaryOption::Versioned {
889 ant_version: safe_version,
890 antnode_version: safenode_version,
891 antctl_version: safenode_manager_version,
892 } => {
893 println!("===============");
894 println!("Version Details");
895 println!("===============");
896 println!(
897 "safe version: {}",
898 safe_version
899 .as_ref()
900 .map_or("N/A".to_string(), |v| v.to_string())
901 );
902 println!("safenode version: {}", safenode_version);
903 println!("safenode-manager version: {}", safenode_manager_version);
904 println!();
905 }
906 }
907
908 if !self.peer_cache_node_vms.is_empty() {
909 println!("==============");
910 println!("Peer Cache VMs");
911 println!("==============");
912 for node_vm in self.peer_cache_node_vms.iter() {
913 println!("{}: {}", node_vm.vm.name, node_vm.vm.public_ip_addr);
914 }
915 println!("Nodes per VM: {}", self.peer_cache_node_count());
916 println!("SSH user: {}", self.ssh_user);
917 println!();
918
919 self.print_peer_cache_webserver();
920 }
921
922 println!("========");
923 println!("Node VMs");
924 println!("========");
925 if let Some(genesis_vm) = &self.genesis_vm {
926 println!("{}: {}", genesis_vm.vm.name, genesis_vm.vm.public_ip_addr);
927 }
928 for node_vm in self.node_vms.iter() {
929 println!("{}: {}", node_vm.vm.name, node_vm.vm.public_ip_addr);
930 }
931 println!("Nodes per VM: {}", self.node_count());
932 println!("SSH user: {}", self.ssh_user);
933 println!();
934
935 println!("=================");
936 println!("Full Cone Private Node VMs");
937 println!("=================");
938 let full_cone_private_node_nat_gateway_map =
939 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
940 self.full_cone_private_node_vms
941 .iter()
942 .map(|node_vm| node_vm.vm.clone())
943 .collect::<Vec<_>>()
944 .as_slice(),
945 &self.full_cone_nat_gateway_vms,
946 )?;
947
948 for (node_vm, nat_gateway_vm) in full_cone_private_node_nat_gateway_map.iter() {
949 println!(
950 "{}: {} ==routed through==> {}: {}",
951 node_vm.name,
952 node_vm.public_ip_addr,
953 nat_gateway_vm.name,
954 nat_gateway_vm.public_ip_addr
955 );
956 let ssh = if let Some(ssh_key_path) = self.ssh_private_key_path.to_str() {
957 format!(
958 "ssh -i {ssh_key_path} root@{}",
959 nat_gateway_vm.public_ip_addr,
960 )
961 } else {
962 format!("ssh root@{}", nat_gateway_vm.public_ip_addr,)
963 };
964 println!("SSH using NAT gateway: {ssh}");
965 }
966 println!("Nodes per VM: {}", self.node_count());
967 println!("SSH user: {}", self.ssh_user);
968 println!();
969
970 println!("=================");
971 println!("Symmetric Private Node VMs");
972 println!("=================");
973 let symmetric_private_node_nat_gateway_map =
974 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
975 self.symmetric_private_node_vms
976 .iter()
977 .map(|node_vm| node_vm.vm.clone())
978 .collect::<Vec<_>>()
979 .as_slice(),
980 &self.symmetric_nat_gateway_vms,
981 )?;
982
983 for (node_vm, nat_gateway_vm) in symmetric_private_node_nat_gateway_map.iter() {
984 println!(
985 "{}: {} ==routed through==> {}: {}",
986 node_vm.name,
987 node_vm.public_ip_addr,
988 nat_gateway_vm.name,
989 nat_gateway_vm.public_ip_addr
990 );
991 let ssh = if let Some(ssh_key_path) = self.ssh_private_key_path.to_str() {
992 format!(
993 "ssh -i {ssh_key_path} -o ProxyCommand=\"ssh -W %h:%p root@{} -i {ssh_key_path}\" root@{}",
994 nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
995 )
996 } else {
997 format!(
998 "ssh -o ProxyCommand=\"ssh -W %h:%p root@{}\" root@{}",
999 nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
1000 )
1001 };
1002 println!("SSH using NAT gateway: {ssh}");
1003 }
1004
1005 if !self.uploader_vms.is_empty() {
1006 println!("============");
1007 println!("Uploader VMs");
1008 println!("============");
1009 for uploader_vm in self.uploader_vms.iter() {
1010 println!("{}: {}", uploader_vm.vm.name, uploader_vm.vm.public_ip_addr);
1011 }
1012 println!();
1013
1014 println!("===========================");
1015 println!("Uploader Wallet Public Keys");
1016 println!("===========================");
1017 for uploader_vm in self.uploader_vms.iter() {
1018 for (user, key) in uploader_vm.wallet_public_key.iter() {
1019 println!("{}@{}: {}", uploader_vm.vm.name, user, key);
1020 }
1021 }
1022 }
1023
1024 if !self.misc_vms.is_empty() {
1025 println!("=========");
1026 println!("Other VMs");
1027 println!("=========");
1028 }
1029 if !self.misc_vms.is_empty() {
1030 for vm in self.misc_vms.iter() {
1031 println!("{}: {}", vm.name, vm.public_ip_addr);
1032 }
1033 }
1034
1035 for nat_gateway_vm in self.full_cone_nat_gateway_vms.iter() {
1036 println!("{}: {}", nat_gateway_vm.name, nat_gateway_vm.public_ip_addr);
1037 }
1038
1039 for nat_gateway_vm in self.symmetric_nat_gateway_vms.iter() {
1040 println!("{}: {}", nat_gateway_vm.name, nat_gateway_vm.public_ip_addr);
1041 }
1042
1043 println!("SSH user: {}", self.ssh_user);
1044 println!();
1045
1046 if full {
1047 println!("===============");
1048 println!("Full Peer List");
1049 println!("===============");
1050 let mut quic_listeners = Vec::new();
1051 let mut ws_listeners = Vec::new();
1052
1053 for node_vm in self.peer_cache_node_vms.iter().chain(self.node_vms.iter()) {
1054 for addresses in &node_vm.node_listen_addresses {
1055 for addr in addresses {
1056 if !addr.starts_with("/ip4/127.0.0.1") && !addr.starts_with("/ip4/10.") {
1057 if addr.contains("/quic") {
1058 quic_listeners.push(addr.clone());
1059 } else if addr.contains("/ws") {
1060 ws_listeners.push(addr.clone());
1061 }
1062 }
1063 }
1064 }
1065 }
1066
1067 if !quic_listeners.is_empty() {
1068 println!("QUIC:");
1069 for addr in quic_listeners {
1070 println!(" {addr}");
1071 }
1072 println!();
1073 }
1074
1075 if !ws_listeners.is_empty() {
1076 println!("Websocket:");
1077 for addr in ws_listeners {
1078 println!(" {addr}");
1079 }
1080 println!();
1081 }
1082 } else {
1083 println!("============");
1084 println!("Sample Peers");
1085 println!("============");
1086 self.peer_cache_node_vms
1087 .iter()
1088 .chain(self.node_vms.iter())
1089 .map(|node_vm| node_vm.vm.public_ip_addr.to_string())
1090 .for_each(|ip| {
1091 if let Some(peer) = self.peers().iter().find(|p| p.contains(&ip)) {
1092 println!("{peer}");
1093 }
1094 });
1095 }
1096 println!();
1097
1098 println!(
1099 "Genesis: {}",
1100 self.genesis_multiaddr
1101 .as_ref()
1102 .map_or("N/A", |genesis| genesis)
1103 );
1104 let inventory_file_path =
1105 get_data_directory()?.join(format!("{}-inventory.json", self.name));
1106 println!(
1107 "The full inventory is at {}",
1108 inventory_file_path.to_string_lossy()
1109 );
1110 println!();
1111
1112 if !self.uploaded_files.is_empty() {
1113 println!("Uploaded files:");
1114 for file in self.uploaded_files.iter() {
1115 println!("{}: {}", file.0, file.1);
1116 }
1117 }
1118
1119 if self.environment_details.evm_data_payments_address.is_some()
1120 || self.environment_details.evm_payment_token_address.is_some()
1121 || self.environment_details.evm_rpc_url.is_some()
1122 {
1123 println!("===========");
1124 println!("EVM Details");
1125 println!("===========");
1126 println!(
1127 "EVM data payments address: {}",
1128 self.environment_details
1129 .evm_data_payments_address
1130 .as_ref()
1131 .map_or("N/A", |addr| addr)
1132 );
1133 println!(
1134 "EVM payment token address: {}",
1135 self.environment_details
1136 .evm_payment_token_address
1137 .as_ref()
1138 .map_or("N/A", |addr| addr)
1139 );
1140 println!(
1141 "EVM RPC URL: {}",
1142 self.environment_details
1143 .evm_rpc_url
1144 .as_ref()
1145 .map_or("N/A", |addr| addr)
1146 );
1147 }
1148
1149 Ok(())
1150 }
1151
1152 pub fn get_genesis_ip(&self) -> Option<IpAddr> {
1153 self.misc_vms
1154 .iter()
1155 .find(|vm| vm.name.contains("genesis"))
1156 .map(|vm| vm.public_ip_addr)
1157 }
1158
1159 pub fn print_peer_cache_webserver(&self) {
1160 println!("=====================");
1161 println!("Peer Cache Webservers");
1162 println!("=====================");
1163
1164 for node_vm in &self.peer_cache_node_vms {
1165 let webserver = get_bootstrap_cache_url(&node_vm.vm.public_ip_addr);
1166 println!("{}: {webserver}", node_vm.vm.name);
1167 }
1168 }
1169}
1170
1171pub fn get_data_directory() -> Result<PathBuf> {
1172 let path = dirs_next::data_dir()
1173 .ok_or_else(|| eyre!("Could not retrieve data directory"))?
1174 .join("autonomi")
1175 .join("testnet-deploy");
1176 if !path.exists() {
1177 std::fs::create_dir_all(path.clone())?;
1178 }
1179 Ok(path)
1180}