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 chunk_tracker_data_addresses: Vec<String>,
35    pub chunk_tracker_services: u16,
36    pub client_env_variables: Option<Vec<(String, String)>>,
37    pub client_vm_count: Option<u16>,
38    pub client_vm_size: Option<String>,
39    pub current_inventory: ClientsDeploymentInventory,
40    pub delayed_verifier_batch_size: Option<u16>,
41    pub delayed_verifier_quorum_value: Option<String>,
42    pub enable_metrics: bool,
43    pub environment_type: EnvironmentType,
44    pub evm_details: EvmDetails,
45    pub file_address: Option<String>,
46    pub expected_hash: Option<String>,
47    pub expected_size: Option<u64>,
48    pub funding_wallet_secret_key: Option<String>,
49    pub initial_gas: Option<U256>,
50    pub initial_tokens: Option<U256>,
51    pub max_archived_log_files: u16,
52    pub max_log_files: u16,
53    pub max_uploads: Option<u32>,
54    pub merkle: bool,
55    pub name: String,
56    pub network_id: Option<u8>,
57    pub network_contacts_url: Option<String>,
58    pub output_inventory_dir_path: PathBuf,
59    pub peer: Option<String>,
60    pub performance_verifier_batch_size: Option<u16>,
61    pub random_verifier_batch_size: Option<u16>,
62    pub repair_service_count: u16,
63    pub data_retrieval_service_count: u16,
64    pub run_chunk_trackers_provision: bool,
65    pub run_data_retrieval_provision: bool,
66    pub run_downloaders_provision: bool,
67    pub run_repair_files_provision: bool,
68    pub run_scan_repair_provision: bool,
69    pub run_uploaders_provision: bool,
70    pub scan_frequency: Option<u64>,
71    pub sleep_duration: Option<u16>,
72    pub sleep_interval: Option<u64>,
73    pub start_chunk_trackers: bool,
74    pub start_data_retrieval: bool,
75    pub start_delayed_verifier: bool,
76    pub start_performance_verifier: bool,
77    pub start_random_verifier: bool,
78    pub start_repair_service: bool,
79    pub start_uploaders: bool,
80    pub uploaders_count: u16,
81    pub upload_size: Option<u16>,
82    pub upload_interval: u16,
83    pub upload_batch_size: Option<u16>,
84    pub wallet_secret_keys: Option<Vec<String>>,
85}
86
87#[derive(Default)]
88pub struct ClientsDeployBuilder {
89    ansible_forks: Option<usize>,
90    ansible_verbose_mode: bool,
91    deployment_type: EnvironmentType,
92    environment_name: String,
93    provider: Option<CloudProvider>,
94    region: Option<String>,
95    ssh_secret_key_path: Option<PathBuf>,
96    state_bucket_name: Option<String>,
97    terraform_binary_path: Option<PathBuf>,
98    vault_password_path: Option<PathBuf>,
99    working_directory_path: Option<PathBuf>,
100}
101
102impl ClientsDeployBuilder {
103    pub fn new() -> Self {
104        Default::default()
105    }
106
107    pub fn ansible_verbose_mode(&mut self, ansible_verbose_mode: bool) -> &mut Self {
108        self.ansible_verbose_mode = ansible_verbose_mode;
109        self
110    }
111
112    pub fn ansible_forks(&mut self, ansible_forks: usize) -> &mut Self {
113        self.ansible_forks = Some(ansible_forks);
114        self
115    }
116
117    pub fn deployment_type(&mut self, deployment_type: EnvironmentType) -> &mut Self {
118        self.deployment_type = deployment_type;
119        self
120    }
121
122    pub fn environment_name(&mut self, name: &str) -> &mut Self {
123        self.environment_name = name.to_string();
124        self
125    }
126
127    pub fn provider(&mut self, provider: CloudProvider) -> &mut Self {
128        self.provider = Some(provider);
129        self
130    }
131
132    pub fn state_bucket_name(&mut self, state_bucket_name: String) -> &mut Self {
133        self.state_bucket_name = Some(state_bucket_name);
134        self
135    }
136
137    pub fn terraform_binary_path(&mut self, terraform_binary_path: PathBuf) -> &mut Self {
138        self.terraform_binary_path = Some(terraform_binary_path);
139        self
140    }
141
142    pub fn working_directory(&mut self, working_directory_path: PathBuf) -> &mut Self {
143        self.working_directory_path = Some(working_directory_path);
144        self
145    }
146
147    pub fn ssh_secret_key_path(&mut self, ssh_secret_key_path: PathBuf) -> &mut Self {
148        self.ssh_secret_key_path = Some(ssh_secret_key_path);
149        self
150    }
151
152    pub fn vault_password_path(&mut self, vault_password_path: PathBuf) -> &mut Self {
153        self.vault_password_path = Some(vault_password_path);
154        self
155    }
156
157    pub fn region(&mut self, region: String) -> &mut Self {
158        self.region = Some(region);
159        self
160    }
161
162    pub fn build(&self) -> Result<ClientsDeployer> {
163        let provider = self.provider.unwrap_or(CloudProvider::DigitalOcean);
164        match provider {
165            CloudProvider::DigitalOcean => {
166                let digital_ocean_pat = std::env::var("DO_PAT").map_err(|_| {
167                    Error::CloudProviderCredentialsNotSupplied("DO_PAT".to_string())
168                })?;
169                // The DO_PAT variable is not actually read by either Terraform or Ansible.
170                // Each tool uses a different variable, so instead we set each of those variables
171                // to the value of DO_PAT. This means the user only needs to set one variable.
172                std::env::set_var("DIGITALOCEAN_TOKEN", digital_ocean_pat.clone());
173                std::env::set_var("DO_API_TOKEN", digital_ocean_pat);
174            }
175            _ => {
176                return Err(Error::CloudProviderNotSupported(provider.to_string()));
177            }
178        }
179
180        let state_bucket_name = match self.state_bucket_name {
181            Some(ref bucket_name) => bucket_name.clone(),
182            None => std::env::var("CLIENT_TERRAFORM_STATE_BUCKET_NAME")?,
183        };
184
185        let default_terraform_bin_path = PathBuf::from("terraform");
186        let terraform_binary_path = self
187            .terraform_binary_path
188            .as_ref()
189            .unwrap_or(&default_terraform_bin_path);
190
191        let working_directory_path = match self.working_directory_path {
192            Some(ref work_dir_path) => work_dir_path.clone(),
193            None => std::env::current_dir()?.join("resources"),
194        };
195
196        let ssh_secret_key_path = match self.ssh_secret_key_path {
197            Some(ref ssh_sk_path) => ssh_sk_path.clone(),
198            None => PathBuf::from(std::env::var("SSH_KEY_PATH")?),
199        };
200
201        let vault_password_path = match self.vault_password_path {
202            Some(ref vault_pw_path) => vault_pw_path.clone(),
203            None => PathBuf::from(std::env::var("ANSIBLE_VAULT_PASSWORD_PATH")?),
204        };
205
206        let region = match self.region {
207            Some(ref region) => region.clone(),
208            None => "lon1".to_string(),
209        };
210
211        let terraform_runner = TerraformRunner::new(
212            terraform_binary_path.to_path_buf(),
213            working_directory_path
214                .join("terraform")
215                .join("clients")
216                .join(provider.to_string()),
217            provider,
218            &state_bucket_name,
219        )?;
220
221        let ansible_runner = AnsibleRunner::new(
222            self.ansible_forks.unwrap_or(ANSIBLE_DEFAULT_FORKS),
223            self.ansible_verbose_mode,
224            &self.environment_name,
225            provider,
226            ssh_secret_key_path.clone(),
227            vault_password_path,
228            working_directory_path.join("ansible"),
229        )?;
230
231        let ssh_client = SshClient::new(ssh_secret_key_path);
232        let ansible_provisioner =
233            AnsibleProvisioner::new(ansible_runner, provider, ssh_client.clone());
234
235        let client_deployer = ClientsDeployer::new(
236            ansible_provisioner,
237            provider,
238            self.deployment_type.clone(),
239            &self.environment_name,
240            S3Repository {},
241            ssh_client,
242            terraform_runner,
243            working_directory_path,
244            region,
245        )?;
246
247        Ok(client_deployer)
248    }
249}
250
251#[derive(Clone)]
252pub struct ClientsDeployer {
253    pub ansible_provisioner: AnsibleProvisioner,
254    pub cloud_provider: CloudProvider,
255    pub deployment_type: EnvironmentType,
256    pub environment_name: String,
257    pub inventory_file_path: PathBuf,
258    pub region: String,
259    pub s3_repository: S3Repository,
260    pub ssh_client: SshClient,
261    pub terraform_runner: TerraformRunner,
262    pub working_directory_path: PathBuf,
263}
264
265impl ClientsDeployer {
266    #[allow(clippy::too_many_arguments)]
267    pub fn new(
268        ansible_provisioner: AnsibleProvisioner,
269        cloud_provider: CloudProvider,
270        deployment_type: EnvironmentType,
271        environment_name: &str,
272        s3_repository: S3Repository,
273        ssh_client: SshClient,
274        terraform_runner: TerraformRunner,
275        working_directory_path: PathBuf,
276        region: String,
277    ) -> Result<ClientsDeployer> {
278        if environment_name.is_empty() {
279            return Err(Error::EnvironmentNameRequired);
280        }
281        let inventory_file_path = working_directory_path
282            .join("ansible")
283            .join("inventory")
284            .join("dev_inventory_digital_ocean.yml");
285
286        Ok(ClientsDeployer {
287            ansible_provisioner,
288            cloud_provider,
289            deployment_type,
290            environment_name: environment_name.to_string(),
291            inventory_file_path,
292            region,
293            s3_repository,
294            ssh_client,
295            terraform_runner,
296            working_directory_path,
297        })
298    }
299
300    pub fn create_or_update_infra(&self, options: &ClientsInfraRunOptions) -> Result<()> {
301        let start = Instant::now();
302        println!("Selecting {} workspace...", options.name);
303        self.terraform_runner.workspace_select(&options.name)?;
304
305        let args = options.build_terraform_args()?;
306
307        println!("Running terraform apply...");
308        self.terraform_runner
309            .apply(args, Some(options.tfvars_filenames.clone()))?;
310        print_duration(start.elapsed());
311        Ok(())
312    }
313
314    pub async fn init(&self) -> Result<()> {
315        self.terraform_runner.init()?;
316        let workspaces = self.terraform_runner.workspace_list()?;
317        if !workspaces.contains(&self.environment_name) {
318            self.terraform_runner
319                .workspace_new(&self.environment_name)?;
320        } else {
321            println!("Workspace {} already exists", self.environment_name);
322        }
323
324        Ok(())
325    }
326
327    pub fn plan(&self, options: &ClientsInfraRunOptions) -> Result<()> {
328        println!("Selecting {} workspace...", options.name);
329        self.terraform_runner.workspace_select(&options.name)?;
330
331        let args = options.build_terraform_args()?;
332
333        self.terraform_runner
334            .plan(Some(args), Some(options.tfvars_filenames.clone()))?;
335        Ok(())
336    }
337
338    pub async fn deploy(&self, options: ClientsDeployOptions) -> Result<()> {
339        println!(
340            "Deploying client for environment: {}",
341            self.environment_name
342        );
343
344        let build_custom_binaries = options.binary_option.should_provision_build_machine();
345
346        let start = Instant::now();
347        println!("Initializing infrastructure...");
348
349        let infra_options = ClientsInfraRunOptions {
350            client_image_id: None,
351            client_vm_count: options.client_vm_count,
352            client_vm_size: options.client_vm_size.clone(),
353            enable_build_vm: build_custom_binaries,
354            name: options.name.clone(),
355            tfvars_filenames: options.current_inventory.get_tfvars_filenames(),
356        };
357
358        self.create_or_update_infra(&infra_options)?;
359
360        write_environment_details(
361            &self.s3_repository,
362            &options.name,
363            &EnvironmentDetails {
364                deployment_type: DeploymentType::Client,
365                environment_type: options.environment_type.clone(),
366                evm_details: EvmDetails {
367                    network: options.evm_details.network.clone(),
368                    data_payments_address: options.evm_details.data_payments_address.clone(),
369                    merkle_payments_address: options.evm_details.merkle_payments_address.clone(),
370                    payment_token_address: options.evm_details.payment_token_address.clone(),
371                    rpc_url: options.evm_details.rpc_url.clone(),
372                },
373                funding_wallet_address: None,
374                network_id: options.network_id,
375                region: self.region.clone(),
376                rewards_address: None,
377            },
378        )
379        .await?;
380
381        let provision_options = ProvisionOptions::from(options.clone());
382        if build_custom_binaries {
383            self.ansible_provisioner
384                .print_ansible_run_banner("Build Custom Binaries");
385            self.ansible_provisioner
386                .build_autonomi_binaries(&provision_options, Some(vec!["ant".to_string()]))
387                .map_err(|err| {
388                    println!("Failed to build safe network binaries {err:?}");
389                    err
390                })?;
391        }
392
393        if options.run_uploaders_provision {
394            self.ansible_provisioner
395                .print_ansible_run_banner("Provision Uploaders");
396            self.ansible_provisioner
397                .provision_uploaders(
398                    &provision_options,
399                    options.peer.clone(),
400                    options.network_contacts_url.clone(),
401                )
402                .await
403                .map_err(|err| {
404                    println!("Failed to provision Clients {err:?}");
405                    err
406                })?;
407        }
408
409        if options.run_downloaders_provision {
410            self.ansible_provisioner
411                .print_ansible_run_banner("Provision Downloaders");
412            self.ansible_provisioner
413                .provision_downloaders(
414                    &provision_options,
415                    options.peer.clone(),
416                    options.network_contacts_url.clone(),
417                )
418                .await
419                .map_err(|err| {
420                    println!("Failed to provision downloaders {err:?}");
421                    err
422                })?;
423        }
424
425        if options.run_chunk_trackers_provision {
426            self.ansible_provisioner
427                .print_ansible_run_banner("Provision Chunk Trackers");
428            self.ansible_provisioner
429                .provision_chunk_trackers(
430                    &provision_options,
431                    options.peer.clone(),
432                    options.network_contacts_url.clone(),
433                )
434                .await
435                .map_err(|err| {
436                    println!("Failed to provision chunk trackers {err:?}");
437                    err
438                })?;
439        }
440
441        if options.run_data_retrieval_provision {
442            self.ansible_provisioner
443                .print_ansible_run_banner("Provision Data Retrieval Service");
444            self.ansible_provisioner
445                .provision_data_retrieval(&provision_options, options.network_contacts_url.clone())
446                .await
447                .map_err(|err| {
448                    println!("Failed to provision data retrieval service {err:?}");
449                    err
450                })?;
451        }
452
453        if options.run_repair_files_provision {
454            self.ansible_provisioner
455                .print_ansible_run_banner("Provision Repair Service");
456            self.ansible_provisioner
457                .provision_repair_files(&provision_options)
458                .await
459                .map_err(|err| {
460                    println!("Failed to provision repair files service {err:?}");
461                    err
462                })?;
463        }
464
465        if options.run_scan_repair_provision {
466            self.ansible_provisioner
467                .print_ansible_run_banner("Provision Scan Repair Service");
468            self.ansible_provisioner
469                .provision_scan_repair(&provision_options)
470                .await
471                .map_err(|err| {
472                    println!("Failed to provision scan repair service {err:?}");
473                    err
474                })?;
475        }
476
477        println!("Deployment completed successfully in {:?}", start.elapsed());
478        Ok(())
479    }
480
481    pub async fn deploy_static_downloaders(&self, options: ClientsDeployOptions) -> Result<()> {
482        println!(
483            "Deploying static downloaders for environment: {}",
484            self.environment_name
485        );
486
487        let build_custom_binaries = options.binary_option.should_provision_build_machine();
488
489        let start = Instant::now();
490        println!("Initializing infrastructure...");
491
492        let infra_options = ClientsInfraRunOptions {
493            client_image_id: None,
494            client_vm_count: options.client_vm_count,
495            client_vm_size: options.client_vm_size.clone(),
496            enable_build_vm: build_custom_binaries,
497            name: options.name.clone(),
498            tfvars_filenames: options.current_inventory.get_tfvars_filenames(),
499        };
500
501        self.create_or_update_infra(&infra_options)?;
502
503        write_environment_details(
504            &self.s3_repository,
505            &options.name,
506            &EnvironmentDetails {
507                deployment_type: DeploymentType::Client,
508                environment_type: options.environment_type.clone(),
509                evm_details: EvmDetails {
510                    network: options.evm_details.network.clone(),
511                    data_payments_address: options.evm_details.data_payments_address.clone(),
512                    merkle_payments_address: options.evm_details.merkle_payments_address.clone(),
513                    payment_token_address: options.evm_details.payment_token_address.clone(),
514                    rpc_url: options.evm_details.rpc_url.clone(),
515                },
516                funding_wallet_address: None,
517                network_id: options.network_id,
518                region: self.region.clone(),
519                rewards_address: None,
520            },
521        )
522        .await?;
523
524        println!("Provisioning static downloaders with Ansible...");
525        let provision_options = ProvisionOptions::from(options.clone());
526
527        if build_custom_binaries {
528            self.ansible_provisioner
529                .print_ansible_run_banner("Build Custom Binaries");
530            self.ansible_provisioner
531                .build_autonomi_binaries(&provision_options, Some(vec!["ant".to_string()]))
532                .map_err(|err| {
533                    println!("Failed to build safe network binaries {err:?}");
534                    err
535                })?;
536        }
537
538        self.ansible_provisioner
539            .print_ansible_run_banner("Provision Static Downloaders");
540        self.ansible_provisioner
541            .provision_static_downloaders(
542                &provision_options,
543                options.peer.clone(),
544                options.network_contacts_url.clone(),
545            )
546            .await
547            .map_err(|err| {
548                println!("Failed to provision static downloaders {err:?}");
549                err
550            })?;
551
552        println!(
553            "Static downloader deployment completed successfully in {:?}",
554            start.elapsed()
555        );
556        Ok(())
557    }
558
559    pub async fn deploy_static_uploader(&self, options: ClientsDeployOptions) -> Result<()> {
560        println!(
561            "Deploying static uploader for environment: {}",
562            self.environment_name
563        );
564
565        let build_custom_binaries = options.binary_option.should_provision_build_machine();
566
567        let start = Instant::now();
568        println!("Initializing infrastructure...");
569
570        let infra_options = ClientsInfraRunOptions {
571            client_image_id: None,
572            client_vm_count: options.client_vm_count,
573            client_vm_size: options.client_vm_size.clone(),
574            enable_build_vm: build_custom_binaries,
575            name: options.name.clone(),
576            tfvars_filenames: options.current_inventory.get_tfvars_filenames(),
577        };
578
579        self.create_or_update_infra(&infra_options)?;
580
581        write_environment_details(
582            &self.s3_repository,
583            &options.name,
584            &EnvironmentDetails {
585                deployment_type: DeploymentType::Client,
586                environment_type: options.environment_type.clone(),
587                evm_details: EvmDetails {
588                    network: options.evm_details.network.clone(),
589                    data_payments_address: options.evm_details.data_payments_address.clone(),
590                    merkle_payments_address: options.evm_details.merkle_payments_address.clone(),
591                    payment_token_address: options.evm_details.payment_token_address.clone(),
592                    rpc_url: options.evm_details.rpc_url.clone(),
593                },
594                funding_wallet_address: None,
595                network_id: options.network_id,
596                region: self.region.clone(),
597                rewards_address: None,
598            },
599        )
600        .await?;
601
602        println!("Provisioning static uploader with Ansible...");
603        let provision_options = ProvisionOptions::from(options.clone());
604
605        if build_custom_binaries {
606            self.ansible_provisioner
607                .print_ansible_run_banner("Build Custom Binaries");
608            self.ansible_provisioner
609                .build_autonomi_binaries(&provision_options, Some(vec!["ant".to_string()]))
610                .map_err(|err| {
611                    println!("Failed to build safe network binaries {err:?}");
612                    err
613                })?;
614        }
615
616        self.ansible_provisioner
617            .print_ansible_run_banner("Provision Static Uploader");
618        self.ansible_provisioner
619            .provision_static_uploader(
620                &provision_options,
621                options.peer.clone(),
622                options.network_contacts_url.clone(),
623            )
624            .await
625            .map_err(|err| {
626                println!("Failed to provision static uploader {err:?}");
627                err
628            })?;
629
630        println!(
631            "Static uploader deployment completed successfully in {:?}",
632            start.elapsed()
633        );
634        Ok(())
635    }
636
637    async fn destroy_infra(&self, environment_details: &EnvironmentDetails) -> Result<()> {
638        crate::infra::select_workspace(&self.terraform_runner, &self.environment_name)?;
639
640        let options = ClientsInfraRunOptions::generate_existing(
641            &self.environment_name,
642            &self.terraform_runner,
643            environment_details,
644        )
645        .await?;
646
647        let mut args = Vec::new();
648        if let Some(vm_count) = options.client_vm_count {
649            args.push(("ant_client_vm_count".to_string(), vm_count.to_string()));
650        }
651        if let Some(vm_size) = &options.client_vm_size {
652            args.push(("ant_client_droplet_size".to_string(), vm_size.clone()));
653        }
654        args.push((
655            "use_custom_bin".to_string(),
656            options.enable_build_vm.to_string(),
657        ));
658
659        self.terraform_runner
660            .destroy(Some(args), Some(options.tfvars_filenames.clone()))?;
661
662        crate::infra::delete_workspace(&self.terraform_runner, &self.environment_name)?;
663
664        Ok(())
665    }
666
667    pub async fn clean(&self) -> Result<()> {
668        let environment_details =
669            get_environment_details(&self.environment_name, &self.s3_repository).await?;
670        crate::funding::drain_funds(&self.ansible_provisioner, &environment_details).await?;
671
672        self.destroy_infra(&environment_details).await?;
673
674        cleanup_environment_inventory(
675            &self.environment_name,
676            &self
677                .working_directory_path
678                .join("ansible")
679                .join("inventory"),
680            None,
681        )?;
682
683        self.s3_repository
684            .delete_object("sn-environment-type", &self.environment_name)
685            .await?;
686        Ok(())
687    }
688}