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 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    /// 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
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        // Convert into a pkg-resolver friendly format.
140        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        // Set up a repo configuration folder for the resolver, and write out the config.
158        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    /// Construct an |OtaEnv| from this |OtaEnvBuilder|.
172    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    /// Run the OTA, targeting the given channel and reporting the given version
212    /// as the current system version.
213    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        // Utilize the repository configs and ssl certificates we were provided,
221        // by placing them in our outgoing directory.
222        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
248/// Run an OTA from a development host. Returns when the system and SSH keys have been installed.
249pub async fn run_devhost_ota(
250    cfg: DevhostConfig,
251    out_dir: ServerEnd<fio::DirectoryMarker>,
252) -> Result<(), Error> {
253    // TODO(https://fxbug.dev/42064284, b/255340851): deduplicate this spinup code with the code in
254    // ota_main.rs. To do that, we'll need to remove the run_devhost_ota call
255    // from //src/recovery/system/src/main.rs and make run_*_ota public to only ota_main.rs.
256    // Also, remove out_dir - ota_main.rs should provide an outgoing directory already spun up.
257    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
271/// Run an OTA against a TUF or Omaha server. Returns Ok after the system has successfully been installed.
272pub 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            // Check for testing override
298            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    /// Wrapper around a ramdisk blobfs.
346    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        /// Get all the blobs inside the blobfs.
357        pub fn list_blobs(&self) -> Result<BTreeSet<fuchsia_merkle::Hash>, Error> {
358            self.blobfs.list_blobs()
359        }
360
361        /// Get the blobfs proxy.
362        pub fn blobfs_root(&self) -> Result<fio::DirectoryProxy, Error> {
363            self.blobfs.root_dir_proxy()
364        }
365    }
366
367    /// This wraps a |FakeConfigHandler| in an |Arc|
368    /// so that we can implement UriPathHandler for it.
369    struct FakeConfigArc {
370        pub arc: Arc<FakeConfigHandler>,
371    }
372
373    /// This class is used to provide the '/config.json' endpoint
374    /// which the OTA process uses to discover information about the devhost repo.
375    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            // We don't expect any contention on this lock: we only need it
402            // because the test doesn't know the address of the server until it's running.
403            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            // This emulates the format returned by `ffx repository serve` running on a devhost.
416            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    /// Represents an OTA that is yet to be run.
444    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        /// Add a package to be installed by this OTA.
460        pub fn add_package(mut self, p: Package) -> Self {
461            self.packages.push(p);
462            self
463        }
464
465        /// Add an image to include in the update package for this OTA.
466        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        /// Generates the packages.json file for the update package.
472        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        /// Build an update package from the list of packages and images included
486        /// in this update.
487        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        /// Run the OTA.
499        pub async fn run_ota(&mut self) -> Result<(), Error> {
500            let update = self.make_update_package().await?;
501            // Create the repo.
502            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            // We expect the update package to be in blobfs, so add it to the list of packages.
514            self.packages.push(update);
515
516            // Add a hook to handle the config.json file, which is exposed by
517            // `pm serve` to enable autoconfiguration of repositories.
518            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            // Configure the address of the repository for config.json
526            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            // Build the environment, and do the OTA.
549            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        /// Check that the blobfs contains exactly the blobs we expect it to contain.
563        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] //TODO(https://fxbug.dev/42053153) Move to integration test
575    #[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}