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::{Error, Result},
10    funding::get_address_from_sk,
11    get_anvil_node_data_hardcoded, get_bootstrap_cache_url, get_genesis_multiaddr,
12    write_environment_details, BinaryOption, DeploymentInventory, DeploymentType,
13    EnvironmentDetails, EnvironmentType, EvmDetails, EvmNetwork, InfraRunOptions, LogFormat,
14    NodeType, TestnetDeployer,
15};
16use alloy::{hex::ToHexExt, primitives::U256};
17use colored::Colorize;
18use log::error;
19use serde::{Deserialize, Serialize};
20use std::{path::PathBuf, time::Duration};
21
22#[derive(Clone, Serialize, Deserialize)]
23pub struct DeployOptions {
24    pub binary_option: BinaryOption,
25    pub chunk_size: Option<u64>,
26    pub chunk_tracker_data_addresses: Vec<String>,
27    pub chunk_tracker_services: u16,
28    pub client_env_variables: Option<Vec<(String, String)>>,
29    pub client_vm_count: Option<u16>,
30    pub client_vm_size: Option<String>,
31    pub current_inventory: DeploymentInventory,
32    pub enable_logging: bool,
33    pub enable_metrics: bool,
34    pub environment_type: EnvironmentType,
35    pub evm_data_payments_address: Option<String>,
36    pub evm_network: EvmNetwork,
37    pub evm_node_vm_size: Option<String>,
38    pub evm_payment_token_address: Option<String>,
39    pub evm_rpc_url: Option<String>,
40    pub full_cone_vm_size: Option<String>,
41    pub full_cone_private_node_count: u16,
42    pub full_cone_private_node_vm_count: Option<u16>,
43    pub full_cone_private_node_volume_size: Option<u16>,
44    pub funding_wallet_secret_key: Option<String>,
45    pub genesis_node_volume_size: Option<u16>,
46    pub initial_gas: Option<U256>,
47    pub initial_tokens: Option<U256>,
48    pub interval: Duration,
49    pub log_format: Option<LogFormat>,
50    pub max_archived_log_files: u16,
51    pub max_log_files: u16,
52    pub max_uploads: Option<u32>,
53    pub name: String,
54    pub network_id: u8,
55    pub network_dashboard_branch: Option<String>,
56    pub node_count: u16,
57    pub node_env_variables: Option<Vec<(String, String)>>,
58    pub node_vm_count: Option<u16>,
59    pub node_vm_size: Option<String>,
60    pub node_volume_size: Option<u16>,
61    pub output_inventory_dir_path: PathBuf,
62    pub peer_cache_node_count: u16,
63    pub peer_cache_node_vm_count: Option<u16>,
64    pub peer_cache_node_vm_size: Option<String>,
65    pub peer_cache_node_volume_size: Option<u16>,
66    pub port_restricted_cone_vm_size: Option<String>,
67    pub port_restricted_cone_private_node_count: u16,
68    pub port_restricted_cone_private_node_vm_count: u16,
69    pub port_restricted_cone_private_node_volume_size: Option<u16>,
70    pub single_node_payment: bool,
71    pub start_chunk_trackers: bool,
72    pub start_delayed_verifier: bool,
73    pub start_performance_verifier: bool,
74    pub start_random_verifier: bool,
75    pub symmetric_nat_gateway_vm_size: Option<String>,
76    pub symmetric_private_node_count: u16,
77    pub symmetric_private_node_vm_count: Option<u16>,
78    pub symmetric_private_node_volume_size: Option<u16>,
79    pub public_rpc: bool,
80    pub region: String,
81    pub rewards_address: String,
82    pub uploaders_count: u16,
83    pub upload_interval: u16,
84    pub upload_size: u16,
85    pub upnp_vm_size: Option<String>,
86    pub upnp_private_node_count: u16,
87    pub upnp_private_node_vm_count: Option<u16>,
88    pub upnp_private_node_volume_size: Option<u16>,
89}
90
91impl TestnetDeployer {
92    pub async fn deploy_to_genesis(
93        &self,
94        options: &DeployOptions,
95    ) -> Result<(ProvisionOptions, (String, String))> {
96        let build_custom_binaries = options.binary_option.should_provision_build_machine();
97
98        self.create_or_update_infra(&InfraRunOptions {
99            client_image_id: None,
100            client_vm_count: options.client_vm_count,
101            client_vm_size: options.client_vm_size.clone(),
102            enable_build_vm: build_custom_binaries,
103            evm_node_count: match options.evm_network {
104                EvmNetwork::Anvil => Some(1),
105                EvmNetwork::ArbitrumOne => Some(0),
106                EvmNetwork::ArbitrumSepoliaTest => Some(0),
107                EvmNetwork::Custom => Some(0),
108            },
109            evm_node_vm_size: options.evm_node_vm_size.clone(),
110            evm_node_image_id: None,
111            full_cone_vm_size: options.full_cone_vm_size.clone(),
112            full_cone_private_node_vm_count: options.full_cone_private_node_vm_count,
113            full_cone_private_node_volume_size: options.full_cone_private_node_volume_size,
114            genesis_vm_count: Some(1),
115            genesis_node_volume_size: options.genesis_node_volume_size,
116            name: options.name.clone(),
117            nat_gateway_image_id: None,
118            node_image_id: None,
119            node_vm_count: options.node_vm_count,
120            node_vm_size: options.node_vm_size.clone(),
121            node_volume_size: options.node_volume_size,
122            peer_cache_image_id: None,
123            peer_cache_node_vm_count: options.peer_cache_node_vm_count,
124            peer_cache_node_vm_size: options.peer_cache_node_vm_size.clone(),
125            peer_cache_node_volume_size: options.peer_cache_node_volume_size,
126            port_restricted_cone_vm_size: options.port_restricted_cone_vm_size.clone(),
127            port_restricted_private_node_vm_count: Some(
128                options.port_restricted_cone_private_node_vm_count,
129            ),
130            port_restricted_private_node_volume_size: options
131                .port_restricted_cone_private_node_volume_size,
132            region: options.region.clone(),
133            symmetric_nat_gateway_vm_size: options.symmetric_nat_gateway_vm_size.clone(),
134            symmetric_private_node_vm_count: options.symmetric_private_node_vm_count,
135            symmetric_private_node_volume_size: options.symmetric_private_node_volume_size,
136            tfvars_filenames: Some(
137                options
138                    .environment_type
139                    .get_tfvars_filenames(&options.name, &options.region),
140            ),
141            upnp_vm_size: options.upnp_vm_size.clone(),
142            upnp_private_node_vm_count: options.upnp_private_node_vm_count,
143            upnp_private_node_volume_size: options.upnp_private_node_volume_size,
144        })
145        .map_err(|err| {
146            println!("Failed to create infra {err:?}");
147            err
148        })?;
149
150        write_environment_details(
151            &self.s3_repository,
152            &options.name,
153            &EnvironmentDetails {
154                deployment_type: DeploymentType::New,
155                environment_type: options.environment_type.clone(),
156                evm_details: EvmDetails {
157                    network: options.evm_network.clone(),
158                    data_payments_address: options.evm_data_payments_address.clone(),
159                    payment_token_address: options.evm_payment_token_address.clone(),
160                    rpc_url: options.evm_rpc_url.clone(),
161                },
162                funding_wallet_address: None,
163                network_id: Some(options.network_id),
164                region: options.region.clone(),
165                rewards_address: Some(options.rewards_address.clone()),
166            },
167        )
168        .await?;
169
170        let mut provision_options = ProvisionOptions::from(options.clone());
171        let anvil_node_data = if options.evm_network == EvmNetwork::Anvil {
172            self.ansible_provisioner
173                .print_ansible_run_banner("Provision Anvil Node");
174            self.ansible_provisioner
175                .provision_evm_nodes(&provision_options)
176                .map_err(|err| {
177                    println!("Failed to provision evm node {err:?}");
178                    err
179                })?;
180
181            Some(
182                get_anvil_node_data_hardcoded(&self.ansible_provisioner.ansible_runner).map_err(
183                    |err| {
184                        println!("Failed to get evm testnet data {err:?}");
185                        err
186                    },
187                )?,
188            )
189        } else {
190            None
191        };
192
193        let funding_wallet_address = if let Some(secret_key) = &options.funding_wallet_secret_key {
194            let address = get_address_from_sk(secret_key)?;
195            Some(address.encode_hex())
196        } else if let Some(emv_data) = &anvil_node_data {
197            let address = get_address_from_sk(&emv_data.deployer_wallet_private_key)?;
198            Some(address.encode_hex())
199        } else {
200            error!("Funding wallet address not provided");
201            None
202        };
203
204        if let Some(custom_evm) = anvil_node_data {
205            provision_options.evm_data_payments_address =
206                Some(custom_evm.data_payments_address.clone());
207            provision_options.evm_payment_token_address =
208                Some(custom_evm.payment_token_address.clone());
209            provision_options.evm_rpc_url = Some(custom_evm.rpc_url.clone());
210            provision_options.funding_wallet_secret_key =
211                Some(custom_evm.deployer_wallet_private_key.clone());
212        };
213
214        write_environment_details(
215            &self.s3_repository,
216            &options.name,
217            &EnvironmentDetails {
218                deployment_type: DeploymentType::New,
219                environment_type: options.environment_type.clone(),
220                evm_details: EvmDetails {
221                    network: options.evm_network.clone(),
222                    data_payments_address: provision_options.evm_data_payments_address.clone(),
223                    payment_token_address: provision_options.evm_payment_token_address.clone(),
224                    rpc_url: provision_options.evm_rpc_url.clone(),
225                },
226                funding_wallet_address,
227                network_id: Some(options.network_id),
228                region: options.region.clone(),
229                rewards_address: Some(options.rewards_address.clone()),
230            },
231        )
232        .await?;
233
234        if build_custom_binaries {
235            self.ansible_provisioner
236                .print_ansible_run_banner("Build Custom Binaries");
237            self.ansible_provisioner
238                .build_autonomi_binaries(&provision_options, None)
239                .map_err(|err| {
240                    println!("Failed to build safe network binaries {err:?}");
241                    err
242                })?;
243        }
244
245        self.ansible_provisioner
246            .print_ansible_run_banner("Provision Genesis Node");
247        self.ansible_provisioner
248            .provision_genesis_node(&provision_options)
249            .map_err(|err| {
250                println!("Failed to provision genesis node {err:?}");
251                err
252            })?;
253
254        let (genesis_multiaddr, genesis_ip) =
255            get_genesis_multiaddr(&self.ansible_provisioner.ansible_runner, &self.ssh_client)?
256                .ok_or_else(|| Error::GenesisListenAddress)?;
257        Ok((
258            provision_options,
259            (genesis_multiaddr, get_bootstrap_cache_url(&genesis_ip)),
260        ))
261    }
262
263    pub async fn deploy(&self, options: &DeployOptions) -> Result<()> {
264        let (mut provision_options, (genesis_multiaddr, genesis_network_contacts)) =
265            self.deploy_to_genesis(options).await?;
266
267        println!("Obtained multiaddr for genesis node: {genesis_multiaddr}, network contact: {genesis_network_contacts}");
268
269        let mut node_provision_failed = false;
270        self.ansible_provisioner
271            .print_ansible_run_banner("Provision Peer Cache Nodes");
272        match self.ansible_provisioner.provision_nodes(
273            &provision_options,
274            Some(genesis_multiaddr.clone()),
275            Some(genesis_network_contacts.clone()),
276            NodeType::PeerCache,
277        ) {
278            Ok(()) => {
279                println!("Provisioned Peer Cache nodes");
280            }
281            Err(err) => {
282                error!("Failed to provision Peer Cache nodes: {err}");
283                node_provision_failed = true;
284            }
285        }
286
287        self.ansible_provisioner
288            .print_ansible_run_banner("Provision Public Nodes");
289        match self.ansible_provisioner.provision_nodes(
290            &provision_options,
291            Some(genesis_multiaddr.clone()),
292            Some(genesis_network_contacts.clone()),
293            NodeType::Generic,
294        ) {
295            Ok(()) => {
296                println!("Provisioned public nodes");
297            }
298            Err(err) => {
299                error!("Failed to provision public nodes: {err}");
300                node_provision_failed = true;
301            }
302        }
303
304        self.ansible_provisioner
305            .print_ansible_run_banner("Provision UPnP Nodes");
306        match self.ansible_provisioner.provision_nodes(
307            &provision_options,
308            Some(genesis_multiaddr.clone()),
309            Some(genesis_network_contacts.clone()),
310            NodeType::Upnp,
311        ) {
312            Ok(()) => {
313                println!("Provisioned UPnP nodes");
314            }
315            Err(err) => {
316                error!("Failed to provision UPnP nodes: {err}");
317                node_provision_failed = true;
318            }
319        }
320
321        let private_node_inventory = PrivateNodeProvisionInventory::new(
322            &self.ansible_provisioner,
323            options.full_cone_private_node_vm_count,
324            options.symmetric_private_node_vm_count,
325            Some(options.port_restricted_cone_private_node_vm_count),
326        )?;
327
328        if private_node_inventory.should_provision_full_cone_private_nodes() {
329            match self.ansible_provisioner.provision_full_cone(
330                &provision_options,
331                Some(genesis_multiaddr.clone()),
332                Some(genesis_network_contacts.clone()),
333                private_node_inventory.clone(),
334                None,
335            ) {
336                Ok(()) => {
337                    println!("Provisioned Full Cone nodes and Gateway");
338                }
339                Err(err) => {
340                    error!("Failed to provision Full Cone nodes and Gateway: {err}");
341                    node_provision_failed = true;
342                }
343            }
344        }
345
346        if private_node_inventory.should_provision_port_restricted_cone_private_nodes() {
347            match self.ansible_provisioner.provision_port_restricted_cone(
348                &provision_options,
349                Some(genesis_multiaddr.clone()),
350                Some(genesis_network_contacts.clone()),
351                private_node_inventory.clone(),
352                None,
353            ) {
354                Ok(()) => {
355                    println!("Provisioned Port Restricted Cone nodes and Gateway");
356                }
357                Err(err) => {
358                    error!("Failed to provision Port Restricted Cone nodes and Gateway: {err}");
359                    node_provision_failed = true;
360                }
361            }
362        }
363
364        if private_node_inventory.should_provision_symmetric_private_nodes() {
365            self.ansible_provisioner
366                .print_ansible_run_banner("Provision Symmetric NAT Gateway");
367            self.ansible_provisioner
368                .provision_symmetric_nat_gateway(&provision_options, &private_node_inventory)
369                .map_err(|err| {
370                    println!("Failed to provision Symmetric NAT gateway {err:?}");
371                    err
372                })?;
373
374            self.ansible_provisioner
375                .print_ansible_run_banner("Provision Symmetric Private Nodes");
376            match self.ansible_provisioner.provision_symmetric_private_nodes(
377                &mut provision_options,
378                Some(genesis_multiaddr.clone()),
379                Some(genesis_network_contacts.clone()),
380                &private_node_inventory,
381            ) {
382                Ok(()) => {
383                    println!("Provisioned Symmetric private nodes");
384                }
385                Err(err) => {
386                    error!("Failed to provision Symmetric Private nodes: {err}");
387                    node_provision_failed = true;
388                }
389            }
390        }
391
392        self.ansible_provisioner
393            .print_ansible_run_banner("Provision Uploaders");
394        self.ansible_provisioner
395            .provision_uploaders(
396                &provision_options,
397                Some(genesis_multiaddr.clone()),
398                Some(genesis_network_contacts.clone()),
399            )
400            .await
401            .map_err(|err| {
402                println!("Failed to provision Clients {err:?}");
403                err
404            })?;
405        self.ansible_provisioner
406            .print_ansible_run_banner("Provision Downloaders");
407        self.ansible_provisioner
408            .provision_downloaders(
409                &provision_options,
410                Some(genesis_multiaddr.clone()),
411                Some(genesis_network_contacts.clone()),
412            )
413            .await
414            .map_err(|err| {
415                println!("Failed to provision downloaders {err:?}");
416                err
417            })?;
418        self.ansible_provisioner
419            .print_ansible_run_banner("Provision Chunk Trackers");
420        self.ansible_provisioner
421            .provision_chunk_trackers(
422                &provision_options,
423                Some(genesis_multiaddr.clone()),
424                Some(genesis_network_contacts.clone()),
425            )
426            .await
427            .map_err(|err| {
428                println!("Failed to provision chunk trackers {err:?}");
429                err
430            })?;
431
432        if node_provision_failed {
433            println!();
434            println!("{}", "WARNING!".yellow());
435            println!("Some nodes failed to provision without error.");
436            println!("This usually means a small number of nodes failed to start on a few VMs.");
437            println!("However, most of the time the deployment will still be usable.");
438            println!("See the output from Ansible to determine which VMs had failures.");
439        }
440
441        Ok(())
442    }
443}