sn_testnet_deploy/
inventory.rs

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