]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/proxmox_backup_client/mount.rs
src/bin/proxmox_backup_client/mount.rs: fix img name completion
[proxmox-backup.git] / src / bin / proxmox_backup_client / mount.rs
CommitLineData
43abba4b
DM
1use std::path::PathBuf;
2use std::sync::Arc;
3use std::os::unix::io::RawFd;
4use std::path::Path;
5use std::ffi::OsStr;
45f9b32e 6use std::collections::HashMap;
43abba4b
DM
7
8use anyhow::{bail, format_err, Error};
9use serde_json::Value;
10use tokio::signal::unix::{signal, SignalKind};
11use nix::unistd::{fork, ForkResult, pipe};
12use futures::select;
13use futures::future::FutureExt;
45f9b32e 14use futures::stream::{StreamExt, TryStreamExt};
43abba4b
DM
15
16use proxmox::{sortable, identity};
cc7995ac 17use proxmox::api::{ApiHandler, ApiMethod, RpcEnvironment, schema::*, cli::*};
43abba4b
DM
18
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!([
84 ("loopdev", false, &StringSchema::new("Path to loopdev (/dev/loopX) or loop device number.").schema()),
43abba4b
DM
85 ]),
86 )
87);
88
89pub fn mount_cmd_def() -> CliCommand {
90
91 CliCommand::new(&API_METHOD_MOUNT)
92 .arg_param(&["snapshot", "archive-name", "target"])
93 .completion_cb("repository", complete_repository)
94 .completion_cb("snapshot", complete_group_or_snapshot)
95 .completion_cb("archive-name", complete_pxar_archive_name)
96 .completion_cb("target", tools::complete_file_name)
97}
98
45f9b32e
SR
99pub fn map_cmd_def() -> CliCommand {
100
101 CliCommand::new(&API_METHOD_MAP)
102 .arg_param(&["snapshot", "archive-name"])
103 .completion_cb("repository", complete_repository)
104 .completion_cb("snapshot", complete_group_or_snapshot)
2995aedf 105 .completion_cb("archive-name", complete_img_archive_name)
45f9b32e
SR
106}
107
108pub fn unmap_cmd_def() -> CliCommand {
109
110 CliCommand::new(&API_METHOD_UNMAP)
111 .arg_param(&["loopdev"])
112 .completion_cb("loopdev", tools::complete_file_name)
113}
114
43abba4b
DM
115fn mount(
116 param: Value,
117 _info: &ApiMethod,
118 _rpcenv: &mut dyn RpcEnvironment,
119) -> Result<Value, Error> {
120
121 let verbose = param["verbose"].as_bool().unwrap_or(false);
122 if verbose {
123 // This will stay in foreground with debug output enabled as None is
124 // passed for the RawFd.
125 return proxmox_backup::tools::runtime::main(mount_do(param, None));
126 }
127
128 // Process should be deamonized.
129 // Make sure to fork before the async runtime is instantiated to avoid troubles.
130 let pipe = pipe()?;
131 match fork() {
132 Ok(ForkResult::Parent { .. }) => {
133 nix::unistd::close(pipe.1).unwrap();
134 // Blocks the parent process until we are ready to go in the child
135 let _res = nix::unistd::read(pipe.0, &mut [0]).unwrap();
136 Ok(Value::Null)
137 }
138 Ok(ForkResult::Child) => {
139 nix::unistd::close(pipe.0).unwrap();
140 nix::unistd::setsid().unwrap();
141 proxmox_backup::tools::runtime::main(mount_do(param, Some(pipe.1)))
142 }
143 Err(_) => bail!("failed to daemonize process"),
144 }
145}
146
147async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
148 let repo = extract_repository_from_value(&param)?;
149 let archive_name = tools::required_string_param(&param, "archive-name")?;
ba20987a 150 let client = connect(repo.host(), repo.port(), repo.user())?;
43abba4b 151
45f9b32e
SR
152 let target = param["target"].as_str();
153
43abba4b
DM
154 record_repository(&repo);
155
156 let path = tools::required_string_param(&param, "snapshot")?;
157 let (backup_type, backup_id, backup_time) = if path.matches('/').count() == 1 {
158 let group: BackupGroup = path.parse()?;
159 api_datastore_latest_snapshot(&client, repo.store(), group).await?
160 } else {
161 let snapshot: BackupDir = path.parse()?;
162 (snapshot.group().backup_type().to_owned(), snapshot.group().backup_id().to_owned(), snapshot.backup_time())
163 };
164
165 let keyfile = param["keyfile"].as_str().map(PathBuf::from);
166 let crypt_config = match keyfile {
167 None => None,
168 Some(path) => {
9696f519 169 let (key, _) = load_and_decrypt_key(&path, &crate::key::get_encryption_key_password)?;
43abba4b
DM
170 Some(Arc::new(CryptConfig::new(key)?))
171 }
172 };
173
174 let server_archive_name = if archive_name.ends_with(".pxar") {
45f9b32e
SR
175 if let None = target {
176 bail!("use the 'mount' command to mount pxar archives");
177 }
43abba4b 178 format!("{}.didx", archive_name)
45f9b32e
SR
179 } else if archive_name.ends_with(".img") {
180 if let Some(_) = target {
181 bail!("use the 'map' command to map drive images");
182 }
183 format!("{}.fidx", archive_name)
43abba4b 184 } else {
45f9b32e 185 bail!("Can only mount/map pxar archives and drive images.");
43abba4b
DM
186 };
187
188 let client = BackupReader::start(
189 client,
190 crypt_config.clone(),
191 repo.store(),
192 &backup_type,
193 &backup_id,
194 backup_time,
195 true,
196 ).await?;
197
2107a5ae 198 let (manifest, _) = client.download_manifest().await?;
43abba4b 199
871181d9 200 let file_info = manifest.lookup_file_info(&server_archive_name)?;
14f6c9cb 201
45f9b32e 202 let daemonize = || -> Result<(), Error> {
43abba4b
DM
203 if let Some(pipe) = pipe {
204 nix::unistd::chdir(Path::new("/")).unwrap();
205 // Finish creation of daemon by redirecting filedescriptors.
206 let nullfd = nix::fcntl::open(
207 "/dev/null",
208 nix::fcntl::OFlag::O_RDWR,
209 nix::sys::stat::Mode::empty(),
210 ).unwrap();
211 nix::unistd::dup2(nullfd, 0).unwrap();
212 nix::unistd::dup2(nullfd, 1).unwrap();
213 nix::unistd::dup2(nullfd, 2).unwrap();
214 if nullfd > 2 {
215 nix::unistd::close(nullfd).unwrap();
216 }
217 // Signal the parent process that we are done with the setup and it can
218 // terminate.
219 nix::unistd::write(pipe, &[0u8])?;
220 nix::unistd::close(pipe).unwrap();
221 }
222
45f9b32e
SR
223 Ok(())
224 };
225
226 let options = OsStr::new("ro,default_permissions");
227
228 // handle SIGINT and SIGTERM
229 let mut interrupt_int = signal(SignalKind::interrupt())?;
230 let mut interrupt_term = signal(SignalKind::terminate())?;
231 let mut interrupt = futures::future::select(interrupt_int.next(), interrupt_term.next());
232
233 if server_archive_name.ends_with(".didx") {
234 let index = client.download_dynamic_index(&manifest, &server_archive_name).await?;
235 let most_used = index.find_most_used_chunks(8);
236 let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), most_used);
237 let reader = BufferedDynamicReader::new(index, chunk_reader);
238 let archive_size = reader.archive_size();
239 let reader: proxmox_backup::pxar::fuse::Reader =
240 Arc::new(BufferedDynamicReadAt::new(reader));
241 let decoder = proxmox_backup::pxar::fuse::Accessor::new(reader, archive_size).await?;
242
243 let session = proxmox_backup::pxar::fuse::Session::mount(
244 decoder,
245 &options,
246 false,
247 Path::new(target.unwrap()),
248 )
249 .map_err(|err| format_err!("pxar mount failed: {}", err))?;
250
251 daemonize()?;
1d0b662b 252
43abba4b
DM
253 select! {
254 res = session.fuse() => res?,
1d0b662b 255 _ = interrupt => {
43abba4b
DM
256 // exit on interrupted
257 }
258 }
45f9b32e
SR
259 } else if server_archive_name.ends_with(".fidx") {
260 let index = client.download_fixed_index(&manifest, &server_archive_name).await?;
261 let size = index.index_bytes();
262 let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), HashMap::new());
263 let reader = AsyncIndexReader::new(index, chunk_reader);
264
265 let mut session = tools::fuse_loop::FuseLoopSession::map_loop(size, reader, options).await?;
266 let loopdev = session.loopdev_path.clone();
267
268 let (st_send, st_recv) = futures::channel::mpsc::channel(1);
269 let (mut abort_send, abort_recv) = futures::channel::mpsc::channel(1);
270 let mut st_recv = st_recv.fuse();
271 let mut session_fut = session.main(st_send, abort_recv).boxed().fuse();
272
273 // poll until loop file is mapped (or errors)
274 select! {
275 res = session_fut => {
276 bail!("FUSE session unexpectedly ended before loop file mapping");
277 },
278 res = st_recv.try_next() => {
279 if let Err(err) = res {
280 // init went wrong, abort now
281 abort_send.try_send(()).map_err(|err|
282 format_err!("error while sending abort signal - {}", err))?;
283 // ignore and keep original error cause
284 let _ = session_fut.await;
285 return Err(err);
286 }
287 }
288 }
289
290 // daemonize only now to be able to print mapped loopdev or startup errors
291 println!("Image mapped as {}", loopdev);
292 daemonize()?;
293
294 // continue polling until complete or interrupted (which also happens on unmap)
295 select! {
296 res = session_fut => res?,
297 _ = interrupt => {
298 // exit on interrupted
299 abort_send.try_send(()).map_err(|err|
300 format_err!("error while sending abort signal - {}", err))?;
301 session_fut.await?;
302 }
303 }
304
305 println!("Image unmapped");
43abba4b 306 } else {
45f9b32e 307 bail!("unknown archive file extension (expected .pxar or .img)");
43abba4b
DM
308 }
309
310 Ok(Value::Null)
311}
45f9b32e
SR
312
313fn unmap(
314 param: Value,
315 _info: &ApiMethod,
316 _rpcenv: &mut dyn RpcEnvironment,
317) -> Result<Value, Error> {
318
319 let mut path = tools::required_string_param(&param, "loopdev")?.to_owned();
320
321 if let Ok(num) = path.parse::<u8>() {
322 path = format!("/dev/loop{}", num);
323 }
324
325 tools::fuse_loop::unmap(path)?;
326
327 Ok(Value::Null)
328}