Skip to main content

ota_lib/
config.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{bail, Context, Error};
6use fidl_fuchsia_boot::{ArgumentsMarker, ArgumentsProxy};
7use fidl_fuchsia_buildinfo::{ProviderMarker as BuildInfoMarker, ProviderProxy as BuildInfoProxy};
8use serde::{Deserialize, Serialize};
9use std::fs::File;
10use std::io::BufReader;
11
12const PATH_TO_RECOVERY_CONFIG: &'static str = "/config/data/recovery-config.json";
13const DEFAULT_OMAHA_SERVICE_URL: &'static str =
14    "https://clients2.google.com/service/update2/fuchsia/json";
15
16#[derive(Debug, PartialEq)]
17pub struct RecoveryUpdateConfig {
18    pub channel: String,
19    pub update_type: UpdateType,
20    pub version: String,
21}
22
23#[derive(Debug, PartialEq)]
24pub enum UpdateType {
25    /// Designates an Omaha based update
26    /// Parameters:
27    ///     app_id: The omaha application id
28    ///     omaha_service_url: the omaha service url query for updates
29    Omaha { app_id: String, service_url: String },
30    /// Designates a TUF based update
31    Tuf,
32}
33
34impl RecoveryUpdateConfig {
35    /// Resolve the update configuration using values from vbmeta (via boot args), the
36    /// build-provided json config, and the current version from BuildInfo.
37    pub async fn resolve_update_config() -> Result<RecoveryUpdateConfig, Error> {
38        let json_config =
39            JsonUpdateConfig::load_json_config_data().context("Failed to load json config data")?;
40
41        let boot_args = {
42            let arguments_proxy =
43                fuchsia_component::client::connect_to_protocol::<ArgumentsMarker>()
44                    .context("Could not load boot arguments.")?;
45            BootloaderArgs::load_from_proxy(arguments_proxy)
46                .await
47                .context("Unable to connect to fuchsia.boot.Arguments.")
48        }
49        .map_err(|e| {
50            // Note: This map_err is a good candidate for .inspect_err(f) when it becomes stable
51            eprintln!(
52                "Error: Error collecting boot argument: '{:?}'. Continuing without boot args.",
53                e
54            );
55        })
56        .unwrap_or_default();
57
58        let build_info_proxy = fuchsia_component::client::connect_to_protocol::<BuildInfoMarker>()?;
59        let resolved_version = Self::resolve_version_from_proxy(&json_config, build_info_proxy)
60            .await
61            .context("Failed to load version")?;
62
63        Self::resolve_update_config_from_structs(boot_args, json_config, resolved_version)
64    }
65
66    async fn resolve_version_from_proxy(
67        json_config: &JsonUpdateConfig,
68        build_info_proxy: BuildInfoProxy,
69    ) -> Result<String, Error> {
70        let version = match &json_config.override_version {
71            Some(version) => version.clone(),
72            None => {
73                let build_info =
74                    build_info_proxy.get_build_info().await.context("Failed to read build info")?;
75                build_info.version.context("No version string provided by build info component")?
76            }
77        };
78        Ok(version)
79    }
80
81    /// Resolves update config from provided boot args and json config.
82    ///
83    /// Note: Version is resolved separately and passed in.
84    fn resolve_update_config_from_structs(
85        boot_args: BootloaderArgs,
86        json_config: JsonUpdateConfig,
87        resolved_version: String,
88    ) -> Result<RecoveryUpdateConfig, Error> {
89        // resolve channel (vbmeta > config)
90        let channel = boot_args.ota_channel.unwrap_or(json_config.default_channel);
91
92        // UpdateType is set by the json config
93        let update_type: UpdateType = match json_config.update_type {
94            JsonUpdateType::Omaha(json_app_id, json_service_url) => {
95                UpdateType::Omaha {
96                    // Resolve app id (vbmeta > config), config must provide a fallback value
97                    app_id: boot_args.omaha_app_id.unwrap_or(json_app_id),
98                    // Resolve service url (vbmeta > config > hard-coded)
99                    service_url: boot_args
100                        .omaha_url
101                        .or(json_service_url)
102                        .unwrap_or_else(|| String::from(DEFAULT_OMAHA_SERVICE_URL)),
103                }
104            }
105            JsonUpdateType::Tuf => UpdateType::Tuf,
106        };
107
108        Ok(RecoveryUpdateConfig {
109            channel: channel,
110            update_type: update_type,
111            version: resolved_version,
112        })
113    }
114}
115
116/// Temporary struct to deserialize configs provided with config-data
117/// TODO(b/259495731) Switch from deprecated config-data
118// pub for integration tests
119#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
120pub struct JsonUpdateConfig {
121    pub default_channel: String,
122    pub update_type: JsonUpdateType,
123    pub override_version: Option<String>,
124}
125
126//pub for integration tests
127#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
128#[serde(rename_all = "snake_case")]
129pub enum JsonUpdateType {
130    /// Designates an Omaha based update
131    /// Parameters:
132    ///     app_id: The omaha application id
133    ///     omaha_service_url: Override the default omaha service to query
134    Omaha(String, Option<String>),
135    /// Designates a TUF based update
136    Tuf,
137}
138
139impl JsonUpdateConfig {
140    fn load_json_config_data() -> Result<JsonUpdateConfig, Error> {
141        let ota_config: JsonUpdateConfig = serde_json::from_reader(BufReader::new(
142            File::open(PATH_TO_RECOVERY_CONFIG).context("Failed to find update config data")?,
143        ))?;
144        Ok(ota_config)
145    }
146}
147
148/// A holder for values read by the `fuchsia.boot.arguments` protocol.
149/// These boot args are expected to be read by the bootloader from the current zircon slot's vbmeta
150/// file (usually zircon_r) and passed to boot zircon.
151#[derive(Debug, Default, PartialEq)]
152struct BootloaderArgs {
153    omaha_app_id: Option<String>,
154    omaha_url: Option<String>,
155    ota_channel: Option<String>,
156}
157
158impl BootloaderArgs {
159    async fn load_from_proxy(arguments_proxy: ArgumentsProxy) -> Result<BootloaderArgs, Error> {
160        let keys = &["omaha_app_id".to_owned(), "omaha_url".to_owned(), "ota_channel".to_owned()];
161        let num_keys = keys.len();
162        let boot_args: Vec<Option<String>> =
163            arguments_proxy.get_strings(keys).await.context("No boot args available.")?;
164        if boot_args.len() != num_keys {
165            bail!("Boot args returned {} values, expected {}", boot_args.len(), num_keys);
166        }
167        Ok(BootloaderArgs {
168            omaha_app_id: boot_args[0].clone(),
169            omaha_url: boot_args[1].clone(),
170            ota_channel: boot_args[2].clone(),
171        })
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use assert_matches::assert_matches;
179    use fidl::endpoints::create_proxy_and_stream;
180    use fidl_fuchsia_buildinfo::{BuildInfo, ProviderRequest as BuildInfoRequest};
181    use fuchsia_async as fasync;
182    use futures::prelude::*;
183    use maplit::hashmap;
184    use mock_boot_arguments::MockBootArgumentsService;
185    use pretty_assertions::assert_eq;
186    use std::collections::HashMap;
187    use std::sync::Arc;
188
189    fn empty_bootloader_args() -> BootloaderArgs {
190        BootloaderArgs { omaha_app_id: None, omaha_url: None, ota_channel: None }
191    }
192
193    // resolve config from structs tests
194    #[test]
195    fn resolve_config_expected_in_field_use_case() {
196        let boot_args = BootloaderArgs {
197            omaha_app_id: Some("avb_app_id".to_string()),
198            omaha_url: None,
199            ota_channel: Some("avb_channel".to_string()),
200        };
201        let json_config = JsonUpdateConfig {
202            default_channel: "json_channel".to_string(),
203            update_type: JsonUpdateType::Omaha("json_app_id".to_string(), None),
204            override_version: None,
205        };
206
207        let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
208            boot_args,
209            json_config,
210            "resolved_version".to_string(),
211        );
212        assert_eq!(
213            resolved_config.unwrap(),
214            RecoveryUpdateConfig {
215                channel: "avb_channel".to_string(),
216                update_type: UpdateType::Omaha {
217                    app_id: "avb_app_id".to_string(),
218                    service_url: DEFAULT_OMAHA_SERVICE_URL.to_string(),
219                },
220                version: "resolved_version".to_string(),
221            }
222        );
223    }
224
225    #[test]
226    fn resolve_tuf_config_without_boot_args() {
227        let boot_args = empty_bootloader_args();
228        let json_config = JsonUpdateConfig {
229            default_channel: "json_channel".to_string(),
230            update_type: JsonUpdateType::Tuf,
231            override_version: None,
232        };
233
234        let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
235            boot_args,
236            json_config,
237            "resolved_version".to_string(),
238        );
239        assert_eq!(
240            resolved_config.unwrap(),
241            RecoveryUpdateConfig {
242                channel: "json_channel".to_string(),
243                update_type: UpdateType::Tuf,
244                version: "resolved_version".to_string(),
245            }
246        );
247    }
248
249    #[test]
250    fn resolve_tuf_config_with_boot_override() {
251        let boot_args = BootloaderArgs {
252            omaha_app_id: None,
253            omaha_url: None,
254            ota_channel: Some("avb_channel".to_string()),
255        };
256        let json_config = JsonUpdateConfig {
257            default_channel: "json_channel".to_string(),
258            update_type: JsonUpdateType::Tuf,
259            override_version: Some("json_version".to_string()),
260        };
261
262        let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
263            boot_args,
264            json_config,
265            "resolved_version".to_string(),
266        );
267        assert_eq!(
268            resolved_config.unwrap(),
269            RecoveryUpdateConfig {
270                channel: "avb_channel".to_string(),
271                update_type: UpdateType::Tuf,
272                version: "resolved_version".to_string(),
273            }
274        );
275    }
276
277    #[test]
278    fn resolve_omaha_config_without_boot_args() {
279        let boot_args = empty_bootloader_args();
280        let json_config = JsonUpdateConfig {
281            default_channel: "json_channel".to_string(),
282            update_type: JsonUpdateType::Omaha("json_app_id".to_string(), None),
283            override_version: None,
284        };
285
286        let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
287            boot_args,
288            json_config,
289            "resolved_version".to_string(),
290        );
291        assert_eq!(
292            resolved_config.unwrap(),
293            RecoveryUpdateConfig {
294                channel: "json_channel".to_string(),
295                update_type: UpdateType::Omaha {
296                    app_id: "json_app_id".to_string(),
297                    service_url: DEFAULT_OMAHA_SERVICE_URL.to_string(),
298                },
299                version: "resolved_version".to_string(),
300            }
301        );
302    }
303
304    #[test]
305    fn resolve_omaha_json_version_and_url() {
306        let boot_args = empty_bootloader_args();
307        let json_config = JsonUpdateConfig {
308            default_channel: "json_channel".to_string(),
309            update_type: JsonUpdateType::Omaha(
310                "json_app_id".to_string(),
311                Some("json_url".to_string()),
312            ),
313            override_version: Some("json_version".to_string()),
314        };
315
316        let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
317            boot_args,
318            json_config,
319            "resolved_version".to_string(),
320        );
321        assert_eq!(
322            resolved_config.unwrap(),
323            RecoveryUpdateConfig {
324                channel: "json_channel".to_string(),
325                update_type: UpdateType::Omaha {
326                    app_id: "json_app_id".to_string(),
327                    service_url: "json_url".to_string(),
328                },
329                version: "resolved_version".to_string(),
330            }
331        );
332    }
333
334    #[test]
335    fn resolve_omaha_all_boot_args() {
336        let boot_args = BootloaderArgs {
337            omaha_app_id: Some("avb_app_id".to_string()),
338            omaha_url: Some("avb_url".to_string()),
339            ota_channel: Some("avb_channel".to_string()),
340        };
341        let json_config = JsonUpdateConfig {
342            default_channel: "json_channel".to_string(),
343            update_type: JsonUpdateType::Omaha("json_app_id".to_string(), None),
344            override_version: None,
345        };
346
347        let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
348            boot_args,
349            json_config,
350            "resolved_version".to_string(),
351        );
352        assert_eq!(
353            resolved_config.unwrap(),
354            RecoveryUpdateConfig {
355                channel: "avb_channel".to_string(),
356                update_type: UpdateType::Omaha {
357                    app_id: "avb_app_id".to_string(),
358                    service_url: "avb_url".to_string(),
359                },
360                version: "resolved_version".to_string(),
361            }
362        );
363    }
364
365    #[test]
366    fn resolve_omaha_all_boot_args_with_json_url() {
367        let boot_args = BootloaderArgs {
368            omaha_app_id: Some("avb_app_id".to_string()),
369            omaha_url: Some("avb_url".to_string()),
370            ota_channel: Some("avb_channel".to_string()),
371        };
372        let json_config = JsonUpdateConfig {
373            default_channel: "json_channel".to_string(),
374            update_type: JsonUpdateType::Omaha(
375                "json_app_id".to_string(),
376                Some("json_url".to_string()),
377            ),
378            override_version: None,
379        };
380
381        let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
382            boot_args,
383            json_config,
384            "resolved_version".to_string(),
385        );
386        assert_eq!(
387            resolved_config.unwrap(),
388            RecoveryUpdateConfig {
389                channel: "avb_channel".to_string(),
390                update_type: UpdateType::Omaha {
391                    app_id: "avb_app_id".to_string(),
392                    service_url: "avb_url".to_string(),
393                },
394                version: "resolved_version".to_string(),
395            }
396        );
397    }
398
399    // Boot Args proxy tests
400    async fn spawn_boot_arg_server_with_values(
401        args: HashMap<String, Option<String>>,
402    ) -> ArgumentsProxy {
403        let mock = Arc::new(MockBootArgumentsService::new(args));
404        let (proxy, stream) = create_proxy_and_stream::<ArgumentsMarker>();
405        fasync::Task::spawn(mock.handle_request_stream(stream)).detach();
406        proxy
407    }
408
409    #[fuchsia::test]
410    async fn full_boot_args_from_proxy() {
411        let proxy = spawn_boot_arg_server_with_values(hashmap! {
412            "omaha_app_id".to_string() => Some("avb_app_id".to_string()),
413            "ota_channel".to_string() => Some("avb_channel".to_string()),
414            "omaha_url".to_string() => Some("avb_url".to_string()),
415        })
416        .await;
417
418        let args = BootloaderArgs::load_from_proxy(proxy).await.unwrap();
419
420        assert_eq!(
421            args,
422            BootloaderArgs {
423                omaha_app_id: Some("avb_app_id".to_string()),
424                omaha_url: Some("avb_url".to_string()),
425                ota_channel: Some("avb_channel".to_string()),
426            }
427        );
428    }
429
430    #[fuchsia::test]
431    async fn partial_boot_args_from_proxy() {
432        let proxy = spawn_boot_arg_server_with_values(hashmap! {
433            "omaha_url".to_string() => Some("avb_url".to_string())
434        })
435        .await;
436        let args = BootloaderArgs::load_from_proxy(proxy).await.unwrap();
437
438        assert_eq!(
439            args,
440            BootloaderArgs {
441                omaha_app_id: None,
442                omaha_url: Some("avb_url".to_string()),
443                ota_channel: None,
444            }
445        );
446    }
447
448    #[fuchsia::test]
449    async fn empty_boot_args_from_proxy() {
450        let proxy = spawn_boot_arg_server_with_values(hashmap! {}).await;
451        let args = BootloaderArgs::load_from_proxy(proxy).await.unwrap();
452
453        assert_eq!(args, BootloaderArgs { omaha_app_id: None, omaha_url: None, ota_channel: None });
454    }
455
456    // Resolve version from proxy tests
457    #[fuchsia::test]
458    async fn resolve_version_from_proxy() {
459        let (proxy, mut stream) = create_proxy_and_stream::<BuildInfoMarker>();
460        fasync::Task::local(async move {
461            match stream.next().await.unwrap() {
462                Ok(BuildInfoRequest::GetBuildInfo { responder }) => {
463                    responder
464                        .send(&BuildInfo {
465                            version: Some("proxy_version".to_string()),
466                            ..Default::default()
467                        })
468                        .unwrap();
469                }
470                request => panic!("Unexpected request: {:?}", request),
471            }
472        })
473        .detach();
474
475        let json_config = JsonUpdateConfig {
476            default_channel: "json_channel".to_string(),
477            update_type: JsonUpdateType::Tuf,
478            override_version: None,
479        };
480        let version =
481            RecoveryUpdateConfig::resolve_version_from_proxy(&json_config, proxy).await.unwrap();
482
483        assert_eq!(version, "proxy_version".to_string());
484    }
485
486    #[fuchsia::test]
487    async fn resolve_version_from_json_override() {
488        let (build_info_proxy, mut stream) = create_proxy_and_stream::<BuildInfoMarker>();
489        fasync::Task::local(async move {
490            let _ = stream.next().await.unwrap();
491            panic!("Runtime version should not be queried if override provided");
492        })
493        .detach();
494        let json_config = JsonUpdateConfig {
495            default_channel: "json_channel".to_string(),
496            update_type: JsonUpdateType::Tuf,
497            override_version: Some("json_version".to_string()),
498        };
499        let version =
500            RecoveryUpdateConfig::resolve_version_from_proxy(&json_config, build_info_proxy)
501                .await
502                .unwrap();
503
504        assert_eq!(version, "json_version".to_string());
505    }
506
507    #[fuchsia::test]
508    async fn error_when_no_version_available() {
509        let (build_info_proxy, mut stream) = create_proxy_and_stream::<BuildInfoMarker>();
510        fasync::Task::local(async move {
511            match stream.next().await.unwrap() {
512                Ok(BuildInfoRequest::GetBuildInfo { responder }) => {
513                    responder.send(&BuildInfo::default()).unwrap();
514                }
515                request => panic!("Unexpected request: {:?}", request),
516            }
517        })
518        .detach();
519
520        let json_config = JsonUpdateConfig {
521            default_channel: "json_channel".to_string(),
522            update_type: JsonUpdateType::Tuf,
523            override_version: None,
524        };
525
526        let version_result =
527            RecoveryUpdateConfig::resolve_version_from_proxy(&json_config, build_info_proxy).await;
528        assert_matches!(version_result, Err(_));
529    }
530
531    // Serde struct tests
532    #[test]
533    fn test_no_json_channel() {
534        let string_version = r#"
535        {
536            "override_version": "1.2.3.4",
537            "update_type": "tuf"
538        }"#;
539        let res: Result<JsonUpdateConfig, serde_json::Error> = serde_json::from_str(string_version);
540        assert_matches!(res, Err(_));
541    }
542
543    #[test]
544    fn test_omaha_config_new_url() {
545        let a = JsonUpdateConfig {
546            default_channel: "some_channel".to_string(),
547            override_version: None,
548            update_type: JsonUpdateType::Omaha(
549                "app_id_here".to_string(),
550                Some("https://override.google.com".to_string()),
551            ),
552        };
553        let string_version = r#"{
554            "default_channel": "some_channel",
555            "update_type": {
556                "omaha": [
557                    "app_id_here",
558                    "https://override.google.com"
559                ]
560            }
561        }"#;
562        assert_eq!(a, serde_json::from_str(string_version).unwrap());
563    }
564
565    #[test]
566    fn test_omaha_config() {
567        let a = JsonUpdateConfig {
568            default_channel: "channel_from_json".to_string(),
569            override_version: None,
570            update_type: JsonUpdateType::Omaha("app_id_here".to_string(), None),
571        };
572        let string_version = r#"{
573            "default_channel": "channel_from_json",
574            "update_type": {
575                "omaha": [
576                    "app_id_here", null
577                ]
578            }
579        }"#;
580        assert_eq!(a, serde_json::from_str(string_version).unwrap());
581    }
582
583    #[test]
584    fn test_tuf_config() {
585        let a = JsonUpdateConfig {
586            default_channel: "channel_from_json".to_string(),
587            override_version: Some("1.2.3.4".to_string()),
588            update_type: JsonUpdateType::Tuf,
589        };
590        let string_version = r#"
591        {
592            "default_channel": "channel_from_json",
593            "override_version": "1.2.3.4",
594            "update_type": "tuf"
595        }"#;
596        assert_eq!(a, serde_json::from_str(string_version).unwrap());
597    }
598}