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