Skip to main content

openvmm_helpers/
disk.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Guest disk helpers.
5
6use anyhow::Context;
7use std::path::Path;
8use vm_resource::Resource;
9use vm_resource::kind::DiskHandleKind;
10
11fn disk_open_error(path: &Path, verb: &str) -> String {
12    let mut msg = format!("{verb} '{}'", path.display());
13
14    // On windows, attempt to detect we ran under wsl by reading the WSLENV and
15    // bail out with a helpful hint that it needs to be a windows path.
16    if cfg!(windows) && std::env::var_os("WSLENV").is_some() {
17        msg += ". Linux paths are not supported when running Windows executables \
18                under WSL, make sure the path is a valid Windows path \
19                (use `wslpath -w` to convert)";
20    }
21
22    msg
23}
24
25/// Options for opening a disk file.
26#[derive(Clone, Copy)]
27pub struct OpenDiskOptions {
28    /// Open the disk as read-only.
29    pub read_only: bool,
30    /// Bypass the OS page cache for direct disk I/O.
31    pub direct: bool,
32}
33
34/// Opens the resources needed for using a disk from a file at `path`.
35///
36/// If the file ends with .vhd and is a fixed VHD1, it will be opened using
37/// the user-mode VHD parser. Otherwise, if the file ends with .vhd, the
38/// file will be opened using the kernel-mode VHD parser (Windows only).
39///
40/// If the file ends with .vhdx, the kernel-mode VHD parser is used on
41/// Windows. On Linux, the pure-Rust VHDX parser is used, with automatic
42/// parent-locator walking for differencing chains.
43pub async fn open_disk_type(
44    path: &Path,
45    options: OpenDiskOptions,
46) -> anyhow::Result<Resource<DiskHandleKind>> {
47    let read_only = options.read_only;
48    let ensure_no_direct = |ext| {
49        if options.direct {
50            anyhow::bail!("direct I/O is not supported for {ext} files");
51        };
52        Ok(())
53    };
54    Ok(match path.extension().and_then(|s| s.to_str()) {
55        Some("vhd") => {
56            let file = std::fs::OpenOptions::new()
57                .read(true)
58                .write(!read_only)
59                .open(path)
60                .with_context(|| disk_open_error(path, "failed to open"))?;
61
62            match disk_vhd1::Vhd1Disk::open_fixed(file, read_only) {
63                Ok(vhd) => {
64                    ensure_no_direct("fixed .vhd")?;
65                    Resource::new(disk_backend_resources::FixedVhd1DiskHandle(
66                        vhd.into_inner(),
67                    ))
68                }
69                Err(disk_vhd1::OpenError::NotFixed) => {
70                    #[cfg(windows)]
71                    {
72                        Resource::new(disk_vhdmp::OpenVhdmpDiskConfig(
73                            disk_vhdmp::VhdmpDisk::options()
74                                .read_only(read_only)
75                                .cached_io(!options.direct)
76                                .open(path)
77                                .with_context(|| disk_open_error(path, "failed to open"))?,
78                        ))
79                    }
80                    #[cfg(not(windows))]
81                    anyhow::bail!("non-fixed VHD not supported on Linux");
82                }
83                Err(err) => return Err(err.into()),
84            }
85        }
86        Some("vhdx") => {
87            #[cfg(windows)]
88            {
89                Resource::new(disk_vhdmp::OpenVhdmpDiskConfig(
90                    disk_vhdmp::VhdmpDisk::options()
91                        .read_only(read_only)
92                        .cached_io(!options.direct)
93                        .open(path)
94                        .with_context(|| disk_open_error(path, "failed to open"))?,
95                ))
96            }
97            #[cfg(not(windows))]
98            {
99                ensure_no_direct(".vhdx")?;
100                disklayer_vhdx::chain::open_vhdx_chain(path, read_only).await?
101            }
102        }
103        Some("iso") if !read_only => {
104            anyhow::bail!("iso file cannot be opened as read/write")
105        }
106        Some("vmgs") => {
107            ensure_no_direct(".vmgs")?;
108            // VMGS files are fixed VHD1s. Don't bother to validate the footer
109            // here; let the resource resolver do that later.
110            let file = std::fs::OpenOptions::new()
111                .read(true)
112                .write(!read_only)
113                .open(path)
114                .with_context(|| disk_open_error(path, "failed to open"))?;
115
116            Resource::new(disk_backend_resources::FixedVhd1DiskHandle(file))
117        }
118        _ => open_raw_disk(path, options, None)?,
119    })
120}
121
122/// Create and open the resources needed for using a disk from a file at `path`.
123pub fn create_disk_type(
124    path: &Path,
125    size: u64,
126    options: OpenDiskOptions,
127) -> anyhow::Result<Resource<DiskHandleKind>> {
128    Ok(match path.extension().and_then(|s| s.to_str()) {
129        Some("vhd") | Some("vmgs") => {
130            if options.direct {
131                anyhow::bail!("direct I/O is not supported for VHD files");
132            }
133            let file = std::fs::OpenOptions::new()
134                .create(true)
135                .truncate(true)
136                .read(true)
137                .write(true)
138                .open(path)
139                .with_context(|| disk_open_error(path, "failed to create"))?;
140
141            file.set_len(size)?;
142            disk_vhd1::Vhd1Disk::make_fixed(&file)?;
143            Resource::new(disk_backend_resources::FixedVhd1DiskHandle(file))
144        }
145        Some("vhdx") => {
146            anyhow::bail!("creating vhdx not supported")
147        }
148        Some("iso") => {
149            anyhow::bail!("creating iso not supported")
150        }
151        _ => open_raw_disk(path, options, Some(size))?,
152    })
153}
154
155/// Open or create a raw file or block device, returning the appropriate
156/// disk resource for the current platform.
157fn open_raw_disk(
158    path: &Path,
159    options: OpenDiskOptions,
160    size: Option<u64>,
161) -> anyhow::Result<Resource<DiskHandleKind>> {
162    if options.direct && !cfg!(target_os = "linux") {
163        anyhow::bail!("direct I/O is only supported on Linux");
164    }
165
166    let create = size.is_some();
167    let mut opts = std::fs::OpenOptions::new();
168    opts.read(true).write(!options.read_only);
169    if create {
170        opts.create(true).truncate(true);
171    }
172
173    #[cfg(target_os = "linux")]
174    if options.direct {
175        use std::os::unix::fs::OpenOptionsExt;
176        opts.custom_flags(libc::O_DIRECT);
177    }
178
179    let verb = if create {
180        "failed to create"
181    } else {
182        "failed to open"
183    };
184    let file = opts
185        .open(path)
186        .with_context(|| disk_open_error(path, verb))?;
187
188    if let Some(size) = size {
189        file.set_len(size)?;
190    }
191
192    #[cfg(target_os = "linux")]
193    {
194        Ok(Resource::new(
195            disk_backend_resources::BlockDeviceDiskHandle { file },
196        ))
197    }
198    #[cfg(not(target_os = "linux"))]
199    {
200        Ok(Resource::new(disk_backend_resources::FileDiskHandle(file)))
201    }
202}