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 if self.retrieved_registries.is_empty() {
629 return;
630 }
631
632 Self::print_banner(&self.inventory_type.to_string());
633 for (vm_name, registry) in self.retrieved_registries.iter() {
634 println!("{vm_name}:");
635 for node in registry.nodes.iter() {
636 println!(
637 " {}: {} {}",
638 node.service_name,
639 node.version,
640 Self::format_status(&node.status)
641 );
642 }
643 }
644 if !self.failed_vms.is_empty() {
645 println!(
646 "Failed to retrieve node registries for {}:",
647 self.inventory_type
648 );
649 for vm_name in self.failed_vms.iter() {
650 println!("- {}", vm_name);
651 }
652 }
653 }
654
655 fn format_status(status: &ServiceStatus) -> String {
656 match status {
657 ServiceStatus::Running => "RUNNING".to_string(),
658 ServiceStatus::Stopped => "STOPPED".to_string(),
659 ServiceStatus::Added => "ADDED".to_string(),
660 ServiceStatus::Removed => "REMOVED".to_string(),
661 }
662 }
663
664 fn print_banner(text: &str) {
665 let padding = 2;
666 let text_width = text.len() + padding * 2;
667 let border_chars = 2;
668 let total_width = text_width + border_chars;
669 let top_bottom = "═".repeat(total_width);
670
671 println!("╔{}╗", top_bottom);
672 println!("║ {:^width$} ║", text, width = text_width);
673 println!("╚{}╝", top_bottom);
674 }
675}
676
677#[derive(Clone, Debug, Serialize, Deserialize)]
678pub struct DeploymentInventory {
679 pub binary_option: BinaryOption,
680 pub environment_details: EnvironmentDetails,
681 pub failed_node_registry_vms: Vec<String>,
682 pub faucet_address: Option<String>,
683 pub full_cone_nat_gateway_vms: Vec<VirtualMachine>,
684 pub full_cone_private_node_vms: Vec<NodeVirtualMachine>,
685 pub genesis_vm: Option<NodeVirtualMachine>,
686 pub genesis_multiaddr: Option<String>,
687 pub misc_vms: Vec<VirtualMachine>,
688 pub name: String,
689 pub node_vms: Vec<NodeVirtualMachine>,
690 pub peer_cache_node_vms: Vec<NodeVirtualMachine>,
691 pub ssh_user: String,
692 pub ssh_private_key_path: PathBuf,
693 pub symmetric_nat_gateway_vms: Vec<VirtualMachine>,
694 pub symmetric_private_node_vms: Vec<NodeVirtualMachine>,
695 pub uploaded_files: Vec<(String, String)>,
696 pub uploader_vms: Vec<UploaderVirtualMachine>,
697}
698
699impl DeploymentInventory {
700 pub fn empty(name: &str, binary_option: BinaryOption) -> DeploymentInventory {
703 Self {
704 binary_option,
705 environment_details: EnvironmentDetails::default(),
706 genesis_vm: Default::default(),
707 genesis_multiaddr: Default::default(),
708 failed_node_registry_vms: Default::default(),
709 faucet_address: Default::default(),
710 full_cone_nat_gateway_vms: Default::default(),
711 full_cone_private_node_vms: Default::default(),
712 misc_vms: Default::default(),
713 name: name.to_string(),
714 node_vms: Default::default(),
715 peer_cache_node_vms: Default::default(),
716 ssh_user: "root".to_string(),
717 ssh_private_key_path: Default::default(),
718 symmetric_nat_gateway_vms: Default::default(),
719 symmetric_private_node_vms: Default::default(),
720 uploaded_files: Default::default(),
721 uploader_vms: Default::default(),
722 }
723 }
724
725 pub fn get_tfvars_filename(&self) -> String {
726 let filename = self
727 .environment_details
728 .environment_type
729 .get_tfvars_filename(&self.name);
730 debug!("Using tfvars file {filename}",);
731 filename
732 }
733
734 pub fn is_empty(&self) -> bool {
735 self.peer_cache_node_vms.is_empty() && self.node_vms.is_empty()
736 }
737
738 pub fn vm_list(&self) -> Vec<VirtualMachine> {
739 let mut list = Vec::new();
740 list.extend(self.symmetric_nat_gateway_vms.clone());
741 list.extend(self.full_cone_nat_gateway_vms.clone());
742 list.extend(
743 self.peer_cache_node_vms
744 .iter()
745 .map(|node_vm| node_vm.vm.clone()),
746 );
747 list.extend(self.genesis_vm.iter().map(|node_vm| node_vm.vm.clone()));
748 list.extend(self.node_vms.iter().map(|node_vm| node_vm.vm.clone()));
749 list.extend(self.misc_vms.clone());
750 list.extend(
751 self.symmetric_private_node_vms
752 .iter()
753 .map(|node_vm| node_vm.vm.clone()),
754 );
755 list.extend(
756 self.full_cone_private_node_vms
757 .iter()
758 .map(|node_vm| node_vm.vm.clone()),
759 );
760 list.extend(
761 self.uploader_vms
762 .iter()
763 .map(|uploader_vm| uploader_vm.vm.clone()),
764 );
765 list
766 }
767
768 pub fn node_vm_list(&self) -> Vec<NodeVirtualMachine> {
769 let mut list = Vec::new();
770 list.extend(self.peer_cache_node_vms.iter().cloned());
771 list.extend(self.genesis_vm.iter().cloned());
772 list.extend(self.node_vms.iter().cloned());
773 list.extend(self.full_cone_private_node_vms.iter().cloned());
774 list.extend(self.symmetric_private_node_vms.iter().cloned());
775
776 list
777 }
778
779 pub fn peers(&self) -> HashSet<String> {
780 let mut list = HashSet::new();
781 list.extend(
782 self.peer_cache_node_vms
783 .iter()
784 .flat_map(|node_vm| node_vm.get_quic_addresses()),
785 );
786 list.extend(
787 self.genesis_vm
788 .iter()
789 .flat_map(|node_vm| node_vm.get_quic_addresses()),
790 );
791 list.extend(
792 self.node_vms
793 .iter()
794 .flat_map(|node_vm| node_vm.get_quic_addresses()),
795 );
796 list.extend(
797 self.full_cone_private_node_vms
798 .iter()
799 .flat_map(|node_vm| node_vm.get_quic_addresses()),
800 );
801 list.extend(
802 self.symmetric_private_node_vms
803 .iter()
804 .flat_map(|node_vm| node_vm.get_quic_addresses()),
805 );
806 list
807 }
808
809 pub fn save(&self) -> Result<()> {
810 let path = get_data_directory()?.join(format!("{}-inventory.json", self.name));
811 let serialized_data = serde_json::to_string_pretty(self)?;
812 let mut file = File::create(path)?;
813 file.write_all(serialized_data.as_bytes())?;
814 Ok(())
815 }
816
817 pub fn read(file_path: &PathBuf) -> Result<Self> {
818 let data = std::fs::read_to_string(file_path)?;
819 let deserialized_data: DeploymentInventory = serde_json::from_str(&data)?;
820 Ok(deserialized_data)
821 }
822
823 pub fn add_uploaded_files(&mut self, uploaded_files: Vec<(String, String)>) {
824 self.uploaded_files.extend_from_slice(&uploaded_files);
825 }
826
827 pub fn get_random_peer(&self) -> Option<String> {
828 let mut rng = rand::thread_rng();
829 self.peers().into_iter().choose(&mut rng)
830 }
831
832 pub fn peer_cache_node_count(&self) -> usize {
833 if let Some(first_vm) = self.peer_cache_node_vms.first() {
834 first_vm.node_count
835 } else {
836 0
837 }
838 }
839
840 pub fn genesis_node_count(&self) -> usize {
841 if let Some(genesis_vm) = &self.genesis_vm {
842 genesis_vm.node_count
843 } else {
844 0
845 }
846 }
847
848 pub fn node_count(&self) -> usize {
849 if let Some(first_vm) = self.node_vms.first() {
850 first_vm.node_count
851 } else {
852 0
853 }
854 }
855
856 pub fn full_cone_private_node_count(&self) -> usize {
857 if let Some(first_vm) = self.full_cone_private_node_vms.first() {
858 first_vm.node_count
859 } else {
860 0
861 }
862 }
863
864 pub fn symmetric_private_node_count(&self) -> usize {
865 if let Some(first_vm) = self.symmetric_private_node_vms.first() {
866 first_vm.node_count
867 } else {
868 0
869 }
870 }
871
872 pub fn print_report(&self, full: bool) -> Result<()> {
873 println!("**************************************");
874 println!("* *");
875 println!("* Inventory Report *");
876 println!("* *");
877 println!("**************************************");
878
879 println!("Environment Name: {}", self.name);
880 println!();
881 match &self.binary_option {
882 BinaryOption::BuildFromSource {
883 repo_owner, branch, ..
884 } => {
885 println!("==============");
886 println!("Branch Details");
887 println!("==============");
888 println!("Repo owner: {repo_owner}");
889 println!("Branch name: {branch}");
890 println!();
891 }
892 BinaryOption::Versioned {
893 ant_version: safe_version,
894 antnode_version: safenode_version,
895 antctl_version: safenode_manager_version,
896 } => {
897 println!("===============");
898 println!("Version Details");
899 println!("===============");
900 println!(
901 "safe version: {}",
902 safe_version
903 .as_ref()
904 .map_or("N/A".to_string(), |v| v.to_string())
905 );
906 println!("safenode version: {}", safenode_version);
907 println!("safenode-manager version: {}", safenode_manager_version);
908 println!();
909 }
910 }
911
912 if !self.peer_cache_node_vms.is_empty() {
913 println!("==============");
914 println!("Peer Cache VMs");
915 println!("==============");
916 for node_vm in self.peer_cache_node_vms.iter() {
917 println!("{}: {}", node_vm.vm.name, node_vm.vm.public_ip_addr);
918 }
919 println!("Nodes per VM: {}", self.peer_cache_node_count());
920 println!("SSH user: {}", self.ssh_user);
921 println!();
922
923 self.print_peer_cache_webserver();
924 }
925
926 println!("========");
927 println!("Node VMs");
928 println!("========");
929 if let Some(genesis_vm) = &self.genesis_vm {
930 println!("{}: {}", genesis_vm.vm.name, genesis_vm.vm.public_ip_addr);
931 }
932 for node_vm in self.node_vms.iter() {
933 println!("{}: {}", node_vm.vm.name, node_vm.vm.public_ip_addr);
934 }
935 println!("Nodes per VM: {}", self.node_count());
936 println!("SSH user: {}", self.ssh_user);
937 println!();
938
939 println!("=================");
940 println!("Full Cone Private Node VMs");
941 println!("=================");
942 let full_cone_private_node_nat_gateway_map =
943 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
944 self.full_cone_private_node_vms
945 .iter()
946 .map(|node_vm| node_vm.vm.clone())
947 .collect::<Vec<_>>()
948 .as_slice(),
949 &self.full_cone_nat_gateway_vms,
950 )?;
951
952 for (node_vm, nat_gateway_vm) in full_cone_private_node_nat_gateway_map.iter() {
953 println!(
954 "{}: {} ==routed through==> {}: {}",
955 node_vm.name,
956 node_vm.public_ip_addr,
957 nat_gateway_vm.name,
958 nat_gateway_vm.public_ip_addr
959 );
960 let ssh = if let Some(ssh_key_path) = self.ssh_private_key_path.to_str() {
961 format!(
962 "ssh -i {ssh_key_path} root@{}",
963 nat_gateway_vm.public_ip_addr,
964 )
965 } else {
966 format!("ssh root@{}", nat_gateway_vm.public_ip_addr,)
967 };
968 println!("SSH using NAT gateway: {ssh}");
969 }
970 println!("Nodes per VM: {}", self.node_count());
971 println!("SSH user: {}", self.ssh_user);
972 println!();
973
974 println!("=================");
975 println!("Symmetric Private Node VMs");
976 println!("=================");
977 let symmetric_private_node_nat_gateway_map =
978 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
979 self.symmetric_private_node_vms
980 .iter()
981 .map(|node_vm| node_vm.vm.clone())
982 .collect::<Vec<_>>()
983 .as_slice(),
984 &self.symmetric_nat_gateway_vms,
985 )?;
986
987 for (node_vm, nat_gateway_vm) in symmetric_private_node_nat_gateway_map.iter() {
988 println!(
989 "{}: {} ==routed through==> {}: {}",
990 node_vm.name,
991 node_vm.public_ip_addr,
992 nat_gateway_vm.name,
993 nat_gateway_vm.public_ip_addr
994 );
995 let ssh = if let Some(ssh_key_path) = self.ssh_private_key_path.to_str() {
996 format!(
997 "ssh -i {ssh_key_path} -o ProxyCommand=\"ssh -W %h:%p root@{} -i {ssh_key_path}\" root@{}",
998 nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
999 )
1000 } else {
1001 format!(
1002 "ssh -o ProxyCommand=\"ssh -W %h:%p root@{}\" root@{}",
1003 nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
1004 )
1005 };
1006 println!("SSH using NAT gateway: {ssh}");
1007 }
1008
1009 if !self.uploader_vms.is_empty() {
1010 println!("============");
1011 println!("Uploader VMs");
1012 println!("============");
1013 for uploader_vm in self.uploader_vms.iter() {
1014 println!("{}: {}", uploader_vm.vm.name, uploader_vm.vm.public_ip_addr);
1015 }
1016 println!();
1017
1018 println!("===========================");
1019 println!("Uploader Wallet Public Keys");
1020 println!("===========================");
1021 for uploader_vm in self.uploader_vms.iter() {
1022 for (user, key) in uploader_vm.wallet_public_key.iter() {
1023 println!("{}@{}: {}", uploader_vm.vm.name, user, key);
1024 }
1025 }
1026 }
1027
1028 if !self.misc_vms.is_empty() {
1029 println!("=========");
1030 println!("Other VMs");
1031 println!("=========");
1032 }
1033 if !self.misc_vms.is_empty() {
1034 for vm in self.misc_vms.iter() {
1035 println!("{}: {}", vm.name, vm.public_ip_addr);
1036 }
1037 }
1038
1039 for nat_gateway_vm in self.full_cone_nat_gateway_vms.iter() {
1040 println!("{}: {}", nat_gateway_vm.name, nat_gateway_vm.public_ip_addr);
1041 }
1042
1043 for nat_gateway_vm in self.symmetric_nat_gateway_vms.iter() {
1044 println!("{}: {}", nat_gateway_vm.name, nat_gateway_vm.public_ip_addr);
1045 }
1046
1047 println!("SSH user: {}", self.ssh_user);
1048 println!();
1049
1050 if full {
1051 println!("===============");
1052 println!("Full Peer List");
1053 println!("===============");
1054 let mut quic_listeners = Vec::new();
1055 let mut ws_listeners = Vec::new();
1056
1057 for node_vm in self.peer_cache_node_vms.iter().chain(self.node_vms.iter()) {
1058 for addresses in &node_vm.node_listen_addresses {
1059 for addr in addresses {
1060 if !addr.starts_with("/ip4/127.0.0.1") && !addr.starts_with("/ip4/10.") {
1061 if addr.contains("/quic") {
1062 quic_listeners.push(addr.clone());
1063 } else if addr.contains("/ws") {
1064 ws_listeners.push(addr.clone());
1065 }
1066 }
1067 }
1068 }
1069 }
1070
1071 if !quic_listeners.is_empty() {
1072 println!("QUIC:");
1073 for addr in quic_listeners {
1074 println!(" {addr}");
1075 }
1076 println!();
1077 }
1078
1079 if !ws_listeners.is_empty() {
1080 println!("Websocket:");
1081 for addr in ws_listeners {
1082 println!(" {addr}");
1083 }
1084 println!();
1085 }
1086 } else {
1087 println!("============");
1088 println!("Sample Peers");
1089 println!("============");
1090 self.peer_cache_node_vms
1091 .iter()
1092 .chain(self.node_vms.iter())
1093 .map(|node_vm| node_vm.vm.public_ip_addr.to_string())
1094 .for_each(|ip| {
1095 if let Some(peer) = self.peers().iter().find(|p| p.contains(&ip)) {
1096 println!("{peer}");
1097 }
1098 });
1099 }
1100 println!();
1101
1102 println!(
1103 "Genesis: {}",
1104 self.genesis_multiaddr
1105 .as_ref()
1106 .map_or("N/A", |genesis| genesis)
1107 );
1108 let inventory_file_path =
1109 get_data_directory()?.join(format!("{}-inventory.json", self.name));
1110 println!(
1111 "The full inventory is at {}",
1112 inventory_file_path.to_string_lossy()
1113 );
1114 println!();
1115
1116 if !self.uploaded_files.is_empty() {
1117 println!("Uploaded files:");
1118 for file in self.uploaded_files.iter() {
1119 println!("{}: {}", file.0, file.1);
1120 }
1121 }
1122
1123 if self.environment_details.evm_data_payments_address.is_some()
1124 || self.environment_details.evm_payment_token_address.is_some()
1125 || self.environment_details.evm_rpc_url.is_some()
1126 {
1127 println!("===========");
1128 println!("EVM Details");
1129 println!("===========");
1130 println!(
1131 "EVM data payments address: {}",
1132 self.environment_details
1133 .evm_data_payments_address
1134 .as_ref()
1135 .map_or("N/A", |addr| addr)
1136 );
1137 println!(
1138 "EVM payment token address: {}",
1139 self.environment_details
1140 .evm_payment_token_address
1141 .as_ref()
1142 .map_or("N/A", |addr| addr)
1143 );
1144 println!(
1145 "EVM RPC URL: {}",
1146 self.environment_details
1147 .evm_rpc_url
1148 .as_ref()
1149 .map_or("N/A", |addr| addr)
1150 );
1151 }
1152
1153 Ok(())
1154 }
1155
1156 pub fn get_genesis_ip(&self) -> Option<IpAddr> {
1157 self.misc_vms
1158 .iter()
1159 .find(|vm| vm.name.contains("genesis"))
1160 .map(|vm| vm.public_ip_addr)
1161 }
1162
1163 pub fn print_peer_cache_webserver(&self) {
1164 println!("=====================");
1165 println!("Peer Cache Webservers");
1166 println!("=====================");
1167
1168 for node_vm in &self.peer_cache_node_vms {
1169 let webserver = get_bootstrap_cache_url(&node_vm.vm.public_ip_addr);
1170 println!("{}: {webserver}", node_vm.vm.name);
1171 }
1172 }
1173}
1174
1175pub fn get_data_directory() -> Result<PathBuf> {
1176 let path = dirs_next::data_dir()
1177 .ok_or_else(|| eyre!("Could not retrieve data directory"))?
1178 .join("autonomi")
1179 .join("testnet-deploy");
1180 if !path.exists() {
1181 std::fs::create_dir_all(path.clone())?;
1182 }
1183 Ok(path)
1184}