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