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