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