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