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