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