sn_testnet_deploy/
infra.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 log::debug;
8
9use crate::{
10    error::{Error, Result},
11    print_duration,
12    terraform::{TerraformResource, TerraformRunner},
13    EnvironmentDetails, TestnetDeployer,
14};
15use std::time::Instant;
16
17const BUILD_VM: &str = "build";
18const CLIENT: &str = "ant_client";
19const EVM_NODE: &str = "evm_node";
20const FULL_CONE_NAT_GATEWAY: &str = "full_cone_nat_gateway";
21const FULL_CONE_PRIVATE_NODE: &str = "full_cone_private_node";
22const FULL_CONE_PRIVATE_NODE_ATTACHED_VOLUME: &str = "full_cone_private_node_attached_volume";
23const GENESIS_NODE: &str = "genesis_bootstrap";
24const GENESIS_NODE_ATTACHED_VOLUME: &str = "genesis_node_attached_volume";
25const NODE: &str = "node";
26const NODE_ATTACHED_VOLUME: &str = "node_attached_volume";
27const PEER_CACHE_NODE: &str = "peer_cache_node";
28const PEER_CACHE_NODE_ATTACHED_VOLUME: &str = "peer_cache_node_attached_volume";
29const SYMMETRIC_NAT_GATEWAY: &str = "symmetric_nat_gateway";
30const SYMMETRIC_PRIVATE_NODE: &str = "symmetric_private_node";
31const SYMMETRIC_PRIVATE_NODE_ATTACHED_VOLUME: &str = "symmetric_private_node_attached_volume";
32
33const SIZE: &str = "size";
34const IMAGE: &str = "image";
35
36#[derive(Clone, Debug)]
37pub struct InfraRunOptions {
38    /// Set to None for new deployments, as the value will be fetched from tfvars.
39    pub client_image_id: Option<String>,
40    pub client_vm_count: Option<u16>,
41    pub client_vm_size: Option<String>,
42    pub enable_build_vm: bool,
43    pub evm_node_count: Option<u16>,
44    pub evm_node_vm_size: Option<String>,
45    /// Set to None for new deployments, as the value will be fetched from tfvars.
46    pub evm_node_image_id: Option<String>,
47    pub full_cone_vm_size: Option<String>,
48    pub full_cone_private_node_vm_count: Option<u16>,
49    pub full_cone_private_node_volume_size: Option<u16>,
50    pub genesis_vm_count: Option<u16>,
51    pub genesis_node_volume_size: Option<u16>,
52    pub name: String,
53    /// Set to None for new deployments, as the value will be fetched from tfvars.
54    pub nat_gateway_image_id: Option<String>,
55    /// Set to None for new deployments, as the value will be fetched from tfvars.
56    pub node_image_id: Option<String>,
57    pub node_vm_count: Option<u16>,
58    pub node_vm_size: Option<String>,
59    pub node_volume_size: Option<u16>,
60    /// Set to None for new deployments, as the value will be fetched from tfvars.
61    pub peer_cache_image_id: Option<String>,
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 region: String,
66    pub symmetric_nat_gateway_vm_size: Option<String>,
67    pub symmetric_private_node_vm_count: Option<u16>,
68    pub symmetric_private_node_volume_size: Option<u16>,
69    pub tfvars_filenames: Option<Vec<String>>,
70}
71
72impl InfraRunOptions {
73    /// Generate the options for an existing deployment.
74    pub async fn generate_existing(
75        name: &str,
76        region: &str,
77        terraform_runner: &TerraformRunner,
78        environment_details: Option<&EnvironmentDetails>,
79    ) -> Result<Self> {
80        let resources = terraform_runner.show(name)?;
81
82        let resource_count = |resource_name: &str| -> u16 {
83            resources
84                .iter()
85                .filter(|r| r.resource_name == resource_name)
86                .count() as u16
87        };
88
89        let peer_cache_node_vm_count = resource_count(PEER_CACHE_NODE);
90        debug!("Peer cache node count: {peer_cache_node_vm_count}");
91        let (peer_cache_node_volume_size, peer_cache_node_vm_size, peer_cache_image_id) =
92            if peer_cache_node_vm_count > 0 {
93                let volume_size =
94                    get_value_for_resource(&resources, PEER_CACHE_NODE_ATTACHED_VOLUME, SIZE)?;
95                debug!("Peer cache node volume size: {volume_size:?}");
96                let vm_size = get_value_for_resource(&resources, PEER_CACHE_NODE, SIZE)?;
97                debug!("Peer cache node size: {vm_size:?}");
98                let image_id = get_value_for_resource(&resources, PEER_CACHE_NODE, IMAGE)?;
99                debug!("Peer cache node image id: {image_id:?}");
100
101                (volume_size, vm_size, image_id)
102            } else {
103                (None, None, None)
104            };
105
106        let genesis_node_vm_count = resource_count(GENESIS_NODE);
107        debug!("Genesis node count: {genesis_node_vm_count}");
108        let genesis_node_volume_size = if genesis_node_vm_count > 0 {
109            get_value_for_resource(&resources, GENESIS_NODE_ATTACHED_VOLUME, SIZE)?
110        } else {
111            None
112        };
113        debug!("Genesis node volume size: {genesis_node_volume_size:?}");
114
115        let node_vm_count = resource_count(NODE);
116        debug!("Node count: {node_vm_count}");
117        let node_volume_size = if node_vm_count > 0 {
118            get_value_for_resource(&resources, NODE_ATTACHED_VOLUME, SIZE)?
119        } else {
120            None
121        };
122        debug!("Node volume size: {node_volume_size:?}");
123
124        let mut nat_gateway_image_id: Option<String> = None;
125        let symmetric_private_node_vm_count = resource_count(SYMMETRIC_PRIVATE_NODE);
126        debug!("Symmetric private node count: {symmetric_private_node_vm_count}");
127        let (symmetric_private_node_volume_size, symmetric_nat_gateway_vm_size) =
128            if symmetric_private_node_vm_count > 0 {
129                let symmetric_private_node_volume_size = get_value_for_resource(
130                    &resources,
131                    SYMMETRIC_PRIVATE_NODE_ATTACHED_VOLUME,
132                    SIZE,
133                )?;
134                debug!(
135                    "Symmetric private node volume size: {symmetric_private_node_volume_size:?}"
136                );
137                // gateways should exists if private nodes exist
138                let symmetric_nat_gateway_vm_size =
139                    get_value_for_resource(&resources, SYMMETRIC_NAT_GATEWAY, SIZE)?;
140
141                debug!("Symmetric nat gateway size: {symmetric_nat_gateway_vm_size:?}");
142
143                nat_gateway_image_id =
144                    get_value_for_resource(&resources, SYMMETRIC_NAT_GATEWAY, IMAGE)?;
145                debug!("Nat gateway image: {nat_gateway_image_id:?}");
146
147                (
148                    symmetric_private_node_volume_size,
149                    symmetric_nat_gateway_vm_size,
150                )
151            } else {
152                (None, None)
153            };
154
155        let full_cone_private_node_vm_count = resource_count(FULL_CONE_PRIVATE_NODE);
156        debug!("Full cone private node count: {full_cone_private_node_vm_count}");
157        let (full_cone_private_node_volume_size, full_cone_vm_size) =
158            if full_cone_private_node_vm_count > 0 {
159                let full_cone_private_node_volume_size = get_value_for_resource(
160                    &resources,
161                    FULL_CONE_PRIVATE_NODE_ATTACHED_VOLUME,
162                    SIZE,
163                )?;
164                debug!(
165                    "Full cone private node volume size: {full_cone_private_node_volume_size:?}"
166                );
167                // gateways should exists if private nodes exist
168                let full_cone_vm_size =
169                    get_value_for_resource(&resources, FULL_CONE_NAT_GATEWAY, SIZE)?;
170                debug!("Full cone nat gateway size: {full_cone_vm_size:?}");
171
172                nat_gateway_image_id =
173                    get_value_for_resource(&resources, FULL_CONE_NAT_GATEWAY, IMAGE)?;
174                debug!("Nat gateway image: {nat_gateway_image_id:?}");
175
176                (full_cone_private_node_volume_size, full_cone_vm_size)
177            } else {
178                (None, None)
179            };
180
181        let client_vm_count = resource_count(CLIENT);
182        debug!("Client count: {client_vm_count}");
183        let (client_vm_size, client_image_id) = if client_vm_count > 0 {
184            let vm_size = get_value_for_resource(&resources, CLIENT, SIZE)?;
185            debug!("Client size: {vm_size:?}");
186            let image_id = get_value_for_resource(&resources, CLIENT, IMAGE)?;
187            debug!("Client image id: {image_id:?}");
188            (vm_size, image_id)
189        } else {
190            (None, None)
191        };
192
193        let build_vm_count = resource_count(BUILD_VM);
194        debug!("Build VM count: {build_vm_count}");
195        let enable_build_vm = build_vm_count > 0;
196
197        // Node VM size var is re-used for nodes, evm nodes, symmetric and full cone private nodes
198        let (node_vm_size, node_image_id) = if node_vm_count > 0 {
199            let vm_size = get_value_for_resource(&resources, NODE, SIZE)?;
200            debug!("Node size obtained from {NODE}: {vm_size:?}");
201            let image_id = get_value_for_resource(&resources, NODE, IMAGE)?;
202            debug!("Node image id obtained from {NODE}: {image_id:?}");
203            (vm_size, image_id)
204        } else if symmetric_private_node_vm_count > 0 {
205            let vm_size = get_value_for_resource(&resources, SYMMETRIC_PRIVATE_NODE, SIZE)?;
206            debug!("Node size obtained from {SYMMETRIC_PRIVATE_NODE}: {vm_size:?}");
207            let image_id = get_value_for_resource(&resources, SYMMETRIC_PRIVATE_NODE, IMAGE)?;
208            debug!("Node image id obtained from {SYMMETRIC_PRIVATE_NODE}: {image_id:?}");
209            (vm_size, image_id)
210        } else if full_cone_private_node_vm_count > 0 {
211            let vm_size = get_value_for_resource(&resources, FULL_CONE_PRIVATE_NODE, SIZE)?;
212            debug!("Node size obtained from {FULL_CONE_PRIVATE_NODE}: {vm_size:?}");
213            let image_id = get_value_for_resource(&resources, FULL_CONE_PRIVATE_NODE, IMAGE)?;
214            debug!("Node image id obtained from {FULL_CONE_PRIVATE_NODE}: {image_id:?}");
215            (vm_size, image_id)
216        } else {
217            (None, None)
218        };
219
220        let evm_node_count = resource_count(EVM_NODE);
221        debug!("EVM node count: {evm_node_count}");
222        let (evm_node_vm_size, evm_node_image_id) = if evm_node_count > 0 {
223            let emv_node_vm_size = get_value_for_resource(&resources, EVM_NODE, SIZE)?;
224            debug!("EVM node size: {emv_node_vm_size:?}");
225            let evm_node_image_id = get_value_for_resource(&resources, EVM_NODE, IMAGE)?;
226            debug!("EVM node image id: {evm_node_image_id:?}");
227            (emv_node_vm_size, evm_node_image_id)
228        } else {
229            (None, None)
230        };
231
232        let options = Self {
233            client_image_id,
234            client_vm_count: Some(client_vm_count),
235            client_vm_size,
236            enable_build_vm,
237            evm_node_count: Some(evm_node_count),
238            evm_node_vm_size,
239            evm_node_image_id,
240            full_cone_vm_size,
241            full_cone_private_node_vm_count: Some(full_cone_private_node_vm_count),
242            full_cone_private_node_volume_size,
243            genesis_vm_count: Some(genesis_node_vm_count),
244            genesis_node_volume_size,
245            name: name.to_string(),
246            nat_gateway_image_id,
247            node_image_id,
248            node_vm_count: Some(node_vm_count),
249            node_vm_size,
250            node_volume_size,
251            peer_cache_image_id,
252            peer_cache_node_vm_count: Some(peer_cache_node_vm_count),
253            peer_cache_node_vm_size,
254            peer_cache_node_volume_size,
255            region: region.to_string(),
256            symmetric_nat_gateway_vm_size,
257            symmetric_private_node_vm_count: Some(symmetric_private_node_vm_count),
258            symmetric_private_node_volume_size,
259            tfvars_filenames: environment_details
260                .map(|details| details.environment_type.get_tfvars_filenames(name, region)),
261        };
262
263        Ok(options)
264    }
265}
266
267impl TestnetDeployer {
268    /// Create or update the infrastructure for a deployment.
269    pub fn create_or_update_infra(&self, options: &InfraRunOptions) -> Result<()> {
270        let start = Instant::now();
271        println!("Selecting {} workspace...", options.name);
272        self.terraform_runner.workspace_select(&options.name)?;
273
274        let args = build_terraform_args(options)?;
275
276        println!("Running terraform apply...");
277        self.terraform_runner
278            .apply(args, options.tfvars_filenames.clone())?;
279        print_duration(start.elapsed());
280        Ok(())
281    }
282}
283
284#[derive(Clone, Debug)]
285pub struct ClientsInfraRunOptions {
286    pub client_image_id: Option<String>,
287    pub client_vm_count: Option<u16>,
288    pub client_vm_size: Option<String>,
289    /// Set to None for new deployments, as the value will be fetched from tfvars.
290    pub enable_build_vm: bool,
291    pub name: String,
292    pub tfvars_filenames: Vec<String>,
293}
294
295impl ClientsInfraRunOptions {
296    /// Generate the options for an existing Client deployment.
297    pub async fn generate_existing(
298        name: &str,
299        terraform_runner: &TerraformRunner,
300        environment_details: &EnvironmentDetails,
301    ) -> Result<Self> {
302        let resources = terraform_runner.show(name)?;
303
304        let resource_count = |resource_name: &str| -> u16 {
305            resources
306                .iter()
307                .filter(|r| r.resource_name == resource_name)
308                .count() as u16
309        };
310
311        let client_vm_count = resource_count(CLIENT);
312        debug!("Client count: {client_vm_count}");
313        let (client_vm_size, client_image_id) = if client_vm_count > 0 {
314            let vm_size = get_value_for_resource(&resources, CLIENT, SIZE)?;
315            debug!("Client size: {vm_size:?}");
316            let image_id = get_value_for_resource(&resources, CLIENT, IMAGE)?;
317            debug!("Client image id: {image_id:?}");
318            (vm_size, image_id)
319        } else {
320            (None, None)
321        };
322
323        let build_vm_count = resource_count(BUILD_VM);
324        debug!("Build VM count: {build_vm_count}");
325        let enable_build_vm = build_vm_count > 0;
326
327        let options = Self {
328            client_image_id,
329            client_vm_count: Some(client_vm_count),
330            client_vm_size,
331            enable_build_vm,
332            name: name.to_string(),
333            tfvars_filenames: environment_details
334                .environment_type
335                .get_tfvars_filenames(name, &environment_details.region),
336        };
337
338        Ok(options)
339    }
340
341    pub fn build_terraform_args(&self) -> Result<Vec<(String, String)>> {
342        let mut args = Vec::new();
343
344        if let Some(client_vm_count) = self.client_vm_count {
345            args.push((
346                "ant_client_vm_count".to_string(),
347                client_vm_count.to_string(),
348            ));
349        }
350        if let Some(client_vm_size) = &self.client_vm_size {
351            args.push((
352                "ant_client_droplet_size".to_string(),
353                client_vm_size.clone(),
354            ));
355        }
356        if let Some(client_image_id) = &self.client_image_id {
357            args.push((
358                "ant_client_droplet_image_id".to_string(),
359                client_image_id.clone(),
360            ));
361        }
362
363        args.push((
364            "use_custom_bin".to_string(),
365            self.enable_build_vm.to_string(),
366        ));
367
368        Ok(args)
369    }
370}
371
372/// Build the terraform arguments from InfraRunOptions
373pub fn build_terraform_args(options: &InfraRunOptions) -> Result<Vec<(String, String)>> {
374    let mut args = Vec::new();
375
376    args.push(("region".to_string(), options.region.clone()));
377
378    if let Some(client_image_id) = &options.client_image_id {
379        args.push((
380            "ant_client_droplet_image_id".to_string(),
381            client_image_id.clone(),
382        ));
383    }
384
385    if let Some(client_vm_count) = options.client_vm_count {
386        args.push((
387            "ant_client_vm_count".to_string(),
388            client_vm_count.to_string(),
389        ));
390    }
391
392    if let Some(client_vm_size) = &options.client_vm_size {
393        args.push((
394            "ant_client_droplet_size".to_string(),
395            client_vm_size.clone(),
396        ));
397    }
398
399    args.push((
400        "use_custom_bin".to_string(),
401        options.enable_build_vm.to_string(),
402    ));
403
404    if let Some(evm_node_count) = options.evm_node_count {
405        args.push(("evm_node_vm_count".to_string(), evm_node_count.to_string()));
406    }
407
408    if let Some(evm_node_vm_size) = &options.evm_node_vm_size {
409        args.push((
410            "evm_node_droplet_size".to_string(),
411            evm_node_vm_size.clone(),
412        ));
413    }
414
415    if let Some(emv_node_image_id) = &options.evm_node_image_id {
416        args.push((
417            "evm_node_droplet_image_id".to_string(),
418            emv_node_image_id.clone(),
419        ));
420    }
421
422    if let Some(full_cone_vm_size) = &options.full_cone_vm_size {
423        args.push((
424            "full_cone_droplet_size".to_string(),
425            full_cone_vm_size.clone(),
426        ));
427    }
428
429    if let Some(full_cone_private_node_vm_count) = options.full_cone_private_node_vm_count {
430        args.push((
431            "full_cone_private_node_vm_count".to_string(),
432            full_cone_private_node_vm_count.to_string(),
433        ));
434    }
435
436    if let Some(full_cone_private_node_volume_size) = options.full_cone_private_node_volume_size {
437        args.push((
438            "full_cone_private_node_volume_size".to_string(),
439            full_cone_private_node_volume_size.to_string(),
440        ));
441    }
442
443    if let Some(genesis_vm_count) = options.genesis_vm_count {
444        args.push(("genesis_vm_count".to_string(), genesis_vm_count.to_string()));
445    }
446
447    if let Some(genesis_node_volume_size) = options.genesis_node_volume_size {
448        args.push((
449            "genesis_node_volume_size".to_string(),
450            genesis_node_volume_size.to_string(),
451        ));
452    }
453
454    if let Some(nat_gateway_image_id) = &options.nat_gateway_image_id {
455        args.push((
456            "nat_gateway_droplet_image_id".to_string(),
457            nat_gateway_image_id.clone(),
458        ));
459    }
460
461    if let Some(node_image_id) = &options.node_image_id {
462        args.push(("node_droplet_image_id".to_string(), node_image_id.clone()));
463    }
464
465    if let Some(node_vm_count) = options.node_vm_count {
466        args.push(("node_vm_count".to_string(), node_vm_count.to_string()));
467    }
468
469    if let Some(node_vm_size) = &options.node_vm_size {
470        args.push(("node_droplet_size".to_string(), node_vm_size.clone()));
471    }
472
473    if let Some(node_volume_size) = options.node_volume_size {
474        args.push(("node_volume_size".to_string(), node_volume_size.to_string()));
475    }
476
477    if let Some(peer_cache_image_id) = &options.peer_cache_image_id {
478        args.push((
479            "peer_cache_droplet_image_id".to_string(),
480            peer_cache_image_id.clone(),
481        ));
482    }
483
484    if let Some(peer_cache_node_vm_count) = options.peer_cache_node_vm_count {
485        args.push((
486            "peer_cache_node_vm_count".to_string(),
487            peer_cache_node_vm_count.to_string(),
488        ));
489    }
490
491    if let Some(peer_cache_vm_size) = &options.peer_cache_node_vm_size {
492        args.push((
493            "peer_cache_droplet_size".to_string(),
494            peer_cache_vm_size.clone(),
495        ));
496    }
497
498    if let Some(reserved_ips) = crate::reserved_ip::get_reserved_ips_args(&options.name) {
499        args.push(("peer_cache_reserved_ips".to_string(), reserved_ips));
500    }
501
502    if let Some(peer_cache_node_volume_size) = options.peer_cache_node_volume_size {
503        args.push((
504            "peer_cache_node_volume_size".to_string(),
505            peer_cache_node_volume_size.to_string(),
506        ));
507    }
508
509    if let Some(nat_gateway_vm_size) = &options.symmetric_nat_gateway_vm_size {
510        args.push((
511            "symmetric_nat_gateway_droplet_size".to_string(),
512            nat_gateway_vm_size.clone(),
513        ));
514    }
515
516    if let Some(symmetric_private_node_vm_count) = options.symmetric_private_node_vm_count {
517        args.push((
518            "symmetric_private_node_vm_count".to_string(),
519            symmetric_private_node_vm_count.to_string(),
520        ));
521    }
522
523    if let Some(symmetric_private_node_volume_size) = options.symmetric_private_node_volume_size {
524        args.push((
525            "symmetric_private_node_volume_size".to_string(),
526            symmetric_private_node_volume_size.to_string(),
527        ));
528    }
529
530    Ok(args)
531}
532
533/// Select a Terraform workspace for an environment.
534/// Returns an error if the environment doesn't exist.
535pub fn select_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
536    terraform_runner.init()?;
537    let workspaces = terraform_runner.workspace_list()?;
538    if !workspaces.contains(&name.to_string()) {
539        return Err(Error::EnvironmentDoesNotExist(name.to_string()));
540    }
541    terraform_runner.workspace_select(name)?;
542    println!("Selected {name} workspace");
543    Ok(())
544}
545
546pub fn delete_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
547    // The 'dev' workspace is one we always expect to exist, for admin purposes.
548    // You can't delete a workspace while it is selected, so we select 'dev' before we delete
549    // the current workspace.
550    terraform_runner.workspace_select("dev")?;
551    terraform_runner.workspace_delete(name)?;
552    println!("Deleted {name} workspace");
553    Ok(())
554}
555
556/// Extract a specific field value from terraform resources with proper type conversion.
557fn get_value_for_resource<T>(
558    resources: &[TerraformResource],
559    resource_name: &str,
560    field_name: &str,
561) -> Result<Option<T>, Error>
562where
563    T: From<TerraformValue>,
564{
565    let field_value = resources
566        .iter()
567        .filter(|r| r.resource_name == resource_name)
568        .try_fold(None, |acc_value: Option<serde_json::Value>, r| {
569            if let Some(value) = r.values.get(field_name) {
570                match acc_value {
571                    Some(ref existing_value) if existing_value != value => {
572                        log::error!("Expected value: {existing_value}, got value: {value}");
573                        Err(Error::TerraformResourceValueMismatch {
574                            expected: existing_value.to_string(),
575                            actual: value.to_string(),
576                        })
577                    }
578                    _ => Ok(Some(value.clone())),
579                }
580            } else {
581                Ok(acc_value)
582            }
583        })?;
584
585    Ok(field_value.map(TerraformValue::from).map(T::from))
586}
587
588/// Wrapper for terraform values to ensure proper conversion
589#[derive(Debug, Clone)]
590enum TerraformValue {
591    String(String),
592    Number(u64),
593    Bool(bool),
594    Other(serde_json::Value),
595}
596
597impl From<serde_json::Value> for TerraformValue {
598    fn from(value: serde_json::Value) -> Self {
599        if value.is_string() {
600            // Extract the inner string without quotes
601            // Unwrap is safe here because we checked is_string above
602            TerraformValue::String(value.as_str().unwrap().to_string())
603        } else if value.is_u64() {
604            // Unwrap is safe here because we checked is_u64 above
605            TerraformValue::Number(value.as_u64().unwrap())
606        } else if value.is_boolean() {
607            // Unwrap is safe here because we checked is_boolean above
608            TerraformValue::Bool(value.as_bool().unwrap())
609        } else {
610            TerraformValue::Other(value)
611        }
612    }
613}
614
615// Implement From<TerraformValue> for the types you need
616impl From<TerraformValue> for String {
617    fn from(value: TerraformValue) -> Self {
618        match value {
619            TerraformValue::String(s) => s,
620            TerraformValue::Number(n) => n.to_string(),
621            TerraformValue::Bool(b) => b.to_string(),
622            TerraformValue::Other(v) => v.to_string(),
623        }
624    }
625}
626
627impl From<TerraformValue> for u16 {
628    fn from(value: TerraformValue) -> Self {
629        match value {
630            TerraformValue::Number(n) => n as u16,
631            TerraformValue::String(s) => s.parse().unwrap_or(0),
632            _ => 0,
633        }
634    }
635}