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