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