[pbs-devel] [PATCH proxmox-backup 5/5] file-restore-daemon/disk: add LVM (thin) support
Stefan Reiter
s.reiter at proxmox.com
Wed Jun 30 17:57:59 CEST 2021
Parses JSON output from 'pvs' and 'lvs' LVM utils and does two passes:
one to scan for thinpools and create a device node for their
metadata_lv, and a second to load all LVs, thin-provisioned or not.
Should support every LV-type that LVM supports, as we only parse LVM
tools and use 'vgscan --mknodes' to create device nodes for us.
Produces a two-layer BucketComponent hierarchy with VGs followed by LVs,
PVs are mapped to their respective disk node.
Signed-off-by: Stefan Reiter <s.reiter at proxmox.com>
---
src/bin/proxmox_restore_daemon/disk.rs | 189 +++++++++++++++++++++++++
1 file changed, 189 insertions(+)
diff --git a/src/bin/proxmox_restore_daemon/disk.rs b/src/bin/proxmox_restore_daemon/disk.rs
index cae62af3..42b8d496 100644
--- a/src/bin/proxmox_restore_daemon/disk.rs
+++ b/src/bin/proxmox_restore_daemon/disk.rs
@@ -62,6 +62,14 @@ struct ZFSBucketData {
size: Option<u64>,
}
+#[derive(Clone)]
+struct LVMBucketData {
+ vg_name: String,
+ lv_name: String,
+ mountpoint: Option<PathBuf>,
+ size: u64,
+}
+
/// A "Bucket" represents a mapping found on a disk, e.g. a partition, a zfs dataset or an LV. A
/// uniquely identifying path to a file then consists of four components:
/// "/disk/bucket/component/path"
@@ -77,6 +85,7 @@ enum Bucket {
Partition(PartitionBucketData),
RawFs(PartitionBucketData),
ZPool(ZFSBucketData),
+ LVM(LVMBucketData),
}
impl Bucket {
@@ -102,6 +111,13 @@ impl Bucket {
false
}
}
+ Bucket::LVM(data) => {
+ if let (Some(ref vg), Some(ref lv)) = (comp.get(0), comp.get(1)) {
+ ty == "lvm" && vg.as_ref() == &data.vg_name && lv.as_ref() == &data.lv_name
+ } else {
+ false
+ }
+ }
})
}
@@ -110,6 +126,7 @@ impl Bucket {
Bucket::Partition(_) => "part",
Bucket::RawFs(_) => "raw",
Bucket::ZPool(_) => "zpool",
+ Bucket::LVM(_) => "lvm",
}
}
@@ -127,6 +144,13 @@ impl Bucket {
Bucket::Partition(data) => data.number.to_string(),
Bucket::RawFs(_) => "raw".to_owned(),
Bucket::ZPool(data) => data.name.clone(),
+ Bucket::LVM(data) => {
+ if idx == 0 {
+ data.vg_name.clone()
+ } else {
+ data.lv_name.clone()
+ }
+ }
})
}
@@ -135,6 +159,7 @@ impl Bucket {
"part" => 1,
"raw" => 0,
"zpool" => 1,
+ "lvm" => 2,
_ => bail!("invalid bucket type for component depth: {}", type_string),
})
}
@@ -143,6 +168,13 @@ impl Bucket {
match self {
Bucket::Partition(data) | Bucket::RawFs(data) => Some(data.size),
Bucket::ZPool(data) => data.size,
+ Bucket::LVM(data) => {
+ if idx == 1 {
+ Some(data.size)
+ } else {
+ None
+ }
+ }
}
}
}
@@ -264,6 +296,21 @@ impl Filesystems {
data.size = Some(size);
}
+ let mp = PathBuf::from(mntpath);
+ data.mountpoint = Some(mp.clone());
+ Ok(mp)
+ }
+ Bucket::LVM(data) => {
+ if let Some(mp) = &data.mountpoint {
+ return Ok(mp.clone());
+ }
+
+ let mntpath = format!("/mnt/lvm/{}/{}", &data.vg_name, &data.lv_name);
+ create_dir_all(&mntpath)?;
+
+ let mapper_path = format!("/dev/mapper/{}-{}", &data.vg_name, &data.lv_name);
+ self.try_mount(&mapper_path, &mntpath)?;
+
let mp = PathBuf::from(mntpath);
data.mountpoint = Some(mp.clone());
Ok(mp)
@@ -444,12 +491,154 @@ impl DiskState {
}
}
+ Self::scan_lvm(&mut disk_map, &drive_info)?;
+
Ok(Self {
filesystems,
disk_map,
})
}
+ /// scan for LVM volumes and create device nodes for them to later mount on demand
+ fn scan_lvm(
+ disk_map: &mut HashMap<String, Vec<Bucket>>,
+ drive_info: &HashMap<String, String>,
+ ) -> Result<(), Error> {
+ // first get mapping between devices and vgs
+ let mut pv_map: HashMap<String, Vec<String>> = HashMap::new();
+ let mut cmd = Command::new("/sbin/pvs");
+ cmd.args(["-o", "pv_name,vg_name", "--reportformat", "json"].iter());
+ let result = run_command(cmd, None).unwrap();
+ let result: serde_json::Value = serde_json::from_str(&result)?;
+ if let Some(result) = result["report"][0]["pv"].as_array() {
+ for pv in result {
+ let vg_name = pv["vg_name"].as_str().unwrap();
+ if vg_name.is_empty() {
+ continue;
+ }
+ let pv_name = pv["pv_name"].as_str().unwrap();
+ // remove '/dev/' part
+ let pv_name = &pv_name[pv_name.rfind('/').map(|i| i + 1).unwrap_or(0)..];
+ if let Some(fidx) = drive_info.get(pv_name) {
+ info!("LVM: found VG '{}' on '{}' ({})", vg_name, pv_name, fidx);
+ match pv_map.get_mut(vg_name) {
+ Some(list) => list.push(fidx.to_owned()),
+ None => {
+ pv_map.insert(vg_name.to_owned(), vec![fidx.to_owned()]);
+ }
+ }
+ }
+ }
+ }
+
+ let mknodes = || {
+ let mut cmd = Command::new("/sbin/vgscan");
+ cmd.arg("--mknodes");
+ if let Err(err) = run_command(cmd, None) {
+ warn!("LVM: 'vgscan --mknodes' failed: {}", err);
+ }
+ };
+
+ // then scan for LVs and assign their buckets to the correct disks
+ let mut cmd = Command::new("/sbin/lvs");
+ cmd.args(
+ [
+ "-o",
+ "vg_name,lv_name,lv_size,metadata_lv",
+ "--units",
+ "B",
+ "--reportformat",
+ "json",
+ ]
+ .iter(),
+ );
+ let result = run_command(cmd, None).unwrap();
+ let result: serde_json::Value = serde_json::from_str(&result)?;
+ let mut thinpools = Vec::new();
+ if let Some(result) = result["report"][0]["lv"].as_array() {
+ // first, look for thin-pools
+ for lv in result {
+ let metadata = lv["metadata_lv"].as_str().unwrap_or_default();
+ if !metadata.is_empty() {
+ // this is a thin-pool, activate the metadata LV
+ let vg_name = lv["vg_name"].as_str().unwrap();
+ let metadata = metadata.trim_matches(&['[', ']'][..]);
+ info!("LVM: attempting to activate thinpool '{}'", metadata);
+ let mut cmd = Command::new("/sbin/lvchange");
+ cmd.args(["-ay", "-y", &format!("{}/{}", vg_name, metadata)].iter());
+ if let Err(err) = run_command(cmd, None) {
+ // not critical, will simply mean its children can't be loaded
+ warn!("LVM: activating thinpool failed: {}", err);
+ } else {
+ thinpools.push((vg_name, metadata));
+ }
+ }
+ }
+
+ // now give the metadata LVs a device node
+ mknodes();
+
+ // cannot leave the metadata LV active, otherwise child-LVs won't activate
+ for (vg_name, metadata) in thinpools {
+ let mut cmd = Command::new("/sbin/lvchange");
+ cmd.args(["-an", "-y", &format!("{}/{}", vg_name, metadata)].iter());
+ let _ = run_command(cmd, None);
+ }
+
+ for lv in result {
+ let lv_name = lv["lv_name"].as_str().unwrap();
+ let vg_name = lv["vg_name"].as_str().unwrap();
+ let metadata = lv["metadata_lv"].as_str().unwrap_or_default();
+ if lv_name.is_empty() || vg_name.is_empty() || !metadata.is_empty() {
+ continue;
+ }
+ let lv_size = lv["lv_size"].as_str().unwrap();
+ // lv_size is in bytes with a capital 'B' at the end
+ let lv_size = lv_size[..lv_size.len() - 1].parse::<u64>().unwrap_or(0);
+
+ let bucket = Bucket::LVM(LVMBucketData {
+ vg_name: vg_name.to_owned(),
+ lv_name: lv_name.to_owned(),
+ size: lv_size,
+ mountpoint: None,
+ });
+
+ // activate the LV so 'vgscan' can create a node later - this may fail, and if it
+ // does, we ignore it and continue
+ let mut cmd = Command::new("/sbin/lvchange");
+ cmd.args(["-ay", &format!("{}/{}", vg_name, lv_name)].iter());
+ if let Err(err) = run_command(cmd, None) {
+ warn!(
+ "LVM: LV '{}' on '{}' ({}B) failed to activate: {}",
+ lv_name, vg_name, lv_size, err
+ );
+ continue;
+ }
+
+ info!(
+ "LVM: found LV '{}' on '{}' ({}B)",
+ lv_name, vg_name, lv_size
+ );
+
+ if let Some(drives) = pv_map.get(vg_name) {
+ for fidx in drives {
+ match disk_map.get_mut(fidx) {
+ Some(v) => v.push(bucket.clone()),
+ None => {
+ disk_map.insert(fidx.to_owned(), vec![bucket.clone()]);
+ }
+ }
+ }
+ }
+ }
+
+ // now that we've imported and activated all LV's, we let vgscan create the dev nodes
+ mknodes();
+ }
+
+ Ok(())
+ }
+
/// Given a path like "/drive-scsi0.img.fidx/part/0/etc/passwd", this will mount the first
/// partition of 'drive-scsi0' on-demand (i.e. if not already mounted) and return a path
/// pointing to the requested file locally, e.g. "/mnt/vda1/etc/passwd", which can be used to
--
2.30.2
More information about the pbs-devel
mailing list