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