]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/proxmox_restore_daemon/disk.rs
http proxy: improve response parser
[proxmox-backup.git] / src / bin / proxmox_restore_daemon / disk.rs
CommitLineData
d32a8652
SR
1//! Low-level disk (image) access functions for file restore VMs.
2use anyhow::{bail, format_err, Error};
3use lazy_static::lazy_static;
4use log::{info, warn};
5
6use std::collections::HashMap;
7use std::fs::{create_dir_all, File};
8use std::io::{BufRead, BufReader};
9use std::path::{Component, Path, PathBuf};
10
11use proxmox::const_regex;
12use proxmox::tools::fs;
13use proxmox_backup::api2::types::BLOCKDEVICE_NAME_REGEX;
14
15const_regex! {
16 VIRTIO_PART_REGEX = r"^vd[a-z]+(\d+)$";
17}
18
19lazy_static! {
20 static ref FS_OPT_MAP: HashMap<&'static str, &'static str> = {
21 let mut m = HashMap::new();
22
23 // otherwise ext complains about mounting read-only
24 m.insert("ext2", "noload");
25 m.insert("ext3", "noload");
26 m.insert("ext4", "noload");
27
28 // ufs2 is used as default since FreeBSD 5.0 released in 2003, so let's assume that
29 // whatever the user is trying to restore is not using anything older...
30 m.insert("ufs", "ufstype=ufs2");
31
32 m
33 };
34}
35
36pub enum ResolveResult {
37 Path(PathBuf),
38 BucketTypes(Vec<&'static str>),
39 BucketComponents(Vec<String>),
40}
41
42struct PartitionBucketData {
43 dev_node: String,
44 number: i32,
45 mountpoint: Option<PathBuf>,
46}
47
48/// A "Bucket" represents a mapping found on a disk, e.g. a partition, a zfs dataset or an LV. A
49/// uniquely identifying path to a file then consists of four components:
50/// "/disk/bucket/component/path"
51/// where
52/// disk: fidx file name
53/// bucket: bucket type
54/// component: identifier of the specific bucket
55/// path: relative path of the file on the filesystem indicated by the other parts, may contain
56/// more subdirectories
57/// e.g.: "/drive-scsi0/part/0/etc/passwd"
58enum Bucket {
59 Partition(PartitionBucketData),
60}
61
62impl Bucket {
63 fn filter_mut<'a, A: AsRef<str>, B: AsRef<str>>(
64 haystack: &'a mut Vec<Bucket>,
65 ty: A,
66 comp: B,
67 ) -> Option<&'a mut Bucket> {
68 let ty = ty.as_ref();
69 let comp = comp.as_ref();
70 haystack.iter_mut().find(|b| match b {
71 Bucket::Partition(data) => ty == "part" && comp.parse::<i32>().unwrap() == data.number,
72 })
73 }
74
75 fn type_string(&self) -> &'static str {
76 match self {
77 Bucket::Partition(_) => "part",
78 }
79 }
80
81 fn component_string(&self) -> String {
82 match self {
83 Bucket::Partition(data) => data.number.to_string(),
84 }
85 }
86}
87
88/// Functions related to the local filesystem. This mostly exists so we can use 'supported_fs' in
89/// try_mount while a Bucket is still mutably borrowed from DiskState.
90struct Filesystems {
91 supported_fs: Vec<String>,
92}
93
94impl Filesystems {
95 fn scan() -> Result<Self, Error> {
96 // detect kernel supported filesystems
97 let mut supported_fs = Vec::new();
98 for f in BufReader::new(File::open("/proc/filesystems")?)
99 .lines()
100 .filter_map(Result::ok)
101 {
102 // ZFS is treated specially, don't attempt to do a regular mount with it
103 let f = f.trim();
104 if !f.starts_with("nodev") && f != "zfs" {
105 supported_fs.push(f.to_owned());
106 }
107 }
108
109 Ok(Self { supported_fs })
110 }
111
112 fn ensure_mounted(&self, bucket: &mut Bucket) -> Result<PathBuf, Error> {
113 match bucket {
114 Bucket::Partition(data) => {
115 // regular data partition à la "/dev/vdxN"
116 if let Some(mp) = &data.mountpoint {
117 return Ok(mp.clone());
118 }
119
120 let mp = format!("/mnt{}/", data.dev_node);
121 self.try_mount(&data.dev_node, &mp)?;
122 let mp = PathBuf::from(mp);
123 data.mountpoint = Some(mp.clone());
124 Ok(mp)
125 }
126 }
127 }
128
129 fn try_mount(&self, source: &str, target: &str) -> Result<(), Error> {
130 use nix::mount::*;
131
132 create_dir_all(target)?;
133
134 // try all supported fs until one works - this is the way Busybox's 'mount' does it too:
135 // https://git.busybox.net/busybox/tree/util-linux/mount.c?id=808d93c0eca49e0b22056e23d965f0d967433fbb#n2152
136 // note that ZFS is intentionally left out (see scan())
137 let flags =
138 MsFlags::MS_RDONLY | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_NODEV;
139 for fs in &self.supported_fs {
140 let fs: &str = fs.as_ref();
141 let opts = FS_OPT_MAP.get(fs).copied();
142 match mount(Some(source), target, Some(fs), flags, opts) {
143 Ok(()) => {
144 info!("mounting '{}' succeeded, fstype: '{}'", source, fs);
145 return Ok(());
146 }
147 Err(err) => {
148 warn!("mount error on '{}' ({}) - {}", source, fs, err);
149 }
150 }
151 }
152
153 bail!("all mounts failed or no supported file system")
154 }
155}
156
157pub struct DiskState {
158 filesystems: Filesystems,
159 disk_map: HashMap<String, Vec<Bucket>>,
160}
161
162impl DiskState {
163 /// Scan all disks for supported buckets.
164 pub fn scan() -> Result<Self, Error> {
165 // create mapping for virtio drives and .fidx files (via serial description)
166 // note: disks::DiskManager relies on udev, which we don't have
167 let mut disk_map = HashMap::new();
168 for entry in proxmox_backup::tools::fs::scan_subdir(
169 libc::AT_FDCWD,
170 "/sys/block",
171 &BLOCKDEVICE_NAME_REGEX,
172 )?
173 .filter_map(Result::ok)
174 {
175 let name = unsafe { entry.file_name_utf8_unchecked() };
176 if !name.starts_with("vd") {
177 continue;
178 }
179
180 let sys_path: &str = &format!("/sys/block/{}", name);
181
182 let serial = fs::file_read_string(&format!("{}/serial", sys_path));
183 let fidx = match serial {
184 Ok(serial) => serial,
185 Err(err) => {
186 warn!("disk '{}': could not read serial file - {}", name, err);
187 continue;
188 }
189 };
190
191 let mut parts = Vec::new();
192 for entry in proxmox_backup::tools::fs::scan_subdir(
193 libc::AT_FDCWD,
194 sys_path,
195 &VIRTIO_PART_REGEX,
196 )?
197 .filter_map(Result::ok)
198 {
199 let part_name = unsafe { entry.file_name_utf8_unchecked() };
200 let devnode = format!("/dev/{}", part_name);
201 let part_path = format!("/sys/block/{}/{}", name, part_name);
202
203 // create partition device node for further use
204 let dev_num_str = fs::file_read_firstline(&format!("{}/dev", part_path))?;
205 let (major, minor) = dev_num_str.split_at(dev_num_str.find(':').unwrap());
206 Self::mknod_blk(&devnode, major.parse()?, minor[1..].trim_end().parse()?)?;
207
208 let number = fs::file_read_firstline(&format!("{}/partition", part_path))?
209 .trim()
210 .parse::<i32>()?;
211
212 info!(
213 "drive '{}' ('{}'): found partition '{}' ({})",
214 name, fidx, devnode, number
215 );
216
217 let bucket = Bucket::Partition(PartitionBucketData {
218 dev_node: devnode,
219 mountpoint: None,
220 number,
221 });
222
223 parts.push(bucket);
224 }
225
226 disk_map.insert(fidx.to_owned(), parts);
227 }
228
229 Ok(Self {
230 filesystems: Filesystems::scan()?,
231 disk_map,
232 })
233 }
234
235 /// Given a path like "/drive-scsi0.img.fidx/part/0/etc/passwd", this will mount the first
236 /// partition of 'drive-scsi0' on-demand (i.e. if not already mounted) and return a path
237 /// pointing to the requested file locally, e.g. "/mnt/vda1/etc/passwd", which can be used to
238 /// read the file. Given a partial path, i.e. only "/drive-scsi0.img.fidx" or
239 /// "/drive-scsi0.img.fidx/part", it will return a list of available bucket types or bucket
240 /// components respectively
241 pub fn resolve(&mut self, path: &Path) -> Result<ResolveResult, Error> {
242 let mut cmp = path.components().peekable();
243 match cmp.peek() {
244 Some(Component::RootDir) | Some(Component::CurDir) => {
245 cmp.next();
246 }
247 None => bail!("empty path cannot be resolved to file location"),
248 _ => {}
249 }
250
251 let req_fidx = match cmp.next() {
252 Some(Component::Normal(x)) => x.to_string_lossy(),
253 _ => bail!("no or invalid image in path"),
254 };
255
256 let buckets = match self.disk_map.get_mut(req_fidx.as_ref()) {
257 Some(x) => x,
258 None => bail!("given image '{}' not found", req_fidx),
259 };
260
261 let bucket_type = match cmp.next() {
262 Some(Component::Normal(x)) => x.to_string_lossy(),
263 Some(c) => bail!("invalid bucket in path: {:?}", c),
264 None => {
265 // list bucket types available
266 let mut types = buckets
267 .iter()
268 .map(|b| b.type_string())
269 .collect::<Vec<&'static str>>();
270 // dedup requires duplicates to be consecutive, which is the case - see scan()
271 types.dedup();
272 return Ok(ResolveResult::BucketTypes(types));
273 }
274 };
275
276 let component = match cmp.next() {
277 Some(Component::Normal(x)) => x.to_string_lossy(),
278 Some(c) => bail!("invalid bucket component in path: {:?}", c),
279 None => {
280 // list bucket components available
281 let comps = buckets
282 .iter()
283 .filter(|b| b.type_string() == bucket_type)
284 .map(Bucket::component_string)
285 .collect();
286 return Ok(ResolveResult::BucketComponents(comps));
287 }
288 };
289
290 let mut bucket = match Bucket::filter_mut(buckets, &bucket_type, &component) {
291 Some(bucket) => bucket,
292 None => bail!(
293 "bucket/component path not found: {}/{}/{}",
294 req_fidx,
295 bucket_type,
296 component
297 ),
298 };
299
300 // bucket found, check mount
301 let mountpoint = self
302 .filesystems
303 .ensure_mounted(&mut bucket)
304 .map_err(|err| {
305 format_err!(
306 "mounting '{}/{}/{}' failed: {}",
307 req_fidx,
308 bucket_type,
309 component,
310 err
311 )
312 })?;
313
314 let mut local_path = PathBuf::new();
315 local_path.push(mountpoint);
316 for rem in cmp {
317 local_path.push(rem);
318 }
319
320 Ok(ResolveResult::Path(local_path))
321 }
322
323 fn mknod_blk(path: &str, maj: u64, min: u64) -> Result<(), Error> {
324 use nix::sys::stat;
325 let dev = stat::makedev(maj, min);
326 stat::mknod(path, stat::SFlag::S_IFBLK, stat::Mode::S_IRWXU, dev)?;
327 Ok(())
328 }
329}