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