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