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