sn_testnet_deploy/
inventory.rs

1// Copyright (c) 2023, MaidSafe.
2// All rights reserved.
3//
4// This SAFE Network Software is licensed under the BSD-3-Clause license.
5// Please see the LICENSE file for more details.
6
7use crate::{
8    ansible::{
9        inventory::{
10            generate_environment_inventory,
11            generate_full_cone_private_node_static_environment_inventory,
12            generate_symmetric_private_node_static_environment_inventory, AnsibleInventoryType,
13        },
14        provisioning::{AnsibleProvisioner, PrivateNodeProvisionInventory},
15        AnsibleRunner,
16    },
17    get_bootstrap_cache_url, get_environment_details, get_genesis_multiaddr,
18    s3::S3Repository,
19    ssh::SshClient,
20    terraform::TerraformRunner,
21    BinaryOption, CloudProvider, DeploymentType, EnvironmentDetails, Error, TestnetDeployer,
22};
23use alloy::hex::ToHexExt;
24use ant_service_management::{NodeRegistry, ServiceStatus};
25use color_eyre::{eyre::eyre, Result};
26use log::debug;
27use rand::seq::{IteratorRandom, SliceRandom};
28use semver::Version;
29use serde::{Deserialize, Serialize};
30use std::{
31    collections::{HashMap, HashSet},
32    convert::From,
33    fs::File,
34    io::Write,
35    net::{IpAddr, SocketAddr},
36    path::PathBuf,
37};
38
39const DEFAULT_CONTACTS_COUNT: usize = 100;
40const UNAVAILABLE_NODE: &str = "-";
41const TESTNET_BUCKET_NAME: &str = "sn-testnet";
42
43pub struct DeploymentInventoryService {
44    pub ansible_runner: AnsibleRunner,
45    // It may seem strange to have both the runner and the provisioner, because the provisioner is
46    // a wrapper around the runner, but it's for the purpose of sharing some code. More things
47    // could go into the provisioner later, which may eliminate the need to have the runner.
48    pub ansible_provisioner: AnsibleProvisioner,
49    pub cloud_provider: CloudProvider,
50    pub inventory_file_path: PathBuf,
51    pub s3_repository: S3Repository,
52    pub ssh_client: SshClient,
53    pub terraform_runner: TerraformRunner,
54    pub working_directory_path: PathBuf,
55}
56
57impl From<&TestnetDeployer> for DeploymentInventoryService {
58    fn from(item: &TestnetDeployer) -> Self {
59        let provider = match item.cloud_provider {
60            CloudProvider::Aws => "aws",
61            CloudProvider::DigitalOcean => "digital_ocean",
62        };
63        DeploymentInventoryService {
64            ansible_runner: item.ansible_provisioner.ansible_runner.clone(),
65            ansible_provisioner: item.ansible_provisioner.clone(),
66            cloud_provider: item.cloud_provider,
67            inventory_file_path: item
68                .working_directory_path
69                .join("ansible")
70                .join("inventory")
71                .join(format!("dev_inventory_{}.yml", provider)),
72            s3_repository: item.s3_repository.clone(),
73            ssh_client: item.ssh_client.clone(),
74            terraform_runner: item.terraform_runner.clone(),
75            working_directory_path: item.working_directory_path.clone(),
76        }
77    }
78}
79
80impl DeploymentInventoryService {
81    /// Generate or retrieve the inventory for the deployment.
82    ///
83    /// If we're creating a new environment and there is no inventory yet, a empty inventory will
84    /// be returned; otherwise the inventory will represent what is deployed currently.
85    ///
86    /// The `force` flag is used when the `deploy` command runs, to make sure that a new inventory
87    /// is generated, because it's possible that an old one with the same environment name has been
88    /// cached.
89    ///
90    /// The binary option will only be present on the first generation of the inventory, when the
91    /// testnet is initially deployed. On any subsequent runs, we don't have access to the initial
92    /// launch arguments. This means any branch specification is lost. In this case, we'll just
93    /// retrieve the version numbers from the genesis node in the node registry. Most of the time
94    /// it is the version numbers that will be of interest.
95    pub async fn generate_or_retrieve_inventory(
96        &self,
97        name: &str,
98        force: bool,
99        binary_option: Option<BinaryOption>,
100    ) -> Result<DeploymentInventory> {
101        println!("======================================");
102        println!("  Generating or Retrieving Inventory  ");
103        println!("======================================");
104        let inventory_path = get_data_directory()?.join(format!("{name}-inventory.json"));
105        if inventory_path.exists() && !force {
106            let inventory = DeploymentInventory::read(&inventory_path)?;
107            return Ok(inventory);
108        }
109
110        // This allows for the inventory to be generated without a Terraform workspace to be
111        // initialised, which is the case in the workflow for printing an inventory.
112        if !force {
113            let environments = self.terraform_runner.workspace_list()?;
114            if !environments.contains(&name.to_string()) {
115                return Err(eyre!("The '{}' environment does not exist", name));
116            }
117        }
118
119        // For new environments, whether it's a new or bootstrap deploy, the inventory files need
120        // to be generated for the Ansible run to work correctly.
121        //
122        // It is an idempotent operation; the files won't be generated if they already exist.
123        let output_inventory_dir_path = self
124            .working_directory_path
125            .join("ansible")
126            .join("inventory");
127        generate_environment_inventory(
128            name,
129            &self.inventory_file_path,
130            &output_inventory_dir_path,
131        )?;
132
133        let environment_details = match get_environment_details(name, &self.s3_repository).await {
134            Ok(details) => details,
135            Err(Error::EnvironmentDetailsNotFound(_)) => {
136                println!("Environment details not found: treating this as a new deployment");
137                return Ok(DeploymentInventory::empty(
138                    name,
139                    binary_option.ok_or_else(|| {
140                        eyre!("For a new deployment the binary option must be set")
141                    })?,
142                ));
143            }
144            Err(e) => return Err(e.into()),
145        };
146
147        let genesis_vm = self
148            .ansible_runner
149            .get_inventory(AnsibleInventoryType::Genesis, false)?;
150
151        let mut misc_vms = Vec::new();
152        let build_vm = self
153            .ansible_runner
154            .get_inventory(AnsibleInventoryType::Build, false)?;
155        misc_vms.extend(build_vm);
156
157        let full_cone_nat_gateway_vms = self
158            .ansible_runner
159            .get_inventory(AnsibleInventoryType::FullConeNatGateway, false)?;
160        let full_cone_private_node_vms = self
161            .ansible_runner
162            .get_inventory(AnsibleInventoryType::FullConePrivateNodes, false)?;
163
164        let symmetric_nat_gateway_vms = self
165            .ansible_runner
166            .get_inventory(AnsibleInventoryType::SymmetricNatGateway, false)?;
167        let symmetric_private_node_vms = self
168            .ansible_runner
169            .get_inventory(AnsibleInventoryType::SymmetricPrivateNodes, false)?;
170
171        let generic_node_vms = self
172            .ansible_runner
173            .get_inventory(AnsibleInventoryType::Nodes, false)?;
174
175        // Create static inventory for private nodes. Will be used during ansible-playbook run.
176        generate_full_cone_private_node_static_environment_inventory(
177            name,
178            &output_inventory_dir_path,
179            &full_cone_private_node_vms,
180            &full_cone_nat_gateway_vms,
181            &self.ssh_client.private_key_path,
182        )?;
183        generate_symmetric_private_node_static_environment_inventory(
184            name,
185            &output_inventory_dir_path,
186            &symmetric_private_node_vms,
187            &symmetric_nat_gateway_vms,
188            &self.ssh_client.private_key_path,
189        )?;
190
191        // Set up the SSH client to route through the NAT gateway if it exists. This updates all the client clones.
192        if !symmetric_nat_gateway_vms.is_empty() {
193            self.ssh_client.set_symmetric_nat_routed_vms(
194                &symmetric_private_node_vms,
195                &symmetric_nat_gateway_vms,
196            )?;
197        }
198        if !full_cone_nat_gateway_vms.is_empty() {
199            self.ssh_client.set_full_cone_nat_routed_vms(
200                &full_cone_private_node_vms,
201                &full_cone_nat_gateway_vms,
202            )?;
203        }
204
205        let peer_cache_node_vms = self
206            .ansible_runner
207            .get_inventory(AnsibleInventoryType::PeerCacheNodes, false)?;
208
209        let uploader_vms = if environment_details.deployment_type != DeploymentType::Bootstrap {
210            let uploader_and_sks = self.ansible_provisioner.get_uploader_secret_keys()?;
211            uploader_and_sks
212                .iter()
213                .map(|(vm, sks)| UploaderVirtualMachine {
214                    vm: vm.clone(),
215                    wallet_public_key: sks
216                        .iter()
217                        .enumerate()
218                        .map(|(user, sk)| (format!("safe{}", user + 1), sk.address().encode_hex()))
219                        .collect(),
220                })
221                .collect()
222        } else {
223            Vec::new()
224        };
225
226        println!("Retrieving node registries from all VMs...");
227        let mut failed_node_registry_vms = Vec::new();
228
229        let peer_cache_node_registries = self
230            .ansible_provisioner
231            .get_node_registries(&AnsibleInventoryType::PeerCacheNodes)?;
232        let peer_cache_node_vms =
233            NodeVirtualMachine::from_list(&peer_cache_node_vms, &peer_cache_node_registries);
234
235        let generic_node_registries = self
236            .ansible_provisioner
237            .get_node_registries(&AnsibleInventoryType::Nodes)?;
238        let generic_node_vms =
239            NodeVirtualMachine::from_list(&generic_node_vms, &generic_node_registries);
240
241        let symmetric_private_node_registries = self
242            .ansible_provisioner
243            .get_node_registries(&AnsibleInventoryType::SymmetricPrivateNodes)?;
244        let symmetric_private_node_vms = NodeVirtualMachine::from_list(
245            &symmetric_private_node_vms,
246            &symmetric_private_node_registries,
247        );
248        let full_cone_private_node_registries = self
249            .ansible_provisioner
250            .get_node_registries(&AnsibleInventoryType::FullConePrivateNodes)?;
251        debug!("full_cone_private_node_vms: {full_cone_private_node_vms:?}");
252        let full_cone_private_node_gateway_vm_map =
253            PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
254                &full_cone_private_node_vms,
255                &full_cone_nat_gateway_vms,
256            )?;
257        debug!("full_cone_private_node_gateway_vm_map: {full_cone_private_node_gateway_vm_map:?}");
258        let full_cone_private_node_vms = NodeVirtualMachine::from_list(
259            &full_cone_private_node_vms,
260            &full_cone_private_node_registries,
261        );
262
263        let genesis_node_registry = self
264            .ansible_provisioner
265            .get_node_registries(&AnsibleInventoryType::Genesis)?;
266        let genesis_vm = NodeVirtualMachine::from_list(&genesis_vm, &genesis_node_registry);
267        let genesis_vm = if !genesis_vm.is_empty() {
268            Some(genesis_vm[0].clone())
269        } else {
270            None
271        };
272
273        failed_node_registry_vms.extend(peer_cache_node_registries.failed_vms);
274        failed_node_registry_vms.extend(generic_node_registries.failed_vms);
275        failed_node_registry_vms.extend(full_cone_private_node_registries.failed_vms);
276        failed_node_registry_vms.extend(symmetric_private_node_registries.failed_vms);
277        failed_node_registry_vms.extend(genesis_node_registry.failed_vms);
278
279        let binary_option = if let Some(binary_option) = binary_option {
280            binary_option
281        } else {
282            let (antnode_version, antctl_version) = {
283                let mut random_vm = None;
284                if !generic_node_vms.is_empty() {
285                    random_vm = generic_node_vms.first().cloned();
286                } else if !peer_cache_node_vms.is_empty() {
287                    random_vm = peer_cache_node_vms.first().cloned();
288                } else if genesis_vm.is_some() {
289                    random_vm = genesis_vm.clone()
290                };
291
292                let Some(random_vm) = random_vm else {
293                    return Err(eyre!("Unable to obtain a VM to retrieve versions"));
294                };
295
296                // It's reasonable to make the assumption that one antnode service is running.
297                let antnode_version = self.get_bin_version(
298                    &random_vm.vm,
299                    "/mnt/antnode-storage/data/antnode1/antnode --version",
300                    "Autonomi Node v",
301                )?;
302                let antctl_version = self.get_bin_version(
303                    &random_vm.vm,
304                    "antctl --version",
305                    "Autonomi Node Manager v",
306                )?;
307                (antnode_version, antctl_version)
308            };
309
310            let ant_version = if environment_details.deployment_type != DeploymentType::Bootstrap {
311                let random_uploader_vm = uploader_vms
312                    .choose(&mut rand::thread_rng())
313                    .ok_or_else(|| eyre!("No uploader VMs available to retrieve ant version"))?;
314                Some(self.get_bin_version(
315                    &random_uploader_vm.vm,
316                    "ant --version",
317                    "Autonomi Client v",
318                )?)
319            } else {
320                None
321            };
322
323            println!("Retrieved binary versions from previous deployment:");
324            println!("  antnode: {}", antnode_version);
325            println!("  antctl: {}", antctl_version);
326            if let Some(version) = &ant_version {
327                println!("  ant: {}", version);
328            }
329
330            BinaryOption::Versioned {
331                ant_version,
332                antnode_version,
333                antctl_version,
334            }
335        };
336
337        let (genesis_multiaddr, genesis_ip) =
338            if environment_details.deployment_type == DeploymentType::New {
339                match get_genesis_multiaddr(&self.ansible_runner, &self.ssh_client) {
340                    Ok((multiaddr, ip)) => (Some(multiaddr), Some(ip)),
341                    Err(_) => (None, None),
342                }
343            } else {
344                (None, None)
345            };
346        let inventory = DeploymentInventory {
347            binary_option,
348            environment_details,
349            failed_node_registry_vms,
350            faucet_address: genesis_ip.map(|ip| format!("{ip}:8000")),
351            full_cone_nat_gateway_vms,
352            full_cone_private_node_vms,
353            genesis_multiaddr,
354            genesis_vm,
355            name: name.to_string(),
356            misc_vms,
357            node_vms: generic_node_vms,
358            peer_cache_node_vms,
359            ssh_user: self.cloud_provider.get_ssh_user(),
360            ssh_private_key_path: self.ssh_client.private_key_path.clone(),
361            symmetric_nat_gateway_vms,
362            symmetric_private_node_vms,
363            uploaded_files: Vec::new(),
364            uploader_vms,
365        };
366        debug!("Inventory: {inventory:?}");
367        Ok(inventory)
368    }
369
370    /// Create all the environment inventory files. This also updates the SSH client to route the private nodes
371    /// the NAT gateway if it exists.
372    ///
373    /// This is used when 'generate_or_retrieve_inventory' is not used, but you still need to set up the inventory files.
374    pub fn setup_environment_inventory(&self, name: &str) -> Result<()> {
375        let output_inventory_dir_path = self
376            .working_directory_path
377            .join("ansible")
378            .join("inventory");
379        generate_environment_inventory(
380            name,
381            &self.inventory_file_path,
382            &output_inventory_dir_path,
383        )?;
384
385        let full_cone_nat_gateway_vms = self
386            .ansible_runner
387            .get_inventory(AnsibleInventoryType::FullConeNatGateway, false)?;
388        let full_cone_private_node_vms = self
389            .ansible_runner
390            .get_inventory(AnsibleInventoryType::FullConePrivateNodes, false)?;
391
392        let symmetric_nat_gateway_vms = self
393            .ansible_runner
394            .get_inventory(AnsibleInventoryType::SymmetricNatGateway, false)?;
395        let symmetric_private_node_vms = self
396            .ansible_runner
397            .get_inventory(AnsibleInventoryType::SymmetricPrivateNodes, false)?;
398
399        // Create static inventory for private nodes. Will be used during ansible-playbook run.
400        generate_symmetric_private_node_static_environment_inventory(
401            name,
402            &output_inventory_dir_path,
403            &symmetric_private_node_vms,
404            &symmetric_nat_gateway_vms,
405            &self.ssh_client.private_key_path,
406        )?;
407
408        generate_full_cone_private_node_static_environment_inventory(
409            name,
410            &output_inventory_dir_path,
411            &full_cone_private_node_vms,
412            &full_cone_nat_gateway_vms,
413            &self.ssh_client.private_key_path,
414        )?;
415
416        // Set up the SSH client to route through the NAT gateway if it exists. This updates all the client clones.
417        if !full_cone_nat_gateway_vms.is_empty() {
418            self.ssh_client.set_full_cone_nat_routed_vms(
419                &full_cone_private_node_vms,
420                &full_cone_nat_gateway_vms,
421            )?;
422        }
423
424        if !symmetric_nat_gateway_vms.is_empty() {
425            self.ssh_client.set_symmetric_nat_routed_vms(
426                &symmetric_private_node_vms,
427                &symmetric_nat_gateway_vms,
428            )?;
429        }
430
431        Ok(())
432    }
433
434    pub async fn upload_network_contacts(
435        &self,
436        inventory: &DeploymentInventory,
437        contacts_file_name: Option<String>,
438    ) -> Result<()> {
439        let temp_dir_path = tempfile::tempdir()?.into_path();
440        let temp_file_path = if let Some(file_name) = contacts_file_name {
441            temp_dir_path.join(file_name)
442        } else {
443            temp_dir_path.join(inventory.name.clone())
444        };
445
446        let mut file = std::fs::File::create(&temp_file_path)?;
447        let mut rng = rand::thread_rng();
448
449        let peer_cache_peers = inventory
450            .peer_cache_node_vms
451            .iter()
452            .flat_map(|vm| vm.get_quic_addresses())
453            .collect::<Vec<_>>();
454        let peer_cache_peers_len = peer_cache_peers.len();
455        for peer in peer_cache_peers
456            .iter()
457            .filter(|&peer| peer != UNAVAILABLE_NODE)
458            .cloned()
459            .choose_multiple(&mut rng, DEFAULT_CONTACTS_COUNT)
460        {
461            writeln!(file, "{peer}",)?;
462        }
463
464        if DEFAULT_CONTACTS_COUNT > peer_cache_peers_len {
465            let node_peers = inventory
466                .node_vms
467                .iter()
468                .flat_map(|vm| vm.get_quic_addresses())
469                .collect::<Vec<_>>();
470            for peer in node_peers
471                .iter()
472                .filter(|&peer| peer != UNAVAILABLE_NODE)
473                .cloned()
474                .choose_multiple(&mut rng, DEFAULT_CONTACTS_COUNT - peer_cache_peers_len)
475            {
476                writeln!(file, "{peer}",)?;
477            }
478        }
479
480        self.s3_repository
481            .upload_file(TESTNET_BUCKET_NAME, &temp_file_path, true)
482            .await?;
483
484        Ok(())
485    }
486
487    /// Connects to a VM with SSH and runs a command to retrieve the version of a binary.
488    fn get_bin_version(&self, vm: &VirtualMachine, command: &str, prefix: &str) -> Result<Version> {
489        let output = self.ssh_client.run_command(
490            &vm.public_ip_addr,
491            &self.cloud_provider.get_ssh_user(),
492            command,
493            true,
494        )?;
495        let version_line = output
496            .first()
497            .ok_or_else(|| eyre!("No output from {} command", command))?;
498        let version_str = version_line
499            .strip_prefix(prefix)
500            .ok_or_else(|| eyre!("Unexpected output format from {} command", command))?;
501        Version::parse(version_str).map_err(|e| eyre!("Failed to parse {} version: {}", command, e))
502    }
503}
504
505impl NodeVirtualMachine {
506    pub fn from_list(
507        vms: &[VirtualMachine],
508        node_registries: &DeploymentNodeRegistries,
509    ) -> Vec<Self> {
510        let mut node_vms = Vec::new();
511        for vm in vms {
512            let node_registry = node_registries
513                .retrieved_registries
514                .iter()
515                .find(|(name, _)| {
516                    if vm.name.contains("private") {
517                        let result = name == &vm.private_ip_addr.to_string();
518                        debug!(
519                            "Vm name: {name} is a private node with result {result}. Vm: {vm:?}"
520                        );
521                        result
522                    } else {
523                        name == &vm.name
524                    }
525                })
526                .map(|(_, reg)| reg);
527            let Some(node_registry) = node_registry else {
528                debug!("No node registry found for vm: {vm:?}. Skipping");
529                continue;
530            };
531
532            let node_vm = Self {
533                node_count: node_registry.nodes.len(),
534                node_listen_addresses: node_registry
535                    .nodes
536                    .iter()
537                    .map(|node| {
538                        if let Some(listen_addresses) = &node.listen_addr {
539                            listen_addresses
540                                .iter()
541                                .map(|addr| addr.to_string())
542                                .collect()
543                        } else {
544                            vec![UNAVAILABLE_NODE.to_string()]
545                        }
546                    })
547                    .collect(),
548                rpc_endpoint: node_registry
549                    .nodes
550                    .iter()
551                    .map(|node| {
552                        let id = if let Some(peer_id) = node.peer_id {
553                            peer_id.to_string().clone()
554                        } else {
555                            UNAVAILABLE_NODE.to_string()
556                        };
557                        (id, node.rpc_socket_addr)
558                    })
559                    .collect(),
560                safenodemand_endpoint: node_registry
561                    .daemon
562                    .as_ref()
563                    .and_then(|daemon| daemon.endpoint),
564                vm: vm.clone(),
565            };
566            node_vms.push(node_vm);
567        }
568        debug!("Node VMs generated from NodeRegistries: {node_vms:?}");
569        node_vms
570    }
571
572    pub fn get_quic_addresses(&self) -> Vec<String> {
573        self.node_listen_addresses
574            .iter()
575            .map(|addresses| {
576                addresses
577                    .iter()
578                    .find(|addr| {
579                        addr.contains("/quic-v1")
580                            && !addr.starts_with("/ip4/127.0.0.1")
581                            && !addr.starts_with("/ip4/10.")
582                    })
583                    .map(|s| s.to_string())
584                    .unwrap_or_else(|| UNAVAILABLE_NODE.to_string())
585            })
586            .collect()
587    }
588}
589
590/// The name of the OS user.
591pub type OsUser = String;
592
593#[derive(Clone, Debug, Serialize, Deserialize)]
594pub struct UploaderVirtualMachine {
595    pub vm: VirtualMachine,
596    /// The public key of the wallet for each OS user (1 uploader instance per OS user).
597    pub wallet_public_key: HashMap<OsUser, String>,
598}
599
600#[derive(Clone, Debug, Serialize, Deserialize)]
601pub struct NodeVirtualMachine {
602    pub vm: VirtualMachine,
603    pub node_count: usize,
604    pub node_listen_addresses: Vec<Vec<String>>,
605    pub rpc_endpoint: HashMap<String, SocketAddr>,
606    pub safenodemand_endpoint: Option<SocketAddr>,
607}
608
609#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
610pub struct VirtualMachine {
611    pub id: u64,
612    pub name: String,
613    pub public_ip_addr: IpAddr,
614    pub private_ip_addr: IpAddr,
615}
616
617#[derive(Clone)]
618pub struct DeploymentNodeRegistries {
619    pub inventory_type: AnsibleInventoryType,
620    /// The (name, NodeRegistry) pairs for each VM that was successfully retrieved.
621    /// Note: for private nodes, the name is set to the private address of the VM.
622    pub retrieved_registries: Vec<(String, NodeRegistry)>,
623    pub failed_vms: Vec<String>,
624}
625
626impl DeploymentNodeRegistries {
627    pub fn print(&self) {
628        Self::print_banner(&self.inventory_type.to_string());
629        for (vm_name, registry) in self.retrieved_registries.iter() {
630            println!("{vm_name}:");
631            for node in registry.nodes.iter() {
632                println!(
633                    "  {}: {} {}",
634                    node.service_name,
635                    node.version,
636                    Self::format_status(&node.status)
637                );
638            }
639        }
640        if !self.failed_vms.is_empty() {
641            println!(
642                "Failed to retrieve node registries for {}:",
643                self.inventory_type
644            );
645            for vm_name in self.failed_vms.iter() {
646                println!("- {}", vm_name);
647            }
648        }
649    }
650
651    fn format_status(status: &ServiceStatus) -> String {
652        match status {
653            ServiceStatus::Running => "RUNNING".to_string(),
654            ServiceStatus::Stopped => "STOPPED".to_string(),
655            ServiceStatus::Added => "ADDED".to_string(),
656            ServiceStatus::Removed => "REMOVED".to_string(),
657        }
658    }
659
660    fn print_banner(text: &str) {
661        let padding = 2;
662        let text_width = text.len() + padding * 2;
663        let border_chars = 2;
664        let total_width = text_width + border_chars;
665        let top_bottom = "═".repeat(total_width);
666
667        println!("╔{}╗", top_bottom);
668        println!("║ {:^width$} ║", text, width = text_width);
669        println!("╚{}╝", top_bottom);
670    }
671}
672
673#[derive(Clone, Debug, Serialize, Deserialize)]
674pub struct DeploymentInventory {
675    pub binary_option: BinaryOption,
676    pub environment_details: EnvironmentDetails,
677    pub failed_node_registry_vms: Vec<String>,
678    pub faucet_address: Option<String>,
679    pub full_cone_nat_gateway_vms: Vec<VirtualMachine>,
680    pub full_cone_private_node_vms: Vec<NodeVirtualMachine>,
681    pub genesis_vm: Option<NodeVirtualMachine>,
682    pub genesis_multiaddr: Option<String>,
683    pub misc_vms: Vec<VirtualMachine>,
684    pub name: String,
685    pub node_vms: Vec<NodeVirtualMachine>,
686    pub peer_cache_node_vms: Vec<NodeVirtualMachine>,
687    pub ssh_user: String,
688    pub ssh_private_key_path: PathBuf,
689    pub symmetric_nat_gateway_vms: Vec<VirtualMachine>,
690    pub symmetric_private_node_vms: Vec<NodeVirtualMachine>,
691    pub uploaded_files: Vec<(String, String)>,
692    pub uploader_vms: Vec<UploaderVirtualMachine>,
693}
694
695impl DeploymentInventory {
696    /// Create an inventory for a new deployment which is initially empty, other than the name and
697    /// binary option, which will have been selected.
698    pub fn empty(name: &str, binary_option: BinaryOption) -> DeploymentInventory {
699        Self {
700            binary_option,
701            environment_details: EnvironmentDetails::default(),
702            genesis_vm: Default::default(),
703            genesis_multiaddr: Default::default(),
704            failed_node_registry_vms: Default::default(),
705            faucet_address: Default::default(),
706            full_cone_nat_gateway_vms: Default::default(),
707            full_cone_private_node_vms: Default::default(),
708            misc_vms: Default::default(),
709            name: name.to_string(),
710            node_vms: Default::default(),
711            peer_cache_node_vms: Default::default(),
712            ssh_user: "root".to_string(),
713            ssh_private_key_path: Default::default(),
714            symmetric_nat_gateway_vms: Default::default(),
715            symmetric_private_node_vms: Default::default(),
716            uploaded_files: Default::default(),
717            uploader_vms: Default::default(),
718        }
719    }
720
721    pub fn get_tfvars_filename(&self) -> String {
722        let filename = self
723            .environment_details
724            .environment_type
725            .get_tfvars_filename(&self.name);
726        debug!("Using tfvars file {filename}",);
727        filename
728    }
729
730    pub fn is_empty(&self) -> bool {
731        self.peer_cache_node_vms.is_empty() && self.node_vms.is_empty()
732    }
733
734    pub fn vm_list(&self) -> Vec<VirtualMachine> {
735        let mut list = Vec::new();
736        list.extend(self.symmetric_nat_gateway_vms.clone());
737        list.extend(self.full_cone_nat_gateway_vms.clone());
738        list.extend(
739            self.peer_cache_node_vms
740                .iter()
741                .map(|node_vm| node_vm.vm.clone()),
742        );
743        list.extend(self.genesis_vm.iter().map(|node_vm| node_vm.vm.clone()));
744        list.extend(self.node_vms.iter().map(|node_vm| node_vm.vm.clone()));
745        list.extend(self.misc_vms.clone());
746        list.extend(
747            self.symmetric_private_node_vms
748                .iter()
749                .map(|node_vm| node_vm.vm.clone()),
750        );
751        list.extend(
752            self.full_cone_private_node_vms
753                .iter()
754                .map(|node_vm| node_vm.vm.clone()),
755        );
756        list.extend(
757            self.uploader_vms
758                .iter()
759                .map(|uploader_vm| uploader_vm.vm.clone()),
760        );
761        list
762    }
763
764    pub fn node_vm_list(&self) -> Vec<NodeVirtualMachine> {
765        let mut list = Vec::new();
766        list.extend(self.peer_cache_node_vms.iter().cloned());
767        list.extend(self.genesis_vm.iter().cloned());
768        list.extend(self.node_vms.iter().cloned());
769        list.extend(self.full_cone_private_node_vms.iter().cloned());
770        list.extend(self.symmetric_private_node_vms.iter().cloned());
771
772        list
773    }
774
775    pub fn peers(&self) -> HashSet<String> {
776        let mut list = HashSet::new();
777        list.extend(
778            self.peer_cache_node_vms
779                .iter()
780                .flat_map(|node_vm| node_vm.get_quic_addresses()),
781        );
782        list.extend(
783            self.genesis_vm
784                .iter()
785                .flat_map(|node_vm| node_vm.get_quic_addresses()),
786        );
787        list.extend(
788            self.node_vms
789                .iter()
790                .flat_map(|node_vm| node_vm.get_quic_addresses()),
791        );
792        list.extend(
793            self.full_cone_private_node_vms
794                .iter()
795                .flat_map(|node_vm| node_vm.get_quic_addresses()),
796        );
797        list.extend(
798            self.symmetric_private_node_vms
799                .iter()
800                .flat_map(|node_vm| node_vm.get_quic_addresses()),
801        );
802        list
803    }
804
805    pub fn save(&self) -> Result<()> {
806        let path = get_data_directory()?.join(format!("{}-inventory.json", self.name));
807        let serialized_data = serde_json::to_string_pretty(self)?;
808        let mut file = File::create(path)?;
809        file.write_all(serialized_data.as_bytes())?;
810        Ok(())
811    }
812
813    pub fn read(file_path: &PathBuf) -> Result<Self> {
814        let data = std::fs::read_to_string(file_path)?;
815        let deserialized_data: DeploymentInventory = serde_json::from_str(&data)?;
816        Ok(deserialized_data)
817    }
818
819    pub fn add_uploaded_files(&mut self, uploaded_files: Vec<(String, String)>) {
820        self.uploaded_files.extend_from_slice(&uploaded_files);
821    }
822
823    pub fn get_random_peer(&self) -> Option<String> {
824        let mut rng = rand::thread_rng();
825        self.peers().into_iter().choose(&mut rng)
826    }
827
828    pub fn peer_cache_node_count(&self) -> usize {
829        if let Some(first_vm) = self.peer_cache_node_vms.first() {
830            first_vm.node_count
831        } else {
832            0
833        }
834    }
835
836    pub fn genesis_node_count(&self) -> usize {
837        if let Some(genesis_vm) = &self.genesis_vm {
838            genesis_vm.node_count
839        } else {
840            0
841        }
842    }
843
844    pub fn node_count(&self) -> usize {
845        if let Some(first_vm) = self.node_vms.first() {
846            first_vm.node_count
847        } else {
848            0
849        }
850    }
851
852    pub fn full_cone_private_node_count(&self) -> usize {
853        if let Some(first_vm) = self.full_cone_private_node_vms.first() {
854            first_vm.node_count
855        } else {
856            0
857        }
858    }
859
860    pub fn symmetric_private_node_count(&self) -> usize {
861        if let Some(first_vm) = self.symmetric_private_node_vms.first() {
862            first_vm.node_count
863        } else {
864            0
865        }
866    }
867
868    pub fn print_report(&self, full: bool) -> Result<()> {
869        println!("**************************************");
870        println!("*                                    *");
871        println!("*          Inventory Report          *");
872        println!("*                                    *");
873        println!("**************************************");
874
875        println!("Environment Name: {}", self.name);
876        println!();
877        match &self.binary_option {
878            BinaryOption::BuildFromSource {
879                repo_owner, branch, ..
880            } => {
881                println!("==============");
882                println!("Branch Details");
883                println!("==============");
884                println!("Repo owner: {repo_owner}");
885                println!("Branch name: {branch}");
886                println!();
887            }
888            BinaryOption::Versioned {
889                ant_version: safe_version,
890                antnode_version: safenode_version,
891                antctl_version: safenode_manager_version,
892            } => {
893                println!("===============");
894                println!("Version Details");
895                println!("===============");
896                println!(
897                    "safe version: {}",
898                    safe_version
899                        .as_ref()
900                        .map_or("N/A".to_string(), |v| v.to_string())
901                );
902                println!("safenode version: {}", safenode_version);
903                println!("safenode-manager version: {}", safenode_manager_version);
904                println!();
905            }
906        }
907
908        if !self.peer_cache_node_vms.is_empty() {
909            println!("==============");
910            println!("Peer Cache VMs");
911            println!("==============");
912            for node_vm in self.peer_cache_node_vms.iter() {
913                println!("{}: {}", node_vm.vm.name, node_vm.vm.public_ip_addr);
914            }
915            println!("Nodes per VM: {}", self.peer_cache_node_count());
916            println!("SSH user: {}", self.ssh_user);
917            println!();
918
919            self.print_peer_cache_webserver();
920        }
921
922        println!("========");
923        println!("Node VMs");
924        println!("========");
925        if let Some(genesis_vm) = &self.genesis_vm {
926            println!("{}: {}", genesis_vm.vm.name, genesis_vm.vm.public_ip_addr);
927        }
928        for node_vm in self.node_vms.iter() {
929            println!("{}: {}", node_vm.vm.name, node_vm.vm.public_ip_addr);
930        }
931        println!("Nodes per VM: {}", self.node_count());
932        println!("SSH user: {}", self.ssh_user);
933        println!();
934
935        println!("=================");
936        println!("Full Cone Private Node VMs");
937        println!("=================");
938        let full_cone_private_node_nat_gateway_map =
939            PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
940                self.full_cone_private_node_vms
941                    .iter()
942                    .map(|node_vm| node_vm.vm.clone())
943                    .collect::<Vec<_>>()
944                    .as_slice(),
945                &self.full_cone_nat_gateway_vms,
946            )?;
947
948        for (node_vm, nat_gateway_vm) in full_cone_private_node_nat_gateway_map.iter() {
949            println!(
950                "{}: {} ==routed through==> {}: {}",
951                node_vm.name,
952                node_vm.public_ip_addr,
953                nat_gateway_vm.name,
954                nat_gateway_vm.public_ip_addr
955            );
956            let ssh = if let Some(ssh_key_path) = self.ssh_private_key_path.to_str() {
957                format!(
958                    "ssh -i {ssh_key_path} root@{}",
959                    nat_gateway_vm.public_ip_addr,
960                )
961            } else {
962                format!("ssh root@{}", nat_gateway_vm.public_ip_addr,)
963            };
964            println!("SSH using NAT gateway: {ssh}");
965        }
966        println!("Nodes per VM: {}", self.node_count());
967        println!("SSH user: {}", self.ssh_user);
968        println!();
969
970        println!("=================");
971        println!("Symmetric Private Node VMs");
972        println!("=================");
973        let symmetric_private_node_nat_gateway_map =
974            PrivateNodeProvisionInventory::match_private_node_vm_and_gateway_vm(
975                self.symmetric_private_node_vms
976                    .iter()
977                    .map(|node_vm| node_vm.vm.clone())
978                    .collect::<Vec<_>>()
979                    .as_slice(),
980                &self.symmetric_nat_gateway_vms,
981            )?;
982
983        for (node_vm, nat_gateway_vm) in symmetric_private_node_nat_gateway_map.iter() {
984            println!(
985                "{}: {} ==routed through==> {}: {}",
986                node_vm.name,
987                node_vm.public_ip_addr,
988                nat_gateway_vm.name,
989                nat_gateway_vm.public_ip_addr
990            );
991            let ssh = if let Some(ssh_key_path) = self.ssh_private_key_path.to_str() {
992                format!(
993                        "ssh -i {ssh_key_path} -o ProxyCommand=\"ssh -W %h:%p root@{} -i {ssh_key_path}\" root@{}",
994                        nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
995                    )
996            } else {
997                format!(
998                    "ssh -o ProxyCommand=\"ssh -W %h:%p root@{}\" root@{}",
999                    nat_gateway_vm.public_ip_addr, node_vm.private_ip_addr
1000                )
1001            };
1002            println!("SSH using NAT gateway: {ssh}");
1003        }
1004
1005        if !self.uploader_vms.is_empty() {
1006            println!("============");
1007            println!("Uploader VMs");
1008            println!("============");
1009            for uploader_vm in self.uploader_vms.iter() {
1010                println!("{}: {}", uploader_vm.vm.name, uploader_vm.vm.public_ip_addr);
1011            }
1012            println!();
1013
1014            println!("===========================");
1015            println!("Uploader Wallet Public Keys");
1016            println!("===========================");
1017            for uploader_vm in self.uploader_vms.iter() {
1018                for (user, key) in uploader_vm.wallet_public_key.iter() {
1019                    println!("{}@{}: {}", uploader_vm.vm.name, user, key);
1020                }
1021            }
1022        }
1023
1024        if !self.misc_vms.is_empty() {
1025            println!("=========");
1026            println!("Other VMs");
1027            println!("=========");
1028        }
1029        if !self.misc_vms.is_empty() {
1030            for vm in self.misc_vms.iter() {
1031                println!("{}: {}", vm.name, vm.public_ip_addr);
1032            }
1033        }
1034
1035        for nat_gateway_vm in self.full_cone_nat_gateway_vms.iter() {
1036            println!("{}: {}", nat_gateway_vm.name, nat_gateway_vm.public_ip_addr);
1037        }
1038
1039        for nat_gateway_vm in self.symmetric_nat_gateway_vms.iter() {
1040            println!("{}: {}", nat_gateway_vm.name, nat_gateway_vm.public_ip_addr);
1041        }
1042
1043        println!("SSH user: {}", self.ssh_user);
1044        println!();
1045
1046        if full {
1047            println!("===============");
1048            println!("Full Peer List");
1049            println!("===============");
1050            let mut quic_listeners = Vec::new();
1051            let mut ws_listeners = Vec::new();
1052
1053            for node_vm in self.peer_cache_node_vms.iter().chain(self.node_vms.iter()) {
1054                for addresses in &node_vm.node_listen_addresses {
1055                    for addr in addresses {
1056                        if !addr.starts_with("/ip4/127.0.0.1") && !addr.starts_with("/ip4/10.") {
1057                            if addr.contains("/quic") {
1058                                quic_listeners.push(addr.clone());
1059                            } else if addr.contains("/ws") {
1060                                ws_listeners.push(addr.clone());
1061                            }
1062                        }
1063                    }
1064                }
1065            }
1066
1067            if !quic_listeners.is_empty() {
1068                println!("QUIC:");
1069                for addr in quic_listeners {
1070                    println!("  {addr}");
1071                }
1072                println!();
1073            }
1074
1075            if !ws_listeners.is_empty() {
1076                println!("Websocket:");
1077                for addr in ws_listeners {
1078                    println!("  {addr}");
1079                }
1080                println!();
1081            }
1082        } else {
1083            println!("============");
1084            println!("Sample Peers");
1085            println!("============");
1086            self.peer_cache_node_vms
1087                .iter()
1088                .chain(self.node_vms.iter())
1089                .map(|node_vm| node_vm.vm.public_ip_addr.to_string())
1090                .for_each(|ip| {
1091                    if let Some(peer) = self.peers().iter().find(|p| p.contains(&ip)) {
1092                        println!("{peer}");
1093                    }
1094                });
1095        }
1096        println!();
1097
1098        println!(
1099            "Genesis: {}",
1100            self.genesis_multiaddr
1101                .as_ref()
1102                .map_or("N/A", |genesis| genesis)
1103        );
1104        let inventory_file_path =
1105            get_data_directory()?.join(format!("{}-inventory.json", self.name));
1106        println!(
1107            "The full inventory is at {}",
1108            inventory_file_path.to_string_lossy()
1109        );
1110        println!();
1111
1112        if !self.uploaded_files.is_empty() {
1113            println!("Uploaded files:");
1114            for file in self.uploaded_files.iter() {
1115                println!("{}: {}", file.0, file.1);
1116            }
1117        }
1118
1119        if self.environment_details.evm_data_payments_address.is_some()
1120            || self.environment_details.evm_payment_token_address.is_some()
1121            || self.environment_details.evm_rpc_url.is_some()
1122        {
1123            println!("===========");
1124            println!("EVM Details");
1125            println!("===========");
1126            println!(
1127                "EVM data payments address: {}",
1128                self.environment_details
1129                    .evm_data_payments_address
1130                    .as_ref()
1131                    .map_or("N/A", |addr| addr)
1132            );
1133            println!(
1134                "EVM payment token address: {}",
1135                self.environment_details
1136                    .evm_payment_token_address
1137                    .as_ref()
1138                    .map_or("N/A", |addr| addr)
1139            );
1140            println!(
1141                "EVM RPC URL: {}",
1142                self.environment_details
1143                    .evm_rpc_url
1144                    .as_ref()
1145                    .map_or("N/A", |addr| addr)
1146            );
1147        }
1148
1149        Ok(())
1150    }
1151
1152    pub fn get_genesis_ip(&self) -> Option<IpAddr> {
1153        self.misc_vms
1154            .iter()
1155            .find(|vm| vm.name.contains("genesis"))
1156            .map(|vm| vm.public_ip_addr)
1157    }
1158
1159    pub fn print_peer_cache_webserver(&self) {
1160        println!("=====================");
1161        println!("Peer Cache Webservers");
1162        println!("=====================");
1163
1164        for node_vm in &self.peer_cache_node_vms {
1165            let webserver = get_bootstrap_cache_url(&node_vm.vm.public_ip_addr);
1166            println!("{}: {webserver}", node_vm.vm.name);
1167        }
1168    }
1169}
1170
1171pub fn get_data_directory() -> Result<PathBuf> {
1172    let path = dirs_next::data_dir()
1173        .ok_or_else(|| eyre!("Could not retrieve data directory"))?
1174        .join("autonomi")
1175        .join("testnet-deploy");
1176    if !path.exists() {
1177        std::fs::create_dir_all(path.clone())?;
1178    }
1179    Ok(path)
1180}