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 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    /// Ota from a devhost.
28    Devhost { cfg: DevhostConfig },
29    /// Ota from a well-known location. TODO(simonshields): implement this.
30    WellKnown,
31}
32
33enum BoardName {
34    /// Use board name from /config/build-info.
35    BuildInfo,
36    /// Override board name with given value.
37    #[allow(dead_code)]
38    Override { name: String },
39}
40
41/// Helper for constructing OTAs.
42pub 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    /// Create a new `OtaEnvBuilder`. Requires an `outgoing_dir` which is served
53    /// by an instantiation of a Rust VFS tied to this component's outgoing
54    /// directory. This is required in order to prepare the outgoing directory
55    /// with capabilities like directories and storage for the `pkg-recovery.cm`
56    /// component which will be created as a child.
57    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    /// Override the board name for this OTA.
70    pub fn board_name(mut self, name: &str) -> Self {
71        self.board_name = BoardName::Override { name: name.to_owned() };
72        self
73    }
74
75    /// Use the given |DevhostConfig| to run an OTA.
76    pub fn devhost(mut self, cfg: DevhostConfig) -> Self {
77        self.ota_type = OtaType::Devhost { cfg };
78        self
79    }
80
81    #[allow(dead_code)]
82    /// Use the given |OmahaConfig| to run an OTA.
83    pub fn omaha_config(mut self, omaha_config: OmahaConfig) -> Self {
84        self.omaha_config = Some(omaha_config);
85        self
86    }
87
88    /// Use the given StorageType as the storage target.
89    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    /// Use the given path for SSL certificates.
96    pub fn ssl_certificates(mut self, path: &str) -> Self {
97        self.ssl_certificates = path.to_owned();
98        self
99    }
100
101    /// Returns the name of the board provided by fidl/fuchsia.buildinfo
102    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    /// Takes a devhost config, and converts into a pkg-resolver friendly format.
120    /// Returns a |File| representing a directory with the repository
121    /// configuration in it.
122    async fn get_devhost_config(&self, cfg: &DevhostConfig) -> Result<File, Error> {
123        // Get the repository information from the devhost (including keys and repo URL).
124        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        // Convert into a pkg-resolver friendly format.
133        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        // Set up a repo configuration folder for the resolver, and write out the config.
151        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    /// Construct an |OtaEnv| from this |OtaEnvBuilder|.
165    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    /// Run the OTA, targeting the given channel and reporting the given version
205    /// as the current system version.
206    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        // Utilize the repository configs and ssl certificates we were provided,
214        // by placing them in our outgoing directory.
215        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
241/// Run an OTA from a development host. Returns when the system and SSH keys have been installed.
242pub async fn run_devhost_ota(
243    cfg: DevhostConfig,
244    out_dir: ServerEnd<fio::DirectoryMarker>,
245) -> Result<(), Error> {
246    // TODO(https://fxbug.dev/42064284, b/255340851): deduplicate this spinup code with the code in
247    // ota_main.rs. To do that, we'll need to remove the run_devhost_ota call
248    // from //src/recovery/system/src/main.rs and make run_*_ota public to only ota_main.rs.
249    // Also, remove out_dir - ota_main.rs should provide an outgoing directory already spun up.
250    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
264/// Run an OTA against a TUF or Omaha server. Returns Ok after the system has successfully been installed.
265pub 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            // Check for testing override
291            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    /// Wrapper around a ramdisk blobfs.
341    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        /// Get all the blobs inside the blobfs.
352        pub fn list_blobs(&self) -> Result<BTreeSet<fuchsia_merkle::Hash>, Error> {
353            self.blobfs.list_blobs()
354        }
355
356        /// Get the blobfs proxy.
357        pub fn blobfs_root(&self) -> Result<fio::DirectoryProxy, Error> {
358            self.blobfs.root_dir_proxy()
359        }
360    }
361
362    /// This wraps a |FakeConfigHandler| in an |Arc|
363    /// so that we can implement UriPathHandler for it.
364    struct FakeConfigArc {
365        pub arc: Arc<FakeConfigHandler>,
366    }
367
368    /// This class is used to provide the '/config.json' endpoint
369    /// which the OTA process uses to discover information about the devhost repo.
370    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            // We don't expect any contention on this lock: we only need it
397            // because the test doesn't know the address of the server until it's running.
398            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            // This emulates the format returned by `ffx repository serve` running on a devhost.
411            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    /// Represents an OTA that is yet to be run.
439    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        /// Add a package to be installed by this OTA.
455        pub fn add_package(mut self, p: Package) -> Self {
456            self.packages.push(p);
457            self
458        }
459
460        /// Add an image to include in the update package for this OTA.
461        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        /// Generates the packages.json file for the update package.
467        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        /// Build an update package from the list of packages and images included
481        /// in this update.
482        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        /// Run the OTA.
494        pub async fn run_ota(&mut self) -> Result<(), Error> {
495            let update = self.make_update_package().await?;
496            // Create the repo.
497            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            // We expect the update package to be in blobfs, so add it to the list of packages.
509            self.packages.push(update);
510
511            // Add a hook to handle the config.json file, which is exposed by
512            // `pm serve` to enable autoconfiguration of repositories.
513            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            // Configure the address of the repository for config.json
521            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            // Build the environment, and do the OTA.
544            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        /// Check that the blobfs contains exactly the blobs we expect it to contain.
558        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] //TODO(https://fxbug.dev/42053153) Move to integration test
570    #[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}