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