mount/map: use names for map/unmap for easier use
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>
This commit is contained in:
parent
4ec17f7eb5
commit
2d7d6e61be
|
@ -4,6 +4,7 @@ use std::os::unix::io::RawFd;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::hash::BuildHasher;
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
@ -81,7 +82,9 @@ const API_METHOD_UNMAP: ApiMethod = ApiMethod::new(
|
||||||
&ObjectSchema::new(
|
&ObjectSchema::new(
|
||||||
"Unmap a loop device mapped with 'map' and release all resources.",
|
"Unmap a loop device mapped with 'map' and release all resources.",
|
||||||
&sorted!([
|
&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 {
|
pub fn unmap_cmd_def() -> CliCommand {
|
||||||
|
|
||||||
CliCommand::new(&API_METHOD_UNMAP)
|
CliCommand::new(&API_METHOD_UNMAP)
|
||||||
.arg_param(&["loopdev"])
|
.arg_param(&["name"])
|
||||||
.completion_cb("loopdev", tools::complete_file_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(
|
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 chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), HashMap::new());
|
||||||
let reader = AsyncIndexReader::new(index, chunk_reader);
|
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 loopdev = session.loopdev_path.clone();
|
||||||
|
|
||||||
let (st_send, st_recv) = futures::channel::mpsc::channel(1);
|
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
|
// 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()?;
|
daemonize()?;
|
||||||
|
|
||||||
// continue polling until complete or interrupted (which also happens on unmap)
|
// continue polling until complete or interrupted (which also happens on unmap)
|
||||||
|
@ -316,13 +334,33 @@ fn unmap(
|
||||||
_rpcenv: &mut dyn RpcEnvironment,
|
_rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Value, Error> {
|
) -> 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>() {
|
// allow loop device number alone
|
||||||
path = format!("/dev/loop{}", num);
|
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)
|
Ok(Value::Null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,23 +3,29 @@
|
||||||
use anyhow::{Error, format_err, bail};
|
use anyhow::{Error, format_err, bail};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::{Path, PathBuf};
|
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::SeekFrom;
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use nix::unistd::{Pid, mkstemp};
|
use nix::unistd::Pid;
|
||||||
use nix::sys::signal::{self, Signal};
|
use nix::sys::signal::{self, Signal};
|
||||||
|
|
||||||
use tokio::io::{AsyncRead, AsyncSeek, AsyncReadExt, AsyncSeekExt};
|
use tokio::io::{AsyncRead, AsyncSeek, AsyncReadExt, AsyncSeekExt};
|
||||||
use futures::stream::{StreamExt, TryStreamExt};
|
use futures::stream::{StreamExt, TryStreamExt};
|
||||||
use futures::channel::mpsc::{Sender, Receiver};
|
use futures::channel::mpsc::{Sender, Receiver};
|
||||||
|
|
||||||
use proxmox::try_block;
|
use proxmox::{try_block, const_regex};
|
||||||
use proxmox_fuse::{*, requests::FuseRequest};
|
use proxmox_fuse::{*, requests::FuseRequest};
|
||||||
use super::loopdev;
|
use super::loopdev;
|
||||||
|
use super::fs;
|
||||||
|
|
||||||
const RUN_DIR: &'static str = "/run/pbs-loopdev";
|
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.
|
/// 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
|
/// Create with map_loop, then call 'main' and poll until startup_chan reports
|
||||||
/// success. Then, daemonize or otherwise finish setup, and continue polling
|
/// 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
|
/// 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.
|
/// /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>
|
-> Result<Self, Error>
|
||||||
{
|
{
|
||||||
// attempt a single read to check if the reader is configured correctly
|
// attempt a single read to check if the reader is configured correctly
|
||||||
let _ = reader.read_u8().await?;
|
let _ = reader.read_u8().await?;
|
||||||
|
|
||||||
std::fs::create_dir_all(RUN_DIR)?;
|
std::fs::create_dir_all(RUN_DIR)?;
|
||||||
let mut base_path = PathBuf::from(RUN_DIR);
|
let mut path = PathBuf::from(RUN_DIR);
|
||||||
base_path.push("XXXXXX"); // template for mkstemp
|
path.push(name.as_ref());
|
||||||
let (_, path) = mkstemp(&base_path)?;
|
|
||||||
let mut pid_path = path.clone();
|
let mut pid_path = path.clone();
|
||||||
pid_path.set_extension("pid");
|
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 res: Result<(Fuse, String), Error> = try_block!{
|
||||||
let session = Fuse::builder("pbs-block-dev")?
|
let session = Fuse::builder("pbs-block-dev")?
|
||||||
.options_os(options)?
|
.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
|
fn get_backing_file(loopdev: &str) -> Result<String, Error> {
|
||||||
/// /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'");
|
|
||||||
}
|
|
||||||
let num = loopdev.split_at(9).1.parse::<u8>().map_err(|err|
|
let num = loopdev.split_at(9).1.parse::<u8>().map_err(|err|
|
||||||
format_err!("malformed loopdev path, does not end with valid number - {}", 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();
|
let backing_file = backing_file.trim();
|
||||||
|
|
||||||
if !backing_file.starts_with(RUN_DIR) {
|
if !backing_file.starts_with(RUN_DIR) {
|
||||||
bail!(
|
bail!(
|
||||||
"loopdev {} is in use, but not by proxmox-backup-client (mapped to '{}')",
|
"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);
|
let mut pid_path = PathBuf::from(backing_file);
|
||||||
pid_path.set_extension("pid");
|
pid_path.set_extension("pid");
|
||||||
|
|
||||||
|
@ -254,6 +270,70 @@ pub fn unmap(loopdev: String) -> Result<(), Error> {
|
||||||
Ok(())
|
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 {
|
fn minimal_stat(size: i64) -> libc::stat {
|
||||||
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
|
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
|
||||||
stat.st_mode = libc::S_IFREG;
|
stat.st_mode = libc::S_IFREG;
|
||||||
|
|
Loading…
Reference in New Issue