]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/proxmox_backup_client/mount.rs
move remaining client tools to pbs-tools/datastore
[proxmox-backup.git] / src / bin / proxmox_backup_client / mount.rs
CommitLineData
45f9b32e 1use std::collections::HashMap;
35fe981c 2use std::ffi::OsStr;
2d7d6e61 3use std::hash::BuildHasher;
35fe981c
WB
4use std::os::unix::io::AsRawFd;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
43abba4b
DM
7
8use anyhow::{bail, format_err, Error};
43abba4b 9use futures::future::FutureExt;
35fe981c 10use futures::select;
45f9b32e 11use futures::stream::{StreamExt, TryStreamExt};
35fe981c
WB
12use nix::unistd::{fork, ForkResult};
13use serde_json::Value;
14use tokio::signal::unix::{signal, SignalKind};
43abba4b
DM
15
16use proxmox::{sortable, identity};
cc7995ac 17use proxmox::api::{ApiHandler, ApiMethod, RpcEnvironment, schema::*, cli::*};
35fe981c 18use proxmox::tools::fd::Fd;
43abba4b 19
eb5e0ae6
WB
20use pbs_datastore::{BackupDir, BackupGroup, CryptConfig, load_and_decrypt_key};
21use pbs_datastore::index::IndexFile;
22use pbs_datastore::dynamic_index::BufferedDynamicReader;
2b7f8dd5
WB
23use pbs_client::tools::key_source::get_encryption_key_password;
24use pbs_client::{BackupReader, RemoteChunkReader};
3c8c2827 25use pbs_tools::json::required_string_param;
2b7f8dd5 26
eb5e0ae6 27use proxmox_backup::backup::CachedChunkReader;
43abba4b 28
43abba4b
DM
29use crate::{
30 REPO_URL_SCHEMA,
31 extract_repository_from_value,
43abba4b 32 complete_pxar_archive_name,
2995aedf 33 complete_img_archive_name,
43abba4b
DM
34 complete_group_or_snapshot,
35 complete_repository,
36 record_repository,
37 connect,
38 api_datastore_latest_snapshot,
39 BufferedDynamicReadAt,
40};
41
42#[sortable]
43const API_METHOD_MOUNT: ApiMethod = ApiMethod::new(
44 &ApiHandler::Sync(&mount),
45 &ObjectSchema::new(
46 "Mount pxar archive.",
47 &sorted!([
48 ("snapshot", false, &StringSchema::new("Group/Snapshot path.").schema()),
49 ("archive-name", false, &StringSchema::new("Backup archive name.").schema()),
50 ("target", false, &StringSchema::new("Target directory path.").schema()),
51 ("repository", true, &REPO_URL_SCHEMA),
52 ("keyfile", true, &StringSchema::new("Path to encryption key.").schema()),
45f9b32e
SR
53 ("verbose", true, &BooleanSchema::new("Verbose output and stay in foreground.").default(false).schema()),
54 ]),
55 )
56);
57
58#[sortable]
59const API_METHOD_MAP: ApiMethod = ApiMethod::new(
60 &ApiHandler::Sync(&mount),
61 &ObjectSchema::new(
62 "Map a drive image from a VM backup to a local loopback device. Use 'unmap' to undo.
63WARNING: Only do this with *trusted* backups!",
64 &sorted!([
65 ("snapshot", false, &StringSchema::new("Group/Snapshot path.").schema()),
66 ("archive-name", false, &StringSchema::new("Backup archive name.").schema()),
67 ("repository", true, &REPO_URL_SCHEMA),
68 ("keyfile", true, &StringSchema::new("Path to encryption key.").schema()),
69 ("verbose", true, &BooleanSchema::new("Verbose output and stay in foreground.").default(false).schema()),
70 ]),
71 )
72);
73
74#[sortable]
75const API_METHOD_UNMAP: ApiMethod = ApiMethod::new(
76 &ApiHandler::Sync(&unmap),
77 &ObjectSchema::new(
78 "Unmap a loop device mapped with 'map' and release all resources.",
79 &sorted!([
2d7d6e61 80 ("name", true, &StringSchema::new(
2deee0e0
SR
81 concat!("Archive name, path to loopdev (/dev/loopX) or loop device number. ",
82 "Omit to list all current mappings and force cleaning up leftover instances.")
2d7d6e61 83 ).schema()),
43abba4b
DM
84 ]),
85 )
86);
87
88pub fn mount_cmd_def() -> CliCommand {
89
90 CliCommand::new(&API_METHOD_MOUNT)
91 .arg_param(&["snapshot", "archive-name", "target"])
92 .completion_cb("repository", complete_repository)
93 .completion_cb("snapshot", complete_group_or_snapshot)
94 .completion_cb("archive-name", complete_pxar_archive_name)
2b7f8dd5 95 .completion_cb("target", pbs_tools::fs::complete_file_name)
43abba4b
DM
96}
97
45f9b32e
SR
98pub fn map_cmd_def() -> CliCommand {
99
100 CliCommand::new(&API_METHOD_MAP)
101 .arg_param(&["snapshot", "archive-name"])
102 .completion_cb("repository", complete_repository)
103 .completion_cb("snapshot", complete_group_or_snapshot)
2995aedf 104 .completion_cb("archive-name", complete_img_archive_name)
45f9b32e
SR
105}
106
107pub fn unmap_cmd_def() -> CliCommand {
108
109 CliCommand::new(&API_METHOD_UNMAP)
2d7d6e61
SR
110 .arg_param(&["name"])
111 .completion_cb("name", complete_mapping_names)
112}
113
114fn complete_mapping_names<S: BuildHasher>(_arg: &str, _param: &HashMap<String, String, S>)
115 -> Vec<String>
116{
eb5e0ae6 117 match pbs_fuse_loop::find_all_mappings() {
2d7d6e61
SR
118 Ok(mappings) => mappings
119 .filter_map(|(name, _)| {
eb5e0ae6 120 pbs_systemd::unescape_unit(&name).ok()
2d7d6e61
SR
121 }).collect(),
122 Err(_) => Vec::new()
123 }
45f9b32e
SR
124}
125
43abba4b
DM
126fn mount(
127 param: Value,
128 _info: &ApiMethod,
129 _rpcenv: &mut dyn RpcEnvironment,
130) -> Result<Value, Error> {
131
132 let verbose = param["verbose"].as_bool().unwrap_or(false);
133 if verbose {
134 // This will stay in foreground with debug output enabled as None is
135 // passed for the RawFd.
d420962f 136 return pbs_runtime::main(mount_do(param, None));
43abba4b
DM
137 }
138
d1d74c43 139 // Process should be daemonized.
43abba4b 140 // Make sure to fork before the async runtime is instantiated to avoid troubles.
eb5e0ae6 141 let (pr, pw) = pbs_tools::io::pipe()?;
0c4c6a7b 142 match unsafe { fork() } {
43abba4b 143 Ok(ForkResult::Parent { .. }) => {
35fe981c 144 drop(pw);
43abba4b 145 // Blocks the parent process until we are ready to go in the child
35fe981c 146 let _res = nix::unistd::read(pr.as_raw_fd(), &mut [0]).unwrap();
43abba4b
DM
147 Ok(Value::Null)
148 }
149 Ok(ForkResult::Child) => {
35fe981c 150 drop(pr);
43abba4b 151 nix::unistd::setsid().unwrap();
d420962f 152 pbs_runtime::main(mount_do(param, Some(pw)))
43abba4b
DM
153 }
154 Err(_) => bail!("failed to daemonize process"),
155 }
156}
157
35fe981c 158async fn mount_do(param: Value, pipe: Option<Fd>) -> Result<Value, Error> {
43abba4b 159 let repo = extract_repository_from_value(&param)?;
3c8c2827 160 let archive_name = required_string_param(&param, "archive-name")?;
f3fde36b 161 let client = connect(&repo)?;
43abba4b 162
45f9b32e
SR
163 let target = param["target"].as_str();
164
43abba4b
DM
165 record_repository(&repo);
166
3c8c2827 167 let path = required_string_param(&param, "snapshot")?;
43abba4b
DM
168 let (backup_type, backup_id, backup_time) = if path.matches('/').count() == 1 {
169 let group: BackupGroup = path.parse()?;
170 api_datastore_latest_snapshot(&client, repo.store(), group).await?
171 } else {
172 let snapshot: BackupDir = path.parse()?;
173 (snapshot.group().backup_type().to_owned(), snapshot.group().backup_id().to_owned(), snapshot.backup_time())
174 };
175
176 let keyfile = param["keyfile"].as_str().map(PathBuf::from);
177 let crypt_config = match keyfile {
178 None => None,
179 Some(path) => {
6f2626ae 180 println!("Encryption key file: '{:?}'", path);
ff8945fd 181 let (key, _, fingerprint) = load_and_decrypt_key(&path, &get_encryption_key_password)?;
6f2626ae 182 println!("Encryption key fingerprint: '{}'", fingerprint);
43abba4b
DM
183 Some(Arc::new(CryptConfig::new(key)?))
184 }
185 };
186
187 let server_archive_name = if archive_name.ends_with(".pxar") {
3984a5fd 188 if target.is_none() {
45f9b32e
SR
189 bail!("use the 'mount' command to mount pxar archives");
190 }
43abba4b 191 format!("{}.didx", archive_name)
45f9b32e 192 } else if archive_name.ends_with(".img") {
3984a5fd 193 if target.is_some() {
45f9b32e
SR
194 bail!("use the 'map' command to map drive images");
195 }
196 format!("{}.fidx", archive_name)
43abba4b 197 } else {
45f9b32e 198 bail!("Can only mount/map pxar archives and drive images.");
43abba4b
DM
199 };
200
201 let client = BackupReader::start(
202 client,
203 crypt_config.clone(),
204 repo.store(),
205 &backup_type,
206 &backup_id,
207 backup_time,
208 true,
209 ).await?;
210
2107a5ae 211 let (manifest, _) = client.download_manifest().await?;
23f9503a 212 manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?;
43abba4b 213
871181d9 214 let file_info = manifest.lookup_file_info(&server_archive_name)?;
14f6c9cb 215
45f9b32e 216 let daemonize = || -> Result<(), Error> {
43abba4b
DM
217 if let Some(pipe) = pipe {
218 nix::unistd::chdir(Path::new("/")).unwrap();
219 // Finish creation of daemon by redirecting filedescriptors.
220 let nullfd = nix::fcntl::open(
221 "/dev/null",
222 nix::fcntl::OFlag::O_RDWR,
223 nix::sys::stat::Mode::empty(),
224 ).unwrap();
225 nix::unistd::dup2(nullfd, 0).unwrap();
226 nix::unistd::dup2(nullfd, 1).unwrap();
227 nix::unistd::dup2(nullfd, 2).unwrap();
228 if nullfd > 2 {
229 nix::unistd::close(nullfd).unwrap();
230 }
231 // Signal the parent process that we are done with the setup and it can
232 // terminate.
35fe981c
WB
233 nix::unistd::write(pipe.as_raw_fd(), &[0u8])?;
234 let _: Fd = pipe;
43abba4b
DM
235 }
236
45f9b32e
SR
237 Ok(())
238 };
239
240 let options = OsStr::new("ro,default_permissions");
241
242 // handle SIGINT and SIGTERM
243 let mut interrupt_int = signal(SignalKind::interrupt())?;
244 let mut interrupt_term = signal(SignalKind::terminate())?;
b5a202ac
FG
245
246 let mut interrupt = futures::future::select(interrupt_int.recv().boxed(), interrupt_term.recv().boxed());
45f9b32e
SR
247
248 if server_archive_name.ends_with(".didx") {
249 let index = client.download_dynamic_index(&manifest, &server_archive_name).await?;
250 let most_used = index.find_most_used_chunks(8);
251 let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), most_used);
252 let reader = BufferedDynamicReader::new(index, chunk_reader);
253 let archive_size = reader.archive_size();
2b7f8dd5 254 let reader: pbs_client::pxar::fuse::Reader =
45f9b32e 255 Arc::new(BufferedDynamicReadAt::new(reader));
2b7f8dd5 256 let decoder = pbs_client::pxar::fuse::Accessor::new(reader, archive_size).await?;
45f9b32e 257
2b7f8dd5 258 let session = pbs_client::pxar::fuse::Session::mount(
45f9b32e
SR
259 decoder,
260 &options,
261 false,
262 Path::new(target.unwrap()),
263 )
264 .map_err(|err| format_err!("pxar mount failed: {}", err))?;
265
266 daemonize()?;
1d0b662b 267
43abba4b
DM
268 select! {
269 res = session.fuse() => res?,
1d0b662b 270 _ = interrupt => {
43abba4b
DM
271 // exit on interrupted
272 }
273 }
45f9b32e
SR
274 } else if server_archive_name.ends_with(".fidx") {
275 let index = client.download_fixed_index(&manifest, &server_archive_name).await?;
276 let size = index.index_bytes();
277 let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), HashMap::new());
1ef6e8b6 278 let reader = CachedChunkReader::new(chunk_reader, index, 8).seekable();
45f9b32e 279
2d7d6e61 280 let name = &format!("{}:{}/{}", repo.to_string(), path, archive_name);
eb5e0ae6 281 let name_escaped = pbs_systemd::escape_unit(name, false);
2d7d6e61 282
eb5e0ae6 283 let mut session = pbs_fuse_loop::FuseLoopSession::map_loop(size, reader, &name_escaped, options).await?;
45f9b32e
SR
284 let loopdev = session.loopdev_path.clone();
285
286 let (st_send, st_recv) = futures::channel::mpsc::channel(1);
287 let (mut abort_send, abort_recv) = futures::channel::mpsc::channel(1);
288 let mut st_recv = st_recv.fuse();
289 let mut session_fut = session.main(st_send, abort_recv).boxed().fuse();
290
291 // poll until loop file is mapped (or errors)
292 select! {
0bfcea6a 293 _res = session_fut => {
45f9b32e
SR
294 bail!("FUSE session unexpectedly ended before loop file mapping");
295 },
296 res = st_recv.try_next() => {
297 if let Err(err) = res {
298 // init went wrong, abort now
299 abort_send.try_send(()).map_err(|err|
300 format_err!("error while sending abort signal - {}", err))?;
301 // ignore and keep original error cause
302 let _ = session_fut.await;
303 return Err(err);
304 }
305 }
306 }
307
308 // daemonize only now to be able to print mapped loopdev or startup errors
2d7d6e61 309 println!("Image '{}' mapped on {}", name, loopdev);
45f9b32e
SR
310 daemonize()?;
311
312 // continue polling until complete or interrupted (which also happens on unmap)
313 select! {
314 res = session_fut => res?,
315 _ = interrupt => {
316 // exit on interrupted
317 abort_send.try_send(()).map_err(|err|
318 format_err!("error while sending abort signal - {}", err))?;
319 session_fut.await?;
320 }
321 }
322
323 println!("Image unmapped");
43abba4b 324 } else {
45f9b32e 325 bail!("unknown archive file extension (expected .pxar or .img)");
43abba4b
DM
326 }
327
328 Ok(Value::Null)
329}
45f9b32e
SR
330
331fn unmap(
332 param: Value,
333 _info: &ApiMethod,
334 _rpcenv: &mut dyn RpcEnvironment,
335) -> Result<Value, Error> {
336
2d7d6e61
SR
337 let mut name = match param["name"].as_str() {
338 Some(name) => name.to_owned(),
339 None => {
eb5e0ae6 340 pbs_fuse_loop::cleanup_unused_run_files(None);
2d7d6e61 341 let mut any = false;
eb5e0ae6
WB
342 for (backing, loopdev) in pbs_fuse_loop::find_all_mappings()? {
343 let name = pbs_systemd::unescape_unit(&backing)?;
4d104cd4 344 println!("{}:\t{}", loopdev.unwrap_or_else(|| "(unmapped)".to_string()), name);
2d7d6e61
SR
345 any = true;
346 }
347 if !any {
348 println!("Nothing mapped.");
349 }
350 return Ok(Value::Null);
351 },
352 };
45f9b32e 353
2d7d6e61
SR
354 // allow loop device number alone
355 if let Ok(num) = name.parse::<u8>() {
356 name = format!("/dev/loop{}", num);
45f9b32e
SR
357 }
358
2d7d6e61 359 if name.starts_with("/dev/loop") {
eb5e0ae6 360 pbs_fuse_loop::unmap_loopdev(name)?;
2d7d6e61 361 } else {
eb5e0ae6
WB
362 let name = pbs_systemd::escape_unit(&name, false);
363 pbs_fuse_loop::unmap_name(name)?;
2d7d6e61 364 }
45f9b32e
SR
365
366 Ok(Value::Null)
367}