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