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