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