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