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