1 use std
::collections
::HashMap
;
3 use std
::hash
::BuildHasher
;
4 use std
::os
::unix
::io
::AsRawFd
;
5 use std
::path
::{Path, PathBuf}
;
8 use anyhow
::{bail, format_err, Error}
;
9 use futures
::future
::FutureExt
;
11 use futures
::stream
::{StreamExt, TryStreamExt}
;
12 use nix
::unistd
::{fork, ForkResult}
;
13 use serde_json
::Value
;
14 use tokio
::signal
::unix
::{signal, SignalKind}
;
16 use proxmox_sys
::sortable
;
17 use proxmox_sys
::fd
::Fd
;
18 use proxmox_router
::{ApiHandler, ApiMethod, RpcEnvironment, cli::*}
;
19 use proxmox_schema
::*;
21 use pbs_tools
::crypt_config
::CryptConfig
;
22 use pbs_config
::key_config
::load_and_decrypt_key
;
23 use pbs_datastore
::{BackupDir, BackupGroup, }
;
24 use pbs_datastore
::index
::IndexFile
;
25 use pbs_datastore
::dynamic_index
::BufferedDynamicReader
;
26 use pbs_datastore
::cached_chunk_reader
::CachedChunkReader
;
27 use pbs_client
::tools
::key_source
::get_encryption_key_password
;
28 use pbs_client
::{BackupReader, RemoteChunkReader}
;
29 use pbs_tools
::json
::required_string_param
;
33 extract_repository_from_value
,
34 complete_pxar_archive_name
,
35 complete_img_archive_name
,
36 complete_group_or_snapshot
,
40 api_datastore_latest_snapshot
,
41 BufferedDynamicReadAt
,
45 const API_METHOD_MOUNT
: ApiMethod
= ApiMethod
::new(
46 &ApiHandler
::Sync(&mount
),
48 "Mount pxar archive.",
50 ("snapshot", false, &StringSchema
::new("Group/Snapshot path.").schema()),
51 ("archive-name", false, &StringSchema
::new("Backup archive name.").schema()),
52 ("target", false, &StringSchema
::new("Target directory path.").schema()),
53 ("repository", true, &REPO_URL_SCHEMA
),
54 ("keyfile", true, &StringSchema
::new("Path to encryption key.").schema()),
55 ("verbose", true, &BooleanSchema
::new("Verbose output and stay in foreground.").default(false).schema()),
61 const API_METHOD_MAP
: ApiMethod
= ApiMethod
::new(
62 &ApiHandler
::Sync(&mount
),
64 "Map a drive image from a VM backup to a local loopback device. Use 'unmap' to undo.
65 WARNING: Only do this with *trusted* backups!",
67 ("snapshot", false, &StringSchema
::new("Group/Snapshot path.").schema()),
68 ("archive-name", false, &StringSchema
::new("Backup archive name.").schema()),
69 ("repository", true, &REPO_URL_SCHEMA
),
70 ("keyfile", true, &StringSchema
::new("Path to encryption key.").schema()),
71 ("verbose", true, &BooleanSchema
::new("Verbose output and stay in foreground.").default(false).schema()),
77 const API_METHOD_UNMAP
: ApiMethod
= ApiMethod
::new(
78 &ApiHandler
::Sync(&unmap
),
80 "Unmap a loop device mapped with 'map' and release all resources.",
82 ("name", true, &StringSchema
::new(
83 concat
!("Archive name, path to loopdev (/dev/loopX) or loop device number. ",
84 "Omit to list all current mappings and force cleaning up leftover instances.")
90 pub fn mount_cmd_def() -> CliCommand
{
92 CliCommand
::new(&API_METHOD_MOUNT
)
93 .arg_param(&["snapshot", "archive-name", "target"])
94 .completion_cb("repository", complete_repository
)
95 .completion_cb("snapshot", complete_group_or_snapshot
)
96 .completion_cb("archive-name", complete_pxar_archive_name
)
97 .completion_cb("target", complete_file_name
)
100 pub fn map_cmd_def() -> CliCommand
{
102 CliCommand
::new(&API_METHOD_MAP
)
103 .arg_param(&["snapshot", "archive-name"])
104 .completion_cb("repository", complete_repository
)
105 .completion_cb("snapshot", complete_group_or_snapshot
)
106 .completion_cb("archive-name", complete_img_archive_name
)
109 pub fn unmap_cmd_def() -> CliCommand
{
111 CliCommand
::new(&API_METHOD_UNMAP
)
112 .arg_param(&["name"])
113 .completion_cb("name", complete_mapping_names
)
116 fn complete_mapping_names
<S
: BuildHasher
>(_arg
: &str, _param
: &HashMap
<String
, String
, S
>)
119 match pbs_fuse_loop
::find_all_mappings() {
120 Ok(mappings
) => mappings
121 .filter_map(|(name
, _
)| {
122 proxmox_sys
::systemd
::unescape_unit(&name
).ok()
131 _rpcenv
: &mut dyn RpcEnvironment
,
132 ) -> Result
<Value
, Error
> {
134 let verbose
= param
["verbose"].as_bool().unwrap_or(false);
136 // This will stay in foreground with debug output enabled as None is
137 // passed for the RawFd.
138 return proxmox_async
::runtime
::main(mount_do(param
, None
));
141 // Process should be daemonized.
142 // Make sure to fork before the async runtime is instantiated to avoid troubles.
143 let (pr
, pw
) = proxmox_sys
::pipe()?
;
144 match unsafe { fork() }
{
145 Ok(ForkResult
::Parent { .. }
) => {
147 // Blocks the parent process until we are ready to go in the child
148 let _res
= nix
::unistd
::read(pr
.as_raw_fd(), &mut [0]).unwrap();
151 Ok(ForkResult
::Child
) => {
153 nix
::unistd
::setsid().unwrap();
154 proxmox_async
::runtime
::main(mount_do(param
, Some(pw
)))
156 Err(_
) => bail
!("failed to daemonize process"),
160 async
fn mount_do(param
: Value
, pipe
: Option
<Fd
>) -> Result
<Value
, Error
> {
161 let repo
= extract_repository_from_value(¶m
)?
;
162 let archive_name
= required_string_param(¶m
, "archive-name")?
;
163 let client
= connect(&repo
)?
;
165 let target
= param
["target"].as_str();
167 record_repository(&repo
);
169 let path
= required_string_param(¶m
, "snapshot")?
;
170 let (backup_type
, backup_id
, backup_time
) = if path
.matches('
/'
).count() == 1 {
171 let group
: BackupGroup
= path
.parse()?
;
172 api_datastore_latest_snapshot(&client
, repo
.store(), group
).await?
174 let snapshot
: BackupDir
= path
.parse()?
;
175 (snapshot
.group().backup_type().to_owned(), snapshot
.group().backup_id().to_owned(), snapshot
.backup_time())
178 let keyfile
= param
["keyfile"].as_str().map(PathBuf
::from
);
179 let crypt_config
= match keyfile
{
182 println
!("Encryption key file: '{:?}'", path
);
183 let (key
, _
, fingerprint
) = load_and_decrypt_key(&path
, &get_encryption_key_password
)?
;
184 println
!("Encryption key fingerprint: '{}'", fingerprint
);
185 Some(Arc
::new(CryptConfig
::new(key
)?
))
189 let server_archive_name
= if archive_name
.ends_with(".pxar") {
190 if target
.is_none() {
191 bail
!("use the 'mount' command to mount pxar archives");
193 format
!("{}.didx", archive_name
)
194 } else if archive_name
.ends_with(".img") {
195 if target
.is_some() {
196 bail
!("use the 'map' command to map drive images");
198 format
!("{}.fidx", archive_name
)
200 bail
!("Can only mount/map pxar archives and drive images.");
203 let client
= BackupReader
::start(
205 crypt_config
.clone(),
213 let (manifest
, _
) = client
.download_manifest().await?
;
214 manifest
.check_fingerprint(crypt_config
.as_ref().map(Arc
::as_ref
))?
;
216 let file_info
= manifest
.lookup_file_info(&server_archive_name
)?
;
218 let daemonize
= || -> Result
<(), Error
> {
219 if let Some(pipe
) = pipe
{
220 nix
::unistd
::chdir(Path
::new("/")).unwrap();
221 // Finish creation of daemon by redirecting filedescriptors.
222 let nullfd
= nix
::fcntl
::open(
224 nix
::fcntl
::OFlag
::O_RDWR
,
225 nix
::sys
::stat
::Mode
::empty(),
227 nix
::unistd
::dup2(nullfd
, 0).unwrap();
228 nix
::unistd
::dup2(nullfd
, 1).unwrap();
229 nix
::unistd
::dup2(nullfd
, 2).unwrap();
231 nix
::unistd
::close(nullfd
).unwrap();
233 // Signal the parent process that we are done with the setup and it can
235 nix
::unistd
::write(pipe
.as_raw_fd(), &[0u8])?
;
242 let options
= OsStr
::new("ro,default_permissions");
244 // handle SIGINT and SIGTERM
245 let mut interrupt_int
= signal(SignalKind
::interrupt())?
;
246 let mut interrupt_term
= signal(SignalKind
::terminate())?
;
248 let mut interrupt
= futures
::future
::select(interrupt_int
.recv().boxed(), interrupt_term
.recv().boxed());
250 if server_archive_name
.ends_with(".didx") {
251 let index
= client
.download_dynamic_index(&manifest
, &server_archive_name
).await?
;
252 let most_used
= index
.find_most_used_chunks(8);
253 let chunk_reader
= RemoteChunkReader
::new(client
.clone(), crypt_config
, file_info
.chunk_crypt_mode(), most_used
);
254 let reader
= BufferedDynamicReader
::new(index
, chunk_reader
);
255 let archive_size
= reader
.archive_size();
256 let reader
: pbs_client
::pxar
::fuse
::Reader
=
257 Arc
::new(BufferedDynamicReadAt
::new(reader
));
258 let decoder
= pbs_client
::pxar
::fuse
::Accessor
::new(reader
, archive_size
).await?
;
260 let session
= pbs_client
::pxar
::fuse
::Session
::mount(
264 Path
::new(target
.unwrap()),
266 .map_err(|err
| format_err
!("pxar mount failed: {}", err
))?
;
271 res
= session
.fuse() => res?
,
273 // exit on interrupted
276 } else if server_archive_name
.ends_with(".fidx") {
277 let index
= client
.download_fixed_index(&manifest
, &server_archive_name
).await?
;
278 let size
= index
.index_bytes();
279 let chunk_reader
= RemoteChunkReader
::new(client
.clone(), crypt_config
, file_info
.chunk_crypt_mode(), HashMap
::new());
280 let reader
= CachedChunkReader
::new(chunk_reader
, index
, 8).seekable();
282 let name
= &format
!("{}:{}/{}", repo
.to_string(), path
, archive_name
);
283 let name_escaped
= proxmox_sys
::systemd
::escape_unit(name
, false);
285 let mut session
= pbs_fuse_loop
::FuseLoopSession
::map_loop(size
, reader
, &name_escaped
, options
).await?
;
286 let loopdev
= session
.loopdev_path
.clone();
288 let (st_send
, st_recv
) = futures
::channel
::mpsc
::channel(1);
289 let (mut abort_send
, abort_recv
) = futures
::channel
::mpsc
::channel(1);
290 let mut st_recv
= st_recv
.fuse();
291 let mut session_fut
= session
.main(st_send
, abort_recv
).boxed().fuse();
293 // poll until loop file is mapped (or errors)
295 _res
= session_fut
=> {
296 bail
!("FUSE session unexpectedly ended before loop file mapping");
298 res
= st_recv
.try_next() => {
299 if let Err(err
) = res
{
300 // init went wrong, abort now
301 abort_send
.try_send(()).map_err(|err
|
302 format_err
!("error while sending abort signal - {}", err
))?
;
303 // ignore and keep original error cause
304 let _
= session_fut
.await
;
310 // daemonize only now to be able to print mapped loopdev or startup errors
311 println
!("Image '{}' mapped on {}", name
, loopdev
);
314 // continue polling until complete or interrupted (which also happens on unmap)
316 res
= session_fut
=> res?
,
318 // exit on interrupted
319 abort_send
.try_send(()).map_err(|err
|
320 format_err
!("error while sending abort signal - {}", err
))?
;
325 println
!("Image unmapped");
327 bail
!("unknown archive file extension (expected .pxar or .img)");
336 _rpcenv
: &mut dyn RpcEnvironment
,
337 ) -> Result
<Value
, Error
> {
339 let mut name
= match param
["name"].as_str() {
340 Some(name
) => name
.to_owned(),
342 pbs_fuse_loop
::cleanup_unused_run_files(None
);
344 for (backing
, loopdev
) in pbs_fuse_loop
::find_all_mappings()?
{
345 let name
= proxmox_sys
::systemd
::unescape_unit(&backing
)?
;
346 println
!("{}:\t{}", loopdev
.unwrap_or_else(|| "(unmapped)".to_string()), name
);
350 println
!("Nothing mapped.");
352 return Ok(Value
::Null
);
356 // allow loop device number alone
357 if let Ok(num
) = name
.parse
::<u8>() {
358 name
= format
!("/dev/loop{}", num
);
361 if name
.starts_with("/dev/loop") {
362 pbs_fuse_loop
::unmap_loopdev(name
)?
;
364 let name
= proxmox_sys
::systemd
::escape_unit(&name
, false);
365 pbs_fuse_loop
::unmap_name(name
)?
;