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 C3D7661245 for ; Mon, 17 Aug 2020 16:14:31 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 314C4B743 for ; Mon, 17 Aug 2020 16:14:01 +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) server-digest SHA256) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id BB6D3B703 for ; Mon, 17 Aug 2020 16:13:58 +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 8CD14431DD for ; Mon, 17 Aug 2020 16:13:58 +0200 (CEST) From: Stefan Reiter To: pbs-devel@lists.proxmox.com Date: Mon, 17 Aug 2020 16:13:39 +0200 Message-Id: <20200817141339.16115-3-s.reiter@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20200817141339.16115-1-s.reiter@proxmox.com> References: <20200817141339.16115-1-s.reiter@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.055 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 Subject: [pbs-devel] [RFC proxmox-backup 2/2 (nbd)] client: implement map/unmap commands for .img backups 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: Mon, 17 Aug 2020 14:14:31 -0000 Allows mapping fixed-index .img files (usually from VM backups) to be mapped to a local NBD device. This uses the nbd-async crate, which implements an NBD server with an async-trait based interface. We can very simply forward read requests to an AsyncIndexReader, write requests are ignored. Since unmapping requires some cleanup, a special 'unmap' command is added, which uses a PID file to send SIGINT to the backup-client instance started with 'map', which will handle the cleanup itself. The client code is placed in the 'mount' module, which, while admittedly a loose fit, allows reuse of the daemonizing code. Signed-off-by: Stefan Reiter --- Cargo.toml | 2 + src/bin/proxmox-backup-client.rs | 2 + src/bin/proxmox_backup_client/mount.rs | 159 +++++++++++++++++++++---- src/tools.rs | 1 + 4 files changed, 140 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 74707f24..99ebd5ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ path = "src/lib.rs" [dependencies] apt-pkg-native = "0.3.1" # custom patched version +async-trait = "0.1" base64 = "0.12" bitflags = "1.2.1" bytes = "0.5" @@ -30,6 +31,7 @@ hyper = "0.13" lazy_static = "1.4" libc = "0.2" log = "0.4" +nbd-async = "0.2" nix = "0.16" num-traits = "0.2" once_cell = "1.3.1" diff --git a/src/bin/proxmox-backup-client.rs b/src/bin/proxmox-backup-client.rs index 9a6f309d..88f9dba5 100644 --- a/src/bin/proxmox-backup-client.rs +++ b/src/bin/proxmox-backup-client.rs @@ -1960,6 +1960,8 @@ fn main() { .insert("status", status_cmd_def) .insert("key", key::cli()) .insert("mount", mount_cmd_def()) + .insert("map", map_cmd_def()) + .insert("unmap", unmap_cmd_def()) .insert("catalog", catalog_mgmt_cli()) .insert("task", task_mgmt_cli()) .insert("version", version_cmd_def) diff --git a/src/bin/proxmox_backup_client/mount.rs b/src/bin/proxmox_backup_client/mount.rs index 7646e98c..9e6aa1a0 100644 --- a/src/bin/proxmox_backup_client/mount.rs +++ b/src/bin/proxmox_backup_client/mount.rs @@ -3,9 +3,11 @@ use std::sync::Arc; use std::os::unix::io::RawFd; use std::path::Path; use std::ffi::OsStr; +use std::collections::HashMap; use anyhow::{bail, format_err, Error}; use serde_json::Value; +use tokio::io::AsyncReadExt; use tokio::signal::unix::{signal, SignalKind}; use nix::unistd::{fork, ForkResult, pipe}; use futures::select; @@ -23,6 +25,7 @@ use proxmox_backup::backup::{ BackupDir, BackupGroup, BufferedDynamicReader, + AsyncIndexReader, }; use proxmox_backup::client::*; @@ -50,7 +53,35 @@ const API_METHOD_MOUNT: ApiMethod = ApiMethod::new( ("target", false, &StringSchema::new("Target directory path.").schema()), ("repository", true, &REPO_URL_SCHEMA), ("keyfile", true, &StringSchema::new("Path to encryption key.").schema()), - ("verbose", true, &BooleanSchema::new("Verbose output.").default(false).schema()), + ("verbose", true, &BooleanSchema::new("Verbose output and stay in foreground.").default(false).schema()), + ]), + ) +); + +#[sortable] +const API_METHOD_MAP: ApiMethod = ApiMethod::new( + &ApiHandler::Sync(&mount), + &ObjectSchema::new( + "Map a drive image from a VM backup to a local NBD device. Use 'unmap' to undo. +WARNING: Only do this with *trusted* backups!", + &sorted!([ + ("snapshot", false, &StringSchema::new("Group/Snapshot path.").schema()), + ("archive-name", false, &StringSchema::new("Backup archive name.").schema()), + ("nbd-path", false, &StringSchema::new("Target NBD path (/dev/nbdX) or device number X.").schema()), + ("repository", true, &REPO_URL_SCHEMA), + ("keyfile", true, &StringSchema::new("Path to encryption key.").schema()), + ("verbose", true, &BooleanSchema::new("Verbose output and stay in foreground.").default(false).schema()), + ]), + ) +); + +#[sortable] +const API_METHOD_UNMAP: ApiMethod = ApiMethod::new( + &ApiHandler::Sync(&unmap), + &ObjectSchema::new( + "Unmap a NBD device mapped with 'map' and release all resources.", + &sorted!([ + ("nbd-path", false, &StringSchema::new("Path to NBD device (/dev/nbdX) or device number X.").schema()), ]), ) ); @@ -65,6 +96,23 @@ pub fn mount_cmd_def() -> CliCommand { .completion_cb("target", tools::complete_file_name) } +pub fn map_cmd_def() -> CliCommand { + + CliCommand::new(&API_METHOD_MAP) + .arg_param(&["snapshot", "archive-name", "nbd-path"]) + .completion_cb("repository", complete_repository) + .completion_cb("snapshot", complete_group_or_snapshot) + .completion_cb("archive-name", complete_pxar_archive_name) + .completion_cb("nbd-path", tools::complete_file_name) +} + +pub fn unmap_cmd_def() -> CliCommand { + + CliCommand::new(&API_METHOD_UNMAP) + .arg_param(&["nbd-path"]) + .completion_cb("nbd-path", tools::complete_file_name) +} + fn mount( param: Value, _info: &ApiMethod, @@ -100,9 +148,11 @@ fn mount( async fn mount_do(param: Value, pipe: Option) -> Result { let repo = extract_repository_from_value(¶m)?; let archive_name = tools::required_string_param(¶m, "archive-name")?; - let target = tools::required_string_param(¶m, "target")?; let client = connect(repo.host(), repo.user())?; + let target = param["target"].as_str(); + let nbd_path = param["nbd-path"].as_str(); + record_repository(&repo); let path = tools::required_string_param(¶m, "snapshot")?; @@ -124,9 +174,17 @@ async fn mount_do(param: Value, pipe: Option) -> Result { }; let server_archive_name = if archive_name.ends_with(".pxar") { + if let None = target { + bail!("use the 'mount' command to mount pxar archives"); + } format!("{}.didx", archive_name) + } else if archive_name.ends_with(".img") { + if let None = nbd_path { + bail!("use the 'map' command to map drive images"); + } + format!("{}.fidx", archive_name) } else { - bail!("Can only mount pxar archives."); + bail!("Can only mount/map pxar archives and drive images."); }; let client = BackupReader::start( @@ -141,27 +199,9 @@ async fn mount_do(param: Value, pipe: Option) -> Result { let (manifest, _) = client.download_manifest().await?; - let file_info = manifest.lookup_file_info(&archive_name)?; - - if server_archive_name.ends_with(".didx") { - let index = client.download_dynamic_index(&manifest, &server_archive_name).await?; - let most_used = index.find_most_used_chunks(8); - let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), most_used); - let reader = BufferedDynamicReader::new(index, chunk_reader); - let archive_size = reader.archive_size(); - let reader: proxmox_backup::pxar::fuse::Reader = - Arc::new(BufferedDynamicReadAt::new(reader)); - let decoder = proxmox_backup::pxar::fuse::Accessor::new(reader, archive_size).await?; - let options = OsStr::new("ro,default_permissions"); - - let session = proxmox_backup::pxar::fuse::Session::mount( - decoder, - &options, - false, - Path::new(target), - ) - .map_err(|err| format_err!("pxar mount failed: {}", err))?; + let file_info = manifest.lookup_file_info(&server_archive_name)?; + let daemonize = || -> Result<(), Error> { if let Some(pipe) = pipe { nix::unistd::chdir(Path::new("/")).unwrap(); // Finish creation of daemon by redirecting filedescriptors. @@ -182,6 +222,31 @@ async fn mount_do(param: Value, pipe: Option) -> Result { nix::unistd::close(pipe).unwrap(); } + Ok(()) + }; + + let options = OsStr::new("ro,default_permissions"); + + if server_archive_name.ends_with(".didx") { + let index = client.download_dynamic_index(&manifest, &server_archive_name).await?; + let most_used = index.find_most_used_chunks(8); + let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), most_used); + let reader = BufferedDynamicReader::new(index, chunk_reader); + let archive_size = reader.archive_size(); + let reader: proxmox_backup::pxar::fuse::Reader = + Arc::new(BufferedDynamicReadAt::new(reader)); + let decoder = proxmox_backup::pxar::fuse::Accessor::new(reader, archive_size).await?; + + let session = proxmox_backup::pxar::fuse::Session::mount( + decoder, + &options, + false, + Path::new(target.unwrap()), + ) + .map_err(|err| format_err!("pxar mount failed: {}", err))?; + + daemonize()?; + let mut interrupt = signal(SignalKind::interrupt())?; select! { res = session.fuse() => res?, @@ -189,9 +254,55 @@ async fn mount_do(param: Value, pipe: Option) -> Result { // exit on interrupted } } + } else if server_archive_name.ends_with(".fidx") { + tools::nbd::check_module_loaded()?; + + // we can unwrap since we fail earlier on None + let nbd_path = if let Ok(num) = nbd_path.unwrap().parse::() { + format!("/dev/nbd{}", num) + } else { + nbd_path.unwrap().to_owned() + }; + + let index = client.download_fixed_index(&manifest, &server_archive_name).await?; + let size = index.index_bytes(); + let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), HashMap::new()); + let mut reader = AsyncIndexReader::new(index, chunk_reader); + + // attempt a single read to check if the reader is configured correctly + let _ = reader.read_u8().await?; + + daemonize()?; + + let mut interrupt = signal(SignalKind::interrupt())?; + + // continue polling until complete or interrupted (which also happens on unmap) + select! { + res = tools::nbd::map(size, reader, &nbd_path).fuse() => res?, + _ = interrupt.recv().fuse() => { + // exit on interrupted + } + } } else { - bail!("unknown archive file extension (expected .pxar)"); + bail!("unknown archive file extension (expected .pxar or .img)"); } Ok(Value::Null) } + +fn unmap( + param: Value, + _info: &ApiMethod, + _rpcenv: &mut dyn RpcEnvironment, +) -> Result { + + let mut path = tools::required_string_param(¶m, "nbd-path")?.to_owned(); + + if let Ok(num) = path.parse::() { + path = format!("/dev/nbd{}", num); + } + + tools::nbd::unmap(path)?; + + Ok(Value::Null) +} diff --git a/src/tools.rs b/src/tools.rs index 8792bf0c..bb514600 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -34,6 +34,7 @@ pub mod ticket; pub mod statistics; pub mod systemd; pub mod nom; +pub mod nbd; mod wrapped_reader_stream; pub use wrapped_reader_stream::*; -- 2.20.1