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