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