1use super::{
8 extra_vars::ExtraVarsDocBuilder,
9 inventory::{
10 generate_full_cone_private_node_static_environment_inventory,
11 generate_symmetric_private_node_static_environment_inventory,
12 },
13 AnsibleInventoryType, AnsiblePlaybook, AnsibleRunner,
14};
15use crate::{
16 ansible::inventory::{
17 generate_custom_environment_inventory,
18 generate_full_cone_nat_gateway_static_environment_inventory,
19 },
20 bootstrap::BootstrapOptions,
21 deploy::DeployOptions,
22 error::{Error, Result},
23 funding::FundingOptions,
24 inventory::{DeploymentNodeRegistries, VirtualMachine},
25 print_duration, run_external_command, BinaryOption, CloudProvider, EvmNetwork, LogFormat,
26 NodeType, SshClient, UpgradeOptions,
27};
28use ant_service_management::NodeRegistry;
29use evmlib::common::U256;
30use log::{debug, error, trace};
31use semver::Version;
32use serde::{Deserialize, Serialize};
33use std::{
34 collections::HashMap,
35 net::{IpAddr, SocketAddr},
36 path::PathBuf,
37 time::{Duration, Instant},
38};
39use walkdir::WalkDir;
40
41use crate::ansible::extra_vars;
42
43pub const DEFAULT_BETA_ENCRYPTION_KEY: &str =
44 "49113d2083f57a976076adbe85decb75115820de1e6e74b47e0429338cef124a";
45
46#[derive(Clone, Serialize, Deserialize)]
47pub struct ProvisionOptions {
48 pub ant_version: Option<String>,
52 pub binary_option: BinaryOption,
53 pub chunk_size: Option<u64>,
54 pub downloaders_count: u16,
55 pub enable_telegraf: bool,
56 pub env_variables: Option<Vec<(String, String)>>,
57 pub evm_data_payments_address: Option<String>,
58 pub evm_network: EvmNetwork,
59 pub evm_payment_token_address: Option<String>,
60 pub evm_rpc_url: Option<String>,
61 pub full_cone_private_node_count: u16,
62 pub funding_wallet_secret_key: Option<String>,
63 pub gas_amount: Option<U256>,
64 pub interval: Duration,
65 pub log_format: Option<LogFormat>,
66 pub logstash_details: Option<(String, Vec<SocketAddr>)>,
67 pub max_archived_log_files: u16,
68 pub max_log_files: u16,
69 pub name: String,
70 pub network_id: Option<u8>,
71 pub node_count: u16,
72 pub output_inventory_dir_path: PathBuf,
73 pub peer_cache_node_count: u16,
74 pub symmetric_private_node_count: u16,
75 pub public_rpc: bool,
76 pub rewards_address: String,
77 pub token_amount: Option<U256>,
78 pub uploaders_count: Option<u16>,
79}
80
81#[derive(Clone, Debug)]
83pub struct PrivateNodeProvisionInventory {
84 pub full_cone_nat_gateway_vms: Vec<VirtualMachine>,
85 pub full_cone_private_node_vms: Vec<VirtualMachine>,
86 pub symmetric_nat_gateway_vms: Vec<VirtualMachine>,
87 pub symmetric_private_node_vms: Vec<VirtualMachine>,
88}
89
90impl PrivateNodeProvisionInventory {
91 pub fn new(
92 provisioner: &AnsibleProvisioner,
93 full_cone_private_node_vm_count: Option<u16>,
94 symmetric_private_node_vm_count: Option<u16>,
95 ) -> Result<Self> {
96 let should_provision_full_cone_private_nodes = full_cone_private_node_vm_count
98 .map(|count| count > 0)
99 .unwrap_or(true);
100 let should_provision_symmetric_private_nodes = symmetric_private_node_vm_count
101 .map(|count| count > 0)
102 .unwrap_or(true);
103
104 let mut inventory = Self {
105 full_cone_nat_gateway_vms: Default::default(),
106 full_cone_private_node_vms: Default::default(),
107 symmetric_nat_gateway_vms: Default::default(),
108 symmetric_private_node_vms: Default::default(),
109 };
110
111 if should_provision_full_cone_private_nodes {
112 let full_cone_private_node_vms = provisioner
113 .ansible_runner
114 .get_inventory(AnsibleInventoryType::FullConePrivateNodes, true)
115 .inspect_err(|err| {
116 println!("Failed to obtain the inventory of Full Cone private node: {err:?}");
117 })?;
118
119 let full_cone_nat_gateway_inventory = provisioner
120 .ansible_runner
121 .get_inventory(AnsibleInventoryType::FullConeNatGateway, true)
122 .inspect_err(|err| {
123 println!("Failed to get Full Cone NAT Gateway inventory {err:?}");
124 })?;
125
126 if full_cone_nat_gateway_inventory.len() != full_cone_private_node_vms.len() {
127 println!("The number of Full Cone private nodes does not match the number of Full Cone NAT Gateway VMs");
128 return Err(Error::VmCountMismatch(
129 Some(AnsibleInventoryType::FullConePrivateNodes),
130 Some(AnsibleInventoryType::FullConeNatGateway),
131 ));
132 }
133
134 inventory.full_cone_private_node_vms = full_cone_private_node_vms;
135 inventory.full_cone_nat_gateway_vms = full_cone_nat_gateway_inventory;
136 }
137
138 if should_provision_symmetric_private_nodes {
139 let symmetric_private_node_vms = provisioner
140 .ansible_runner
141 .get_inventory(AnsibleInventoryType::SymmetricPrivateNodes, true)
142 .inspect_err(|err| {
143 println!("Failed to obtain the inventory of Symmetric private node: {err:?}");
144 })?;
145
146 let symmetric_nat_gateway_inventory = provisioner
147 .ansible_runner
148 .get_inventory(AnsibleInventoryType::SymmetricNatGateway, true)
149 .inspect_err(|err| {
150 println!("Failed to get Symmetric NAT Gateway inventory {err:?}");
151 })?;
152
153 if symmetric_nat_gateway_inventory.len() != symmetric_private_node_vms.len() {
154 println!("The number of Symmetric private nodes does not match the number of Symmetric NAT Gateway VMs");
155 return Err(Error::VmCountMismatch(
156 Some(AnsibleInventoryType::SymmetricPrivateNodes),
157 Some(AnsibleInventoryType::SymmetricNatGateway),
158 ));
159 }
160
161 inventory.symmetric_private_node_vms = symmetric_private_node_vms;
162 inventory.symmetric_nat_gateway_vms = symmetric_nat_gateway_inventory;
163 }
164
165 Ok(inventory)
166 }
167
168 pub fn should_provision_full_cone_private_nodes(&self) -> bool {
169 !self.full_cone_private_node_vms.is_empty()
170 }
171
172 pub fn should_provision_symmetric_private_nodes(&self) -> bool {
173 !self.symmetric_private_node_vms.is_empty()
174 }
175
176 pub fn symmetric_private_node_and_gateway_map(
177 &self,
178 ) -> Result<HashMap<VirtualMachine, VirtualMachine>> {
179 Self::match_private_node_vm_and_gateway_vm(
180 &self.symmetric_private_node_vms,
181 &self.symmetric_nat_gateway_vms,
182 )
183 }
184
185 pub fn full_cone_private_node_and_gateway_map(
186 &self,
187 ) -> Result<HashMap<VirtualMachine, VirtualMachine>> {
188 Self::match_private_node_vm_and_gateway_vm(
189 &self.full_cone_private_node_vms,
190 &self.full_cone_nat_gateway_vms,
191 )
192 }
193
194 pub fn match_private_node_vm_and_gateway_vm(
195 private_node_vms: &[VirtualMachine],
196 nat_gateway_vms: &[VirtualMachine],
197 ) -> Result<HashMap<VirtualMachine, VirtualMachine>> {
198 if private_node_vms.len() != nat_gateway_vms.len() {
199 println!(
200 "The number of private node VMs ({}) does not match the number of NAT Gateway VMs ({})",
201 private_node_vms.len(),
202 nat_gateway_vms.len()
203 );
204 error!("The number of private node VMs does not match the number of NAT Gateway VMs: Private VMs: {private_node_vms:?} Nat gateway VMs: {nat_gateway_vms:?}");
205 return Err(Error::VmCountMismatch(None, None));
206 }
207
208 let mut map = HashMap::new();
209 for private_vm in private_node_vms {
210 let nat_gateway = nat_gateway_vms
211 .iter()
212 .find(|vm| {
213 let private_node_name = private_vm.name.split('-').last().unwrap();
214 let nat_gateway_name = vm.name.split('-').last().unwrap();
215 private_node_name == nat_gateway_name
216 })
217 .ok_or_else(|| {
218 println!(
219 "Failed to find a matching NAT Gateway for private node: {}",
220 private_vm.name
221 );
222 error!("Failed to find a matching NAT Gateway for private node: {}. Private VMs: {private_node_vms:?} Nat gateway VMs: {nat_gateway_vms:?}", private_vm.name);
223 Error::VmCountMismatch(None, None)
224 })?;
225
226 let _ = map.insert(private_vm.clone(), nat_gateway.clone());
227 }
228
229 Ok(map)
230 }
231}
232
233impl From<BootstrapOptions> for ProvisionOptions {
234 fn from(bootstrap_options: BootstrapOptions) -> Self {
235 ProvisionOptions {
236 ant_version: None,
237 binary_option: bootstrap_options.binary_option,
238 chunk_size: bootstrap_options.chunk_size,
239 downloaders_count: 0,
240 enable_telegraf: true,
241 env_variables: bootstrap_options.env_variables,
242 evm_data_payments_address: bootstrap_options.evm_data_payments_address,
243 evm_network: bootstrap_options.evm_network,
244 evm_payment_token_address: bootstrap_options.evm_payment_token_address,
245 evm_rpc_url: bootstrap_options.evm_rpc_url,
246 full_cone_private_node_count: bootstrap_options.full_cone_private_node_count,
247 funding_wallet_secret_key: None,
248 gas_amount: None,
249 interval: bootstrap_options.interval,
250 log_format: bootstrap_options.log_format,
251 logstash_details: None,
252 max_archived_log_files: bootstrap_options.max_archived_log_files,
253 max_log_files: bootstrap_options.max_log_files,
254 name: bootstrap_options.name,
255 network_id: bootstrap_options.network_id,
256 node_count: bootstrap_options.node_count,
257 output_inventory_dir_path: bootstrap_options.output_inventory_dir_path,
258 peer_cache_node_count: 0,
259 symmetric_private_node_count: bootstrap_options.symmetric_private_node_count,
260 public_rpc: false,
261 rewards_address: bootstrap_options.rewards_address,
262 token_amount: None,
263 uploaders_count: None,
264 }
265 }
266}
267
268impl From<DeployOptions> for ProvisionOptions {
269 fn from(deploy_options: DeployOptions) -> Self {
270 ProvisionOptions {
271 ant_version: None,
272 binary_option: deploy_options.binary_option,
273 chunk_size: deploy_options.chunk_size,
274 downloaders_count: deploy_options.downloaders_count,
275 enable_telegraf: deploy_options.enable_telegraf,
276 env_variables: deploy_options.env_variables,
277 evm_data_payments_address: deploy_options.evm_data_payments_address,
278 evm_network: deploy_options.evm_network,
279 evm_payment_token_address: deploy_options.evm_payment_token_address,
280 evm_rpc_url: deploy_options.evm_rpc_url,
281 full_cone_private_node_count: deploy_options.full_cone_private_node_count,
282 funding_wallet_secret_key: deploy_options.funding_wallet_secret_key,
283 gas_amount: deploy_options.initial_gas,
284 interval: deploy_options.interval,
285 log_format: deploy_options.log_format,
286 logstash_details: deploy_options.logstash_details,
287 max_archived_log_files: deploy_options.max_archived_log_files,
288 max_log_files: deploy_options.max_log_files,
289 name: deploy_options.name,
290 network_id: deploy_options.network_id,
291 node_count: deploy_options.node_count,
292 output_inventory_dir_path: deploy_options.output_inventory_dir_path,
293 peer_cache_node_count: deploy_options.peer_cache_node_count,
294 symmetric_private_node_count: deploy_options.symmetric_private_node_count,
295 public_rpc: deploy_options.public_rpc,
296 rewards_address: deploy_options.rewards_address,
297 token_amount: deploy_options.initial_tokens,
298 uploaders_count: Some(deploy_options.uploaders_count),
299 }
300 }
301}
302
303#[derive(Clone)]
304pub struct AnsibleProvisioner {
305 pub ansible_runner: AnsibleRunner,
306 pub cloud_provider: CloudProvider,
307 pub ssh_client: SshClient,
308}
309
310impl AnsibleProvisioner {
311 pub fn new(
312 ansible_runner: AnsibleRunner,
313 cloud_provider: CloudProvider,
314 ssh_client: SshClient,
315 ) -> Self {
316 Self {
317 ansible_runner,
318 cloud_provider,
319 ssh_client,
320 }
321 }
322
323 pub fn build_safe_network_binaries(&self, options: &ProvisionOptions) -> Result<()> {
324 let start = Instant::now();
325 println!("Obtaining IP address for build VM...");
326 let build_inventory = self
327 .ansible_runner
328 .get_inventory(AnsibleInventoryType::Build, true)?;
329 let build_ip = build_inventory[0].public_ip_addr;
330 self.ssh_client
331 .wait_for_ssh_availability(&build_ip, &self.cloud_provider.get_ssh_user())?;
332
333 println!("Running ansible against build VM...");
334 let extra_vars = extra_vars::build_binaries_extra_vars_doc(options)?;
335 self.ansible_runner.run_playbook(
336 AnsiblePlaybook::Build,
337 AnsibleInventoryType::Build,
338 Some(extra_vars),
339 )?;
340 print_duration(start.elapsed());
341 Ok(())
342 }
343
344 pub fn cleanup_node_logs(&self, setup_cron: bool) -> Result<()> {
345 for node_inv_type in AnsibleInventoryType::iter_node_type() {
346 self.ansible_runner.run_playbook(
347 AnsiblePlaybook::CleanupLogs,
348 node_inv_type,
349 Some(format!("{{ \"setup_cron\": \"{setup_cron}\" }}")),
350 )?;
351 }
352
353 Ok(())
354 }
355
356 pub fn copy_logs(&self, name: &str, resources_only: bool) -> Result<()> {
357 for node_inv_type in AnsibleInventoryType::iter_node_type() {
358 self.ansible_runner.run_playbook(
359 AnsiblePlaybook::CopyLogs,
360 node_inv_type,
361 Some(format!(
362 "{{ \"env_name\": \"{name}\", \"resources_only\" : \"{resources_only}\" }}"
363 )),
364 )?;
365 }
366 Ok(())
367 }
368
369 pub fn get_all_node_inventory(&self) -> Result<Vec<VirtualMachine>> {
370 let mut all_node_inventory = Vec::new();
371 for node_inv_type in AnsibleInventoryType::iter_node_type() {
372 all_node_inventory.extend(self.ansible_runner.get_inventory(node_inv_type, false)?);
373 }
374
375 Ok(all_node_inventory)
376 }
377
378 pub fn get_symmetric_nat_gateway_inventory(&self) -> Result<Vec<VirtualMachine>> {
379 self.ansible_runner
380 .get_inventory(AnsibleInventoryType::SymmetricNatGateway, false)
381 }
382
383 pub fn get_full_cone_nat_gateway_inventory(&self) -> Result<Vec<VirtualMachine>> {
384 self.ansible_runner
385 .get_inventory(AnsibleInventoryType::FullConeNatGateway, false)
386 }
387
388 pub fn get_node_registries(
389 &self,
390 inventory_type: &AnsibleInventoryType,
391 ) -> Result<DeploymentNodeRegistries> {
392 debug!("Fetching node manager inventory for {inventory_type:?}");
393 let temp_dir_path = tempfile::tempdir()?.into_path();
394 let temp_dir_json = serde_json::to_string(&temp_dir_path)?;
395
396 self.ansible_runner.run_playbook(
397 AnsiblePlaybook::AntCtlInventory,
398 *inventory_type,
399 Some(format!("{{ \"dest\": {temp_dir_json} }}")),
400 )?;
401
402 let node_registry_paths = WalkDir::new(temp_dir_path)
403 .into_iter()
404 .flatten()
405 .filter_map(|entry| {
406 if entry.file_type().is_file()
407 && entry.path().extension().is_some_and(|ext| ext == "json")
408 {
409 let mut vm_name = entry.path().to_path_buf();
411 trace!("Found file with json extension: {vm_name:?}");
412 vm_name.pop();
413 vm_name.pop();
414 vm_name.pop();
415 trace!("Extracting the vm name from the path");
417 let vm_name = vm_name.file_name()?.to_str()?;
418 trace!("Extracted vm name from path: {vm_name}");
419 Some((vm_name.to_string(), entry.path().to_path_buf()))
420 } else {
421 None
422 }
423 })
424 .collect::<Vec<(String, PathBuf)>>();
425
426 let mut node_registries = Vec::new();
427 let mut failed_vms = Vec::new();
428 for (vm_name, file_path) in node_registry_paths {
429 match NodeRegistry::load(&file_path) {
430 Ok(node_registry) => node_registries.push((vm_name.clone(), node_registry)),
431 Err(_) => failed_vms.push(vm_name.clone()),
432 }
433 }
434
435 let deployment_registries = DeploymentNodeRegistries {
436 inventory_type: *inventory_type,
437 retrieved_registries: node_registries,
438 failed_vms,
439 };
440 Ok(deployment_registries)
441 }
442
443 pub fn provision_evm_nodes(&self, options: &ProvisionOptions) -> Result<()> {
444 let start = Instant::now();
445 println!("Obtaining IP address for EVM nodes...");
446 let evm_node_inventory = self
447 .ansible_runner
448 .get_inventory(AnsibleInventoryType::EvmNodes, true)?;
449 let evm_node_ip = evm_node_inventory[0].public_ip_addr;
450 self.ssh_client
451 .wait_for_ssh_availability(&evm_node_ip, &self.cloud_provider.get_ssh_user())?;
452
453 println!("Running ansible against EVM nodes...");
454 self.ansible_runner.run_playbook(
455 AnsiblePlaybook::EvmNodes,
456 AnsibleInventoryType::EvmNodes,
457 Some(extra_vars::build_evm_nodes_extra_vars_doc(
458 &options.name,
459 &self.cloud_provider,
460 &options.binary_option,
461 )),
462 )?;
463 print_duration(start.elapsed());
464 Ok(())
465 }
466
467 pub fn provision_genesis_node(&self, options: &ProvisionOptions) -> Result<()> {
468 let start = Instant::now();
469 let genesis_inventory = self
470 .ansible_runner
471 .get_inventory(AnsibleInventoryType::Genesis, true)?;
472 let genesis_ip = genesis_inventory[0].public_ip_addr;
473 self.ssh_client
474 .wait_for_ssh_availability(&genesis_ip, &self.cloud_provider.get_ssh_user())?;
475 self.ansible_runner.run_playbook(
476 AnsiblePlaybook::Genesis,
477 AnsibleInventoryType::Genesis,
478 Some(extra_vars::build_node_extra_vars_doc(
479 &self.cloud_provider.to_string(),
480 options,
481 NodeType::Genesis,
482 None,
483 None,
484 1,
485 options.evm_network.clone(),
486 false,
487 )?),
488 )?;
489
490 print_duration(start.elapsed());
491
492 Ok(())
493 }
494
495 pub fn provision_full_cone(
496 &self,
497 options: &ProvisionOptions,
498 initial_contact_peer: Option<String>,
499 initial_network_contacts_url: Option<String>,
500 private_node_inventory: PrivateNodeProvisionInventory,
501 new_full_cone_nat_gateway_new_vms_for_upscale: Option<Vec<VirtualMachine>>,
502 ) -> Result<()> {
503 let start = Instant::now();
505 self.print_ansible_run_banner("Provision Full Cone NAT Gateway - Step 1");
506
507 for vm in new_full_cone_nat_gateway_new_vms_for_upscale
508 .as_ref()
509 .unwrap_or(&private_node_inventory.full_cone_nat_gateway_vms)
510 .iter()
511 {
512 println!(
513 "Checking SSH availability for Full Cone NAT Gateway: {}",
514 vm.public_ip_addr
515 );
516 self.ssh_client
517 .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
518 .map_err(|e| {
519 println!(
520 "Failed to establish SSH connection to Full Cone NAT Gateway: {}",
521 e
522 );
523 e
524 })?;
525 }
526
527 let mut modified_private_node_inventory = private_node_inventory.clone();
528
529 if let Some(new_full_cone_nat_gateway_new_vms_for_upscale) =
531 &new_full_cone_nat_gateway_new_vms_for_upscale
532 {
533 debug!("Removing existing full cone NAT Gateway and private node VMs from the inventory. Old inventory: {modified_private_node_inventory:?}");
534 let mut names_to_keep = Vec::new();
535
536 for vm in new_full_cone_nat_gateway_new_vms_for_upscale.iter() {
537 let nat_gateway_name = vm.name.split('-').last().unwrap();
538 names_to_keep.push(nat_gateway_name);
539 }
540
541 modified_private_node_inventory
542 .full_cone_nat_gateway_vms
543 .retain(|vm| {
544 let nat_gateway_name = vm.name.split('-').last().unwrap();
545 names_to_keep.contains(&nat_gateway_name)
546 });
547 modified_private_node_inventory
548 .full_cone_private_node_vms
549 .retain(|vm| {
550 let nat_gateway_name = vm.name.split('-').last().unwrap();
551 names_to_keep.contains(&nat_gateway_name)
552 });
553 debug!("New inventory after removing existing full cone NAT Gateway and private node VMs: {modified_private_node_inventory:?}");
554 }
555
556 if modified_private_node_inventory
557 .full_cone_nat_gateway_vms
558 .is_empty()
559 {
560 error!("There are no full cone NAT Gateway VMs available to upscale");
561 return Ok(());
562 }
563
564 let private_node_ip_map = modified_private_node_inventory
565 .full_cone_private_node_and_gateway_map()?
566 .into_iter()
567 .map(|(k, v)| {
568 let gateway_name = if new_full_cone_nat_gateway_new_vms_for_upscale.is_some() {
569 debug!("Upscaling, using public IP address for gateway name");
570 v.public_ip_addr.to_string()
571 } else {
572 v.name.clone()
573 };
574 (gateway_name, k.private_ip_addr)
575 })
576 .collect::<HashMap<String, IpAddr>>();
577
578 if private_node_ip_map.is_empty() {
579 println!("There are no full cone private node VM available to be routed through the full cone NAT Gateway");
580 return Err(Error::EmptyInventory(
581 AnsibleInventoryType::FullConePrivateNodes,
582 ));
583 }
584
585 let vars = extra_vars::build_nat_gateway_extra_vars_doc(
586 &options.name,
587 private_node_ip_map.clone(),
588 "step1",
589 );
590 debug!("Provisioning Full Cone NAT Gateway - Step 1 with vars: {vars}");
591 let gateway_inventory = if new_full_cone_nat_gateway_new_vms_for_upscale.is_some() {
592 debug!("Upscaling, using static inventory for full cone nat gateway.");
593 generate_full_cone_nat_gateway_static_environment_inventory(
594 &modified_private_node_inventory.full_cone_nat_gateway_vms,
595 &options.name,
596 &options.output_inventory_dir_path,
597 )?;
598
599 AnsibleInventoryType::FullConeNatGatewayStatic
600 } else {
601 AnsibleInventoryType::FullConeNatGateway
602 };
603 self.ansible_runner.run_playbook(
604 AnsiblePlaybook::FullConeNatGateway,
605 gateway_inventory,
606 Some(vars),
607 )?;
608
609 self.print_ansible_run_banner("Provisioning Full Cone Private Node Config");
611
612 generate_full_cone_private_node_static_environment_inventory(
613 &options.name,
614 &options.output_inventory_dir_path,
615 &private_node_inventory.full_cone_private_node_vms,
616 &private_node_inventory.full_cone_nat_gateway_vms,
617 &self.ssh_client.private_key_path,
618 )
619 .inspect_err(|err| {
620 error!("Failed to generate full cone private node static inv with err: {err:?}")
621 })?;
622
623 println!("Obtaining IP addresses for nodes...");
627 let inventory = self
628 .ansible_runner
629 .get_inventory(AnsibleInventoryType::FullConePrivateNodes, true)?;
630
631 println!("Waiting for SSH availability on Symmetric Private nodes...");
632 for vm in inventory.iter() {
633 println!(
634 "Checking SSH availability for {}: {}",
635 vm.name, vm.public_ip_addr
636 );
637 self.ssh_client
638 .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
639 .map_err(|e| {
640 println!("Failed to establish SSH connection to {}: {}", vm.name, e);
641 e
642 })?;
643 }
644
645 println!("SSH is available on all nodes. Proceeding with provisioning...");
646
647 self.ansible_runner.run_playbook(
648 AnsiblePlaybook::PrivateNodeConfig,
649 AnsibleInventoryType::FullConePrivateNodes,
650 Some(
651 extra_vars::build_full_cone_private_node_config_extra_vars_docs(
652 &private_node_inventory,
653 )?,
654 ),
655 )?;
656
657 let vars = extra_vars::build_nat_gateway_extra_vars_doc(
660 &options.name,
661 private_node_ip_map,
662 "step2",
663 );
664
665 self.print_ansible_run_banner("Provisioning Full Cone NAT Gateway - Step 2");
666 debug!("Provisioning Full Cone NAT Gateway - Step 2 with vars: {vars}");
667 self.ansible_runner.run_playbook(
668 AnsiblePlaybook::FullConeNatGateway,
669 gateway_inventory,
670 Some(vars),
671 )?;
672
673 let home_dir = std::env::var("HOME").inspect_err(|err| {
676 println!("Failed to get home directory with error: {err:?}",);
677 })?;
678 let known_hosts_path = format!("{}/.ssh/known_hosts", home_dir);
679 debug!("Cleaning up known hosts file at {known_hosts_path} ");
680 run_external_command(
681 PathBuf::from("rm"),
682 std::env::current_dir()?,
683 vec![known_hosts_path],
684 false,
685 false,
686 )?;
687
688 self.print_ansible_run_banner("Provision Full Cone Private Nodes");
689
690 self.ssh_client.set_full_cone_nat_routed_vms(
691 &private_node_inventory.full_cone_private_node_vms,
692 &private_node_inventory.full_cone_nat_gateway_vms,
693 )?;
694
695 self.provision_nodes(
696 options,
697 initial_contact_peer,
698 initial_network_contacts_url,
699 NodeType::FullConePrivateNode,
700 )?;
701
702 print_duration(start.elapsed());
703 Ok(())
704 }
705 pub fn provision_symmetric_nat_gateway(
706 &self,
707 options: &ProvisionOptions,
708 private_node_inventory: &PrivateNodeProvisionInventory,
709 ) -> Result<()> {
710 let start = Instant::now();
711 for vm in &private_node_inventory.symmetric_nat_gateway_vms {
712 println!(
713 "Checking SSH availability for Symmetric NAT Gateway: {}",
714 vm.public_ip_addr
715 );
716 self.ssh_client
717 .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
718 .map_err(|e| {
719 println!(
720 "Failed to establish SSH connection to Symmetric NAT Gateway: {}",
721 e
722 );
723 e
724 })?;
725 }
726
727 let private_node_ip_map = private_node_inventory
728 .symmetric_private_node_and_gateway_map()?
729 .into_iter()
730 .map(|(k, v)| (v.name.clone(), k.private_ip_addr))
731 .collect::<HashMap<String, IpAddr>>();
732
733 if private_node_ip_map.is_empty() {
734 println!("There are no Symmetric private node VM available to be routed through the Symmetric NAT Gateway");
735 return Err(Error::EmptyInventory(
736 AnsibleInventoryType::SymmetricPrivateNodes,
737 ));
738 }
739
740 let vars = extra_vars::build_nat_gateway_extra_vars_doc(
741 &options.name,
742 private_node_ip_map,
743 "symmetric",
744 );
745 debug!("Provisioning Symmetric NAT Gateway with vars: {vars}");
746 self.ansible_runner.run_playbook(
747 AnsiblePlaybook::SymmetricNatGateway,
748 AnsibleInventoryType::SymmetricNatGateway,
749 Some(vars),
750 )?;
751
752 print_duration(start.elapsed());
753 Ok(())
754 }
755
756 pub fn provision_nodes(
757 &self,
758 options: &ProvisionOptions,
759 initial_contact_peer: Option<String>,
760 initial_network_contacts_url: Option<String>,
761 node_type: NodeType,
762 ) -> Result<()> {
763 let start = Instant::now();
764 let mut relay = false;
765 let (inventory_type, node_count) = match &node_type {
766 NodeType::FullConePrivateNode => {
767 relay = true;
768 (
769 node_type.to_ansible_inventory_type(),
770 options.full_cone_private_node_count,
771 )
772 }
773 NodeType::Generic => (node_type.to_ansible_inventory_type(), options.node_count),
775 NodeType::Genesis => return Err(Error::InvalidNodeType(node_type)),
776 NodeType::PeerCache => (
777 node_type.to_ansible_inventory_type(),
778 options.peer_cache_node_count,
779 ),
780 NodeType::SymmetricPrivateNode => {
781 relay = true;
782 (
783 node_type.to_ansible_inventory_type(),
784 options.symmetric_private_node_count,
785 )
786 }
787 };
788
789 println!("Obtaining IP addresses for nodes...");
793 let inventory = self.ansible_runner.get_inventory(inventory_type, true)?;
794
795 println!("Waiting for SSH availability on {node_type:?} nodes...");
796 for vm in inventory.iter() {
797 println!(
798 "Checking SSH availability for {}: {}",
799 vm.name, vm.public_ip_addr
800 );
801 self.ssh_client
802 .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
803 .map_err(|e| {
804 println!("Failed to establish SSH connection to {}: {}", vm.name, e);
805 e
806 })?;
807 }
808
809 println!("SSH is available on all nodes. Proceeding with provisioning...");
810
811 let playbook = match node_type {
812 NodeType::Generic => AnsiblePlaybook::Nodes,
813 NodeType::PeerCache => AnsiblePlaybook::PeerCacheNodes,
814 NodeType::FullConePrivateNode => AnsiblePlaybook::Nodes,
815 NodeType::SymmetricPrivateNode => AnsiblePlaybook::Nodes,
816 _ => return Err(Error::InvalidNodeType(node_type.clone())),
817 };
818 self.ansible_runner.run_playbook(
819 playbook,
820 inventory_type,
821 Some(extra_vars::build_node_extra_vars_doc(
822 &self.cloud_provider.to_string(),
823 options,
824 node_type.clone(),
825 initial_contact_peer,
826 initial_network_contacts_url,
827 node_count,
828 options.evm_network.clone(),
829 relay,
830 )?),
831 )?;
832
833 print_duration(start.elapsed());
834 Ok(())
835 }
836
837 pub fn provision_symmetric_private_nodes(
838 &self,
839 options: &mut ProvisionOptions,
840 initial_contact_peer: Option<String>,
841 initial_network_contacts_url: Option<String>,
842 private_node_inventory: &PrivateNodeProvisionInventory,
843 ) -> Result<()> {
844 let start = Instant::now();
845 self.print_ansible_run_banner("Provision Symmetric Private Node Config");
846
847 generate_symmetric_private_node_static_environment_inventory(
848 &options.name,
849 &options.output_inventory_dir_path,
850 &private_node_inventory.symmetric_private_node_vms,
851 &private_node_inventory.symmetric_nat_gateway_vms,
852 &self.ssh_client.private_key_path,
853 )
854 .inspect_err(|err| {
855 error!("Failed to generate symmetric private node static inv with err: {err:?}")
856 })?;
857
858 self.ssh_client.set_symmetric_nat_routed_vms(
859 &private_node_inventory.symmetric_private_node_vms,
860 &private_node_inventory.symmetric_nat_gateway_vms,
861 )?;
862
863 let inventory_type = AnsibleInventoryType::SymmetricPrivateNodes;
864
865 println!("Obtaining IP addresses for nodes...");
869 let inventory = self.ansible_runner.get_inventory(inventory_type, true)?;
870
871 println!("Waiting for SSH availability on Symmetric Private nodes...");
872 for vm in inventory.iter() {
873 println!(
874 "Checking SSH availability for {}: {}",
875 vm.name, vm.public_ip_addr
876 );
877 self.ssh_client
878 .wait_for_ssh_availability(&vm.public_ip_addr, &self.cloud_provider.get_ssh_user())
879 .map_err(|e| {
880 println!("Failed to establish SSH connection to {}: {}", vm.name, e);
881 e
882 })?;
883 }
884
885 println!("SSH is available on all nodes. Proceeding with provisioning...");
886
887 self.ansible_runner.run_playbook(
888 AnsiblePlaybook::PrivateNodeConfig,
889 inventory_type,
890 Some(
891 extra_vars::build_symmetric_private_node_config_extra_vars_doc(
892 private_node_inventory,
893 )?,
894 ),
895 )?;
896
897 println!("Provisioned Symmetric Private Node Config");
898 print_duration(start.elapsed());
899
900 self.provision_nodes(
901 options,
902 initial_contact_peer,
903 initial_network_contacts_url,
904 NodeType::SymmetricPrivateNode,
905 )?;
906
907 Ok(())
908 }
909
910 pub async fn provision_uploaders(
911 &self,
912 options: &ProvisionOptions,
913 genesis_multiaddr: Option<String>,
914 genesis_network_contacts_url: Option<String>,
915 ) -> Result<()> {
916 let start = Instant::now();
917
918 let sk_map = self
919 .deposit_funds_to_uploaders(&FundingOptions {
920 evm_data_payments_address: options.evm_data_payments_address.clone(),
921 evm_network: options.evm_network.clone(),
922 evm_payment_token_address: options.evm_payment_token_address.clone(),
923 evm_rpc_url: options.evm_rpc_url.clone(),
924 funding_wallet_secret_key: options.funding_wallet_secret_key.clone(),
925 gas_amount: options.gas_amount,
926 token_amount: options.token_amount,
927 uploaders_count: options.uploaders_count,
928 })
929 .await?;
930
931 println!("Running ansible against uploader machine to start the uploader script.");
932 debug!("Running ansible against uploader machine to start the uploader script.");
933
934 self.ansible_runner.run_playbook(
935 AnsiblePlaybook::Uploaders,
936 AnsibleInventoryType::Uploaders,
937 Some(extra_vars::build_uploaders_extra_vars_doc(
938 &self.cloud_provider.to_string(),
939 options,
940 genesis_multiaddr,
941 genesis_network_contacts_url,
942 &sk_map,
943 )?),
944 )?;
945 print_duration(start.elapsed());
946 Ok(())
947 }
948
949 pub fn start_nodes(
950 &self,
951 environment_name: &str,
952 interval: Duration,
953 node_type: Option<NodeType>,
954 custom_inventory: Option<Vec<VirtualMachine>>,
955 ) -> Result<()> {
956 let mut extra_vars = ExtraVarsDocBuilder::default();
957 extra_vars.add_variable("interval", &interval.as_millis().to_string());
958
959 if let Some(node_type) = node_type {
960 println!("Running the start nodes playbook for {node_type:?} nodes");
961 self.ansible_runner.run_playbook(
962 AnsiblePlaybook::StartNodes,
963 node_type.to_ansible_inventory_type(),
964 Some(extra_vars.build()),
965 )?;
966 return Ok(());
967 }
968
969 if let Some(custom_inventory) = custom_inventory {
970 println!("Running the start nodes playbook with a custom inventory");
971 generate_custom_environment_inventory(
972 &custom_inventory,
973 environment_name,
974 &self.ansible_runner.working_directory_path.join("inventory"),
975 )?;
976 self.ansible_runner.run_playbook(
977 AnsiblePlaybook::StartNodes,
978 AnsibleInventoryType::Custom,
979 Some(extra_vars.build()),
980 )?;
981 return Ok(());
982 }
983
984 println!("Running the start nodes playbook for all node types");
985 for node_inv_type in AnsibleInventoryType::iter_node_type() {
986 self.ansible_runner.run_playbook(
987 AnsiblePlaybook::StartNodes,
988 node_inv_type,
989 Some(extra_vars.build()),
990 )?;
991 }
992 Ok(())
993 }
994
995 pub fn status(&self) -> Result<()> {
996 for node_inv_type in AnsibleInventoryType::iter_node_type() {
997 self.ansible_runner
998 .run_playbook(AnsiblePlaybook::Status, node_inv_type, None)?;
999 }
1000 Ok(())
1001 }
1002
1003 pub fn start_telegraf(
1004 &self,
1005 environment_name: &str,
1006 node_type: Option<NodeType>,
1007 custom_inventory: Option<Vec<VirtualMachine>>,
1008 ) -> Result<()> {
1009 if let Some(node_type) = node_type {
1010 println!("Running the start telegraf playbook for {node_type:?} nodes");
1011 self.ansible_runner.run_playbook(
1012 AnsiblePlaybook::StartTelegraf,
1013 node_type.to_ansible_inventory_type(),
1014 None,
1015 )?;
1016 return Ok(());
1017 }
1018
1019 if let Some(custom_inventory) = custom_inventory {
1020 println!("Running the start telegraf playbook with a custom inventory");
1021 generate_custom_environment_inventory(
1022 &custom_inventory,
1023 environment_name,
1024 &self.ansible_runner.working_directory_path.join("inventory"),
1025 )?;
1026 self.ansible_runner.run_playbook(
1027 AnsiblePlaybook::StartTelegraf,
1028 AnsibleInventoryType::Custom,
1029 None,
1030 )?;
1031 return Ok(());
1032 }
1033
1034 println!("Running the start telegraf playbook for all node types");
1035 for node_inv_type in AnsibleInventoryType::iter_node_type() {
1036 self.ansible_runner.run_playbook(
1037 AnsiblePlaybook::StartTelegraf,
1038 node_inv_type,
1039 None,
1040 )?;
1041 }
1042
1043 Ok(())
1044 }
1045
1046 pub fn stop_nodes(
1047 &self,
1048 environment_name: &str,
1049 interval: Duration,
1050 node_type: Option<NodeType>,
1051 custom_inventory: Option<Vec<VirtualMachine>>,
1052 delay: Option<u64>,
1053 service_names: Option<Vec<String>>,
1054 ) -> Result<()> {
1055 let mut extra_vars = ExtraVarsDocBuilder::default();
1056 extra_vars.add_variable("interval", &interval.as_millis().to_string());
1057 if let Some(delay) = delay {
1058 extra_vars.add_variable("delay", &delay.to_string());
1059 }
1060 if let Some(service_names) = service_names {
1061 extra_vars.add_list_variable("service_names", service_names);
1062 }
1063 let extra_vars = extra_vars.build();
1064
1065 if let Some(node_type) = node_type {
1066 println!("Running the stop nodes playbook for {node_type:?} nodes");
1067 self.ansible_runner.run_playbook(
1068 AnsiblePlaybook::StopNodes,
1069 node_type.to_ansible_inventory_type(),
1070 Some(extra_vars),
1071 )?;
1072 return Ok(());
1073 }
1074
1075 if let Some(custom_inventory) = custom_inventory {
1076 println!("Running the stop nodes playbook with a custom inventory");
1077 generate_custom_environment_inventory(
1078 &custom_inventory,
1079 environment_name,
1080 &self.ansible_runner.working_directory_path.join("inventory"),
1081 )?;
1082 self.ansible_runner.run_playbook(
1083 AnsiblePlaybook::StopNodes,
1084 AnsibleInventoryType::Custom,
1085 Some(extra_vars),
1086 )?;
1087 return Ok(());
1088 }
1089
1090 println!("Running the stop nodes playbook for all node types");
1091 for node_inv_type in AnsibleInventoryType::iter_node_type() {
1092 self.ansible_runner.run_playbook(
1093 AnsiblePlaybook::StopNodes,
1094 node_inv_type,
1095 Some(extra_vars.clone()),
1096 )?;
1097 }
1098
1099 Ok(())
1100 }
1101
1102 pub fn stop_telegraf(
1103 &self,
1104 environment_name: &str,
1105 node_type: Option<NodeType>,
1106 custom_inventory: Option<Vec<VirtualMachine>>,
1107 ) -> Result<()> {
1108 if let Some(node_type) = node_type {
1109 println!("Running the stop telegraf playbook for {node_type:?} nodes");
1110 self.ansible_runner.run_playbook(
1111 AnsiblePlaybook::StopTelegraf,
1112 node_type.to_ansible_inventory_type(),
1113 None,
1114 )?;
1115 return Ok(());
1116 }
1117
1118 if let Some(custom_inventory) = custom_inventory {
1119 println!("Running the stop telegraf playbook with a custom inventory");
1120 generate_custom_environment_inventory(
1121 &custom_inventory,
1122 environment_name,
1123 &self.ansible_runner.working_directory_path.join("inventory"),
1124 )?;
1125 self.ansible_runner.run_playbook(
1126 AnsiblePlaybook::StopTelegraf,
1127 AnsibleInventoryType::Custom,
1128 None,
1129 )?;
1130 return Ok(());
1131 }
1132
1133 println!("Running the stop telegraf playbook for all node types");
1134 for node_inv_type in AnsibleInventoryType::iter_node_type() {
1135 self.ansible_runner
1136 .run_playbook(AnsiblePlaybook::StopTelegraf, node_inv_type, None)?;
1137 }
1138
1139 Ok(())
1140 }
1141
1142 pub fn upgrade_node_telegraf(&self, name: &str) -> Result<()> {
1143 self.ansible_runner.run_playbook(
1144 AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1145 AnsibleInventoryType::PeerCacheNodes,
1146 Some(extra_vars::build_node_telegraf_upgrade(
1147 name,
1148 &NodeType::PeerCache,
1149 )?),
1150 )?;
1151 self.ansible_runner.run_playbook(
1152 AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1153 AnsibleInventoryType::Nodes,
1154 Some(extra_vars::build_node_telegraf_upgrade(
1155 name,
1156 &NodeType::Generic,
1157 )?),
1158 )?;
1159
1160 self.ansible_runner.run_playbook(
1161 AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1162 AnsibleInventoryType::SymmetricPrivateNodes,
1163 Some(extra_vars::build_node_telegraf_upgrade(
1164 name,
1165 &NodeType::SymmetricPrivateNode,
1166 )?),
1167 )?;
1168
1169 self.ansible_runner.run_playbook(
1170 AnsiblePlaybook::UpgradeNodeTelegrafConfig,
1171 AnsibleInventoryType::FullConePrivateNodes,
1172 Some(extra_vars::build_node_telegraf_upgrade(
1173 name,
1174 &NodeType::FullConePrivateNode,
1175 )?),
1176 )?;
1177 Ok(())
1178 }
1179
1180 pub fn upgrade_uploader_telegraf(&self, name: &str) -> Result<()> {
1181 self.ansible_runner.run_playbook(
1182 AnsiblePlaybook::UpgradeUploaderTelegrafConfig,
1183 AnsibleInventoryType::Uploaders,
1184 Some(extra_vars::build_uploader_telegraf_upgrade(name)?),
1185 )?;
1186 Ok(())
1187 }
1188
1189 pub fn upgrade_nodes(&self, options: &UpgradeOptions) -> Result<()> {
1190 if let Some(custom_inventory) = &options.custom_inventory {
1191 println!("Running the UpgradeNodes with a custom inventory");
1192 generate_custom_environment_inventory(
1193 custom_inventory,
1194 &options.name,
1195 &self.ansible_runner.working_directory_path.join("inventory"),
1196 )?;
1197 match self.ansible_runner.run_playbook(
1198 AnsiblePlaybook::UpgradeNodes,
1199 AnsibleInventoryType::Custom,
1200 Some(options.get_ansible_vars()),
1201 ) {
1202 Ok(()) => println!("All nodes were successfully upgraded"),
1203 Err(_) => {
1204 println!("WARNING: some nodes may not have been upgraded or restarted");
1205 }
1206 }
1207 return Ok(());
1208 }
1209
1210 if let Some(node_type) = &options.node_type {
1211 println!("Running the UpgradeNodes playbook for {node_type:?} nodes");
1212 match self.ansible_runner.run_playbook(
1213 AnsiblePlaybook::UpgradeNodes,
1214 node_type.to_ansible_inventory_type(),
1215 Some(options.get_ansible_vars()),
1216 ) {
1217 Ok(()) => println!("All {node_type:?} nodes were successfully upgraded"),
1218 Err(_) => {
1219 println!(
1220 "WARNING: some {node_type:?} nodes may not have been upgraded or restarted"
1221 );
1222 }
1223 }
1224 return Ok(());
1225 }
1226
1227 println!("Running the UpgradeNodes playbook for all node types");
1228
1229 match self.ansible_runner.run_playbook(
1230 AnsiblePlaybook::UpgradeNodes,
1231 AnsibleInventoryType::PeerCacheNodes,
1232 Some(options.get_ansible_vars()),
1233 ) {
1234 Ok(()) => println!("All Peer Cache nodes were successfully upgraded"),
1235 Err(_) => {
1236 println!("WARNING: some Peer Cacche nodes may not have been upgraded or restarted");
1237 }
1238 }
1239 match self.ansible_runner.run_playbook(
1240 AnsiblePlaybook::UpgradeNodes,
1241 AnsibleInventoryType::Nodes,
1242 Some(options.get_ansible_vars()),
1243 ) {
1244 Ok(()) => println!("All generic nodes were successfully upgraded"),
1245 Err(_) => {
1246 println!("WARNING: some nodes may not have been upgraded or restarted");
1247 }
1248 }
1249 match self.ansible_runner.run_playbook(
1250 AnsiblePlaybook::UpgradeNodes,
1251 AnsibleInventoryType::SymmetricPrivateNodes,
1252 Some(options.get_ansible_vars()),
1253 ) {
1254 Ok(()) => println!("All private nodes were successfully upgraded"),
1255 Err(_) => {
1256 println!("WARNING: some nodes may not have been upgraded or restarted");
1257 }
1258 }
1259 match self.ansible_runner.run_playbook(
1261 AnsiblePlaybook::UpgradeNodes,
1262 AnsibleInventoryType::Genesis,
1263 Some(options.get_ansible_vars()),
1264 ) {
1265 Ok(()) => println!("The genesis nodes was successfully upgraded"),
1266 Err(_) => {
1267 println!("WARNING: the genesis node may not have been upgraded or restarted");
1268 }
1269 }
1270 Ok(())
1271 }
1272
1273 pub fn upgrade_antctl(
1274 &self,
1275 environment_name: &str,
1276 version: &Version,
1277 node_type: Option<NodeType>,
1278 custom_inventory: Option<Vec<VirtualMachine>>,
1279 ) -> Result<()> {
1280 let mut extra_vars = ExtraVarsDocBuilder::default();
1281 extra_vars.add_variable("version", &version.to_string());
1282
1283 if let Some(node_type) = node_type {
1284 println!("Running the upgrade safenode-manager playbook for {node_type:?} nodes");
1285 self.ansible_runner.run_playbook(
1286 AnsiblePlaybook::UpgradeAntctl,
1287 node_type.to_ansible_inventory_type(),
1288 Some(extra_vars.build()),
1289 )?;
1290 return Ok(());
1291 }
1292
1293 if let Some(custom_inventory) = custom_inventory {
1294 println!("Running the upgrade safenode-manager playbook with a custom inventory");
1295 generate_custom_environment_inventory(
1296 &custom_inventory,
1297 environment_name,
1298 &self.ansible_runner.working_directory_path.join("inventory"),
1299 )?;
1300 self.ansible_runner.run_playbook(
1301 AnsiblePlaybook::UpgradeAntctl,
1302 AnsibleInventoryType::Custom,
1303 Some(extra_vars.build()),
1304 )?;
1305 return Ok(());
1306 }
1307
1308 println!("Running the upgrade safenode-manager playbook for all node types");
1309 for node_inv_type in AnsibleInventoryType::iter_node_type() {
1310 self.ansible_runner.run_playbook(
1311 AnsiblePlaybook::UpgradeAntctl,
1312 node_inv_type,
1313 Some(extra_vars.build()),
1314 )?;
1315 }
1316
1317 Ok(())
1318 }
1319
1320 pub fn print_ansible_run_banner(&self, s: &str) {
1321 let ansible_run_msg = "Ansible Run: ";
1322 let line = "=".repeat(s.len() + ansible_run_msg.len());
1323 println!("{}\n{}{}\n{}", line, ansible_run_msg, s, line);
1324 }
1325}