1use 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 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}