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 clients::ClientsDeployer,
18 get_bootstrap_cache_url, get_environment_details, get_genesis_multiaddr,
19 s3::S3Repository,
20 ssh::SshClient,
21 terraform::TerraformRunner,
22 BinaryOption, CloudProvider, DeploymentType, EnvironmentDetails, EnvironmentType, Error,
23 EvmDetails, TestnetDeployer,
24};
25use alloy::hex::ToHexExt;
26use ant_service_management::{NodeRegistry, ServiceStatus};
27use color_eyre::{eyre::eyre, Result};
28use log::debug;
29use rand::seq::{IteratorRandom, SliceRandom};
30use semver::Version;
31use serde::{Deserialize, Serialize};
32use std::{
33 collections::{HashMap, HashSet},
34 convert::From,
35 fs::File,
36 io::Write,
37 net::{IpAddr, SocketAddr},
38 path::PathBuf,
39};
40
41const DEFAULT_CONTACTS_COUNT: usize = 100;
42const UNAVAILABLE_NODE: &str = "-";
43const TESTNET_BUCKET_NAME: &str = "sn-testnet";
44
45pub struct DeploymentInventoryService {
46 pub ansible_runner: AnsibleRunner,
47 pub ansible_provisioner: AnsibleProvisioner,
51 pub cloud_provider: CloudProvider,
52 pub inventory_file_path: PathBuf,
53 pub s3_repository: S3Repository,
54 pub ssh_client: SshClient,
55 pub terraform_runner: TerraformRunner,
56 pub working_directory_path: PathBuf,
57}
58
59impl From<&TestnetDeployer> for DeploymentInventoryService {
60 fn from(item: &TestnetDeployer) -> Self {
61 let provider = match item.cloud_provider {
62 CloudProvider::Aws => "aws",
63 CloudProvider::DigitalOcean => "digital_ocean",
64 };
65 DeploymentInventoryService {
66 ansible_runner: item.ansible_provisioner.ansible_runner.clone(),
67 ansible_provisioner: item.ansible_provisioner.clone(),
68 cloud_provider: item.cloud_provider,
69 inventory_file_path: item
70 .working_directory_path
71 .join("ansible")
72 .join("inventory")
73 .join(format!("dev_inventory_{}.yml", provider)),
74 s3_repository: item.s3_repository.clone(),
75 ssh_client: item.ssh_client.clone(),
76 terraform_runner: item.terraform_runner.clone(),
77 working_directory_path: item.working_directory_path.clone(),
78 }
79 }
80}
81
82impl From<&ClientsDeployer> for DeploymentInventoryService {
83 fn from(item: &ClientsDeployer) -> Self {
84 let provider = match item.cloud_provider {
85 CloudProvider::Aws => "aws",
86 CloudProvider::DigitalOcean => "digital_ocean",
87 };
88 DeploymentInventoryService {
89 ansible_runner: item.ansible_provisioner.ansible_runner.clone(),
90 ansible_provisioner: item.ansible_provisioner.clone(),
91 cloud_provider: item.cloud_provider,
92 inventory_file_path: item
93 .working_directory_path
94 .join("ansible")
95 .join("inventory")
96 .join(format!("dev_inventory_{}.yml", provider)),
97 s3_repository: item.s3_repository.clone(),
98 ssh_client: item.ssh_client.clone(),
99 terraform_runner: item.terraform_runner.clone(),
100 working_directory_path: item.working_directory_path.clone(),
101 }
102 }
103}
104
105impl DeploymentInventoryService {
106 pub async fn generate_or_retrieve_inventory(
121 &self,
122 name: &str,
123 force: bool,
124 binary_option: Option<BinaryOption>,
125 ) -> Result<DeploymentInventory> {
126 println!("======================================");
127 println!(" Generating or Retrieving Inventory ");
128 println!("======================================");
129 let inventory_path = get_data_directory()?.join(format!("{name}-inventory.json"));
130 if inventory_path.exists() && !force {
131 let inventory = DeploymentInventory::read(&inventory_path)?;
132 return Ok(inventory);
133 }
134
135 if !force {
138 let environments = self.terraform_runner.workspace_list()?;
139 if !environments.contains(&name.to_string()) {
140 return Err(eyre!("The '{}' environment does not exist", name));
141 }
142 }
143
144 let output_inventory_dir_path = self
149 .working_directory_path
150 .join("ansible")
151 .join("inventory");
152 generate_environment_inventory(
153 name,
154 &self.inventory_file_path,
155 &output_inventory_dir_path,
156 )?;
157
158 let environment_details = match get_environment_details(name, &self.s3_repository).await {
159 Ok(details) => details,
160 Err(Error::EnvironmentDetailsNotFound(_)) => {
161 println!("Environment details not found: treating this as a new deployment");
162 return Ok(DeploymentInventory::empty(
163 name,
164 binary_option.ok_or_else(|| {
165 eyre!("For a new deployment the binary option must be set")
166 })?,
167 ));
168 }
169 Err(e) => return Err(e.into()),
170 };
171
172 let genesis_vm = self
173 .ansible_runner
174 .get_inventory(AnsibleInventoryType::Genesis, false)?;
175
176 let mut misc_vms = Vec::new();
177 let build_vm = self
178 .ansible_runner
179 .get_inventory(AnsibleInventoryType::Build, false)?;
180 misc_vms.extend(build_vm);
181
182 let full_cone_nat_gateway_vms = self
183 .ansible_runner
184 .get_inventory(AnsibleInventoryType::FullConeNatGateway, false)?;
185 let full_cone_private_node_vms = self
186 .ansible_runner
187 .get_inventory(AnsibleInventoryType::FullConePrivateNodes, false)?;
188 debug!("full_cone_private_node_vms: {full_cone_private_node_vms:?}");
189 debug!("full_cone_nat_gateway_vms: {full_cone_nat_gateway_vms:?}");
190
191 let symmetric_nat_gateway_vms = self
192 .ansible_runner
193 .get_inventory(AnsibleInventoryType::SymmetricNatGateway, false)?;
194 let symmetric_private_node_vms = self
195 .ansible_runner
196 .get_inventory(AnsibleInventoryType::SymmetricPrivateNodes, false)?;
197 debug!("symmetric_private_node_vms: {symmetric_private_node_vms:?}");
198 debug!("symmetric_nat_gateway_vms: {symmetric_nat_gateway_vms:?}");
199
200 let generic_node_vms = self
201 .ansible_runner
202 .get_inventory(AnsibleInventoryType::Nodes, false)?;
203
204 generate_full_cone_private_node_static_environment_inventory(
206 name,
207 &output_inventory_dir_path,
208 &full_cone_private_node_vms,
209 &full_cone_nat_gateway_vms,
210 &self.ssh_client.private_key_path,
211 )?;
212 generate_symmetric_private_node_static_environment_inventory(
213 name,
214 &output_inventory_dir_path,
215 &symmetric_private_node_vms,
216 &symmetric_nat_gateway_vms,
217 &self.ssh_client.private_key_path,
218 )?;
219
220 if !symmetric_nat_gateway_vms.is_empty() {
222 self.ssh_client.set_symmetric_nat_routed_vms(
223 &symmetric_private_node_vms,
224 &symmetric_nat_gateway_vms,
225 )?;
226 }
227 if !full_cone_nat_gateway_vms.is_empty() {
228 self.ssh_client.set_full_cone_nat_routed_vms(
229 &full_cone_private_node_vms,
230 &full_cone_nat_gateway_vms,
231 )?;
232 }
233
234 let peer_cache_node_vms = self
235 .ansible_runner
236 .get_inventory(AnsibleInventoryType::PeerCacheNodes, false)?;
237
238 let client_inventories = self
239 .ansible_runner
240 .get_inventory(AnsibleInventoryType::Clients, true)?;
241 let client_vms = if !client_inventories.is_empty()
242 && environment_details.deployment_type != DeploymentType::Bootstrap
243 {
244 let client_and_sks = self.ansible_provisioner.get_client_secret_keys()?;
245 client_and_sks
246 .iter()
247 .map(|(vm, sks)| ClientVirtualMachine {
248 vm: vm.clone(),
249 wallet_public_key: sks
250 .iter()
251 .enumerate()
252 .map(|(user, sk)| (format!("safe{}", user + 1), sk.address().encode_hex()))
253 .collect(),
254 })
255 .collect()
256 } else {
257 Vec::new()
258 };
259
260 println!("Retrieving node registries from all VMs...");
261 let mut failed_node_registry_vms = Vec::new();
262
263 let peer_cache_node_registries = self
264 .ansible_provisioner
265 .get_node_registries(&AnsibleInventoryType::PeerCacheNodes)?;
266 let peer_cache_node_vms =
267 NodeVirtualMachine::from_list(&peer_cache_node_vms, &peer_cache_node_registries);
268
269 let generic_node_registries = self
270 .ansible_provisioner
271 .get_node_registries(&AnsibleInventoryType::Nodes)?;
272 let generic_node_vms =
273 NodeVirtualMachine::from_list(&generic_node_vms, &generic_node_registries);
274
275 let symmetric_private_node_registries = self
276 .ansible_provisioner
277 .get_node_registries(&AnsibleInventoryType::SymmetricPrivateNodes)?;
278 let symmetric_private_node_vms = NodeVirtualMachine::from_list(
279 &symmetric_private_node_vms,
280 &symmetric_private_node_registries,
281 );
282 debug!("symmetric_private_node_vms after conversion: {symmetric_private_node_vms:?}");
283
284 let full_cone_private_node_registries = self
285 .ansible_provisioner
286 .get_node_registries(&AnsibleInventoryType::FullConePrivateNodes)?;
287 debug!("full_cone_private_node_vms: {full_cone_private_node_vms:?}");
288 let full_cone_private_node_gateway_vm_map =
289 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
290 &full_cone_private_node_vms,
291 &full_cone_nat_gateway_vms,
292 )?;
293 debug!("full_cone_private_node_gateway_vm_map: {full_cone_private_node_gateway_vm_map:?}");
294 let full_cone_private_node_vms = NodeVirtualMachine::from_list(
295 &full_cone_private_node_vms,
296 &full_cone_private_node_registries,
297 );
298 debug!("full_cone_private_node_vms after conversion: {full_cone_private_node_vms:?}");
299
300 let genesis_node_registry = self
301 .ansible_provisioner
302 .get_node_registries(&AnsibleInventoryType::Genesis)?;
303 let genesis_vm = NodeVirtualMachine::from_list(&genesis_vm, &genesis_node_registry);
304 let genesis_vm = if !genesis_vm.is_empty() {
305 Some(genesis_vm[0].clone())
306 } else {
307 None
308 };
309
310 failed_node_registry_vms.extend(peer_cache_node_registries.failed_vms);
311 failed_node_registry_vms.extend(generic_node_registries.failed_vms);
312 failed_node_registry_vms.extend(full_cone_private_node_registries.failed_vms);
313 failed_node_registry_vms.extend(symmetric_private_node_registries.failed_vms);
314 failed_node_registry_vms.extend(genesis_node_registry.failed_vms);
315
316 let binary_option = if let Some(binary_option) = binary_option {
317 binary_option
318 } else {
319 let (antnode_version, antctl_version) = {
320 let mut random_vm = None;
321 if !generic_node_vms.is_empty() {
322 random_vm = generic_node_vms.first().cloned();
323 } else if !peer_cache_node_vms.is_empty() {
324 random_vm = peer_cache_node_vms.first().cloned();
325 } else if genesis_vm.is_some() {
326 random_vm = genesis_vm.clone()
327 };
328
329 let Some(random_vm) = random_vm else {
330 return Err(eyre!("Unable to obtain a VM to retrieve versions"));
331 };
332
333 let antnode_version = self.get_bin_version(
335 &random_vm.vm,
336 "/mnt/antnode-storage/data/antnode1/antnode --version",
337 "Autonomi Node v",
338 )?;
339 let antctl_version = self.get_bin_version(
340 &random_vm.vm,
341 "antctl --version",
342 "Autonomi Node Manager v",
343 )?;
344 (Some(antnode_version), Some(antctl_version))
345 };
346
347 let ant_version = if !client_vms.is_empty()
348 && environment_details.deployment_type != DeploymentType::Bootstrap
349 {
350 let random_client_vm = client_vms
351 .choose(&mut rand::thread_rng())
352 .ok_or_else(|| eyre!("No Client VMs available to retrieve ant version"))?;
353 self.get_bin_version(&random_client_vm.vm, "ant --version", "Autonomi Client v")
354 .ok()
355 } else {
356 None
357 };
358
359 println!("Retrieved binary versions from previous deployment:");
360 if let Some(version) = &antnode_version {
361 println!(" antnode: {}", version);
362 }
363 if let Some(version) = &antctl_version {
364 println!(" antctl: {}", version);
365 }
366 if let Some(version) = &ant_version {
367 println!(" ant: {}", version);
368 }
369
370 BinaryOption::Versioned {
371 ant_version,
372 antnode_version,
373 antctl_version,
374 }
375 };
376
377 let (genesis_multiaddr, genesis_ip) =
378 if environment_details.deployment_type == DeploymentType::New {
379 match get_genesis_multiaddr(&self.ansible_runner, &self.ssh_client) {
380 Ok((multiaddr, ip)) => (Some(multiaddr), Some(ip)),
381 Err(_) => (None, None),
382 }
383 } else {
384 (None, None)
385 };
386 let inventory = DeploymentInventory {
387 binary_option,
388 client_vms,
389 environment_details,
390 failed_node_registry_vms,
391 faucet_address: genesis_ip.map(|ip| format!("{ip}:8000")),
392 full_cone_nat_gateway_vms,
393 full_cone_private_node_vms,
394 genesis_multiaddr,
395 genesis_vm,
396 name: name.to_string(),
397 misc_vms,
398 node_vms: generic_node_vms,
399 peer_cache_node_vms,
400 ssh_user: self.cloud_provider.get_ssh_user(),
401 ssh_private_key_path: self.ssh_client.private_key_path.clone(),
402 symmetric_nat_gateway_vms,
403 symmetric_private_node_vms,
404 uploaded_files: Vec::new(),
405 };
406 debug!("Inventory: {inventory:?}");
407 Ok(inventory)
408 }
409
410 pub fn setup_environment_inventory(&self, name: &str) -> Result<()> {
415 let output_inventory_dir_path = self
416 .working_directory_path
417 .join("ansible")
418 .join("inventory");
419 generate_environment_inventory(
420 name,
421 &self.inventory_file_path,
422 &output_inventory_dir_path,
423 )?;
424
425 let full_cone_nat_gateway_vms = self
426 .ansible_runner
427 .get_inventory(AnsibleInventoryType::FullConeNatGateway, false)?;
428 let full_cone_private_node_vms = self
429 .ansible_runner
430 .get_inventory(AnsibleInventoryType::FullConePrivateNodes, false)?;
431
432 let symmetric_nat_gateway_vms = self
433 .ansible_runner
434 .get_inventory(AnsibleInventoryType::SymmetricNatGateway, false)?;
435 let symmetric_private_node_vms = self
436 .ansible_runner
437 .get_inventory(AnsibleInventoryType::SymmetricPrivateNodes, false)?;
438
439 generate_symmetric_private_node_static_environment_inventory(
441 name,
442 &output_inventory_dir_path,
443 &symmetric_private_node_vms,
444 &symmetric_nat_gateway_vms,
445 &self.ssh_client.private_key_path,
446 )?;
447
448 generate_full_cone_private_node_static_environment_inventory(
449 name,
450 &output_inventory_dir_path,
451 &full_cone_private_node_vms,
452 &full_cone_nat_gateway_vms,
453 &self.ssh_client.private_key_path,
454 )?;
455
456 if !full_cone_nat_gateway_vms.is_empty() {
458 self.ssh_client.set_full_cone_nat_routed_vms(
459 &full_cone_private_node_vms,
460 &full_cone_nat_gateway_vms,
461 )?;
462 }
463
464 if !symmetric_nat_gateway_vms.is_empty() {
465 self.ssh_client.set_symmetric_nat_routed_vms(
466 &symmetric_private_node_vms,
467 &symmetric_nat_gateway_vms,
468 )?;
469 }
470
471 Ok(())
472 }
473
474 pub async fn upload_network_contacts(
475 &self,
476 inventory: &DeploymentInventory,
477 contacts_file_name: Option<String>,
478 ) -> Result<()> {
479 let temp_dir_path = tempfile::tempdir()?.into_path();
480 let temp_file_path = if let Some(file_name) = contacts_file_name {
481 temp_dir_path.join(file_name)
482 } else {
483 temp_dir_path.join(inventory.name.clone())
484 };
485
486 let mut file = std::fs::File::create(&temp_file_path)?;
487 let mut rng = rand::thread_rng();
488
489 let peer_cache_peers = inventory
490 .peer_cache_node_vms
491 .iter()
492 .flat_map(|vm| vm.get_quic_addresses())
493 .collect::<Vec<_>>();
494 let peer_cache_peers_len = peer_cache_peers.len();
495 for peer in peer_cache_peers
496 .iter()
497 .filter(|&peer| peer != UNAVAILABLE_NODE)
498 .cloned()
499 .choose_multiple(&mut rng, DEFAULT_CONTACTS_COUNT)
500 {
501 writeln!(file, "{peer}",)?;
502 }
503
504 if DEFAULT_CONTACTS_COUNT > peer_cache_peers_len {
505 let node_peers = inventory
506 .node_vms
507 .iter()
508 .flat_map(|vm| vm.get_quic_addresses())
509 .collect::<Vec<_>>();
510 for peer in node_peers
511 .iter()
512 .filter(|&peer| peer != UNAVAILABLE_NODE)
513 .cloned()
514 .choose_multiple(&mut rng, DEFAULT_CONTACTS_COUNT - peer_cache_peers_len)
515 {
516 writeln!(file, "{peer}",)?;
517 }
518 }
519
520 self.s3_repository
521 .upload_file(TESTNET_BUCKET_NAME, &temp_file_path, true)
522 .await?;
523
524 Ok(())
525 }
526
527 fn get_bin_version(&self, vm: &VirtualMachine, command: &str, prefix: &str) -> Result<Version> {
529 let output = self.ssh_client.run_command(
530 &vm.public_ip_addr,
531 &self.cloud_provider.get_ssh_user(),
532 command,
533 true,
534 )?;
535 let version_line = output
536 .first()
537 .ok_or_else(|| eyre!("No output from {} command", command))?;
538 let version_str = version_line
539 .strip_prefix(prefix)
540 .ok_or_else(|| eyre!("Unexpected output format from {} command", command))?;
541 Version::parse(version_str).map_err(|e| eyre!("Failed to parse {} version: {}", command, e))
542 }
543
544 pub async fn generate_or_retrieve_client_inventory(
553 &self,
554 name: &str,
555 region: &str,
556 force: bool,
557 binary_option: Option<BinaryOption>,
558 ) -> Result<ClientsDeploymentInventory> {
559 println!("===============================================");
560 println!(" Generating or Retrieving Client Inventory ");
561 println!("===============================================");
562 let inventory_path = get_data_directory()?.join(format!("{name}-clients-inventory.json"));
563 if inventory_path.exists() && !force {
564 let inventory = ClientsDeploymentInventory::read(&inventory_path)?;
565 return Ok(inventory);
566 }
567
568 if !force {
571 let environments = self.terraform_runner.workspace_list()?;
572 if !environments.contains(&name.to_string()) {
573 return Err(eyre!("The '{}' environment does not exist", name));
574 }
575 }
576
577 let output_inventory_dir_path = self
582 .working_directory_path
583 .join("ansible")
584 .join("inventory");
585 generate_environment_inventory(
586 name,
587 &self.inventory_file_path,
588 &output_inventory_dir_path,
589 )?;
590
591 let environment_details = match get_environment_details(name, &self.s3_repository).await {
592 Ok(details) => details,
593 Err(Error::EnvironmentDetailsNotFound(_)) => {
594 println!("Environment details not found: treating this as a new deployment");
595 return Ok(ClientsDeploymentInventory::empty(
596 name,
597 binary_option.ok_or_else(|| {
598 eyre!("For a new deployment the binary option must be set")
599 })?,
600 region,
601 ));
602 }
603 Err(e) => return Err(e.into()),
604 };
605
606 let client_and_sks = self.ansible_provisioner.get_client_secret_keys()?;
607 let client_vms: Vec<ClientVirtualMachine> = client_and_sks
608 .iter()
609 .map(|(vm, sks)| ClientVirtualMachine {
610 vm: vm.clone(),
611 wallet_public_key: sks
612 .iter()
613 .enumerate()
614 .map(|(user, sk)| (format!("safe{}", user + 1), sk.address().encode_hex()))
615 .collect(),
616 })
617 .collect();
618
619 let binary_option = if let Some(binary_option) = binary_option {
620 binary_option
621 } else {
622 let ant_version = if !client_vms.is_empty() {
623 let random_client_vm = client_vms
624 .choose(&mut rand::thread_rng())
625 .ok_or_else(|| eyre!("No Client VMs available to retrieve ant version"))?;
626 self.get_bin_version(&random_client_vm.vm, "ant --version", "Autonomi Client v")
627 .ok()
628 } else {
629 None
630 };
631
632 println!("Retrieved binary versions from previous deployment:");
633 if let Some(version) = &ant_version {
634 println!(" ant: {}", version);
635 }
636
637 BinaryOption::Versioned {
638 ant_version,
639 antnode_version: None,
640 antctl_version: None,
641 }
642 };
643
644 let inventory = ClientsDeploymentInventory {
645 binary_option,
646 client_vms,
647 environment_type: environment_details.environment_type,
648 evm_details: environment_details.evm_details,
649 funding_wallet_address: None, network_id: environment_details.network_id,
651 failed_node_registry_vms: Vec::new(),
652 name: name.to_string(),
653 region: environment_details.region,
654 ssh_user: self.cloud_provider.get_ssh_user(),
655 ssh_private_key_path: self.ssh_client.private_key_path.clone(),
656 uploaded_files: Vec::new(),
657 };
658
659 debug!("Client Inventory: {inventory:?}");
660 Ok(inventory)
661 }
662}
663
664impl NodeVirtualMachine {
665 pub fn from_list(
666 vms: &[VirtualMachine],
667 node_registries: &DeploymentNodeRegistries,
668 ) -> Vec<Self> {
669 let mut node_vms = Vec::new();
670 for vm in vms {
671 let node_registry = node_registries
672 .retrieved_registries
673 .iter()
674 .find(|(name, _)| {
675 if vm.name.contains("private") {
676 let result = name == &vm.private_ip_addr.to_string();
677 debug!(
678 "Vm name: {name} is a private node with result {result}. Vm: {vm:?}"
679 );
680 result
681 } else {
682 name == &vm.name
683 }
684 })
685 .map(|(_, reg)| reg);
686
687 let node_vm = Self {
690 node_count: node_registry.map_or(0, |reg| reg.nodes.len()),
691 node_listen_addresses: node_registry.map_or_else(Vec::new, |reg| {
692 if reg.nodes.is_empty() {
693 Vec::new()
694 } else {
695 reg.nodes
696 .iter()
697 .map(|node| {
698 node.listen_addr
699 .as_ref()
700 .map(|addrs| {
701 addrs.iter().map(|addr| addr.to_string()).collect()
702 })
703 .unwrap_or_default()
704 })
705 .collect()
706 }
707 }),
708 rpc_endpoint: node_registry.map_or_else(HashMap::new, |reg| {
709 reg.nodes
710 .iter()
711 .filter_map(|node| {
712 node.peer_id
713 .map(|peer_id| (peer_id.to_string(), node.rpc_socket_addr))
714 })
715 .collect()
716 }),
717 safenodemand_endpoint: node_registry
718 .and_then(|reg| reg.daemon.as_ref())
719 .and_then(|daemon| daemon.endpoint),
720 vm: vm.clone(),
721 };
722 node_vms.push(node_vm.clone());
723 debug!("Added node VM: {node_vm:?}");
724 }
725 debug!("Node VMs generated from NodeRegistries: {node_vms:?}");
726 node_vms
727 }
728
729 pub fn get_quic_addresses(&self) -> Vec<String> {
730 self.node_listen_addresses
731 .iter()
732 .map(|addresses| {
733 addresses
734 .iter()
735 .find(|addr| {
736 addr.contains("/quic-v1")
737 && !addr.starts_with("/ip4/127.0.0.1")
738 && !addr.starts_with("/ip4/10.")
739 })
740 .map(|s| s.to_string())
741 .unwrap_or_else(|| UNAVAILABLE_NODE.to_string())
742 })
743 .collect()
744 }
745}
746
747pub type OsUser = String;
749
750#[derive(Clone, Debug, Serialize, Deserialize)]
751pub struct ClientVirtualMachine {
752 pub vm: VirtualMachine,
753 pub wallet_public_key: HashMap<OsUser, String>,
755}
756
757#[derive(Clone, Debug, Serialize, Deserialize)]
758pub struct NodeVirtualMachine {
759 pub vm: VirtualMachine,
760 pub node_count: usize,
761 pub node_listen_addresses: Vec<Vec<String>>,
762 pub rpc_endpoint: HashMap<String, SocketAddr>,
763 pub safenodemand_endpoint: Option<SocketAddr>,
764}
765
766#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
767pub struct VirtualMachine {
768 pub id: u64,
769 pub name: String,
770 pub public_ip_addr: IpAddr,
771 pub private_ip_addr: IpAddr,
772}
773
774#[derive(Clone)]
775pub struct DeploymentNodeRegistries {
776 pub inventory_type: AnsibleInventoryType,
777 pub retrieved_registries: Vec<(String, NodeRegistry)>,
780 pub failed_vms: Vec<String>,
781}
782
783impl DeploymentNodeRegistries {
784 pub fn print(&self) {
785 if self.retrieved_registries.is_empty() {
786 return;
787 }
788
789 Self::print_banner(&self.inventory_type.to_string());
790 for (vm_name, registry) in self.retrieved_registries.iter() {
791 println!("{vm_name}:");
792 for node in registry.nodes.iter() {
793 println!(
794 " {}: {} {}",
795 node.service_name,
796 node.version,
797 Self::format_status(&node.status)
798 );
799 }
800 }
801 if !self.failed_vms.is_empty() {
802 println!(
803 "Failed to retrieve node registries for {}:",
804 self.inventory_type
805 );
806 for vm_name in self.failed_vms.iter() {
807 println!("- {}", vm_name);
808 }
809 }
810 }
811
812 fn format_status(status: &ServiceStatus) -> String {
813 match status {
814 ServiceStatus::Running => "RUNNING".to_string(),
815 ServiceStatus::Stopped => "STOPPED".to_string(),
816 ServiceStatus::Added => "ADDED".to_string(),
817 ServiceStatus::Removed => "REMOVED".to_string(),
818 }
819 }
820
821 fn print_banner(text: &str) {
822 let padding = 2;
823 let text_width = text.len() + padding * 2;
824 let border_chars = 2;
825 let total_width = text_width + border_chars;
826 let top_bottom = "═".repeat(total_width);
827
828 println!("╔{}╗", top_bottom);
829 println!("║ {:^width$} ║", text, width = text_width);
830 println!("╚{}╝", top_bottom);
831 }
832}
833
834#[derive(Clone, Debug, Serialize, Deserialize)]
835pub struct DeploymentInventory {
836 pub binary_option: BinaryOption,
837 pub client_vms: Vec<ClientVirtualMachine>,
838 pub environment_details: EnvironmentDetails,
839 pub failed_node_registry_vms: Vec<String>,
840 pub faucet_address: Option<String>,
841 pub full_cone_nat_gateway_vms: Vec<VirtualMachine>,
842 pub full_cone_private_node_vms: Vec<NodeVirtualMachine>,
843 pub genesis_vm: Option<NodeVirtualMachine>,
844 pub genesis_multiaddr: Option<String>,
845 pub misc_vms: Vec<VirtualMachine>,
846 pub name: String,
847 pub node_vms: Vec<NodeVirtualMachine>,
848 pub peer_cache_node_vms: Vec<NodeVirtualMachine>,
849 pub ssh_user: String,
850 pub ssh_private_key_path: PathBuf,
851 pub symmetric_nat_gateway_vms: Vec<VirtualMachine>,
852 pub symmetric_private_node_vms: Vec<NodeVirtualMachine>,
853 pub uploaded_files: Vec<(String, String)>,
854}
855
856impl DeploymentInventory {
857 pub fn empty(name: &str, binary_option: BinaryOption) -> DeploymentInventory {
860 Self {
861 binary_option,
862 client_vms: Default::default(),
863 environment_details: EnvironmentDetails::default(),
864 genesis_vm: Default::default(),
865 genesis_multiaddr: Default::default(),
866 failed_node_registry_vms: Default::default(),
867 faucet_address: Default::default(),
868 full_cone_nat_gateway_vms: Default::default(),
869 full_cone_private_node_vms: Default::default(),
870 misc_vms: Default::default(),
871 name: name.to_string(),
872 node_vms: Default::default(),
873 peer_cache_node_vms: Default::default(),
874 ssh_user: "root".to_string(),
875 ssh_private_key_path: Default::default(),
876 symmetric_nat_gateway_vms: Default::default(),
877 symmetric_private_node_vms: Default::default(),
878 uploaded_files: Default::default(),
879 }
880 }
881
882 pub fn get_tfvars_filenames(&self) -> Vec<String> {
883 let filenames = self
884 .environment_details
885 .environment_type
886 .get_tfvars_filenames(&self.name, &self.environment_details.region);
887 debug!("Using tfvars files {filenames:?}");
888 filenames
889 }
890
891 pub fn is_empty(&self) -> bool {
892 self.peer_cache_node_vms.is_empty() && self.node_vms.is_empty()
893 }
894
895 pub fn vm_list(&self) -> Vec<VirtualMachine> {
896 let mut list = Vec::new();
897 list.extend(self.symmetric_nat_gateway_vms.clone());
898 list.extend(self.full_cone_nat_gateway_vms.clone());
899 list.extend(
900 self.peer_cache_node_vms
901 .iter()
902 .map(|node_vm| node_vm.vm.clone()),
903 );
904 list.extend(self.genesis_vm.iter().map(|node_vm| node_vm.vm.clone()));
905 list.extend(self.node_vms.iter().map(|node_vm| node_vm.vm.clone()));
906 list.extend(self.misc_vms.clone());
907 list.extend(
908 self.symmetric_private_node_vms
909 .iter()
910 .map(|node_vm| node_vm.vm.clone()),
911 );
912 list.extend(
913 self.full_cone_private_node_vms
914 .iter()
915 .map(|node_vm| node_vm.vm.clone()),
916 );
917 list.extend(self.client_vms.iter().map(|client_vm| client_vm.vm.clone()));
918 list
919 }
920
921 pub fn node_vm_list(&self) -> Vec<NodeVirtualMachine> {
922 let mut list = Vec::new();
923 list.extend(self.peer_cache_node_vms.iter().cloned());
924 list.extend(self.genesis_vm.iter().cloned());
925 list.extend(self.node_vms.iter().cloned());
926 list.extend(self.full_cone_private_node_vms.iter().cloned());
927 list.extend(self.symmetric_private_node_vms.iter().cloned());
928
929 list
930 }
931
932 pub fn peers(&self) -> HashSet<String> {
933 let mut list = HashSet::new();
934 list.extend(
935 self.peer_cache_node_vms
936 .iter()
937 .flat_map(|node_vm| node_vm.get_quic_addresses()),
938 );
939 list.extend(
940 self.genesis_vm
941 .iter()
942 .flat_map(|node_vm| node_vm.get_quic_addresses()),
943 );
944 list.extend(
945 self.node_vms
946 .iter()
947 .flat_map(|node_vm| node_vm.get_quic_addresses()),
948 );
949 list.extend(
950 self.full_cone_private_node_vms
951 .iter()
952 .flat_map(|node_vm| node_vm.get_quic_addresses()),
953 );
954 list.extend(
955 self.symmetric_private_node_vms
956 .iter()
957 .flat_map(|node_vm| node_vm.get_quic_addresses()),
958 );
959 list
960 }
961
962 pub fn save(&self) -> Result<()> {
963 let path = get_data_directory()?.join(format!("{}-inventory.json", self.name));
964 let serialized_data = serde_json::to_string_pretty(self)?;
965 let mut file = File::create(path)?;
966 file.write_all(serialized_data.as_bytes())?;
967 Ok(())
968 }
969
970 pub fn read(file_path: &PathBuf) -> Result<Self> {
971 let data = std::fs::read_to_string(file_path)?;
972 let deserialized_data: DeploymentInventory = serde_json::from_str(&data)?;
973 Ok(deserialized_data)
974 }
975
976 pub fn add_uploaded_files(&mut self, uploaded_files: Vec<(String, String)>) {
977 self.uploaded_files.extend_from_slice(&uploaded_files);
978 }
979
980 pub fn get_random_peer(&self) -> Option<String> {
981 let mut rng = rand::thread_rng();
982 self.peers().into_iter().choose(&mut rng)
983 }
984
985 pub fn peer_cache_node_count(&self) -> usize {
986 if let Some(first_vm) = self.peer_cache_node_vms.first() {
987 first_vm.node_count
988 } else {
989 0
990 }
991 }
992
993 pub fn genesis_node_count(&self) -> usize {
994 if let Some(genesis_vm) = &self.genesis_vm {
995 genesis_vm.node_count
996 } else {
997 0
998 }
999 }
1000
1001 pub fn node_count(&self) -> usize {
1002 if let Some(first_vm) = self.node_vms.first() {
1003 first_vm.node_count
1004 } else {
1005 0
1006 }
1007 }
1008
1009 pub fn full_cone_private_node_count(&self) -> usize {
1010 if let Some(first_vm) = self.full_cone_private_node_vms.first() {
1011 first_vm.node_count
1012 } else {
1013 0
1014 }
1015 }
1016
1017 pub fn symmetric_private_node_count(&self) -> usize {
1018 if let Some(first_vm) = self.symmetric_private_node_vms.first() {
1019 first_vm.node_count
1020 } else {
1021 0
1022 }
1023 }
1024
1025 pub fn print_report(&self, full: bool) -> Result<()> {
1026 println!("**************************************");
1027 println!("* *");
1028 println!("* Inventory Report *");
1029 println!("* *");
1030 println!("**************************************");
1031
1032 println!("Environment Name: {}", self.name);
1033 println!();
1034 match &self.binary_option {
1035 BinaryOption::BuildFromSource {
1036 repo_owner, branch, ..
1037 } => {
1038 println!("==============");
1039 println!("Branch Details");
1040 println!("==============");
1041 println!("Repo owner: {repo_owner}");
1042 println!("Branch name: {branch}");
1043 println!();
1044 }
1045 BinaryOption::Versioned {
1046 ant_version,
1047 antnode_version,
1048 antctl_version,
1049 } => {
1050 println!("===============");
1051 println!("Version Details");
1052 println!("===============");
1053 println!(
1054 "ant version: {}",
1055 ant_version
1056 .as_ref()
1057 .map_or("N/A".to_string(), |v| v.to_string())
1058 );
1059 println!(
1060 "antnode version: {}",
1061 antnode_version
1062 .as_ref()
1063 .map_or("N/A".to_string(), |v| v.to_string())
1064 );
1065 println!(
1066 "antctl version: {}",
1067 antctl_version
1068 .as_ref()
1069 .map_or("N/A".to_string(), |v| v.to_string())
1070 );
1071 println!();
1072 }
1073 }
1074
1075 if !self.peer_cache_node_vms.is_empty() {
1076 println!("==============");
1077 println!("Peer Cache VMs");
1078 println!("==============");
1079 for node_vm in self.peer_cache_node_vms.iter() {
1080 println!("{}: {}", node_vm.vm.name, node_vm.vm.public_ip_addr);
1081 }
1082 println!("Nodes per VM: {}", self.peer_cache_node_count());
1083 println!("SSH user: {}", self.ssh_user);
1084 println!();
1085
1086 self.print_peer_cache_webserver();
1087 }
1088
1089 println!("========");
1090 println!("Node VMs");
1091 println!("========");
1092 if let Some(genesis_vm) = &self.genesis_vm {
1093 println!("{}: {}", genesis_vm.vm.name, genesis_vm.vm.public_ip_addr);
1094 }
1095 for node_vm in self.node_vms.iter() {
1096 println!("{}: {}", node_vm.vm.name, node_vm.vm.public_ip_addr);
1097 }
1098 println!("Nodes per VM: {}", self.node_count());
1099 println!("SSH user: {}", self.ssh_user);
1100 println!();
1101
1102 if !self.full_cone_private_node_vms.is_empty() {
1103 println!("=================");
1104 println!("Full Cone Private Node VMs");
1105 println!("=================");
1106 let full_cone_private_node_nat_gateway_map =
1107 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
1108 self.full_cone_private_node_vms
1109 .iter()
1110 .map(|node_vm| node_vm.vm.clone())
1111 .collect::<Vec<_>>()
1112 .as_slice(),
1113 &self.full_cone_nat_gateway_vms,
1114 )?;
1115
1116 for (node_vm, nat_gateway_vm) in full_cone_private_node_nat_gateway_map.iter() {
1117 println!(
1118 "{}: {} ==routed through==> {}: {}",
1119 node_vm.name,
1120 node_vm.public_ip_addr,
1121 nat_gateway_vm.name,
1122 nat_gateway_vm.public_ip_addr
1123 );
1124 let ssh = if let Some(ssh_key_path) = self.ssh_private_key_path.to_str() {
1125 format!(
1126 "ssh -i {ssh_key_path} root@{}",
1127 nat_gateway_vm.public_ip_addr,
1128 )
1129 } else {
1130 format!("ssh root@{}", nat_gateway_vm.public_ip_addr,)
1131 };
1132 println!("SSH using NAT gateway: {ssh}");
1133 }
1134 println!("Nodes per VM: {}", self.full_cone_private_node_count());
1135 println!("SSH user: {}", self.ssh_user);
1136 println!();
1137 }
1138
1139 if !self.symmetric_private_node_vms.is_empty() {
1140 println!("=================");
1141 println!("Symmetric Private Node VMs");
1142 println!("=================");
1143 let symmetric_private_node_nat_gateway_map =
1144 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
1145 self.symmetric_private_node_vms
1146 .iter()
1147 .map(|node_vm| node_vm.vm.clone())
1148 .collect::<Vec<_>>()
1149 .as_slice(),
1150 &self.symmetric_nat_gateway_vms,
1151 )?;
1152
1153 for (node_vm, nat_gateway_vm) in symmetric_private_node_nat_gateway_map.iter() {
1154 println!(
1155 "{}: {} ==routed through==> {}: {}",
1156 node_vm.name,
1157 node_vm.public_ip_addr,
1158 nat_gateway_vm.name,
1159 nat_gateway_vm.public_ip_addr
1160 );
1161 let ssh = if let Some(ssh_key_path) = self.ssh_private_key_path.to_str() {
1162 format!(
1163 "ssh -i {ssh_key_path} -o ProxyCommand=\"ssh -W %h:%p root@{} -i {ssh_key_path}\" root@{}",
1164 nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
1165 )
1166 } else {
1167 format!(
1168 "ssh -o ProxyCommand=\"ssh -W %h:%p root@{}\" root@{}",
1169 nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
1170 )
1171 };
1172 println!("SSH using NAT gateway: {ssh}");
1173 }
1174 println!("Nodes per VM: {}", self.symmetric_private_node_count());
1175 println!("SSH user: {}", self.ssh_user);
1176 println!();
1177 }
1178
1179 if !self.client_vms.is_empty() {
1180 println!("==========");
1181 println!("Client VMs");
1182 println!("==========");
1183 for client_vm in self.client_vms.iter() {
1184 println!("{}: {}", client_vm.vm.name, client_vm.vm.public_ip_addr);
1185 }
1186 println!();
1187
1188 println!("=============================");
1189 println!("Ant Client Wallet Public Keys");
1190 println!("=============================");
1191 for client_vm in self.client_vms.iter() {
1192 for (user, key) in client_vm.wallet_public_key.iter() {
1193 println!("{}@{}: {}", client_vm.vm.name, user, key);
1194 }
1195 }
1196 }
1197
1198 if !self.misc_vms.is_empty() {
1199 println!("=========");
1200 println!("Other VMs");
1201 println!("=========");
1202 }
1203 if !self.misc_vms.is_empty() {
1204 for vm in self.misc_vms.iter() {
1205 println!("{}: {}", vm.name, vm.public_ip_addr);
1206 }
1207 }
1208
1209 for nat_gateway_vm in self.full_cone_nat_gateway_vms.iter() {
1210 println!("{}: {}", nat_gateway_vm.name, nat_gateway_vm.public_ip_addr);
1211 }
1212
1213 for nat_gateway_vm in self.symmetric_nat_gateway_vms.iter() {
1214 println!("{}: {}", nat_gateway_vm.name, nat_gateway_vm.public_ip_addr);
1215 }
1216
1217 println!("SSH user: {}", self.ssh_user);
1218 println!();
1219
1220 if full {
1221 println!("===============");
1222 println!("Full Peer List");
1223 println!("===============");
1224 let mut quic_listeners = Vec::new();
1225 let mut ws_listeners = Vec::new();
1226
1227 for node_vm in self.peer_cache_node_vms.iter().chain(self.node_vms.iter()) {
1228 for addresses in &node_vm.node_listen_addresses {
1229 for addr in addresses {
1230 if !addr.starts_with("/ip4/127.0.0.1") && !addr.starts_with("/ip4/10.") {
1231 if addr.contains("/quic") {
1232 quic_listeners.push(addr.clone());
1233 } else if addr.contains("/ws") {
1234 ws_listeners.push(addr.clone());
1235 }
1236 }
1237 }
1238 }
1239 }
1240
1241 if !quic_listeners.is_empty() {
1242 println!("QUIC:");
1243 for addr in quic_listeners {
1244 println!(" {addr}");
1245 }
1246 println!();
1247 }
1248
1249 if !ws_listeners.is_empty() {
1250 println!("Websocket:");
1251 for addr in ws_listeners {
1252 println!(" {addr}");
1253 }
1254 println!();
1255 }
1256 } else {
1257 println!("============");
1258 println!("Sample Peers");
1259 println!("============");
1260 self.peer_cache_node_vms
1261 .iter()
1262 .chain(self.node_vms.iter())
1263 .map(|node_vm| node_vm.vm.public_ip_addr.to_string())
1264 .for_each(|ip| {
1265 if let Some(peer) = self.peers().iter().find(|p| p.contains(&ip)) {
1266 println!("{peer}");
1267 }
1268 });
1269 }
1270 println!();
1271
1272 println!(
1273 "Genesis: {}",
1274 self.genesis_multiaddr
1275 .as_ref()
1276 .map_or("N/A", |genesis| genesis)
1277 );
1278 let inventory_file_path =
1279 get_data_directory()?.join(format!("{}-inventory.json", self.name));
1280 println!(
1281 "The full inventory is at {}",
1282 inventory_file_path.to_string_lossy()
1283 );
1284 println!();
1285
1286 if !self.uploaded_files.is_empty() {
1287 println!("Uploaded files:");
1288 for file in self.uploaded_files.iter() {
1289 println!("{}: {}", file.0, file.1);
1290 }
1291 }
1292
1293 if self
1294 .environment_details
1295 .evm_details
1296 .data_payments_address
1297 .is_some()
1298 || self
1299 .environment_details
1300 .evm_details
1301 .payment_token_address
1302 .is_some()
1303 || self.environment_details.evm_details.rpc_url.is_some()
1304 {
1305 println!("===========");
1306 println!("EVM Details");
1307 println!("===========");
1308 println!(
1309 "EVM data payments address: {}",
1310 self.environment_details
1311 .evm_details
1312 .data_payments_address
1313 .as_ref()
1314 .map_or("N/A", |addr| addr)
1315 );
1316 println!(
1317 "EVM payment token address: {}",
1318 self.environment_details
1319 .evm_details
1320 .payment_token_address
1321 .as_ref()
1322 .map_or("N/A", |addr| addr)
1323 );
1324 println!(
1325 "EVM RPC URL: {}",
1326 self.environment_details
1327 .evm_details
1328 .rpc_url
1329 .as_ref()
1330 .map_or("N/A", |addr| addr)
1331 );
1332 }
1333
1334 Ok(())
1335 }
1336
1337 pub fn get_genesis_ip(&self) -> Option<IpAddr> {
1338 self.misc_vms
1339 .iter()
1340 .find(|vm| vm.name.contains("genesis"))
1341 .map(|vm| vm.public_ip_addr)
1342 }
1343
1344 pub fn print_peer_cache_webserver(&self) {
1345 println!("=====================");
1346 println!("Peer Cache Webservers");
1347 println!("=====================");
1348
1349 for node_vm in &self.peer_cache_node_vms {
1350 let webserver = get_bootstrap_cache_url(&node_vm.vm.public_ip_addr);
1351 println!("{}: {webserver}", node_vm.vm.name);
1352 }
1353 }
1354}
1355
1356#[derive(Clone, Debug, Serialize, Deserialize)]
1357pub struct ClientsDeploymentInventory {
1358 pub binary_option: BinaryOption,
1359 pub client_vms: Vec<ClientVirtualMachine>,
1360 pub environment_type: EnvironmentType,
1361 pub evm_details: EvmDetails,
1362 pub funding_wallet_address: Option<String>,
1363 pub network_id: Option<u8>,
1364 pub failed_node_registry_vms: Vec<String>,
1365 pub name: String,
1366 pub region: String,
1367 pub ssh_user: String,
1368 pub ssh_private_key_path: PathBuf,
1369 pub uploaded_files: Vec<(String, String)>,
1370}
1371
1372impl ClientsDeploymentInventory {
1373 pub fn empty(
1376 name: &str,
1377 binary_option: BinaryOption,
1378 region: &str,
1379 ) -> ClientsDeploymentInventory {
1380 Self {
1381 binary_option,
1382 client_vms: Default::default(),
1383 environment_type: EnvironmentType::default(),
1384 evm_details: EvmDetails::default(),
1385 funding_wallet_address: None,
1386 network_id: None,
1387 failed_node_registry_vms: Default::default(),
1388 name: name.to_string(),
1389 region: region.to_string(),
1390 ssh_user: "root".to_string(),
1391 ssh_private_key_path: Default::default(),
1392 uploaded_files: Default::default(),
1393 }
1394 }
1395
1396 pub fn get_tfvars_filenames(&self) -> Vec<String> {
1397 debug!("Environment type: {:?}", self.environment_type);
1398 let filenames = self
1399 .environment_type
1400 .get_tfvars_filenames(&self.name, &self.region);
1401 debug!("Using tfvars files {filenames:?}");
1402 filenames
1403 }
1404
1405 pub fn is_empty(&self) -> bool {
1406 self.client_vms.is_empty()
1407 }
1408
1409 pub fn vm_list(&self) -> Vec<VirtualMachine> {
1410 self.client_vms
1411 .iter()
1412 .map(|client_vm| client_vm.vm.clone())
1413 .collect()
1414 }
1415
1416 pub fn save(&self) -> Result<()> {
1417 let path = get_data_directory()?.join(format!("{}-clients-inventory.json", self.name));
1418 let serialized_data = serde_json::to_string_pretty(self)?;
1419 let mut file = File::create(path)?;
1420 file.write_all(serialized_data.as_bytes())?;
1421 Ok(())
1422 }
1423
1424 pub fn read(file_path: &PathBuf) -> Result<Self> {
1425 let data = std::fs::read_to_string(file_path)?;
1426 let deserialized_data: ClientsDeploymentInventory = serde_json::from_str(&data)?;
1427 Ok(deserialized_data)
1428 }
1429
1430 pub fn add_uploaded_files(&mut self, uploaded_files: Vec<(String, String)>) {
1431 self.uploaded_files.extend_from_slice(&uploaded_files);
1432 }
1433
1434 pub fn print_report(&self) -> Result<()> {
1435 println!("*************************************");
1436 println!("* *");
1437 println!("* Clients Inventory Report *");
1438 println!("* *");
1439 println!("*************************************");
1440
1441 println!("Environment Name: {}", self.name);
1442 println!();
1443 match &self.binary_option {
1444 BinaryOption::BuildFromSource {
1445 repo_owner, branch, ..
1446 } => {
1447 println!("==============");
1448 println!("Branch Details");
1449 println!("==============");
1450 println!("Repo owner: {repo_owner}");
1451 println!("Branch name: {branch}");
1452 println!();
1453 }
1454 BinaryOption::Versioned { ant_version, .. } => {
1455 println!("===============");
1456 println!("Version Details");
1457 println!("===============");
1458 println!(
1459 "ant version: {}",
1460 ant_version
1461 .as_ref()
1462 .map_or("N/A".to_string(), |v| v.to_string())
1463 );
1464 println!();
1465 }
1466 }
1467
1468 if !self.client_vms.is_empty() {
1469 println!("==========");
1470 println!("Client VMs");
1471 println!("==========");
1472 for client_vm in self.client_vms.iter() {
1473 println!("{}: {}", client_vm.vm.name, client_vm.vm.public_ip_addr);
1474 }
1475 println!("SSH user: {}", self.ssh_user);
1476 println!();
1477
1478 println!("=============================");
1479 println!("Ant Client Wallet Public Keys");
1480 println!("=============================");
1481 for client_vm in self.client_vms.iter() {
1482 for (user, key) in client_vm.wallet_public_key.iter() {
1483 println!("{}@{}: {}", client_vm.vm.name, user, key);
1484 }
1485 }
1486 println!();
1487 }
1488
1489 if !self.uploaded_files.is_empty() {
1490 println!("==============");
1491 println!("Uploaded files");
1492 println!("==============");
1493 for file in self.uploaded_files.iter() {
1494 println!("{}: {}", file.0, file.1);
1495 }
1496 println!();
1497 }
1498
1499 if self.evm_details.data_payments_address.is_some()
1500 || self.evm_details.payment_token_address.is_some()
1501 || self.evm_details.rpc_url.is_some()
1502 {
1503 println!("===========");
1504 println!("EVM Details");
1505 println!("===========");
1506 println!(
1507 "EVM data payments address: {}",
1508 self.evm_details
1509 .data_payments_address
1510 .as_ref()
1511 .map_or("N/A", |addr| addr)
1512 );
1513 println!(
1514 "EVM payment token address: {}",
1515 self.evm_details
1516 .payment_token_address
1517 .as_ref()
1518 .map_or("N/A", |addr| addr)
1519 );
1520 println!(
1521 "EVM RPC URL: {}",
1522 self.evm_details.rpc_url.as_ref().map_or("N/A", |addr| addr)
1523 );
1524 println!();
1525 }
1526
1527 if let Some(funding_wallet_address) = &self.funding_wallet_address {
1528 println!("======================");
1529 println!("Funding Wallet Address");
1530 println!("======================");
1531 println!("{}", funding_wallet_address);
1532 println!();
1533 }
1534
1535 if let Some(network_id) = &self.network_id {
1536 println!("==========");
1537 println!("Network ID");
1538 println!("==========");
1539 println!("{}", network_id);
1540 println!();
1541 }
1542
1543 let inventory_file_path =
1544 get_data_directory()?.join(format!("{}-clients-inventory.json", self.name));
1545 println!(
1546 "The full Clients inventory is at {}",
1547 inventory_file_path.to_string_lossy()
1548 );
1549 println!();
1550
1551 Ok(())
1552 }
1553}
1554
1555pub fn get_data_directory() -> Result<PathBuf> {
1556 let path = dirs_next::data_dir()
1557 .ok_or_else(|| eyre!("Could not retrieve data directory"))?
1558 .join("autonomi")
1559 .join("testnet-deploy");
1560 if !path.exists() {
1561 std::fs::create_dir_all(path.clone())?;
1562 }
1563 Ok(path)
1564}