blob: cc94506be561d5b2b8ec87f32d026a4b9df53e0a [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
anyhow::{bail, Context, Error},
fidl_fuchsia_boot::{ArgumentsMarker, ArgumentsProxy},
fidl_fuchsia_buildinfo::{ProviderMarker as BuildInfoMarker, ProviderProxy as BuildInfoProxy},
serde::{Deserialize, Serialize},
std::{fs::File, io::BufReader},
};
const PATH_TO_RECOVERY_CONFIG: &'static str = "/config/data/recovery-config.json";
const DEFAULT_OMAHA_SERVICE_URL: &'static str =
"https://clients2.google.com/service/update2/fuchsia/json";
#[derive(Debug, PartialEq)]
pub struct RecoveryUpdateConfig {
pub channel: String,
pub update_type: UpdateType,
pub version: String,
}
#[derive(Debug, PartialEq)]
pub enum UpdateType {
/// Designates an Omaha based update
/// Parameters:
/// app_id: The omaha application id
/// omaha_service_url: the omaha service url query for updates
Omaha { app_id: String, service_url: String },
/// Designates a TUF based update
Tuf,
}
impl RecoveryUpdateConfig {
/// Resolve the update configuration using values from vbmeta (via boot args), the
/// build-provided json config, and the current version from BuildInfo.
pub async fn resolve_update_config() -> Result<RecoveryUpdateConfig, Error> {
let json_config =
JsonUpdateConfig::load_json_config_data().context("Failed to load json config data")?;
let boot_args = {
let arguments_proxy =
fuchsia_component::client::connect_to_protocol::<ArgumentsMarker>()
.context("Could not load boot arguments.")?;
BootloaderArgs::load_from_proxy(arguments_proxy)
.await
.context("Unable to connect to fuchsia.boot.Arguments.")
}
.map_err(|e| {
// Note: This map_err is a good candidate for .inspect_err(f) when it becomes stable
eprintln!(
"Error: Error collecting boot argument: '{:?}'. Continuing without boot args.",
e
);
})
.unwrap_or_default();
let build_info_proxy = fuchsia_component::client::connect_to_protocol::<BuildInfoMarker>()?;
let resolved_version = Self::resolve_version_from_proxy(&json_config, build_info_proxy)
.await
.context("Failed to load version")?;
Self::resolve_update_config_from_structs(boot_args, json_config, resolved_version)
}
async fn resolve_version_from_proxy(
json_config: &JsonUpdateConfig,
build_info_proxy: BuildInfoProxy,
) -> Result<String, Error> {
let version = match &json_config.override_version {
Some(version) => version.clone(),
None => {
let build_info =
build_info_proxy.get_build_info().await.context("Failed to read build info")?;
build_info.version.context("No version string provided by build info component")?
}
};
Ok(version)
}
/// Resolves update config from provided boot args and json config.
///
/// Note: Version is resolved separately and passed in.
fn resolve_update_config_from_structs(
boot_args: BootloaderArgs,
json_config: JsonUpdateConfig,
resolved_version: String,
) -> Result<RecoveryUpdateConfig, Error> {
// resolve channel (vbmeta > config)
let channel = boot_args.ota_channel.unwrap_or(json_config.default_channel);
// UpdateType is set by the json config
let update_type: UpdateType = match json_config.update_type {
JsonUpdateType::Omaha(json_app_id, json_service_url) => {
UpdateType::Omaha {
// Resolve app id (vbmeta > config), config must provide a fallback value
app_id: boot_args.omaha_app_id.unwrap_or(json_app_id),
// Resolve service url (vbmeta > config > hard-coded)
service_url: boot_args
.omaha_url
.or(json_service_url)
.unwrap_or(String::from(DEFAULT_OMAHA_SERVICE_URL)),
}
}
JsonUpdateType::Tuf => UpdateType::Tuf,
};
Ok(RecoveryUpdateConfig {
channel: channel,
update_type: update_type,
version: resolved_version,
})
}
}
/// Temporary struct to deserialize configs provided with config-data
/// TODO(b/259495731) Switch from deprecated config-data
// pub for integration tests
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct JsonUpdateConfig {
pub default_channel: String,
pub update_type: JsonUpdateType,
pub override_version: Option<String>,
}
//pub for integration tests
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum JsonUpdateType {
/// Designates an Omaha based update
/// Parameters:
/// app_id: The omaha application id
/// omaha_service_url: Override the default omaha service to query
Omaha(String, Option<String>),
/// Designates a TUF based update
Tuf,
}
impl JsonUpdateConfig {
fn load_json_config_data() -> Result<JsonUpdateConfig, Error> {
let ota_config: JsonUpdateConfig = serde_json::from_reader(BufReader::new(
File::open(PATH_TO_RECOVERY_CONFIG).context("Failed to find update config data")?,
))?;
Ok(ota_config)
}
}
/// A holder for values read by the `fuchsia.boot.arguments` protocol.
/// These boot args are expected to be read by the bootloader from the current zircon slot's vbmeta
/// file (usually zircon_r) and passed to boot zircon.
#[derive(Debug, Default, PartialEq)]
struct BootloaderArgs {
omaha_app_id: Option<String>,
omaha_url: Option<String>,
ota_channel: Option<String>,
}
impl BootloaderArgs {
async fn load_from_proxy(arguments_proxy: ArgumentsProxy) -> Result<BootloaderArgs, Error> {
let keys = &["omaha_app_id".to_owned(), "omaha_url".to_owned(), "ota_channel".to_owned()];
let num_keys = keys.len();
let boot_args: Vec<Option<String>> =
arguments_proxy.get_strings(keys).await.context("No boot args available.")?;
if boot_args.len() != num_keys {
bail!("Boot args returned {} values, expected {}", boot_args.len(), num_keys);
}
Ok(BootloaderArgs {
omaha_app_id: boot_args[0].clone(),
omaha_url: boot_args[1].clone(),
ota_channel: boot_args[2].clone(),
})
}
}
#[cfg(test)]
mod tests {
use {
super::*,
assert_matches::assert_matches,
fidl::endpoints::create_proxy_and_stream,
fidl_fuchsia_buildinfo::{BuildInfo, ProviderRequest as BuildInfoRequest},
fuchsia_async as fasync,
futures::prelude::*,
maplit::hashmap,
mock_boot_arguments::MockBootArgumentsService,
pretty_assertions::assert_eq,
std::{collections::HashMap, sync::Arc},
};
fn empty_bootloader_args() -> BootloaderArgs {
BootloaderArgs { omaha_app_id: None, omaha_url: None, ota_channel: None }
}
// resolve config from structs tests
#[test]
fn resolve_config_expected_in_field_use_case() {
let boot_args = BootloaderArgs {
omaha_app_id: Some("avb_app_id".to_string()),
omaha_url: None,
ota_channel: Some("avb_channel".to_string()),
};
let json_config = JsonUpdateConfig {
default_channel: "json_channel".to_string(),
update_type: JsonUpdateType::Omaha("json_app_id".to_string(), None),
override_version: None,
};
let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
boot_args,
json_config,
"resolved_version".to_string(),
);
assert_eq!(
resolved_config.unwrap(),
RecoveryUpdateConfig {
channel: "avb_channel".to_string(),
update_type: UpdateType::Omaha {
app_id: "avb_app_id".to_string(),
service_url: DEFAULT_OMAHA_SERVICE_URL.to_string(),
},
version: "resolved_version".to_string(),
}
);
}
#[test]
fn resolve_tuf_config_without_boot_args() {
let boot_args = empty_bootloader_args();
let json_config = JsonUpdateConfig {
default_channel: "json_channel".to_string(),
update_type: JsonUpdateType::Tuf,
override_version: None,
};
let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
boot_args,
json_config,
"resolved_version".to_string(),
);
assert_eq!(
resolved_config.unwrap(),
RecoveryUpdateConfig {
channel: "json_channel".to_string(),
update_type: UpdateType::Tuf,
version: "resolved_version".to_string(),
}
);
}
#[test]
fn resolve_tuf_config_with_boot_override() {
let boot_args = BootloaderArgs {
omaha_app_id: None,
omaha_url: None,
ota_channel: Some("avb_channel".to_string()),
};
let json_config = JsonUpdateConfig {
default_channel: "json_channel".to_string(),
update_type: JsonUpdateType::Tuf,
override_version: Some("json_version".to_string()),
};
let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
boot_args,
json_config,
"resolved_version".to_string(),
);
assert_eq!(
resolved_config.unwrap(),
RecoveryUpdateConfig {
channel: "avb_channel".to_string(),
update_type: UpdateType::Tuf,
version: "resolved_version".to_string(),
}
);
}
#[test]
fn resolve_omaha_config_without_boot_args() {
let boot_args = empty_bootloader_args();
let json_config = JsonUpdateConfig {
default_channel: "json_channel".to_string(),
update_type: JsonUpdateType::Omaha("json_app_id".to_string(), None),
override_version: None,
};
let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
boot_args,
json_config,
"resolved_version".to_string(),
);
assert_eq!(
resolved_config.unwrap(),
RecoveryUpdateConfig {
channel: "json_channel".to_string(),
update_type: UpdateType::Omaha {
app_id: "json_app_id".to_string(),
service_url: DEFAULT_OMAHA_SERVICE_URL.to_string(),
},
version: "resolved_version".to_string(),
}
);
}
#[test]
fn resolve_omaha_json_version_and_url() {
let boot_args = empty_bootloader_args();
let json_config = JsonUpdateConfig {
default_channel: "json_channel".to_string(),
update_type: JsonUpdateType::Omaha(
"json_app_id".to_string(),
Some("json_url".to_string()),
),
override_version: Some("json_version".to_string()),
};
let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
boot_args,
json_config,
"resolved_version".to_string(),
);
assert_eq!(
resolved_config.unwrap(),
RecoveryUpdateConfig {
channel: "json_channel".to_string(),
update_type: UpdateType::Omaha {
app_id: "json_app_id".to_string(),
service_url: "json_url".to_string(),
},
version: "resolved_version".to_string(),
}
);
}
#[test]
fn resolve_omaha_all_boot_args() {
let boot_args = BootloaderArgs {
omaha_app_id: Some("avb_app_id".to_string()),
omaha_url: Some("avb_url".to_string()),
ota_channel: Some("avb_channel".to_string()),
};
let json_config = JsonUpdateConfig {
default_channel: "json_channel".to_string(),
update_type: JsonUpdateType::Omaha("json_app_id".to_string(), None),
override_version: None,
};
let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
boot_args,
json_config,
"resolved_version".to_string(),
);
assert_eq!(
resolved_config.unwrap(),
RecoveryUpdateConfig {
channel: "avb_channel".to_string(),
update_type: UpdateType::Omaha {
app_id: "avb_app_id".to_string(),
service_url: "avb_url".to_string(),
},
version: "resolved_version".to_string(),
}
);
}
#[test]
fn resolve_omaha_all_boot_args_with_json_url() {
let boot_args = BootloaderArgs {
omaha_app_id: Some("avb_app_id".to_string()),
omaha_url: Some("avb_url".to_string()),
ota_channel: Some("avb_channel".to_string()),
};
let json_config = JsonUpdateConfig {
default_channel: "json_channel".to_string(),
update_type: JsonUpdateType::Omaha(
"json_app_id".to_string(),
Some("json_url".to_string()),
),
override_version: None,
};
let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
boot_args,
json_config,
"resolved_version".to_string(),
);
assert_eq!(
resolved_config.unwrap(),
RecoveryUpdateConfig {
channel: "avb_channel".to_string(),
update_type: UpdateType::Omaha {
app_id: "avb_app_id".to_string(),
service_url: "avb_url".to_string(),
},
version: "resolved_version".to_string(),
}
);
}
// Boot Args proxy tests
async fn spawn_boot_arg_server_with_values(
args: HashMap<String, Option<String>>,
) -> ArgumentsProxy {
let mock = Arc::new(MockBootArgumentsService::new(args));
let (proxy, stream) = create_proxy_and_stream::<ArgumentsMarker>().unwrap();
fasync::Task::spawn(mock.handle_request_stream(stream)).detach();
proxy
}
#[fuchsia::test]
async fn full_boot_args_from_proxy() {
let proxy = spawn_boot_arg_server_with_values(hashmap! {
"omaha_app_id".to_string() => Some("avb_app_id".to_string()),
"ota_channel".to_string() => Some("avb_channel".to_string()),
"omaha_url".to_string() => Some("avb_url".to_string()),
})
.await;
let args = BootloaderArgs::load_from_proxy(proxy).await.unwrap();
assert_eq!(
args,
BootloaderArgs {
omaha_app_id: Some("avb_app_id".to_string()),
omaha_url: Some("avb_url".to_string()),
ota_channel: Some("avb_channel".to_string()),
}
);
}
#[fuchsia::test]
async fn partial_boot_args_from_proxy() {
let proxy = spawn_boot_arg_server_with_values(hashmap! {
"omaha_url".to_string() => Some("avb_url".to_string())
})
.await;
let args = BootloaderArgs::load_from_proxy(proxy).await.unwrap();
assert_eq!(
args,
BootloaderArgs {
omaha_app_id: None,
omaha_url: Some("avb_url".to_string()),
ota_channel: None,
}
);
}
#[fuchsia::test]
async fn empty_boot_args_from_proxy() {
let proxy = spawn_boot_arg_server_with_values(hashmap! {}).await;
let args = BootloaderArgs::load_from_proxy(proxy).await.unwrap();
assert_eq!(args, BootloaderArgs { omaha_app_id: None, omaha_url: None, ota_channel: None });
}
// Resolve version from proxy tests
#[fuchsia::test]
async fn resolve_version_from_proxy() {
let (proxy, mut stream) = create_proxy_and_stream::<BuildInfoMarker>().unwrap();
fasync::Task::local(async move {
match stream.next().await.unwrap() {
Ok(BuildInfoRequest::GetBuildInfo { responder }) => {
responder
.send(&BuildInfo {
version: Some("proxy_version".to_string()),
..Default::default()
})
.unwrap();
}
request => panic!("Unexpected request: {:?}", request),
}
})
.detach();
let json_config = JsonUpdateConfig {
default_channel: "json_channel".to_string(),
update_type: JsonUpdateType::Tuf,
override_version: None,
};
let version =
RecoveryUpdateConfig::resolve_version_from_proxy(&json_config, proxy).await.unwrap();
assert_eq!(version, "proxy_version".to_string());
}
#[fuchsia::test]
async fn resolve_version_from_json_override() {
let (build_info_proxy, mut stream) = create_proxy_and_stream::<BuildInfoMarker>().unwrap();
fasync::Task::local(async move {
let _ = stream.next().await.unwrap();
panic!("Runtime version should not be queried if override provided");
})
.detach();
let json_config = JsonUpdateConfig {
default_channel: "json_channel".to_string(),
update_type: JsonUpdateType::Tuf,
override_version: Some("json_version".to_string()),
};
let version =
RecoveryUpdateConfig::resolve_version_from_proxy(&json_config, build_info_proxy)
.await
.unwrap();
assert_eq!(version, "json_version".to_string());
}
#[fuchsia::test]
async fn error_when_no_version_available() {
let (build_info_proxy, mut stream) = create_proxy_and_stream::<BuildInfoMarker>().unwrap();
fasync::Task::local(async move {
match stream.next().await.unwrap() {
Ok(BuildInfoRequest::GetBuildInfo { responder }) => {
responder.send(&BuildInfo::default()).unwrap();
}
request => panic!("Unexpected request: {:?}", request),
}
})
.detach();
let json_config = JsonUpdateConfig {
default_channel: "json_channel".to_string(),
update_type: JsonUpdateType::Tuf,
override_version: None,
};
let version_result =
RecoveryUpdateConfig::resolve_version_from_proxy(&json_config, build_info_proxy).await;
assert_matches!(version_result, Err(_));
}
// Serde struct tests
#[test]
fn test_no_json_channel() {
let string_version = r#"
{
"override_version": "1.2.3.4",
"update_type": "tuf"
}"#;
let res: Result<JsonUpdateConfig, serde_json::Error> = serde_json::from_str(string_version);
assert_matches!(res, Err(_));
}
#[test]
fn test_omaha_config_new_url() {
let a = JsonUpdateConfig {
default_channel: "some_channel".to_string(),
override_version: None,
update_type: JsonUpdateType::Omaha(
"app_id_here".to_string(),
Some("https://override.google.com".to_string()),
),
};
let string_version = r#"{
"default_channel": "some_channel",
"update_type": {
"omaha": [
"app_id_here",
"https://override.google.com"
]
}
}"#;
assert_eq!(a, serde_json::from_str(string_version).unwrap());
}
#[test]
fn test_omaha_config() {
let a = JsonUpdateConfig {
default_channel: "channel_from_json".to_string(),
override_version: None,
update_type: JsonUpdateType::Omaha("app_id_here".to_string(), None),
};
let string_version = r#"{
"default_channel": "channel_from_json",
"update_type": {
"omaha": [
"app_id_here", null
]
}
}"#;
assert_eq!(a, serde_json::from_str(string_version).unwrap());
}
#[test]
fn test_tuf_config() {
let a = JsonUpdateConfig {
default_channel: "channel_from_json".to_string(),
override_version: Some("1.2.3.4".to_string()),
update_type: JsonUpdateType::Tuf,
};
let string_version = r#"
{
"default_channel": "channel_from_json",
"override_version": "1.2.3.4",
"update_type": "tuf"
}"#;
assert_eq!(a, serde_json::from_str(string_version).unwrap());
}
}