1use crate::config::{RecoveryUpdateConfig, UpdateType};
5use crate::setup::DevhostConfig;
6use anyhow::{Context, Error, bail, format_err};
7use fidl::endpoints::ServerEnd;
8use fidl_fuchsia_buildinfo::ProviderMarker as BuildInfoMarker;
9use fidl_fuchsia_io as fio;
10use fuchsia_async as fasync;
11use fuchsia_component::client;
12use http_body_util::BodyExt;
13use hyper::Uri;
14use isolated_ota::{OmahaConfig, download_and_apply_update};
15use serde_json::{Value, json};
16use std::fs::File;
17use std::str::FromStr;
18use std::sync::Arc;
19use vfs::directory::helper::DirectlyMutable;
20use vfs::directory::immutable::simple::Simple;
21
22const PATH_TO_CONFIGS_DIR: &'static str = "/config/data/ota-configs";
23const SERVE_FLAGS: fio::Flags =
24 fio::PERM_READABLE.union(fio::PERM_WRITABLE).union(fio::PERM_EXECUTABLE);
25
26enum OtaType {
27 Devhost { cfg: DevhostConfig },
29 WellKnown,
31}
32
33enum BoardName {
34 BuildInfo,
36 #[allow(dead_code)]
38 Override { name: String },
39}
40
41pub struct OtaEnvBuilder {
43 board_name: BoardName,
44 omaha_config: Option<OmahaConfig>,
45 ota_type: OtaType,
46 ssl_certificates: String,
47 outgoing_dir: Arc<Simple>,
48 blobfs_proxy: Option<fio::DirectoryProxy>,
49}
50
51impl OtaEnvBuilder {
52 pub fn new(outgoing_dir: Arc<Simple>) -> Self {
58 OtaEnvBuilder {
59 board_name: BoardName::BuildInfo,
60 omaha_config: None,
61 ota_type: OtaType::WellKnown,
62 ssl_certificates: "/config/ssl".to_owned(),
63 outgoing_dir,
64 blobfs_proxy: None,
65 }
66 }
67
68 #[cfg(test)]
69 pub fn board_name(mut self, name: &str) -> Self {
71 self.board_name = BoardName::Override { name: name.to_owned() };
72 self
73 }
74
75 pub fn devhost(mut self, cfg: DevhostConfig) -> Self {
77 self.ota_type = OtaType::Devhost { cfg };
78 self
79 }
80
81 #[allow(dead_code)]
82 pub fn omaha_config(mut self, omaha_config: OmahaConfig) -> Self {
84 self.omaha_config = Some(omaha_config);
85 self
86 }
87
88 pub fn blobfs_proxy(mut self, blobfs_proxy: fio::DirectoryProxy) -> Self {
90 self.blobfs_proxy = Some(blobfs_proxy);
91 self
92 }
93
94 #[cfg(test)]
95 pub fn ssl_certificates(mut self, path: &str) -> Self {
97 self.ssl_certificates = path.to_owned();
98 self
99 }
100
101 async fn get_board_name(&self) -> Result<String, Error> {
103 match &self.board_name {
104 BoardName::BuildInfo => {
105 let proxy = match client::connect_to_protocol::<BuildInfoMarker>() {
106 Ok(p) => p,
107 Err(err) => {
108 bail!("Failed to connect to fuchsia.buildinfo.Provider proxy: {:?}", err)
109 }
110 };
111 let build_info =
112 proxy.get_build_info().await.context("Failed to read build info")?;
113 build_info.board_config.ok_or_else(|| format_err!("No board name provided"))
114 }
115 BoardName::Override { name } => Ok(name.to_owned()),
116 }
117 }
118
119 async fn get_devhost_config(&self, cfg: &DevhostConfig) -> Result<File, Error> {
123 let client = fuchsia_hyper::new_client();
125 let response = client
126 .get(Uri::from_str(&cfg.url).context("Bad URL")?)
127 .await
128 .context("Fetching config from devhost")?;
129 let body = response.into_body().collect().await.context("into body")?.to_bytes();
130 let repo_info: Value = serde_json::from_slice(&body).context("Failed to parse JSON")?;
131
132 let config_for_resolver = json!({
134 "version": "1",
135 "content": [
136 {
137 "repo_url": "fuchsia-pkg://fuchsia.com",
138 "root_version": 1,
139 "root_threshold": 1,
140 "root_keys": repo_info["root_keys"],
141 "mirrors":[{
142 "mirror_url": repo_info["repo_url"],
143 "subscribe": true
144 }],
145 "update_package_url": null
146 }
147 ]
148 });
149
150 let tempdir = tempfile::tempdir().context("tempdir")?;
152 let file = tempdir.path().join("devhost.json");
153 let tmp_file = File::create(file).context("Creating file")?;
154 serde_json::to_writer(tmp_file, &config_for_resolver).context("Writing JSON")?;
155
156 Ok(File::open(tempdir.keep()).context("Opening tmpdir")?)
157 }
158
159 async fn get_wellknown_config(&self) -> Result<File, Error> {
160 println!("recovery-ota: passing in config from config_data");
161 Ok(File::open(PATH_TO_CONFIGS_DIR).context("Opening config data path")?)
162 }
163
164 pub async fn build(self) -> Result<OtaEnv, Error> {
166 let repo_dir = match &self.ota_type {
167 OtaType::Devhost { cfg } => {
168 self.get_devhost_config(cfg).await.context("Getting devhost config")?
169 }
170 OtaType::WellKnown => {
171 self.get_wellknown_config().await.context("Preparing wellknown config")?
172 }
173 };
174
175 let ssl_certificates =
176 File::open(&self.ssl_certificates).context("Opening SSL certificate folder")?;
177
178 let board_name = self.get_board_name().await.context("Could not get board name")?;
179
180 let blobfs_proxy =
181 self.blobfs_proxy.ok_or_else(|| format_err!("Blobfs proxy not found"))?;
182
183 Ok(OtaEnv {
184 blobfs_proxy,
185 board_name,
186 omaha_config: self.omaha_config,
187 repo_dir,
188 ssl_certificates,
189 outgoing_dir: self.outgoing_dir,
190 })
191 }
192}
193
194pub struct OtaEnv {
195 blobfs_proxy: fio::DirectoryProxy,
196 board_name: String,
197 omaha_config: Option<OmahaConfig>,
198 repo_dir: File,
199 ssl_certificates: File,
200 outgoing_dir: Arc<Simple>,
201}
202
203impl OtaEnv {
204 pub async fn do_ota(self, channel: &str, version: &str) -> Result<(), Error> {
207 fn proxy_from_file(file: File) -> Result<fio::DirectoryProxy, Error> {
208 Ok(fio::DirectoryProxy::new(fuchsia_async::Channel::from_channel(
209 fdio::transfer_fd(file)?.into(),
210 )))
211 }
212
213 self.outgoing_dir.add_entry(
216 "config",
217 vfs::pseudo_directory! {
218 "data" => vfs::pseudo_directory!{
219 "repositories" => vfs::remote::remote_dir(proxy_from_file(self.repo_dir)?)
220 },
221 "ssl" => vfs::remote::remote_dir(
222 proxy_from_file(self.ssl_certificates)?
223 ),
224 "build-info" => vfs::pseudo_directory!{
225 "board" => vfs::file::vmo::read_only(self.board_name),
226 "version" => vfs::file::vmo::read_only(String::from(version)),
227 }
228 },
229 )?;
230
231 self.outgoing_dir.add_entry("blob", vfs::remote::remote_dir(self.blobfs_proxy))?;
232
233 download_and_apply_update(channel, version, self.omaha_config)
234 .await
235 .context("Installing OTA")?;
236
237 Ok(())
238 }
239}
240
241pub async fn run_devhost_ota(
243 cfg: DevhostConfig,
244 out_dir: ServerEnd<fio::DirectoryMarker>,
245) -> Result<(), Error> {
246 let outgoing_dir_vfs = vfs::pseudo_directory! {};
251
252 let scope = vfs::execution_scope::ExecutionScope::new();
253 vfs::directory::serve_on(outgoing_dir_vfs.clone(), SERVE_FLAGS, scope.clone(), out_dir);
254 fasync::Task::local(async move { scope.wait().await }).detach();
255
256 let ota_env = OtaEnvBuilder::new(outgoing_dir_vfs)
257 .devhost(cfg)
258 .build()
259 .await
260 .context("Failed to create devhost OTA env")?;
261 ota_env.do_ota("devhost", "20200101.1.1").await
262}
263
264pub async fn run_wellknown_ota(
266 blobfs_proxy: fio::DirectoryProxy,
267 outgoing_dir: Arc<Simple>,
268) -> Result<(), Error> {
269 let config =
270 RecoveryUpdateConfig::resolve_update_config().await.context("Couldn't get config")?;
271 let channel = config.channel;
272 let version = config.version;
273
274 match config.update_type {
275 UpdateType::Tuf => {
276 println!("recovery-ota: Creating TUF OTA environment");
277 let ota_env = OtaEnvBuilder::new(outgoing_dir)
278 .blobfs_proxy(blobfs_proxy)
279 .build()
280 .await
281 .context("Failed to create OTA env")?;
282 println!(
283 "recovery-ota: Starting TUF OTA on channel '{}' against version '{}'",
284 channel, version
285 );
286 ota_env.do_ota(&channel, &version).await
287 }
288 UpdateType::Omaha { app_id, service_url } => {
289 println!("recovery-ota: Creating Omaha OTA environment");
290 println!(
292 "recovery-ota: trying Omaha OTA on channel '{}' against version '{}', with service URL '{}' and app id '{}'",
293 channel, version, service_url, app_id
294 );
295
296 let ota_env = OtaEnvBuilder::new(outgoing_dir)
297 .omaha_config(OmahaConfig { app_id: app_id, server_url: service_url })
298 .blobfs_proxy(blobfs_proxy)
299 .build()
300 .await
301 .context("Failed to create OTA env");
302
303 match ota_env {
304 Ok(ref _ota_env) => {
305 println!("got no error while creating OTA env...")
306 }
307 Err(ref e) => {
308 eprintln!("got error while creating OTA env: {:?}", e)
309 }
310 }
311
312 println!(
313 "recovery-ota: Starting Omaha OTA on channel '{}' against version '{}'",
314 channel, version
315 );
316 let res = ota_env?.do_ota(&channel, &version).await;
317 println!("recovery-ota: OTA result: {:?}", res);
318 res
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use blobfs_ramdisk::BlobfsRamdisk;
327 use fidl_fuchsia_pkg_ext::RepositoryKey;
328 use fuchsia_async as fasync;
329 use fuchsia_pkg_testing::serve::HttpResponder;
330 use fuchsia_pkg_testing::{Package, PackageBuilder, RepositoryBuilder, make_epoch_json};
331 use fuchsia_repo::body::Body;
332 use fuchsia_runtime::{HandleType, take_startup_handle};
333 use fuchsia_sync::Mutex;
334 use futures::FutureExt as _;
335 use futures::future::{BoxFuture, ready};
336 use hyper::{Request, Response, StatusCode, header};
337 use std::collections::{BTreeSet, HashMap};
338 use url::Url;
339
340 struct FakeStorage {
342 blobfs: BlobfsRamdisk,
343 }
344
345 impl FakeStorage {
346 pub async fn new() -> Result<Self, Error> {
347 let blobfs = BlobfsRamdisk::start().await.context("launching blobfs")?;
348 Ok(FakeStorage { blobfs })
349 }
350
351 pub fn list_blobs(&self) -> Result<BTreeSet<fuchsia_merkle::Hash>, Error> {
353 self.blobfs.list_blobs()
354 }
355
356 pub fn blobfs_root(&self) -> Result<fio::DirectoryProxy, Error> {
358 self.blobfs.root_dir_proxy()
359 }
360 }
361
362 struct FakeConfigArc {
365 pub arc: Arc<FakeConfigHandler>,
366 }
367
368 struct FakeConfigHandler {
371 repo_keys: BTreeSet<RepositoryKey>,
372 address: Mutex<String>,
373 }
374
375 impl FakeConfigHandler {
376 pub fn new(repo_keys: BTreeSet<RepositoryKey>) -> Arc<Self> {
377 Arc::new(FakeConfigHandler { repo_keys, address: Mutex::new("unknown".to_owned()) })
378 }
379
380 pub fn set_repo_address(self: Arc<Self>, addr: String) {
381 let mut val = self.address.lock();
382 *val = addr;
383 }
384 }
385
386 impl HttpResponder for FakeConfigArc {
387 fn respond(
388 &self,
389 request: &Request<Body>,
390 response: Response<Body>,
391 ) -> BoxFuture<'_, Response<Body>> {
392 if request.uri().path() != "/config.json" {
393 return ready(response).boxed();
394 }
395
396 let val = self.arc.address.lock();
399 if *val == "unknown" {
400 panic!("Expected address to be set!");
401 }
402 let repo_url = match Url::parse(&*val) {
403 Ok(u) => match u.host_str() {
404 Some(host) => format!("fuchsia-pkg://{}", host),
405 _ => "default".into(),
406 },
407 _ => panic!("Invalid address provided: {}", &*val),
408 };
409
410 let config = json!({
412 "repo_url": repo_url,
413 "root_version": "1",
414 "root_threshold": "1",
415 "root_keys": self.arc.repo_keys,
416 "mirrors":[{
417 "mirror_url": &*val,
418 "subscribe": true
419 }],
420 "use_local_mirror": false,
421 "repo_storage_type": "ephemeral",
422 });
423
424 let json_str = serde_json::to_string(&config).context("Serializing JSON").unwrap();
425 let response = Response::builder()
426 .status(StatusCode::OK)
427 .header(header::CONTENT_LENGTH, json_str.len())
428 .body(Body::from(json_str))
429 .unwrap();
430
431 ready(response).boxed()
432 }
433 }
434
435 const EMPTY_REPO_PATH: &str = "/pkg/empty-repo";
436 const TEST_SSL_CERTS: &str = "/pkg/data/ssl";
437
438 struct TestOtaEnv {
440 images: HashMap<String, Vec<u8>>,
441 packages: Vec<Package>,
442 storage: FakeStorage,
443 }
444
445 impl TestOtaEnv {
446 pub async fn new() -> Result<Self, Error> {
447 Ok(TestOtaEnv {
448 images: HashMap::new(),
449 packages: vec![],
450 storage: FakeStorage::new().await.context("Starting fake storage")?,
451 })
452 }
453
454 pub fn add_package(mut self, p: Package) -> Self {
456 self.packages.push(p);
457 self
458 }
459
460 pub fn add_image(mut self, name: &str, data: &str) -> Self {
462 self.images.insert(name.to_owned(), data.to_owned().into_bytes());
463 self
464 }
465
466 fn generate_packages_list(&self) -> String {
468 let package_urls: Vec<String> = self
469 .packages
470 .iter()
471 .map(|p| format!("fuchsia-pkg://fuchsia.com/{}/0?hash={}", p.name(), p.hash()))
472 .collect();
473 let packages = json!({
474 "version": 1,
475 "content": package_urls,
476 });
477 serde_json::to_string(&packages).unwrap()
478 }
479
480 async fn make_update_package(&self) -> Result<Package, Error> {
483 let mut update = PackageBuilder::new("update")
484 .add_resource_at("packages.json", self.generate_packages_list().as_bytes());
485
486 for (name, data) in self.images.iter() {
487 update = update.add_resource_at(name, data.as_slice());
488 }
489
490 update.build().await.context("Building update package")
491 }
492
493 pub async fn run_ota(&mut self) -> Result<(), Error> {
495 let update = self.make_update_package().await?;
496 let repo = Arc::new(
498 self.packages
499 .iter()
500 .fold(
501 RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH).add_package(&update),
502 |repo, package| repo.add_package(package),
503 )
504 .build()
505 .await
506 .context("Building repo")?,
507 );
508 self.packages.push(update);
510
511 let request_handler = FakeConfigHandler::new(repo.root_keys());
514 let served_repo = Arc::clone(&repo)
515 .server()
516 .response_overrider(FakeConfigArc { arc: Arc::clone(&request_handler) })
517 .start()
518 .context("Starting repository")?;
519
520 let url = served_repo.local_url();
522 let config_url = format!("{}/config.json", url);
523 request_handler.set_repo_address(url);
524
525 let cfg = DevhostConfig { url: config_url };
526
527 let directory_handle = take_startup_handle(HandleType::DirectoryRequest.into())
528 .expect("cannot take startup handle");
529 let outgoing_dir = zx::Channel::from(directory_handle);
530 let outgoing_dir_vfs = vfs::pseudo_directory! {};
531
532 let scope = vfs::execution_scope::ExecutionScope::new();
533 vfs::directory::serve_on(
534 outgoing_dir_vfs.clone(),
535 SERVE_FLAGS,
536 scope.clone(),
537 outgoing_dir.into(),
538 );
539 fasync::Task::local(async move { scope.wait().await }).detach();
540
541 let blobfs_proxy = self.storage.blobfs_root()?;
542
543 let ota_env = OtaEnvBuilder::new(outgoing_dir_vfs)
545 .board_name("x64")
546 .blobfs_proxy(blobfs_proxy)
547 .ssl_certificates(TEST_SSL_CERTS)
548 .devhost(cfg)
549 .build()
550 .await
551 .context("Building environment")?;
552
553 ota_env.do_ota("devhost", "20240101.1.1").await.context("Running OTA")?;
554 Ok(())
555 }
556
557 pub async fn check_blobs(&self) {
559 let written_blobs = self.storage.list_blobs().expect("Listing blobfs blobs");
560 let mut all_package_blobs = BTreeSet::new();
561 for package in self.packages.iter() {
562 all_package_blobs.append(&mut package.list_blobs());
563 }
564
565 assert_eq!(written_blobs, all_package_blobs);
566 }
567 }
568
569 #[ignore] #[fuchsia::test]
571 async fn test_run_devhost_ota() -> Result<(), Error> {
572 let package = PackageBuilder::new("test-package")
573 .add_resource_at("data/file1", "Hello, world!".as_bytes())
574 .build()
575 .await
576 .unwrap();
577 let mut env = TestOtaEnv::new()
578 .await?
579 .add_package(package)
580 .add_image("zbi.signed", "zbi image")
581 .add_image("fuchsia.vbmeta", "fuchsia vbmeta")
582 .add_image("epoch.json", &make_epoch_json(1));
583
584 env.run_ota().await?;
585 env.check_blobs().await;
586 Ok(())
587 }
588}