sn_testnet_deploy/
inventory.rs

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