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