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 enable_download_verifier: bool,
39 pub enable_performance_verifier: bool,
40 pub enable_random_verifier: bool,
41 pub enable_telegraf: bool,
42 pub enable_uploaders: 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 uploaders_count: u16,
60 pub upload_size: Option<u16>,
61 pub wallet_secret_keys: Option<Vec<String>>,
62}
63
64#[derive(Default)]
65pub struct ClientsDeployBuilder {
66 ansible_forks: Option<usize>,
67 ansible_verbose_mode: bool,
68 deployment_type: EnvironmentType,
69 environment_name: String,
70 provider: Option<CloudProvider>,
71 region: Option<String>,
72 ssh_secret_key_path: Option<PathBuf>,
73 state_bucket_name: Option<String>,
74 terraform_binary_path: Option<PathBuf>,
75 vault_password_path: Option<PathBuf>,
76 working_directory_path: Option<PathBuf>,
77}
78
79impl ClientsDeployBuilder {
80 pub fn new() -> Self {
81 Default::default()
82 }
83
84 pub fn ansible_verbose_mode(&mut self, ansible_verbose_mode: bool) -> &mut Self {
85 self.ansible_verbose_mode = ansible_verbose_mode;
86 self
87 }
88
89 pub fn ansible_forks(&mut self, ansible_forks: usize) -> &mut Self {
90 self.ansible_forks = Some(ansible_forks);
91 self
92 }
93
94 pub fn deployment_type(&mut self, deployment_type: EnvironmentType) -> &mut Self {
95 self.deployment_type = deployment_type;
96 self
97 }
98
99 pub fn environment_name(&mut self, name: &str) -> &mut Self {
100 self.environment_name = name.to_string();
101 self
102 }
103
104 pub fn provider(&mut self, provider: CloudProvider) -> &mut Self {
105 self.provider = Some(provider);
106 self
107 }
108
109 pub fn state_bucket_name(&mut self, state_bucket_name: String) -> &mut Self {
110 self.state_bucket_name = Some(state_bucket_name);
111 self
112 }
113
114 pub fn terraform_binary_path(&mut self, terraform_binary_path: PathBuf) -> &mut Self {
115 self.terraform_binary_path = Some(terraform_binary_path);
116 self
117 }
118
119 pub fn working_directory(&mut self, working_directory_path: PathBuf) -> &mut Self {
120 self.working_directory_path = Some(working_directory_path);
121 self
122 }
123
124 pub fn ssh_secret_key_path(&mut self, ssh_secret_key_path: PathBuf) -> &mut Self {
125 self.ssh_secret_key_path = Some(ssh_secret_key_path);
126 self
127 }
128
129 pub fn vault_password_path(&mut self, vault_password_path: PathBuf) -> &mut Self {
130 self.vault_password_path = Some(vault_password_path);
131 self
132 }
133
134 pub fn region(&mut self, region: String) -> &mut Self {
135 self.region = Some(region);
136 self
137 }
138
139 pub fn build(&self) -> Result<ClientsDeployer> {
140 let provider = self.provider.unwrap_or(CloudProvider::DigitalOcean);
141 match provider {
142 CloudProvider::DigitalOcean => {
143 let digital_ocean_pat = std::env::var("DO_PAT").map_err(|_| {
144 Error::CloudProviderCredentialsNotSupplied("DO_PAT".to_string())
145 })?;
146 std::env::set_var("DIGITALOCEAN_TOKEN", digital_ocean_pat.clone());
150 std::env::set_var("DO_API_TOKEN", digital_ocean_pat);
151 }
152 _ => {
153 return Err(Error::CloudProviderNotSupported(provider.to_string()));
154 }
155 }
156
157 let state_bucket_name = match self.state_bucket_name {
158 Some(ref bucket_name) => bucket_name.clone(),
159 None => std::env::var("CLIENT_TERRAFORM_STATE_BUCKET_NAME")?,
160 };
161
162 let default_terraform_bin_path = PathBuf::from("terraform");
163 let terraform_binary_path = self
164 .terraform_binary_path
165 .as_ref()
166 .unwrap_or(&default_terraform_bin_path);
167
168 let working_directory_path = match self.working_directory_path {
169 Some(ref work_dir_path) => work_dir_path.clone(),
170 None => std::env::current_dir()?.join("resources"),
171 };
172
173 let ssh_secret_key_path = match self.ssh_secret_key_path {
174 Some(ref ssh_sk_path) => ssh_sk_path.clone(),
175 None => PathBuf::from(std::env::var("SSH_KEY_PATH")?),
176 };
177
178 let vault_password_path = match self.vault_password_path {
179 Some(ref vault_pw_path) => vault_pw_path.clone(),
180 None => PathBuf::from(std::env::var("ANSIBLE_VAULT_PASSWORD_PATH")?),
181 };
182
183 let region = match self.region {
184 Some(ref region) => region.clone(),
185 None => "lon1".to_string(),
186 };
187
188 let terraform_runner = TerraformRunner::new(
189 terraform_binary_path.to_path_buf(),
190 working_directory_path
191 .join("terraform")
192 .join("clients")
193 .join(provider.to_string()),
194 provider,
195 &state_bucket_name,
196 )?;
197
198 let ansible_runner = AnsibleRunner::new(
199 self.ansible_forks.unwrap_or(ANSIBLE_DEFAULT_FORKS),
200 self.ansible_verbose_mode,
201 &self.environment_name,
202 provider,
203 ssh_secret_key_path.clone(),
204 vault_password_path,
205 working_directory_path.join("ansible"),
206 )?;
207
208 let ssh_client = SshClient::new(ssh_secret_key_path);
209 let ansible_provisioner =
210 AnsibleProvisioner::new(ansible_runner, provider, ssh_client.clone());
211
212 let client_deployer = ClientsDeployer::new(
213 ansible_provisioner,
214 provider,
215 self.deployment_type.clone(),
216 &self.environment_name,
217 S3Repository {},
218 ssh_client,
219 terraform_runner,
220 working_directory_path,
221 region,
222 )?;
223
224 Ok(client_deployer)
225 }
226}
227
228#[derive(Clone)]
229pub struct ClientsDeployer {
230 pub ansible_provisioner: AnsibleProvisioner,
231 pub cloud_provider: CloudProvider,
232 pub deployment_type: EnvironmentType,
233 pub environment_name: String,
234 pub inventory_file_path: PathBuf,
235 pub region: String,
236 pub s3_repository: S3Repository,
237 pub ssh_client: SshClient,
238 pub terraform_runner: TerraformRunner,
239 pub working_directory_path: PathBuf,
240}
241
242impl ClientsDeployer {
243 #[allow(clippy::too_many_arguments)]
244 pub fn new(
245 ansible_provisioner: AnsibleProvisioner,
246 cloud_provider: CloudProvider,
247 deployment_type: EnvironmentType,
248 environment_name: &str,
249 s3_repository: S3Repository,
250 ssh_client: SshClient,
251 terraform_runner: TerraformRunner,
252 working_directory_path: PathBuf,
253 region: String,
254 ) -> Result<ClientsDeployer> {
255 if environment_name.is_empty() {
256 return Err(Error::EnvironmentNameRequired);
257 }
258 let inventory_file_path = working_directory_path
259 .join("ansible")
260 .join("inventory")
261 .join("dev_inventory_digital_ocean.yml");
262
263 Ok(ClientsDeployer {
264 ansible_provisioner,
265 cloud_provider,
266 deployment_type,
267 environment_name: environment_name.to_string(),
268 inventory_file_path,
269 region,
270 s3_repository,
271 ssh_client,
272 terraform_runner,
273 working_directory_path,
274 })
275 }
276
277 pub fn create_or_update_infra(&self, options: &ClientsInfraRunOptions) -> Result<()> {
278 let start = Instant::now();
279 println!("Selecting {} workspace...", options.name);
280 self.terraform_runner.workspace_select(&options.name)?;
281
282 let args = options.build_terraform_args()?;
283
284 println!("Running terraform apply...");
285 self.terraform_runner
286 .apply(args, Some(options.tfvars_filenames.clone()))?;
287 print_duration(start.elapsed());
288 Ok(())
289 }
290
291 pub async fn init(&self) -> Result<()> {
292 self.terraform_runner.init()?;
293 let workspaces = self.terraform_runner.workspace_list()?;
294 if !workspaces.contains(&self.environment_name) {
295 self.terraform_runner
296 .workspace_new(&self.environment_name)?;
297 } else {
298 println!("Workspace {} already exists", self.environment_name);
299 }
300
301 Ok(())
302 }
303
304 pub fn plan(&self, options: &ClientsInfraRunOptions) -> Result<()> {
305 println!("Selecting {} workspace...", options.name);
306 self.terraform_runner.workspace_select(&options.name)?;
307
308 let args = options.build_terraform_args()?;
309
310 self.terraform_runner
311 .plan(Some(args), Some(options.tfvars_filenames.clone()))?;
312 Ok(())
313 }
314
315 pub async fn deploy(&self, options: ClientsDeployOptions) -> Result<()> {
316 println!(
317 "Deploying client for environment: {}",
318 self.environment_name
319 );
320
321 let build_custom_binaries = {
322 match &options.binary_option {
323 BinaryOption::BuildFromSource { .. } => true,
324 BinaryOption::Versioned { .. } => false,
325 }
326 };
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_safe_network_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 async fn destroy_infra(&self, environment_details: &EnvironmentDetails) -> Result<()> {
409 crate::infra::select_workspace(&self.terraform_runner, &self.environment_name)?;
410
411 let options = ClientsInfraRunOptions::generate_existing(
412 &self.environment_name,
413 &self.terraform_runner,
414 environment_details,
415 )
416 .await?;
417
418 let mut args = Vec::new();
419 if let Some(vm_count) = options.client_vm_count {
420 args.push(("ant_client_vm_count".to_string(), vm_count.to_string()));
421 }
422 if let Some(vm_size) = &options.client_vm_size {
423 args.push(("ant_client_droplet_size".to_string(), vm_size.clone()));
424 }
425 args.push((
426 "use_custom_bin".to_string(),
427 options.enable_build_vm.to_string(),
428 ));
429
430 self.terraform_runner
431 .destroy(Some(args), Some(options.tfvars_filenames.clone()))?;
432
433 crate::infra::delete_workspace(&self.terraform_runner, &self.environment_name)?;
434
435 Ok(())
436 }
437
438 pub async fn clean(&self) -> Result<()> {
439 let environment_details =
440 get_environment_details(&self.environment_name, &self.s3_repository).await?;
441 crate::funding::drain_funds(&self.ansible_provisioner, &environment_details).await?;
442
443 self.destroy_infra(&environment_details).await?;
444
445 cleanup_environment_inventory(
446 &self.environment_name,
447 &self
448 .working_directory_path
449 .join("ansible")
450 .join("inventory"),
451 None,
452 )?;
453
454 self.s3_repository
455 .delete_object("sn-environment-type", &self.environment_name)
456 .await?;
457 Ok(())
458 }
459}