sn_testnet_deploy/
deploy.rs

1// Copyright (c) 2023, MaidSafe.
2// All rights reserved.
3//
4// This SAFE Network Software is licensed under the BSD-3-Clause license.
5// Please see the LICENSE file for more details.
6
7use crate::{
8    ansible::provisioning::{PrivateNodeProvisionInventory, ProvisionOptions},
9    error::Result,
10    funding::get_address_from_sk,
11    get_anvil_node_data, get_bootstrap_cache_url, get_genesis_multiaddr, write_environment_details,
12    BinaryOption, DeploymentInventory, DeploymentType, EnvironmentDetails, EnvironmentType,
13    EvmNetwork, InfraRunOptions, LogFormat, NodeType, TestnetDeployer,
14};
15use alloy::{hex::ToHexExt, primitives::U256};
16use colored::Colorize;
17use log::error;
18use serde::{Deserialize, Serialize};
19use std::{net::SocketAddr, path::PathBuf, time::Duration};
20
21#[derive(Clone, Serialize, Deserialize)]
22pub struct DeployOptions {
23    pub binary_option: BinaryOption,
24    pub chunk_size: Option<u64>,
25    pub client_env_variables: Option<Vec<(String, String)>>,
26    pub current_inventory: DeploymentInventory,
27    pub downloaders_count: u16,
28    pub enable_telegraf: bool,
29    pub environment_type: EnvironmentType,
30    pub evm_data_payments_address: Option<String>,
31    pub evm_network: EvmNetwork,
32    pub evm_node_vm_size: Option<String>,
33    pub evm_payment_token_address: Option<String>,
34    pub evm_rpc_url: Option<String>,
35    pub full_cone_nat_gateway_vm_size: Option<String>,
36    pub full_cone_private_node_count: u16,
37    pub full_cone_private_node_vm_count: Option<u16>,
38    pub full_cone_private_node_volume_size: Option<u16>,
39    pub funding_wallet_secret_key: Option<String>,
40    pub genesis_node_volume_size: Option<u16>,
41    pub initial_gas: Option<U256>,
42    pub initial_tokens: Option<U256>,
43    pub interval: Duration,
44    pub log_format: Option<LogFormat>,
45    pub logstash_details: Option<(String, Vec<SocketAddr>)>,
46    pub max_archived_log_files: u16,
47    pub max_log_files: u16,
48    pub name: String,
49    pub network_id: Option<u8>,
50    pub node_count: u16,
51    pub node_env_variables: Option<Vec<(String, String)>>,
52    pub node_vm_count: Option<u16>,
53    pub node_vm_size: Option<String>,
54    pub node_volume_size: Option<u16>,
55    pub output_inventory_dir_path: PathBuf,
56    pub peer_cache_node_count: u16,
57    pub peer_cache_node_vm_count: Option<u16>,
58    pub peer_cache_node_vm_size: Option<String>,
59    pub peer_cache_node_volume_size: Option<u16>,
60    pub symmetric_nat_gateway_vm_size: Option<String>,
61    pub symmetric_private_node_count: u16,
62    pub symmetric_private_node_vm_count: Option<u16>,
63    pub symmetric_private_node_volume_size: Option<u16>,
64    pub public_rpc: bool,
65    pub rewards_address: String,
66    pub uploader_vm_count: Option<u16>,
67    pub uploader_vm_size: Option<String>,
68    pub uploaders_count: u16,
69}
70
71impl TestnetDeployer {
72    pub async fn deploy_to_genesis(
73        &self,
74        options: &DeployOptions,
75    ) -> Result<(ProvisionOptions, (String, String))> {
76        let build_custom_binaries = {
77            match &options.binary_option {
78                BinaryOption::BuildFromSource { .. } => true,
79                BinaryOption::Versioned { .. } => false,
80            }
81        };
82
83        self.create_or_update_infra(&InfraRunOptions {
84            enable_build_vm: build_custom_binaries,
85            evm_node_count: match options.evm_network {
86                EvmNetwork::Anvil => Some(1),
87                EvmNetwork::ArbitrumOne => Some(0),
88                EvmNetwork::ArbitrumSepolia => Some(0),
89                EvmNetwork::Custom => Some(0),
90            },
91            evm_node_vm_size: options.evm_node_vm_size.clone(),
92            full_cone_nat_gateway_vm_size: options.full_cone_nat_gateway_vm_size.clone(),
93            full_cone_private_node_vm_count: options.full_cone_private_node_vm_count,
94            full_cone_private_node_volume_size: options.full_cone_private_node_volume_size,
95            genesis_vm_count: Some(1),
96            genesis_node_volume_size: options.genesis_node_volume_size,
97            name: options.name.clone(),
98            node_vm_count: options.node_vm_count,
99            node_vm_size: options.node_vm_size.clone(),
100            node_volume_size: options.node_volume_size,
101            peer_cache_node_vm_count: options.peer_cache_node_vm_count,
102            peer_cache_node_vm_size: options.peer_cache_node_vm_size.clone(),
103            peer_cache_node_volume_size: options.peer_cache_node_volume_size,
104            symmetric_nat_gateway_vm_size: options.symmetric_nat_gateway_vm_size.clone(),
105            symmetric_private_node_vm_count: options.symmetric_private_node_vm_count,
106            symmetric_private_node_volume_size: options.symmetric_private_node_volume_size,
107            tfvars_filename: options.environment_type.get_tfvars_filename(&options.name),
108            uploader_vm_count: options.uploader_vm_count,
109            uploader_vm_size: options.uploader_vm_size.clone(),
110        })
111        .map_err(|err| {
112            println!("Failed to create infra {err:?}");
113            err
114        })?;
115
116        write_environment_details(
117            &self.s3_repository,
118            &options.name,
119            &EnvironmentDetails {
120                deployment_type: DeploymentType::New,
121                environment_type: options.environment_type.clone(),
122                evm_network: options.evm_network.clone(),
123                evm_data_payments_address: options.evm_data_payments_address.clone(),
124                evm_payment_token_address: options.evm_payment_token_address.clone(),
125                evm_rpc_url: options.evm_rpc_url.clone(),
126                funding_wallet_address: None,
127                network_id: options.network_id,
128                rewards_address: options.rewards_address.clone(),
129            },
130        )
131        .await?;
132
133        let mut provision_options = ProvisionOptions::from(options.clone());
134        let anvil_node_data = if options.evm_network == EvmNetwork::Anvil {
135            self.ansible_provisioner
136                .print_ansible_run_banner("Provision Anvil Node");
137            self.ansible_provisioner
138                .provision_evm_nodes(&provision_options)
139                .map_err(|err| {
140                    println!("Failed to provision evm node {err:?}");
141                    err
142                })?;
143
144            Some(
145                get_anvil_node_data(&self.ansible_provisioner.ansible_runner, &self.ssh_client)
146                    .map_err(|err| {
147                        println!("Failed to get evm testnet data {err:?}");
148                        err
149                    })?,
150            )
151        } else {
152            None
153        };
154
155        let funding_wallet_address = if let Some(secret_key) = &options.funding_wallet_secret_key {
156            let address = get_address_from_sk(secret_key)?;
157            Some(address.encode_hex())
158        } else if let Some(emv_data) = &anvil_node_data {
159            let address = get_address_from_sk(&emv_data.deployer_wallet_private_key)?;
160            Some(address.encode_hex())
161        } else {
162            error!("Funding wallet address not provided");
163            None
164        };
165
166        if let Some(custom_evm) = anvil_node_data {
167            provision_options.evm_data_payments_address =
168                Some(custom_evm.data_payments_address.clone());
169            provision_options.evm_payment_token_address =
170                Some(custom_evm.payment_token_address.clone());
171            provision_options.evm_rpc_url = Some(custom_evm.rpc_url.clone());
172            provision_options.funding_wallet_secret_key =
173                Some(custom_evm.deployer_wallet_private_key.clone());
174        };
175
176        write_environment_details(
177            &self.s3_repository,
178            &options.name,
179            &EnvironmentDetails {
180                deployment_type: DeploymentType::New,
181                environment_type: options.environment_type.clone(),
182                evm_network: options.evm_network.clone(),
183                evm_data_payments_address: provision_options.evm_data_payments_address.clone(),
184                evm_payment_token_address: provision_options.evm_payment_token_address.clone(),
185                evm_rpc_url: provision_options.evm_rpc_url.clone(),
186                funding_wallet_address,
187                network_id: options.network_id,
188                rewards_address: options.rewards_address.clone(),
189            },
190        )
191        .await?;
192
193        if build_custom_binaries {
194            self.ansible_provisioner
195                .print_ansible_run_banner("Build Custom Binaries");
196            self.ansible_provisioner
197                .build_safe_network_binaries(&provision_options)
198                .map_err(|err| {
199                    println!("Failed to build safe network binaries {err:?}");
200                    err
201                })?;
202        }
203
204        self.ansible_provisioner
205            .print_ansible_run_banner("Provision Genesis Node");
206        self.ansible_provisioner
207            .provision_genesis_node(&provision_options)
208            .map_err(|err| {
209                println!("Failed to provision genesis node {err:?}");
210                err
211            })?;
212
213        let (genesis_multiaddr, genesis_ip) =
214            get_genesis_multiaddr(&self.ansible_provisioner.ansible_runner, &self.ssh_client)
215                .map_err(|err| {
216                    println!("Failed to get genesis multiaddr {err:?}");
217                    err
218                })?;
219
220        Ok((
221            provision_options,
222            (genesis_multiaddr, get_bootstrap_cache_url(&genesis_ip)),
223        ))
224    }
225
226    pub async fn deploy(&self, options: &DeployOptions) -> Result<()> {
227        let (mut provision_options, (genesis_multiaddr, genesis_network_contacts)) =
228            self.deploy_to_genesis(options).await?;
229
230        println!("Obtained multiaddr for genesis node: {genesis_multiaddr}, network contact: {genesis_network_contacts}");
231
232        let mut node_provision_failed = false;
233        self.ansible_provisioner
234            .print_ansible_run_banner("Provision Peer Cache Nodes");
235        match self.ansible_provisioner.provision_nodes(
236            &provision_options,
237            Some(genesis_multiaddr.clone()),
238            Some(genesis_network_contacts.clone()),
239            NodeType::PeerCache,
240        ) {
241            Ok(()) => {
242                println!("Provisioned Peer Cache nodes");
243            }
244            Err(err) => {
245                error!("Failed to provision Peer Cache nodes: {err}");
246                node_provision_failed = true;
247            }
248        }
249
250        self.ansible_provisioner
251            .print_ansible_run_banner("Provision Normal Nodes");
252        match self.ansible_provisioner.provision_nodes(
253            &provision_options,
254            Some(genesis_multiaddr.clone()),
255            Some(genesis_network_contacts.clone()),
256            NodeType::Generic,
257        ) {
258            Ok(()) => {
259                println!("Provisioned normal nodes");
260            }
261            Err(err) => {
262                error!("Failed to provision normal nodes: {err}");
263                node_provision_failed = true;
264            }
265        }
266
267        let private_node_inventory = PrivateNodeProvisionInventory::new(
268            &self.ansible_provisioner,
269            options.full_cone_private_node_vm_count,
270            options.symmetric_private_node_vm_count,
271        )?;
272
273        if private_node_inventory.should_provision_full_cone_private_nodes() {
274            match self.ansible_provisioner.provision_full_cone(
275                &provision_options,
276                Some(genesis_multiaddr.clone()),
277                Some(genesis_network_contacts.clone()),
278                private_node_inventory.clone(),
279                None,
280            ) {
281                Ok(()) => {
282                    println!("Provisioned Full Cone nodes and Gateway");
283                }
284                Err(err) => {
285                    error!("Failed to provision Full Cone nodes and Gateway: {err}");
286                    node_provision_failed = true;
287                }
288            }
289        }
290
291        if private_node_inventory.should_provision_symmetric_private_nodes() {
292            self.ansible_provisioner
293                .print_ansible_run_banner("Provision Symmetric NAT Gateway");
294            self.ansible_provisioner
295                .provision_symmetric_nat_gateway(&provision_options, &private_node_inventory)
296                .map_err(|err| {
297                    println!("Failed to provision Symmetric NAT gateway {err:?}");
298                    err
299                })?;
300
301            self.ansible_provisioner
302                .print_ansible_run_banner("Provision Symmetric Private Nodes");
303            match self.ansible_provisioner.provision_symmetric_private_nodes(
304                &mut provision_options,
305                Some(genesis_multiaddr.clone()),
306                Some(genesis_network_contacts.clone()),
307                &private_node_inventory,
308            ) {
309                Ok(()) => {
310                    println!("Provisioned Symmetric private nodes");
311                }
312                Err(err) => {
313                    error!("Failed to provision Symmetric Private nodes: {err}");
314                    node_provision_failed = true;
315                }
316            }
317        }
318
319        if options.current_inventory.is_empty() {
320            self.ansible_provisioner
321                .print_ansible_run_banner("Provision Uploaders");
322            self.ansible_provisioner
323                .provision_uploaders(
324                    &provision_options,
325                    Some(genesis_multiaddr.clone()),
326                    Some(genesis_network_contacts.clone()),
327                )
328                .await
329                .map_err(|err| {
330                    println!("Failed to provision uploaders {err:?}");
331                    err
332                })?;
333        }
334
335        if node_provision_failed {
336            println!();
337            println!("{}", "WARNING!".yellow());
338            println!("Some nodes failed to provision without error.");
339            println!("This usually means a small number of nodes failed to start on a few VMs.");
340            println!("However, most of the time the deployment will still be usable.");
341            println!("See the output from Ansible to determine which VMs had failures.");
342        }
343
344        Ok(())
345    }
346}