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