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