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::{anyhow, Context as _, Result};
10use async_trait::async_trait;
11use fidl::endpoints::create_proxy;
12use fidl::HandleBased as _;
13use fidl_fuchsia_blackout_test::{ControllerRequest, ControllerRequestStream};
14use fidl_fuchsia_device::ControllerMarker;
15use fidl_fuchsia_hardware_block_volume::VolumeManagerMarker;
16use fs_management::filesystem::BlockConnector;
17use fs_management::format::DiskFormat;
18use fuchsia_component::client::{connect_to_protocol, connect_to_protocol_at_path, Service};
19use fuchsia_component::server::{ServiceFs, ServiceObj};
20use fuchsia_fs::directory::readdir;
21use futures::{future, FutureExt, StreamExt, TryFutureExt, TryStreamExt};
22use rand::rngs::StdRng;
23use rand::{distributions, Rng, SeedableRng};
24use std::pin::pin;
25use std::sync::Arc;
26use storage_isolated_driver_manager::{
27    create_random_guid, find_block_device, find_block_device_devfs, into_guid,
28    wait_for_block_device_devfs, BlockDeviceMatcher, Guid,
29};
30use {
31    fidl_fuchsia_io as fio, fidl_fuchsia_storage_partitions as fpartitions, fuchsia_async as fasync,
32};
33
34pub mod random_op;
35pub mod static_tree;
36
37/// The three steps the target-side of a blackout test needs to implement.
38#[async_trait]
39pub trait Test {
40    /// Setup the test run on the given block_device.
41    async fn setup(
42        self: Arc<Self>,
43        device_label: String,
44        device_path: Option<String>,
45        seed: u64,
46    ) -> Result<()>;
47    /// Run the test body on the given device_path.
48    async fn test(
49        self: Arc<Self>,
50        device_label: String,
51        device_path: Option<String>,
52        seed: u64,
53    ) -> Result<()>;
54    /// Verify the consistency of the filesystem on the device_path.
55    async fn verify(
56        self: Arc<Self>,
57        device_label: String,
58        device_path: Option<String>,
59        seed: u64,
60    ) -> Result<()>;
61}
62
63struct BlackoutController(ControllerRequestStream);
64
65/// A test server, which serves the fuchsia.blackout.test.Controller protocol.
66pub struct TestServer<'a, T> {
67    fs: ServiceFs<ServiceObj<'a, BlackoutController>>,
68    test: Arc<T>,
69}
70
71impl<'a, T> TestServer<'a, T>
72where
73    T: Test + 'static,
74{
75    /// Create a new test server for this test.
76    pub fn new(test: T) -> Result<TestServer<'a, T>> {
77        let mut fs = ServiceFs::new();
78        fs.dir("svc").add_fidl_service(BlackoutController);
79        fs.take_and_serve_directory_handle()?;
80
81        Ok(TestServer { fs, test: Arc::new(test) })
82    }
83
84    /// Start serving the outgoing directory. Blocks until all connections are closed.
85    pub async fn serve(self) {
86        const MAX_CONCURRENT: usize = 10_000;
87        let test = self.test;
88        self.fs
89            .for_each_concurrent(MAX_CONCURRENT, move |stream| {
90                handle_request(test.clone(), stream).unwrap_or_else(|e| log::error!("{}", e))
91            })
92            .await;
93    }
94}
95
96async fn handle_request<T: Test + 'static>(
97    test: Arc<T>,
98    BlackoutController(mut stream): BlackoutController,
99) -> Result<()> {
100    while let Some(request) = stream.try_next().await? {
101        handle_controller(test.clone(), request).await?;
102    }
103
104    Ok(())
105}
106
107async fn handle_controller<T: Test + 'static>(
108    test: Arc<T>,
109    request: ControllerRequest,
110) -> Result<()> {
111    match request {
112        ControllerRequest::Setup { responder, device_label, device_path, seed } => {
113            let res = test.setup(device_label, device_path, seed).await.map_err(|err| {
114                log::error!(err:?; "Setup failed");
115                zx::Status::INTERNAL.into_raw()
116            });
117            responder.send(res)?;
118        }
119        ControllerRequest::Test { responder, device_label, device_path, seed, duration } => {
120            let test_fut = test.test(device_label, device_path, seed).map_err(|err| {
121                log::error!(err:?; "Test failed");
122                zx::Status::INTERNAL.into_raw()
123            });
124            if duration != 0 {
125                // If a non-zero duration is provided, spawn the test and then return after that
126                // duration.
127                log::info!("starting test and replying in {} seconds...", duration);
128                let timer = pin!(fasync::Timer::new(std::time::Duration::from_secs(duration)));
129                let res = match future::select(test_fut, timer).await {
130                    future::Either::Left((res, _)) => res,
131                    future::Either::Right((_, test_fut)) => {
132                        fasync::Task::spawn(test_fut.map(|_| ())).detach();
133                        Ok(())
134                    }
135                };
136                responder.send(res)?;
137            } else {
138                // If a zero duration is provided, return once the test step is complete.
139                log::info!("starting test...");
140                responder.send(test_fut.await)?;
141            }
142        }
143        ControllerRequest::Verify { responder, device_label, device_path, seed } => {
144            let res = test.verify(device_label, device_path, seed).await.map_err(|e| {
145                // The test tries failing on purpose, so only print errors as warnings.
146                log::warn!("{:?}", e);
147                zx::Status::BAD_STATE.into_raw()
148            });
149            responder.send(res)?;
150        }
151    }
152
153    Ok(())
154}
155
156/// Generate a Vec<u8> of random bytes from a seed using a standard distribution.
157pub fn generate_content(seed: u64) -> Vec<u8> {
158    let mut rng = StdRng::seed_from_u64(seed);
159
160    let size = rng.gen_range(1..1 << 16);
161    rng.sample_iter(&distributions::Standard).take(size).collect()
162}
163
164/// Find the device in /dev/class/block that represents a given topological path. Returns the full
165/// path of the device in /dev/class/block.
166pub async fn find_dev(dev: &str) -> Result<String> {
167    let dev_class_block =
168        fuchsia_fs::directory::open_in_namespace("/dev/class/block", fio::PERM_READABLE)?;
169    for entry in readdir(&dev_class_block).await? {
170        let path = format!("/dev/class/block/{}", entry.name);
171        let proxy = connect_to_protocol_at_path::<ControllerMarker>(&path)?;
172        let topo_path = proxy.get_topological_path().await?.map_err(|s| zx::Status::from_raw(s))?;
173        log::info!("{} => {}", path, topo_path);
174        if dev == topo_path {
175            return Ok(path);
176        }
177    }
178    Err(anyhow::anyhow!("Couldn't find {} in /dev/class/block", dev))
179}
180
181/// Returns a directory proxy connected to /dev.
182pub fn dev() -> fio::DirectoryProxy {
183    fuchsia_fs::directory::open_in_namespace("/dev", fio::PERM_READABLE)
184        .expect("failed to open /dev")
185}
186
187/// This type guid is only used if the test has to create the gpt partition itself. Otherwise, only
188/// the label is used to find the partition.
189const BLACKOUT_TYPE_GUID: &Guid = &[
190    0x68, 0x45, 0x23, 0x01, 0xab, 0x89, 0xef, 0xcd, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
191];
192
193const GPT_PARTITION_SIZE: u64 = 60 * 1024 * 1024;
194
195/// Set up a partition for testing using the device label, returning a block connector for it. If
196/// the partition already exists with this label, it's used. If no existing device is found with
197/// this label, create a new gpt partition to use. If storage-host is enabled, it uses the new
198/// partition apis from fshost, if not it falls back to devfs.
199pub async fn set_up_partition(
200    device_label: String,
201    storage_host: bool,
202) -> Result<Box<dyn BlockConnector>> {
203    if !storage_host {
204        return set_up_partition_devfs(device_label).await;
205    }
206
207    let partitions = Service::open(fpartitions::PartitionServiceMarker).unwrap();
208    let manager = connect_to_protocol::<fpartitions::PartitionsManagerMarker>().unwrap();
209
210    let service_instances =
211        partitions.clone().enumerate().await.expect("Failed to enumerate partitions");
212    if let Some(connector) =
213        find_block_device(&[BlockDeviceMatcher::Name(&device_label)], service_instances.into_iter())
214            .await
215            .context("Failed to find block device")?
216    {
217        log::info!(device_label:%; "found existing partition");
218        Ok(Box::new(connector))
219    } else {
220        log::info!(device_label:%; "adding new partition to the system gpt");
221        let info =
222            manager.get_block_info().await.expect("FIDL error").expect("get_block_info failed");
223        let transaction = manager
224            .create_transaction()
225            .await
226            .expect("FIDL error")
227            .map_err(zx::Status::from_raw)
228            .expect("create_transaction failed");
229        let request = fpartitions::PartitionsManagerAddPartitionRequest {
230            transaction: Some(transaction.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap()),
231            name: Some(device_label.clone()),
232            type_guid: Some(into_guid(BLACKOUT_TYPE_GUID.clone())),
233            instance_guid: Some(into_guid(create_random_guid())),
234            num_blocks: Some(GPT_PARTITION_SIZE / info.1 as u64),
235            ..Default::default()
236        };
237        manager
238            .add_partition(request)
239            .await
240            .expect("FIDL error")
241            .map_err(zx::Status::from_raw)
242            .expect("add_partition failed");
243        manager
244            .commit_transaction(transaction)
245            .await
246            .expect("FIDL error")
247            .map_err(zx::Status::from_raw)
248            .expect("add_partition failed");
249        let service_instances =
250            partitions.enumerate().await.expect("Failed to enumerate partitions");
251        let connector = find_block_device(
252            &[BlockDeviceMatcher::Name(&device_label)],
253            service_instances.into_iter(),
254        )
255        .await
256        .context("Failed to find block device")?
257        .unwrap();
258        Ok(Box::new(connector))
259    }
260}
261
262/// Fallback logic for setting up a partition on devfs.
263/// TODO(https://fxbug.dev/394968352): remove when everything uses storage-host.
264async fn set_up_partition_devfs(device_label: String) -> Result<Box<dyn BlockConnector>> {
265    let mut partition_path = if let Ok(path) =
266        find_block_device_devfs(&[BlockDeviceMatcher::Name(&device_label)]).await
267    {
268        log::info!("found existing partition");
269        path
270    } else {
271        log::info!("finding existing gpt and adding a new partition to it");
272        let mut gpt_block_path =
273            find_block_device_devfs(&[BlockDeviceMatcher::ContentsMatch(DiskFormat::Gpt)])
274                .await
275                .context("finding gpt device failed")?;
276        gpt_block_path.push("device_controller");
277        let gpt_block_controller =
278            connect_to_protocol_at_path::<ControllerMarker>(gpt_block_path.to_str().unwrap())
279                .context("connecting to block controller")?;
280        let gpt_path = gpt_block_controller
281            .get_topological_path()
282            .await
283            .context("get_topo fidl error")?
284            .map_err(zx::Status::from_raw)
285            .context("get_topo failed")?;
286        let gpt_controller = connect_to_protocol_at_path::<ControllerMarker>(&format!(
287            "{}/gpt/device_controller",
288            gpt_path
289        ))
290        .context("connecting to gpt controller")?;
291
292        let (volume_manager, server) = create_proxy::<VolumeManagerMarker>();
293        gpt_controller
294            .connect_to_device_fidl(server.into_channel())
295            .context("connecting to gpt fidl")?;
296        let slice_size = {
297            let (status, info) = volume_manager.get_info().await.context("get_info fidl error")?;
298            zx::ok(status).context("get_info returned error")?;
299            info.unwrap().slice_size
300        };
301        let slice_count = GPT_PARTITION_SIZE / slice_size;
302        let instance_guid = into_guid(create_random_guid());
303        let status = volume_manager
304            .allocate_partition(
305                slice_count,
306                &into_guid(BLACKOUT_TYPE_GUID.clone()),
307                &instance_guid,
308                &device_label,
309                0,
310            )
311            .await
312            .context("allocating test partition fidl error")?;
313        zx::ok(status).context("allocating test partition returned error")?;
314
315        wait_for_block_device_devfs(&[
316            BlockDeviceMatcher::Name(&device_label),
317            BlockDeviceMatcher::TypeGuid(&BLACKOUT_TYPE_GUID),
318        ])
319        .await
320        .context("waiting for new gpt partition")?
321    };
322    partition_path.push("device_controller");
323    log::info!(partition_path:?; "found partition to use");
324    Ok(Box::new(
325        connect_to_protocol_at_path::<ControllerMarker>(partition_path.to_str().unwrap())
326            .context("connecting to provided path")?,
327    ))
328}
329
330/// Find an existing test partition using the device label and return a block connector for it. If
331/// storage-host is enabled, use the new partition service apis from fshost, otherwise fall back to
332/// devfs.
333pub async fn find_partition(
334    device_label: String,
335    storage_host: bool,
336) -> Result<Box<dyn BlockConnector>> {
337    if !storage_host {
338        return find_partition_devfs(device_label).await;
339    }
340
341    let partitions = Service::open(fpartitions::PartitionServiceMarker).unwrap();
342    let service_instances = partitions.enumerate().await.expect("Failed to enumerate partitions");
343    let connector = find_block_device(
344        &[BlockDeviceMatcher::Name(&device_label)],
345        service_instances.into_iter(),
346    )
347    .await
348    .context("Failed to find block device")?
349    .ok_or_else(|| anyhow!("Block device not found"))?;
350    log::info!(device_label:%; "found existing partition");
351    Ok(Box::new(connector))
352}
353
354/// Fallback logic for finding a partition on devfs.
355/// TODO(https://fxbug.dev/394968352): remove when everything uses storage-host.
356async fn find_partition_devfs(device_label: String) -> Result<Box<dyn BlockConnector>> {
357    log::info!("finding gpt");
358    let mut partition_path = find_block_device_devfs(&[BlockDeviceMatcher::Name(&device_label)])
359        .await
360        .context("finding block device")?;
361    partition_path.push("device_controller");
362    log::info!(partition_path:?; "found partition to use");
363    Ok(Box::new(
364        connect_to_protocol_at_path::<ControllerMarker>(partition_path.to_str().unwrap())
365            .context("connecting to provided path")?,
366    ))
367}