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