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