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