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