From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 944DF60486 for ; Wed, 7 Oct 2020 13:53:49 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8DDF62E308 for ; Wed, 7 Oct 2020 13:53:19 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 13F242E281 for ; Wed, 7 Oct 2020 13:53:15 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id D439045C2D for ; Wed, 7 Oct 2020 13:53:14 +0200 (CEST) From: Stefan Reiter To: pbs-devel@lists.proxmox.com Date: Wed, 7 Oct 2020 13:53:05 +0200 Message-Id: <20201007115308.6275-5-s.reiter@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201007115308.6275-1-s.reiter@proxmox.com> References: <20201007115308.6275-1-s.reiter@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.040 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [mount.rs] Subject: [pbs-devel] [PATCH proxmox-backup 4/7] mount/map: use names for map/unmap for easier use X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 07 Oct 2020 11:53:49 -0000 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 --- 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(_arg: &str, _param: &HashMap) + -> Vec +{ + 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) -> Result { 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) -> Result { } // 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 { - 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::() { - path = format!("/dev/loop{}", num); + // allow loop device number alone + if let Ok(num) = name.parse::() { + 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 FuseLoopSession { /// 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>(size: u64, mut reader: R, name: P, options: &OsStr) -> Result { // 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 FuseLoopSession { } } -/// 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 { let num = loopdev.split_at(9).1.parse::().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)>, 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>(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>(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