1use crate::{
8 ansible::{
9 inventory::{
10 generate_environment_inventory,
11 generate_full_cone_private_node_static_environment_inventory,
12 generate_port_restricted_cone_private_node_static_environment_inventory,
13 generate_symmetric_private_node_static_environment_inventory, AnsibleInventoryType,
14 },
15 provisioning::{AnsibleProvisioner, PrivateNodeProvisionInventory},
16 AnsibleRunner,
17 },
18 clients::ClientsDeployer,
19 get_bootstrap_cache_url, get_environment_details, get_genesis_multiaddr,
20 s3::S3Repository,
21 ssh::SshClient,
22 terraform::TerraformRunner,
23 BinaryOption, CloudProvider, DeploymentType, EnvironmentDetails, EnvironmentType, Error,
24 EvmDetails, TestnetDeployer,
25};
26use alloy::hex::ToHexExt;
27use ant_service_management::{NodeRegistryManager, ServiceStatus};
28use color_eyre::{eyre::eyre, Result};
29use log::debug;
30use rand::seq::{IteratorRandom, SliceRandom};
31use semver::Version;
32use serde::{Deserialize, Serialize};
33use std::{
34 collections::{HashMap, HashSet},
35 convert::From,
36 fs::File,
37 io::Write,
38 net::{IpAddr, SocketAddr},
39 path::PathBuf,
40};
41
42const DEFAULT_CONTACTS_COUNT: usize = 100;
43const UNAVAILABLE_NODE: &str = "-";
44const TESTNET_BUCKET_NAME: &str = "sn-testnet";
45
46pub struct DeploymentInventoryService {
47 pub ansible_runner: AnsibleRunner,
48 pub ansible_provisioner: AnsibleProvisioner,
52 pub cloud_provider: CloudProvider,
53 pub inventory_file_path: PathBuf,
54 pub s3_repository: S3Repository,
55 pub ssh_client: SshClient,
56 pub terraform_runner: TerraformRunner,
57 pub working_directory_path: PathBuf,
58}
59
60impl From<&TestnetDeployer> for DeploymentInventoryService {
61 fn from(item: &TestnetDeployer) -> Self {
62 let provider = match item.cloud_provider {
63 CloudProvider::Aws => "aws",
64 CloudProvider::DigitalOcean => "digital_ocean",
65 };
66 DeploymentInventoryService {
67 ansible_runner: item.ansible_provisioner.ansible_runner.clone(),
68 ansible_provisioner: item.ansible_provisioner.clone(),
69 cloud_provider: item.cloud_provider,
70 inventory_file_path: item
71 .working_directory_path
72 .join("ansible")
73 .join("inventory")
74 .join(format!("dev_inventory_{provider}.yml")),
75 s3_repository: item.s3_repository.clone(),
76 ssh_client: item.ssh_client.clone(),
77 terraform_runner: item.terraform_runner.clone(),
78 working_directory_path: item.working_directory_path.clone(),
79 }
80 }
81}
82
83impl From<&ClientsDeployer> for DeploymentInventoryService {
84 fn from(item: &ClientsDeployer) -> Self {
85 let provider = match item.cloud_provider {
86 CloudProvider::Aws => "aws",
87 CloudProvider::DigitalOcean => "digital_ocean",
88 };
89 DeploymentInventoryService {
90 ansible_runner: item.ansible_provisioner.ansible_runner.clone(),
91 ansible_provisioner: item.ansible_provisioner.clone(),
92 cloud_provider: item.cloud_provider,
93 inventory_file_path: item
94 .working_directory_path
95 .join("ansible")
96 .join("inventory")
97 .join(format!("dev_inventory_{provider}.yml")),
98 s3_repository: item.s3_repository.clone(),
99 ssh_client: item.ssh_client.clone(),
100 terraform_runner: item.terraform_runner.clone(),
101 working_directory_path: item.working_directory_path.clone(),
102 }
103 }
104}
105
106impl DeploymentInventoryService {
107 pub async fn generate_or_retrieve_inventory(
122 &self,
123 name: &str,
124 force: bool,
125 binary_option: Option<BinaryOption>,
126 ) -> Result<DeploymentInventory> {
127 println!("======================================");
128 println!(" Generating or Retrieving Inventory ");
129 println!("======================================");
130 let inventory_path = get_data_directory()?.join(format!("{name}-inventory.json"));
131 if inventory_path.exists() && !force {
132 let inventory = DeploymentInventory::read(&inventory_path)?;
133 return Ok(inventory);
134 }
135
136 if !force {
139 let environments = self.terraform_runner.workspace_list()?;
140 if !environments.contains(&name.to_string()) {
141 return Err(eyre!("The '{}' environment does not exist", name));
142 }
143 }
144
145 let output_inventory_dir_path = self
150 .working_directory_path
151 .join("ansible")
152 .join("inventory");
153 generate_environment_inventory(
154 name,
155 &self.inventory_file_path,
156 &output_inventory_dir_path,
157 )?;
158
159 let environment_details = match get_environment_details(name, &self.s3_repository).await {
160 Ok(details) => details,
161 Err(Error::EnvironmentDetailsNotFound(_)) => {
162 println!("Environment details not found: treating this as a new deployment");
163 return Ok(DeploymentInventory::empty(
164 name,
165 binary_option.ok_or_else(|| {
166 eyre!("For a new deployment the binary option must be set")
167 })?,
168 ));
169 }
170 Err(e) => return Err(e.into()),
171 };
172
173 let ansible_runner = self.ansible_runner.clone();
174 let genesis_handle = std::thread::spawn(move || {
175 ansible_runner.get_inventory(AnsibleInventoryType::Genesis, false)
176 });
177 let ansible_runner = self.ansible_runner.clone();
178 let build_handle = std::thread::spawn(move || {
179 ansible_runner.get_inventory(AnsibleInventoryType::Build, false)
180 });
181 let ansible_runner = self.ansible_runner.clone();
182 let full_cone_nat_gateway_handle = std::thread::spawn(move || {
183 ansible_runner.get_inventory(AnsibleInventoryType::FullConeNatGateway, false)
184 });
185 let ansible_runner = self.ansible_runner.clone();
186 let full_cone_private_node_handle = std::thread::spawn(move || {
187 ansible_runner.get_inventory(AnsibleInventoryType::FullConePrivateNodes, false)
188 });
189 let ansible_runner = self.ansible_runner.clone();
190 let symmetric_nat_gateway_handle = std::thread::spawn(move || {
191 ansible_runner.get_inventory(AnsibleInventoryType::SymmetricNatGateway, false)
192 });
193 let ansible_runner = self.ansible_runner.clone();
194 let symmetric_private_node_handle = std::thread::spawn(move || {
195 ansible_runner.get_inventory(AnsibleInventoryType::SymmetricPrivateNodes, false)
196 });
197 let ansible_runner = self.ansible_runner.clone();
198 let generic_node_handle = std::thread::spawn(move || {
199 ansible_runner.get_inventory(AnsibleInventoryType::Nodes, false)
200 });
201 let ansible_runner = self.ansible_runner.clone();
202 let peer_cache_node_handle = std::thread::spawn(move || {
203 ansible_runner.get_inventory(AnsibleInventoryType::PeerCacheNodes, false)
204 });
205 let ansible_runner = self.ansible_runner.clone();
206 let port_restricted_cone_nat_gateway_handle = std::thread::spawn(move || {
207 ansible_runner.get_inventory(AnsibleInventoryType::PortRestrictedConeNatGateway, false)
208 });
209 let ansible_runner = self.ansible_runner.clone();
210 let port_restricted_cone_private_node_handle = std::thread::spawn(move || {
211 ansible_runner
212 .get_inventory(AnsibleInventoryType::PortRestrictedConePrivateNodes, false)
213 });
214 let ansible_runner = self.ansible_runner.clone();
215 let upnp_private_node_handle = std::thread::spawn(move || {
216 ansible_runner.get_inventory(AnsibleInventoryType::Upnp, false)
217 });
218 let ansible_runner = self.ansible_runner.clone();
219 let client_handle = std::thread::spawn(move || {
220 ansible_runner.get_inventory(AnsibleInventoryType::Clients, true)
221 });
222
223 let genesis_vm = genesis_handle.join().expect("Thread panicked")?;
224 let mut misc_vms = Vec::new();
225 misc_vms.extend(build_handle.join().expect("Thread panicked")?);
226 let full_cone_nat_gateway_vms = full_cone_nat_gateway_handle
227 .join()
228 .expect("Thread panicked")?;
229 let full_cone_private_node_vms = full_cone_private_node_handle
230 .join()
231 .expect("Thread panicked")?;
232 let symmetric_nat_gateway_vms = symmetric_nat_gateway_handle
233 .join()
234 .expect("Thread panicked")?;
235 let symmetric_private_node_vms = symmetric_private_node_handle
236 .join()
237 .expect("Thread panicked")?;
238 let generic_node_vms = generic_node_handle.join().expect("Thread panicked")?;
239 let peer_cache_node_vms = peer_cache_node_handle.join().expect("Thread panicked")?;
240 let port_restricted_cone_nat_gateway_vms = port_restricted_cone_nat_gateway_handle
241 .join()
242 .expect("Thread panicked")?;
243 let port_restricted_cone_private_node_vms = port_restricted_cone_private_node_handle
244 .join()
245 .expect("Thread panicked")?;
246 let upnp_private_node_vms = upnp_private_node_handle.join().expect("Thread panicked")?;
247 let client_vms = if !client_handle.join().expect("Thread panicked")?.is_empty()
248 && environment_details.deployment_type != DeploymentType::Bootstrap
249 {
250 let client_and_sks = self.ansible_provisioner.get_client_secret_keys()?;
251 client_and_sks
252 .iter()
253 .map(|(vm, sks)| ClientVirtualMachine {
254 vm: vm.clone(),
255 wallet_public_key: sks
256 .iter()
257 .enumerate()
258 .map(|(user, sk)| {
259 let user_number = user + 1;
260 (format!("safe{user_number}"), sk.address().encode_hex())
261 })
262 .collect(),
263 })
264 .collect()
265 } else {
266 Vec::new()
267 };
268
269 debug!("full_cone_private_node_vms: {full_cone_private_node_vms:?}");
270 debug!("full_cone_nat_gateway_vms: {full_cone_nat_gateway_vms:?}");
271 debug!("port_restricted_cone_private_node_vms: {port_restricted_cone_private_node_vms:?}");
272 debug!("port_restricted_cone_nat_gateway_vms: {port_restricted_cone_nat_gateway_vms:?}");
273 debug!("symmetric_private_node_vms: {symmetric_private_node_vms:?}");
274 debug!("symmetric_nat_gateway_vms: {symmetric_nat_gateway_vms:?}");
275
276 generate_full_cone_private_node_static_environment_inventory(
278 name,
279 &output_inventory_dir_path,
280 &full_cone_private_node_vms,
281 &full_cone_nat_gateway_vms,
282 &self.ssh_client.private_key_path,
283 )?;
284 generate_symmetric_private_node_static_environment_inventory(
285 name,
286 &output_inventory_dir_path,
287 &symmetric_private_node_vms,
288 &symmetric_nat_gateway_vms,
289 &self.ssh_client.private_key_path,
290 )?;
291 generate_port_restricted_cone_private_node_static_environment_inventory(
292 name,
293 &output_inventory_dir_path,
294 &port_restricted_cone_private_node_vms,
295 &port_restricted_cone_nat_gateway_vms,
296 &self.ssh_client.private_key_path,
297 )?;
298
299 if !symmetric_nat_gateway_vms.is_empty() {
301 self.ssh_client.set_symmetric_nat_routed_vms(
302 &symmetric_private_node_vms,
303 &symmetric_nat_gateway_vms,
304 )?;
305 }
306 if !full_cone_nat_gateway_vms.is_empty() {
307 self.ssh_client.set_full_cone_nat_routed_vms(
308 &full_cone_private_node_vms,
309 &full_cone_nat_gateway_vms,
310 )?;
311 }
312 if !port_restricted_cone_nat_gateway_vms.is_empty() {
313 self.ssh_client.set_port_restricted_cone_nat_routed_vms(
314 &port_restricted_cone_private_node_vms,
315 &port_restricted_cone_nat_gateway_vms,
316 )?;
317 }
318
319 println!("Retrieving node registries from all VMs...");
320 let ansible_provisioner = self.ansible_provisioner.clone();
321 let peer_cache_node_registries_handle = tokio::spawn(async move {
322 ansible_provisioner
323 .get_node_registries(&AnsibleInventoryType::PeerCacheNodes)
324 .await
325 });
326 let ansible_provisioner = self.ansible_provisioner.clone();
327 let generic_node_registries_handle = tokio::spawn(async move {
328 ansible_provisioner
329 .get_node_registries(&AnsibleInventoryType::Nodes)
330 .await
331 });
332 let ansible_provisioner = self.ansible_provisioner.clone();
333 let symmetric_private_node_registries_handle = tokio::spawn(async move {
334 ansible_provisioner
335 .get_node_registries(&AnsibleInventoryType::SymmetricPrivateNodes)
336 .await
337 });
338 let ansible_provisioner = self.ansible_provisioner.clone();
339 let full_cone_private_node_registries_handle = tokio::spawn(async move {
340 ansible_provisioner
341 .get_node_registries(&AnsibleInventoryType::FullConePrivateNodes)
342 .await
343 });
344 let ansible_provisioner = self.ansible_provisioner.clone();
345 let port_restricted_cone_private_node_registries_handle = tokio::spawn(async move {
346 ansible_provisioner
347 .get_node_registries(&AnsibleInventoryType::PortRestrictedConePrivateNodes)
348 .await
349 });
350 let ansible_provisioner = self.ansible_provisioner.clone();
351 let upnp_private_node_registries_handle = tokio::spawn(async move {
352 ansible_provisioner
353 .get_node_registries(&AnsibleInventoryType::Upnp)
354 .await
355 });
356 let ansible_provisioner = self.ansible_provisioner.clone();
357 let genesis_node_registry_handle = tokio::spawn(async move {
358 ansible_provisioner
359 .get_node_registries(&AnsibleInventoryType::Genesis)
360 .await
361 });
362
363 let peer_cache_node_registries = peer_cache_node_registries_handle
364 .await
365 .expect("Task panicked")?;
366 let generic_node_registries = generic_node_registries_handle
367 .await
368 .expect("Task panicked")?;
369 let symmetric_private_node_registries = symmetric_private_node_registries_handle
370 .await
371 .expect("Task panicked")?;
372 let full_cone_private_node_registries = full_cone_private_node_registries_handle
373 .await
374 .expect("Task panicked")?;
375 let port_restricted_cone_private_node_registries =
376 port_restricted_cone_private_node_registries_handle
377 .await
378 .expect("Task panicked")?;
379 let upnp_private_node_registries = upnp_private_node_registries_handle
380 .await
381 .expect("Task panicked")?;
382 let genesis_node_registry = genesis_node_registry_handle.await.expect("Task panicked")?;
383
384 let peer_cache_node_vms =
385 NodeVirtualMachine::from_list(&peer_cache_node_vms, &peer_cache_node_registries).await;
386
387 let generic_node_vms =
388 NodeVirtualMachine::from_list(&generic_node_vms, &generic_node_registries).await;
389
390 let symmetric_private_node_vms = NodeVirtualMachine::from_list(
391 &symmetric_private_node_vms,
392 &symmetric_private_node_registries,
393 )
394 .await;
395 debug!("symmetric_private_node_vms after conversion: {symmetric_private_node_vms:?}");
396
397 debug!("full_cone_private_node_vms: {full_cone_private_node_vms:?}");
398 let full_cone_private_node_gateway_vm_map =
399 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
400 &full_cone_private_node_vms,
401 &full_cone_nat_gateway_vms,
402 )?;
403 debug!("full_cone_private_node_gateway_vm_map: {full_cone_private_node_gateway_vm_map:?}");
404 let full_cone_private_node_vms = NodeVirtualMachine::from_list(
405 &full_cone_private_node_vms,
406 &full_cone_private_node_registries,
407 )
408 .await;
409 debug!("full_cone_private_node_vms after conversion: {full_cone_private_node_vms:?}");
410
411 debug!("port_restricted_cone_private_node_vms: {port_restricted_cone_private_node_vms:?}");
412 let port_restricted_cone_private_node_gateway_vm_map =
413 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
414 &port_restricted_cone_private_node_vms,
415 &port_restricted_cone_nat_gateway_vms,
416 )?;
417 debug!("port_restricted_cone_private_node_gateway_vm_map: {port_restricted_cone_private_node_gateway_vm_map:?}");
418 let port_restricted_cone_private_node_vms = NodeVirtualMachine::from_list(
419 &port_restricted_cone_private_node_vms,
420 &port_restricted_cone_private_node_registries,
421 )
422 .await;
423 debug!("port_restricted_cone_private_node_vms after conversion: {port_restricted_cone_private_node_vms:?}");
424
425 let upnp_private_node_vms =
426 NodeVirtualMachine::from_list(&upnp_private_node_vms, &upnp_private_node_registries)
427 .await;
428 debug!("upnp_private_node_vms after conversion: {upnp_private_node_vms:?}");
429
430 let genesis_vm = NodeVirtualMachine::from_list(&genesis_vm, &genesis_node_registry).await;
431 let genesis_vm = if !genesis_vm.is_empty() {
432 Some(genesis_vm[0].clone())
433 } else {
434 None
435 };
436
437 let mut failed_node_registry_vms = Vec::new();
438 failed_node_registry_vms.extend(peer_cache_node_registries.failed_vms);
439 failed_node_registry_vms.extend(generic_node_registries.failed_vms);
440 failed_node_registry_vms.extend(full_cone_private_node_registries.failed_vms);
441 failed_node_registry_vms.extend(port_restricted_cone_private_node_registries.failed_vms);
442 failed_node_registry_vms.extend(symmetric_private_node_registries.failed_vms);
443 failed_node_registry_vms.extend(upnp_private_node_registries.failed_vms);
444 failed_node_registry_vms.extend(genesis_node_registry.failed_vms);
445
446 let binary_option = if let Some(binary_option) = binary_option {
447 binary_option
448 } else {
449 let (antnode_version, antctl_version) = {
450 let mut random_vm = None;
451 if !generic_node_vms.is_empty() {
452 random_vm = generic_node_vms.first().cloned();
453 } else if !peer_cache_node_vms.is_empty() {
454 random_vm = peer_cache_node_vms.first().cloned();
455 } else if genesis_vm.is_some() {
456 random_vm = genesis_vm.clone()
457 };
458
459 if let Some(random_vm) = random_vm {
460 let antnode_version = self.get_antnode_version(&random_vm.vm)?;
461 let antctl_version = self.get_bin_version(
462 &random_vm.vm,
463 "antctl --version",
464 "Autonomi Node Manager v",
465 )?;
466 (Some(antnode_version), Some(antctl_version))
467 } else {
468 (None, None)
469 }
470 };
471
472 let ant_version = if !client_vms.is_empty()
473 && environment_details.deployment_type != DeploymentType::Bootstrap
474 {
475 let random_client_vm = client_vms
476 .choose(&mut rand::thread_rng())
477 .ok_or_else(|| eyre!("No Client VMs available to retrieve ant version"))?;
478 self.get_bin_version(&random_client_vm.vm, "ant --version", "Autonomi Client v")
479 .ok()
480 } else {
481 None
482 };
483
484 println!("Retrieved binary versions from previous deployment:");
485 if let Some(version) = &antnode_version {
486 println!(" antnode: {version}");
487 }
488 if let Some(version) = &antctl_version {
489 println!(" antctl: {version}");
490 }
491 if let Some(version) = &ant_version {
492 println!(" ant: {version}");
493 }
494
495 BinaryOption::Versioned {
496 ant_version,
497 antnode_version,
498 antctl_version,
499 }
500 };
501
502 let (genesis_multiaddr, genesis_ip) =
503 if environment_details.deployment_type == DeploymentType::New {
504 match get_genesis_multiaddr(&self.ansible_runner, &self.ssh_client) {
505 Ok(Some((multiaddr, ip))) => (Some(multiaddr), Some(ip)),
506 Ok(None) => (None, None),
507 Err(_) => (None, None),
508 }
509 } else {
510 (None, None)
511 };
512 let inventory = DeploymentInventory {
513 binary_option,
514 client_vms,
515 environment_details,
516 failed_node_registry_vms,
517 faucet_address: genesis_ip.map(|ip| format!("{ip}:8000")),
518 full_cone_nat_gateway_vms,
519 full_cone_private_node_vms,
520 genesis_multiaddr,
521 genesis_vm,
522 name: name.to_string(),
523 misc_vms,
524 node_vms: generic_node_vms,
525 peer_cache_node_vms,
526 port_restricted_cone_nat_gateway_vms,
527 port_restricted_cone_private_node_vms,
528 ssh_user: self.cloud_provider.get_ssh_user(),
529 ssh_private_key_path: self.ssh_client.private_key_path.clone(),
530 symmetric_nat_gateway_vms,
531 symmetric_private_node_vms,
532 upnp_private_node_vms,
533 uploaded_files: Vec::new(),
534 };
535 debug!("Inventory: {inventory:?}");
536 Ok(inventory)
537 }
538
539 pub fn setup_environment_inventory(&self, name: &str) -> Result<()> {
544 let output_inventory_dir_path = self
545 .working_directory_path
546 .join("ansible")
547 .join("inventory");
548 generate_environment_inventory(
549 name,
550 &self.inventory_file_path,
551 &output_inventory_dir_path,
552 )?;
553
554 let full_cone_nat_gateway_vms = self
555 .ansible_runner
556 .get_inventory(AnsibleInventoryType::FullConeNatGateway, false)?;
557 let full_cone_private_node_vms = self
558 .ansible_runner
559 .get_inventory(AnsibleInventoryType::FullConePrivateNodes, false)?;
560
561 let symmetric_nat_gateway_vms = self
562 .ansible_runner
563 .get_inventory(AnsibleInventoryType::SymmetricNatGateway, false)?;
564 let symmetric_private_node_vms = self
565 .ansible_runner
566 .get_inventory(AnsibleInventoryType::SymmetricPrivateNodes, false)?;
567
568 let port_restricted_cone_nat_gateway_vms = self
569 .ansible_runner
570 .get_inventory(AnsibleInventoryType::PortRestrictedConeNatGateway, false)?;
571 let port_restricted_cone_private_node_vms = self
572 .ansible_runner
573 .get_inventory(AnsibleInventoryType::PortRestrictedConePrivateNodes, false)?;
574
575 generate_symmetric_private_node_static_environment_inventory(
577 name,
578 &output_inventory_dir_path,
579 &symmetric_private_node_vms,
580 &symmetric_nat_gateway_vms,
581 &self.ssh_client.private_key_path,
582 )?;
583
584 generate_full_cone_private_node_static_environment_inventory(
585 name,
586 &output_inventory_dir_path,
587 &full_cone_private_node_vms,
588 &full_cone_nat_gateway_vms,
589 &self.ssh_client.private_key_path,
590 )?;
591
592 generate_port_restricted_cone_private_node_static_environment_inventory(
593 name,
594 &output_inventory_dir_path,
595 &port_restricted_cone_private_node_vms,
596 &port_restricted_cone_nat_gateway_vms,
597 &self.ssh_client.private_key_path,
598 )?;
599
600 if !full_cone_nat_gateway_vms.is_empty() {
602 self.ssh_client.set_full_cone_nat_routed_vms(
603 &full_cone_private_node_vms,
604 &full_cone_nat_gateway_vms,
605 )?;
606 }
607
608 if !symmetric_nat_gateway_vms.is_empty() {
609 self.ssh_client.set_symmetric_nat_routed_vms(
610 &symmetric_private_node_vms,
611 &symmetric_nat_gateway_vms,
612 )?;
613 }
614
615 if !port_restricted_cone_nat_gateway_vms.is_empty() {
616 self.ssh_client.set_port_restricted_cone_nat_routed_vms(
617 &port_restricted_cone_private_node_vms,
618 &port_restricted_cone_nat_gateway_vms,
619 )?;
620 }
621
622 Ok(())
623 }
624
625 pub async fn upload_network_contacts(
626 &self,
627 inventory: &DeploymentInventory,
628 contacts_file_name: Option<String>,
629 ) -> Result<()> {
630 let temp_dir_path = tempfile::tempdir()?.keep();
631 let temp_file_path = if let Some(file_name) = contacts_file_name {
632 temp_dir_path.join(file_name)
633 } else {
634 temp_dir_path.join(inventory.name.clone())
635 };
636
637 let mut file = std::fs::File::create(&temp_file_path)?;
638 let mut rng = rand::thread_rng();
639
640 let peer_cache_peers = inventory
641 .peer_cache_node_vms
642 .iter()
643 .flat_map(|vm| vm.get_quic_addresses())
644 .collect::<Vec<_>>();
645 let peer_cache_peers_len = peer_cache_peers.len();
646 for peer in peer_cache_peers
647 .iter()
648 .filter(|&peer| peer != UNAVAILABLE_NODE)
649 .cloned()
650 .choose_multiple(&mut rng, DEFAULT_CONTACTS_COUNT)
651 {
652 writeln!(file, "{peer}",)?;
653 }
654
655 if DEFAULT_CONTACTS_COUNT > peer_cache_peers_len {
656 let node_peers = inventory
657 .node_vms
658 .iter()
659 .flat_map(|vm| vm.get_quic_addresses())
660 .collect::<Vec<_>>();
661 for peer in node_peers
662 .iter()
663 .filter(|&peer| peer != UNAVAILABLE_NODE)
664 .cloned()
665 .choose_multiple(&mut rng, DEFAULT_CONTACTS_COUNT - peer_cache_peers_len)
666 {
667 writeln!(file, "{peer}",)?;
668 }
669 }
670
671 self.s3_repository
672 .upload_file(TESTNET_BUCKET_NAME, &temp_file_path, true)
673 .await?;
674
675 Ok(())
676 }
677
678 fn get_bin_version(&self, vm: &VirtualMachine, command: &str, prefix: &str) -> Result<Version> {
680 let output = self.ssh_client.run_command(
681 &vm.public_ip_addr,
682 &self.cloud_provider.get_ssh_user(),
683 command,
684 true,
685 )?;
686 let version_line = output
687 .first()
688 .ok_or_else(|| eyre!("No output from {} command", command))?;
689 let version_str = version_line
690 .strip_prefix(prefix)
691 .ok_or_else(|| eyre!("Unexpected output format from {} command", command))?;
692 Version::parse(version_str).map_err(|e| eyre!("Failed to parse {} version: {}", command, e))
693 }
694
695 fn get_antnode_version(&self, vm: &VirtualMachine) -> Result<Version> {
698 let output = self.ssh_client.run_command(
699 &vm.public_ip_addr,
700 &self.cloud_provider.get_ssh_user(),
701 "antctl status --json | jq -r '.nodes[] | .version' | head -n1",
702 true,
703 )?;
704
705 let version_line = output
706 .first()
707 .ok_or_else(|| eyre!("No running antnode found from antctl status"))?;
708
709 if version_line.trim().is_empty() || version_line.trim() == "null" {
710 return Err(eyre!("No running antnode services found"));
711 }
712
713 Version::parse(version_line.trim())
714 .map_err(|e| eyre!("Failed to parse antnode version from antctl: {}", e))
715 }
716
717 pub async fn generate_or_retrieve_client_inventory(
726 &self,
727 name: &str,
728 region: &str,
729 force: bool,
730 binary_option: Option<BinaryOption>,
731 ) -> Result<ClientsDeploymentInventory> {
732 println!("===============================================");
733 println!(" Generating or Retrieving Client Inventory ");
734 println!("===============================================");
735 let inventory_path = get_data_directory()?.join(format!("{name}-clients-inventory.json"));
736 if inventory_path.exists() && !force {
737 let inventory = ClientsDeploymentInventory::read(&inventory_path)?;
738 return Ok(inventory);
739 }
740
741 if !force {
744 let environments = self.terraform_runner.workspace_list()?;
745 if !environments.contains(&name.to_string()) {
746 return Err(eyre!("The '{}' environment does not exist", name));
747 }
748 }
749
750 let output_inventory_dir_path = self
755 .working_directory_path
756 .join("ansible")
757 .join("inventory");
758 generate_environment_inventory(
759 name,
760 &self.inventory_file_path,
761 &output_inventory_dir_path,
762 )?;
763
764 let environment_details = match get_environment_details(name, &self.s3_repository).await {
765 Ok(details) => details,
766 Err(Error::EnvironmentDetailsNotFound(_)) => {
767 println!("Environment details not found: treating this as a new deployment");
768 return Ok(ClientsDeploymentInventory::empty(
769 name,
770 binary_option.ok_or_else(|| {
771 eyre!("For a new deployment the binary option must be set")
772 })?,
773 region,
774 ));
775 }
776 Err(e) => return Err(e.into()),
777 };
778
779 let client_and_sks = self.ansible_provisioner.get_client_secret_keys()?;
780 let client_vms: Vec<ClientVirtualMachine> = client_and_sks
781 .iter()
782 .map(|(vm, sks)| ClientVirtualMachine {
783 vm: vm.clone(),
784 wallet_public_key: sks
785 .iter()
786 .enumerate()
787 .map(|(user, sk)| {
788 let user_number = user + 1;
789 (format!("safe{user_number}"), sk.address().encode_hex())
790 })
791 .collect(),
792 })
793 .collect();
794
795 let binary_option = if let Some(binary_option) = binary_option {
796 binary_option
797 } else {
798 let ant_version = if !client_vms.is_empty() {
799 let random_client_vm = client_vms
800 .choose(&mut rand::thread_rng())
801 .ok_or_else(|| eyre!("No Client VMs available to retrieve ant version"))?;
802 self.get_bin_version(&random_client_vm.vm, "ant --version", "Autonomi Client v")
803 .ok()
804 } else {
805 None
806 };
807
808 println!("Retrieved binary versions from previous deployment:");
809 if let Some(version) = &ant_version {
810 println!(" ant: {version}");
811 }
812
813 BinaryOption::Versioned {
814 ant_version,
815 antnode_version: None,
816 antctl_version: None,
817 }
818 };
819
820 let inventory = ClientsDeploymentInventory {
821 binary_option,
822 client_vms,
823 environment_type: environment_details.environment_type,
824 evm_details: environment_details.evm_details,
825 funding_wallet_address: None, network_id: environment_details.network_id,
827 failed_node_registry_vms: Vec::new(),
828 name: name.to_string(),
829 region: environment_details.region,
830 ssh_user: self.cloud_provider.get_ssh_user(),
831 ssh_private_key_path: self.ssh_client.private_key_path.clone(),
832 uploaded_files: Vec::new(),
833 };
834
835 debug!("Client Inventory: {inventory:?}");
836 Ok(inventory)
837 }
838}
839
840impl NodeVirtualMachine {
841 pub async fn from_list(
842 vms: &[VirtualMachine],
843 node_registries: &DeploymentNodeRegistries,
844 ) -> Vec<Self> {
845 let mut node_vms = Vec::new();
846 for vm in vms {
847 let node_registry = node_registries
848 .retrieved_registries
849 .iter()
850 .find(|(name, _)| {
851 if vm.name.contains("private") {
852 let result = name == &vm.private_ip_addr.to_string();
853 debug!(
854 "Vm name: {name} is a private node with result {result}. Vm: {vm:?}"
855 );
856 result
857 } else {
858 name == &vm.name
859 }
860 })
861 .map(|(_, reg)| reg);
862
863 let node_registry = match node_registry {
864 Some(registry) => {
865 debug!("Found node registry for VM: {vm:?}");
866 registry
867 }
868 None => {
869 debug!("No node registry found for VM: {vm:?}");
870 let node_vm = Self {
871 vm: vm.clone(),
872 node_count: 0,
873 node_listen_addresses: Default::default(),
874 rpc_endpoint: HashMap::new(),
875 safenodemand_endpoint: None,
876 };
877 node_vms.push(node_vm);
878 continue;
879 }
880 };
881
882 let node_count = node_registry.nodes.read().await.len();
883 let mut node_listen_addresses = Vec::with_capacity(node_count);
884 for node in node_registry.nodes.read().await.iter() {
885 let addrs = node.read().await.listen_addr.clone();
886 if let Some(addrs) = addrs {
887 node_listen_addresses.push(addrs.iter().map(|addr| addr.to_string()).collect());
888 }
889 }
890
891 let node_vm = Self {
892 vm: vm.clone(),
893 node_count,
894 node_listen_addresses,
895 rpc_endpoint: HashMap::new(),
896 safenodemand_endpoint: None,
897 };
898 node_vms.push(node_vm.clone());
899 debug!("Added node VM: {node_vm:?}");
900 }
901 debug!("Node VMs generated from NodeRegistries: {node_vms:?}");
902 node_vms
903 }
904
905 pub fn get_quic_addresses(&self) -> Vec<String> {
906 self.node_listen_addresses
907 .iter()
908 .map(|addresses| {
909 addresses
910 .iter()
911 .find(|addr| {
912 addr.contains("/quic-v1")
913 && !addr.starts_with("/ip4/127.0.0.1")
914 && !addr.starts_with("/ip4/10.")
915 })
916 .map(|s| s.to_string())
917 .unwrap_or_else(|| UNAVAILABLE_NODE.to_string())
918 })
919 .collect()
920 }
921}
922
923pub type OsUser = String;
925
926#[derive(Clone, Debug, Serialize, Deserialize)]
927pub struct ClientVirtualMachine {
928 pub vm: VirtualMachine,
929 pub wallet_public_key: HashMap<OsUser, String>,
931}
932
933#[derive(Clone, Debug, Serialize, Deserialize)]
934pub struct NodeVirtualMachine {
935 pub vm: VirtualMachine,
936 pub node_count: usize,
937 pub node_listen_addresses: Vec<Vec<String>>,
938 pub rpc_endpoint: HashMap<String, SocketAddr>,
939 pub safenodemand_endpoint: Option<SocketAddr>,
940}
941
942#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
943pub struct VirtualMachine {
944 pub id: u64,
945 pub name: String,
946 pub public_ip_addr: IpAddr,
947 pub private_ip_addr: IpAddr,
948}
949
950#[derive(Clone)]
951pub struct DeploymentNodeRegistries {
952 pub inventory_type: AnsibleInventoryType,
953 pub retrieved_registries: Vec<(String, NodeRegistryManager)>,
956 pub failed_vms: Vec<String>,
957}
958
959impl DeploymentNodeRegistries {
960 pub async fn get_node_count(&self) -> usize {
961 let mut count = 0;
962 for (_, registry) in self.retrieved_registries.iter() {
963 count += registry.nodes.read().await.len();
964 }
965 count
966 }
967
968 pub async fn print(&self) {
969 if self.retrieved_registries.is_empty() {
970 return;
971 }
972
973 Self::print_banner(&self.inventory_type.to_string());
974 for (vm_name, registry) in self.retrieved_registries.iter() {
975 println!("{vm_name}:");
976 for node in registry.nodes.read().await.iter() {
977 let node = node.read().await;
978 println!(
979 " {}: {} {}",
980 node.service_name,
981 node.version,
982 Self::format_status(&node.status)
983 );
984 }
985 }
986 if !self.failed_vms.is_empty() {
987 println!(
988 "Failed to retrieve node registries for {}:",
989 self.inventory_type
990 );
991 for vm_name in self.failed_vms.iter() {
992 println!("- {vm_name}");
993 }
994 }
995 }
996
997 fn format_status(status: &ServiceStatus) -> String {
998 match status {
999 ServiceStatus::Running => "RUNNING".to_string(),
1000 ServiceStatus::Stopped => "STOPPED".to_string(),
1001 ServiceStatus::Added => "ADDED".to_string(),
1002 ServiceStatus::Removed => "REMOVED".to_string(),
1003 }
1004 }
1005
1006 fn print_banner(text: &str) {
1007 let padding = 2;
1008 let text_width = text.len() + padding * 2;
1009 let border_chars = 2;
1010 let total_width = text_width + border_chars;
1011 let top_bottom = "═".repeat(total_width);
1012
1013 println!("╔{top_bottom}╗");
1014 println!("║ {text:^text_width$} ║");
1015 println!("╚{top_bottom}╝");
1016 }
1017}
1018
1019#[derive(Clone, Debug, Serialize, Deserialize)]
1020pub struct DeploymentInventory {
1021 pub binary_option: BinaryOption,
1022 pub client_vms: Vec<ClientVirtualMachine>,
1023 pub environment_details: EnvironmentDetails,
1024 pub failed_node_registry_vms: Vec<String>,
1025 pub faucet_address: Option<String>,
1026 pub full_cone_nat_gateway_vms: Vec<VirtualMachine>,
1027 pub full_cone_private_node_vms: Vec<NodeVirtualMachine>,
1028 pub genesis_vm: Option<NodeVirtualMachine>,
1029 pub genesis_multiaddr: Option<String>,
1030 pub misc_vms: Vec<VirtualMachine>,
1031 pub name: String,
1032 pub node_vms: Vec<NodeVirtualMachine>,
1033 pub peer_cache_node_vms: Vec<NodeVirtualMachine>,
1034 pub port_restricted_cone_nat_gateway_vms: Vec<VirtualMachine>,
1035 pub port_restricted_cone_private_node_vms: Vec<NodeVirtualMachine>,
1036 pub ssh_user: String,
1037 pub ssh_private_key_path: PathBuf,
1038 pub symmetric_nat_gateway_vms: Vec<VirtualMachine>,
1039 pub symmetric_private_node_vms: Vec<NodeVirtualMachine>,
1040 pub upnp_private_node_vms: Vec<NodeVirtualMachine>,
1041 pub uploaded_files: Vec<(String, String)>,
1042}
1043
1044impl DeploymentInventory {
1045 pub fn empty(name: &str, binary_option: BinaryOption) -> DeploymentInventory {
1048 Self {
1049 binary_option,
1050 client_vms: Default::default(),
1051 environment_details: EnvironmentDetails::default(),
1052 genesis_vm: Default::default(),
1053 genesis_multiaddr: Default::default(),
1054 failed_node_registry_vms: Default::default(),
1055 faucet_address: Default::default(),
1056 full_cone_nat_gateway_vms: Default::default(),
1057 full_cone_private_node_vms: Default::default(),
1058 misc_vms: Default::default(),
1059 name: name.to_string(),
1060 node_vms: Default::default(),
1061 peer_cache_node_vms: Default::default(),
1062 port_restricted_cone_nat_gateway_vms: Default::default(),
1063 port_restricted_cone_private_node_vms: Default::default(),
1064 ssh_user: "root".to_string(),
1065 ssh_private_key_path: Default::default(),
1066 symmetric_nat_gateway_vms: Default::default(),
1067 symmetric_private_node_vms: Default::default(),
1068 upnp_private_node_vms: Default::default(),
1069 uploaded_files: Default::default(),
1070 }
1071 }
1072
1073 pub fn get_tfvars_filenames(&self) -> Vec<String> {
1074 let filenames = self
1075 .environment_details
1076 .environment_type
1077 .get_tfvars_filenames(&self.name, &self.environment_details.region);
1078 debug!("Using tfvars files {filenames:?}");
1079 filenames
1080 }
1081
1082 pub fn is_empty(&self) -> bool {
1083 self.genesis_vm.is_none()
1084 && self.node_vms.is_empty()
1085 && self.peer_cache_node_vms.is_empty()
1086 && self.full_cone_private_node_vms.is_empty()
1087 && self.symmetric_private_node_vms.is_empty()
1088 && self.client_vms.is_empty()
1089 }
1090
1091 pub fn vm_list(&self) -> Vec<VirtualMachine> {
1092 let mut list = Vec::new();
1093 list.extend(self.symmetric_nat_gateway_vms.clone());
1094 list.extend(self.full_cone_nat_gateway_vms.clone());
1095 list.extend(self.port_restricted_cone_nat_gateway_vms.clone());
1096 list.extend(
1097 self.peer_cache_node_vms
1098 .iter()
1099 .map(|node_vm| node_vm.vm.clone()),
1100 );
1101 list.extend(self.genesis_vm.iter().map(|node_vm| node_vm.vm.clone()));
1102 list.extend(self.node_vms.iter().map(|node_vm| node_vm.vm.clone()));
1103 list.extend(self.misc_vms.clone());
1104 list.extend(
1105 self.symmetric_private_node_vms
1106 .iter()
1107 .map(|node_vm| node_vm.vm.clone()),
1108 );
1109 list.extend(
1110 self.full_cone_private_node_vms
1111 .iter()
1112 .map(|node_vm| node_vm.vm.clone()),
1113 );
1114 list.extend(
1115 self.port_restricted_cone_private_node_vms
1116 .iter()
1117 .map(|node_vm| node_vm.vm.clone()),
1118 );
1119 list.extend(
1120 self.upnp_private_node_vms
1121 .iter()
1122 .map(|node_vm| node_vm.vm.clone()),
1123 );
1124 list.extend(self.client_vms.iter().map(|client_vm| client_vm.vm.clone()));
1125 list
1126 }
1127
1128 pub fn node_vm_list(&self) -> Vec<NodeVirtualMachine> {
1129 let mut list = Vec::new();
1130 list.extend(self.peer_cache_node_vms.iter().cloned());
1131 list.extend(self.genesis_vm.iter().cloned());
1132 list.extend(self.node_vms.iter().cloned());
1133 list.extend(self.full_cone_private_node_vms.iter().cloned());
1134 list.extend(self.port_restricted_cone_private_node_vms.iter().cloned());
1135 list.extend(self.symmetric_private_node_vms.iter().cloned());
1136 list.extend(self.upnp_private_node_vms.iter().cloned());
1137
1138 list
1139 }
1140
1141 pub fn peers(&self) -> HashSet<String> {
1142 let mut list = HashSet::new();
1143 list.extend(
1144 self.peer_cache_node_vms
1145 .iter()
1146 .flat_map(|node_vm| node_vm.get_quic_addresses()),
1147 );
1148 list.extend(
1149 self.genesis_vm
1150 .iter()
1151 .flat_map(|node_vm| node_vm.get_quic_addresses()),
1152 );
1153 list.extend(
1154 self.node_vms
1155 .iter()
1156 .flat_map(|node_vm| node_vm.get_quic_addresses()),
1157 );
1158 list.extend(
1159 self.full_cone_private_node_vms
1160 .iter()
1161 .flat_map(|node_vm| node_vm.get_quic_addresses()),
1162 );
1163 list.extend(
1164 self.port_restricted_cone_private_node_vms
1165 .iter()
1166 .flat_map(|node_vm| node_vm.get_quic_addresses()),
1167 );
1168 list.extend(
1169 self.symmetric_private_node_vms
1170 .iter()
1171 .flat_map(|node_vm| node_vm.get_quic_addresses()),
1172 );
1173 list.extend(
1174 self.upnp_private_node_vms
1175 .iter()
1176 .flat_map(|node_vm| node_vm.get_quic_addresses()),
1177 );
1178 list
1179 }
1180
1181 pub fn save(&self) -> Result<()> {
1182 let path = get_data_directory()?.join(format!("{}-inventory.json", self.name));
1183 let serialized_data = serde_json::to_string_pretty(self)?;
1184 let mut file = File::create(path)?;
1185 file.write_all(serialized_data.as_bytes())?;
1186 Ok(())
1187 }
1188
1189 pub fn read(file_path: &PathBuf) -> Result<Self> {
1190 let data = std::fs::read_to_string(file_path)?;
1191 let deserialized_data: DeploymentInventory = serde_json::from_str(&data)?;
1192 Ok(deserialized_data)
1193 }
1194
1195 pub fn add_uploaded_files(&mut self, uploaded_files: Vec<(String, String)>) {
1196 self.uploaded_files.extend_from_slice(&uploaded_files);
1197 }
1198
1199 pub fn get_random_peer(&self) -> Option<String> {
1200 let mut rng = rand::thread_rng();
1201 self.peers().into_iter().choose(&mut rng)
1202 }
1203
1204 pub fn peer_cache_node_count(&self) -> usize {
1205 if let Some(first_vm) = self.peer_cache_node_vms.first() {
1206 first_vm.node_count
1207 } else {
1208 0
1209 }
1210 }
1211
1212 pub fn genesis_node_count(&self) -> usize {
1213 if let Some(genesis_vm) = &self.genesis_vm {
1214 genesis_vm.node_count
1215 } else {
1216 0
1217 }
1218 }
1219
1220 pub fn node_count(&self) -> usize {
1221 if let Some(first_vm) = self.node_vms.first() {
1222 first_vm.node_count
1223 } else {
1224 0
1225 }
1226 }
1227
1228 pub fn full_cone_private_node_count(&self) -> usize {
1229 if let Some(first_vm) = self.full_cone_private_node_vms.first() {
1230 first_vm.node_count
1231 } else {
1232 0
1233 }
1234 }
1235
1236 pub fn port_restricted_cone_private_node_count(&self) -> usize {
1237 if let Some(first_vm) = self.port_restricted_cone_private_node_vms.first() {
1238 first_vm.node_count
1239 } else {
1240 0
1241 }
1242 }
1243
1244 pub fn symmetric_private_node_count(&self) -> usize {
1245 if let Some(first_vm) = self.symmetric_private_node_vms.first() {
1246 first_vm.node_count
1247 } else {
1248 0
1249 }
1250 }
1251
1252 pub fn upnp_private_node_count(&self) -> usize {
1253 if let Some(first_vm) = self.upnp_private_node_vms.first() {
1254 first_vm.node_count
1255 } else {
1256 0
1257 }
1258 }
1259
1260 pub fn print_report(&self, full: bool) -> Result<()> {
1261 println!("**************************************");
1262 println!("* *");
1263 println!("* Inventory Report *");
1264 println!("* *");
1265 println!("**************************************");
1266
1267 println!("Environment Name: {}", self.name);
1268 println!();
1269 match &self.binary_option {
1270 BinaryOption::BuildFromSource {
1271 repo_owner, branch, ..
1272 } => {
1273 println!("==============");
1274 println!("Branch Details");
1275 println!("==============");
1276 println!("Repo owner: {repo_owner}");
1277 println!("Branch name: {branch}");
1278 println!();
1279 }
1280 BinaryOption::Versioned {
1281 ant_version,
1282 antnode_version,
1283 antctl_version,
1284 } => {
1285 println!("===============");
1286 println!("Version Details");
1287 println!("===============");
1288 println!(
1289 "ant version: {}",
1290 ant_version
1291 .as_ref()
1292 .map_or("N/A".to_string(), |v| v.to_string())
1293 );
1294 println!(
1295 "antnode version: {}",
1296 antnode_version
1297 .as_ref()
1298 .map_or("N/A".to_string(), |v| v.to_string())
1299 );
1300 println!(
1301 "antctl version: {}",
1302 antctl_version
1303 .as_ref()
1304 .map_or("N/A".to_string(), |v| v.to_string())
1305 );
1306 println!();
1307 }
1308 }
1309
1310 if !self.peer_cache_node_vms.is_empty() {
1311 println!("==============");
1312 println!("Peer Cache VMs");
1313 println!("==============");
1314 for node_vm in self.peer_cache_node_vms.iter() {
1315 println!("{}: {}", node_vm.vm.name, node_vm.vm.public_ip_addr);
1316 }
1317 println!("Nodes per VM: {}", self.peer_cache_node_count());
1318 println!("SSH user: {}", self.ssh_user);
1319 println!();
1320
1321 self.print_peer_cache_webserver();
1322 }
1323
1324 println!("========");
1325 println!("Node VMs");
1326 println!("========");
1327 if let Some(genesis_vm) = &self.genesis_vm {
1328 println!("{}: {}", genesis_vm.vm.name, genesis_vm.vm.public_ip_addr);
1329 }
1330 for node_vm in self.node_vms.iter() {
1331 println!("{}: {}", node_vm.vm.name, node_vm.vm.public_ip_addr);
1332 }
1333 println!("Nodes per VM: {}", self.node_count());
1334 println!("SSH user: {}", self.ssh_user);
1335 println!();
1336
1337 if !self.full_cone_private_node_vms.is_empty() {
1338 println!("=================");
1339 println!("Full Cone Private Node VMs");
1340 println!("=================");
1341 let full_cone_private_node_nat_gateway_map =
1342 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
1343 self.full_cone_private_node_vms
1344 .iter()
1345 .map(|node_vm| node_vm.vm.clone())
1346 .collect::<Vec<_>>()
1347 .as_slice(),
1348 &self.full_cone_nat_gateway_vms,
1349 )?;
1350
1351 for (node_vm, nat_gateway_vm) in full_cone_private_node_nat_gateway_map.iter() {
1352 println!(
1353 "{}: {} ==routed through==> {}: {}",
1354 node_vm.name,
1355 node_vm.public_ip_addr,
1356 nat_gateway_vm.name,
1357 nat_gateway_vm.public_ip_addr
1358 );
1359 let ssh = if let Some(ssh_key_path) = self.ssh_private_key_path.to_str() {
1360 format!(
1361 "ssh -i {ssh_key_path} root@{}",
1362 nat_gateway_vm.public_ip_addr,
1363 )
1364 } else {
1365 format!("ssh root@{}", nat_gateway_vm.public_ip_addr,)
1366 };
1367 println!("SSH using NAT gateway: {ssh}");
1368 }
1369 println!("Nodes per VM: {}", self.full_cone_private_node_count());
1370 println!("SSH user: {}", self.ssh_user);
1371 println!();
1372 }
1373
1374 if !self.port_restricted_cone_private_node_vms.is_empty() {
1375 println!("============================");
1376 println!("Port Restricted Cone Private Node VMs");
1377 println!("============================");
1378 let port_restricted_cone_private_node_nat_gateway_map =
1379 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
1380 self.port_restricted_cone_private_node_vms
1381 .iter()
1382 .map(|node_vm| node_vm.vm.clone())
1383 .collect::<Vec<_>>()
1384 .as_slice(),
1385 &self.port_restricted_cone_nat_gateway_vms,
1386 )?;
1387
1388 for (node_vm, nat_gateway_vm) in
1389 port_restricted_cone_private_node_nat_gateway_map.iter()
1390 {
1391 println!(
1392 "{}: {} ==routed through==> {}: {}",
1393 node_vm.name,
1394 node_vm.public_ip_addr,
1395 nat_gateway_vm.name,
1396 nat_gateway_vm.public_ip_addr
1397 );
1398 let ssh = if let Some(ssh_key_path) = self.ssh_private_key_path.to_str() {
1399 format!(
1400 "ssh -i {ssh_key_path} -o ProxyCommand=\"ssh -W %h:%p root@{} -i {ssh_key_path}\" root@{}",
1401 nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
1402 )
1403 } else {
1404 format!(
1405 "ssh -o ProxyCommand=\"ssh -W %h:%p root@{}\" root@{}",
1406 nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
1407 )
1408 };
1409 println!("SSH using NAT gateway: {ssh}");
1410 }
1411 println!(
1412 "Nodes per VM: {}",
1413 self.port_restricted_cone_private_node_count()
1414 );
1415 println!("SSH user: {}", self.ssh_user);
1416 println!();
1417 }
1418
1419 if !self.symmetric_private_node_vms.is_empty() {
1420 println!("=================");
1421 println!("Symmetric Private Node VMs");
1422 println!("=================");
1423 let symmetric_private_node_nat_gateway_map =
1424 PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
1425 self.symmetric_private_node_vms
1426 .iter()
1427 .map(|node_vm| node_vm.vm.clone())
1428 .collect::<Vec<_>>()
1429 .as_slice(),
1430 &self.symmetric_nat_gateway_vms,
1431 )?;
1432
1433 for (node_vm, nat_gateway_vm) in symmetric_private_node_nat_gateway_map.iter() {
1434 println!(
1435 "{}: {} ==routed through==> {}: {}",
1436 node_vm.name,
1437 node_vm.public_ip_addr,
1438 nat_gateway_vm.name,
1439 nat_gateway_vm.public_ip_addr
1440 );
1441 let ssh = if let Some(ssh_key_path) = self.ssh_private_key_path.to_str() {
1442 format!(
1443 "ssh -i {ssh_key_path} -o ProxyCommand=\"ssh -W %h:%p root@{} -i {ssh_key_path}\" root@{}",
1444 nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
1445 )
1446 } else {
1447 format!(
1448 "ssh -o ProxyCommand=\"ssh -W %h:%p root@{}\" root@{}",
1449 nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
1450 )
1451 };
1452 println!("SSH using NAT gateway: {ssh}");
1453 }
1454 println!("Nodes per VM: {}", self.symmetric_private_node_count());
1455 println!("SSH user: {}", self.ssh_user);
1456 println!();
1457 }
1458
1459 if !self.upnp_private_node_vms.is_empty() {
1460 println!("================");
1461 println!("UPnP Private Node VMs");
1462 println!("================");
1463 for node_vm in self.upnp_private_node_vms.iter() {
1464 println!("{}: {}", node_vm.vm.name, node_vm.vm.public_ip_addr);
1465 }
1466 println!("Nodes per VM: {}", self.upnp_private_node_count());
1467 println!("SSH user: {}", self.ssh_user);
1468 println!();
1469 }
1470
1471 if !self.client_vms.is_empty() {
1472 println!("==========");
1473 println!("Client VMs");
1474 println!("==========");
1475 for client_vm in self.client_vms.iter() {
1476 println!("{}: {}", client_vm.vm.name, client_vm.vm.public_ip_addr);
1477 }
1478 println!();
1479
1480 println!("=============================");
1481 println!("Ant Client Wallet Public Keys");
1482 println!("=============================");
1483 for client_vm in self.client_vms.iter() {
1484 for (user, key) in client_vm.wallet_public_key.iter() {
1485 println!("{}@{}: {}", client_vm.vm.name, user, key);
1486 }
1487 }
1488 }
1489
1490 if !self.misc_vms.is_empty() {
1491 println!("=========");
1492 println!("Other VMs");
1493 println!("=========");
1494 }
1495 if !self.misc_vms.is_empty() {
1496 for vm in self.misc_vms.iter() {
1497 println!("{}: {}", vm.name, vm.public_ip_addr);
1498 }
1499 }
1500
1501 for nat_gateway_vm in self.full_cone_nat_gateway_vms.iter() {
1502 println!("{}: {}", nat_gateway_vm.name, nat_gateway_vm.public_ip_addr);
1503 }
1504
1505 for nat_gateway_vm in self.port_restricted_cone_nat_gateway_vms.iter() {
1506 println!("{}: {}", nat_gateway_vm.name, nat_gateway_vm.public_ip_addr);
1507 }
1508
1509 for nat_gateway_vm in self.symmetric_nat_gateway_vms.iter() {
1510 println!("{}: {}", nat_gateway_vm.name, nat_gateway_vm.public_ip_addr);
1511 }
1512
1513 println!("SSH user: {}", self.ssh_user);
1514 println!();
1515
1516 if full {
1517 println!("===============");
1518 println!("Full Peer List");
1519 println!("===============");
1520 let mut quic_listeners = Vec::new();
1521 let mut ws_listeners = Vec::new();
1522
1523 for node_vm in self
1524 .peer_cache_node_vms
1525 .iter()
1526 .chain(self.node_vms.iter())
1527 .chain(self.full_cone_private_node_vms.iter())
1528 .chain(self.port_restricted_cone_private_node_vms.iter())
1529 .chain(self.symmetric_private_node_vms.iter())
1530 .chain(self.upnp_private_node_vms.iter())
1531 {
1532 for addresses in &node_vm.node_listen_addresses {
1533 for addr in addresses {
1534 if !addr.starts_with("/ip4/127.0.0.1") && !addr.starts_with("/ip4/10.") {
1535 if addr.contains("/quic") {
1536 quic_listeners.push(addr.clone());
1537 } else if addr.contains("/ws") {
1538 ws_listeners.push(addr.clone());
1539 }
1540 }
1541 }
1542 }
1543 }
1544
1545 if !quic_listeners.is_empty() {
1546 println!("QUIC:");
1547 for addr in quic_listeners {
1548 println!(" {addr}");
1549 }
1550 println!();
1551 }
1552
1553 if !ws_listeners.is_empty() {
1554 println!("Websocket:");
1555 for addr in ws_listeners {
1556 println!(" {addr}");
1557 }
1558 println!();
1559 }
1560 } else {
1561 println!("============");
1562 println!("Sample Peers");
1563 println!("============");
1564 self.peer_cache_node_vms
1565 .iter()
1566 .chain(self.node_vms.iter())
1567 .chain(self.full_cone_private_node_vms.iter())
1568 .chain(self.port_restricted_cone_private_node_vms.iter())
1569 .chain(self.symmetric_private_node_vms.iter())
1570 .chain(self.upnp_private_node_vms.iter())
1571 .map(|node_vm| node_vm.vm.public_ip_addr.to_string())
1572 .for_each(|ip| {
1573 if let Some(peer) = self.peers().iter().find(|p| p.contains(&ip)) {
1574 println!("{peer}");
1575 }
1576 });
1577 }
1578 println!();
1579
1580 println!(
1581 "Genesis: {}",
1582 self.genesis_multiaddr
1583 .as_ref()
1584 .map_or("N/A", |genesis| genesis)
1585 );
1586 let inventory_file_path =
1587 get_data_directory()?.join(format!("{}-inventory.json", self.name));
1588 println!(
1589 "The full inventory is at {}",
1590 inventory_file_path.to_string_lossy()
1591 );
1592 println!();
1593
1594 if !self.uploaded_files.is_empty() {
1595 println!("Uploaded files:");
1596 for file in self.uploaded_files.iter() {
1597 println!("{}: {}", file.0, file.1);
1598 }
1599 }
1600
1601 if self
1602 .environment_details
1603 .evm_details
1604 .data_payments_address
1605 .is_some()
1606 || self
1607 .environment_details
1608 .evm_details
1609 .payment_token_address
1610 .is_some()
1611 || self.environment_details.evm_details.rpc_url.is_some()
1612 {
1613 println!("===========");
1614 println!("EVM Details");
1615 println!("===========");
1616 println!(
1617 "EVM data payments address: {}",
1618 self.environment_details
1619 .evm_details
1620 .data_payments_address
1621 .as_ref()
1622 .map_or("N/A", |addr| addr)
1623 );
1624 println!(
1625 "EVM payment token address: {}",
1626 self.environment_details
1627 .evm_details
1628 .payment_token_address
1629 .as_ref()
1630 .map_or("N/A", |addr| addr)
1631 );
1632 println!(
1633 "EVM RPC URL: {}",
1634 self.environment_details
1635 .evm_details
1636 .rpc_url
1637 .as_ref()
1638 .map_or("N/A", |addr| addr)
1639 );
1640 }
1641
1642 Ok(())
1643 }
1644
1645 pub fn get_genesis_ip(&self) -> Option<IpAddr> {
1646 self.misc_vms
1647 .iter()
1648 .find(|vm| vm.name.contains("genesis"))
1649 .map(|vm| vm.public_ip_addr)
1650 }
1651
1652 pub fn print_peer_cache_webserver(&self) {
1653 println!("=====================");
1654 println!("Peer Cache Webservers");
1655 println!("=====================");
1656
1657 for node_vm in &self.peer_cache_node_vms {
1658 let webserver = get_bootstrap_cache_url(&node_vm.vm.public_ip_addr);
1659 println!("{}: {webserver}", node_vm.vm.name);
1660 }
1661 }
1662}
1663
1664#[derive(Clone, Debug, Serialize, Deserialize)]
1665pub struct ClientsDeploymentInventory {
1666 pub binary_option: BinaryOption,
1667 pub client_vms: Vec<ClientVirtualMachine>,
1668 pub environment_type: EnvironmentType,
1669 pub evm_details: EvmDetails,
1670 pub funding_wallet_address: Option<String>,
1671 pub network_id: Option<u8>,
1672 pub failed_node_registry_vms: Vec<String>,
1673 pub name: String,
1674 pub region: String,
1675 pub ssh_user: String,
1676 pub ssh_private_key_path: PathBuf,
1677 pub uploaded_files: Vec<(String, String)>,
1678}
1679
1680impl ClientsDeploymentInventory {
1681 pub fn empty(
1684 name: &str,
1685 binary_option: BinaryOption,
1686 region: &str,
1687 ) -> ClientsDeploymentInventory {
1688 Self {
1689 binary_option,
1690 client_vms: Default::default(),
1691 environment_type: EnvironmentType::default(),
1692 evm_details: EvmDetails::default(),
1693 funding_wallet_address: None,
1694 network_id: None,
1695 failed_node_registry_vms: Default::default(),
1696 name: name.to_string(),
1697 region: region.to_string(),
1698 ssh_user: "root".to_string(),
1699 ssh_private_key_path: Default::default(),
1700 uploaded_files: Default::default(),
1701 }
1702 }
1703
1704 pub fn get_tfvars_filenames(&self) -> Vec<String> {
1705 debug!("Environment type: {:?}", self.environment_type);
1706 let filenames = self
1707 .environment_type
1708 .get_tfvars_filenames(&self.name, &self.region);
1709 debug!("Using tfvars files {filenames:?}");
1710 filenames
1711 }
1712
1713 pub fn is_empty(&self) -> bool {
1714 self.client_vms.is_empty()
1715 }
1716
1717 pub fn vm_list(&self) -> Vec<VirtualMachine> {
1718 self.client_vms
1719 .iter()
1720 .map(|client_vm| client_vm.vm.clone())
1721 .collect()
1722 }
1723
1724 pub fn save(&self) -> Result<()> {
1725 let path = get_data_directory()?.join(format!("{}-clients-inventory.json", self.name));
1726 let serialized_data = serde_json::to_string_pretty(self)?;
1727 let mut file = File::create(path)?;
1728 file.write_all(serialized_data.as_bytes())?;
1729 Ok(())
1730 }
1731
1732 pub fn read(file_path: &PathBuf) -> Result<Self> {
1733 let data = std::fs::read_to_string(file_path)?;
1734 let deserialized_data: ClientsDeploymentInventory = serde_json::from_str(&data)?;
1735 Ok(deserialized_data)
1736 }
1737
1738 pub fn add_uploaded_files(&mut self, uploaded_files: Vec<(String, String)>) {
1739 self.uploaded_files.extend_from_slice(&uploaded_files);
1740 }
1741
1742 pub fn print_report(&self) -> Result<()> {
1743 println!("*************************************");
1744 println!("* *");
1745 println!("* Clients Inventory Report *");
1746 println!("* *");
1747 println!("*************************************");
1748
1749 println!("Environment Name: {}", self.name);
1750 println!();
1751 match &self.binary_option {
1752 BinaryOption::BuildFromSource {
1753 repo_owner, branch, ..
1754 } => {
1755 println!("==============");
1756 println!("Branch Details");
1757 println!("==============");
1758 println!("Repo owner: {repo_owner}");
1759 println!("Branch name: {branch}");
1760 println!();
1761 }
1762 BinaryOption::Versioned { ant_version, .. } => {
1763 println!("===============");
1764 println!("Version Details");
1765 println!("===============");
1766 println!(
1767 "ant version: {}",
1768 ant_version
1769 .as_ref()
1770 .map_or("N/A".to_string(), |v| v.to_string())
1771 );
1772 println!();
1773 }
1774 }
1775
1776 if !self.client_vms.is_empty() {
1777 println!("==========");
1778 println!("Client VMs");
1779 println!("==========");
1780 for client_vm in self.client_vms.iter() {
1781 println!("{}: {}", client_vm.vm.name, client_vm.vm.public_ip_addr);
1782 }
1783 println!("SSH user: {}", self.ssh_user);
1784 println!();
1785
1786 println!("=============================");
1787 println!("Ant Client Wallet Public Keys");
1788 println!("=============================");
1789 for client_vm in self.client_vms.iter() {
1790 for (user, key) in client_vm.wallet_public_key.iter() {
1791 println!("{}@{}: {}", client_vm.vm.name, user, key);
1792 }
1793 }
1794 println!();
1795 }
1796
1797 if !self.uploaded_files.is_empty() {
1798 println!("==============");
1799 println!("Uploaded files");
1800 println!("==============");
1801 for file in self.uploaded_files.iter() {
1802 println!("{}: {}", file.0, file.1);
1803 }
1804 println!();
1805 }
1806
1807 if self.evm_details.data_payments_address.is_some()
1808 || self.evm_details.payment_token_address.is_some()
1809 || self.evm_details.rpc_url.is_some()
1810 {
1811 println!("===========");
1812 println!("EVM Details");
1813 println!("===========");
1814 println!(
1815 "EVM data payments address: {}",
1816 self.evm_details
1817 .data_payments_address
1818 .as_ref()
1819 .map_or("N/A", |addr| addr)
1820 );
1821 println!(
1822 "EVM payment token address: {}",
1823 self.evm_details
1824 .payment_token_address
1825 .as_ref()
1826 .map_or("N/A", |addr| addr)
1827 );
1828 println!(
1829 "EVM RPC URL: {}",
1830 self.evm_details.rpc_url.as_ref().map_or("N/A", |addr| addr)
1831 );
1832 println!();
1833 }
1834
1835 if let Some(funding_wallet_address) = &self.funding_wallet_address {
1836 println!("======================");
1837 println!("Funding Wallet Address");
1838 println!("======================");
1839 println!("{funding_wallet_address}");
1840 println!();
1841 }
1842
1843 if let Some(network_id) = &self.network_id {
1844 println!("==========");
1845 println!("Network ID");
1846 println!("==========");
1847 println!("{network_id}");
1848 println!();
1849 }
1850
1851 let inventory_file_path =
1852 get_data_directory()?.join(format!("{}-clients-inventory.json", self.name));
1853 println!(
1854 "The full Clients inventory is at {}",
1855 inventory_file_path.to_string_lossy()
1856 );
1857 println!();
1858
1859 Ok(())
1860 }
1861}
1862
1863pub fn get_data_directory() -> Result<PathBuf> {
1864 let path = dirs_next::data_dir()
1865 .ok_or_else(|| eyre!("Could not retrieve data directory"))?
1866 .join("autonomi")
1867 .join("testnet-deploy");
1868 if !path.exists() {
1869 std::fs::create_dir_all(path.clone())?;
1870 }
1871 Ok(path)
1872}