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