sn_testnet_deploy/
uploaders.rs

1// Copyright (c) 2023, MaidSafe.
2// All rights reserved.
3//
4// This SAFE Network Software is licensed under the BSD-3-Clause license.
5// Please see the LICENSE file for more details.
6
7use crate::{
8    ansible::{
9        inventory::cleanup_environment_inventory,
10        provisioning::{AnsibleProvisioner, ProvisionOptions},
11        AnsibleRunner,
12    },
13    error::{Error, Result},
14    get_environment_details,
15    infra::UploaderInfraRunOptions,
16    inventory::UploaderDeploymentInventory,
17    print_duration,
18    s3::S3Repository,
19    ssh::SshClient,
20    terraform::TerraformRunner,
21    write_environment_details, BinaryOption, CloudProvider, DeploymentType, EnvironmentDetails,
22    EnvironmentType, EvmDetails,
23};
24use alloy::primitives::U256;
25use serde::{Deserialize, Serialize};
26use std::{path::PathBuf, time::Instant};
27
28const ANSIBLE_DEFAULT_FORKS: usize = 50;
29
30#[derive(Clone, Serialize, Deserialize)]
31pub struct UploaderDeployOptions {
32    pub binary_option: BinaryOption,
33    pub chunk_size: Option<u64>,
34    pub client_env_variables: Option<Vec<(String, String)>>,
35    pub current_inventory: UploaderDeploymentInventory,
36    pub enable_telegraf: bool,
37    pub environment_type: EnvironmentType,
38    pub evm_details: EvmDetails,
39    pub funding_wallet_secret_key: Option<String>,
40    pub initial_gas: Option<U256>,
41    pub initial_tokens: Option<U256>,
42    pub max_archived_log_files: u16,
43    pub max_log_files: u16,
44    pub max_uploads: Option<u32>,
45    pub name: String,
46    pub network_id: Option<u8>,
47    pub network_contacts_url: String,
48    pub output_inventory_dir_path: PathBuf,
49    pub peer: String,
50    pub uploader_vm_count: Option<u16>,
51    pub uploader_vm_size: Option<String>,
52    pub uploaders_count: u16,
53    pub wallet_secret_keys: Option<Vec<String>>,
54}
55
56#[derive(Default)]
57pub struct UploaderDeployBuilder {
58    ansible_forks: Option<usize>,
59    ansible_verbose_mode: bool,
60    deployment_type: EnvironmentType,
61    environment_name: String,
62    provider: Option<CloudProvider>,
63    ssh_secret_key_path: Option<PathBuf>,
64    state_bucket_name: Option<String>,
65    terraform_binary_path: Option<PathBuf>,
66    vault_password_path: Option<PathBuf>,
67    working_directory_path: Option<PathBuf>,
68}
69
70impl UploaderDeployBuilder {
71    pub fn new() -> Self {
72        Default::default()
73    }
74
75    pub fn ansible_verbose_mode(&mut self, ansible_verbose_mode: bool) -> &mut Self {
76        self.ansible_verbose_mode = ansible_verbose_mode;
77        self
78    }
79
80    pub fn ansible_forks(&mut self, ansible_forks: usize) -> &mut Self {
81        self.ansible_forks = Some(ansible_forks);
82        self
83    }
84
85    pub fn deployment_type(&mut self, deployment_type: EnvironmentType) -> &mut Self {
86        self.deployment_type = deployment_type;
87        self
88    }
89
90    pub fn environment_name(&mut self, name: &str) -> &mut Self {
91        self.environment_name = name.to_string();
92        self
93    }
94
95    pub fn provider(&mut self, provider: CloudProvider) -> &mut Self {
96        self.provider = Some(provider);
97        self
98    }
99
100    pub fn state_bucket_name(&mut self, state_bucket_name: String) -> &mut Self {
101        self.state_bucket_name = Some(state_bucket_name);
102        self
103    }
104
105    pub fn terraform_binary_path(&mut self, terraform_binary_path: PathBuf) -> &mut Self {
106        self.terraform_binary_path = Some(terraform_binary_path);
107        self
108    }
109
110    pub fn working_directory(&mut self, working_directory_path: PathBuf) -> &mut Self {
111        self.working_directory_path = Some(working_directory_path);
112        self
113    }
114
115    pub fn ssh_secret_key_path(&mut self, ssh_secret_key_path: PathBuf) -> &mut Self {
116        self.ssh_secret_key_path = Some(ssh_secret_key_path);
117        self
118    }
119
120    pub fn vault_password_path(&mut self, vault_password_path: PathBuf) -> &mut Self {
121        self.vault_password_path = Some(vault_password_path);
122        self
123    }
124
125    pub fn build(&self) -> Result<UploaderDeployer> {
126        let provider = self.provider.unwrap_or(CloudProvider::DigitalOcean);
127        match provider {
128            CloudProvider::DigitalOcean => {
129                let digital_ocean_pat = std::env::var("DO_PAT").map_err(|_| {
130                    Error::CloudProviderCredentialsNotSupplied("DO_PAT".to_string())
131                })?;
132                // The DO_PAT variable is not actually read by either Terraform or Ansible.
133                // Each tool uses a different variable, so instead we set each of those variables
134                // to the value of DO_PAT. This means the user only needs to set one variable.
135                std::env::set_var("DIGITALOCEAN_TOKEN", digital_ocean_pat.clone());
136                std::env::set_var("DO_API_TOKEN", digital_ocean_pat);
137            }
138            _ => {
139                return Err(Error::CloudProviderNotSupported(provider.to_string()));
140            }
141        }
142
143        let state_bucket_name = match self.state_bucket_name {
144            Some(ref bucket_name) => bucket_name.clone(),
145            None => std::env::var("UPLOADERS_TERRAFORM_STATE_BUCKET_NAME")?,
146        };
147
148        let default_terraform_bin_path = PathBuf::from("terraform");
149        let terraform_binary_path = self
150            .terraform_binary_path
151            .as_ref()
152            .unwrap_or(&default_terraform_bin_path);
153
154        let working_directory_path = match self.working_directory_path {
155            Some(ref work_dir_path) => work_dir_path.clone(),
156            None => std::env::current_dir()?.join("resources"),
157        };
158
159        let ssh_secret_key_path = match self.ssh_secret_key_path {
160            Some(ref ssh_sk_path) => ssh_sk_path.clone(),
161            None => PathBuf::from(std::env::var("SSH_KEY_PATH")?),
162        };
163
164        let vault_password_path = match self.vault_password_path {
165            Some(ref vault_pw_path) => vault_pw_path.clone(),
166            None => PathBuf::from(std::env::var("ANSIBLE_VAULT_PASSWORD_PATH")?),
167        };
168
169        let terraform_runner = TerraformRunner::new(
170            terraform_binary_path.to_path_buf(),
171            working_directory_path
172                .join("terraform")
173                .join("uploaders")
174                .join(provider.to_string()),
175            provider,
176            &state_bucket_name,
177        )?;
178
179        let ansible_runner = AnsibleRunner::new(
180            self.ansible_forks.unwrap_or(ANSIBLE_DEFAULT_FORKS),
181            self.ansible_verbose_mode,
182            &self.environment_name,
183            provider,
184            ssh_secret_key_path.clone(),
185            vault_password_path,
186            working_directory_path.join("ansible"),
187        )?;
188
189        let ssh_client = SshClient::new(ssh_secret_key_path);
190        let ansible_provisioner =
191            AnsibleProvisioner::new(ansible_runner, provider, ssh_client.clone());
192
193        let uploader_deployer = UploaderDeployer::new(
194            ansible_provisioner,
195            provider,
196            self.deployment_type.clone(),
197            &self.environment_name,
198            S3Repository {},
199            ssh_client,
200            terraform_runner,
201            working_directory_path,
202        )?;
203
204        Ok(uploader_deployer)
205    }
206}
207
208#[derive(Clone)]
209pub struct UploaderDeployer {
210    pub ansible_provisioner: AnsibleProvisioner,
211    pub cloud_provider: CloudProvider,
212    pub deployment_type: EnvironmentType,
213    pub environment_name: String,
214    pub inventory_file_path: PathBuf,
215    pub s3_repository: S3Repository,
216    pub ssh_client: SshClient,
217    pub terraform_runner: TerraformRunner,
218    pub working_directory_path: PathBuf,
219}
220
221impl UploaderDeployer {
222    #[allow(clippy::too_many_arguments)]
223    pub fn new(
224        ansible_provisioner: AnsibleProvisioner,
225        cloud_provider: CloudProvider,
226        deployment_type: EnvironmentType,
227        environment_name: &str,
228        s3_repository: S3Repository,
229        ssh_client: SshClient,
230        terraform_runner: TerraformRunner,
231        working_directory_path: PathBuf,
232    ) -> Result<UploaderDeployer> {
233        if environment_name.is_empty() {
234            return Err(Error::EnvironmentNameRequired);
235        }
236        let inventory_file_path = working_directory_path
237            .join("ansible")
238            .join("inventory")
239            .join("dev_inventory_digital_ocean.yml");
240
241        Ok(UploaderDeployer {
242            ansible_provisioner,
243            cloud_provider,
244            deployment_type,
245            environment_name: environment_name.to_string(),
246            inventory_file_path,
247            s3_repository,
248            ssh_client,
249            terraform_runner,
250            working_directory_path,
251        })
252    }
253
254    pub fn create_or_update_infra(&self, options: &UploaderInfraRunOptions) -> Result<()> {
255        let start = Instant::now();
256        println!("Selecting {} workspace...", options.name);
257        self.terraform_runner.workspace_select(&options.name)?;
258
259        let args = options.build_terraform_args()?;
260
261        println!("Running terraform apply...");
262        self.terraform_runner
263            .apply(args, Some(options.tfvars_filename.clone()))?;
264        print_duration(start.elapsed());
265        Ok(())
266    }
267
268    pub async fn init(&self) -> Result<()> {
269        self.terraform_runner.init()?;
270        let workspaces = self.terraform_runner.workspace_list()?;
271        if !workspaces.contains(&self.environment_name) {
272            self.terraform_runner
273                .workspace_new(&self.environment_name)?;
274        } else {
275            println!("Workspace {} already exists", self.environment_name);
276        }
277
278        Ok(())
279    }
280
281    pub fn plan(&self, options: &UploaderInfraRunOptions) -> Result<()> {
282        println!("Selecting {} workspace...", options.name);
283        self.terraform_runner.workspace_select(&options.name)?;
284
285        let args = options.build_terraform_args()?;
286
287        self.terraform_runner
288            .plan(Some(args), Some(options.tfvars_filename.clone()))?;
289        Ok(())
290    }
291
292    pub async fn deploy(&self, options: UploaderDeployOptions) -> Result<()> {
293        println!(
294            "Deploying uploaders for environment: {}",
295            self.environment_name
296        );
297
298        let build_custom_binaries = {
299            match &options.binary_option {
300                BinaryOption::BuildFromSource { .. } => true,
301                BinaryOption::Versioned { .. } => false,
302            }
303        };
304
305        let start = Instant::now();
306        println!("Initializing infrastructure...");
307
308        let infra_options = UploaderInfraRunOptions {
309            enable_build_vm: build_custom_binaries,
310            name: options.name.clone(),
311            tfvars_filename: options.current_inventory.get_tfvars_filename(),
312            uploader_image_id: None,
313            uploader_vm_count: options.uploader_vm_count,
314            uploader_vm_size: options.uploader_vm_size.clone(),
315        };
316
317        self.create_or_update_infra(&infra_options)?;
318
319        write_environment_details(
320            &self.s3_repository,
321            &options.name,
322            &EnvironmentDetails {
323                deployment_type: DeploymentType::Uploaders,
324                environment_type: options.environment_type.clone(),
325                evm_details: EvmDetails {
326                    network: options.evm_details.network.clone(),
327                    data_payments_address: options.evm_details.data_payments_address.clone(),
328                    payment_token_address: options.evm_details.payment_token_address.clone(),
329                    rpc_url: options.evm_details.rpc_url.clone(),
330                },
331                funding_wallet_address: None,
332                network_id: options.network_id,
333                rewards_address: None,
334            },
335        )
336        .await?;
337
338        println!("Provisioning uploaders with Ansible...");
339        let provision_options = ProvisionOptions::from(options.clone());
340
341        if build_custom_binaries {
342            self.ansible_provisioner
343                .print_ansible_run_banner("Build Custom Binaries");
344            self.ansible_provisioner
345                .build_safe_network_binaries(&provision_options, Some(vec!["ant".to_string()]))
346                .map_err(|err| {
347                    println!("Failed to build safe network binaries {err:?}");
348                    err
349                })?;
350        }
351
352        self.ansible_provisioner
353            .print_ansible_run_banner("Provision Uploaders");
354        self.ansible_provisioner
355            .provision_uploaders(
356                &provision_options,
357                Some(options.peer.clone()),
358                Some(options.network_contacts_url.clone()),
359            )
360            .await
361            .map_err(|err| {
362                println!("Failed to provision uploaders {err:?}");
363                err
364            })?;
365        println!("Deployment completed successfully in {:?}", start.elapsed());
366        Ok(())
367    }
368
369    async fn destroy_infra(&self, environment_details: &EnvironmentDetails) -> Result<()> {
370        crate::infra::select_workspace(&self.terraform_runner, &self.environment_name)?;
371
372        let options = UploaderInfraRunOptions::generate_existing(
373            &self.environment_name,
374            &self.terraform_runner,
375            environment_details,
376        )
377        .await?;
378
379        let mut args = Vec::new();
380        if let Some(vm_count) = options.uploader_vm_count {
381            args.push(("uploader_vm_count".to_string(), vm_count.to_string()));
382        }
383        if let Some(vm_size) = &options.uploader_vm_size {
384            args.push(("uploader_droplet_size".to_string(), vm_size.clone()));
385        }
386        args.push((
387            "use_custom_bin".to_string(),
388            options.enable_build_vm.to_string(),
389        ));
390
391        self.terraform_runner
392            .destroy(Some(args), Some(options.tfvars_filename.clone()))?;
393
394        crate::infra::delete_workspace(&self.terraform_runner, &self.environment_name)?;
395
396        Ok(())
397    }
398
399    pub async fn clean(&self) -> Result<()> {
400        let environment_details =
401            get_environment_details(&self.environment_name, &self.s3_repository).await?;
402        crate::funding::drain_funds(&self.ansible_provisioner, &environment_details).await?;
403
404        self.destroy_infra(&environment_details).await?;
405
406        cleanup_environment_inventory(
407            &self.environment_name,
408            &self
409                .working_directory_path
410                .join("ansible")
411                .join("inventory"),
412            None,
413        )?;
414
415        self.s3_repository
416            .delete_object("sn-environment-type", &self.environment_name)
417            .await?;
418        Ok(())
419    }
420}