]>
Commit | Line | Data |
---|---|---|
9de69cdb DM |
1 | use std::os::unix::fs::OpenOptionsExt; |
2 | use std::io::{Seek, SeekFrom}; | |
9de69cdb DM |
3 | use std::sync::Arc; |
4 | ||
5 | use anyhow::{bail, format_err, Error}; | |
6 | use serde_json::Value; | |
7 | ||
6ef1b649 WB |
8 | use proxmox_schema::api; |
9 | use proxmox_router::cli::*; | |
9de69cdb | 10 | |
2b7f8dd5 WB |
11 | use pbs_client::tools::key_source::get_encryption_key_password; |
12 | use pbs_client::{BackupReader, RemoteChunkReader}; | |
3c8c2827 | 13 | use pbs_tools::json::required_string_param; |
bbdda58b | 14 | use pbs_tools::crypt_config::CryptConfig; |
9de69cdb DM |
15 | |
16 | use crate::{ | |
17 | REPO_URL_SCHEMA, | |
d86034af | 18 | KEYFD_SCHEMA, |
9de69cdb | 19 | extract_repository_from_value, |
2f26b866 | 20 | format_key_source, |
9de69cdb | 21 | record_repository, |
d86034af | 22 | decrypt_key, |
9de69cdb DM |
23 | api_datastore_latest_snapshot, |
24 | complete_repository, | |
25 | complete_backup_snapshot, | |
26 | complete_group_or_snapshot, | |
27 | complete_pxar_archive_name, | |
28 | connect, | |
c6a7ea0a | 29 | crypto_parameters, |
9de69cdb DM |
30 | BackupDir, |
31 | BackupGroup, | |
32 | BufferedDynamicReader, | |
33 | BufferedDynamicReadAt, | |
34 | CatalogReader, | |
35 | CATALOG_NAME, | |
9de69cdb DM |
36 | DynamicIndexReader, |
37 | IndexFile, | |
38 | Shell, | |
39 | }; | |
40 | ||
9de69cdb DM |
41 | #[api( |
42 | input: { | |
43 | properties: { | |
44 | repository: { | |
45 | schema: REPO_URL_SCHEMA, | |
46 | optional: true, | |
47 | }, | |
48 | snapshot: { | |
49 | type: String, | |
50 | description: "Snapshot path.", | |
51 | }, | |
d86034af DM |
52 | "keyfile": { |
53 | optional: true, | |
54 | type: String, | |
55 | description: "Path to encryption key.", | |
56 | }, | |
57 | "keyfd": { | |
58 | schema: KEYFD_SCHEMA, | |
59 | optional: true, | |
60 | }, | |
9de69cdb DM |
61 | } |
62 | } | |
63 | )] | |
64 | /// Dump catalog. | |
65 | async fn dump_catalog(param: Value) -> Result<Value, Error> { | |
66 | ||
67 | let repo = extract_repository_from_value(¶m)?; | |
68 | ||
3c8c2827 | 69 | let path = required_string_param(¶m, "snapshot")?; |
9de69cdb DM |
70 | let snapshot: BackupDir = path.parse()?; |
71 | ||
c6a7ea0a | 72 | let crypto = crypto_parameters(¶m)?; |
9de69cdb | 73 | |
c6a7ea0a | 74 | let crypt_config = match crypto.enc_key { |
9de69cdb | 75 | None => None, |
d86034af | 76 | Some(key) => { |
2f26b866 FG |
77 | let (key, _created, _fingerprint) = decrypt_key(&key.key, &get_encryption_key_password) |
78 | .map_err(|err| { | |
79 | eprintln!("{}", format_key_source(&key.source, "encryption")); | |
80 | err | |
81 | })?; | |
d86034af DM |
82 | let crypt_config = CryptConfig::new(key)?; |
83 | Some(Arc::new(crypt_config)) | |
9de69cdb DM |
84 | } |
85 | }; | |
86 | ||
f3fde36b | 87 | let client = connect(&repo)?; |
9de69cdb DM |
88 | |
89 | let client = BackupReader::start( | |
90 | client, | |
91 | crypt_config.clone(), | |
92 | repo.store(), | |
9a37bd6c FG |
93 | snapshot.group().backup_type(), |
94 | snapshot.group().backup_id(), | |
9de69cdb DM |
95 | snapshot.backup_time(), |
96 | true, | |
97 | ).await?; | |
98 | ||
2107a5ae | 99 | let (manifest, _) = client.download_manifest().await?; |
23f9503a | 100 | manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?; |
9de69cdb DM |
101 | |
102 | let index = client.download_dynamic_index(&manifest, CATALOG_NAME).await?; | |
103 | ||
104 | let most_used = index.find_most_used_chunks(8); | |
105 | ||
9a37bd6c | 106 | let file_info = manifest.lookup_file_info(CATALOG_NAME)?; |
14f6c9cb FG |
107 | |
108 | let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), most_used); | |
9de69cdb DM |
109 | |
110 | let mut reader = BufferedDynamicReader::new(index, chunk_reader); | |
111 | ||
112 | let mut catalogfile = std::fs::OpenOptions::new() | |
113 | .write(true) | |
114 | .read(true) | |
115 | .custom_flags(libc::O_TMPFILE) | |
116 | .open("/tmp")?; | |
117 | ||
118 | std::io::copy(&mut reader, &mut catalogfile) | |
119 | .map_err(|err| format_err!("unable to download catalog - {}", err))?; | |
120 | ||
121 | catalogfile.seek(SeekFrom::Start(0))?; | |
122 | ||
123 | let mut catalog_reader = CatalogReader::new(catalogfile); | |
124 | ||
125 | catalog_reader.dump()?; | |
126 | ||
127 | record_repository(&repo); | |
128 | ||
129 | Ok(Value::Null) | |
130 | } | |
131 | ||
132 | #[api( | |
133 | input: { | |
134 | properties: { | |
135 | "snapshot": { | |
136 | type: String, | |
137 | description: "Group/Snapshot path.", | |
138 | }, | |
139 | "archive-name": { | |
140 | type: String, | |
141 | description: "Backup archive name.", | |
142 | }, | |
143 | "repository": { | |
144 | optional: true, | |
145 | schema: REPO_URL_SCHEMA, | |
146 | }, | |
147 | "keyfile": { | |
148 | optional: true, | |
149 | type: String, | |
150 | description: "Path to encryption key.", | |
151 | }, | |
d86034af DM |
152 | "keyfd": { |
153 | schema: KEYFD_SCHEMA, | |
154 | optional: true, | |
155 | }, | |
156 | }, | |
9de69cdb DM |
157 | }, |
158 | )] | |
159 | /// Shell to interactively inspect and restore snapshots. | |
160 | async fn catalog_shell(param: Value) -> Result<(), Error> { | |
161 | let repo = extract_repository_from_value(¶m)?; | |
f3fde36b | 162 | let client = connect(&repo)?; |
3c8c2827 WB |
163 | let path = required_string_param(¶m, "snapshot")?; |
164 | let archive_name = required_string_param(¶m, "archive-name")?; | |
9de69cdb DM |
165 | |
166 | let (backup_type, backup_id, backup_time) = if path.matches('/').count() == 1 { | |
167 | let group: BackupGroup = path.parse()?; | |
168 | api_datastore_latest_snapshot(&client, repo.store(), group).await? | |
169 | } else { | |
170 | let snapshot: BackupDir = path.parse()?; | |
171 | (snapshot.group().backup_type().to_owned(), snapshot.group().backup_id().to_owned(), snapshot.backup_time()) | |
172 | }; | |
173 | ||
c6a7ea0a | 174 | let crypto = crypto_parameters(¶m)?; |
d86034af | 175 | |
c6a7ea0a | 176 | let crypt_config = match crypto.enc_key { |
9de69cdb | 177 | None => None, |
d86034af | 178 | Some(key) => { |
2f26b866 FG |
179 | let (key, _created, _fingerprint) = decrypt_key(&key.key, &get_encryption_key_password) |
180 | .map_err(|err| { | |
181 | eprintln!("{}", format_key_source(&key.source, "encryption")); | |
182 | err | |
183 | })?; | |
d86034af DM |
184 | let crypt_config = CryptConfig::new(key)?; |
185 | Some(Arc::new(crypt_config)) | |
9de69cdb DM |
186 | } |
187 | }; | |
188 | ||
189 | let server_archive_name = if archive_name.ends_with(".pxar") { | |
190 | format!("{}.didx", archive_name) | |
191 | } else { | |
192 | bail!("Can only mount pxar archives."); | |
193 | }; | |
194 | ||
195 | let client = BackupReader::start( | |
196 | client, | |
197 | crypt_config.clone(), | |
198 | repo.store(), | |
199 | &backup_type, | |
200 | &backup_id, | |
201 | backup_time, | |
202 | true, | |
203 | ).await?; | |
204 | ||
205 | let mut tmpfile = std::fs::OpenOptions::new() | |
206 | .write(true) | |
207 | .read(true) | |
208 | .custom_flags(libc::O_TMPFILE) | |
209 | .open("/tmp")?; | |
210 | ||
2107a5ae | 211 | let (manifest, _) = client.download_manifest().await?; |
23f9503a | 212 | manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?; |
9de69cdb DM |
213 | |
214 | let index = client.download_dynamic_index(&manifest, &server_archive_name).await?; | |
215 | let most_used = index.find_most_used_chunks(8); | |
14f6c9cb FG |
216 | |
217 | let file_info = manifest.lookup_file_info(&server_archive_name)?; | |
218 | let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config.clone(), file_info.chunk_crypt_mode(), most_used); | |
9de69cdb DM |
219 | let reader = BufferedDynamicReader::new(index, chunk_reader); |
220 | let archive_size = reader.archive_size(); | |
2b7f8dd5 | 221 | let reader: pbs_client::pxar::fuse::Reader = |
9de69cdb | 222 | Arc::new(BufferedDynamicReadAt::new(reader)); |
2b7f8dd5 | 223 | let decoder = pbs_client::pxar::fuse::Accessor::new(reader, archive_size).await?; |
9de69cdb DM |
224 | |
225 | client.download(CATALOG_NAME, &mut tmpfile).await?; | |
226 | let index = DynamicIndexReader::new(tmpfile) | |
227 | .map_err(|err| format_err!("unable to read catalog index - {}", err))?; | |
228 | ||
229 | // Note: do not use values stored in index (not trusted) - instead, computed them again | |
230 | let (csum, size) = index.compute_csum(); | |
231 | manifest.verify_file(CATALOG_NAME, &csum, size)?; | |
232 | ||
233 | let most_used = index.find_most_used_chunks(8); | |
14f6c9cb | 234 | |
9a37bd6c | 235 | let file_info = manifest.lookup_file_info(CATALOG_NAME)?; |
14f6c9cb | 236 | let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), most_used); |
9de69cdb DM |
237 | let mut reader = BufferedDynamicReader::new(index, chunk_reader); |
238 | let mut catalogfile = std::fs::OpenOptions::new() | |
239 | .write(true) | |
240 | .read(true) | |
241 | .custom_flags(libc::O_TMPFILE) | |
242 | .open("/tmp")?; | |
243 | ||
244 | std::io::copy(&mut reader, &mut catalogfile) | |
245 | .map_err(|err| format_err!("unable to download catalog - {}", err))?; | |
246 | ||
247 | catalogfile.seek(SeekFrom::Start(0))?; | |
248 | let catalog_reader = CatalogReader::new(catalogfile); | |
249 | let state = Shell::new( | |
250 | catalog_reader, | |
251 | &server_archive_name, | |
252 | decoder, | |
253 | ).await?; | |
254 | ||
255 | println!("Starting interactive shell"); | |
256 | state.shell().await?; | |
257 | ||
258 | record_repository(&repo); | |
259 | ||
260 | Ok(()) | |
261 | } | |
262 | ||
263 | pub fn catalog_mgmt_cli() -> CliCommandMap { | |
264 | let catalog_shell_cmd_def = CliCommand::new(&API_METHOD_CATALOG_SHELL) | |
265 | .arg_param(&["snapshot", "archive-name"]) | |
266 | .completion_cb("repository", complete_repository) | |
267 | .completion_cb("archive-name", complete_pxar_archive_name) | |
268 | .completion_cb("snapshot", complete_group_or_snapshot); | |
269 | ||
270 | let catalog_dump_cmd_def = CliCommand::new(&API_METHOD_DUMP_CATALOG) | |
271 | .arg_param(&["snapshot"]) | |
272 | .completion_cb("repository", complete_repository) | |
273 | .completion_cb("snapshot", complete_backup_snapshot); | |
274 | ||
275 | CliCommandMap::new() | |
276 | .insert("dump", catalog_dump_cmd_def) | |
277 | .insert("shell", catalog_shell_cmd_def) | |
278 | } |