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 or
38/// .vhdx, the file will be opened using the kernel-mode VHD parser.
39pub async fn open_disk_type(
40    path: &Path,
41    options: OpenDiskOptions,
42) -> anyhow::Result<Resource<DiskHandleKind>> {
43    let read_only = options.read_only;
44    let ensure_no_direct = |ext| {
45        if options.direct {
46            anyhow::bail!("direct I/O is not supported for {ext} files");
47        };
48        Ok(())
49    };
50    Ok(match path.extension().and_then(|s| s.to_str()) {
51        Some("vhd") => {
52            let file = std::fs::OpenOptions::new()
53                .read(true)
54                .write(!read_only)
55                .open(path)
56                .with_context(|| disk_open_error(path, "failed to open"))?;
57
58            match disk_vhd1::Vhd1Disk::open_fixed(file, read_only) {
59                Ok(vhd) => {
60                    ensure_no_direct("fixed .vhd")?;
61                    Resource::new(disk_backend_resources::FixedVhd1DiskHandle(
62                        vhd.into_inner(),
63                    ))
64                }
65                Err(disk_vhd1::OpenError::NotFixed) => {
66                    #[cfg(windows)]
67                    {
68                        Resource::new(disk_vhdmp::OpenVhdmpDiskConfig(
69                            disk_vhdmp::VhdmpDisk::options()
70                                .read_only(read_only)
71                                .cached_io(!options.direct)
72                                .open(path)
73                                .with_context(|| disk_open_error(path, "failed to open"))?,
74                        ))
75                    }
76                    #[cfg(not(windows))]
77                    anyhow::bail!("non-fixed VHD not supported on Linux");
78                }
79                Err(err) => return Err(err.into()),
80            }
81        }
82        Some("vhdx") => {
83            #[cfg(windows)]
84            {
85                Resource::new(disk_vhdmp::OpenVhdmpDiskConfig(
86                    disk_vhdmp::VhdmpDisk::options()
87                        .read_only(read_only)
88                        .cached_io(!options.direct)
89                        .open(path)
90                        .with_context(|| disk_open_error(path, "failed to open"))?,
91                ))
92            }
93            #[cfg(not(windows))]
94            anyhow::bail!("VHDX not supported on Linux");
95        }
96        Some("iso") if !read_only => {
97            anyhow::bail!("iso file cannot be opened as read/write")
98        }
99        Some("vmgs") => {
100            ensure_no_direct(".vmgs")?;
101            // VMGS files are fixed VHD1s. Don't bother to validate the footer
102            // here; let the resource resolver do that later.
103            let file = std::fs::OpenOptions::new()
104                .read(true)
105                .write(!read_only)
106                .open(path)
107                .with_context(|| disk_open_error(path, "failed to open"))?;
108
109            Resource::new(disk_backend_resources::FixedVhd1DiskHandle(file))
110        }
111        _ => open_raw_disk(path, options, None)?,
112    })
113}
114
115/// Create and open the resources needed for using a disk from a file at `path`.
116pub fn create_disk_type(
117    path: &Path,
118    size: u64,
119    options: OpenDiskOptions,
120) -> anyhow::Result<Resource<DiskHandleKind>> {
121    Ok(match path.extension().and_then(|s| s.to_str()) {
122        Some("vhd") | Some("vmgs") => {
123            if options.direct {
124                anyhow::bail!("direct I/O is not supported for VHD files");
125            }
126            let file = std::fs::OpenOptions::new()
127                .create(true)
128                .truncate(true)
129                .read(true)
130                .write(true)
131                .open(path)
132                .with_context(|| disk_open_error(path, "failed to create"))?;
133
134            file.set_len(size)?;
135            disk_vhd1::Vhd1Disk::make_fixed(&file)?;
136            Resource::new(disk_backend_resources::FixedVhd1DiskHandle(file))
137        }
138        Some("vhdx") => {
139            anyhow::bail!("creating vhdx not supported")
140        }
141        Some("iso") => {
142            anyhow::bail!("creating iso not supported")
143        }
144        _ => open_raw_disk(path, options, Some(size))?,
145    })
146}
147
148/// Open or create a raw file or block device, returning the appropriate
149/// disk resource for the current platform.
150fn open_raw_disk(
151    path: &Path,
152    options: OpenDiskOptions,
153    size: Option<u64>,
154) -> anyhow::Result<Resource<DiskHandleKind>> {
155    if options.direct && !cfg!(target_os = "linux") {
156        anyhow::bail!("direct I/O is only supported on Linux");
157    }
158
159    let create = size.is_some();
160    let mut opts = std::fs::OpenOptions::new();
161    opts.read(true).write(!options.read_only);
162    if create {
163        opts.create(true).truncate(true);
164    }
165
166    #[cfg(target_os = "linux")]
167    if options.direct {
168        use std::os::unix::fs::OpenOptionsExt;
169        opts.custom_flags(libc::O_DIRECT);
170    }
171
172    let verb = if create {
173        "failed to create"
174    } else {
175        "failed to open"
176    };
177    let file = opts
178        .open(path)
179        .with_context(|| disk_open_error(path, verb))?;
180
181    if let Some(size) = size {
182        file.set_len(size)?;
183    }
184
185    #[cfg(target_os = "linux")]
186    {
187        Ok(Resource::new(
188            disk_backend_resources::BlockDeviceDiskHandle { file },
189        ))
190    }
191    #[cfg(not(target_os = "linux"))]
192    {
193        Ok(Resource::new(disk_backend_resources::FileDiskHandle(file)))
194    }
195}