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 futures::prelude::*;
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
130 .into_body()
131 .try_fold(Vec::new(), |mut vec, b| async move {
132 vec.extend(b);
133 Ok(vec)
134 })
135 .await
136 .context("into body")?;
137 let repo_info: Value = serde_json::from_slice(&body).context("Failed to parse JSON")?;
138
139 let config_for_resolver = json!({
141 "version": "1",
142 "content": [
143 {
144 "repo_url": "fuchsia-pkg://fuchsia.com",
145 "root_version": 1,
146 "root_threshold": 1,
147 "root_keys": repo_info["root_keys"],
148 "mirrors":[{
149 "mirror_url": repo_info["repo_url"],
150 "subscribe": true
151 }],
152 "update_package_url": null
153 }
154 ]
155 });
156
157 let tempdir = tempfile::tempdir().context("tempdir")?;
159 let file = tempdir.path().join("devhost.json");
160 let tmp_file = File::create(file).context("Creating file")?;
161 serde_json::to_writer(tmp_file, &config_for_resolver).context("Writing JSON")?;
162
163 Ok(File::open(tempdir.keep()).context("Opening tmpdir")?)
164 }
165
166 async fn get_wellknown_config(&self) -> Result<File, Error> {
167 println!("recovery-ota: passing in config from config_data");
168 Ok(File::open(PATH_TO_CONFIGS_DIR).context("Opening config data path")?)
169 }
170
171 pub async fn build(self) -> Result<OtaEnv, Error> {
173 let repo_dir = match &self.ota_type {
174 OtaType::Devhost { cfg } => {
175 self.get_devhost_config(cfg).await.context("Getting devhost config")?
176 }
177 OtaType::WellKnown => {
178 self.get_wellknown_config().await.context("Preparing wellknown config")?
179 }
180 };
181
182 let ssl_certificates =
183 File::open(&self.ssl_certificates).context("Opening SSL certificate folder")?;
184
185 let board_name = self.get_board_name().await.context("Could not get board name")?;
186
187 let blobfs_proxy =
188 self.blobfs_proxy.ok_or_else(|| format_err!("Blobfs proxy not found"))?;
189
190 Ok(OtaEnv {
191 blobfs_proxy,
192 board_name,
193 omaha_config: self.omaha_config,
194 repo_dir,
195 ssl_certificates,
196 outgoing_dir: self.outgoing_dir,
197 })
198 }
199}
200
201pub struct OtaEnv {
202 blobfs_proxy: fio::DirectoryProxy,
203 board_name: String,
204 omaha_config: Option<OmahaConfig>,
205 repo_dir: File,
206 ssl_certificates: File,
207 outgoing_dir: Arc<Simple>,
208}
209
210impl OtaEnv {
211 pub async fn do_ota(self, channel: &str, version: &str) -> Result<(), Error> {
214 fn proxy_from_file(file: File) -> Result<fio::DirectoryProxy, Error> {
215 Ok(fio::DirectoryProxy::new(fuchsia_async::Channel::from_channel(
216 fdio::transfer_fd(file)?.into(),
217 )))
218 }
219
220 self.outgoing_dir.add_entry(
223 "config",
224 vfs::pseudo_directory! {
225 "data" => vfs::pseudo_directory!{
226 "repositories" => vfs::remote::remote_dir(proxy_from_file(self.repo_dir)?)
227 },
228 "ssl" => vfs::remote::remote_dir(
229 proxy_from_file(self.ssl_certificates)?
230 ),
231 "build-info" => vfs::pseudo_directory!{
232 "board" => vfs::file::vmo::read_only(self.board_name),
233 "version" => vfs::file::vmo::read_only(String::from(version)),
234 }
235 },
236 )?;
237
238 self.outgoing_dir.add_entry("blob", vfs::remote::remote_dir(self.blobfs_proxy))?;
239
240 download_and_apply_update(channel, version, self.omaha_config)
241 .await
242 .context("Installing OTA")?;
243
244 Ok(())
245 }
246}
247
248pub async fn run_devhost_ota(
250 cfg: DevhostConfig,
251 out_dir: ServerEnd<fio::DirectoryMarker>,
252) -> Result<(), Error> {
253 let outgoing_dir_vfs = vfs::pseudo_directory! {};
258
259 let scope = vfs::execution_scope::ExecutionScope::new();
260 vfs::directory::serve_on(outgoing_dir_vfs.clone(), SERVE_FLAGS, scope.clone(), out_dir);
261 fasync::Task::local(async move { scope.wait().await }).detach();
262
263 let ota_env = OtaEnvBuilder::new(outgoing_dir_vfs)
264 .devhost(cfg)
265 .build()
266 .await
267 .context("Failed to create devhost OTA env")?;
268 ota_env.do_ota("devhost", "20200101.1.1").await
269}
270
271pub async fn run_wellknown_ota(
273 blobfs_proxy: fio::DirectoryProxy,
274 outgoing_dir: Arc<Simple>,
275) -> Result<(), Error> {
276 let config =
277 RecoveryUpdateConfig::resolve_update_config().await.context("Couldn't get config")?;
278 let channel = config.channel;
279 let version = config.version;
280
281 match config.update_type {
282 UpdateType::Tuf => {
283 println!("recovery-ota: Creating TUF OTA environment");
284 let ota_env = OtaEnvBuilder::new(outgoing_dir)
285 .blobfs_proxy(blobfs_proxy)
286 .build()
287 .await
288 .context("Failed to create OTA env")?;
289 println!(
290 "recovery-ota: Starting TUF OTA on channel '{}' against version '{}'",
291 channel, version
292 );
293 ota_env.do_ota(&channel, &version).await
294 }
295 UpdateType::Omaha { app_id, service_url } => {
296 println!("recovery-ota: Creating Omaha OTA environment");
297 println!(
299 "recovery-ota: trying Omaha OTA on channel '{}' against version '{}', with service URL '{}' and app id '{}'",
300 channel, version, service_url, app_id
301 );
302
303 let ota_env = OtaEnvBuilder::new(outgoing_dir)
304 .omaha_config(OmahaConfig { app_id: app_id, server_url: service_url })
305 .blobfs_proxy(blobfs_proxy)
306 .build()
307 .await
308 .context("Failed to create OTA env");
309
310 match ota_env {
311 Ok(ref _ota_env) => {
312 println!("got no error while creating OTA env...")
313 }
314 Err(ref e) => {
315 eprintln!("got error while creating OTA env: {:?}", e)
316 }
317 }
318
319 println!(
320 "recovery-ota: Starting Omaha OTA on channel '{}' against version '{}'",
321 channel, version
322 );
323 let res = ota_env?.do_ota(&channel, &version).await;
324 println!("recovery-ota: OTA result: {:?}", res);
325 res
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use blobfs_ramdisk::BlobfsRamdisk;
334 use fidl_fuchsia_pkg_ext::RepositoryKey;
335 use fuchsia_async as fasync;
336 use fuchsia_pkg_testing::serve::HttpResponder;
337 use fuchsia_pkg_testing::{Package, PackageBuilder, RepositoryBuilder, make_epoch_json};
338 use fuchsia_runtime::{HandleType, take_startup_handle};
339 use fuchsia_sync::Mutex;
340 use futures::future::{BoxFuture, ready};
341 use hyper::{Body, Request, Response, StatusCode, header};
342 use std::collections::{BTreeSet, HashMap};
343 use url::Url;
344
345 struct FakeStorage {
347 blobfs: BlobfsRamdisk,
348 }
349
350 impl FakeStorage {
351 pub async fn new() -> Result<Self, Error> {
352 let blobfs = BlobfsRamdisk::start().await.context("launching blobfs")?;
353 Ok(FakeStorage { blobfs })
354 }
355
356 pub fn list_blobs(&self) -> Result<BTreeSet<fuchsia_merkle::Hash>, Error> {
358 self.blobfs.list_blobs()
359 }
360
361 pub fn blobfs_root(&self) -> Result<fio::DirectoryProxy, Error> {
363 self.blobfs.root_dir_proxy()
364 }
365 }
366
367 struct FakeConfigArc {
370 pub arc: Arc<FakeConfigHandler>,
371 }
372
373 struct FakeConfigHandler {
376 repo_keys: BTreeSet<RepositoryKey>,
377 address: Mutex<String>,
378 }
379
380 impl FakeConfigHandler {
381 pub fn new(repo_keys: BTreeSet<RepositoryKey>) -> Arc<Self> {
382 Arc::new(FakeConfigHandler { repo_keys, address: Mutex::new("unknown".to_owned()) })
383 }
384
385 pub fn set_repo_address(self: Arc<Self>, addr: String) {
386 let mut val = self.address.lock();
387 *val = addr;
388 }
389 }
390
391 impl HttpResponder for FakeConfigArc {
392 fn respond(
393 &self,
394 request: &Request<Body>,
395 response: Response<Body>,
396 ) -> BoxFuture<'_, Response<Body>> {
397 if request.uri().path() != "/config.json" {
398 return ready(response).boxed();
399 }
400
401 let val = self.arc.address.lock();
404 if *val == "unknown" {
405 panic!("Expected address to be set!");
406 }
407 let repo_url = match Url::parse(&*val) {
408 Ok(u) => match u.host_str() {
409 Some(host) => format!("fuchsia-pkg://{}", host),
410 _ => "default".into(),
411 },
412 _ => panic!("Invalid address provided: {}", &*val),
413 };
414
415 let config = json!({
417 "repo_url": repo_url,
418 "root_version": "1",
419 "root_threshold": "1",
420 "root_keys": self.arc.repo_keys,
421 "mirrors":[{
422 "mirror_url": &*val,
423 "subscribe": true
424 }],
425 "use_local_mirror": false,
426 "repo_storage_type": "ephemeral",
427 });
428
429 let json_str = serde_json::to_string(&config).context("Serializing JSON").unwrap();
430 let response = Response::builder()
431 .status(StatusCode::OK)
432 .header(header::CONTENT_LENGTH, json_str.len())
433 .body(Body::from(json_str))
434 .unwrap();
435
436 ready(response).boxed()
437 }
438 }
439
440 const EMPTY_REPO_PATH: &str = "/pkg/empty-repo";
441 const TEST_SSL_CERTS: &str = "/pkg/data/ssl";
442
443 struct TestOtaEnv {
445 images: HashMap<String, Vec<u8>>,
446 packages: Vec<Package>,
447 storage: FakeStorage,
448 }
449
450 impl TestOtaEnv {
451 pub async fn new() -> Result<Self, Error> {
452 Ok(TestOtaEnv {
453 images: HashMap::new(),
454 packages: vec![],
455 storage: FakeStorage::new().await.context("Starting fake storage")?,
456 })
457 }
458
459 pub fn add_package(mut self, p: Package) -> Self {
461 self.packages.push(p);
462 self
463 }
464
465 pub fn add_image(mut self, name: &str, data: &str) -> Self {
467 self.images.insert(name.to_owned(), data.to_owned().into_bytes());
468 self
469 }
470
471 fn generate_packages_list(&self) -> String {
473 let package_urls: Vec<String> = self
474 .packages
475 .iter()
476 .map(|p| format!("fuchsia-pkg://fuchsia.com/{}/0?hash={}", p.name(), p.hash()))
477 .collect();
478 let packages = json!({
479 "version": 1,
480 "content": package_urls,
481 });
482 serde_json::to_string(&packages).unwrap()
483 }
484
485 async fn make_update_package(&self) -> Result<Package, Error> {
488 let mut update = PackageBuilder::new("update")
489 .add_resource_at("packages.json", self.generate_packages_list().as_bytes());
490
491 for (name, data) in self.images.iter() {
492 update = update.add_resource_at(name, data.as_slice());
493 }
494
495 update.build().await.context("Building update package")
496 }
497
498 pub async fn run_ota(&mut self) -> Result<(), Error> {
500 let update = self.make_update_package().await?;
501 let repo = Arc::new(
503 self.packages
504 .iter()
505 .fold(
506 RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH).add_package(&update),
507 |repo, package| repo.add_package(package),
508 )
509 .build()
510 .await
511 .context("Building repo")?,
512 );
513 self.packages.push(update);
515
516 let request_handler = FakeConfigHandler::new(repo.root_keys());
519 let served_repo = Arc::clone(&repo)
520 .server()
521 .response_overrider(FakeConfigArc { arc: Arc::clone(&request_handler) })
522 .start()
523 .context("Starting repository")?;
524
525 let url = served_repo.local_url();
527 let config_url = format!("{}/config.json", url);
528 request_handler.set_repo_address(url);
529
530 let cfg = DevhostConfig { url: config_url };
531
532 let directory_handle = take_startup_handle(HandleType::DirectoryRequest.into())
533 .expect("cannot take startup handle");
534 let outgoing_dir = zx::Channel::from(directory_handle);
535 let outgoing_dir_vfs = vfs::pseudo_directory! {};
536
537 let scope = vfs::execution_scope::ExecutionScope::new();
538 vfs::directory::serve_on(
539 outgoing_dir_vfs.clone(),
540 SERVE_FLAGS,
541 scope.clone(),
542 outgoing_dir.into(),
543 );
544 fasync::Task::local(async move { scope.wait().await }).detach();
545
546 let blobfs_proxy = self.storage.blobfs_root()?;
547
548 let ota_env = OtaEnvBuilder::new(outgoing_dir_vfs)
550 .board_name("x64")
551 .blobfs_proxy(blobfs_proxy)
552 .ssl_certificates(TEST_SSL_CERTS)
553 .devhost(cfg)
554 .build()
555 .await
556 .context("Building environment")?;
557
558 ota_env.do_ota("devhost", "20240101.1.1").await.context("Running OTA")?;
559 Ok(())
560 }
561
562 pub async fn check_blobs(&self) {
564 let written_blobs = self.storage.list_blobs().expect("Listing blobfs blobs");
565 let mut all_package_blobs = BTreeSet::new();
566 for package in self.packages.iter() {
567 all_package_blobs.append(&mut package.list_blobs());
568 }
569
570 assert_eq!(written_blobs, all_package_blobs);
571 }
572 }
573
574 #[ignore] #[fasync::run_singlethreaded(test)]
576 async fn test_run_devhost_ota() -> Result<(), Error> {
577 let package = PackageBuilder::new("test-package")
578 .add_resource_at("data/file1", "Hello, world!".as_bytes())
579 .build()
580 .await
581 .unwrap();
582 let mut env = TestOtaEnv::new()
583 .await?
584 .add_package(package)
585 .add_image("zbi.signed", "zbi image")
586 .add_image("fuchsia.vbmeta", "fuchsia vbmeta")
587 .add_image("epoch.json", &make_epoch_json(1));
588
589 env.run_ota().await?;
590 env.check_blobs().await;
591 Ok(())
592 }
593}