[pbs-devel] [PATCH proxmox-backup 4/7] mount/map: use names for map/unmap for easier use

Stefan Reiter s.reiter at proxmox.com
Wed Oct 7 13:53:05 CEST 2020


So user doesn't need to remember which loop devices he has mapped to
what.

systemd unit encoding is used to transform a unique identifier for the
mapped image into a suitable name. The files created in /run/pbs-loopdev
will be named accordingly.

The encoding all happens outside fuse_loop.rs, so the fuse_loop module
does not need to care about encodings - it can always assume a name is a
valid filename.

'unmap' without parameter displays all current mappings. It's
autocompletion handler will list the names of all currently mapped
images for easy selection. Unmap by /dev/loopX or loopdev number is
maintained, as those can be distinguished from mapping names.

Signed-off-by: Stefan Reiter <s.reiter at proxmox.com>
---
 src/bin/proxmox_backup_client/mount.rs |  56 ++++++++++---
 src/tools/fuse_loop.rs                 | 106 ++++++++++++++++++++++---
 2 files changed, 140 insertions(+), 22 deletions(-)

diff --git a/src/bin/proxmox_backup_client/mount.rs b/src/bin/proxmox_backup_client/mount.rs
index 4cadec55..ad06cba4 100644
--- a/src/bin/proxmox_backup_client/mount.rs
+++ b/src/bin/proxmox_backup_client/mount.rs
@@ -4,6 +4,7 @@ use std::os::unix::io::RawFd;
 use std::path::Path;
 use std::ffi::OsStr;
 use std::collections::HashMap;
+use std::hash::BuildHasher;
 
 use anyhow::{bail, format_err, Error};
 use serde_json::Value;
@@ -81,7 +82,9 @@ const API_METHOD_UNMAP: ApiMethod = ApiMethod::new(
     &ObjectSchema::new(
         "Unmap a loop device mapped with 'map' and release all resources.",
         &sorted!([
-            ("loopdev", false, &StringSchema::new("Path to loopdev (/dev/loopX) or loop device number.").schema()),
+            ("name", true, &StringSchema::new(
+                "Archive name, path to loopdev (/dev/loopX) or loop device number. Omit to list all current mappings."
+            ).schema()),
         ]),
     )
 );
@@ -108,8 +111,20 @@ pub fn map_cmd_def() -> CliCommand {
 pub fn unmap_cmd_def() -> CliCommand {
 
     CliCommand::new(&API_METHOD_UNMAP)
-        .arg_param(&["loopdev"])
-        .completion_cb("loopdev", tools::complete_file_name)
+        .arg_param(&["name"])
+        .completion_cb("name", complete_mapping_names)
+}
+
+fn complete_mapping_names<S: BuildHasher>(_arg: &str, _param: &HashMap<String, String, S>)
+    -> Vec<String>
+{
+    match tools::fuse_loop::find_all_mappings() {
+        Ok(mappings) => mappings
+            .filter_map(|(name, _)| {
+                tools::systemd::unescape_unit(&name).ok()
+            }).collect(),
+        Err(_) => Vec::new()
+    }
 }
 
 fn mount(
@@ -262,7 +277,10 @@ async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
         let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), HashMap::new());
         let reader = AsyncIndexReader::new(index, chunk_reader);
 
-        let mut session = tools::fuse_loop::FuseLoopSession::map_loop(size, reader, options).await?;
+        let name = &format!("{}:{}/{}", repo.to_string(), path, archive_name);
+        let name_escaped = tools::systemd::escape_unit(name, false);
+
+        let mut session = tools::fuse_loop::FuseLoopSession::map_loop(size, reader, &name_escaped, options).await?;
         let loopdev = session.loopdev_path.clone();
 
         let (st_send, st_recv) = futures::channel::mpsc::channel(1);
@@ -288,7 +306,7 @@ async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
         }
 
         // daemonize only now to be able to print mapped loopdev or startup errors
-        println!("Image mapped as {}", loopdev);
+        println!("Image '{}' mapped on {}", name, loopdev);
         daemonize()?;
 
         // continue polling until complete or interrupted (which also happens on unmap)
@@ -316,13 +334,33 @@ fn unmap(
     _rpcenv: &mut dyn RpcEnvironment,
 ) -> Result<Value, Error> {
 
-    let mut path = tools::required_string_param(&param, "loopdev")?.to_owned();
+    let mut name = match param["name"].as_str() {
+        Some(name) => name.to_owned(),
+        None => {
+            let mut any = false;
+            for (backing, loopdev) in tools::fuse_loop::find_all_mappings()? {
+                let name = tools::systemd::unescape_unit(&backing)?;
+                println!("{}:\t{}", loopdev.unwrap_or("(unmapped)".to_owned()), name);
+                any = true;
+            }
+            if !any {
+                println!("Nothing mapped.");
+            }
+            return Ok(Value::Null);
+        },
+    };
 
-    if let Ok(num) = path.parse::<u8>() {
-        path = format!("/dev/loop{}", num);
+    // allow loop device number alone
+    if let Ok(num) = name.parse::<u8>() {
+        name = format!("/dev/loop{}", num);
     }
 
-    tools::fuse_loop::unmap(path)?;
+    if name.starts_with("/dev/loop") {
+        tools::fuse_loop::unmap_loopdev(name)?;
+    } else {
+        let name = tools::systemd::escape_unit(&name, false);
+        tools::fuse_loop::unmap_name(name)?;
+    }
 
     Ok(Value::Null)
 }
diff --git a/src/tools/fuse_loop.rs b/src/tools/fuse_loop.rs
index cdad0230..f0d19acc 100644
--- a/src/tools/fuse_loop.rs
+++ b/src/tools/fuse_loop.rs
@@ -3,23 +3,29 @@
 use anyhow::{Error, format_err, bail};
 use std::ffi::OsStr;
 use std::path::{Path, PathBuf};
-use std::fs::{File, remove_file, read_to_string};
+use std::fs::{File, remove_file, read_to_string, OpenOptions};
 use std::io::SeekFrom;
 use std::io::prelude::*;
+use std::collections::HashMap;
 
-use nix::unistd::{Pid, mkstemp};
+use nix::unistd::Pid;
 use nix::sys::signal::{self, Signal};
 
 use tokio::io::{AsyncRead, AsyncSeek, AsyncReadExt, AsyncSeekExt};
 use futures::stream::{StreamExt, TryStreamExt};
 use futures::channel::mpsc::{Sender, Receiver};
 
-use proxmox::try_block;
+use proxmox::{try_block, const_regex};
 use proxmox_fuse::{*, requests::FuseRequest};
 use super::loopdev;
+use super::fs;
 
 const RUN_DIR: &'static str = "/run/pbs-loopdev";
 
+const_regex! {
+    pub LOOPDEV_REGEX = r"^loop\d+$";
+}
+
 /// Represents an ongoing FUSE-session that has been mapped onto a loop device.
 /// Create with map_loop, then call 'main' and poll until startup_chan reports
 /// success. Then, daemonize or otherwise finish setup, and continue polling
@@ -37,19 +43,29 @@ impl<R: AsyncRead + AsyncSeek + Unpin> FuseLoopSession<R> {
 
     /// Prepare for mapping the given reader as a block device node at
     /// /dev/loopN. Creates a temporary file for FUSE and a PID file for unmap.
-    pub async fn map_loop(size: u64, mut reader: R, options: &OsStr)
+    pub async fn map_loop<P: AsRef<str>>(size: u64, mut reader: R, name: P, options: &OsStr)
         -> Result<Self, Error>
     {
         // attempt a single read to check if the reader is configured correctly
         let _ = reader.read_u8().await?;
 
         std::fs::create_dir_all(RUN_DIR)?;
-        let mut base_path = PathBuf::from(RUN_DIR);
-        base_path.push("XXXXXX"); // template for mkstemp
-        let (_, path) = mkstemp(&base_path)?;
+        let mut path = PathBuf::from(RUN_DIR);
+        path.push(name.as_ref());
         let mut pid_path = path.clone();
         pid_path.set_extension("pid");
 
+        match OpenOptions::new().write(true).create_new(true).open(&path) {
+            Ok(_) => { /* file created, continue on */ },
+            Err(e) => {
+                if e.kind() == std::io::ErrorKind::AlreadyExists {
+                    bail!("the given archive is already mapped, cannot map twice");
+                } else {
+                    bail!("error while creating backing file ({:?}) - {}", &path, e);
+                }
+            },
+        }
+
         let res: Result<(Fuse, String), Error> = try_block!{
             let session = Fuse::builder("pbs-block-dev")?
                 .options_os(options)?
@@ -213,12 +229,7 @@ impl<R: AsyncRead + AsyncSeek + Unpin> FuseLoopSession<R> {
     }
 }
 
-/// Try and unmap a running proxmox-backup-client instance from the given
-/// /dev/loopN device
-pub fn unmap(loopdev: String) -> Result<(), Error> {
-    if loopdev.len() < 10 || !loopdev.starts_with("/dev/loop") {
-        bail!("malformed loopdev path, must be in format '/dev/loopX'");
-    }
+fn get_backing_file(loopdev: &str) -> Result<String, Error> {
     let num = loopdev.split_at(9).1.parse::<u8>().map_err(|err|
         format_err!("malformed loopdev path, does not end with valid number - {}", err))?;
 
@@ -232,6 +243,7 @@ pub fn unmap(loopdev: String) -> Result<(), Error> {
     })?;
 
     let backing_file = backing_file.trim();
+
     if !backing_file.starts_with(RUN_DIR) {
         bail!(
             "loopdev {} is in use, but not by proxmox-backup-client (mapped to '{}')",
@@ -240,6 +252,10 @@ pub fn unmap(loopdev: String) -> Result<(), Error> {
         );
     }
 
+    Ok(backing_file.to_owned())
+}
+
+fn unmap_from_backing(backing_file: &Path) -> Result<(), Error> {
     let mut pid_path = PathBuf::from(backing_file);
     pid_path.set_extension("pid");
 
@@ -254,6 +270,70 @@ pub fn unmap(loopdev: String) -> Result<(), Error> {
     Ok(())
 }
 
+/// Returns an Iterator over a set of currently active mappings, i.e.
+/// FuseLoopSession instances. Returns ("backing-file-name", Some("/dev/loopX"))
+/// where .1 is None when a user has manually called 'losetup -d' or similar but
+/// the FUSE instance is still running.
+pub fn find_all_mappings() -> Result<impl Iterator<Item = (String, Option<String>)>, Error> {
+    // get map of all /dev/loop mappings belonging to us
+    let mut loopmap = HashMap::new();
+    for ent in fs::scan_subdir(libc::AT_FDCWD, Path::new("/dev/"), &LOOPDEV_REGEX)? {
+        match ent {
+            Ok(ent) => {
+                let loopdev = format!("/dev/{}", ent.file_name().to_string_lossy());
+                match get_backing_file(&loopdev) {
+                    Ok(file) => {
+                        // insert filename only, strip RUN_DIR/
+                        loopmap.insert(file[RUN_DIR.len()+1..].to_owned(), loopdev);
+                    },
+                    Err(_) => {},
+                }
+            },
+            Err(_) => {},
+        }
+    }
+
+    Ok(fs::read_subdir(libc::AT_FDCWD, Path::new(RUN_DIR))?
+        .filter_map(move |ent| {
+            match ent {
+                Ok(ent) => {
+                    let file = ent.file_name().to_string_lossy();
+                    if file == "." || file == ".." || file.ends_with(".pid") {
+                        None
+                    } else {
+                        let loopdev = loopmap.get(file.as_ref()).map(String::to_owned);
+                        Some((file.into_owned(), loopdev))
+                    }
+                },
+                Err(_) => None,
+            }
+        }))
+}
+
+/// Try and unmap a running proxmox-backup-client instance from the given
+/// /dev/loopN device
+pub fn unmap_loopdev<S: AsRef<str>>(loopdev: S) -> Result<(), Error> {
+    let loopdev = loopdev.as_ref();
+    if loopdev.len() < 10 || !loopdev.starts_with("/dev/loop") {
+        bail!("malformed loopdev path, must be in format '/dev/loopX'");
+    }
+
+    let backing_file = get_backing_file(loopdev)?;
+    unmap_from_backing(Path::new(&backing_file))
+}
+
+/// Try and unmap a running proxmox-backup-client instance from the given name
+pub fn unmap_name<S: AsRef<str>>(name: S) -> Result<(), Error> {
+    for (mapping, _) in find_all_mappings()? {
+        if mapping.ends_with(name.as_ref()) {
+            let mut path = PathBuf::from(RUN_DIR);
+            path.push(&mapping);
+            return unmap_from_backing(&path);
+        }
+    }
+    Err(format_err!("no mapping for name '{}' found", name.as_ref()))
+}
+
 fn minimal_stat(size: i64) -> libc::stat {
     let mut stat: libc::stat = unsafe { std::mem::zeroed() };
     stat.st_mode = libc::S_IFREG;
-- 
2.20.1






More information about the pbs-devel mailing list