sn_testnet_deploy/
clients.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::ClientsInfraRunOptions,
16    inventory::ClientsDeploymentInventory,
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 ClientsDeployOptions {
32    pub binary_option: BinaryOption,
33    pub chunk_size: Option<u64>,
34    pub client_env_variables: Option<Vec<(String, String)>>,
35    pub client_vm_count: Option<u16>,
36    pub client_vm_size: Option<String>,
37    pub current_inventory: ClientsDeploymentInventory,
38    pub delayed_verifier_batch_size: Option<u16>,
39    pub delayed_verifier_quorum_value: Option<String>,
40    pub enable_delayed_verifier: bool,
41    pub enable_performance_verifier: bool,
42    pub enable_random_verifier: bool,
43    pub enable_telegraf: bool,
44    pub enable_uploaders: bool,
45    pub environment_type: EnvironmentType,
46    pub evm_details: EvmDetails,
47    pub file_address: Option<String>,
48    pub expected_hash: Option<String>,
49    pub expected_size: Option<u64>,
50    pub funding_wallet_secret_key: Option<String>,
51    pub initial_gas: Option<U256>,
52    pub initial_tokens: Option<U256>,
53    pub max_archived_log_files: u16,
54    pub max_log_files: u16,
55    pub max_uploads: Option<u32>,
56    pub name: String,
57    pub network_id: Option<u8>,
58    pub network_contacts_url: Option<String>,
59    pub output_inventory_dir_path: PathBuf,
60    pub peer: Option<String>,
61    pub performance_verifier_batch_size: Option<u16>,
62    pub random_verifier_batch_size: Option<u16>,
63    pub uploaders_count: u16,
64    pub upload_size: Option<u16>,
65    pub upload_interval: u16,
66    pub wallet_secret_keys: Option<Vec<String>>,
67}
68
69#[derive(Default)]
70pub struct ClientsDeployBuilder {
71    ansible_forks: Option<usize>,
72    ansible_verbose_mode: bool,
73    deployment_type: EnvironmentType,
74    environment_name: String,
75    provider: Option<CloudProvider>,
76    region: Option<String>,
77    ssh_secret_key_path: Option<PathBuf>,
78    state_bucket_name: Option<String>,
79    terraform_binary_path: Option<PathBuf>,
80    vault_password_path: Option<PathBuf>,
81    working_directory_path: Option<PathBuf>,
82}
83
84impl ClientsDeployBuilder {
85    pub fn new() -> Self {
86        Default::default()
87    }
88
89    pub fn ansible_verbose_mode(&mut self, ansible_verbose_mode: bool) -> &mut Self {
90        self.ansible_verbose_mode = ansible_verbose_mode;
91        self
92    }
93
94    pub fn ansible_forks(&mut self, ansible_forks: usize) -> &mut Self {
95        self.ansible_forks = Some(ansible_forks);
96        self
97    }
98
99    pub fn deployment_type(&mut self, deployment_type: EnvironmentType) -> &mut Self {
100        self.deployment_type = deployment_type;
101        self
102    }
103
104    pub fn environment_name(&mut self, name: &str) -> &mut Self {
105        self.environment_name = name.to_string();
106        self
107    }
108
109    pub fn provider(&mut self, provider: CloudProvider) -> &mut Self {
110        self.provider = Some(provider);
111        self
112    }
113
114    pub fn state_bucket_name(&mut self, state_bucket_name: String) -> &mut Self {
115        self.state_bucket_name = Some(state_bucket_name);
116        self
117    }
118
119    pub fn terraform_binary_path(&mut self, terraform_binary_path: PathBuf) -> &mut Self {
120        self.terraform_binary_path = Some(terraform_binary_path);
121        self
122    }
123
124    pub fn working_directory(&mut self, working_directory_path: PathBuf) -> &mut Self {
125        self.working_directory_path = Some(working_directory_path);
126        self
127    }
128
129    pub fn ssh_secret_key_path(&mut self, ssh_secret_key_path: PathBuf) -> &mut Self {
130        self.ssh_secret_key_path = Some(ssh_secret_key_path);
131        self
132    }
133
134    pub fn vault_password_path(&mut self, vault_password_path: PathBuf) -> &mut Self {
135        self.vault_password_path = Some(vault_password_path);
136        self
137    }
138
139    pub fn region(&mut self, region: String) -> &mut Self {
140        self.region = Some(region);
141        self
142    }
143
144    pub fn build(&self) -> Result<ClientsDeployer> {
145        let provider = self.provider.unwrap_or(CloudProvider::DigitalOcean);
146        match provider {
147            CloudProvider::DigitalOcean => {
148                let digital_ocean_pat = std::env::var("DO_PAT").map_err(|_| {
149                    Error::CloudProviderCredentialsNotSupplied("DO_PAT".to_string())
150                })?;
151                // The DO_PAT variable is not actually read by either Terraform or Ansible.
152                // Each tool uses a different variable, so instead we set each of those variables
153                // to the value of DO_PAT. This means the user only needs to set one variable.
154                std::env::set_var("DIGITALOCEAN_TOKEN", digital_ocean_pat.clone());
155                std::env::set_var("DO_API_TOKEN", digital_ocean_pat);
156            }
157            _ => {
158                return Err(Error::CloudProviderNotSupported(provider.to_string()));
159            }
160        }
161
162        let state_bucket_name = match self.state_bucket_name {
163            Some(ref bucket_name) => bucket_name.clone(),
164            None => std::env::var("CLIENT_TERRAFORM_STATE_BUCKET_NAME")?,
165        };
166
167        let default_terraform_bin_path = PathBuf::from("terraform");
168        let terraform_binary_path = self
169            .terraform_binary_path
170            .as_ref()
171            .unwrap_or(&default_terraform_bin_path);
172
173        let working_directory_path = match self.working_directory_path {
174            Some(ref work_dir_path) => work_dir_path.clone(),
175            None => std::env::current_dir()?.join("resources"),
176        };
177
178        let ssh_secret_key_path = match self.ssh_secret_key_path {
179            Some(ref ssh_sk_path) => ssh_sk_path.clone(),
180            None => PathBuf::from(std::env::var("SSH_KEY_PATH")?),
181        };
182
183        let vault_password_path = match self.vault_password_path {
184            Some(ref vault_pw_path) => vault_pw_path.clone(),
185            None => PathBuf::from(std::env::var("ANSIBLE_VAULT_PASSWORD_PATH")?),
186        };
187
188        let region = match self.region {
189            Some(ref region) => region.clone(),
190            None => "lon1".to_string(),
191        };
192
193        let terraform_runner = TerraformRunner::new(
194            terraform_binary_path.to_path_buf(),
195            working_directory_path
196                .join("terraform")
197                .join("clients")
198                .join(provider.to_string()),
199            provider,
200            &state_bucket_name,
201        )?;
202
203        let ansible_runner = AnsibleRunner::new(
204            self.ansible_forks.unwrap_or(ANSIBLE_DEFAULT_FORKS),
205            self.ansible_verbose_mode,
206            &self.environment_name,
207            provider,
208            ssh_secret_key_path.clone(),
209            vault_password_path,
210            working_directory_path.join("ansible"),
211        )?;
212
213        let ssh_client = SshClient::new(ssh_secret_key_path);
214        let ansible_provisioner =
215            AnsibleProvisioner::new(ansible_runner, provider, ssh_client.clone());
216
217        let client_deployer = ClientsDeployer::new(
218            ansible_provisioner,
219            provider,
220            self.deployment_type.clone(),
221            &self.environment_name,
222            S3Repository {},
223            ssh_client,
224            terraform_runner,
225            working_directory_path,
226            region,
227        )?;
228
229        Ok(client_deployer)
230    }
231}
232
233#[derive(Clone)]
234pub struct ClientsDeployer {
235    pub ansible_provisioner: AnsibleProvisioner,
236    pub cloud_provider: CloudProvider,
237    pub deployment_type: EnvironmentType,
238    pub environment_name: String,
239    pub inventory_file_path: PathBuf,
240    pub region: String,
241    pub s3_repository: S3Repository,
242    pub ssh_client: SshClient,
243    pub terraform_runner: TerraformRunner,
244    pub working_directory_path: PathBuf,
245}
246
247impl ClientsDeployer {
248    #[allow(clippy::too_many_arguments)]
249    pub fn new(
250        ansible_provisioner: AnsibleProvisioner,
251        cloud_provider: CloudProvider,
252        deployment_type: EnvironmentType,
253        environment_name: &str,
254        s3_repository: S3Repository,
255        ssh_client: SshClient,
256        terraform_runner: TerraformRunner,
257        working_directory_path: PathBuf,
258        region: String,
259    ) -> Result<ClientsDeployer> {
260        if environment_name.is_empty() {
261            return Err(Error::EnvironmentNameRequired);
262        }
263        let inventory_file_path = working_directory_path
264            .join("ansible")
265            .join("inventory")
266            .join("dev_inventory_digital_ocean.yml");
267
268        Ok(ClientsDeployer {
269            ansible_provisioner,
270            cloud_provider,
271            deployment_type,
272            environment_name: environment_name.to_string(),
273            inventory_file_path,
274            region,
275            s3_repository,
276            ssh_client,
277            terraform_runner,
278            working_directory_path,
279        })
280    }
281
282    pub fn create_or_update_infra(&self, options: &ClientsInfraRunOptions) -> Result<()> {
283        let start = Instant::now();
284        println!("Selecting {} workspace...", options.name);
285        self.terraform_runner.workspace_select(&options.name)?;
286
287        let args = options.build_terraform_args()?;
288
289        println!("Running terraform apply...");
290        self.terraform_runner
291            .apply(args, Some(options.tfvars_filenames.clone()))?;
292        print_duration(start.elapsed());
293        Ok(())
294    }
295
296    pub async fn init(&self) -> Result<()> {
297        self.terraform_runner.init()?;
298        let workspaces = self.terraform_runner.workspace_list()?;
299        if !workspaces.contains(&self.environment_name) {
300            self.terraform_runner
301                .workspace_new(&self.environment_name)?;
302        } else {
303            println!("Workspace {} already exists", self.environment_name);
304        }
305
306        Ok(())
307    }
308
309    pub fn plan(&self, options: &ClientsInfraRunOptions) -> Result<()> {
310        println!("Selecting {} workspace...", options.name);
311        self.terraform_runner.workspace_select(&options.name)?;
312
313        let args = options.build_terraform_args()?;
314
315        self.terraform_runner
316            .plan(Some(args), Some(options.tfvars_filenames.clone()))?;
317        Ok(())
318    }
319
320    pub async fn deploy(&self, options: ClientsDeployOptions) -> Result<()> {
321        println!(
322            "Deploying client for environment: {}",
323            self.environment_name
324        );
325
326        let build_custom_binaries = options.binary_option.should_provision_build_machine();
327
328        let start = Instant::now();
329        println!("Initializing infrastructure...");
330
331        let infra_options = ClientsInfraRunOptions {
332            client_image_id: None,
333            client_vm_count: options.client_vm_count,
334            client_vm_size: options.client_vm_size.clone(),
335            enable_build_vm: build_custom_binaries,
336            name: options.name.clone(),
337            tfvars_filenames: options.current_inventory.get_tfvars_filenames(),
338        };
339
340        self.create_or_update_infra(&infra_options)?;
341
342        write_environment_details(
343            &self.s3_repository,
344            &options.name,
345            &EnvironmentDetails {
346                deployment_type: DeploymentType::Client,
347                environment_type: options.environment_type.clone(),
348                evm_details: EvmDetails {
349                    network: options.evm_details.network.clone(),
350                    data_payments_address: options.evm_details.data_payments_address.clone(),
351                    payment_token_address: options.evm_details.payment_token_address.clone(),
352                    rpc_url: options.evm_details.rpc_url.clone(),
353                },
354                funding_wallet_address: None,
355                network_id: options.network_id,
356                region: self.region.clone(),
357                rewards_address: None,
358            },
359        )
360        .await?;
361
362        println!("Provisioning Client with Ansible...");
363        let provision_options = ProvisionOptions::from(options.clone());
364
365        if build_custom_binaries {
366            self.ansible_provisioner
367                .print_ansible_run_banner("Build Custom Binaries");
368            self.ansible_provisioner
369                .build_autonomi_binaries(&provision_options, Some(vec!["ant".to_string()]))
370                .map_err(|err| {
371                    println!("Failed to build safe network binaries {err:?}");
372                    err
373                })?;
374        }
375
376        self.ansible_provisioner
377            .print_ansible_run_banner("Provision Clients");
378        self.ansible_provisioner
379            .provision_clients(
380                &provision_options,
381                options.peer.clone(),
382                options.network_contacts_url.clone(),
383            )
384            .await
385            .map_err(|err| {
386                println!("Failed to provision Clients {err:?}");
387                err
388            })?;
389
390        self.ansible_provisioner
391            .print_ansible_run_banner("Provision Downloaders");
392        self.ansible_provisioner
393            .provision_downloaders(
394                &provision_options,
395                options.peer.clone(),
396                options.network_contacts_url.clone(),
397            )
398            .await
399            .map_err(|err| {
400                println!("Failed to provision downloaders {err:?}");
401                err
402            })?;
403
404        println!("Deployment completed successfully in {:?}", start.elapsed());
405        Ok(())
406    }
407
408    pub async fn deploy_static_downloaders(&self, options: ClientsDeployOptions) -> Result<()> {
409        println!(
410            "Deploying static downloaders for environment: {}",
411            self.environment_name
412        );
413
414        let build_custom_binaries = options.binary_option.should_provision_build_machine();
415
416        let start = Instant::now();
417        println!("Initializing infrastructure...");
418
419        let infra_options = ClientsInfraRunOptions {
420            client_image_id: None,
421            client_vm_count: options.client_vm_count,
422            client_vm_size: options.client_vm_size.clone(),
423            enable_build_vm: build_custom_binaries,
424            name: options.name.clone(),
425            tfvars_filenames: options.current_inventory.get_tfvars_filenames(),
426        };
427
428        self.create_or_update_infra(&infra_options)?;
429
430        write_environment_details(
431            &self.s3_repository,
432            &options.name,
433            &EnvironmentDetails {
434                deployment_type: DeploymentType::Client,
435                environment_type: options.environment_type.clone(),
436                evm_details: EvmDetails {
437                    network: options.evm_details.network.clone(),
438                    data_payments_address: options.evm_details.data_payments_address.clone(),
439                    payment_token_address: options.evm_details.payment_token_address.clone(),
440                    rpc_url: options.evm_details.rpc_url.clone(),
441                },
442                funding_wallet_address: None,
443                network_id: options.network_id,
444                region: self.region.clone(),
445                rewards_address: None,
446            },
447        )
448        .await?;
449
450        println!("Provisioning static downloaders with Ansible...");
451        let provision_options = ProvisionOptions::from(options.clone());
452
453        if build_custom_binaries {
454            self.ansible_provisioner
455                .print_ansible_run_banner("Build Custom Binaries");
456            self.ansible_provisioner
457                .build_autonomi_binaries(&provision_options, Some(vec!["ant".to_string()]))
458                .map_err(|err| {
459                    println!("Failed to build safe network binaries {err:?}");
460                    err
461                })?;
462        }
463
464        self.ansible_provisioner
465            .print_ansible_run_banner("Provision Static Downloaders");
466        self.ansible_provisioner
467            .provision_static_downloaders(
468                &provision_options,
469                options.peer.clone(),
470                options.network_contacts_url.clone(),
471            )
472            .await
473            .map_err(|err| {
474                println!("Failed to provision static downloaders {err:?}");
475                err
476            })?;
477
478        println!(
479            "Static downloader deployment completed successfully in {:?}",
480            start.elapsed()
481        );
482        Ok(())
483    }
484
485    async fn destroy_infra(&self, environment_details: &EnvironmentDetails) -> Result<()> {
486        crate::infra::select_workspace(&self.terraform_runner, &self.environment_name)?;
487
488        let options = ClientsInfraRunOptions::generate_existing(
489            &self.environment_name,
490            &self.terraform_runner,
491            environment_details,
492        )
493        .await?;
494
495        let mut args = Vec::new();
496        if let Some(vm_count) = options.client_vm_count {
497            args.push(("ant_client_vm_count".to_string(), vm_count.to_string()));
498        }
499        if let Some(vm_size) = &options.client_vm_size {
500            args.push(("ant_client_droplet_size".to_string(), vm_size.clone()));
501        }
502        args.push((
503            "use_custom_bin".to_string(),
504            options.enable_build_vm.to_string(),
505        ));
506
507        self.terraform_runner
508            .destroy(Some(args), Some(options.tfvars_filenames.clone()))?;
509
510        crate::infra::delete_workspace(&self.terraform_runner, &self.environment_name)?;
511
512        Ok(())
513    }
514
515    pub async fn clean(&self) -> Result<()> {
516        let environment_details =
517            get_environment_details(&self.environment_name, &self.s3_repository).await?;
518        crate::funding::drain_funds(&self.ansible_provisioner, &environment_details).await?;
519
520        self.destroy_infra(&environment_details).await?;
521
522        cleanup_environment_inventory(
523            &self.environment_name,
524            &self
525                .working_directory_path
526                .join("ansible")
527                .join("inventory"),
528            None,
529        )?;
530
531        self.s3_repository
532            .delete_object("sn-environment-type", &self.environment_name)
533            .await?;
534        Ok(())
535    }
536}