]>
Commit | Line | Data |
---|---|---|
d32a8652 SR |
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 | // 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 | ||
36 | pub enum ResolveResult { | |
37 | Path(PathBuf), | |
38 | BucketTypes(Vec<&'static str>), | |
39 | BucketComponents(Vec<String>), | |
40 | } | |
41 | ||
42 | struct 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" | |
58 | enum Bucket { | |
59 | Partition(PartitionBucketData), | |
60 | } | |
61 | ||
62 | impl 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. | |
90 | struct Filesystems { | |
91 | supported_fs: Vec<String>, | |
92 | } | |
93 | ||
94 | impl 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 | ||
157 | pub struct DiskState { | |
158 | filesystems: Filesystems, | |
159 | disk_map: HashMap<String, Vec<Bucket>>, | |
160 | } | |
161 | ||
162 | impl 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 | } |