]>
Commit | Line | Data |
---|---|---|
45f9b32e | 1 | use std::collections::HashMap; |
35fe981c | 2 | use std::ffi::OsStr; |
2d7d6e61 | 3 | use std::hash::BuildHasher; |
35fe981c WB |
4 | use std::os::unix::io::AsRawFd; |
5 | use std::path::{Path, PathBuf}; | |
6 | use std::sync::Arc; | |
43abba4b DM |
7 | |
8 | use anyhow::{bail, format_err, Error}; | |
43abba4b | 9 | use futures::future::FutureExt; |
35fe981c | 10 | use futures::select; |
45f9b32e | 11 | use futures::stream::{StreamExt, TryStreamExt}; |
35fe981c WB |
12 | use nix::unistd::{fork, ForkResult}; |
13 | use serde_json::Value; | |
14 | use tokio::signal::unix::{signal, SignalKind}; | |
43abba4b DM |
15 | |
16 | use proxmox::{sortable, identity}; | |
35fe981c | 17 | use proxmox::tools::fd::Fd; |
6ef1b649 WB |
18 | use proxmox_router::{ApiHandler, ApiMethod, RpcEnvironment, cli::*}; |
19 | use proxmox_schema::*; | |
43abba4b | 20 | |
bbdda58b DM |
21 | use pbs_tools::crypt_config::CryptConfig; |
22 | use pbs_config::key_config::load_and_decrypt_key; | |
23 | use pbs_datastore::{BackupDir, BackupGroup, }; | |
eb5e0ae6 WB |
24 | use pbs_datastore::index::IndexFile; |
25 | use pbs_datastore::dynamic_index::BufferedDynamicReader; | |
ef4df211 | 26 | use pbs_datastore::cached_chunk_reader::CachedChunkReader; |
2b7f8dd5 WB |
27 | use pbs_client::tools::key_source::get_encryption_key_password; |
28 | use pbs_client::{BackupReader, RemoteChunkReader}; | |
3c8c2827 | 29 | use pbs_tools::json::required_string_param; |
2b7f8dd5 | 30 | |
43abba4b DM |
31 | use crate::{ |
32 | REPO_URL_SCHEMA, | |
33 | extract_repository_from_value, | |
43abba4b | 34 | complete_pxar_archive_name, |
2995aedf | 35 | complete_img_archive_name, |
43abba4b DM |
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()), | |
45f9b32e SR |
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!([ | |
2d7d6e61 | 82 | ("name", true, &StringSchema::new( |
2deee0e0 SR |
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.") | |
2d7d6e61 | 85 | ).schema()), |
43abba4b DM |
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) | |
b3f279e2 | 97 | .completion_cb("target", complete_file_name) |
43abba4b DM |
98 | } |
99 | ||
45f9b32e SR |
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) | |
2995aedf | 106 | .completion_cb("archive-name", complete_img_archive_name) |
45f9b32e SR |
107 | } |
108 | ||
109 | pub fn unmap_cmd_def() -> CliCommand { | |
110 | ||
111 | CliCommand::new(&API_METHOD_UNMAP) | |
2d7d6e61 SR |
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 | { | |
eb5e0ae6 | 119 | match pbs_fuse_loop::find_all_mappings() { |
2d7d6e61 SR |
120 | Ok(mappings) => mappings |
121 | .filter_map(|(name, _)| { | |
81867f05 | 122 | proxmox::tools::systemd::unescape_unit(&name).ok() |
2d7d6e61 SR |
123 | }).collect(), |
124 | Err(_) => Vec::new() | |
125 | } | |
45f9b32e SR |
126 | } |
127 | ||
43abba4b DM |
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. | |
9a1b24b6 | 138 | return proxmox_async::runtime::main(mount_do(param, None)); |
43abba4b DM |
139 | } |
140 | ||
d1d74c43 | 141 | // Process should be daemonized. |
43abba4b | 142 | // Make sure to fork before the async runtime is instantiated to avoid troubles. |
eb5e0ae6 | 143 | let (pr, pw) = pbs_tools::io::pipe()?; |
0c4c6a7b | 144 | match unsafe { fork() } { |
43abba4b | 145 | Ok(ForkResult::Parent { .. }) => { |
35fe981c | 146 | drop(pw); |
43abba4b | 147 | // Blocks the parent process until we are ready to go in the child |
35fe981c | 148 | let _res = nix::unistd::read(pr.as_raw_fd(), &mut [0]).unwrap(); |
43abba4b DM |
149 | Ok(Value::Null) |
150 | } | |
151 | Ok(ForkResult::Child) => { | |
35fe981c | 152 | drop(pr); |
43abba4b | 153 | nix::unistd::setsid().unwrap(); |
9a1b24b6 | 154 | proxmox_async::runtime::main(mount_do(param, Some(pw))) |
43abba4b DM |
155 | } |
156 | Err(_) => bail!("failed to daemonize process"), | |
157 | } | |
158 | } | |
159 | ||
35fe981c | 160 | async fn mount_do(param: Value, pipe: Option<Fd>) -> Result<Value, Error> { |
43abba4b | 161 | let repo = extract_repository_from_value(¶m)?; |
3c8c2827 | 162 | let archive_name = required_string_param(¶m, "archive-name")?; |
f3fde36b | 163 | let client = connect(&repo)?; |
43abba4b | 164 | |
45f9b32e SR |
165 | let target = param["target"].as_str(); |
166 | ||
43abba4b DM |
167 | record_repository(&repo); |
168 | ||
3c8c2827 | 169 | let path = required_string_param(¶m, "snapshot")?; |
43abba4b DM |
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) => { | |
6f2626ae | 182 | println!("Encryption key file: '{:?}'", path); |
ff8945fd | 183 | let (key, _, fingerprint) = load_and_decrypt_key(&path, &get_encryption_key_password)?; |
6f2626ae | 184 | println!("Encryption key fingerprint: '{}'", fingerprint); |
43abba4b DM |
185 | Some(Arc::new(CryptConfig::new(key)?)) |
186 | } | |
187 | }; | |
188 | ||
189 | let server_archive_name = if archive_name.ends_with(".pxar") { | |
3984a5fd | 190 | if target.is_none() { |
45f9b32e SR |
191 | bail!("use the 'mount' command to mount pxar archives"); |
192 | } | |
43abba4b | 193 | format!("{}.didx", archive_name) |
45f9b32e | 194 | } else if archive_name.ends_with(".img") { |
3984a5fd | 195 | if target.is_some() { |
45f9b32e SR |
196 | bail!("use the 'map' command to map drive images"); |
197 | } | |
198 | format!("{}.fidx", archive_name) | |
43abba4b | 199 | } else { |
45f9b32e | 200 | bail!("Can only mount/map pxar archives and drive images."); |
43abba4b DM |
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 | ||
2107a5ae | 213 | let (manifest, _) = client.download_manifest().await?; |
23f9503a | 214 | manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?; |
43abba4b | 215 | |
871181d9 | 216 | let file_info = manifest.lookup_file_info(&server_archive_name)?; |
14f6c9cb | 217 | |
45f9b32e | 218 | let daemonize = || -> Result<(), Error> { |
43abba4b DM |
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. | |
35fe981c WB |
235 | nix::unistd::write(pipe.as_raw_fd(), &[0u8])?; |
236 | let _: Fd = pipe; | |
43abba4b DM |
237 | } |
238 | ||
45f9b32e SR |
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())?; | |
b5a202ac FG |
247 | |
248 | let mut interrupt = futures::future::select(interrupt_int.recv().boxed(), interrupt_term.recv().boxed()); | |
45f9b32e SR |
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(); | |
2b7f8dd5 | 256 | let reader: pbs_client::pxar::fuse::Reader = |
45f9b32e | 257 | Arc::new(BufferedDynamicReadAt::new(reader)); |
2b7f8dd5 | 258 | let decoder = pbs_client::pxar::fuse::Accessor::new(reader, archive_size).await?; |
45f9b32e | 259 | |
2b7f8dd5 | 260 | let session = pbs_client::pxar::fuse::Session::mount( |
45f9b32e SR |
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()?; | |
1d0b662b | 269 | |
43abba4b DM |
270 | select! { |
271 | res = session.fuse() => res?, | |
1d0b662b | 272 | _ = interrupt => { |
43abba4b DM |
273 | // exit on interrupted |
274 | } | |
275 | } | |
45f9b32e SR |
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()); | |
1ef6e8b6 | 280 | let reader = CachedChunkReader::new(chunk_reader, index, 8).seekable(); |
45f9b32e | 281 | |
2d7d6e61 | 282 | let name = &format!("{}:{}/{}", repo.to_string(), path, archive_name); |
81867f05 | 283 | let name_escaped = proxmox::tools::systemd::escape_unit(name, false); |
2d7d6e61 | 284 | |
eb5e0ae6 | 285 | let mut session = pbs_fuse_loop::FuseLoopSession::map_loop(size, reader, &name_escaped, options).await?; |
45f9b32e SR |
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! { | |
0bfcea6a | 295 | _res = session_fut => { |
45f9b32e SR |
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 | |
2d7d6e61 | 311 | println!("Image '{}' mapped on {}", name, loopdev); |
45f9b32e SR |
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"); | |
43abba4b | 326 | } else { |
45f9b32e | 327 | bail!("unknown archive file extension (expected .pxar or .img)"); |
43abba4b DM |
328 | } |
329 | ||
330 | Ok(Value::Null) | |
331 | } | |
45f9b32e SR |
332 | |
333 | fn unmap( | |
334 | param: Value, | |
335 | _info: &ApiMethod, | |
336 | _rpcenv: &mut dyn RpcEnvironment, | |
337 | ) -> Result<Value, Error> { | |
338 | ||
2d7d6e61 SR |
339 | let mut name = match param["name"].as_str() { |
340 | Some(name) => name.to_owned(), | |
341 | None => { | |
eb5e0ae6 | 342 | pbs_fuse_loop::cleanup_unused_run_files(None); |
2d7d6e61 | 343 | let mut any = false; |
eb5e0ae6 | 344 | for (backing, loopdev) in pbs_fuse_loop::find_all_mappings()? { |
81867f05 | 345 | let name = proxmox::tools::systemd::unescape_unit(&backing)?; |
4d104cd4 | 346 | println!("{}:\t{}", loopdev.unwrap_or_else(|| "(unmapped)".to_string()), name); |
2d7d6e61 SR |
347 | any = true; |
348 | } | |
349 | if !any { | |
350 | println!("Nothing mapped."); | |
351 | } | |
352 | return Ok(Value::Null); | |
353 | }, | |
354 | }; | |
45f9b32e | 355 | |
2d7d6e61 SR |
356 | // allow loop device number alone |
357 | if let Ok(num) = name.parse::<u8>() { | |
358 | name = format!("/dev/loop{}", num); | |
45f9b32e SR |
359 | } |
360 | ||
2d7d6e61 | 361 | if name.starts_with("/dev/loop") { |
eb5e0ae6 | 362 | pbs_fuse_loop::unmap_loopdev(name)?; |
2d7d6e61 | 363 | } else { |
81867f05 | 364 | let name = proxmox::tools::systemd::escape_unit(&name, false); |
eb5e0ae6 | 365 | pbs_fuse_loop::unmap_name(name)?; |
2d7d6e61 | 366 | } |
45f9b32e SR |
367 | |
368 | Ok(Value::Null) | |
369 | } |