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