1 //! Low-level disk (image) access functions for file restore VMs.
2 use anyhow
::{bail, format_err, Error}
;
3 use lazy_static
::lazy_static
;
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}
;
11 use proxmox
::const_regex
;
12 use proxmox
::tools
::fs
;
13 use proxmox_backup
::api2
::types
::BLOCKDEVICE_NAME_REGEX
;
16 VIRTIO_PART_REGEX
= r
"^vd[a-z]+(\d+)$";
20 static ref FS_OPT_MAP
: HashMap
<&'
static str, &'
static str> = {
21 let mut m
= HashMap
::new();
23 // otherwise ext complains about mounting read-only
24 m
.insert("ext2", "noload");
25 m
.insert("ext3", "noload");
26 m
.insert("ext4", "noload");
28 m
.insert("xfs", "norecovery");
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");
38 pub enum ResolveResult
{
40 BucketTypes(Vec
<&'
static str>),
41 BucketComponents(Vec
<(String
, u64)>),
44 struct PartitionBucketData
{
47 mountpoint
: Option
<PathBuf
>,
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"
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"
62 Partition(PartitionBucketData
),
66 fn filter_mut
<'a
, A
: AsRef
<str>, B
: AsRef
<str>>(
67 haystack
: &'a
mut Vec
<Bucket
>,
70 ) -> Option
<&'a
mut Bucket
> {
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
,
78 fn type_string(&self) -> &'
static str {
80 Bucket
::Partition(_
) => "part",
84 fn component_string(&self) -> String
{
86 Bucket
::Partition(data
) => data
.number
.to_string(),
90 fn size(&self) -> u64 {
92 Bucket
::Partition(data
) => data
.size
,
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.
100 supported_fs
: Vec
<String
>,
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")?
)
109 .filter_map(Result
::ok
)
111 // ZFS is treated specially, don't attempt to do a regular mount with it
113 if !f
.starts_with("nodev") && f
!= "zfs" {
114 supported_fs
.push(f
.to_owned());
118 info
!("Supported FS: {}", supported_fs
.join(", "));
120 Ok(Self { supported_fs }
)
123 fn ensure_mounted(&self, bucket
: &mut Bucket
) -> Result
<PathBuf
, Error
> {
125 Bucket
::Partition(data
) => {
126 // regular data partition à la "/dev/vdxN"
127 if let Some(mp
) = &data
.mountpoint
{
128 return Ok(mp
.clone());
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());
140 fn try_mount(&self, source
: &str, target
: &str) -> Result
<(), Error
> {
143 create_dir_all(target
)?
;
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())
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
) {
155 info
!("mounting '{}' succeeded, fstype: '{}'", source
, fs
);
159 warn
!("mount error on '{}' ({}) - {}", source
, fs
, err
);
164 bail
!("all mounts failed or no supported file system")
168 pub struct DiskState
{
169 filesystems
: Filesystems
,
170 disk_map
: HashMap
<String
, Vec
<Bucket
>>,
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(
182 &BLOCKDEVICE_NAME_REGEX
,
184 .filter_map(Result
::ok
)
186 let name
= unsafe { entry.file_name_utf8_unchecked() }
;
187 if !name
.starts_with("vd") {
191 let sys_path
: &str = &format
!("/sys/block/{}", name
);
193 let serial
= fs
::file_read_string(&format
!("{}/serial", sys_path
));
194 let fidx
= match serial
{
195 Ok(serial
) => serial
,
197 warn
!("disk '{}': could not read serial file - {}", name
, err
);
202 let mut parts
= Vec
::new();
203 for entry
in proxmox_backup
::tools
::fs
::scan_subdir(
208 .filter_map(Result
::ok
)
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
);
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()?
)?
;
219 let number
= fs
::file_read_firstline(&format
!("{}/partition", part_path
))?
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
))?
231 "drive '{}' ('{}'): found partition '{}' ({}, {}B)",
232 name
, fidx
, devnode
, number
, size
235 let bucket
= Bucket
::Partition(PartitionBucketData
{
245 disk_map
.insert(fidx
.to_owned(), parts
);
249 filesystems
: Filesystems
::scan()?
,
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();
263 Some(Component
::RootDir
) | Some(Component
::CurDir
) => {
266 None
=> bail
!("empty path cannot be resolved to file location"),
270 let req_fidx
= match cmp
.next() {
271 Some(Component
::Normal(x
)) => x
.to_string_lossy(),
272 _
=> bail
!("no or invalid image in path"),
275 let buckets
= match self.disk_map
.get_mut(
277 .strip_suffix(".img.fidx")
278 .unwrap_or_else(|| req_fidx
.as_ref()),
281 None
=> bail
!("given image '{}' not found", req_fidx
),
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
),
288 // list bucket types available
289 let mut types
= buckets
291 .map(|b
| b
.type_string())
292 .collect
::<Vec
<&'
static str>>();
293 // dedup requires duplicates to be consecutive, which is the case - see scan()
295 return Ok(ResolveResult
::BucketTypes(types
));
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
),
303 // list bucket components available
306 .filter(|b
| b
.type_string() == bucket_type
)
307 .map(|b
| (b
.component_string(), b
.size()))
309 return Ok(ResolveResult
::BucketComponents(comps
));
313 let mut bucket
= match Bucket
::filter_mut(buckets
, &bucket_type
, &component
) {
314 Some(bucket
) => bucket
,
316 "bucket/component path not found: {}/{}/{}",
323 // bucket found, check mount
324 let mountpoint
= self
326 .ensure_mounted(&mut bucket
)
329 "mounting '{}/{}/{}' failed: {}",
337 let mut local_path
= PathBuf
::new();
338 local_path
.push(mountpoint
);
340 local_path
.push(rem
);
343 Ok(ResolveResult
::Path(local_path
))
346 fn mknod_blk(path
: &str, maj
: u64, min
: u64) -> Result
<(), Error
> {
348 let dev
= stat
::makedev(maj
, min
);
349 stat
::mknod(path
, stat
::SFlag
::S_IFBLK
, stat
::Mode
::S_IRWXU
, dev
)?
;