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