Skip to main content

isolated_ota_env/
lib.rs

1// Copyright 2022 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.
4
5#![allow(clippy::let_unit_value)]
6
7use anyhow::{Context, Error};
8use async_trait::async_trait;
9use fidl::endpoints::{ClientEnd, DiscoverableProtocolMarker, Proxy, ServerEnd};
10use fidl_fuchsia_io as fio;
11use fidl_fuchsia_io::DirectoryProxy;
12use fidl_fuchsia_paver::PaverRequestStream;
13use fidl_fuchsia_pkg_ext::RepositoryConfigs;
14use fuchsia_async as fasync;
15use fuchsia_component::server::ServiceFs;
16use fuchsia_component_test::LocalComponentHandles;
17use fuchsia_merkle::Hash;
18use fuchsia_pkg_testing::serve::ServedRepository;
19use fuchsia_pkg_testing::{Package, RepositoryBuilder};
20use fuchsia_sync::Mutex;
21use futures::prelude::*;
22use isolated_ota::{OmahaConfig, UpdateUrlSource};
23use mock_omaha_server::{
24    OmahaResponse, OmahaServer, OmahaServerBuilder, ResponseAndMetadata, ResponseMap,
25};
26use mock_paver::{MockPaverService, MockPaverServiceBuilder};
27use std::collections::{BTreeMap, BTreeSet};
28use std::io::Write;
29use std::str::FromStr;
30use std::sync::Arc;
31use tempfile::TempDir;
32
33pub const GLOBAL_SSL_CERTS_PATH: &str = "/config/ssl";
34const EMPTY_REPO_PATH: &str = "/pkg/empty-repo";
35const TEST_CERTS_PATH: &str = "/pkg/data/ssl";
36const TEST_REPO_URL: &str = "fuchsia-pkg://integration.test.fuchsia.com";
37
38pub enum OmahaState {
39    /// Don't use Omaha for this update, instead use the provided, or default, update URL.
40    Disabled(Option<http::Uri>),
41    /// Set up an Omaha server automatically.
42    Auto(OmahaResponse),
43    /// Pass the given OmahaConfig to Omaha.
44    Manual(OmahaConfig),
45}
46
47pub struct TestParams {
48    pub blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
49    pub board: String,
50    pub channel: String,
51    pub expected_blobfs_contents: BTreeSet<Hash>,
52    pub paver: Arc<MockPaverService>,
53    pub repo_config_dir: TempDir,
54    pub ssl_certs: DirectoryProxy,
55    pub update_merkle: Hash,
56    pub version: String,
57    pub update_url_source: UpdateUrlSource,
58    pub paver_connector: ClientEnd<fio::DirectoryMarker>,
59    pub system_image_hash: Option<Hash>,
60}
61
62/// Connects the local component to a mock paver.
63///
64/// Unlike other mocks, the `fuchsia.paver.Paver` is serviced by [`isolated_ota_env::TestEnv`], so
65/// this function proxies to the given `paver_dir_proxy` which is expected to host a
66/// file named "fuchsia.paver.Paver" which implements the `fuchsia.paver.Paver` FIDL protocol.
67pub async fn expose_mock_paver(
68    handles: LocalComponentHandles,
69    paver_dir_proxy: fio::DirectoryProxy,
70) -> Result<(), Error> {
71    let mut fs = ServiceFs::new();
72
73    fs.dir("svc").add_service_connector(
74        move |server_end: ServerEnd<fidl_fuchsia_paver::PaverMarker>| {
75            fdio::service_connect_at(
76                paver_dir_proxy.as_channel().as_ref(),
77                &format!("/{}", fidl_fuchsia_paver::PaverMarker::PROTOCOL_NAME),
78                server_end.into_channel(),
79            )
80            .expect("failed to connect to paver service node");
81        },
82    );
83
84    fs.serve_connection(handles.outgoing_dir).expect("failed to serve paver fs connection");
85    fs.collect::<()>().await;
86    Ok(())
87}
88
89#[async_trait(?Send)]
90pub trait TestExecutor<R> {
91    async fn run(&self, params: TestParams) -> R;
92}
93
94pub struct TestEnvBuilder<R> {
95    blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
96    board: String,
97    channel: String,
98    omaha: OmahaState,
99    packages: Vec<Package>,
100    paver: MockPaverServiceBuilder,
101    repo_config: Option<RepositoryConfigs>,
102    version: String,
103    test_executor: Option<Box<dyn TestExecutor<R>>>,
104    // The zbi and optional vbmeta contents.
105    fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
106    // The zbi and optional vbmeta contents of the recovery partition.
107    recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
108    firmware_images: BTreeMap<String, Vec<u8>>,
109    system_image_hash: Option<Hash>,
110}
111
112impl<R> TestEnvBuilder<R> {
113    #[allow(clippy::new_without_default)]
114    pub fn new() -> Self {
115        TestEnvBuilder {
116            blobfs: None,
117            board: "test-board".to_owned(),
118            channel: "test".to_owned(),
119            omaha: OmahaState::Disabled(Some(format!("{TEST_REPO_URL}/update").parse().unwrap())),
120            packages: vec![],
121            paver: MockPaverServiceBuilder::new(),
122            repo_config: None,
123            version: "0.1.2.3".to_owned(),
124            test_executor: None,
125            fuchsia_image: None,
126            recovery_image: None,
127            firmware_images: BTreeMap::new(),
128            system_image_hash: None,
129        }
130    }
131
132    /// Add a package to the repository generated by this TestEnvBuilder.
133    /// The package will also be listed in the generated update package
134    /// so that it will be downloaded as part of the OTA.
135    pub fn add_package(mut self, pkg: Package) -> Self {
136        self.packages.push(pkg);
137        self
138    }
139
140    pub fn blobfs(mut self, client: ClientEnd<fio::DirectoryMarker>) -> Self {
141        self.blobfs = Some(client);
142        self
143    }
144
145    /// Provide a TUF repository configuration to the package resolver.
146    /// This will override the repository that the builder would otherwise generate.
147    pub fn repo_config(mut self, repo: RepositoryConfigs) -> Self {
148        self.repo_config = Some(repo);
149        self
150    }
151
152    pub fn system_image_hash(mut self, hash: Hash) -> Self {
153        self.system_image_hash = Some(hash);
154        self
155    }
156
157    /// Enable/disable Omaha. OmahaState::Auto will automatically set up an Omaha server and tell
158    /// the updater to use it.
159    pub fn omaha_state(mut self, state: OmahaState) -> Self {
160        self.omaha = state;
161        self
162    }
163
164    /// Mutate the MockPaverServiecBuilder used by this TestEnvBuilder.
165    pub fn paver<F>(mut self, func: F) -> Self
166    where
167        F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
168    {
169        self.paver = func(self.paver);
170        self
171    }
172
173    pub fn test_executor(mut self, executor: Box<dyn TestExecutor<R>>) -> Self {
174        self.test_executor = Some(executor);
175        self
176    }
177
178    /// The zbi and optional vbmeta images to write.
179    pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
180        assert_eq!(self.fuchsia_image, None);
181        self.fuchsia_image = Some((zbi, vbmeta));
182        self
183    }
184
185    /// The zbi and optional vbmeta images to write to the recovery partition.
186    pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
187        assert_eq!(self.recovery_image, None);
188        self.recovery_image = Some((zbi, vbmeta));
189        self
190    }
191
192    /// A firmware image to write.
193    pub fn firmware_image(mut self, type_: String, content: Vec<u8>) -> Self {
194        assert_eq!(self.firmware_images.insert(type_, content), None);
195        self
196    }
197
198    /// Turn this |TestEnvBuilder| into a |TestEnv|
199    pub async fn build(mut self) -> Result<TestEnv<R>, Error> {
200        let (repo_config, served_repo, ssl_certs, expected_blobfs_contents, merkle) =
201            if let Some(repo_config) = self.repo_config {
202                // Use the provided repo config. Assume that this means we'll actually want to use
203                // real SSL certificates, and that we don't need to host our own repository.
204                (
205                    repo_config,
206                    None,
207                    fuchsia_fs::directory::open_in_namespace(
208                        GLOBAL_SSL_CERTS_PATH,
209                        fio::PERM_READABLE,
210                    )
211                    .unwrap(),
212                    BTreeSet::new(),
213                    Hash::from_str(
214                        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
215                    )
216                    .expect("make merkle"),
217                )
218            } else {
219                // If no repo config was specified, host a repo containing the provided packages,
220                // and an update package containing given images + all packages in the repo.
221                let mut update =
222                    fuchsia_pkg_testing::UpdatePackageBuilder::new(TEST_REPO_URL.parse().unwrap())
223                        .packages(
224                            self.packages
225                                .iter()
226                                .map(|p| {
227                                    fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl::new(
228                                        TEST_REPO_URL.parse().unwrap(),
229                                        p.name().clone(),
230                                        None,
231                                        *p.hash(),
232                                    )
233                                })
234                                .collect::<Vec<_>>(),
235                        )
236                        .firmware_images(self.firmware_images);
237                if let Some((zbi, vbmeta)) = self.fuchsia_image {
238                    update = update.fuchsia_image(zbi, vbmeta);
239                }
240                if let Some((zbi, vbmeta)) = self.recovery_image {
241                    update = update.recovery_image(zbi, vbmeta);
242                }
243                let (update, images) = update.build().await;
244
245                // Do not include the images package, system-updater triggers GC after resolving it.
246                let expected_blobfs_contents = self
247                    .packages
248                    .iter()
249                    .chain([update.as_package()])
250                    .flat_map(|p| p.list_blobs())
251                    .collect();
252
253                let repo = Arc::new(
254                    self.packages
255                        .iter()
256                        .chain([update.as_package(), &images])
257                        .fold(
258                            RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
259                                .add_package(update.as_package()),
260                            |repo, package| repo.add_package(package),
261                        )
262                        .build()
263                        .await
264                        .expect("build repo"),
265                );
266
267                let served_repo = Arc::clone(&repo).server().start().expect("serve repo");
268                let config = RepositoryConfigs::Version1(vec![
269                    served_repo.make_repo_config(TEST_REPO_URL.parse().expect("make repo config")),
270                ]);
271
272                let update_merkle = *update.as_package().hash();
273                // Add the update package to the list of packages, so that TestResult::check_packages
274                // will expect to see the update package's blobs in blobfs.
275                let mut packages = vec![update.into_package()];
276                packages.append(&mut self.packages);
277                (
278                    config,
279                    Some(served_repo),
280                    fuchsia_fs::directory::open_in_namespace(TEST_CERTS_PATH, fio::PERM_READABLE)
281                        .unwrap(),
282                    expected_blobfs_contents,
283                    update_merkle,
284                )
285            };
286
287        let dir = tempfile::tempdir()?;
288        let mut path = dir.path().to_owned();
289        path.push("repo_config.json");
290        let path = path.as_path();
291        let mut file =
292            std::io::BufWriter::new(std::fs::File::create(path).context("creating file")?);
293        serde_json::to_writer(&mut file, &repo_config).unwrap();
294        file.flush().unwrap();
295
296        Ok(TestEnv {
297            blobfs: self.blobfs,
298            board: self.board,
299            channel: self.channel,
300            omaha: self.omaha,
301            expected_blobfs_contents,
302            paver: Arc::new(self.paver.build()),
303            _repo: served_repo,
304            repo_config_dir: dir,
305            ssl_certs,
306            update_merkle: merkle,
307            version: self.version,
308            test_executor: self.test_executor.expect("test executor must be set"),
309            system_image_hash: self.system_image_hash,
310        })
311    }
312}
313
314pub struct TestEnv<R> {
315    blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
316    channel: String,
317    omaha: OmahaState,
318    expected_blobfs_contents: BTreeSet<Hash>,
319    paver: Arc<MockPaverService>,
320    _repo: Option<ServedRepository>,
321    repo_config_dir: tempfile::TempDir,
322    ssl_certs: DirectoryProxy,
323    update_merkle: Hash,
324    board: String,
325    version: String,
326    test_executor: Box<dyn TestExecutor<R>>,
327    system_image_hash: Option<Hash>,
328}
329
330impl<R> TestEnv<R> {
331    async fn start_omaha(omaha: OmahaState, merkle: Hash) -> Result<UpdateUrlSource, Error> {
332        match omaha {
333            OmahaState::Disabled(url) => Ok(match url {
334                Some(url) => UpdateUrlSource::UpdateUrl(url),
335                None => UpdateUrlSource::UseDefault,
336            }),
337            OmahaState::Manual(cfg) => Ok(UpdateUrlSource::OmahaConfig(cfg)),
338            OmahaState::Auto(response) => {
339                // Amend the default struct with the expected package hash
340                let mut response = ResponseAndMetadata { response, ..Default::default() };
341                let p: Vec<_> = response.package_name.split("?hash=").collect();
342                assert_eq!(p.len(), 2);
343                response.package_name = format!("{}?hash={}", p[0], merkle);
344                let server = OmahaServerBuilder::default()
345                    .responses_by_appid(
346                        vec![("integration-test-appid".to_string(), response)]
347                            .into_iter()
348                            .collect::<ResponseMap>(),
349                    )
350                    .build()
351                    .unwrap();
352                let addr = OmahaServer::start_and_detach(Arc::new(Mutex::new(server)), None)
353                    .await
354                    .context("Starting omaha server")?;
355                let config =
356                    OmahaConfig { app_id: "integration-test-appid".to_owned(), server_url: addr };
357
358                Ok(UpdateUrlSource::OmahaConfig(config))
359            }
360        }
361    }
362
363    /// Run the update, consuming this |TestEnv| and returning a |TestResult|.
364    pub async fn run(self) -> R {
365        let update_url_source = TestEnv::<R>::start_omaha(self.omaha, self.update_merkle)
366            .await
367            .expect("Starting Omaha server");
368
369        let mut service_fs = ServiceFs::new();
370        let paver_clone = Arc::clone(&self.paver);
371        service_fs.add_fidl_service(move |stream: PaverRequestStream| {
372            fasync::Task::spawn(
373                Arc::clone(&paver_clone)
374                    .run_paver_service(stream)
375                    .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
376            )
377            .detach();
378        });
379
380        let (client, server) =
381            fidl::endpoints::create_endpoints::<fidl_fuchsia_io::DirectoryMarker>();
382        service_fs
383            .serve_connection(server.into_channel().into())
384            .expect("Failed to serve connection");
385        fasync::Task::spawn(service_fs.collect()).detach();
386
387        let params = TestParams {
388            blobfs: self.blobfs,
389            board: self.board,
390            channel: self.channel,
391            expected_blobfs_contents: self.expected_blobfs_contents,
392            paver: self.paver,
393            repo_config_dir: self.repo_config_dir,
394            ssl_certs: self.ssl_certs,
395            update_merkle: self.update_merkle,
396            version: self.version,
397            update_url_source,
398            paver_connector: client,
399            system_image_hash: self.system_image_hash,
400        };
401
402        self.test_executor.run(params).await
403    }
404}