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