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_symmetric_private_node_static_environment_inventory,
12    },
13    AnsibleInventoryType, AnsiblePlaybook, AnsibleRunner,
14};
15use crate::{
16    ansible::inventory::{
17        generate_custom_environment_inventory,
18        generate_full_cone_nat_gateway_static_environment_inventory,
19    },
20    bootstrap::BootstrapOptions,
21    deploy::DeployOptions,
22    error::{Error, Result},
23    funding::FundingOptions,
24    inventory::{DeploymentNodeRegistries, VirtualMachine},
25    print_duration, run_external_command, BinaryOption, CloudProvider, EvmNetwork, LogFormat,
26    NodeType, SshClient, UpgradeOptions,
27};
28use ant_service_management::NodeRegistry;
29use evmlib::common::U256;
30use log::{debug, error, trace};
31use semver::Version;
32use serde::{Deserialize, Serialize};
33use std::{
34    collections::HashMap,
35    net::{IpAddr, SocketAddr},
36    path::PathBuf,
37    time::{Duration, Instant},
38};
39use walkdir::WalkDir;
40
41use crate::ansible::extra_vars;
42
43pub const DEFAULT_BETA_ENCRYPTION_KEY: &str =
44    "49113d2083f57a976076adbe85decb75115820de1e6e74b47e0429338cef124a";
45
46#[derive(Clone, Serialize, Deserialize)]
47pub struct ProvisionOptions {
48    /// The safe version is also in the binary option, but only for an initial deployment.
49    /// For the upscale, it needs to be provided explicitly, because currently it is not
50    /// recorded in the inventory.
51    pub ant_version: Option<String>,
52    pub binary_option: BinaryOption,
53    pub chunk_size: Option<u64>,
54    pub client_env_variables: Option<Vec<(String, String)>>,
55    pub downloaders_count: u16,
56    pub enable_telegraf: bool,
57    pub evm_data_payments_address: Option<String>,
58    pub evm_network: EvmNetwork,
59    pub evm_payment_token_address: Option<String>,
60    pub evm_rpc_url: Option<String>,
61    pub full_cone_private_node_count: u16,
62    pub funding_wallet_secret_key: Option<String>,
63    pub gas_amount: Option<U256>,
64    pub interval: Duration,
65    pub log_format: Option<LogFormat>,
66    pub logstash_details: Option<(String, Vec<SocketAddr>)>,
67    pub max_archived_log_files: u16,
68    pub max_log_files: u16,
69    pub name: String,
70    pub network_id: Option<u8>,
71    pub node_count: u16,
72    pub node_env_variables: Option<Vec<(String, String)>>,
73    pub output_inventory_dir_path: PathBuf,
74    pub peer_cache_node_count: u16,
75    pub public_rpc: bool,
76    pub rewards_address: String,
77    pub symmetric_private_node_count: u16,
78    pub token_amount: Option<U256>,
79    pub uploaders_count: Option<u16>,
80}
81
82/// These are obtained by running the inventory playbook
83#[derive(Clone, Debug)]
84pub struct PrivateNodeProvisionInventory {
85    pub full_cone_nat_gateway_vms: Vec<VirtualMachine>,
86    pub full_cone_private_node_vms: Vec<VirtualMachine>,
87    pub symmetric_nat_gateway_vms: Vec<VirtualMachine>,
88    pub symmetric_private_node_vms: Vec<VirtualMachine>,
89}
90
91impl PrivateNodeProvisionInventory {
92    pub fn new(
93        provisioner: &AnsibleProvisioner,
94        full_cone_private_node_vm_count: Option<u16>,
95        symmetric_private_node_vm_count: Option<u16>,
96    ) -> Result<Self> {
97        // All the environment types set private_node_vm count to >0 if not specified.
98        let should_provision_full_cone_private_nodes = full_cone_private_node_vm_count
99            .map(|count| count > 0)
100            .unwrap_or(true);
101        let should_provision_symmetric_private_nodes = symmetric_private_node_vm_count
102            .map(|count| count > 0)
103            .unwrap_or(true);
104
105        let mut inventory = Self {
106            full_cone_nat_gateway_vms: Default::default(),
107            full_cone_private_node_vms: Default::default(),
108            symmetric_nat_gateway_vms: Default::default(),
109            symmetric_private_node_vms: Default::default(),
110        };
111
112        if should_provision_full_cone_private_nodes {
113            let full_cone_private_node_vms = provisioner
114                .ansible_runner
115                .get_inventory(AnsibleInventoryType::FullConePrivateNodes, true)
116                .inspect_err(|err| {
117                    println!("Failed to obtain the inventory of Full Cone private node: {err:?}");
118                })?;
119
120            let full_cone_nat_gateway_inventory = provisioner
121                .ansible_runner
122                .get_inventory(AnsibleInventoryType::FullConeNatGateway, true)
123                .inspect_err(|err| {
124                    println!("Failed to get Full Cone NAT Gateway inventory {err:?}");
125                })?;
126
127            if full_cone_nat_gateway_inventory.len() != full_cone_private_node_vms.len() {
128                println!("The number of Full Cone private nodes does not match the number of Full Cone NAT Gateway VMs");
129                return Err(Error::VmCountMismatch(
130                    Some(AnsibleInventoryType::FullConePrivateNodes),
131                    Some(AnsibleInventoryType::FullConeNatGateway),
132                ));
133            }
134
135            inventory.full_cone_private_node_vms = full_cone_private_node_vms;
136            inventory.full_cone_nat_gateway_vms = full_cone_nat_gateway_inventory;
137        }
138
139        if should_provision_symmetric_private_nodes {
140            let symmetric_private_node_vms = provisioner
141                .ansible_runner
142                .get_inventory(AnsibleInventoryType::SymmetricPrivateNodes, true)
143                .inspect_err(|err| {
144                    println!("Failed to obtain the inventory of Symmetric private node: {err:?}");
145                })?;
146
147            let symmetric_nat_gateway_inventory = provisioner
148                .ansible_runner
149                .get_inventory(AnsibleInventoryType::SymmetricNatGateway, true)
150                .inspect_err(|err| {
151                    println!("Failed to get Symmetric NAT Gateway inventory {err:?}");
152                })?;
153
154            if symmetric_nat_gateway_inventory.len() != symmetric_private_node_vms.len() {
155                println!("The number of Symmetric private nodes does not match the number of Symmetric NAT Gateway VMs");
156                return Err(Error::VmCountMismatch(
157                    Some(AnsibleInventoryType::SymmetricPrivateNodes),
158                    Some(AnsibleInventoryType::SymmetricNatGateway),
159                ));
160            }
161
162            inventory.symmetric_private_node_vms = symmetric_private_node_vms;
163            inventory.symmetric_nat_gateway_vms = symmetric_nat_gateway_inventory;
164        }
165
166        Ok(inventory)
167    }
168
169    pub fn should_provision_full_cone_private_nodes(&self) -> bool {
170        !self.full_cone_private_node_vms.is_empty()
171    }
172
173    pub fn should_provision_symmetric_private_nodes(&self) -> bool {
174        !self.symmetric_private_node_vms.is_empty()
175    }
176
177    pub fn symmetric_private_node_and_gateway_map(
178        &self,
179    ) -> Result<HashMap<VirtualMachine, VirtualMachine>> {
180        Self::match_private_node_vm_and_gateway_vm(
181            &self.symmetric_private_node_vms,
182            &self.symmetric_nat_gateway_vms,
183        )
184    }
185
186    pub fn full_cone_private_node_and_gateway_map(
187        &self,
188    ) -> Result<HashMap<VirtualMachine, VirtualMachine>> {
189        Self::match_private_node_vm_and_gateway_vm(
190            &self.full_cone_private_node_vms,
191            &self.full_cone_nat_gateway_vms,
192        )
193    }
194
195    pub fn match_private_node_vm_and_gateway_vm(
196        private_node_vms: &[VirtualMachine],
197        nat_gateway_vms: &[VirtualMachine],
198    ) -> Result<HashMap<VirtualMachine, VirtualMachine>> {
199        if private_node_vms.len() != nat_gateway_vms.len() {
200            println!(
201            "The number of private node VMs ({}) does not match the number of NAT Gateway VMs ({})",
202            private_node_vms.len(),
203            nat_gateway_vms.len()
204        );
205            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:?}");
206            return Err(Error::VmCountMismatch(None, None));
207        }
208
209        let mut map = HashMap::new();
210        for private_vm in private_node_vms {
211            let nat_gateway = nat_gateway_vms
212                .iter()
213                .find(|vm| {
214                    let private_node_name = private_vm.name.split('-').last().unwrap();
215                    let nat_gateway_name = vm.name.split('-').last().unwrap();
216                    private_node_name == nat_gateway_name
217                })
218                .ok_or_else(|| {
219                    println!(
220                        "Failed to find a matching NAT Gateway for private node: {}",
221                        private_vm.name
222                    );
223                    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);
224                    Error::VmCountMismatch(None, None)
225                })?;
226
227            let _ = map.insert(private_vm.clone(), nat_gateway.clone());
228        }
229
230        Ok(map)
231    }
232}
233
234impl From<BootstrapOptions> for ProvisionOptions {
235    fn from(bootstrap_options: BootstrapOptions) -> Self {
236        ProvisionOptions {
237            ant_version: None,
238            binary_option: bootstrap_options.binary_option,
239            chunk_size: bootstrap_options.chunk_size,
240            downloaders_count: 0,
241            enable_telegraf: true,
242            node_env_variables: bootstrap_options.node_env_variables,
243            evm_data_payments_address: bootstrap_options.evm_data_payments_address,
244            evm_network: bootstrap_options.evm_network,
245            evm_payment_token_address: bootstrap_options.evm_payment_token_address,
246            evm_rpc_url: bootstrap_options.evm_rpc_url,
247            full_cone_private_node_count: bootstrap_options.full_cone_private_node_count,
248            funding_wallet_secret_key: None,
249            gas_amount: None,
250            interval: bootstrap_options.interval,
251            log_format: bootstrap_options.log_format,
252            logstash_details: None,
253            max_archived_log_files: bootstrap_options.max_archived_log_files,
254            max_log_files: bootstrap_options.max_log_files,
255            name: bootstrap_options.name,
256            network_id: bootstrap_options.network_id,
257            node_count: bootstrap_options.node_count,
258            output_inventory_dir_path: bootstrap_options.output_inventory_dir_path,
259            peer_cache_node_count: 0,
260            symmetric_private_node_count: bootstrap_options.symmetric_private_node_count,
261            public_rpc: false,
262            rewards_address: bootstrap_options.rewards_address,
263            token_amount: None,
264            uploaders_count: None,
265            client_env_variables: None,
266        }
267    }
268}
269
270impl From<DeployOptions> for ProvisionOptions {
271    fn from(deploy_options: DeployOptions) -> Self {
272        ProvisionOptions {
273            ant_version: None,
274            binary_option: deploy_options.binary_option,
275            chunk_size: deploy_options.chunk_size,
276            downloaders_count: deploy_options.downloaders_count,
277            enable_telegraf: deploy_options.enable_telegraf,
278            node_env_variables: deploy_options.node_env_variables,
279            evm_data_payments_address: deploy_options.evm_data_payments_address,
280            evm_network: deploy_options.evm_network,
281            evm_payment_token_address: deploy_options.evm_payment_token_address,
282            evm_rpc_url: deploy_options.evm_rpc_url,
283            full_cone_private_node_count: deploy_options.full_cone_private_node_count,
284            funding_wallet_secret_key: deploy_options.funding_wallet_secret_key,
285            gas_amount: deploy_options.initial_gas,
286            interval: deploy_options.interval,
287            log_format: deploy_options.log_format,
288            logstash_details: deploy_options.logstash_details,
289            max_archived_log_files: deploy_options.max_archived_log_files,
290            max_log_files: deploy_options.max_log_files,
291            name: deploy_options.name,
292            network_id: deploy_options.network_id,
293            node_count: deploy_options.node_count,
294            output_inventory_dir_path: deploy_options.output_inventory_dir_path,
295            peer_cache_node_count: deploy_options.peer_cache_node_count,
296            symmetric_private_node_count: deploy_options.symmetric_private_node_count,
297            public_rpc: deploy_options.public_rpc,
298            rewards_address: deploy_options.rewards_address,
299            token_amount: deploy_options.initial_tokens,
300            uploaders_count: Some(deploy_options.uploaders_count),
301            client_env_variables: deploy_options.client_env_variables,
302        }
303    }
304}
305
306#[derive(Clone)]
307pub struct AnsibleProvisioner {
308    pub ansible_runner: AnsibleRunner,
309    pub cloud_provider: CloudProvider,
310    pub ssh_client: SshClient,
311}
312
313impl AnsibleProvisioner {
314    pub fn new(
315        ansible_runner: AnsibleRunner,
316        cloud_provider: CloudProvider,
317        ssh_client: SshClient,
318    ) -> Self {
319        Self {
320            ansible_runner,
321            cloud_provider,
322            ssh_client,
323        }
324    }
325
326    pub fn build_safe_network_binaries(&self, options: &ProvisionOptions) -> Result<()> {
327        let start = Instant::now();
328        println!("Obtaining IP address for build VM...");
329        let build_inventory = self
330            .ansible_runner
331            .get_inventory(AnsibleInventoryType::Build, true)?;
332        let build_ip = build_inventory[0].public_ip_addr;
333        self.ssh_client
334            .wait_for_ssh_availability(&build_ip, &self.cloud_provider.get_ssh_user())?;
335
336        println!("Running ansible against build VM...");
337        let extra_vars = extra_vars::build_binaries_extra_vars_doc(options)?;
338        self.ansible_runner.run_playbook(
339            AnsiblePlaybook::Build,
340            AnsibleInventoryType::Build,
341            Some(extra_vars),
342        )?;
343        print_duration(start.elapsed());
344        Ok(())
345    }
346
347    pub fn cleanup_node_logs(&self, setup_cron: bool) -> Result<()> {
348        for node_inv_type in AnsibleInventoryType::iter_node_type() {
349            self.ansible_runner.run_playbook(
350                AnsiblePlaybook::CleanupLogs,
351                node_inv_type,
352                Some(format!("{{ \"setup_cron\": \"{setup_cron}\" }}")),
353            )?;
354        }
355
356        Ok(())
357    }
358
359    pub fn copy_logs(&self, name: &str, resources_only: bool) -> Result<()> {
360        for node_inv_type in AnsibleInventoryType::iter_node_type() {
361            self.ansible_runner.run_playbook(
362                AnsiblePlaybook::CopyLogs,
363                node_inv_type,
364                Some(format!(
365                    "{{ \"env_name\": \"{name}\", \"resources_only\" : \"{resources_only}\" }}"
366                )),
367            )?;
368        }
369        Ok(())
370    }
371
372    pub fn get_all_node_inventory(&self) -> Result<Vec<VirtualMachine>> {
373        let mut all_node_inventory = Vec::new();
374        for node_inv_type in AnsibleInventoryType::iter_node_type() {
375            all_node_inventory.extend(self.ansible_runner.get_inventory(node_inv_type, false)?);
376        }
377
378        Ok(all_node_inventory)
379    }
380
381    pub fn get_symmetric_nat_gateway_inventory(&self) -> Result<Vec<VirtualMachine>> {
382        self.ansible_runner
383            .get_inventory(AnsibleInventoryType::SymmetricNatGateway, false)
384    }
385
386    pub fn get_full_cone_nat_gateway_inventory(&self) -> Result<Vec<VirtualMachine>> {
387        self.ansible_runner
388            .get_inventory(AnsibleInventoryType::FullConeNatGateway, false)
389    }
390
391    pub fn get_node_registries(
392        &self,
393        inventory_type: &AnsibleInventoryType,
394    ) -> Result<DeploymentNodeRegistries> {
395        debug!("Fetching node manager inventory for {inventory_type:?}");
396        let temp_dir_path = tempfile::tempdir()?.into_path();
397        let temp_dir_json = serde_json::to_string(&temp_dir_path)?;
398
399        self.ansible_runner.run_playbook(
400            AnsiblePlaybook::AntCtlInventory,
401            *inventory_type,
402            Some(format!("{{ \"dest\": {temp_dir_json} }}")),
403        )?;
404
405        let node_registry_paths = WalkDir::new(temp_dir_path)
406            .into_iter()
407            .flatten()
408            .filter_map(|entry| {
409                if entry.file_type().is_file()
410                    && entry.path().extension().is_some_and(|ext| ext == "json")
411                {
412                    // tempdir/<testnet_name>-node/var/safenode-manager/node_registry.json
413                    let mut vm_name = entry.path().to_path_buf();
414                    trace!("Found file with json extension: {vm_name:?}");
415                    vm_name.pop();
416                    vm_name.pop();
417                    vm_name.pop();
418                    // Extract the <testnet_name>-node string
419                    trace!("Extracting the vm name from the path");
420                    let vm_name = vm_name.file_name()?.to_str()?;
421                    trace!("Extracted vm name from path: {vm_name}");
422                    Some((vm_name.to_string(), entry.path().to_path_buf()))
423                } else {
424                    None
425                }
426            })
427            .collect::<Vec<(String, PathBuf)>>();
428
429        let mut node_registries = Vec::new();
430        let mut failed_vms = Vec::new();
431        for (vm_name, file_path) in node_registry_paths {
432            match NodeRegistry::load(&file_path) {
433                Ok(node_registry) => node_registries.push((vm_name.clone(), node_registry)),
434                Err(_) => failed_vms.push(vm_name.clone()),
435            }
436        }
437
438        let deployment_registries = DeploymentNodeRegistries {
439            inventory_type: *inventory_type,
440            retrieved_registries: node_registries,
441            failed_vms,
442        };
443        Ok(deployment_registries)
444    }
445
446    pub fn provision_evm_nodes(&self, options: &ProvisionOptions) -> Result<()> {
447        let start = Instant::now();
448        println!("Obtaining IP address for EVM nodes...");
449        let evm_node_inventory = self
450            .ansible_runner
451            .get_inventory(AnsibleInventoryType::EvmNodes, true)?;
452        let evm_node_ip = evm_node_inventory[0].public_ip_addr;
453        self.ssh_client
454            .wait_for_ssh_availability(&evm_node_ip, &self.cloud_provider.get_ssh_user())?;
455
456        println!("Running ansible against EVM nodes...");
457        self.ansible_runner.run_playbook(
458            AnsiblePlaybook::EvmNodes,
459            AnsibleInventoryType::EvmNodes,
460            Some(extra_vars::build_evm_nodes_extra_vars_doc(
461                &options.name,
462                &self.cloud_provider,
463                &options.binary_option,
464            )),
465        )?;
466        print_duration(start.elapsed());
467        Ok(())
468    }
469
470    pub fn provision_genesis_node(&self, options: &ProvisionOptions) -> Result<()> {
471        let start = Instant::now();
472        let genesis_inventory = self
473            .ansible_runner
474            .get_inventory(AnsibleInventoryType::Genesis, true)?;
475        let genesis_ip = genesis_inventory[0].public_ip_addr;
476        self.ssh_client
477            .wait_for_ssh_availability(&genesis_ip, &self.cloud_provider.get_ssh_user())?;
478        self.ansible_runner.run_playbook(
479            AnsiblePlaybook::Genesis,
480            AnsibleInventoryType::Genesis,
481            Some(extra_vars::build_node_extra_vars_doc(
482                &self.cloud_provider.to_string(),
483                options,
484                NodeType::Genesis,
485                None,
486                None,
487                1,
488                options.evm_network.clone(),
489                false,
490            )?),
491        )?;
492
493        print_duration(start.elapsed());
494
495        Ok(())
496    }
497
498    pub fn provision_full_cone(
499        &self,
500        options: &ProvisionOptions,
501        initial_contact_peer: Option<String>,
502        initial_network_contacts_url: Option<String>,
503        private_node_inventory: PrivateNodeProvisionInventory,
504        new_full_cone_nat_gateway_new_vms_for_upscale: Option<Vec<VirtualMachine>>,
505    ) -> Result<()> {
506        // Step 1 of Full Cone NAT Gateway
507        let start = Instant::now();
508        self.print_ansible_run_banner("Provision Full Cone NAT Gateway - Step 1");
509
510        for vm in new_full_cone_nat_gateway_new_vms_for_upscale
511            .as_ref()
512            .unwrap_or(&private_node_inventory.full_cone_nat_gateway_vms)
513            .iter()
514        {
515            println!(
516                "Checking SSH availability for Full Cone NAT Gateway: {}",
517                vm.public_ip_addr
518            );
519            self.ssh_client
520                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
521                .map_err(|e| {
522                    println!(
523                        "Failed to establish SSH connection to Full Cone NAT Gateway: {}",
524                        e
525                    );
526                    e
527                })?;
528        }
529
530        let mut modified_private_node_inventory = private_node_inventory.clone();
531
532        // If we are upscaling, then we cannot access the gateway VMs which are already deployed.
533        if let Some(new_full_cone_nat_gateway_new_vms_for_upscale) =
534            &new_full_cone_nat_gateway_new_vms_for_upscale
535        {
536            debug!("Removing existing full cone NAT Gateway and private node VMs from the inventory. Old inventory: {modified_private_node_inventory:?}");
537            let mut names_to_keep = Vec::new();
538
539            for vm in new_full_cone_nat_gateway_new_vms_for_upscale.iter() {
540                let nat_gateway_name = vm.name.split('-').last().unwrap();
541                names_to_keep.push(nat_gateway_name);
542            }
543
544            modified_private_node_inventory
545                .full_cone_nat_gateway_vms
546                .retain(|vm| {
547                    let nat_gateway_name = vm.name.split('-').last().unwrap();
548                    names_to_keep.contains(&nat_gateway_name)
549                });
550            modified_private_node_inventory
551                .full_cone_private_node_vms
552                .retain(|vm| {
553                    let nat_gateway_name = vm.name.split('-').last().unwrap();
554                    names_to_keep.contains(&nat_gateway_name)
555                });
556            debug!("New inventory after removing existing full cone NAT Gateway and private node VMs: {modified_private_node_inventory:?}");
557        }
558
559        if modified_private_node_inventory
560            .full_cone_nat_gateway_vms
561            .is_empty()
562        {
563            error!("There are no full cone NAT Gateway VMs available to upscale");
564            return Ok(());
565        }
566
567        let private_node_ip_map = modified_private_node_inventory
568            .full_cone_private_node_and_gateway_map()?
569            .into_iter()
570            .map(|(k, v)| {
571                let gateway_name = if new_full_cone_nat_gateway_new_vms_for_upscale.is_some() {
572                    debug!("Upscaling, using public IP address for gateway name");
573                    v.public_ip_addr.to_string()
574                } else {
575                    v.name.clone()
576                };
577                (gateway_name, k.private_ip_addr)
578            })
579            .collect::<HashMap<String, IpAddr>>();
580
581        if private_node_ip_map.is_empty() {
582            println!("There are no full cone private node VM available to be routed through the full cone NAT Gateway");
583            return Err(Error::EmptyInventory(
584                AnsibleInventoryType::FullConePrivateNodes,
585            ));
586        }
587
588        let vars = extra_vars::build_nat_gateway_extra_vars_doc(
589            &options.name,
590            private_node_ip_map.clone(),
591            "step1",
592        );
593        debug!("Provisioning Full Cone NAT Gateway - Step 1 with vars: {vars}");
594        let gateway_inventory = if new_full_cone_nat_gateway_new_vms_for_upscale.is_some() {
595            debug!("Upscaling, using static inventory for full cone nat gateway.");
596            generate_full_cone_nat_gateway_static_environment_inventory(
597                &modified_private_node_inventory.full_cone_nat_gateway_vms,
598                &options.name,
599                &options.output_inventory_dir_path,
600            )?;
601
602            AnsibleInventoryType::FullConeNatGatewayStatic
603        } else {
604            AnsibleInventoryType::FullConeNatGateway
605        };
606        self.ansible_runner.run_playbook(
607            AnsiblePlaybook::FullConeNatGateway,
608            gateway_inventory,
609            Some(vars),
610        )?;
611
612        // setup private node config
613        self.print_ansible_run_banner("Provisioning Full Cone Private Node Config");
614
615        generate_full_cone_private_node_static_environment_inventory(
616            &options.name,
617            &options.output_inventory_dir_path,
618            &private_node_inventory.full_cone_private_node_vms,
619            &private_node_inventory.full_cone_nat_gateway_vms,
620            &self.ssh_client.private_key_path,
621        )
622        .inspect_err(|err| {
623            error!("Failed to generate full cone private node static inv with err: {err:?}")
624        })?;
625
626        // For a new deployment, it's quite probable that SSH is available, because this part occurs
627        // after the genesis node has been provisioned. However, for a bootstrap deploy, we need to
628        // check that SSH is available before proceeding.
629        println!("Obtaining IP addresses for nodes...");
630        let inventory = self
631            .ansible_runner
632            .get_inventory(AnsibleInventoryType::FullConePrivateNodes, true)?;
633
634        println!("Waiting for SSH availability on Symmetric Private nodes...");
635        for vm in inventory.iter() {
636            println!(
637                "Checking SSH availability for {}: {}",
638                vm.name, vm.public_ip_addr
639            );
640            self.ssh_client
641                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
642                .map_err(|e| {
643                    println!("Failed to establish SSH connection to {}: {}", vm.name, e);
644                    e
645                })?;
646        }
647
648        println!("SSH is available on all nodes. Proceeding with provisioning...");
649
650        self.ansible_runner.run_playbook(
651            AnsiblePlaybook::PrivateNodeConfig,
652            AnsibleInventoryType::FullConePrivateNodes,
653            Some(
654                extra_vars::build_full_cone_private_node_config_extra_vars_docs(
655                    &private_node_inventory,
656                )?,
657            ),
658        )?;
659
660        // Step 2 of Full Cone NAT Gateway
661
662        let vars = extra_vars::build_nat_gateway_extra_vars_doc(
663            &options.name,
664            private_node_ip_map,
665            "step2",
666        );
667
668        self.print_ansible_run_banner("Provisioning Full Cone NAT Gateway - Step 2");
669        debug!("Provisioning Full Cone NAT Gateway - Step 2 with vars: {vars}");
670        self.ansible_runner.run_playbook(
671            AnsiblePlaybook::FullConeNatGateway,
672            gateway_inventory,
673            Some(vars),
674        )?;
675
676        // provision the nodes
677
678        let home_dir = std::env::var("HOME").inspect_err(|err| {
679            println!("Failed to get home directory with error: {err:?}",);
680        })?;
681        let known_hosts_path = format!("{}/.ssh/known_hosts", home_dir);
682        debug!("Cleaning up known hosts file at {known_hosts_path} ");
683        run_external_command(
684            PathBuf::from("rm"),
685            std::env::current_dir()?,
686            vec![known_hosts_path],
687            false,
688            false,
689        )?;
690
691        self.print_ansible_run_banner("Provision Full Cone Private Nodes");
692
693        self.ssh_client.set_full_cone_nat_routed_vms(
694            &private_node_inventory.full_cone_private_node_vms,
695            &private_node_inventory.full_cone_nat_gateway_vms,
696        )?;
697
698        self.provision_nodes(
699            options,
700            initial_contact_peer,
701            initial_network_contacts_url,
702            NodeType::FullConePrivateNode,
703        )?;
704
705        print_duration(start.elapsed());
706        Ok(())
707    }
708    pub fn provision_symmetric_nat_gateway(
709        &self,
710        options: &ProvisionOptions,
711        private_node_inventory: &PrivateNodeProvisionInventory,
712    ) -> Result<()> {
713        let start = Instant::now();
714        for vm in &private_node_inventory.symmetric_nat_gateway_vms {
715            println!(
716                "Checking SSH availability for Symmetric NAT Gateway: {}",
717                vm.public_ip_addr
718            );
719            self.ssh_client
720                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
721                .map_err(|e| {
722                    println!(
723                        "Failed to establish SSH connection to Symmetric NAT Gateway: {}",
724                        e
725                    );
726                    e
727                })?;
728        }
729
730        let private_node_ip_map = private_node_inventory
731            .symmetric_private_node_and_gateway_map()?
732            .into_iter()
733            .map(|(k, v)| (v.name.clone(), k.private_ip_addr))
734            .collect::<HashMap<String, IpAddr>>();
735
736        if private_node_ip_map.is_empty() {
737            println!("There are no Symmetric private node VM available to be routed through the Symmetric NAT Gateway");
738            return Err(Error::EmptyInventory(
739                AnsibleInventoryType::SymmetricPrivateNodes,
740            ));
741        }
742
743        let vars = extra_vars::build_nat_gateway_extra_vars_doc(
744            &options.name,
745            private_node_ip_map,
746            "symmetric",
747        );
748        debug!("Provisioning Symmetric NAT Gateway with vars: {vars}");
749        self.ansible_runner.run_playbook(
750            AnsiblePlaybook::SymmetricNatGateway,
751            AnsibleInventoryType::SymmetricNatGateway,
752            Some(vars),
753        )?;
754
755        print_duration(start.elapsed());
756        Ok(())
757    }
758
759    pub fn provision_nodes(
760        &self,
761        options: &ProvisionOptions,
762        initial_contact_peer: Option<String>,
763        initial_network_contacts_url: Option<String>,
764        node_type: NodeType,
765    ) -> Result<()> {
766        let start = Instant::now();
767        let mut relay = false;
768        let (inventory_type, node_count) = match &node_type {
769            NodeType::FullConePrivateNode => {
770                relay = true;
771                (
772                    node_type.to_ansible_inventory_type(),
773                    options.full_cone_private_node_count,
774                )
775            }
776            // use provision_genesis_node fn
777            NodeType::Generic => (node_type.to_ansible_inventory_type(), options.node_count),
778            NodeType::Genesis => return Err(Error::InvalidNodeType(node_type)),
779            NodeType::PeerCache => (
780                node_type.to_ansible_inventory_type(),
781                options.peer_cache_node_count,
782            ),
783            NodeType::SymmetricPrivateNode => {
784                relay = true;
785                (
786                    node_type.to_ansible_inventory_type(),
787                    options.symmetric_private_node_count,
788                )
789            }
790        };
791
792        // For a new deployment, it's quite probable that SSH is available, because this part occurs
793        // after the genesis node has been provisioned. However, for a bootstrap deploy, we need to
794        // check that SSH is available before proceeding.
795        println!("Obtaining IP addresses for nodes...");
796        let inventory = self.ansible_runner.get_inventory(inventory_type, true)?;
797
798        println!("Waiting for SSH availability on {node_type:?} nodes...");
799        for vm in inventory.iter() {
800            println!(
801                "Checking SSH availability for {}: {}",
802                vm.name, vm.public_ip_addr
803            );
804            self.ssh_client
805                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
806                .map_err(|e| {
807                    println!("Failed to establish SSH connection to {}: {}", vm.name, e);
808                    e
809                })?;
810        }
811
812        println!("SSH is available on all nodes. Proceeding with provisioning...");
813
814        let playbook = match node_type {
815            NodeType::Generic => AnsiblePlaybook::Nodes,
816            NodeType::PeerCache => AnsiblePlaybook::PeerCacheNodes,
817            NodeType::FullConePrivateNode => AnsiblePlaybook::Nodes,
818            NodeType::SymmetricPrivateNode => AnsiblePlaybook::Nodes,
819            _ => return Err(Error::InvalidNodeType(node_type.clone())),
820        };
821        self.ansible_runner.run_playbook(
822            playbook,
823            inventory_type,
824            Some(extra_vars::build_node_extra_vars_doc(
825                &self.cloud_provider.to_string(),
826                options,
827                node_type.clone(),
828                initial_contact_peer,
829                initial_network_contacts_url,
830                node_count,
831                options.evm_network.clone(),
832                relay,
833            )?),
834        )?;
835
836        print_duration(start.elapsed());
837        Ok(())
838    }
839
840    pub fn provision_symmetric_private_nodes(
841        &self,
842        options: &mut ProvisionOptions,
843        initial_contact_peer: Option<String>,
844        initial_network_contacts_url: Option<String>,
845        private_node_inventory: &PrivateNodeProvisionInventory,
846    ) -> Result<()> {
847        let start = Instant::now();
848        self.print_ansible_run_banner("Provision Symmetric Private Node Config");
849
850        generate_symmetric_private_node_static_environment_inventory(
851            &options.name,
852            &options.output_inventory_dir_path,
853            &private_node_inventory.symmetric_private_node_vms,
854            &private_node_inventory.symmetric_nat_gateway_vms,
855            &self.ssh_client.private_key_path,
856        )
857        .inspect_err(|err| {
858            error!("Failed to generate symmetric private node static inv with err: {err:?}")
859        })?;
860
861        self.ssh_client.set_symmetric_nat_routed_vms(
862            &private_node_inventory.symmetric_private_node_vms,
863            &private_node_inventory.symmetric_nat_gateway_vms,
864        )?;
865
866        let inventory_type = AnsibleInventoryType::SymmetricPrivateNodes;
867
868        // For a new deployment, it's quite probable that SSH is available, because this part occurs
869        // after the genesis node has been provisioned. However, for a bootstrap deploy, we need to
870        // check that SSH is available before proceeding.
871        println!("Obtaining IP addresses for nodes...");
872        let inventory = self.ansible_runner.get_inventory(inventory_type, true)?;
873
874        println!("Waiting for SSH availability on Symmetric Private nodes...");
875        for vm in inventory.iter() {
876            println!(
877                "Checking SSH availability for {}: {}",
878                vm.name, vm.public_ip_addr
879            );
880            self.ssh_client
881                .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
882                .map_err(|e| {
883                    println!("Failed to establish SSH connection to {}: {}", vm.name, e);
884                    e
885                })?;
886        }
887
888        println!("SSH is available on all nodes. Proceeding with provisioning...");
889
890        self.ansible_runner.run_playbook(
891            AnsiblePlaybook::PrivateNodeConfig,
892            inventory_type,
893            Some(
894                extra_vars::build_symmetric_private_node_config_extra_vars_doc(
895                    private_node_inventory,
896                )?,
897            ),
898        )?;
899
900        println!("Provisioned Symmetric Private Node Config");
901        print_duration(start.elapsed());
902
903        self.provision_nodes(
904            options,
905            initial_contact_peer,
906            initial_network_contacts_url,
907            NodeType::SymmetricPrivateNode,
908        )?;
909
910        Ok(())
911    }
912
913    pub async fn provision_uploaders(
914        &self,
915        options: &ProvisionOptions,
916        genesis_multiaddr: Option<String>,
917        genesis_network_contacts_url: Option<String>,
918    ) -> Result<()> {
919        let start = Instant::now();
920
921        let sk_map = self
922            .deposit_funds_to_uploaders(&FundingOptions {
923                evm_data_payments_address: options.evm_data_payments_address.clone(),
924                evm_network: options.evm_network.clone(),
925                evm_payment_token_address: options.evm_payment_token_address.clone(),
926                evm_rpc_url: options.evm_rpc_url.clone(),
927                funding_wallet_secret_key: options.funding_wallet_secret_key.clone(),
928                gas_amount: options.gas_amount,
929                token_amount: options.token_amount,
930                uploaders_count: options.uploaders_count,
931            })
932            .await?;
933
934        println!("Running ansible against uploader machine to start the uploader script.");
935        debug!("Running ansible against uploader machine to start the uploader script.");
936
937        self.ansible_runner.run_playbook(
938            AnsiblePlaybook::Uploaders,
939            AnsibleInventoryType::Uploaders,
940            Some(extra_vars::build_uploaders_extra_vars_doc(
941                &self.cloud_provider.to_string(),
942                options,
943                genesis_multiaddr,
944                genesis_network_contacts_url,
945                &sk_map,
946            )?),
947        )?;
948        print_duration(start.elapsed());
949        Ok(())
950    }
951
952    pub fn start_nodes(
953        &self,
954        environment_name: &str,
955        interval: Duration,
956        node_type: Option<NodeType>,
957        custom_inventory: Option<Vec<VirtualMachine>>,
958    ) -> Result<()> {
959        let mut extra_vars = ExtraVarsDocBuilder::default();
960        extra_vars.add_variable("interval", &interval.as_millis().to_string());
961
962        if let Some(node_type) = node_type {
963            println!("Running the start nodes playbook for {node_type:?} nodes");
964            self.ansible_runner.run_playbook(
965                AnsiblePlaybook::StartNodes,
966                node_type.to_ansible_inventory_type(),
967                Some(extra_vars.build()),
968            )?;
969            return Ok(());
970        }
971
972        if let Some(custom_inventory) = custom_inventory {
973            println!("Running the start nodes playbook with a custom inventory");
974            generate_custom_environment_inventory(
975                &custom_inventory,
976                environment_name,
977                &self.ansible_runner.working_directory_path.join("inventory"),
978            )?;
979            self.ansible_runner.run_playbook(
980                AnsiblePlaybook::StartNodes,
981                AnsibleInventoryType::Custom,
982                Some(extra_vars.build()),
983            )?;
984            return Ok(());
985        }
986
987        println!("Running the start nodes playbook for all node types");
988        for node_inv_type in AnsibleInventoryType::iter_node_type() {
989            self.ansible_runner.run_playbook(
990                AnsiblePlaybook::StartNodes,
991                node_inv_type,
992                Some(extra_vars.build()),
993            )?;
994        }
995        Ok(())
996    }
997
998    pub fn status(&self) -> Result<()> {
999        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1000            self.ansible_runner
1001                .run_playbook(AnsiblePlaybook::Status, node_inv_type, None)?;
1002        }
1003        Ok(())
1004    }
1005
1006    pub fn start_telegraf(
1007        &self,
1008        environment_name: &str,
1009        node_type: Option<NodeType>,
1010        custom_inventory: Option<Vec<VirtualMachine>>,
1011    ) -> Result<()> {
1012        if let Some(node_type) = node_type {
1013            println!("Running the start telegraf playbook for {node_type:?} nodes");
1014            self.ansible_runner.run_playbook(
1015                AnsiblePlaybook::StartTelegraf,
1016                node_type.to_ansible_inventory_type(),
1017                None,
1018            )?;
1019            return Ok(());
1020        }
1021
1022        if let Some(custom_inventory) = custom_inventory {
1023            println!("Running the start telegraf playbook with a custom inventory");
1024            generate_custom_environment_inventory(
1025                &custom_inventory,
1026                environment_name,
1027                &self.ansible_runner.working_directory_path.join("inventory"),
1028            )?;
1029            self.ansible_runner.run_playbook(
1030                AnsiblePlaybook::StartTelegraf,
1031                AnsibleInventoryType::Custom,
1032                None,
1033            )?;
1034            return Ok(());
1035        }
1036
1037        println!("Running the start telegraf playbook for all node types");
1038        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1039            self.ansible_runner.run_playbook(
1040                AnsiblePlaybook::StartTelegraf,
1041                node_inv_type,
1042                None,
1043            )?;
1044        }
1045
1046        Ok(())
1047    }
1048
1049    pub fn stop_nodes(
1050        &self,
1051        environment_name: &str,
1052        interval: Duration,
1053        node_type: Option<NodeType>,
1054        custom_inventory: Option<Vec<VirtualMachine>>,
1055        delay: Option<u64>,
1056        service_names: Option<Vec<String>>,
1057    ) -> Result<()> {
1058        let mut extra_vars = ExtraVarsDocBuilder::default();
1059        extra_vars.add_variable("interval", &interval.as_millis().to_string());
1060        if let Some(delay) = delay {
1061            extra_vars.add_variable("delay", &delay.to_string());
1062        }
1063        if let Some(service_names) = service_names {
1064            extra_vars.add_list_variable("service_names", service_names);
1065        }
1066        let extra_vars = extra_vars.build();
1067
1068        if let Some(node_type) = node_type {
1069            println!("Running the stop nodes playbook for {node_type:?} nodes");
1070            self.ansible_runner.run_playbook(
1071                AnsiblePlaybook::StopNodes,
1072                node_type.to_ansible_inventory_type(),
1073                Some(extra_vars),
1074            )?;
1075            return Ok(());
1076        }
1077
1078        if let Some(custom_inventory) = custom_inventory {
1079            println!("Running the stop nodes playbook with a custom inventory");
1080            generate_custom_environment_inventory(
1081                &custom_inventory,
1082                environment_name,
1083                &self.ansible_runner.working_directory_path.join("inventory"),
1084            )?;
1085            self.ansible_runner.run_playbook(
1086                AnsiblePlaybook::StopNodes,
1087                AnsibleInventoryType::Custom,
1088                Some(extra_vars),
1089            )?;
1090            return Ok(());
1091        }
1092
1093        println!("Running the stop nodes playbook for all node types");
1094        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1095            self.ansible_runner.run_playbook(
1096                AnsiblePlaybook::StopNodes,
1097                node_inv_type,
1098                Some(extra_vars.clone()),
1099            )?;
1100        }
1101
1102        Ok(())
1103    }
1104
1105    pub fn stop_telegraf(
1106        &self,
1107        environment_name: &str,
1108        node_type: Option<NodeType>,
1109        custom_inventory: Option<Vec<VirtualMachine>>,
1110    ) -> Result<()> {
1111        if let Some(node_type) = node_type {
1112            println!("Running the stop telegraf playbook for {node_type:?} nodes");
1113            self.ansible_runner.run_playbook(
1114                AnsiblePlaybook::StopTelegraf,
1115                node_type.to_ansible_inventory_type(),
1116                None,
1117            )?;
1118            return Ok(());
1119        }
1120
1121        if let Some(custom_inventory) = custom_inventory {
1122            println!("Running the stop telegraf playbook with a custom inventory");
1123            generate_custom_environment_inventory(
1124                &custom_inventory,
1125                environment_name,
1126                &self.ansible_runner.working_directory_path.join("inventory"),
1127            )?;
1128            self.ansible_runner.run_playbook(
1129                AnsiblePlaybook::StopTelegraf,
1130                AnsibleInventoryType::Custom,
1131                None,
1132            )?;
1133            return Ok(());
1134        }
1135
1136        println!("Running the stop telegraf playbook for all node types");
1137        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1138            self.ansible_runner
1139                .run_playbook(AnsiblePlaybook::StopTelegraf, node_inv_type, None)?;
1140        }
1141
1142        Ok(())
1143    }
1144
1145    pub fn upgrade_node_telegraf(&self, name: &str) -> Result<()> {
1146        self.ansible_runner.run_playbook(
1147            AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1148            AnsibleInventoryType::PeerCacheNodes,
1149            Some(extra_vars::build_node_telegraf_upgrade(
1150                name,
1151                &NodeType::PeerCache,
1152            )?),
1153        )?;
1154        self.ansible_runner.run_playbook(
1155            AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1156            AnsibleInventoryType::Nodes,
1157            Some(extra_vars::build_node_telegraf_upgrade(
1158                name,
1159                &NodeType::Generic,
1160            )?),
1161        )?;
1162
1163        self.ansible_runner.run_playbook(
1164            AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1165            AnsibleInventoryType::SymmetricPrivateNodes,
1166            Some(extra_vars::build_node_telegraf_upgrade(
1167                name,
1168                &NodeType::SymmetricPrivateNode,
1169            )?),
1170        )?;
1171
1172        self.ansible_runner.run_playbook(
1173            AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1174            AnsibleInventoryType::FullConePrivateNodes,
1175            Some(extra_vars::build_node_telegraf_upgrade(
1176                name,
1177                &NodeType::FullConePrivateNode,
1178            )?),
1179        )?;
1180        Ok(())
1181    }
1182
1183    pub fn upgrade_uploader_telegraf(&self, name: &str) -> Result<()> {
1184        self.ansible_runner.run_playbook(
1185            AnsiblePlaybook::UpgradeUploaderTelegrafConfig,
1186            AnsibleInventoryType::Uploaders,
1187            Some(extra_vars::build_uploader_telegraf_upgrade(name)?),
1188        )?;
1189        Ok(())
1190    }
1191
1192    pub fn upgrade_nodes(&self, options: &UpgradeOptions) -> Result<()> {
1193        if let Some(custom_inventory) = &options.custom_inventory {
1194            println!("Running the UpgradeNodes with a custom inventory");
1195            generate_custom_environment_inventory(
1196                custom_inventory,
1197                &options.name,
1198                &self.ansible_runner.working_directory_path.join("inventory"),
1199            )?;
1200            match self.ansible_runner.run_playbook(
1201                AnsiblePlaybook::UpgradeNodes,
1202                AnsibleInventoryType::Custom,
1203                Some(options.get_ansible_vars()),
1204            ) {
1205                Ok(()) => println!("All nodes were successfully upgraded"),
1206                Err(_) => {
1207                    println!("WARNING: some nodes may not have been upgraded or restarted");
1208                }
1209            }
1210            return Ok(());
1211        }
1212
1213        if let Some(node_type) = &options.node_type {
1214            println!("Running the UpgradeNodes playbook for {node_type:?} nodes");
1215            match self.ansible_runner.run_playbook(
1216                AnsiblePlaybook::UpgradeNodes,
1217                node_type.to_ansible_inventory_type(),
1218                Some(options.get_ansible_vars()),
1219            ) {
1220                Ok(()) => println!("All {node_type:?} nodes were successfully upgraded"),
1221                Err(_) => {
1222                    println!(
1223                        "WARNING: some {node_type:?} nodes may not have been upgraded or restarted"
1224                    );
1225                }
1226            }
1227            return Ok(());
1228        }
1229
1230        println!("Running the UpgradeNodes playbook for all node types");
1231
1232        match self.ansible_runner.run_playbook(
1233            AnsiblePlaybook::UpgradeNodes,
1234            AnsibleInventoryType::PeerCacheNodes,
1235            Some(options.get_ansible_vars()),
1236        ) {
1237            Ok(()) => println!("All Peer Cache nodes were successfully upgraded"),
1238            Err(_) => {
1239                println!("WARNING: some Peer Cacche nodes may not have been upgraded or restarted");
1240            }
1241        }
1242        match self.ansible_runner.run_playbook(
1243            AnsiblePlaybook::UpgradeNodes,
1244            AnsibleInventoryType::Nodes,
1245            Some(options.get_ansible_vars()),
1246        ) {
1247            Ok(()) => println!("All generic nodes were successfully upgraded"),
1248            Err(_) => {
1249                println!("WARNING: some nodes may not have been upgraded or restarted");
1250            }
1251        }
1252        match self.ansible_runner.run_playbook(
1253            AnsiblePlaybook::UpgradeNodes,
1254            AnsibleInventoryType::SymmetricPrivateNodes,
1255            Some(options.get_ansible_vars()),
1256        ) {
1257            Ok(()) => println!("All private nodes were successfully upgraded"),
1258            Err(_) => {
1259                println!("WARNING: some nodes may not have been upgraded or restarted");
1260            }
1261        }
1262        // Don't use AnsibleInventoryType::iter_node_type() here, because the genesis node should be upgraded last
1263        match self.ansible_runner.run_playbook(
1264            AnsiblePlaybook::UpgradeNodes,
1265            AnsibleInventoryType::Genesis,
1266            Some(options.get_ansible_vars()),
1267        ) {
1268            Ok(()) => println!("The genesis nodes was successfully upgraded"),
1269            Err(_) => {
1270                println!("WARNING: the genesis node may not have been upgraded or restarted");
1271            }
1272        }
1273        Ok(())
1274    }
1275
1276    pub fn upgrade_antctl(
1277        &self,
1278        environment_name: &str,
1279        version: &Version,
1280        node_type: Option<NodeType>,
1281        custom_inventory: Option<Vec<VirtualMachine>>,
1282    ) -> Result<()> {
1283        let mut extra_vars = ExtraVarsDocBuilder::default();
1284        extra_vars.add_variable("version", &version.to_string());
1285
1286        if let Some(node_type) = node_type {
1287            println!("Running the upgrade safenode-manager playbook for {node_type:?} nodes");
1288            self.ansible_runner.run_playbook(
1289                AnsiblePlaybook::UpgradeAntctl,
1290                node_type.to_ansible_inventory_type(),
1291                Some(extra_vars.build()),
1292            )?;
1293            return Ok(());
1294        }
1295
1296        if let Some(custom_inventory) = custom_inventory {
1297            println!("Running the upgrade safenode-manager playbook with a custom inventory");
1298            generate_custom_environment_inventory(
1299                &custom_inventory,
1300                environment_name,
1301                &self.ansible_runner.working_directory_path.join("inventory"),
1302            )?;
1303            self.ansible_runner.run_playbook(
1304                AnsiblePlaybook::UpgradeAntctl,
1305                AnsibleInventoryType::Custom,
1306                Some(extra_vars.build()),
1307            )?;
1308            return Ok(());
1309        }
1310
1311        println!("Running the upgrade safenode-manager playbook for all node types");
1312        for node_inv_type in AnsibleInventoryType::iter_node_type() {
1313            self.ansible_runner.run_playbook(
1314                AnsiblePlaybook::UpgradeAntctl,
1315                node_inv_type,
1316                Some(extra_vars.build()),
1317            )?;
1318        }
1319
1320        Ok(())
1321    }
1322
1323    pub fn print_ansible_run_banner(&self, s: &str) {
1324        let ansible_run_msg = "Ansible Run: ";
1325        let line = "=".repeat(s.len() + ansible_run_msg.len());
1326        println!("{}\n{}{}\n{}", line, ansible_run_msg, s, line);
1327    }
1328}