From: Stefan Reiter <s.reiter@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox-backup 4/7] mount/map: use names for map/unmap for easier use
Date: Wed, 7 Oct 2020 13:53:05 +0200 [thread overview]
Message-ID: <20201007115308.6275-5-s.reiter@proxmox.com> (raw)
In-Reply-To: <20201007115308.6275-1-s.reiter@proxmox.com>
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@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(¶m, "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
next prev parent reply other threads:[~2020-10-07 11:53 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-10-07 11:53 [pbs-devel] [PATCH 0/7] fuse_loop cleanups and named mappings Stefan Reiter
2020-10-07 11:53 ` [pbs-devel] [PATCH proxmox-backup 1/7] format: fix typo in function name Stefan Reiter
2020-10-07 11:53 ` [pbs-devel] [PATCH proxmox-backup 2/7] fuse_loop: add documentation Stefan Reiter
2020-10-07 11:53 ` [pbs-devel] [PATCH proxmox-backup 3/7] loopdev: add module doc Stefan Reiter
2020-10-07 11:53 ` Stefan Reiter [this message]
2020-10-07 11:53 ` [pbs-devel] [PATCH proxmox-backup 5/7] fuse_loop: add automatic cleanup of run files and dangling instances Stefan Reiter
2020-10-07 11:53 ` [pbs-devel] [PATCH proxmox-backup 6/7] fuse_loop: wait for instance to close after killing Stefan Reiter
2020-10-07 11:53 ` [pbs-devel] [PATCH proxmox-backup 7/7] fuse_loop: handle unmap on crashed instance Stefan Reiter
2020-10-08 6:45 ` [pbs-devel] applied: [PATCH 0/7] fuse_loop cleanups and named mappings Dietmar Maurer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20201007115308.6275-5-s.reiter@proxmox.com \
--to=s.reiter@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal