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