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