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