]> git.proxmox.com Git - proxmox-backup.git/blob - proxmox-backup-client/src/snapshot.rs
b63e84a6cda78105f5171da86fa94694c14ecc8a
[proxmox-backup.git] / proxmox-backup-client / src / snapshot.rs
1 use std::sync::Arc;
2
3 use anyhow::Error;
4 use serde_json::{json, Value};
5
6 use proxmox::{
7 api::{api, cli::*},
8 tools::fs::file_get_contents,
9 };
10
11 use pbs_api_types::SnapshotListItem;
12 use pbs_client::tools::key_source::get_encryption_key_password;
13 use pbs_datastore::{BackupGroup, CryptMode, CryptConfig, decrypt_key};
14 use pbs_datastore::data_blob::DataBlob;
15 use pbs_tools::json::required_string_param;
16
17 use crate::{
18 REPO_URL_SCHEMA,
19 KEYFILE_SCHEMA,
20 KEYFD_SCHEMA,
21 BackupDir,
22 api_datastore_list_snapshots,
23 complete_backup_snapshot,
24 complete_backup_group,
25 complete_repository,
26 connect,
27 crypto_parameters,
28 extract_repository_from_value,
29 record_repository,
30 };
31
32 #[api(
33 input: {
34 properties: {
35 repository: {
36 schema: REPO_URL_SCHEMA,
37 optional: true,
38 },
39 group: {
40 type: String,
41 description: "Backup group.",
42 optional: true,
43 },
44 "output-format": {
45 schema: OUTPUT_FORMAT,
46 optional: true,
47 },
48 }
49 }
50 )]
51 /// List backup snapshots.
52 async fn list_snapshots(param: Value) -> Result<Value, Error> {
53
54 let repo = extract_repository_from_value(&param)?;
55
56 let output_format = get_output_format(&param);
57
58 let client = connect(&repo)?;
59
60 let group: Option<BackupGroup> = if let Some(path) = param["group"].as_str() {
61 Some(path.parse()?)
62 } else {
63 None
64 };
65
66 let mut data = api_datastore_list_snapshots(&client, repo.store(), group).await?;
67
68 record_repository(&repo);
69
70 let render_snapshot_path = |_v: &Value, record: &Value| -> Result<String, Error> {
71 let item: SnapshotListItem = serde_json::from_value(record.to_owned())?;
72 let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time)?;
73 Ok(snapshot.relative_path().to_str().unwrap().to_owned())
74 };
75
76 let render_files = |_v: &Value, record: &Value| -> Result<String, Error> {
77 let item: SnapshotListItem = serde_json::from_value(record.to_owned())?;
78 let mut filenames = Vec::new();
79 for file in &item.files {
80 filenames.push(file.filename.to_string());
81 }
82 Ok(pbs_tools::format::render_backup_file_list(&filenames[..]))
83 };
84
85 let options = default_table_format_options()
86 .sortby("backup-type", false)
87 .sortby("backup-id", false)
88 .sortby("backup-time", false)
89 .column(ColumnConfig::new("backup-id").renderer(render_snapshot_path).header("snapshot"))
90 .column(ColumnConfig::new("size").renderer(pbs_tools::format::render_bytes_human_readable))
91 .column(ColumnConfig::new("files").renderer(render_files))
92 ;
93
94 let return_type = &pbs_api_types::ADMIN_DATASTORE_LIST_SNAPSHOTS_RETURN_TYPE;
95
96 format_and_print_result_full(&mut data, return_type, &output_format, &options);
97
98 Ok(Value::Null)
99 }
100
101 #[api(
102 input: {
103 properties: {
104 repository: {
105 schema: REPO_URL_SCHEMA,
106 optional: true,
107 },
108 snapshot: {
109 type: String,
110 description: "Snapshot path.",
111 },
112 "output-format": {
113 schema: OUTPUT_FORMAT,
114 optional: true,
115 },
116 }
117 }
118 )]
119 /// List snapshot files.
120 async fn list_snapshot_files(param: Value) -> Result<Value, Error> {
121
122 let repo = extract_repository_from_value(&param)?;
123
124 let path = required_string_param(&param, "snapshot")?;
125 let snapshot: BackupDir = path.parse()?;
126
127 let output_format = get_output_format(&param);
128
129 let client = connect(&repo)?;
130
131 let path = format!("api2/json/admin/datastore/{}/files", repo.store());
132
133 let mut result = client.get(&path, Some(json!({
134 "backup-type": snapshot.group().backup_type(),
135 "backup-id": snapshot.group().backup_id(),
136 "backup-time": snapshot.backup_time(),
137 }))).await?;
138
139 record_repository(&repo);
140
141 let return_type = &pbs_api_types::ADMIN_DATASTORE_LIST_SNAPSHOT_FILES_RETURN_TYPE;
142
143 let mut data: Value = result["data"].take();
144
145 let options = default_table_format_options();
146
147 format_and_print_result_full(&mut data, return_type, &output_format, &options);
148
149 Ok(Value::Null)
150 }
151
152 #[api(
153 input: {
154 properties: {
155 repository: {
156 schema: REPO_URL_SCHEMA,
157 optional: true,
158 },
159 snapshot: {
160 type: String,
161 description: "Snapshot path.",
162 },
163 }
164 }
165 )]
166 /// Forget (remove) backup snapshots.
167 async fn forget_snapshots(param: Value) -> Result<Value, Error> {
168
169 let repo = extract_repository_from_value(&param)?;
170
171 let path = required_string_param(&param, "snapshot")?;
172 let snapshot: BackupDir = path.parse()?;
173
174 let mut client = connect(&repo)?;
175
176 let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
177
178 let result = client.delete(&path, Some(json!({
179 "backup-type": snapshot.group().backup_type(),
180 "backup-id": snapshot.group().backup_id(),
181 "backup-time": snapshot.backup_time(),
182 }))).await?;
183
184 record_repository(&repo);
185
186 Ok(result)
187 }
188
189 #[api(
190 input: {
191 properties: {
192 repository: {
193 schema: REPO_URL_SCHEMA,
194 optional: true,
195 },
196 snapshot: {
197 type: String,
198 description: "Group/Snapshot path.",
199 },
200 logfile: {
201 type: String,
202 description: "The path to the log file you want to upload.",
203 },
204 keyfile: {
205 schema: KEYFILE_SCHEMA,
206 optional: true,
207 },
208 "keyfd": {
209 schema: KEYFD_SCHEMA,
210 optional: true,
211 },
212 "crypt-mode": {
213 type: CryptMode,
214 optional: true,
215 },
216 }
217 }
218 )]
219 /// Upload backup log file.
220 async fn upload_log(param: Value) -> Result<Value, Error> {
221
222 let logfile = required_string_param(&param, "logfile")?;
223 let repo = extract_repository_from_value(&param)?;
224
225 let snapshot = required_string_param(&param, "snapshot")?;
226 let snapshot: BackupDir = snapshot.parse()?;
227
228 let mut client = connect(&repo)?;
229
230 let crypto = crypto_parameters(&param)?;
231
232 let crypt_config = match crypto.enc_key {
233 None => None,
234 Some(key) => {
235 let (key, _created, _) = decrypt_key(&key.key, &get_encryption_key_password)?;
236 let crypt_config = CryptConfig::new(key)?;
237 Some(Arc::new(crypt_config))
238 }
239 };
240
241 let data = file_get_contents(logfile)?;
242
243 // fixme: howto sign log?
244 let blob = match crypto.mode {
245 CryptMode::None | CryptMode::SignOnly => DataBlob::encode(&data, None, true)?,
246 CryptMode::Encrypt => DataBlob::encode(&data, crypt_config.as_ref().map(Arc::as_ref), true)?,
247 };
248
249 let raw_data = blob.into_inner();
250
251 let path = format!("api2/json/admin/datastore/{}/upload-backup-log", repo.store());
252
253 let args = json!({
254 "backup-type": snapshot.group().backup_type(),
255 "backup-id": snapshot.group().backup_id(),
256 "backup-time": snapshot.backup_time(),
257 });
258
259 let body = hyper::Body::from(raw_data);
260
261 client.upload("application/octet-stream", body, &path, Some(args)).await
262 }
263
264 #[api(
265 input: {
266 properties: {
267 repository: {
268 schema: REPO_URL_SCHEMA,
269 optional: true,
270 },
271 snapshot: {
272 type: String,
273 description: "Snapshot path.",
274 },
275 "output-format": {
276 schema: OUTPUT_FORMAT,
277 optional: true,
278 },
279 }
280 }
281 )]
282 /// Show notes
283 async fn show_notes(param: Value) -> Result<Value, Error> {
284 let repo = extract_repository_from_value(&param)?;
285 let path = required_string_param(&param, "snapshot")?;
286
287 let snapshot: BackupDir = path.parse()?;
288 let client = connect(&repo)?;
289
290 let path = format!("api2/json/admin/datastore/{}/notes", repo.store());
291
292 let args = json!({
293 "backup-type": snapshot.group().backup_type(),
294 "backup-id": snapshot.group().backup_id(),
295 "backup-time": snapshot.backup_time(),
296 });
297
298 let output_format = get_output_format(&param);
299
300 let mut result = client.get(&path, Some(args)).await?;
301
302 let notes = result["data"].take();
303
304 if output_format == "text" {
305 if let Some(notes) = notes.as_str() {
306 println!("{}", notes);
307 }
308 } else {
309 format_and_print_result(
310 &json!({
311 "notes": notes,
312 }),
313 &output_format,
314 );
315 }
316
317 Ok(Value::Null)
318 }
319
320 #[api(
321 input: {
322 properties: {
323 repository: {
324 schema: REPO_URL_SCHEMA,
325 optional: true,
326 },
327 snapshot: {
328 type: String,
329 description: "Snapshot path.",
330 },
331 notes: {
332 type: String,
333 description: "The Notes.",
334 },
335 }
336 }
337 )]
338 /// Update Notes
339 async fn update_notes(param: Value) -> Result<Value, Error> {
340 let repo = extract_repository_from_value(&param)?;
341 let path = required_string_param(&param, "snapshot")?;
342 let notes = required_string_param(&param, "notes")?;
343
344 let snapshot: BackupDir = path.parse()?;
345 let mut client = connect(&repo)?;
346
347 let path = format!("api2/json/admin/datastore/{}/notes", repo.store());
348
349 let args = json!({
350 "backup-type": snapshot.group().backup_type(),
351 "backup-id": snapshot.group().backup_id(),
352 "backup-time": snapshot.backup_time(),
353 "notes": notes,
354 });
355
356 client.put(&path, Some(args)).await?;
357
358 Ok(Value::Null)
359 }
360
361 fn notes_cli() -> CliCommandMap {
362 CliCommandMap::new()
363 .insert(
364 "show",
365 CliCommand::new(&API_METHOD_SHOW_NOTES)
366 .arg_param(&["snapshot"])
367 .completion_cb("snapshot", complete_backup_snapshot),
368 )
369 .insert(
370 "update",
371 CliCommand::new(&API_METHOD_UPDATE_NOTES)
372 .arg_param(&["snapshot", "notes"])
373 .completion_cb("snapshot", complete_backup_snapshot),
374 )
375 }
376
377 pub fn snapshot_mgtm_cli() -> CliCommandMap {
378 CliCommandMap::new()
379 .insert("notes", notes_cli())
380 .insert(
381 "list",
382 CliCommand::new(&API_METHOD_LIST_SNAPSHOTS)
383 .arg_param(&["group"])
384 .completion_cb("group", complete_backup_group)
385 .completion_cb("repository", complete_repository)
386 )
387 .insert(
388 "files",
389 CliCommand::new(&API_METHOD_LIST_SNAPSHOT_FILES)
390 .arg_param(&["snapshot"])
391 .completion_cb("repository", complete_repository)
392 .completion_cb("snapshot", complete_backup_snapshot)
393 )
394 .insert(
395 "forget",
396 CliCommand::new(&API_METHOD_FORGET_SNAPSHOTS)
397 .arg_param(&["snapshot"])
398 .completion_cb("repository", complete_repository)
399 .completion_cb("snapshot", complete_backup_snapshot)
400 )
401 .insert(
402 "upload-log",
403 CliCommand::new(&API_METHOD_UPLOAD_LOG)
404 .arg_param(&["snapshot", "logfile"])
405 .completion_cb("snapshot", complete_backup_snapshot)
406 .completion_cb("logfile", pbs_tools::fs::complete_file_name)
407 .completion_cb("keyfile", pbs_tools::fs::complete_file_name)
408 .completion_cb("repository", complete_repository)
409 )
410 }