Skip to main content

ota_lib/
ota.rs

1// Copyright 2020 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.
4use 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    /// Ota from a devhost.
27    Devhost { cfg: DevhostConfig },
28    /// Ota from a well-known location. TODO(simonshields): implement this.
29    WellKnown,
30}
31
32enum BoardName {
33    /// Use board name from /config/build-info.
34    BuildInfo,
35    /// Override board name with given value.
36    #[allow(dead_code)]
37    Override { name: String },
38}
39
40/// Helper for constructing OTAs.
41pub 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    /// Create a new `OtaEnvBuilder`. Requires an `outgoing_dir` which is served
52    /// by an instantiation of a Rust VFS tied to this component's outgoing
53    /// directory. This is required in order to prepare the outgoing directory
54    /// with capabilities like directories and storage for the `pkg-recovery.cm`
55    /// component which will be created as a child.
56    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    /// Override the board name for this OTA.
69    pub fn board_name(mut self, name: &str) -> Self {
70        self.board_name = BoardName::Override { name: name.to_owned() };
71        self
72    }
73
74    /// Use the given |DevhostConfig| to run an OTA.
75    pub fn devhost(mut self, cfg: DevhostConfig) -> Self {
76        self.ota_type = OtaType::Devhost { cfg };
77        self
78    }
79
80    #[allow(dead_code)]
81    /// Use the given |OmahaConfig| to run an OTA.
82    pub fn omaha_config(mut self, omaha_config: OmahaConfig) -> Self {
83        self.omaha_config = Some(omaha_config);
84        self
85    }
86
87    /// Use the given StorageType as the storage target.
88    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    /// Use the given path for SSL certificates.
95    pub fn ssl_certificates(mut self, path: &str) -> Self {
96        self.ssl_certificates = path.to_owned();
97        self
98    }
99
100    /// Returns the name of the board provided by fidl/fuchsia.buildinfo
101    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    /// Takes a devhost config, and converts into a pkg-resolver friendly format.
119    /// Returns a |File| representing a directory with the repository
120    /// configuration in it.
121    async fn get_devhost_config(&self, cfg: &DevhostConfig) -> Result<File, Error> {
122        // Get the repository information from the devhost (including keys and repo URL).
123        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        // Convert into a pkg-resolver friendly format.
139        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        // Set up a repo configuration folder for the resolver, and write out the config.
157        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    /// Construct an |OtaEnv| from this |OtaEnvBuilder|.
171    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    /// Run the OTA, targeting the given channel and reporting the given version
211    /// as the current system version.
212    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        // Utilize the repository configs and ssl certificates we were provided,
220        // by placing them in our outgoing directory.
221        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
247/// Run an OTA from a development host. Returns when the system and SSH keys have been installed.
248pub async fn run_devhost_ota(
249    cfg: DevhostConfig,
250    out_dir: ServerEnd<fio::DirectoryMarker>,
251) -> Result<(), Error> {
252    // TODO(https://fxbug.dev/42064284, b/255340851): deduplicate this spinup code with the code in
253    // ota_main.rs. To do that, we'll need to remove the run_devhost_ota call
254    // from //src/recovery/system/src/main.rs and make run_*_ota public to only ota_main.rs.
255    // Also, remove out_dir - ota_main.rs should provide an outgoing directory already spun up.
256    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
270/// Run an OTA against a TUF or Omaha server. Returns Ok after the system has successfully been installed.
271pub 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            // Check for testing override
297            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    /// Wrapper around a ramdisk blobfs.
345    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        /// Get all the blobs inside the blobfs.
356        pub fn list_blobs(&self) -> Result<BTreeSet<fuchsia_merkle::Hash>, Error> {
357            self.blobfs.list_blobs()
358        }
359
360        /// Get the blobfs proxy.
361        pub fn blobfs_root(&self) -> Result<fio::DirectoryProxy, Error> {
362            self.blobfs.root_dir_proxy()
363        }
364    }
365
366    /// This wraps a |FakeConfigHandler| in an |Arc|
367    /// so that we can implement UriPathHandler for it.
368    struct FakeConfigArc {
369        pub arc: Arc<FakeConfigHandler>,
370    }
371
372    /// This class is used to provide the '/config.json' endpoint
373    /// which the OTA process uses to discover information about the devhost repo.
374    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            // We don't expect any contention on this lock: we only need it
401            // because the test doesn't know the address of the server until it's running.
402            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            // This emulates the format returned by `ffx repository serve` running on a devhost.
415            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    /// Represents an OTA that is yet to be run.
443    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        /// Add a package to be installed by this OTA.
459        pub fn add_package(mut self, p: Package) -> Self {
460            self.packages.push(p);
461            self
462        }
463
464        /// Add an image to include in the update package for this OTA.
465        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        /// Generates the packages.json file for the update package.
471        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        /// Build an update package from the list of packages and images included
485        /// in this update.
486        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        /// Run the OTA.
498        pub async fn run_ota(&mut self) -> Result<(), Error> {
499            let update = self.make_update_package().await?;
500            // Create the repo.
501            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            // We expect the update package to be in blobfs, so add it to the list of packages.
513            self.packages.push(update);
514
515            // Add a hook to handle the config.json file, which is exposed by
516            // `pm serve` to enable autoconfiguration of repositories.
517            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            // Configure the address of the repository for config.json
525            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            // Build the environment, and do the OTA.
548            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        /// Check that the blobfs contains exactly the blobs we expect it to contain.
562        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] //TODO(https://fxbug.dev/42053153) Move to integration test
574    #[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}