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