sn_testnet_deploy/ansible/
provisioning.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 super::{
8    extra_vars::ExtraVarsDocBuilder,
9    inventory::{
10        generate_full_cone_private_node_static_environment_inventory,
11        generate_port_restricted_cone_private_node_static_environment_inventory,
12        generate_symmetric_private_node_static_environment_inventory,
13    },
14    AnsibleInventoryType, AnsiblePlaybook, AnsibleRunner,
15};
16use crate::{
17    ansible::inventory::{
18        generate_custom_environment_inventory,
19        generate_full_cone_nat_gateway_static_environment_inventory,
20        generate_port_restricted_cone_nat_gateway_static_environment_inventory,
21    },
22    bootstrap::BootstrapOptions,
23    clients::ClientsDeployOptions,
24    deploy::DeployOptions,
25    error::{Error, Result},
26    funding::FundingOptions,
27    inventory::{DeploymentNodeRegistries, VirtualMachine},
28    print_duration, run_external_command, BinaryOption, CloudProvider, EvmNetwork, LogFormat,
29    NodeType, SshClient, UpgradeOptions,
30};
31use ant_service_management::NodeRegistryManager;
32use evmlib::common::U256;
33use log::{debug, error, trace};
34use semver::Version;
35use serde::{Deserialize, Serialize};
36use std::{
37    collections::HashMap,
38    net::IpAddr,
39    path::PathBuf,
40    time::{Duration, Instant},
41};
42use walkdir::WalkDir;
43
44use crate::ansible::extra_vars;
45
46pub const DEFAULT_BETA_ENCRYPTION_KEY: &str =
47    "49113d2083f57a976076adbe85decb75115820de1e6e74b47e0429338cef124a";
48
49#[derive(Clone, Serialize, Deserialize)]
50pub struct ProvisionOptions {
51    /// The safe version is also in the binary option, but only for an initial deployment.
52    /// For the upscale, it needs to be provided explicitly, because currently it is not
53    /// recorded in the inventory.
54    pub ant_version: Option<String>,
55    pub binary_option: BinaryOption,
56    pub chunk_size: Option<u64>,
57    pub chunk_tracker_data_addresses: Option<Vec<String>>,
58    pub chunk_tracker_services: Option<u16>,
59    pub client_env_variables: Option<Vec<(String, String)>>,
60    pub delayed_verifier_batch_size: Option<u16>,
61    pub disable_nodes: bool,
62    pub delayed_verifier_quorum_value: Option<String>,
63    pub start_delayed_verifier: bool,
64    pub enable_logging: bool,
65    pub enable_metrics: bool,
66    pub start_random_verifier: bool,
67    pub start_performance_verifier: bool,
68    pub start_uploaders: bool,
69    pub evm_data_payments_address: Option<String>,
70    pub evm_merkle_payments_address: Option<String>,
71    pub evm_network: EvmNetwork,
72    pub evm_payment_token_address: Option<String>,
73    pub evm_rpc_url: Option<String>,
74    pub expected_hash: Option<String>,
75    pub expected_size: Option<u64>,
76    pub file_address: Option<String>,
77    pub full_cone_private_node_count: u16,
78    pub funding_wallet_secret_key: Option<String>,
79    pub gas_amount: Option<U256>,
80    pub interval: Option<Duration>,
81    pub log_format: Option<LogFormat>,
82    pub max_archived_log_files: u16,
83    pub max_log_files: u16,
84    pub max_uploads: Option<u32>,
85    pub merkle: bool,
86    pub name: String,
87    pub network_id: Option<u8>,
88    pub network_dashboard_branch: Option<String>,
89    pub node_count: u16,
90    pub node_env_variables: Option<Vec<(String, String)>>,
91    pub output_inventory_dir_path: PathBuf,
92    pub peer_cache_node_count: u16,
93    pub performance_verifier_batch_size: Option<u16>,
94    pub port_restricted_cone_private_node_count: u16,
95    pub public_rpc: bool,
96    pub random_verifier_batch_size: Option<u16>,
97    pub repair_service_count: u16,
98    pub data_retrieval_service_count: u16,
99    pub rewards_address: Option<String>,
100    pub scan_frequency: Option<u64>,
101    pub sleep_duration: Option<u16>,
102    pub single_node_payment: bool,
103    pub start_chunk_trackers: bool,
104    pub start_data_retrieval: bool,
105    pub symmetric_private_node_count: u16,
106    pub token_amount: Option<U256>,
107    pub upload_batch_size: Option<u16>,
108    pub upload_size: Option<u16>,
109    pub upload_interval: Option<u16>,
110    pub uploaders_count: Option<u16>,
111    pub upnp_private_node_count: u16,
112    pub wallet_secret_keys: Option<Vec<String>>,
113}
114
115/// These are obtained by running the inventory playbook
116#[derive(Clone, Debug)]
117pub struct PrivateNodeProvisionInventory {
118    pub full_cone_nat_gateway_vms: Vec<VirtualMachine>,
119    pub full_cone_private_node_vms: Vec<VirtualMachine>,
120    pub symmetric_nat_gateway_vms: Vec<VirtualMachine>,
121    pub symmetric_private_node_vms: Vec<VirtualMachine>,
122    pub port_restricted_cone_nat_gateway_vms: Vec<VirtualMachine>,
123    pub port_restricted_cone_private_node_vms: Vec<VirtualMachine>,
124}
125
126impl PrivateNodeProvisionInventory {
127    pub fn new(
128        provisioner: &AnsibleProvisioner,
129        full_cone_private_node_vm_count: Option<u16>,
130        symmetric_private_node_vm_count: Option<u16>,
131        port_restricted_cone_private_node_vm_count: Option<u16>,
132    ) -> Result<Self> {
133        // All the environment types set private_node_vm count to >0 if not specified.
134        let should_provision_full_cone_private_nodes = full_cone_private_node_vm_count
135            .map(|count| count > 0)
136            .unwrap_or(true);
137        let should_provision_symmetric_private_nodes = symmetric_private_node_vm_count
138            .map(|count| count > 0)
139            .unwrap_or(true);
140        let should_provision_port_restricted_cone_private_nodes =
141            port_restricted_cone_private_node_vm_count
142                .map(|count| count > 0)
143                .unwrap_or(true);
144
145        let mut inventory = Self {
146            full_cone_nat_gateway_vms: Default::default(),
147            full_cone_private_node_vms: Default::default(),
148            symmetric_nat_gateway_vms: Default::default(),
149            symmetric_private_node_vms: Default::default(),
150            port_restricted_cone_nat_gateway_vms: Default::default(),
151            port_restricted_cone_private_node_vms: Default::default(),
152        };
153
154        if should_provision_full_cone_private_nodes {
155            let full_cone_private_node_vms = provisioner
156                .ansible_runner
157                .get_inventory(AnsibleInventoryType::FullConePrivateNodes, true)
158                .inspect_err(|err| {
159                    println!("Failed to obtain the inventory of Full Cone private node: {err:?}");
160                })?;
161
162            let full_cone_nat_gateway_inventory = provisioner
163                .ansible_runner
164                .get_inventory(AnsibleInventoryType::FullConeNatGateway, true)
165                .inspect_err(|err| {
166                    println!("Failed to get Full Cone NAT Gateway inventory {err:?}");
167                })?;
168
169            if full_cone_nat_gateway_inventory.len() != full_cone_private_node_vms.len() {
170                println!("The number of Full Cone private nodes does not match the number of Full Cone NAT Gateway VMs");
171                return Err(Error::VmCountMismatch(
172                    Some(AnsibleInventoryType::FullConePrivateNodes),
173                    Some(AnsibleInventoryType::FullConeNatGateway),
174                ));
175            }
176
177            inventory.full_cone_private_node_vms = full_cone_private_node_vms;
178            inventory.full_cone_nat_gateway_vms = full_cone_nat_gateway_inventory;
179        }
180
181        if should_provision_symmetric_private_nodes {
182            let symmetric_private_node_vms = provisioner
183                .ansible_runner
184                .get_inventory(AnsibleInventoryType::SymmetricPrivateNodes, true)
185                .inspect_err(|err| {
186                    println!("Failed to obtain the inventory of Symmetric private node: {err:?}");
187                })?;
188
189            let symmetric_nat_gateway_inventory = provisioner
190                .ansible_runner
191                .get_inventory(AnsibleInventoryType::SymmetricNatGateway, true)
192                .inspect_err(|err| {
193                    println!("Failed to get Symmetric NAT Gateway inventory {err:?}");
194                })?;
195
196            if symmetric_nat_gateway_inventory.len() != symmetric_private_node_vms.len() {
197                println!("The number of Symmetric private nodes does not match the number of Symmetric NAT Gateway VMs");
198                return Err(Error::VmCountMismatch(
199                    Some(AnsibleInventoryType::SymmetricPrivateNodes),
200                    Some(AnsibleInventoryType::SymmetricNatGateway),
201                ));
202            }
203
204            inventory.symmetric_private_node_vms = symmetric_private_node_vms;
205            inventory.symmetric_nat_gateway_vms = symmetric_nat_gateway_inventory;
206        }
207
208        if should_provision_port_restricted_cone_private_nodes {
209            let port_restricted_cone_private_node_vms = provisioner
210                .ansible_runner
211                .get_inventory(AnsibleInventoryType::PortRestrictedConePrivateNodes, true)
212                .inspect_err(|err| {
213                    println!("Failed to get Port Restricted Cone Private Node inventory {err:?}");
214                })?;
215
216            let port_restricted_cone_nat_gateway_inventory = provisioner
217                .ansible_runner
218                .get_inventory(AnsibleInventoryType::PortRestrictedConeNatGateway, true)
219                .inspect_err(|err| {
220                    println!("Failed to get Port Restricted Cone NAT Gateway inventory {err:?}");
221                })?;
222
223            if port_restricted_cone_nat_gateway_inventory.len()
224                != port_restricted_cone_private_node_vms.len()
225            {
226                println!("The number of Port Restricted Cone private nodes does not match the number of Port Restricted Cone NAT Gateway VMs");
227                return Err(Error::VmCountMismatch(
228                    Some(AnsibleInventoryType::PortRestrictedConePrivateNodes),
229                    Some(AnsibleInventoryType::PortRestrictedConeNatGateway),
230                ));
231            }
232
233            inventory.port_restricted_cone_private_node_vms = port_restricted_cone_private_node_vms;
234            inventory.port_restricted_cone_nat_gateway_vms =
235                port_restricted_cone_nat_gateway_inventory;
236        }
237
238        Ok(inventory)
239    }
240
241    pub fn should_provision_full_cone_private_nodes(&self) -> bool {
242        !self.full_cone_private_node_vms.is_empty()
243    }
244
245    pub fn should_provision_symmetric_private_nodes(&self) -> bool {
246        !self.symmetric_private_node_vms.is_empty()
247    }
248
249    pub fn should_provision_port_restricted_cone_private_nodes(&self) -> bool {
250        !self.port_restricted_cone_private_node_vms.is_empty()
251    }
252
253    pub fn symmetric_private_node_and_gateway_map(
254        &self,
255    ) -> Result<HashMap<VirtualMachine, VirtualMachine>> {
256        Self::match_private_node_vm_and_gateway_vm(
257            &self.symmetric_private_node_vms,
258            &self.symmetric_nat_gateway_vms,
259        )
260    }
261
262    pub fn full_cone_private_node_and_gateway_map(
263        &self,
264    ) -> Result<HashMap<VirtualMachine, VirtualMachine>> {
265        Self::match_private_node_vm_and_gateway_vm(
266            &self.full_cone_private_node_vms,
267            &self.full_cone_nat_gateway_vms,
268        )
269    }
270
271    pub fn port_restricted_cone_private_node_and_gateway_map(
272        &self,
273    ) -> Result<HashMap<VirtualMachine, VirtualMachine>> {
274        Self::match_private_node_vm_and_gateway_vm(
275            &self.port_restricted_cone_private_node_vms,
276            &self.port_restricted_cone_nat_gateway_vms,
277        )
278    }
279
280    pub fn match_private_node_vm_and_gateway_vm(
281        private_node_vms: &[VirtualMachine],
282        nat_gateway_vms: &[VirtualMachine],
283    ) -> Result<HashMap<VirtualMachine, VirtualMachine>> {
284        if private_node_vms.len() != nat_gateway_vms.len() {
285            println!(
286            "The number of private node VMs ({}) does not match the number of NAT Gateway VMs ({})",
287            private_node_vms.len(),
288            nat_gateway_vms.len()
289        );
290            error!("The number of private node VMs does not match the number of NAT Gateway VMs: Private VMs: {private_node_vms:?} Nat gateway VMs: {nat_gateway_vms:?}");
291            return Err(Error::VmCountMismatch(None, None));
292        }
293
294        let mut map = HashMap::new();
295        for private_vm in private_node_vms {
296            let nat_gateway = nat_gateway_vms
297                .iter()
298                .find(|vm| {
299                    let private_node_name = private_vm.name.split('-').next_back().unwrap();
300                    let nat_gateway_name = vm.name.split('-').next_back().unwrap();
301                    private_node_name == nat_gateway_name
302                })
303                .ok_or_else(|| {
304                    println!(
305                        "Failed to find a matching NAT Gateway for private node: {}",
306                        private_vm.name
307                    );
308                    error!("Failed to find a matching NAT Gateway for private node: {}. Private VMs: {private_node_vms:?} Nat gateway VMs: {nat_gateway_vms:?}", private_vm.name);
309                    Error::VmCountMismatch(None, None)
310                })?;
311
312            let _ = map.insert(private_vm.clone(), nat_gateway.clone());
313        }
314
315        Ok(map)
316    }
317}
318
319impl From<BootstrapOptions> for ProvisionOptions {
320    fn from(bootstrap_options: BootstrapOptions) -> Self {
321        ProvisionOptions {
322            ant_version: None,
323            binary_option: bootstrap_options.binary_option,
324            chunk_size: bootstrap_options.chunk_size,
325            chunk_tracker_data_addresses: None,
326            chunk_tracker_services: None,
327            client_env_variables: None,
328            delayed_verifier_batch_size: None,
329            disable_nodes: false,
330            delayed_verifier_quorum_value: None,
331            enable_logging: bootstrap_options.enable_logging,
332            enable_metrics: true,
333            evm_data_payments_address: bootstrap_options.evm_data_payments_address,
334            evm_merkle_payments_address: bootstrap_options.evm_merkle_payments_address,
335            evm_network: bootstrap_options.evm_network,
336            evm_payment_token_address: bootstrap_options.evm_payment_token_address,
337            evm_rpc_url: bootstrap_options.evm_rpc_url,
338            expected_hash: None,
339            expected_size: None,
340            file_address: None,
341            full_cone_private_node_count: bootstrap_options.full_cone_private_node_count,
342            funding_wallet_secret_key: None,
343            gas_amount: None,
344            interval: Some(bootstrap_options.interval),
345            log_format: bootstrap_options.log_format,
346            max_archived_log_files: bootstrap_options.max_archived_log_files,
347            max_log_files: bootstrap_options.max_log_files,
348            max_uploads: None,
349            merkle: false,
350            name: bootstrap_options.name,
351            network_id: Some(bootstrap_options.network_id),
352            network_dashboard_branch: None,
353            node_count: bootstrap_options.node_count,
354            node_env_variables: bootstrap_options.node_env_variables,
355            output_inventory_dir_path: bootstrap_options.output_inventory_dir_path,
356            peer_cache_node_count: 0,
357            performance_verifier_batch_size: None,
358            public_rpc: false,
359            random_verifier_batch_size: None,
360            repair_service_count: 0,
361            data_retrieval_service_count: 0,
362            rewards_address: Some(bootstrap_options.rewards_address),
363            scan_frequency: None,
364            sleep_duration: None,
365            single_node_payment: false,
366            start_chunk_trackers: false,
367            start_data_retrieval: false,
368            start_delayed_verifier: false,
369            start_random_verifier: false,
370            start_performance_verifier: false,
371            start_uploaders: false,
372            symmetric_private_node_count: bootstrap_options.symmetric_private_node_count,
373            token_amount: None,
374            upload_batch_size: None,
375            upload_size: None,
376            upload_interval: None,
377            uploaders_count: None,
378            upnp_private_node_count: bootstrap_options.upnp_private_node_count,
379            port_restricted_cone_private_node_count: 0,
380            wallet_secret_keys: None,
381        }
382    }
383}
384
385impl From<DeployOptions> for ProvisionOptions {
386    fn from(deploy_options: DeployOptions) -> Self {
387        ProvisionOptions {
388            ant_version: None,
389            binary_option: deploy_options.binary_option,
390            chunk_size: deploy_options.chunk_size,
391            chunk_tracker_data_addresses: if deploy_options.chunk_tracker_data_addresses.is_empty()
392            {
393                None
394            } else {
395                Some(deploy_options.chunk_tracker_data_addresses)
396            },
397            chunk_tracker_services: Some(deploy_options.chunk_tracker_services),
398            client_env_variables: deploy_options.client_env_variables,
399            delayed_verifier_batch_size: None,
400            disable_nodes: false,
401            delayed_verifier_quorum_value: None,
402            enable_logging: deploy_options.enable_logging,
403            enable_metrics: deploy_options.enable_metrics,
404            node_env_variables: deploy_options.node_env_variables,
405            evm_data_payments_address: deploy_options.evm_data_payments_address,
406            evm_merkle_payments_address: deploy_options.evm_merkle_payments_address,
407            evm_network: deploy_options.evm_network,
408            evm_payment_token_address: deploy_options.evm_payment_token_address,
409            evm_rpc_url: deploy_options.evm_rpc_url,
410            expected_hash: None,
411            expected_size: None,
412            file_address: None,
413            full_cone_private_node_count: deploy_options.full_cone_private_node_count,
414            funding_wallet_secret_key: deploy_options.funding_wallet_secret_key,
415            gas_amount: deploy_options.initial_gas,
416            interval: Some(deploy_options.interval),
417            log_format: deploy_options.log_format,
418            max_archived_log_files: deploy_options.max_archived_log_files,
419            max_log_files: deploy_options.max_log_files,
420            max_uploads: deploy_options.max_uploads,
421            merkle: deploy_options.merkle,
422            name: deploy_options.name,
423            network_id: Some(deploy_options.network_id),
424            network_dashboard_branch: deploy_options.network_dashboard_branch,
425            node_count: deploy_options.node_count,
426            output_inventory_dir_path: deploy_options.output_inventory_dir_path,
427            peer_cache_node_count: deploy_options.peer_cache_node_count,
428            performance_verifier_batch_size: None,
429            port_restricted_cone_private_node_count: deploy_options
430                .port_restricted_cone_private_node_count,
431            public_rpc: deploy_options.public_rpc,
432            random_verifier_batch_size: None,
433            repair_service_count: 0,
434            data_retrieval_service_count: 0,
435            rewards_address: Some(deploy_options.rewards_address),
436            scan_frequency: None,
437            sleep_duration: None,
438            single_node_payment: deploy_options.single_node_payment,
439            start_chunk_trackers: deploy_options.start_chunk_trackers,
440            start_data_retrieval: false,
441            start_delayed_verifier: deploy_options.start_delayed_verifier,
442            start_performance_verifier: deploy_options.start_performance_verifier,
443            start_random_verifier: deploy_options.start_random_verifier,
444            start_uploaders: false,
445            symmetric_private_node_count: deploy_options.symmetric_private_node_count,
446            token_amount: deploy_options.initial_tokens,
447            upload_batch_size: None,
448            upload_size: Some(deploy_options.upload_size),
449            upload_interval: Some(deploy_options.upload_interval),
450            uploaders_count: Some(deploy_options.uploaders_count),
451            upnp_private_node_count: deploy_options.upnp_private_node_count,
452            wallet_secret_keys: None,
453        }
454    }
455}
456
457impl From<ClientsDeployOptions> for ProvisionOptions {
458    fn from(client_options: ClientsDeployOptions) -> Self {
459        Self {
460            ant_version: None,
461            binary_option: client_options.binary_option,
462            chunk_size: client_options.chunk_size,
463            chunk_tracker_data_addresses: if client_options.chunk_tracker_data_addresses.is_empty()
464            {
465                None
466            } else {
467                Some(client_options.chunk_tracker_data_addresses)
468            },
469            chunk_tracker_services: Some(client_options.chunk_tracker_services),
470            client_env_variables: client_options.client_env_variables,
471            delayed_verifier_batch_size: client_options.delayed_verifier_batch_size,
472            disable_nodes: false,
473            delayed_verifier_quorum_value: client_options.delayed_verifier_quorum_value,
474            enable_logging: false,
475            enable_metrics: client_options.enable_metrics,
476            start_random_verifier: client_options.start_random_verifier,
477            start_performance_verifier: client_options.start_performance_verifier,
478            start_uploaders: client_options.start_uploaders,
479            evm_data_payments_address: client_options.evm_details.data_payments_address,
480            evm_merkle_payments_address: client_options.evm_details.merkle_payments_address,
481            evm_network: client_options.evm_details.network,
482            evm_payment_token_address: client_options.evm_details.payment_token_address,
483            evm_rpc_url: client_options.evm_details.rpc_url,
484            expected_hash: client_options.expected_hash,
485            expected_size: client_options.expected_size,
486            file_address: client_options.file_address,
487            full_cone_private_node_count: 0,
488            funding_wallet_secret_key: client_options.funding_wallet_secret_key,
489            gas_amount: client_options.initial_gas,
490            interval: None,
491            log_format: None,
492            max_archived_log_files: client_options.max_archived_log_files,
493            max_log_files: client_options.max_log_files,
494            max_uploads: client_options.max_uploads,
495            merkle: false,
496            name: client_options.name,
497            network_id: client_options.network_id,
498            network_dashboard_branch: None,
499            node_count: 0,
500            node_env_variables: None,
501            output_inventory_dir_path: client_options.output_inventory_dir_path,
502            peer_cache_node_count: 0,
503            performance_verifier_batch_size: client_options.performance_verifier_batch_size,
504            public_rpc: false,
505            random_verifier_batch_size: client_options.random_verifier_batch_size,
506            repair_service_count: client_options.repair_service_count,
507            data_retrieval_service_count: client_options.data_retrieval_service_count,
508            rewards_address: None,
509            scan_frequency: client_options.scan_frequency,
510            sleep_duration: client_options.sleep_duration,
511            single_node_payment: false,
512            start_chunk_trackers: client_options.start_chunk_trackers,
513            start_data_retrieval: client_options.start_data_retrieval,
514            start_delayed_verifier: client_options.start_delayed_verifier,
515            symmetric_private_node_count: 0,
516            token_amount: client_options.initial_tokens,
517            upload_batch_size: client_options.upload_batch_size,
518            upload_size: client_options.upload_size,
519            upload_interval: None,
520            uploaders_count: Some(client_options.uploaders_count),
521            upnp_private_node_count: 0,
522            port_restricted_cone_private_node_count: 0,
523            wallet_secret_keys: client_options.wallet_secret_keys,
524        }
525    }
526}
527
528#[derive(Clone)]
529pub struct AnsibleProvisioner {
530    pub ansible_runner: AnsibleRunner,
531    pub cloud_provider: CloudProvider,
532    pub ssh_client: SshClient,
533}
534
535impl AnsibleProvisioner {
536    pub fn new(
537        ansible_runner: AnsibleRunner,
538        cloud_provider: CloudProvider,
539        ssh_client: SshClient,
540    ) -> Self {
541        Self {
542            ansible_runner,
543            cloud_provider,
544            ssh_client,
545        }
546    }
547
548    pub fn build_autonomi_binaries(
549        &self,
550        options: &ProvisionOptions,
551        binaries_to_build: Option<Vec<String>>,
552    ) -> Result<()> {
553        let start = Instant::now();
554        println!("Obtaining IP address for build VM...");
555        let build_inventory = self
556            .ansible_runner
557            .get_inventory(AnsibleInventoryType::Build, true)?;
558        let build_ip = build_inventory[0].public_ip_addr;
559        self.ssh_client
560            .wait_for_ssh_availability(&build_ip, &self.cloud_provider.get_ssh_user())?;
561
562        println!("Running ansible against build VM...");
563        let base_extra_vars = extra_vars::build_binaries_extra_vars_doc(options)?;
564
565        let extra_vars = if let Some(binaries) = binaries_to_build {
566            let mut build_ant = false;
567            let mut build_antnode = false;
568            let mut build_antctl = false;
569            let mut build_antctld = false;
570
571            for binary in &binaries {
572                match binary.as_str() {
573                    "ant" => build_ant = true,
574                    "antnode" => build_antnode = true,
575                    "antctl" => build_antctl = true,
576                    "antctld" => build_antctld = true,
577                    _ => return Err(Error::InvalidBinaryName(binary.clone())),
578                }
579            }
580
581            let mut json_value: serde_json::Value = serde_json::from_str(&base_extra_vars)?;
582            if let serde_json::Value::Object(ref mut map) = json_value {
583                map.insert("build_ant".to_string(), serde_json::Value::Bool(build_ant));
584                map.insert(
585                    "build_antnode".to_string(),
586                    serde_json::Value::Bool(build_antnode),
587                );
588                map.insert(
589                    "build_antctl".to_string(),
590                    serde_json::Value::Bool(build_antctl),
591                );
592                map.insert(
593                    "build_antctld".to_string(),
594                    serde_json::Value::Bool(build_antctld),
595                );
596            }
597            json_value.to_string()
598        } else {
599            base_extra_vars
600        };
601
602        self.ansible_runner.run_playbook(
603            AnsiblePlaybook::Build,
604            AnsibleInventoryType::Build,
605            Some(extra_vars),
606        )?;
607        print_duration(start.elapsed());
608        Ok(())
609    }
610
611    pub fn cleanup_node_logs(&self, setup_cron: bool) -> Result<()> {
612        for node_inv_type in AnsibleInventoryType::iter_node_type() {
613            self.ansible_runner.run_playbook(
614                AnsiblePlaybook::CleanupLogs,
615                node_inv_type,
616                Some(format!("{{ \"setup_cron\": \"{setup_cron}\" }}")),
617            )?;
618        }
619
620        Ok(())
621    }
622
623    pub fn copy_logs(&self, name: &str, resources_only: bool) -> Result<()> {
624        for node_inv_type in AnsibleInventoryType::iter_node_type() {
625            self.ansible_runner.run_playbook(
626                AnsiblePlaybook::CopyLogs,
627                node_inv_type,
628                Some(format!(
629                    "{{ \"env_name\": \"{name}\", \"resources_only\" : \"{resources_only}\" }}"
630                )),
631            )?;
632        }
633        Ok(())
634    }
635
636    pub fn get_all_node_inventory(&self) -> Result<Vec<VirtualMachine>> {
637        let mut all_node_inventory = Vec::new();
638        for node_inv_type in AnsibleInventoryType::iter_node_type() {
639            all_node_inventory.extend(self.ansible_runner.get_inventory(node_inv_type, false)?);
640        }
641
642        Ok(all_node_inventory)
643    }
644
645    pub fn get_symmetric_nat_gateway_inventory(&self) -> Result<Vec<VirtualMachine>> {
646        self.ansible_runner
647            .get_inventory(AnsibleInventoryType::SymmetricNatGateway, false)
648    }
649
650    pub fn get_full_cone_nat_gateway_inventory(&self) -> Result<Vec<VirtualMachine>> {
651        self.ansible_runner
652            .get_inventory(AnsibleInventoryType::FullConeNatGateway, false)
653    }
654
655    pub fn get_client_inventory(&self) -> Result<Vec<VirtualMachine>> {
656        self.ansible_runner
657            .get_inventory(AnsibleInventoryType::Clients, false)
658    }
659
660    pub async fn get_node_registries(
661        &self,
662        inventory_type: &AnsibleInventoryType,
663    ) -> Result<DeploymentNodeRegistries> {
664        debug!("Fetching node manager inventory for {inventory_type:?}");
665        let temp_dir_path = tempfile::tempdir()?.keep();
666        let temp_dir_json = serde_json::to_string(&temp_dir_path)?;
667
668        self.ansible_runner.run_playbook(
669            AnsiblePlaybook::AntCtlInventory,
670            *inventory_type,
671            Some(format!("{{ \"dest\": {temp_dir_json} }}")),
672        )?;
673
674        let node_registry_paths = WalkDir::new(temp_dir_path)
675            .into_iter()
676            .flatten()
677            .filter_map(|entry| {
678                if entry.file_type().is_file()
679                    && entry.path().extension().is_some_and(|ext| ext == "json")
680                {
681                    // tempdir/<testnet_name>-node/var/safenode-manager/node_registry.json
682                    let mut vm_name = entry.path().to_path_buf();
683                    trace!("Found file with json extension: {vm_name:?}");
684                    vm_name.pop();
685                    vm_name.pop();
686                    vm_name.pop();
687                    // Extract the <testnet_name>-node string
688                    trace!("Extracting the vm name from the path");
689                    let vm_name = vm_name.file_name()?.to_str()?;
690                    trace!("Extracted vm name from path: {vm_name}");
691                    Some((vm_name.to_string(), entry.path().to_path_buf()))
692                } else {
693                    None
694                }
695            })
696            .collect::<Vec<(String, PathBuf)>>();
697
698        let mut node_registries = Vec::new();
699        let mut failed_vms = Vec::new();
700        for (vm_name, file_path) in node_registry_paths {
701            match NodeRegistryManager::load(&file_path).await {
702                Ok(node_registry) => node_registries.push((vm_name.clone(), node_registry)),
703                Err(_) => failed_vms.push(vm_name.clone()),
704            }
705        }
706
707        let deployment_registries = DeploymentNodeRegistries {
708            inventory_type: *inventory_type,
709            retrieved_registries: node_registries,
710            failed_vms,
711        };
712        Ok(deployment_registries)
713    }
714
715    pub fn provision_evm_nodes(&self, options: &ProvisionOptions) -> Result<()> {
716        let start = Instant::now();
717        println!("Obtaining IP address for EVM nodes...");
718        let evm_node_inventory = self
719            .ansible_runner
720            .get_inventory(AnsibleInventoryType::EvmNodes, true)?;
721        let evm_node_ip = evm_node_inventory[0].public_ip_addr;
722        self.ssh_client
723            .wait_for_ssh_availability(&evm_node_ip, &self.cloud_provider.get_ssh_user())?;
724
725        println!("Running ansible against EVM nodes...");
726        self.ansible_runner.run_playbook(
727            AnsiblePlaybook::EvmNodes,
728            AnsibleInventoryType::EvmNodes,
729            Some(extra_vars::build_evm_nodes_extra_vars_doc(
730                &options.name,
731                &self.cloud_provider,
732                &options.binary_option,
733            )),
734        )?;
735        print_duration(start.elapsed());
736        Ok(())
737    }
738
739    pub fn provision_genesis_node(&self, options: &ProvisionOptions) -> Result<()> {
740        let start = Instant::now();
741        let genesis_inventory = self
742            .ansible_runner
743            .get_inventory(AnsibleInventoryType::Genesis, true)?;
744        let genesis_ip = genesis_inventory[0].public_ip_addr;
745        self.ssh_client
746            .wait_for_ssh_availability(&genesis_ip, &self.cloud_provider.get_ssh_user())?;
747        self.ansible_runner.run_playbook(
748            AnsiblePlaybook::Genesis,
749            AnsibleInventoryType::Genesis,
750            Some(extra_vars::build_node_extra_vars_doc(
751                &self.cloud_provider.to_string(),
752                options,
753                NodeType::Genesis,
754                None,
755                None,
756                1,
757                options.evm_network.clone(),
758                false,
759            )?),
760        )?;
761
762        print_duration(start.elapsed());
763
764        Ok(())
765    }
766
767    pub fn provision_full_cone(
768        &self,
769        options: &ProvisionOptions,
770        initial_contact_peer: Option<String>,
771        initial_network_contacts_url: Option<String>,
772        private_node_inventory: PrivateNodeProvisionInventory,
773        new_full_cone_nat_gateway_new_vms_for_upscale: Option<Vec<VirtualMachine>>,
774    ) -> Result<()> {
775        // Step 1 of Full Cone NAT Gateway
776        let start = Instant::now();
777        self.print_ansible_run_banner("Provision Full Cone NAT Gateway - Step 1");
778
779        for vm in new_full_cone_nat_gateway_new_vms_for_upscale
780            .as_ref()
781            .unwrap_or(&private_node_inventory.full_cone_nat_gateway_vms)
782            .iter()
783        {
784            println!(
785                "Checking SSH availability for Full Cone NAT Gateway: {}",
786                vm.public_ip_addr
787            );
788            self.ssh_client
789                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
790                .map_err(|e| {
791                    println!("Failed to establish SSH connection to Full Cone NAT Gateway: {e}");
792                    e
793                })?;
794        }
795
796        let mut modified_private_node_inventory = private_node_inventory.clone();
797
798        // If we are upscaling, then we cannot access the gateway VMs which are already deployed.
799        if let Some(new_full_cone_nat_gateway_new_vms_for_upscale) =
800            &new_full_cone_nat_gateway_new_vms_for_upscale
801        {
802            debug!("Removing existing full cone NAT Gateway and private node VMs from the inventory. Old inventory: {modified_private_node_inventory:?}");
803            let mut names_to_keep = Vec::new();
804
805            for vm in new_full_cone_nat_gateway_new_vms_for_upscale.iter() {
806                let nat_gateway_name = vm.name.split('-').next_back().unwrap();
807                names_to_keep.push(nat_gateway_name);
808            }
809
810            modified_private_node_inventory
811                .full_cone_nat_gateway_vms
812                .retain(|vm| {
813                    let nat_gateway_name = vm.name.split('-').next_back().unwrap();
814                    names_to_keep.contains(&nat_gateway_name)
815                });
816            modified_private_node_inventory
817                .full_cone_private_node_vms
818                .retain(|vm| {
819                    let nat_gateway_name = vm.name.split('-').next_back().unwrap();
820                    names_to_keep.contains(&nat_gateway_name)
821                });
822            debug!("New inventory after removing existing full cone NAT Gateway and private node VMs: {modified_private_node_inventory:?}");
823        }
824
825        if modified_private_node_inventory
826            .full_cone_nat_gateway_vms
827            .is_empty()
828        {
829            error!("There are no full cone NAT Gateway VMs available to upscale");
830            return Ok(());
831        }
832
833        let private_node_ip_map = modified_private_node_inventory
834            .full_cone_private_node_and_gateway_map()?
835            .into_iter()
836            .map(|(k, v)| {
837                let gateway_name = if new_full_cone_nat_gateway_new_vms_for_upscale.is_some() {
838                    debug!("Upscaling, using public IP address for gateway name");
839                    v.public_ip_addr.to_string()
840                } else {
841                    v.name.clone()
842                };
843                (gateway_name, k.private_ip_addr)
844            })
845            .collect::<HashMap<String, IpAddr>>();
846
847        if private_node_ip_map.is_empty() {
848            println!("There are no full cone private node VM available to be routed through the full cone NAT Gateway");
849            return Err(Error::EmptyInventory(
850                AnsibleInventoryType::FullConePrivateNodes,
851            ));
852        }
853
854        let vars = extra_vars::build_nat_gateway_extra_vars_doc(
855            &options.name,
856            private_node_ip_map.clone(),
857            "step1",
858        );
859        debug!("Provisioning Full Cone NAT Gateway - Step 1 with vars: {vars}");
860        let gateway_inventory = if new_full_cone_nat_gateway_new_vms_for_upscale.is_some() {
861            debug!("Upscaling, using static inventory for full cone nat gateway.");
862            generate_full_cone_nat_gateway_static_environment_inventory(
863                &modified_private_node_inventory.full_cone_nat_gateway_vms,
864                &options.name,
865                &options.output_inventory_dir_path,
866            )?;
867
868            AnsibleInventoryType::FullConeNatGatewayStatic
869        } else {
870            AnsibleInventoryType::FullConeNatGateway
871        };
872        self.ansible_runner.run_playbook(
873            AnsiblePlaybook::StaticFullConeNatGateway,
874            gateway_inventory,
875            Some(vars),
876        )?;
877
878        // setup private node config
879        self.print_ansible_run_banner("Provisioning Full Cone Private Node Config");
880
881        generate_full_cone_private_node_static_environment_inventory(
882            &options.name,
883            &options.output_inventory_dir_path,
884            &private_node_inventory.full_cone_private_node_vms,
885            &private_node_inventory.full_cone_nat_gateway_vms,
886            &self.ssh_client.private_key_path,
887        )
888        .inspect_err(|err| {
889            error!("Failed to generate full cone private node static inv with err: {err:?}")
890        })?;
891
892        // For a new deployment, it's quite probable that SSH is available, because this part occurs
893        // after the genesis node has been provisioned. However, for a bootstrap deploy, we need to
894        // check that SSH is available before proceeding.
895        println!("Obtaining IP addresses for nodes...");
896        let inventory = self
897            .ansible_runner
898            .get_inventory(AnsibleInventoryType::FullConePrivateNodes, true)?;
899
900        println!("Waiting for SSH availability on Symmetric Private nodes...");
901        for vm in inventory.iter() {
902            println!(
903                "Checking SSH availability for {}: {}",
904                vm.name, vm.public_ip_addr
905            );
906            self.ssh_client
907                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
908                .map_err(|e| {
909                    println!("Failed to establish SSH connection to {}: {}", vm.name, e);
910                    e
911                })?;
912        }
913
914        println!("SSH is available on all nodes. Proceeding with provisioning...");
915
916        self.ansible_runner.run_playbook(
917            AnsiblePlaybook::PrivateNodeConfig,
918            AnsibleInventoryType::FullConePrivateNodes,
919            Some(
920                extra_vars::build_full_cone_private_node_config_extra_vars_docs(
921                    &private_node_inventory,
922                )?,
923            ),
924        )?;
925
926        // Step 2 of Full Cone NAT Gateway
927
928        let vars = extra_vars::build_nat_gateway_extra_vars_doc(
929            &options.name,
930            private_node_ip_map,
931            "step2",
932        );
933
934        self.print_ansible_run_banner("Provisioning Full Cone NAT Gateway - Step 2");
935        debug!("Provisioning Full Cone NAT Gateway - Step 2 with vars: {vars}");
936        self.ansible_runner.run_playbook(
937            AnsiblePlaybook::StaticFullConeNatGateway,
938            gateway_inventory,
939            Some(vars),
940        )?;
941
942        // provision the nodes
943
944        let home_dir = std::env::var("HOME").inspect_err(|err| {
945            println!("Failed to get home directory with error: {err:?}",);
946        })?;
947        let known_hosts_path = format!("{home_dir}/.ssh/known_hosts");
948        debug!("Cleaning up known hosts file at {known_hosts_path} ");
949        run_external_command(
950            PathBuf::from("rm"),
951            std::env::current_dir()?,
952            vec![known_hosts_path],
953            false,
954            false,
955        )?;
956
957        self.print_ansible_run_banner("Provision Full Cone Private Nodes");
958
959        self.ssh_client.set_full_cone_nat_routed_vms(
960            &private_node_inventory.full_cone_private_node_vms,
961            &private_node_inventory.full_cone_nat_gateway_vms,
962        )?;
963
964        self.provision_nodes(
965            options,
966            initial_contact_peer,
967            initial_network_contacts_url,
968            NodeType::FullConePrivateNode,
969        )?;
970
971        print_duration(start.elapsed());
972        Ok(())
973    }
974
975    pub fn provision_port_restricted_cone(
976        &self,
977        options: &ProvisionOptions,
978        initial_contact_peer: Option<String>,
979        initial_network_contacts_url: Option<String>,
980        private_node_inventory: PrivateNodeProvisionInventory,
981        new_port_restricted_cone_nat_gateway_new_vms_for_upscale: Option<Vec<VirtualMachine>>,
982    ) -> Result<()> {
983        // Step 1 of Port Restricted Cone NAT Gateway
984        let start = Instant::now();
985        self.print_ansible_run_banner("Provision Port Restricted Cone NAT Gateway - Step 1");
986
987        for vm in new_port_restricted_cone_nat_gateway_new_vms_for_upscale
988            .as_ref()
989            .unwrap_or(&private_node_inventory.port_restricted_cone_nat_gateway_vms)
990            .iter()
991        {
992            println!(
993                "Checking SSH availability for Port Restricted Cone NAT Gateway: {}",
994                vm.public_ip_addr
995            );
996            self.ssh_client
997                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
998                .map_err(|e| {
999                    println!("Failed to establish SSH connection to Port Restricted Cone NAT Gateway: {e}");
1000                    e
1001                })?;
1002        }
1003
1004        let mut modified_private_node_inventory = private_node_inventory.clone();
1005
1006        // If we are upscaling, then we cannot access the gateway VMs which are already deployed.
1007        if let Some(new_port_restricted_cone_nat_gateway_new_vms_for_upscale) =
1008            &new_port_restricted_cone_nat_gateway_new_vms_for_upscale
1009        {
1010            debug!("Removing existing port restricted cone NAT Gateway and private node VMs from the inventory. Old inventory: {modified_private_node_inventory:?}");
1011            let mut names_to_keep = Vec::new();
1012
1013            for vm in new_port_restricted_cone_nat_gateway_new_vms_for_upscale.iter() {
1014                let nat_gateway_name = vm.name.split('-').next_back().unwrap();
1015                names_to_keep.push(nat_gateway_name);
1016            }
1017
1018            modified_private_node_inventory
1019                .port_restricted_cone_nat_gateway_vms
1020                .retain(|vm| {
1021                    let nat_gateway_name = vm.name.split('-').next_back().unwrap();
1022                    names_to_keep.contains(&nat_gateway_name)
1023                });
1024            modified_private_node_inventory
1025                .port_restricted_cone_private_node_vms
1026                .retain(|vm| {
1027                    let nat_gateway_name = vm.name.split('-').next_back().unwrap();
1028                    names_to_keep.contains(&nat_gateway_name)
1029                });
1030            debug!("New inventory after removing existing port restricted cone NAT Gateway and private node VMs: {modified_private_node_inventory:?}");
1031        }
1032
1033        if modified_private_node_inventory
1034            .port_restricted_cone_nat_gateway_vms
1035            .is_empty()
1036        {
1037            error!("There are no port restricted cone NAT Gateway VMs available to upscale");
1038            return Ok(());
1039        }
1040
1041        let private_node_ip_map = modified_private_node_inventory
1042            .port_restricted_cone_private_node_and_gateway_map()?
1043            .into_iter()
1044            .map(|(k, v)| {
1045                let gateway_name =
1046                    if new_port_restricted_cone_nat_gateway_new_vms_for_upscale.is_some() {
1047                        debug!("Upscaling, using public IP address for gateway name");
1048                        v.public_ip_addr.to_string()
1049                    } else {
1050                        v.name.clone()
1051                    };
1052                (gateway_name, k.private_ip_addr)
1053            })
1054            .collect::<HashMap<String, IpAddr>>();
1055
1056        if private_node_ip_map.is_empty() {
1057            println!("There are no port restricted cone private node VM available to be routed through the port restricted cone NAT Gateway");
1058            return Err(Error::EmptyInventory(
1059                AnsibleInventoryType::PortRestrictedConePrivateNodes,
1060            ));
1061        }
1062
1063        let vars = extra_vars::build_nat_gateway_extra_vars_doc(
1064            &options.name,
1065            private_node_ip_map.clone(),
1066            "step1",
1067        );
1068        debug!("Provisioning Port Restricted Cone NAT Gateway - Step 1 with vars: {vars}");
1069        let gateway_inventory =
1070            if new_port_restricted_cone_nat_gateway_new_vms_for_upscale.is_some() {
1071                debug!("Upscaling, using static inventory for port restricted cone nat gateway.");
1072                generate_port_restricted_cone_nat_gateway_static_environment_inventory(
1073                    &modified_private_node_inventory.port_restricted_cone_nat_gateway_vms,
1074                    &options.name,
1075                    &options.output_inventory_dir_path,
1076                )?;
1077
1078                AnsibleInventoryType::PortRestrictedConeNatGatewayStatic
1079            } else {
1080                AnsibleInventoryType::PortRestrictedConeNatGateway
1081            };
1082        self.ansible_runner.run_playbook(
1083            AnsiblePlaybook::PortRestrictedConeNatGateway,
1084            gateway_inventory,
1085            Some(vars),
1086        )?;
1087
1088        // setup private node config
1089        self.print_ansible_run_banner("Provisioning Port Restricted Cone Private Node Config");
1090
1091        generate_port_restricted_cone_private_node_static_environment_inventory(
1092            &options.name,
1093            &options.output_inventory_dir_path,
1094            &private_node_inventory.port_restricted_cone_private_node_vms,
1095            &private_node_inventory.port_restricted_cone_nat_gateway_vms,
1096            &self.ssh_client.private_key_path,
1097        )
1098        .inspect_err(|err| {
1099            error!(
1100                "Failed to generate port restricted cone private node static inv with err: {err:?}"
1101            )
1102        })?;
1103
1104        // For a new deployment, it's quite probable that SSH is available, because this part occurs
1105        // after the genesis node has been provisioned. However, for a bootstrap deploy, we need to
1106        // check that SSH is available before proceeding.
1107        println!("Obtaining IP addresses for nodes...");
1108        let inventory = self
1109            .ansible_runner
1110            .get_inventory(AnsibleInventoryType::PortRestrictedConePrivateNodes, true)?;
1111
1112        println!("Waiting for SSH availability on Port Restricted Cone Private nodes...");
1113        for vm in inventory.iter() {
1114            println!(
1115                "Checking SSH availability for {}: {}",
1116                vm.name, vm.public_ip_addr
1117            );
1118            self.ssh_client
1119                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
1120                .map_err(|e| {
1121                    println!("Failed to establish SSH connection to {}: {}", vm.name, e);
1122                    e
1123                })?;
1124        }
1125
1126        println!("SSH is available on all nodes. Proceeding with provisioning...");
1127
1128        self.ansible_runner.run_playbook(
1129            AnsiblePlaybook::PrivateNodeConfig,
1130            AnsibleInventoryType::PortRestrictedConePrivateNodesStatic,
1131            Some(
1132                extra_vars::build_port_restricted_cone_private_node_config_extra_vars_docs(
1133                    &private_node_inventory,
1134                )?,
1135            ),
1136        )?;
1137
1138        // Step 2 of Port Restricted Cone NAT Gateway
1139
1140        let vars = extra_vars::build_nat_gateway_extra_vars_doc(
1141            &options.name,
1142            private_node_ip_map,
1143            "step2",
1144        );
1145
1146        self.print_ansible_run_banner("Provisioning Port Restricted Cone NAT Gateway - Step 2");
1147        debug!("Provisioning Port Restricted Cone NAT Gateway - Step 2 with vars: {vars}");
1148        self.ansible_runner.run_playbook(
1149            AnsiblePlaybook::PortRestrictedConeNatGateway,
1150            gateway_inventory,
1151            Some(vars),
1152        )?;
1153
1154        // provision the nodes
1155
1156        let home_dir = std::env::var("HOME").inspect_err(|err| {
1157            println!("Failed to get home directory with error: {err:?}",);
1158        })?;
1159        let known_hosts_path = format!("{home_dir}/.ssh/known_hosts");
1160        debug!("Cleaning up known hosts file at {known_hosts_path} ");
1161        run_external_command(
1162            PathBuf::from("rm"),
1163            std::env::current_dir()?,
1164            vec![known_hosts_path],
1165            false,
1166            false,
1167        )?;
1168
1169        self.print_ansible_run_banner("Provision Port Restricted Cone Private Nodes");
1170
1171        self.ssh_client.set_port_restricted_cone_nat_routed_vms(
1172            &private_node_inventory.port_restricted_cone_private_node_vms,
1173            &private_node_inventory.port_restricted_cone_nat_gateway_vms,
1174        )?;
1175
1176        self.provision_nodes(
1177            options,
1178            initial_contact_peer,
1179            initial_network_contacts_url,
1180            NodeType::PortRestrictedConePrivateNode,
1181        )?;
1182
1183        print_duration(start.elapsed());
1184        Ok(())
1185    }
1186
1187    pub fn provision_symmetric_nat_gateway(
1188        &self,
1189        options: &ProvisionOptions,
1190        private_node_inventory: &PrivateNodeProvisionInventory,
1191    ) -> Result<()> {
1192        let start = Instant::now();
1193        for vm in &private_node_inventory.symmetric_nat_gateway_vms {
1194            println!(
1195                "Checking SSH availability for Symmetric NAT Gateway: {}",
1196                vm.public_ip_addr
1197            );
1198            self.ssh_client
1199                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
1200                .map_err(|e| {
1201                    println!("Failed to establish SSH connection to Symmetric NAT Gateway: {e}");
1202                    e
1203                })?;
1204        }
1205
1206        let private_node_ip_map = private_node_inventory
1207            .symmetric_private_node_and_gateway_map()?
1208            .into_iter()
1209            .map(|(k, v)| (v.name.clone(), k.private_ip_addr))
1210            .collect::<HashMap<String, IpAddr>>();
1211
1212        if private_node_ip_map.is_empty() {
1213            println!("There are no Symmetric private node VM available to be routed through the Symmetric NAT Gateway");
1214            return Err(Error::EmptyInventory(
1215                AnsibleInventoryType::SymmetricPrivateNodes,
1216            ));
1217        }
1218
1219        let vars = extra_vars::build_nat_gateway_extra_vars_doc(
1220            &options.name,
1221            private_node_ip_map,
1222            "symmetric",
1223        );
1224        debug!("Provisioning Symmetric NAT Gateway with vars: {vars}");
1225        self.ansible_runner.run_playbook(
1226            AnsiblePlaybook::SymmetricNatGateway,
1227            AnsibleInventoryType::SymmetricNatGateway,
1228            Some(vars),
1229        )?;
1230
1231        print_duration(start.elapsed());
1232        Ok(())
1233    }
1234
1235    pub fn provision_nodes(
1236        &self,
1237        options: &ProvisionOptions,
1238        initial_contact_peer: Option<String>,
1239        initial_network_contacts_url: Option<String>,
1240        node_type: NodeType,
1241    ) -> Result<()> {
1242        let start = Instant::now();
1243        let mut write_older_cache_files = false;
1244        let (inventory_type, node_count) = match &node_type {
1245            NodeType::FullConePrivateNode => (
1246                node_type.to_ansible_inventory_type(),
1247                options.full_cone_private_node_count,
1248            ),
1249            NodeType::Generic => (node_type.to_ansible_inventory_type(), options.node_count),
1250            NodeType::Genesis => return Err(Error::InvalidNodeType(node_type)),
1251            NodeType::PeerCache => {
1252                write_older_cache_files = true;
1253                (
1254                    node_type.to_ansible_inventory_type(),
1255                    options.peer_cache_node_count,
1256                )
1257            }
1258            NodeType::SymmetricPrivateNode => (
1259                node_type.to_ansible_inventory_type(),
1260                options.symmetric_private_node_count,
1261            ),
1262            NodeType::Upnp => (
1263                node_type.to_ansible_inventory_type(),
1264                options.upnp_private_node_count,
1265            ),
1266            NodeType::PortRestrictedConePrivateNode => (
1267                node_type.to_ansible_inventory_type(),
1268                options.port_restricted_cone_private_node_count,
1269            ),
1270        };
1271
1272        // For a new deployment, it's quite probable that SSH is available, because this part occurs
1273        // after the genesis node has been provisioned. However, for a bootstrap deploy, we need to
1274        // check that SSH is available before proceeding.
1275        println!("Obtaining IP addresses for nodes...");
1276        let inventory = self.ansible_runner.get_inventory(inventory_type, true)?;
1277
1278        println!("Waiting for SSH availability on {node_type:?} nodes...");
1279        for vm in inventory.iter() {
1280            println!(
1281                "Checking SSH availability for {}: {}",
1282                vm.name, vm.public_ip_addr
1283            );
1284            self.ssh_client
1285                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
1286                .map_err(|e| {
1287                    println!("Failed to establish SSH connection to {}: {}", vm.name, e);
1288                    e
1289                })?;
1290        }
1291
1292        println!("SSH is available on all nodes. Proceeding with provisioning...");
1293
1294        let playbook = match node_type {
1295            NodeType::Generic => AnsiblePlaybook::Nodes,
1296            NodeType::PeerCache => AnsiblePlaybook::PeerCacheNodes,
1297            NodeType::FullConePrivateNode => AnsiblePlaybook::Nodes,
1298            NodeType::PortRestrictedConePrivateNode => AnsiblePlaybook::Nodes,
1299            NodeType::SymmetricPrivateNode => AnsiblePlaybook::Nodes,
1300            NodeType::Upnp => AnsiblePlaybook::Upnp,
1301            _ => return Err(Error::InvalidNodeType(node_type.clone())),
1302        };
1303        self.ansible_runner.run_playbook(
1304            playbook,
1305            inventory_type,
1306            Some(extra_vars::build_node_extra_vars_doc(
1307                &self.cloud_provider.to_string(),
1308                options,
1309                node_type.clone(),
1310                initial_contact_peer,
1311                initial_network_contacts_url,
1312                node_count,
1313                options.evm_network.clone(),
1314                write_older_cache_files,
1315            )?),
1316        )?;
1317
1318        print_duration(start.elapsed());
1319        Ok(())
1320    }
1321
1322    pub fn provision_symmetric_private_nodes(
1323        &self,
1324        options: &mut ProvisionOptions,
1325        initial_contact_peer: Option<String>,
1326        initial_network_contacts_url: Option<String>,
1327        private_node_inventory: &PrivateNodeProvisionInventory,
1328    ) -> Result<()> {
1329        let start = Instant::now();
1330        self.print_ansible_run_banner("Provision Symmetric Private Node Config");
1331
1332        generate_symmetric_private_node_static_environment_inventory(
1333            &options.name,
1334            &options.output_inventory_dir_path,
1335            &private_node_inventory.symmetric_private_node_vms,
1336            &private_node_inventory.symmetric_nat_gateway_vms,
1337            &self.ssh_client.private_key_path,
1338        )
1339        .inspect_err(|err| {
1340            error!("Failed to generate symmetric private node static inv with err: {err:?}")
1341        })?;
1342
1343        self.ssh_client.set_symmetric_nat_routed_vms(
1344            &private_node_inventory.symmetric_private_node_vms,
1345            &private_node_inventory.symmetric_nat_gateway_vms,
1346        )?;
1347
1348        let inventory_type = AnsibleInventoryType::SymmetricPrivateNodes;
1349
1350        // For a new deployment, it's quite probable that SSH is available, because this part occurs
1351        // after the genesis node has been provisioned. However, for a bootstrap deploy, we need to
1352        // check that SSH is available before proceeding.
1353        println!("Obtaining IP addresses for nodes...");
1354        let inventory = self.ansible_runner.get_inventory(inventory_type, true)?;
1355
1356        println!("Waiting for SSH availability on Symmetric Private nodes...");
1357        for vm in inventory.iter() {
1358            println!(
1359                "Checking SSH availability for {}: {}",
1360                vm.name, vm.public_ip_addr
1361            );
1362            self.ssh_client
1363                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
1364                .map_err(|e| {
1365                    println!("Failed to establish SSH connection to {}: {}", vm.name, e);
1366                    e
1367                })?;
1368        }
1369
1370        println!("SSH is available on all nodes. Proceeding with provisioning...");
1371
1372        self.ansible_runner.run_playbook(
1373            AnsiblePlaybook::PrivateNodeConfig,
1374            inventory_type,
1375            Some(
1376                extra_vars::build_symmetric_private_node_config_extra_vars_doc(
1377                    private_node_inventory,
1378                )?,
1379            ),
1380        )?;
1381
1382        println!("Provisioned Symmetric Private Node Config");
1383        print_duration(start.elapsed());
1384
1385        self.provision_nodes(
1386            options,
1387            initial_contact_peer,
1388            initial_network_contacts_url,
1389            NodeType::SymmetricPrivateNode,
1390        )?;
1391
1392        Ok(())
1393    }
1394
1395    pub async fn provision_downloaders(
1396        &self,
1397        options: &ProvisionOptions,
1398        genesis_multiaddr: Option<String>,
1399        genesis_network_contacts_url: Option<String>,
1400    ) -> Result<()> {
1401        let start = Instant::now();
1402
1403        println!("Running ansible against Client machine to start the downloader script.");
1404        debug!("Running ansible against Client machine to start the downloader script.");
1405
1406        self.ansible_runner.run_playbook(
1407            AnsiblePlaybook::Downloaders,
1408            AnsibleInventoryType::Clients,
1409            Some(extra_vars::build_downloaders_extra_vars_doc(
1410                &self.cloud_provider.to_string(),
1411                options,
1412                genesis_multiaddr,
1413                genesis_network_contacts_url,
1414            )?),
1415        )?;
1416        print_duration(start.elapsed());
1417        Ok(())
1418    }
1419
1420    pub async fn provision_static_downloaders(
1421        &self,
1422        options: &ProvisionOptions,
1423        genesis_multiaddr: Option<String>,
1424        genesis_network_contacts_url: Option<String>,
1425    ) -> Result<()> {
1426        let start = Instant::now();
1427
1428        println!("Running ansible against client machine to start the static downloaders.");
1429        debug!("Running ansible against client machine to start the static downloaders.");
1430
1431        self.ansible_runner.run_playbook(
1432            AnsiblePlaybook::StaticDownloaders,
1433            AnsibleInventoryType::Clients,
1434            Some(extra_vars::build_downloaders_extra_vars_doc(
1435                &self.cloud_provider.to_string(),
1436                options,
1437                genesis_multiaddr,
1438                genesis_network_contacts_url,
1439            )?),
1440        )?;
1441        print_duration(start.elapsed());
1442        Ok(())
1443    }
1444
1445    pub async fn provision_static_uploader(
1446        &self,
1447        options: &ProvisionOptions,
1448        genesis_multiaddr: Option<String>,
1449        genesis_network_contacts_url: Option<String>,
1450    ) -> Result<()> {
1451        let start = Instant::now();
1452
1453        println!("Running ansible against client machine to provision the static uploader");
1454        debug!("Running ansible against client machine to provision the static uploader");
1455
1456        let sk_map = if let Some(wallet_keys) = &options.wallet_secret_keys {
1457            self.prepare_pre_funded_wallets(wallet_keys).await?
1458        } else {
1459            self.deposit_funds_to_clients(&FundingOptions {
1460                evm_data_payments_address: options.evm_data_payments_address.clone(),
1461                evm_merkle_payments_address: options.evm_merkle_payments_address.clone(),
1462                evm_network: options.evm_network.clone(),
1463                evm_payment_token_address: options.evm_payment_token_address.clone(),
1464                evm_rpc_url: options.evm_rpc_url.clone(),
1465                funding_wallet_secret_key: options.funding_wallet_secret_key.clone(),
1466                gas_amount: options.gas_amount,
1467                token_amount: options.token_amount,
1468                uploaders_count: Some(1),
1469            })
1470            .await?
1471        };
1472
1473        let client_vms: Vec<_> = sk_map.keys().cloned().collect();
1474        self.ansible_runner.run_playbook(
1475            AnsiblePlaybook::StaticUploader,
1476            AnsibleInventoryType::Clients,
1477            Some(extra_vars::build_clients_extra_vars_doc(
1478                &self.cloud_provider.to_string(),
1479                options,
1480                genesis_multiaddr,
1481                genesis_network_contacts_url,
1482                &sk_map,
1483                &client_vms,
1484            )?),
1485        )?;
1486        print_duration(start.elapsed());
1487        Ok(())
1488    }
1489
1490    pub async fn provision_uploaders(
1491        &self,
1492        options: &ProvisionOptions,
1493        genesis_multiaddr: Option<String>,
1494        genesis_network_contacts_url: Option<String>,
1495    ) -> Result<()> {
1496        let start = Instant::now();
1497
1498        let sk_map = if let Some(wallet_keys) = &options.wallet_secret_keys {
1499            self.prepare_pre_funded_wallets(wallet_keys).await?
1500        } else {
1501            self.deposit_funds_to_clients(&FundingOptions {
1502                evm_data_payments_address: options.evm_data_payments_address.clone(),
1503                evm_merkle_payments_address: options.evm_merkle_payments_address.clone(),
1504                evm_network: options.evm_network.clone(),
1505                evm_payment_token_address: options.evm_payment_token_address.clone(),
1506                evm_rpc_url: options.evm_rpc_url.clone(),
1507                funding_wallet_secret_key: options.funding_wallet_secret_key.clone(),
1508                gas_amount: options.gas_amount,
1509                token_amount: options.token_amount,
1510                uploaders_count: options.uploaders_count,
1511            })
1512            .await?
1513        };
1514
1515        let client_vms: Vec<_> = sk_map.keys().cloned().collect();
1516        self.ansible_runner.run_playbook(
1517            AnsiblePlaybook::Uploaders,
1518            AnsibleInventoryType::Clients,
1519            Some(extra_vars::build_clients_extra_vars_doc(
1520                &self.cloud_provider.to_string(),
1521                options,
1522                genesis_multiaddr,
1523                genesis_network_contacts_url,
1524                &sk_map,
1525                &client_vms,
1526            )?),
1527        )?;
1528        print_duration(start.elapsed());
1529        Ok(())
1530    }
1531
1532    pub async fn provision_chunk_trackers(
1533        &self,
1534        options: &ProvisionOptions,
1535        genesis_multiaddr: Option<String>,
1536        genesis_network_contacts_url: Option<String>,
1537    ) -> Result<()> {
1538        let start = Instant::now();
1539
1540        let client_vms = self
1541            .ansible_runner
1542            .get_inventory(AnsibleInventoryType::Clients, true)?;
1543
1544        self.ansible_runner.run_playbook(
1545            AnsiblePlaybook::ChunkTrackers,
1546            AnsibleInventoryType::Clients,
1547            Some(extra_vars::build_clients_extra_vars_doc(
1548                &self.cloud_provider.to_string(),
1549                options,
1550                genesis_multiaddr,
1551                genesis_network_contacts_url,
1552                &HashMap::new(), // Empty map since chunk-trackers don't need wallets
1553                &client_vms,
1554            )?),
1555        )?;
1556        print_duration(start.elapsed());
1557        Ok(())
1558    }
1559
1560    pub async fn provision_data_retrieval(
1561        &self,
1562        options: &ProvisionOptions,
1563        genesis_network_contacts_url: Option<String>,
1564    ) -> Result<()> {
1565        let start = Instant::now();
1566
1567        self.ansible_runner.run_playbook(
1568            AnsiblePlaybook::DataRetrieval,
1569            AnsibleInventoryType::Clients,
1570            Some(extra_vars::build_data_retrieval_extra_vars_doc(
1571                &self.cloud_provider.to_string(),
1572                options,
1573                genesis_network_contacts_url,
1574            )?),
1575        )?;
1576        print_duration(start.elapsed());
1577        Ok(())
1578    }
1579
1580    pub async fn provision_repair_files(&self, options: &ProvisionOptions) -> Result<()> {
1581        let start = Instant::now();
1582
1583        let client_vms = self
1584            .ansible_runner
1585            .get_inventory(AnsibleInventoryType::Clients, true)?;
1586        let sk_map = self
1587            .prepare_pre_funded_wallets(
1588                &options
1589                    .wallet_secret_keys
1590                    .clone()
1591                    .ok_or_else(|| Error::RepairWalletAddressNotProvided)?,
1592            )
1593            .await?;
1594
1595        self.ansible_runner.run_playbook(
1596            AnsiblePlaybook::RepairFiles,
1597            AnsibleInventoryType::Clients,
1598            Some(extra_vars::build_clients_extra_vars_doc(
1599                &self.cloud_provider.to_string(),
1600                options,
1601                None,
1602                None,
1603                &sk_map,
1604                &client_vms,
1605            )?),
1606        )?;
1607        print_duration(start.elapsed());
1608        Ok(())
1609    }
1610
1611    pub async fn provision_scan_repair(&self, options: &ProvisionOptions) -> Result<()> {
1612        let start = Instant::now();
1613
1614        let client_vms = self
1615            .ansible_runner
1616            .get_inventory(AnsibleInventoryType::Clients, true)?;
1617        let sk_map = self
1618            .prepare_pre_funded_wallets(
1619                &options
1620                    .wallet_secret_keys
1621                    .clone()
1622                    .ok_or_else(|| Error::RepairWalletAddressNotProvided)?,
1623            )
1624            .await?;
1625
1626        self.ansible_runner.run_playbook(
1627            AnsiblePlaybook::ScanRepair,
1628            AnsibleInventoryType::Clients,
1629            Some(extra_vars::build_clients_extra_vars_doc(
1630                &self.cloud_provider.to_string(),
1631                options,
1632                None,
1633                None,
1634                &sk_map,
1635                &client_vms,
1636            )?),
1637        )?;
1638        print_duration(start.elapsed());
1639        Ok(())
1640    }
1641
1642    pub fn start_nodes(
1643        &self,
1644        environment_name: &str,
1645        interval: Duration,
1646        node_type: Option<NodeType>,
1647        custom_inventory: Option<Vec<VirtualMachine>>,
1648    ) -> Result<()> {
1649        let mut extra_vars = ExtraVarsDocBuilder::default();
1650        extra_vars.add_variable("interval", &interval.as_millis().to_string());
1651
1652        if let Some(node_type) = node_type {
1653            println!("Running the start nodes playbook for {node_type:?} nodes");
1654            self.ansible_runner.run_playbook(
1655                AnsiblePlaybook::StartNodes,
1656                node_type.to_ansible_inventory_type(),
1657                Some(extra_vars.build()),
1658            )?;
1659            return Ok(());
1660        }
1661
1662        if let Some(custom_inventory) = custom_inventory {
1663            println!("Running the start nodes playbook with a custom inventory");
1664            generate_custom_environment_inventory(
1665                &custom_inventory,
1666                environment_name,
1667                &self.ansible_runner.working_directory_path.join("inventory"),
1668            )?;
1669            self.ansible_runner.run_playbook(
1670                AnsiblePlaybook::StartNodes,
1671                AnsibleInventoryType::Custom,
1672                Some(extra_vars.build()),
1673            )?;
1674            return Ok(());
1675        }
1676
1677        println!("Running the start nodes playbook for all node types");
1678        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1679            self.ansible_runner.run_playbook(
1680                AnsiblePlaybook::StartNodes,
1681                node_inv_type,
1682                Some(extra_vars.build()),
1683            )?;
1684        }
1685        Ok(())
1686    }
1687
1688    pub fn apply_delete_node_records_cron(
1689        &self,
1690        environment_name: &str,
1691        node_type: Option<NodeType>,
1692        custom_inventory: Option<Vec<VirtualMachine>>,
1693    ) -> Result<()> {
1694        if let Some(node_type) = node_type {
1695            println!("Applying delete node records cron for {node_type:?} nodes");
1696            self.ansible_runner.run_playbook(
1697                AnsiblePlaybook::ApplyDeleteNodeRecordsCron,
1698                node_type.to_ansible_inventory_type(),
1699                None,
1700            )?;
1701            return Ok(());
1702        }
1703
1704        if let Some(custom_inventory) = custom_inventory {
1705            println!("Applying delete node records cron with a custom inventory");
1706            generate_custom_environment_inventory(
1707                &custom_inventory,
1708                environment_name,
1709                &self.ansible_runner.working_directory_path.join("inventory"),
1710            )?;
1711            self.ansible_runner.run_playbook(
1712                AnsiblePlaybook::ApplyDeleteNodeRecordsCron,
1713                AnsibleInventoryType::Custom,
1714                None,
1715            )?;
1716            return Ok(());
1717        }
1718
1719        println!("Applying delete node records cron for all node types");
1720        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1721            self.ansible_runner.run_playbook(
1722                AnsiblePlaybook::ApplyDeleteNodeRecordsCron,
1723                node_inv_type,
1724                None,
1725            )?;
1726        }
1727        Ok(())
1728    }
1729
1730    pub fn reset_nodes(
1731        &self,
1732        environment_name: &str,
1733        node_type: Option<NodeType>,
1734        custom_inventory: Option<Vec<VirtualMachine>>,
1735    ) -> Result<()> {
1736        if let Some(node_type) = node_type {
1737            println!("Running the reset nodes playbook for {node_type:?} nodes");
1738            self.ansible_runner.run_playbook(
1739                AnsiblePlaybook::ResetNodes,
1740                node_type.to_ansible_inventory_type(),
1741                None,
1742            )?;
1743            return Ok(());
1744        }
1745
1746        if let Some(custom_inventory) = custom_inventory {
1747            println!("Running the reset nodes playbook with a custom inventory");
1748            generate_custom_environment_inventory(
1749                &custom_inventory,
1750                environment_name,
1751                &self.ansible_runner.working_directory_path.join("inventory"),
1752            )?;
1753            self.ansible_runner.run_playbook(
1754                AnsiblePlaybook::ResetNodes,
1755                AnsibleInventoryType::Custom,
1756                None,
1757            )?;
1758            return Ok(());
1759        }
1760
1761        println!("Running the reset nodes playbook for all node types");
1762        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1763            self.ansible_runner
1764                .run_playbook(AnsiblePlaybook::ResetNodes, node_inv_type, None)?;
1765        }
1766        Ok(())
1767    }
1768
1769    pub fn status(&self) -> Result<()> {
1770        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1771            self.ansible_runner
1772                .run_playbook(AnsiblePlaybook::Status, node_inv_type, None)?;
1773        }
1774        Ok(())
1775    }
1776
1777    pub fn start_telegraf(
1778        &self,
1779        environment_name: &str,
1780        node_type: Option<NodeType>,
1781        custom_inventory: Option<Vec<VirtualMachine>>,
1782    ) -> Result<()> {
1783        if let Some(node_type) = node_type {
1784            println!("Running the start telegraf playbook for {node_type:?} nodes");
1785            self.ansible_runner.run_playbook(
1786                AnsiblePlaybook::StartTelegraf,
1787                node_type.to_ansible_inventory_type(),
1788                None,
1789            )?;
1790            return Ok(());
1791        }
1792
1793        if let Some(custom_inventory) = custom_inventory {
1794            println!("Running the start telegraf playbook with a custom inventory");
1795            generate_custom_environment_inventory(
1796                &custom_inventory,
1797                environment_name,
1798                &self.ansible_runner.working_directory_path.join("inventory"),
1799            )?;
1800            self.ansible_runner.run_playbook(
1801                AnsiblePlaybook::StartTelegraf,
1802                AnsibleInventoryType::Custom,
1803                None,
1804            )?;
1805            return Ok(());
1806        }
1807
1808        println!("Running the start telegraf playbook for all node types");
1809        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1810            self.ansible_runner.run_playbook(
1811                AnsiblePlaybook::StartTelegraf,
1812                node_inv_type,
1813                None,
1814            )?;
1815        }
1816
1817        Ok(())
1818    }
1819
1820    pub fn stop_nodes(
1821        &self,
1822        environment_name: &str,
1823        interval: Duration,
1824        node_type: Option<NodeType>,
1825        custom_inventory: Option<Vec<VirtualMachine>>,
1826        delay: Option<u64>,
1827        service_names: Option<Vec<String>>,
1828    ) -> Result<()> {
1829        let mut extra_vars = ExtraVarsDocBuilder::default();
1830        extra_vars.add_variable("interval", &interval.as_millis().to_string());
1831        if let Some(delay) = delay {
1832            extra_vars.add_variable("delay", &delay.to_string());
1833        }
1834        if let Some(service_names) = service_names {
1835            extra_vars.add_list_variable("service_names", service_names);
1836        }
1837        let extra_vars = extra_vars.build();
1838
1839        if let Some(node_type) = node_type {
1840            println!("Running the stop nodes playbook for {node_type:?} nodes");
1841            self.ansible_runner.run_playbook(
1842                AnsiblePlaybook::StopNodes,
1843                node_type.to_ansible_inventory_type(),
1844                Some(extra_vars),
1845            )?;
1846            return Ok(());
1847        }
1848
1849        if let Some(custom_inventory) = custom_inventory {
1850            println!("Running the stop nodes playbook with a custom inventory");
1851            generate_custom_environment_inventory(
1852                &custom_inventory,
1853                environment_name,
1854                &self.ansible_runner.working_directory_path.join("inventory"),
1855            )?;
1856            self.ansible_runner.run_playbook(
1857                AnsiblePlaybook::StopNodes,
1858                AnsibleInventoryType::Custom,
1859                Some(extra_vars),
1860            )?;
1861            return Ok(());
1862        }
1863
1864        println!("Running the stop nodes playbook for all node types");
1865        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1866            self.ansible_runner.run_playbook(
1867                AnsiblePlaybook::StopNodes,
1868                node_inv_type,
1869                Some(extra_vars.clone()),
1870            )?;
1871        }
1872
1873        Ok(())
1874    }
1875
1876    pub fn stop_telegraf(
1877        &self,
1878        environment_name: &str,
1879        node_type: Option<NodeType>,
1880        custom_inventory: Option<Vec<VirtualMachine>>,
1881    ) -> Result<()> {
1882        if let Some(node_type) = node_type {
1883            println!("Running the stop telegraf playbook for {node_type:?} nodes");
1884            self.ansible_runner.run_playbook(
1885                AnsiblePlaybook::StopTelegraf,
1886                node_type.to_ansible_inventory_type(),
1887                None,
1888            )?;
1889            return Ok(());
1890        }
1891
1892        if let Some(custom_inventory) = custom_inventory {
1893            println!("Running the stop telegraf playbook with a custom inventory");
1894            generate_custom_environment_inventory(
1895                &custom_inventory,
1896                environment_name,
1897                &self.ansible_runner.working_directory_path.join("inventory"),
1898            )?;
1899            self.ansible_runner.run_playbook(
1900                AnsiblePlaybook::StopTelegraf,
1901                AnsibleInventoryType::Custom,
1902                None,
1903            )?;
1904            return Ok(());
1905        }
1906
1907        println!("Running the stop telegraf playbook for all node types");
1908        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1909            self.ansible_runner
1910                .run_playbook(AnsiblePlaybook::StopTelegraf, node_inv_type, None)?;
1911        }
1912
1913        Ok(())
1914    }
1915
1916    pub fn upgrade_node_telegraf(&self, name: &str) -> Result<()> {
1917        self.ansible_runner.run_playbook(
1918            AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1919            AnsibleInventoryType::PeerCacheNodes,
1920            Some(extra_vars::build_node_telegraf_upgrade(
1921                name,
1922                &NodeType::PeerCache,
1923            )?),
1924        )?;
1925        self.ansible_runner.run_playbook(
1926            AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1927            AnsibleInventoryType::Nodes,
1928            Some(extra_vars::build_node_telegraf_upgrade(
1929                name,
1930                &NodeType::Generic,
1931            )?),
1932        )?;
1933
1934        self.ansible_runner.run_playbook(
1935            AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1936            AnsibleInventoryType::SymmetricPrivateNodes,
1937            Some(extra_vars::build_node_telegraf_upgrade(
1938                name,
1939                &NodeType::SymmetricPrivateNode,
1940            )?),
1941        )?;
1942
1943        self.ansible_runner.run_playbook(
1944            AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1945            AnsibleInventoryType::FullConePrivateNodes,
1946            Some(extra_vars::build_node_telegraf_upgrade(
1947                name,
1948                &NodeType::FullConePrivateNode,
1949            )?),
1950        )?;
1951        Ok(())
1952    }
1953
1954    pub fn upgrade_client_telegraf(&self, name: &str) -> Result<()> {
1955        self.ansible_runner.run_playbook(
1956            AnsiblePlaybook::UpgradeClientTelegrafConfig,
1957            AnsibleInventoryType::Clients,
1958            Some(extra_vars::build_client_telegraf_upgrade(name)?),
1959        )?;
1960        Ok(())
1961    }
1962
1963    pub fn upgrade_nodes(&self, options: &UpgradeOptions) -> Result<()> {
1964        if let Some(custom_inventory) = &options.custom_inventory {
1965            println!("Running the UpgradeNodes with a custom inventory");
1966            generate_custom_environment_inventory(
1967                custom_inventory,
1968                &options.name,
1969                &self.ansible_runner.working_directory_path.join("inventory"),
1970            )?;
1971            match self.ansible_runner.run_playbook(
1972                AnsiblePlaybook::UpgradeNodes,
1973                AnsibleInventoryType::Custom,
1974                Some(options.get_ansible_vars()),
1975            ) {
1976                Ok(()) => println!("All nodes were successfully upgraded"),
1977                Err(_) => {
1978                    println!("WARNING: some nodes may not have been upgraded or restarted");
1979                }
1980            }
1981            return Ok(());
1982        }
1983
1984        if let Some(node_type) = &options.node_type {
1985            println!("Running the UpgradeNodes playbook for {node_type:?} nodes");
1986            match self.ansible_runner.run_playbook(
1987                AnsiblePlaybook::UpgradeNodes,
1988                node_type.to_ansible_inventory_type(),
1989                Some(options.get_ansible_vars()),
1990            ) {
1991                Ok(()) => println!("All {node_type:?} nodes were successfully upgraded"),
1992                Err(_) => {
1993                    println!(
1994                        "WARNING: some {node_type:?} nodes may not have been upgraded or restarted"
1995                    );
1996                }
1997            }
1998            return Ok(());
1999        }
2000
2001        println!("Running the UpgradeNodes playbook for all node types");
2002
2003        match self.ansible_runner.run_playbook(
2004            AnsiblePlaybook::UpgradeNodes,
2005            AnsibleInventoryType::PeerCacheNodes,
2006            Some(options.get_ansible_vars()),
2007        ) {
2008            Ok(()) => println!("All Peer Cache nodes were successfully upgraded"),
2009            Err(_) => {
2010                println!("WARNING: some Peer Cacche nodes may not have been upgraded or restarted");
2011            }
2012        }
2013        match self.ansible_runner.run_playbook(
2014            AnsiblePlaybook::UpgradeNodes,
2015            AnsibleInventoryType::Nodes,
2016            Some(options.get_ansible_vars()),
2017        ) {
2018            Ok(()) => println!("All generic nodes were successfully upgraded"),
2019            Err(_) => {
2020                println!("WARNING: some nodes may not have been upgraded or restarted");
2021            }
2022        }
2023        match self.ansible_runner.run_playbook(
2024            AnsiblePlaybook::UpgradeNodes,
2025            AnsibleInventoryType::SymmetricPrivateNodes,
2026            Some(options.get_ansible_vars()),
2027        ) {
2028            Ok(()) => println!("All private nodes were successfully upgraded"),
2029            Err(_) => {
2030                println!("WARNING: some nodes may not have been upgraded or restarted");
2031            }
2032        }
2033        // Don't use AnsibleInventoryType::iter_node_type() here, because the genesis node should be upgraded last
2034        match self.ansible_runner.run_playbook(
2035            AnsiblePlaybook::UpgradeNodes,
2036            AnsibleInventoryType::Genesis,
2037            Some(options.get_ansible_vars()),
2038        ) {
2039            Ok(()) => println!("The genesis nodes was successfully upgraded"),
2040            Err(_) => {
2041                println!("WARNING: the genesis node may not have been upgraded or restarted");
2042            }
2043        }
2044        Ok(())
2045    }
2046
2047    pub fn upgrade_antctl(
2048        &self,
2049        environment_name: &str,
2050        version: &Version,
2051        node_type: Option<NodeType>,
2052        custom_inventory: Option<Vec<VirtualMachine>>,
2053    ) -> Result<()> {
2054        let mut extra_vars = ExtraVarsDocBuilder::default();
2055        extra_vars.add_variable("version", &version.to_string());
2056
2057        if let Some(node_type) = node_type {
2058            println!("Running the upgrade safenode-manager playbook for {node_type:?} nodes");
2059            self.ansible_runner.run_playbook(
2060                AnsiblePlaybook::UpgradeAntctl,
2061                node_type.to_ansible_inventory_type(),
2062                Some(extra_vars.build()),
2063            )?;
2064            return Ok(());
2065        }
2066
2067        if let Some(custom_inventory) = custom_inventory {
2068            println!("Running the upgrade safenode-manager playbook with a custom inventory");
2069            generate_custom_environment_inventory(
2070                &custom_inventory,
2071                environment_name,
2072                &self.ansible_runner.working_directory_path.join("inventory"),
2073            )?;
2074            self.ansible_runner.run_playbook(
2075                AnsiblePlaybook::UpgradeAntctl,
2076                AnsibleInventoryType::Custom,
2077                Some(extra_vars.build()),
2078            )?;
2079            return Ok(());
2080        }
2081
2082        println!("Running the upgrade safenode-manager playbook for all node types");
2083        for node_inv_type in AnsibleInventoryType::iter_node_type() {
2084            self.ansible_runner.run_playbook(
2085                AnsiblePlaybook::UpgradeAntctl,
2086                node_inv_type,
2087                Some(extra_vars.build()),
2088            )?;
2089        }
2090
2091        Ok(())
2092    }
2093
2094    pub fn upgrade_nginx_config(
2095        &self,
2096        environment_name: &str,
2097        custom_inventory: Option<Vec<VirtualMachine>>,
2098    ) -> Result<()> {
2099        if let Some(custom_inventory) = custom_inventory {
2100            println!("Running the upgrade nginx config playbook with a custom inventory");
2101            generate_custom_environment_inventory(
2102                &custom_inventory,
2103                environment_name,
2104                &self.ansible_runner.working_directory_path.join("inventory"),
2105            )?;
2106            self.ansible_runner.run_playbook(
2107                AnsiblePlaybook::UpgradeNginx,
2108                AnsibleInventoryType::Custom,
2109                None,
2110            )?;
2111            return Ok(());
2112        }
2113
2114        println!("Running the upgrade nginx config playbook for peer cache nodes");
2115        self.ansible_runner.run_playbook(
2116            AnsiblePlaybook::UpgradeNginx,
2117            AnsibleInventoryType::PeerCacheNodes,
2118            None,
2119        )?;
2120        Ok(())
2121    }
2122
2123    pub fn upgrade_geoip_telegraf(&self, name: &str) -> Result<()> {
2124        self.ansible_runner.run_playbook(
2125            AnsiblePlaybook::UpgradeGeoIpTelegrafConfig,
2126            AnsibleInventoryType::PeerCacheNodes,
2127            Some(extra_vars::build_node_telegraf_upgrade(
2128                name,
2129                &NodeType::PeerCache,
2130            )?),
2131        )?;
2132        Ok(())
2133    }
2134
2135    pub fn print_ansible_run_banner(&self, s: &str) {
2136        let ansible_run_msg = "Ansible Run: ";
2137        let line = "=".repeat(s.len() + ansible_run_msg.len());
2138        println!("{line}\n{ansible_run_msg}{s}\n{line}");
2139    }
2140}