Skip to main content

blackout_target/
lib.rs

1// Copyright 2019 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//! library for target side of filesystem integrity host-target interaction tests
6
7#![deny(missing_docs)]
8
9use anyhow::{Context as _, Result, anyhow};
10use async_trait::async_trait;
11use block_matcher::{BlockDeviceMatcher, Guid, create_random_guid, find_block_device, into_guid};
12use fidl_fuchsia_blackout_test::{ControllerRequest, ControllerRequestStream};
13use fidl_fuchsia_device::ControllerMarker;
14use fidl_fuchsia_io as fio;
15use fidl_fuchsia_storage_partitions as fpartitions;
16use fs_management::filesystem::BlockConnector;
17use fuchsia_async as fasync;
18use fuchsia_component::client::{Service, connect_to_protocol, connect_to_protocol_at_path};
19use fuchsia_component::server::{ServiceFs, ServiceObj};
20use fuchsia_fs::directory::readdir;
21use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future};
22use rand::distr::StandardUniform;
23use rand::rngs::StdRng;
24use rand::{Rng, SeedableRng};
25use std::pin::pin;
26use std::sync::Arc;
27
28pub mod random_op;
29pub mod static_tree;
30
31/// The three steps the target-side of a blackout test needs to implement.
32#[async_trait]
33pub trait Test {
34    /// Setup the test run on the given block_device.
35    async fn setup(
36        self: Arc<Self>,
37        device_label: String,
38        device_path: Option<String>,
39        seed: u64,
40    ) -> Result<()>;
41    /// Run the test body on the given device_path.
42    async fn test(
43        self: Arc<Self>,
44        device_label: String,
45        device_path: Option<String>,
46        seed: u64,
47    ) -> Result<()>;
48    /// Verify the consistency of the filesystem on the device_path.
49    async fn verify(
50        self: Arc<Self>,
51        device_label: String,
52        device_path: Option<String>,
53        seed: u64,
54    ) -> Result<()>;
55}
56
57struct BlackoutController(ControllerRequestStream);
58
59/// A test server, which serves the fuchsia.blackout.test.Controller protocol.
60pub struct TestServer<'a, T> {
61    fs: ServiceFs<ServiceObj<'a, BlackoutController>>,
62    test: Arc<T>,
63}
64
65impl<'a, T> TestServer<'a, T>
66where
67    T: Test + 'static,
68{
69    /// Create a new test server for this test.
70    pub fn new(test: T) -> Result<TestServer<'a, T>> {
71        let mut fs = ServiceFs::new();
72        fs.dir("svc").add_fidl_service(BlackoutController);
73        fs.take_and_serve_directory_handle()?;
74
75        Ok(TestServer { fs, test: Arc::new(test) })
76    }
77
78    /// Start serving the outgoing directory. Blocks until all connections are closed.
79    pub async fn serve(self) {
80        const MAX_CONCURRENT: usize = 10_000;
81        let test = self.test;
82        self.fs
83            .for_each_concurrent(MAX_CONCURRENT, move |stream| {
84                handle_request(test.clone(), stream).unwrap_or_else(|e| log::error!("{}", e))
85            })
86            .await;
87    }
88}
89
90async fn handle_request<T: Test + 'static>(
91    test: Arc<T>,
92    BlackoutController(mut stream): BlackoutController,
93) -> Result<()> {
94    while let Some(request) = stream.try_next().await? {
95        handle_controller(test.clone(), request).await?;
96    }
97
98    Ok(())
99}
100
101async fn handle_controller<T: Test + 'static>(
102    test: Arc<T>,
103    request: ControllerRequest,
104) -> Result<()> {
105    match request {
106        ControllerRequest::Setup { responder, device_label, device_path, seed } => {
107            let res = test.setup(device_label, device_path, seed).await.map_err(|err| {
108                log::error!(err:?; "Setup failed");
109                zx::Status::INTERNAL.into_raw()
110            });
111            responder.send(res)?;
112        }
113        ControllerRequest::Test { responder, device_label, device_path, seed, duration } => {
114            let test_fut = test.test(device_label, device_path, seed).map_err(|err| {
115                log::error!(err:?; "Test failed");
116                zx::Status::INTERNAL.into_raw()
117            });
118            if duration != 0 {
119                // If a non-zero duration is provided, spawn the test and then return after that
120                // duration.
121                log::info!("starting test and replying in {} seconds...", duration);
122                let timer = pin!(fasync::Timer::new(std::time::Duration::from_secs(duration)));
123                let res = match future::select(test_fut, timer).await {
124                    future::Either::Left((res, _)) => res,
125                    future::Either::Right((_, test_fut)) => {
126                        fasync::Task::spawn(test_fut.map(|_| ())).detach();
127                        Ok(())
128                    }
129                };
130                responder.send(res)?;
131            } else {
132                // If a zero duration is provided, return once the test step is complete.
133                log::info!("starting test...");
134                responder.send(test_fut.await)?;
135            }
136        }
137        ControllerRequest::Verify { responder, device_label, device_path, seed } => {
138            let res = test.verify(device_label, device_path, seed).await.map_err(|e| {
139                // The test tries failing on purpose, so only print errors as warnings.
140                log::warn!("{:?}", e);
141                zx::Status::BAD_STATE.into_raw()
142            });
143            responder.send(res)?;
144        }
145    }
146
147    Ok(())
148}
149
150/// Generate a Vec<u8> of random bytes from a seed using a standard distribution.
151pub fn generate_content(seed: u64) -> Vec<u8> {
152    let mut rng = StdRng::seed_from_u64(seed);
153
154    let size = rng.random_range(1..1 << 16);
155    rng.sample_iter(&StandardUniform).take(size).collect()
156}
157
158/// Find the device in /dev/class/block that represents a given topological path. Returns the full
159/// path of the device in /dev/class/block.
160pub async fn find_dev(dev: &str) -> Result<String> {
161    let dev_class_block =
162        fuchsia_fs::directory::open_in_namespace("/dev/class/block", fio::PERM_READABLE)?;
163    for entry in readdir(&dev_class_block).await? {
164        let path = format!("/dev/class/block/{}", entry.name);
165        let proxy = connect_to_protocol_at_path::<ControllerMarker>(&path)?;
166        let topo_path = proxy.get_topological_path().await?.map_err(|s| zx::Status::from_raw(s))?;
167        log::info!("{} => {}", path, topo_path);
168        if dev == topo_path {
169            return Ok(path);
170        }
171    }
172    Err(anyhow::anyhow!("Couldn't find {} in /dev/class/block", dev))
173}
174
175/// Returns a directory proxy connected to /dev.
176pub fn dev() -> fio::DirectoryProxy {
177    fuchsia_fs::directory::open_in_namespace("/dev", fio::PERM_READABLE)
178        .expect("failed to open /dev")
179}
180
181/// This type guid is only used if the test has to create the gpt partition itself. Otherwise, only
182/// the label is used to find the partition.
183const BLACKOUT_TYPE_GUID: &Guid = &[
184    0x68, 0x45, 0x23, 0x01, 0xab, 0x89, 0xef, 0xcd, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
185];
186
187const GPT_PARTITION_SIZE: u64 = 60 * 1024 * 1024;
188
189/// Set up a partition for testing using the device label, returning a block connector for it. If
190/// the partition already exists with this label, it's used. If no existing device is found with
191/// this label, create a new gpt partition to use.
192pub async fn set_up_partition(device_label: String) -> Result<Box<dyn BlockConnector>> {
193    let partitions = Service::open(fpartitions::PartitionServiceMarker).unwrap();
194    let manager = connect_to_protocol::<fpartitions::PartitionsManagerMarker>().unwrap();
195
196    let service_instances =
197        partitions.clone().enumerate().await.expect("Failed to enumerate partitions");
198    if let Some(connector) =
199        find_block_device(&[BlockDeviceMatcher::Name(&device_label)], service_instances.into_iter())
200            .await
201            .context("Failed to find block device")?
202    {
203        log::info!(device_label:%; "found existing partition");
204        Ok(Box::new(connector))
205    } else {
206        log::info!(device_label:%; "adding new partition to the system gpt");
207        let info =
208            manager.get_block_info().await.expect("FIDL error").expect("get_block_info failed");
209        let transaction = manager
210            .create_transaction()
211            .await
212            .expect("FIDL error")
213            .map_err(zx::Status::from_raw)
214            .expect("create_transaction failed");
215        let request = fpartitions::PartitionsManagerAddPartitionRequest {
216            transaction: Some(transaction.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap()),
217            name: Some(device_label.clone()),
218            type_guid: Some(into_guid(BLACKOUT_TYPE_GUID.clone())),
219            instance_guid: Some(into_guid(create_random_guid())),
220            num_blocks: Some(GPT_PARTITION_SIZE / info.1 as u64),
221            ..Default::default()
222        };
223        manager
224            .add_partition(request)
225            .await
226            .expect("FIDL error")
227            .map_err(zx::Status::from_raw)
228            .expect("add_partition failed");
229        manager
230            .commit_transaction(transaction)
231            .await
232            .expect("FIDL error")
233            .map_err(zx::Status::from_raw)
234            .expect("add_partition failed");
235        let service_instances =
236            partitions.enumerate().await.expect("Failed to enumerate partitions");
237        let connector = find_block_device(
238            &[BlockDeviceMatcher::Name(&device_label)],
239            service_instances.into_iter(),
240        )
241        .await
242        .context("Failed to find block device")?
243        .unwrap();
244        Ok(Box::new(connector))
245    }
246}
247
248/// Find an existing test partition using the device label and return a block connector for it.
249pub async fn find_partition(device_label: String) -> Result<Box<dyn BlockConnector>> {
250    let partitions = Service::open(fpartitions::PartitionServiceMarker).unwrap();
251    let service_instances = partitions.enumerate().await.expect("Failed to enumerate partitions");
252    let connector = find_block_device(
253        &[BlockDeviceMatcher::Name(&device_label)],
254        service_instances.into_iter(),
255    )
256    .await
257    .context("Failed to find block device")?
258    .ok_or_else(|| anyhow!("Block device not found"))?;
259    log::info!(device_label:%; "found existing partition");
260    Ok(Box::new(connector))
261}