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