]> git.proxmox.com Git - proxmox-backup.git/blob - proxmox-backup-client/src/mount.rs
tree-wide: fix needless borrows
[proxmox-backup.git] / proxmox-backup-client / src / mount.rs
1 use std::collections::HashMap;
2 use std::ffi::OsStr;
3 use std::hash::BuildHasher;
4 use std::os::unix::io::AsRawFd;
5 use std::path::{Path, PathBuf};
6 use std::sync::Arc;
7
8 use anyhow::{bail, format_err, Error};
9 use futures::future::FutureExt;
10 use futures::select;
11 use futures::stream::{StreamExt, TryStreamExt};
12 use nix::unistd::{fork, ForkResult};
13 use serde_json::Value;
14 use tokio::signal::unix::{signal, SignalKind};
15
16 use proxmox_sys::sortable;
17 use proxmox_sys::fd::Fd;
18 use proxmox_router::{ApiHandler, ApiMethod, RpcEnvironment, cli::*};
19 use proxmox_schema::*;
20
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;
30
31 use crate::{
32 REPO_URL_SCHEMA,
33 extract_repository_from_value,
34 complete_pxar_archive_name,
35 complete_img_archive_name,
36 complete_group_or_snapshot,
37 complete_repository,
38 record_repository,
39 connect,
40 api_datastore_latest_snapshot,
41 BufferedDynamicReadAt,
42 };
43
44 #[sortable]
45 const API_METHOD_MOUNT: ApiMethod = ApiMethod::new(
46 &ApiHandler::Sync(&mount),
47 &ObjectSchema::new(
48 "Mount pxar archive.",
49 &sorted!([
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()),
56 ]),
57 )
58 );
59
60 #[sortable]
61 const API_METHOD_MAP: ApiMethod = ApiMethod::new(
62 &ApiHandler::Sync(&mount),
63 &ObjectSchema::new(
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!",
66 &sorted!([
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()),
72 ]),
73 )
74 );
75
76 #[sortable]
77 const API_METHOD_UNMAP: ApiMethod = ApiMethod::new(
78 &ApiHandler::Sync(&unmap),
79 &ObjectSchema::new(
80 "Unmap a loop device mapped with 'map' and release all resources.",
81 &sorted!([
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.")
85 ).schema()),
86 ]),
87 )
88 );
89
90 pub fn mount_cmd_def() -> CliCommand {
91
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)
98 }
99
100 pub fn map_cmd_def() -> CliCommand {
101
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)
107 }
108
109 pub fn unmap_cmd_def() -> CliCommand {
110
111 CliCommand::new(&API_METHOD_UNMAP)
112 .arg_param(&["name"])
113 .completion_cb("name", complete_mapping_names)
114 }
115
116 fn complete_mapping_names<S: BuildHasher>(_arg: &str, _param: &HashMap<String, String, S>)
117 -> Vec<String>
118 {
119 match pbs_fuse_loop::find_all_mappings() {
120 Ok(mappings) => mappings
121 .filter_map(|(name, _)| {
122 proxmox_sys::systemd::unescape_unit(&name).ok()
123 }).collect(),
124 Err(_) => Vec::new()
125 }
126 }
127
128 fn mount(
129 param: Value,
130 _info: &ApiMethod,
131 _rpcenv: &mut dyn RpcEnvironment,
132 ) -> Result<Value, Error> {
133
134 let verbose = param["verbose"].as_bool().unwrap_or(false);
135 if verbose {
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));
139 }
140
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 { .. }) => {
146 drop(pw);
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();
149 Ok(Value::Null)
150 }
151 Ok(ForkResult::Child) => {
152 drop(pr);
153 nix::unistd::setsid().unwrap();
154 proxmox_async::runtime::main(mount_do(param, Some(pw)))
155 }
156 Err(_) => bail!("failed to daemonize process"),
157 }
158 }
159
160 async fn mount_do(param: Value, pipe: Option<Fd>) -> Result<Value, Error> {
161 let repo = extract_repository_from_value(&param)?;
162 let archive_name = required_string_param(&param, "archive-name")?;
163 let client = connect(&repo)?;
164
165 let target = param["target"].as_str();
166
167 record_repository(&repo);
168
169 let path = required_string_param(&param, "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?
173 } else {
174 let snapshot: BackupDir = path.parse()?;
175 (snapshot.group().backup_type().to_owned(), snapshot.group().backup_id().to_owned(), snapshot.backup_time())
176 };
177
178 let keyfile = param["keyfile"].as_str().map(PathBuf::from);
179 let crypt_config = match keyfile {
180 None => None,
181 Some(path) => {
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)?))
186 }
187 };
188
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");
192 }
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");
197 }
198 format!("{}.fidx", archive_name)
199 } else {
200 bail!("Can only mount/map pxar archives and drive images.");
201 };
202
203 let client = BackupReader::start(
204 client,
205 crypt_config.clone(),
206 repo.store(),
207 &backup_type,
208 &backup_id,
209 backup_time,
210 true,
211 ).await?;
212
213 let (manifest, _) = client.download_manifest().await?;
214 manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?;
215
216 let file_info = manifest.lookup_file_info(&server_archive_name)?;
217
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(
223 "/dev/null",
224 nix::fcntl::OFlag::O_RDWR,
225 nix::sys::stat::Mode::empty(),
226 ).unwrap();
227 nix::unistd::dup2(nullfd, 0).unwrap();
228 nix::unistd::dup2(nullfd, 1).unwrap();
229 nix::unistd::dup2(nullfd, 2).unwrap();
230 if nullfd > 2 {
231 nix::unistd::close(nullfd).unwrap();
232 }
233 // Signal the parent process that we are done with the setup and it can
234 // terminate.
235 nix::unistd::write(pipe.as_raw_fd(), &[0u8])?;
236 let _: Fd = pipe;
237 }
238
239 Ok(())
240 };
241
242 let options = OsStr::new("ro,default_permissions");
243
244 // handle SIGINT and SIGTERM
245 let mut interrupt_int = signal(SignalKind::interrupt())?;
246 let mut interrupt_term = signal(SignalKind::terminate())?;
247
248 let mut interrupt = futures::future::select(interrupt_int.recv().boxed(), interrupt_term.recv().boxed());
249
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?;
259
260 let session = pbs_client::pxar::fuse::Session::mount(
261 decoder,
262 options,
263 false,
264 Path::new(target.unwrap()),
265 )
266 .map_err(|err| format_err!("pxar mount failed: {}", err))?;
267
268 daemonize()?;
269
270 select! {
271 res = session.fuse() => res?,
272 _ = interrupt => {
273 // exit on interrupted
274 }
275 }
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();
281
282 let name = &format!("{}:{}/{}", repo.to_string(), path, archive_name);
283 let name_escaped = proxmox_sys::systemd::escape_unit(name, false);
284
285 let mut session = pbs_fuse_loop::FuseLoopSession::map_loop(size, reader, &name_escaped, options).await?;
286 let loopdev = session.loopdev_path.clone();
287
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();
292
293 // poll until loop file is mapped (or errors)
294 select! {
295 _res = session_fut => {
296 bail!("FUSE session unexpectedly ended before loop file mapping");
297 },
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;
305 return Err(err);
306 }
307 }
308 }
309
310 // daemonize only now to be able to print mapped loopdev or startup errors
311 println!("Image '{}' mapped on {}", name, loopdev);
312 daemonize()?;
313
314 // continue polling until complete or interrupted (which also happens on unmap)
315 select! {
316 res = session_fut => res?,
317 _ = interrupt => {
318 // exit on interrupted
319 abort_send.try_send(()).map_err(|err|
320 format_err!("error while sending abort signal - {}", err))?;
321 session_fut.await?;
322 }
323 }
324
325 println!("Image unmapped");
326 } else {
327 bail!("unknown archive file extension (expected .pxar or .img)");
328 }
329
330 Ok(Value::Null)
331 }
332
333 fn unmap(
334 param: Value,
335 _info: &ApiMethod,
336 _rpcenv: &mut dyn RpcEnvironment,
337 ) -> Result<Value, Error> {
338
339 let mut name = match param["name"].as_str() {
340 Some(name) => name.to_owned(),
341 None => {
342 pbs_fuse_loop::cleanup_unused_run_files(None);
343 let mut any = false;
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);
347 any = true;
348 }
349 if !any {
350 println!("Nothing mapped.");
351 }
352 return Ok(Value::Null);
353 },
354 };
355
356 // allow loop device number alone
357 if let Ok(num) = name.parse::<u8>() {
358 name = format!("/dev/loop{}", num);
359 }
360
361 if name.starts_with("/dev/loop") {
362 pbs_fuse_loop::unmap_loopdev(name)?;
363 } else {
364 let name = proxmox_sys::systemd::escape_unit(&name, false);
365 pbs_fuse_loop::unmap_name(name)?;
366 }
367
368 Ok(Value::Null)
369 }