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