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