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