]>
Commit | Line | Data |
---|---|---|
a44c934b | 1 | use std::panic::UnwindSafe; |
7bb720cb | 2 | use std::path::Path; |
6dbad5b4 | 3 | use std::sync::Arc; |
28570d19 | 4 | use std::collections::HashMap; |
6dbad5b4 | 5 | |
a44c934b | 6 | use anyhow::{bail, format_err, Error}; |
5d908606 DM |
7 | use serde_json::Value; |
8 | ||
7bb720cb DM |
9 | use proxmox::{ |
10 | sortable, | |
11 | identity, | |
12 | list_subdirs_api_method, | |
13 | tools::Uuid, | |
7bb720cb DM |
14 | api::{ |
15 | api, | |
25aa55b5 | 16 | section_config::SectionConfigData, |
6dbad5b4 | 17 | RpcEnvironment, |
cb022525 | 18 | RpcEnvironmentType, |
8cd63df0 | 19 | Permission, |
7bb720cb DM |
20 | Router, |
21 | SubdirMap, | |
22 | }, | |
23 | }; | |
5d908606 | 24 | |
c23192d3 WB |
25 | use pbs_datastore::task_log; |
26 | ||
5d908606 | 27 | use crate::{ |
8cd63df0 DM |
28 | config::{ |
29 | self, | |
30 | cached_user_info::CachedUserInfo, | |
31 | acl::{ | |
32 | PRIV_TAPE_AUDIT, | |
b4975d31 DM |
33 | PRIV_TAPE_READ, |
34 | PRIV_TAPE_WRITE, | |
8cd63df0 DM |
35 | }, |
36 | }, | |
b017bbc4 DM |
37 | api2::{ |
38 | types::{ | |
39 | UPID_SCHEMA, | |
18262a88 | 40 | CHANGER_NAME_SCHEMA, |
b017bbc4 DM |
41 | DRIVE_NAME_SCHEMA, |
42 | MEDIA_LABEL_SCHEMA, | |
43 | MEDIA_POOL_NAME_SCHEMA, | |
44 | Authid, | |
5fdaecf6 | 45 | DriveListEntry, |
a79082a0 | 46 | LtoTapeDrive, |
b017bbc4 DM |
47 | MediaIdFlat, |
48 | LabelUuidMap, | |
49 | MamAttribute, | |
a79082a0 | 50 | LtoDriveAndMediaStatus, |
109ccd30 | 51 | Lp17VolumeStatistics, |
b017bbc4 | 52 | }, |
074503f2 DM |
53 | tape::restore::{ |
54 | fast_catalog_restore, | |
55 | restore_media, | |
56 | }, | |
5d908606 | 57 | }, |
6dbad5b4 | 58 | server::WorkerTask, |
5d908606 | 59 | tape::{ |
7bb720cb | 60 | TAPE_STATUS_DIR, |
7bb720cb | 61 | Inventory, |
34605654 | 62 | MediaCatalog, |
7bb720cb | 63 | MediaId, |
318b3106 | 64 | BlockReadError, |
30316192 DM |
65 | lock_media_set, |
66 | lock_media_pool, | |
67 | lock_unassigned_media_pool, | |
a79082a0 | 68 | lto_tape_device_list, |
b5b99a52 | 69 | lookup_device_identification, |
645a044b | 70 | file_formats::{ |
a78348ac | 71 | MediaLabel, |
7bb720cb DM |
72 | MediaSetLabel, |
73 | }, | |
37796ff7 DM |
74 | drive::{ |
75 | TapeDriver, | |
a79082a0 | 76 | LtoTapeHandle, |
a79082a0 | 77 | open_lto_tape_device, |
1ce8e905 | 78 | open_lto_tape_drive, |
25aa55b5 | 79 | media_changer, |
37796ff7 DM |
80 | required_media_changer, |
81 | open_drive, | |
25aa55b5 | 82 | lock_tape_device, |
a44c934b | 83 | set_tape_device_state, |
8bf57693 | 84 | get_tape_device_state, |
d95c74c6 | 85 | tape_alert_flags_critical, |
37796ff7 DM |
86 | }, |
87 | changer::update_changer_online_status, | |
5d908606 DM |
88 | }, |
89 | }; | |
90 | ||
a44c934b DC |
91 | fn run_drive_worker<F>( |
92 | rpcenv: &dyn RpcEnvironment, | |
93 | drive: String, | |
94 | worker_type: &str, | |
95 | job_id: Option<String>, | |
96 | f: F, | |
97 | ) -> Result<String, Error> | |
98 | where | |
99 | F: Send | |
100 | + UnwindSafe | |
101 | + 'static | |
102 | + FnOnce(Arc<WorkerTask>, SectionConfigData) -> Result<(), Error>, | |
103 | { | |
104 | // early check/lock before starting worker | |
1ce8e905 | 105 | let (config, _digest) = pbs_config::drive::config()?; |
a44c934b DC |
106 | let lock_guard = lock_tape_device(&config, &drive)?; |
107 | ||
108 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; | |
109 | let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI; | |
110 | ||
111 | WorkerTask::new_thread(worker_type, job_id, auth_id, to_stdout, move |worker| { | |
112 | let _lock_guard = lock_guard; | |
113 | set_tape_device_state(&drive, &worker.upid().to_string()) | |
114 | .map_err(|err| format_err!("could not set tape device state: {}", err))?; | |
115 | ||
116 | let result = f(worker, config); | |
117 | set_tape_device_state(&drive, "") | |
118 | .map_err(|err| format_err!("could not unset tape device state: {}", err))?; | |
119 | result | |
120 | }) | |
121 | } | |
122 | ||
54c77b3d DC |
123 | async fn run_drive_blocking_task<F, R>(drive: String, state: String, f: F) -> Result<R, Error> |
124 | where | |
125 | F: Send + 'static + FnOnce(SectionConfigData) -> Result<R, Error>, | |
126 | R: Send + 'static, | |
127 | { | |
128 | // early check/lock before starting worker | |
1ce8e905 | 129 | let (config, _digest) = pbs_config::drive::config()?; |
54c77b3d DC |
130 | let lock_guard = lock_tape_device(&config, &drive)?; |
131 | tokio::task::spawn_blocking(move || { | |
132 | let _lock_guard = lock_guard; | |
133 | set_tape_device_state(&drive, &state) | |
134 | .map_err(|err| format_err!("could not set tape device state: {}", err))?; | |
135 | let result = f(config); | |
136 | set_tape_device_state(&drive, "") | |
137 | .map_err(|err| format_err!("could not unset tape device state: {}", err))?; | |
138 | result | |
139 | }) | |
140 | .await? | |
141 | } | |
142 | ||
5d908606 DM |
143 | #[api( |
144 | input: { | |
145 | properties: { | |
93829fc6 | 146 | drive: { |
49c965a4 | 147 | schema: DRIVE_NAME_SCHEMA, |
5d908606 | 148 | }, |
8446fbca | 149 | "label-text": { |
46a1863f | 150 | schema: MEDIA_LABEL_SCHEMA, |
5d908606 DM |
151 | }, |
152 | }, | |
153 | }, | |
d0647e5a DM |
154 | returns: { |
155 | schema: UPID_SCHEMA, | |
156 | }, | |
b4975d31 DM |
157 | access: { |
158 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
159 | }, | |
5d908606 | 160 | )] |
46a1863f DM |
161 | /// Load media with specified label |
162 | /// | |
163 | /// Issue a media load request to the associated changer device. | |
d0647e5a DM |
164 | pub fn load_media( |
165 | drive: String, | |
166 | label_text: String, | |
167 | rpcenv: &mut dyn RpcEnvironment, | |
168 | ) -> Result<Value, Error> { | |
d0647e5a DM |
169 | let job_id = format!("{}:{}", drive, label_text); |
170 | ||
a1c55753 DC |
171 | let upid_str = run_drive_worker( |
172 | rpcenv, | |
173 | drive.clone(), | |
d0647e5a DM |
174 | "load-media", |
175 | Some(job_id), | |
a1c55753 | 176 | move |worker, config| { |
d0647e5a DM |
177 | task_log!(worker, "loading media '{}' into drive '{}'", label_text, drive); |
178 | let (mut changer, _) = required_media_changer(&config, &drive)?; | |
86d9f4e7 DM |
179 | changer.load_media(&label_text)?; |
180 | Ok(()) | |
a1c55753 | 181 | }, |
d0647e5a DM |
182 | )?; |
183 | ||
184 | Ok(upid_str.into()) | |
5d908606 | 185 | } |
483da89d | 186 | |
e49f0c03 DM |
187 | #[api( |
188 | input: { | |
189 | properties: { | |
190 | drive: { | |
49c965a4 | 191 | schema: DRIVE_NAME_SCHEMA, |
e49f0c03 | 192 | }, |
46a1863f DM |
193 | "source-slot": { |
194 | description: "Source slot number.", | |
195 | minimum: 1, | |
e49f0c03 DM |
196 | }, |
197 | }, | |
198 | }, | |
b4975d31 DM |
199 | access: { |
200 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
201 | }, | |
e49f0c03 | 202 | )] |
46a1863f | 203 | /// Load media from the specified slot |
e49f0c03 DM |
204 | /// |
205 | /// Issue a media load request to the associated changer device. | |
46a1863f | 206 | pub async fn load_slot(drive: String, source_slot: u64) -> Result<(), Error> { |
47a72414 DC |
207 | run_drive_blocking_task( |
208 | drive.clone(), | |
209 | format!("load from slot {}", source_slot), | |
210 | move |config| { | |
211 | let (mut changer, _) = required_media_changer(&config, &drive)?; | |
86d9f4e7 DM |
212 | changer.load_media_from_slot(source_slot)?; |
213 | Ok(()) | |
47a72414 DC |
214 | }, |
215 | ) | |
216 | .await | |
e49f0c03 DM |
217 | } |
218 | ||
483da89d DM |
219 | #[api( |
220 | input: { | |
221 | properties: { | |
222 | drive: { | |
223 | schema: DRIVE_NAME_SCHEMA, | |
224 | }, | |
8446fbca | 225 | "label-text": { |
483da89d DM |
226 | schema: MEDIA_LABEL_SCHEMA, |
227 | }, | |
228 | }, | |
229 | }, | |
230 | returns: { | |
d1d74c43 | 231 | description: "The import-export slot number the media was transferred to.", |
483da89d DM |
232 | type: u64, |
233 | minimum: 1, | |
234 | }, | |
b4975d31 DM |
235 | access: { |
236 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
237 | }, | |
483da89d DM |
238 | )] |
239 | /// Export media with specified label | |
8446fbca | 240 | pub async fn export_media(drive: String, label_text: String) -> Result<u64, Error> { |
47a72414 DC |
241 | run_drive_blocking_task( |
242 | drive.clone(), | |
243 | format!("export media {}", label_text), | |
244 | move |config| { | |
245 | let (mut changer, changer_name) = required_media_changer(&config, &drive)?; | |
246 | match changer.export_media(&label_text)? { | |
247 | Some(slot) => Ok(slot), | |
248 | None => bail!( | |
249 | "media '{}' is not online (via changer '{}')", | |
250 | label_text, | |
251 | changer_name | |
252 | ), | |
253 | } | |
483da89d | 254 | } |
47a72414 DC |
255 | ) |
256 | .await | |
483da89d DM |
257 | } |
258 | ||
5d908606 DM |
259 | #[api( |
260 | input: { | |
261 | properties: { | |
a3c709ef | 262 | drive: { |
49c965a4 | 263 | schema: DRIVE_NAME_SCHEMA, |
5d908606 | 264 | }, |
46a1863f | 265 | "target-slot": { |
5d908606 DM |
266 | description: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.", |
267 | minimum: 1, | |
268 | optional: true, | |
269 | }, | |
270 | }, | |
271 | }, | |
d0647e5a DM |
272 | returns: { |
273 | schema: UPID_SCHEMA, | |
274 | }, | |
b4975d31 DM |
275 | access: { |
276 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
277 | }, | |
5d908606 DM |
278 | )] |
279 | /// Unload media via changer | |
d0647e5a | 280 | pub fn unload( |
a3c709ef | 281 | drive: String, |
46a1863f | 282 | target_slot: Option<u64>, |
d0647e5a DM |
283 | rpcenv: &mut dyn RpcEnvironment, |
284 | ) -> Result<Value, Error> { | |
a1c55753 DC |
285 | let upid_str = run_drive_worker( |
286 | rpcenv, | |
287 | drive.clone(), | |
d0647e5a DM |
288 | "unload-media", |
289 | Some(drive.clone()), | |
a1c55753 | 290 | move |worker, config| { |
d0647e5a DM |
291 | task_log!(worker, "unloading media from drive '{}'", drive); |
292 | ||
293 | let (mut changer, _) = required_media_changer(&config, &drive)?; | |
86d9f4e7 DM |
294 | changer.unload_media(target_slot)?; |
295 | Ok(()) | |
a1c55753 | 296 | }, |
d0647e5a DM |
297 | )?; |
298 | ||
299 | Ok(upid_str.into()) | |
5d908606 DM |
300 | } |
301 | ||
583a68a4 DM |
302 | #[api( |
303 | input: { | |
304 | properties: { | |
305 | drive: { | |
49c965a4 | 306 | schema: DRIVE_NAME_SCHEMA, |
583a68a4 DM |
307 | }, |
308 | fast: { | |
309 | description: "Use fast erase.", | |
310 | type: bool, | |
311 | optional: true, | |
312 | default: true, | |
313 | }, | |
be61c56c DC |
314 | "label-text": { |
315 | schema: MEDIA_LABEL_SCHEMA, | |
316 | optional: true, | |
317 | }, | |
583a68a4 DM |
318 | }, |
319 | }, | |
663ef859 DM |
320 | returns: { |
321 | schema: UPID_SCHEMA, | |
322 | }, | |
b4975d31 DM |
323 | access: { |
324 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false), | |
325 | }, | |
583a68a4 | 326 | )] |
e29f456e DM |
327 | /// Format media. Check for label-text if given (cancels if wrong media). |
328 | pub fn format_media( | |
663ef859 DM |
329 | drive: String, |
330 | fast: Option<bool>, | |
be61c56c | 331 | label_text: Option<String>, |
663ef859 DM |
332 | rpcenv: &mut dyn RpcEnvironment, |
333 | ) -> Result<Value, Error> { | |
a1c55753 DC |
334 | let upid_str = run_drive_worker( |
335 | rpcenv, | |
336 | drive.clone(), | |
e29f456e | 337 | "format-media", |
663ef859 | 338 | Some(drive.clone()), |
a1c55753 | 339 | move |worker, config| { |
3cdd1a34 DM |
340 | if let Some(ref label) = label_text { |
341 | task_log!(worker, "try to load media '{}'", label); | |
342 | if let Some((mut changer, _)) = media_changer(&config, &drive)? { | |
343 | changer.load_media(label)?; | |
344 | } | |
345 | } | |
346 | ||
347 | let mut handle = open_drive(&config, &drive)?; | |
7b1bf4c0 | 348 | |
3cdd1a34 | 349 | match handle.read_label() { |
7b1bf4c0 | 350 | Err(err) => { |
be61c56c DC |
351 | if let Some(label) = label_text { |
352 | bail!("expected label '{}', found unrelated data", label); | |
353 | } | |
7b1bf4c0 DM |
354 | /* assume drive contains no or unrelated data */ |
355 | task_log!(worker, "unable to read media label: {}", err); | |
e29f456e DM |
356 | task_log!(worker, "format anyways"); |
357 | handle.format_media(fast.unwrap_or(true))?; | |
7b1bf4c0 DM |
358 | } |
359 | Ok((None, _)) => { | |
be61c56c DC |
360 | if let Some(label) = label_text { |
361 | bail!("expected label '{}', found empty tape", label); | |
362 | } | |
e29f456e DM |
363 | task_log!(worker, "found empty media - format anyways"); |
364 | handle.format_media(fast.unwrap_or(true))?; | |
7b1bf4c0 DM |
365 | } |
366 | Ok((Some(media_id), _key_config)) => { | |
be61c56c DC |
367 | if let Some(label_text) = label_text { |
368 | if media_id.label.label_text != label_text { | |
369 | bail!( | |
370 | "expected label '{}', found '{}', aborting", | |
371 | label_text, | |
372 | media_id.label.label_text | |
373 | ); | |
374 | } | |
375 | } | |
376 | ||
7b1bf4c0 DM |
377 | task_log!( |
378 | worker, | |
379 | "found media '{}' with uuid '{}'", | |
380 | media_id.label.label_text, media_id.label.uuid, | |
381 | ); | |
382 | ||
383 | let status_path = Path::new(TAPE_STATUS_DIR); | |
30316192 DM |
384 | let mut inventory = Inventory::new(status_path); |
385 | ||
386 | if let Some(MediaSetLabel { ref pool, ref uuid, ..}) = media_id.media_set_label { | |
387 | let _pool_lock = lock_media_pool(status_path, pool)?; | |
388 | let _media_set_lock = lock_media_set(status_path, uuid, None)?; | |
389 | MediaCatalog::destroy(status_path, &media_id.label.uuid)?; | |
390 | inventory.remove_media(&media_id.label.uuid)?; | |
391 | } else { | |
392 | let _lock = lock_unassigned_media_pool(status_path)?; | |
393 | MediaCatalog::destroy(status_path, &media_id.label.uuid)?; | |
394 | inventory.remove_media(&media_id.label.uuid)?; | |
395 | }; | |
7b1bf4c0 | 396 | |
e29f456e | 397 | handle.format_media(fast.unwrap_or(true))?; |
7b1bf4c0 DM |
398 | } |
399 | } | |
400 | ||
663ef859 | 401 | Ok(()) |
a1c55753 | 402 | }, |
663ef859 DM |
403 | )?; |
404 | ||
405 | Ok(upid_str.into()) | |
583a68a4 DM |
406 | } |
407 | ||
5fb694e8 DM |
408 | #[api( |
409 | input: { | |
410 | properties: { | |
411 | drive: { | |
49c965a4 | 412 | schema: DRIVE_NAME_SCHEMA, |
5fb694e8 DM |
413 | }, |
414 | }, | |
415 | }, | |
663ef859 DM |
416 | returns: { |
417 | schema: UPID_SCHEMA, | |
418 | }, | |
b4975d31 DM |
419 | access: { |
420 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
421 | }, | |
5fb694e8 DM |
422 | )] |
423 | /// Rewind tape | |
663ef859 DM |
424 | pub fn rewind( |
425 | drive: String, | |
426 | rpcenv: &mut dyn RpcEnvironment, | |
427 | ) -> Result<Value, Error> { | |
a1c55753 DC |
428 | let upid_str = run_drive_worker( |
429 | rpcenv, | |
430 | drive.clone(), | |
663ef859 DM |
431 | "rewind-media", |
432 | Some(drive.clone()), | |
a1c55753 | 433 | move |_worker, config| { |
663ef859 DM |
434 | let mut drive = open_drive(&config, &drive)?; |
435 | drive.rewind()?; | |
436 | Ok(()) | |
a1c55753 | 437 | }, |
663ef859 DM |
438 | )?; |
439 | ||
440 | Ok(upid_str.into()) | |
5fb694e8 DM |
441 | } |
442 | ||
0098b712 DM |
443 | #[api( |
444 | input: { | |
445 | properties: { | |
446 | drive: { | |
49c965a4 | 447 | schema: DRIVE_NAME_SCHEMA, |
0098b712 DM |
448 | }, |
449 | }, | |
450 | }, | |
41dacd5d DM |
451 | returns: { |
452 | schema: UPID_SCHEMA, | |
453 | }, | |
b4975d31 DM |
454 | access: { |
455 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
456 | }, | |
0098b712 DM |
457 | )] |
458 | /// Eject/Unload drive media | |
41dacd5d DM |
459 | pub fn eject_media( |
460 | drive: String, | |
461 | rpcenv: &mut dyn RpcEnvironment, | |
462 | ) -> Result<Value, Error> { | |
a1c55753 DC |
463 | let upid_str = run_drive_worker( |
464 | rpcenv, | |
465 | drive.clone(), | |
41dacd5d DM |
466 | "eject-media", |
467 | Some(drive.clone()), | |
a1c55753 DC |
468 | move |_worker, config| { |
469 | if let Some((mut changer, _)) = media_changer(&config, &drive)? { | |
41dacd5d DM |
470 | changer.unload_media(None)?; |
471 | } else { | |
472 | let mut drive = open_drive(&config, &drive)?; | |
473 | drive.eject_media()?; | |
474 | } | |
475 | Ok(()) | |
a1c55753 DC |
476 | }, |
477 | )?; | |
41dacd5d DM |
478 | |
479 | Ok(upid_str.into()) | |
0098b712 DM |
480 | } |
481 | ||
7bb720cb DM |
482 | #[api( |
483 | input: { | |
484 | properties: { | |
485 | drive: { | |
49c965a4 | 486 | schema: DRIVE_NAME_SCHEMA, |
7bb720cb | 487 | }, |
8446fbca | 488 | "label-text": { |
7bb720cb DM |
489 | schema: MEDIA_LABEL_SCHEMA, |
490 | }, | |
491 | pool: { | |
492 | schema: MEDIA_POOL_NAME_SCHEMA, | |
493 | optional: true, | |
494 | }, | |
495 | }, | |
496 | }, | |
6dbad5b4 DM |
497 | returns: { |
498 | schema: UPID_SCHEMA, | |
499 | }, | |
b4975d31 DM |
500 | access: { |
501 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false), | |
502 | }, | |
7bb720cb DM |
503 | )] |
504 | /// Label media | |
505 | /// | |
506 | /// Write a new media label to the media in 'drive'. The media is | |
507 | /// assigned to the specified 'pool', or else to the free media pool. | |
508 | /// | |
e29f456e | 509 | /// Note: The media need to be empty (you may want to format it first). |
7bb720cb DM |
510 | pub fn label_media( |
511 | drive: String, | |
512 | pool: Option<String>, | |
8446fbca | 513 | label_text: String, |
6dbad5b4 DM |
514 | rpcenv: &mut dyn RpcEnvironment, |
515 | ) -> Result<Value, Error> { | |
7bb720cb DM |
516 | if let Some(ref pool) = pool { |
517 | let (pool_config, _digest) = config::media_pool::config()?; | |
518 | ||
519 | if pool_config.sections.get(pool).is_none() { | |
520 | bail!("no such pool ('{}')", pool); | |
521 | } | |
522 | } | |
a1c55753 DC |
523 | let upid_str = run_drive_worker( |
524 | rpcenv, | |
525 | drive.clone(), | |
6dbad5b4 DM |
526 | "label-media", |
527 | Some(drive.clone()), | |
a1c55753 | 528 | move |worker, config| { |
6dbad5b4 DM |
529 | let mut drive = open_drive(&config, &drive)?; |
530 | ||
531 | drive.rewind()?; | |
532 | ||
533 | match drive.read_next_file() { | |
318b3106 DM |
534 | Ok(_reader) => bail!("media is not empty (format it first)"), |
535 | Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ }, | |
536 | Err(BlockReadError::EndOfStream) => { /* tape is empty */ }, | |
6dbad5b4 | 537 | Err(err) => { |
318b3106 | 538 | bail!("media read error - {}", err); |
6dbad5b4 DM |
539 | } |
540 | } | |
7bb720cb | 541 | |
6dbad5b4 | 542 | let ctime = proxmox::tools::time::epoch_i64(); |
a78348ac | 543 | let label = MediaLabel { |
8446fbca | 544 | label_text: label_text.to_string(), |
6dbad5b4 DM |
545 | uuid: Uuid::generate(), |
546 | ctime, | |
547 | }; | |
7bb720cb | 548 | |
6dbad5b4 | 549 | write_media_label(worker, &mut drive, label, pool) |
a1c55753 | 550 | }, |
6dbad5b4 | 551 | )?; |
7bb720cb | 552 | |
6dbad5b4 | 553 | Ok(upid_str.into()) |
7bb720cb DM |
554 | } |
555 | ||
556 | fn write_media_label( | |
6dbad5b4 | 557 | worker: Arc<WorkerTask>, |
7bb720cb | 558 | drive: &mut Box<dyn TapeDriver>, |
a78348ac | 559 | label: MediaLabel, |
7bb720cb DM |
560 | pool: Option<String>, |
561 | ) -> Result<(), Error> { | |
562 | ||
563 | drive.label_tape(&label)?; | |
564 | ||
30316192 | 565 | let status_path = Path::new(TAPE_STATUS_DIR); |
7bb720cb | 566 | |
30316192 | 567 | let media_id = if let Some(ref pool) = pool { |
7bb720cb | 568 | // assign media to pool by writing special media set label |
8446fbca | 569 | worker.log(format!("Label media '{}' for pool '{}'", label.label_text, pool)); |
8a0046f5 | 570 | let set = MediaSetLabel::with_data(&pool, [0u8; 16].into(), 0, label.ctime, None); |
7bb720cb | 571 | |
feb1645f | 572 | drive.write_media_set_label(&set, None)?; |
30316192 DM |
573 | |
574 | let media_id = MediaId { label, media_set_label: Some(set) }; | |
575 | ||
576 | // Create the media catalog | |
577 | MediaCatalog::overwrite(status_path, &media_id, false)?; | |
578 | ||
579 | let mut inventory = Inventory::new(status_path); | |
580 | inventory.store(media_id.clone(), false)?; | |
581 | ||
582 | media_id | |
7bb720cb | 583 | } else { |
8446fbca | 584 | worker.log(format!("Label media '{}' (no pool assignment)", label.label_text)); |
7bb720cb | 585 | |
30316192 | 586 | let media_id = MediaId { label, media_set_label: None }; |
7bb720cb | 587 | |
30316192 DM |
588 | // Create the media catalog |
589 | MediaCatalog::overwrite(status_path, &media_id, false)?; | |
34605654 | 590 | |
30316192 DM |
591 | let mut inventory = Inventory::new(status_path); |
592 | inventory.store(media_id.clone(), false)?; | |
34605654 | 593 | |
30316192 DM |
594 | media_id |
595 | }; | |
7bb720cb DM |
596 | |
597 | drive.rewind()?; | |
598 | ||
599 | match drive.read_label() { | |
feb1645f | 600 | Ok((Some(info), _)) => { |
7bb720cb DM |
601 | if info.label.uuid != media_id.label.uuid { |
602 | bail!("verify label failed - got wrong label uuid"); | |
603 | } | |
604 | if let Some(ref pool) = pool { | |
605 | match info.media_set_label { | |
fe6c1938 | 606 | Some(set) => { |
7bb720cb DM |
607 | if set.uuid != [0u8; 16].into() { |
608 | bail!("verify media set label failed - got wrong set uuid"); | |
609 | } | |
610 | if &set.pool != pool { | |
611 | bail!("verify media set label failed - got wrong pool"); | |
612 | } | |
613 | } | |
614 | None => { | |
615 | bail!("verify media set label failed (missing set label)"); | |
616 | } | |
617 | } | |
618 | } | |
619 | }, | |
feb1645f | 620 | Ok((None, _)) => bail!("verify label failed (got empty media)"), |
7bb720cb DM |
621 | Err(err) => bail!("verify label failed - {}", err), |
622 | }; | |
623 | ||
624 | drive.rewind()?; | |
625 | ||
626 | Ok(()) | |
627 | } | |
628 | ||
4606f343 | 629 | #[api( |
feb1645f DM |
630 | protected: true, |
631 | input: { | |
632 | properties: { | |
633 | drive: { | |
634 | schema: DRIVE_NAME_SCHEMA, | |
635 | }, | |
636 | password: { | |
637 | description: "Encryption key password.", | |
638 | }, | |
639 | }, | |
640 | }, | |
b4975d31 DM |
641 | access: { |
642 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
643 | }, | |
feb1645f DM |
644 | )] |
645 | /// Try to restore a tape encryption key | |
646 | pub async fn restore_key( | |
647 | drive: String, | |
648 | password: String, | |
649 | ) -> Result<(), Error> { | |
47a72414 DC |
650 | run_drive_blocking_task( |
651 | drive.clone(), | |
652 | "restore key".to_string(), | |
653 | move |config| { | |
654 | let mut drive = open_drive(&config, &drive)?; | |
feb1645f | 655 | |
47a72414 | 656 | let (_media_id, key_config) = drive.read_label()?; |
feb1645f | 657 | |
47a72414 DC |
658 | if let Some(key_config) = key_config { |
659 | let password_fn = || { Ok(password.as_bytes().to_vec()) }; | |
660 | let (key, ..) = key_config.decrypt(&password_fn)?; | |
661 | config::tape_encryption_keys::insert_key(key, key_config, true)?; | |
662 | } else { | |
663 | bail!("media does not contain any encryption key configuration"); | |
664 | } | |
feb1645f | 665 | |
47a72414 | 666 | Ok(()) |
feb1645f | 667 | } |
47a72414 DC |
668 | ) |
669 | .await | |
feb1645f DM |
670 | } |
671 | ||
672 | #[api( | |
4606f343 DM |
673 | input: { |
674 | properties: { | |
675 | drive: { | |
49c965a4 | 676 | schema: DRIVE_NAME_SCHEMA, |
4606f343 | 677 | }, |
781da7f6 DM |
678 | inventorize: { |
679 | description: "Inventorize media", | |
680 | optional: true, | |
681 | }, | |
4606f343 DM |
682 | }, |
683 | }, | |
684 | returns: { | |
fe6c1938 | 685 | type: MediaIdFlat, |
4606f343 | 686 | }, |
b4975d31 DM |
687 | access: { |
688 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
689 | }, | |
4606f343 | 690 | )] |
feb1645f | 691 | /// Read media label (optionally inventorize media) |
781da7f6 DM |
692 | pub async fn read_label( |
693 | drive: String, | |
694 | inventorize: Option<bool>, | |
695 | ) -> Result<MediaIdFlat, Error> { | |
47a72414 DC |
696 | run_drive_blocking_task( |
697 | drive.clone(), | |
698 | "reading label".to_string(), | |
699 | move |config| { | |
700 | let mut drive = open_drive(&config, &drive)?; | |
4606f343 | 701 | |
47a72414 DC |
702 | let (media_id, _key_config) = drive.read_label()?; |
703 | ||
704 | let media_id = match media_id { | |
705 | Some(media_id) => { | |
706 | let mut flat = MediaIdFlat { | |
707 | uuid: media_id.label.uuid.clone(), | |
708 | label_text: media_id.label.label_text.clone(), | |
709 | ctime: media_id.label.ctime, | |
710 | media_set_ctime: None, | |
711 | media_set_uuid: None, | |
712 | encryption_key_fingerprint: None, | |
713 | pool: None, | |
714 | seq_nr: None, | |
715 | }; | |
716 | if let Some(ref set) = media_id.media_set_label { | |
717 | flat.pool = Some(set.pool.clone()); | |
718 | flat.seq_nr = Some(set.seq_nr); | |
719 | flat.media_set_uuid = Some(set.uuid.clone()); | |
720 | flat.media_set_ctime = Some(set.ctime); | |
721 | flat.encryption_key_fingerprint = set | |
722 | .encryption_key_fingerprint | |
723 | .as_ref() | |
770a36e5 | 724 | .map(|fp| pbs_tools::format::as_fingerprint(fp.bytes())); |
47a72414 DC |
725 | |
726 | let encrypt_fingerprint = set.encryption_key_fingerprint.clone() | |
727 | .map(|fp| (fp, set.uuid.clone())); | |
728 | ||
729 | if let Err(err) = drive.set_encryption(encrypt_fingerprint) { | |
730 | // try, but ignore errors. just log to stderr | |
30316192 | 731 | eprintln!("unable to load encryption key: {}", err); |
47a72414 DC |
732 | } |
733 | } | |
25aa55b5 | 734 | |
47a72414 DC |
735 | if let Some(true) = inventorize { |
736 | let state_path = Path::new(TAPE_STATUS_DIR); | |
30316192 DM |
737 | let mut inventory = Inventory::new(state_path); |
738 | ||
739 | if let Some(MediaSetLabel { ref pool, ref uuid, ..}) = media_id.media_set_label { | |
740 | let _pool_lock = lock_media_pool(state_path, pool)?; | |
741 | let _lock = lock_media_set(state_path, uuid, None)?; | |
742 | MediaCatalog::destroy_unrelated_catalog(state_path, &media_id)?; | |
743 | inventory.store(media_id, false)?; | |
744 | } else { | |
745 | let _lock = lock_unassigned_media_pool(state_path)?; | |
746 | MediaCatalog::destroy(state_path, &media_id.label.uuid)?; | |
747 | inventory.store(media_id, false)?; | |
748 | }; | |
780bc4ca | 749 | } |
781da7f6 | 750 | |
47a72414 | 751 | flat |
781da7f6 | 752 | } |
47a72414 DC |
753 | None => { |
754 | bail!("Media is empty (no label)."); | |
755 | } | |
756 | }; | |
781da7f6 | 757 | |
47a72414 DC |
758 | Ok(media_id) |
759 | } | |
760 | ) | |
761 | .await | |
4606f343 DM |
762 | } |
763 | ||
df69a4fc DM |
764 | #[api( |
765 | input: { | |
766 | properties: { | |
767 | drive: { | |
768 | schema: DRIVE_NAME_SCHEMA, | |
769 | }, | |
770 | }, | |
771 | }, | |
772 | returns: { | |
773 | schema: UPID_SCHEMA, | |
774 | }, | |
b4975d31 DM |
775 | access: { |
776 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
777 | }, | |
df69a4fc DM |
778 | )] |
779 | /// Clean drive | |
780 | pub fn clean_drive( | |
781 | drive: String, | |
782 | rpcenv: &mut dyn RpcEnvironment, | |
783 | ) -> Result<Value, Error> { | |
a1c55753 DC |
784 | let upid_str = run_drive_worker( |
785 | rpcenv, | |
786 | drive.clone(), | |
df69a4fc DM |
787 | "clean-drive", |
788 | Some(drive.clone()), | |
a1c55753 | 789 | move |worker, config| { |
df69a4fc DM |
790 | let (mut changer, _changer_name) = required_media_changer(&config, &drive)?; |
791 | ||
792 | worker.log("Starting drive clean"); | |
793 | ||
794 | changer.clean_drive()?; | |
795 | ||
a79082a0 | 796 | if let Ok(drive_config) = config.lookup::<LtoTapeDrive>("lto", &drive) { |
d95c74c6 | 797 | // Note: clean_drive unloads the cleaning media, so we cannot use drive_config.open |
a79082a0 | 798 | let mut handle = LtoTapeHandle::new(open_lto_tape_device(&drive_config.path)?)?; |
d95c74c6 DM |
799 | |
800 | // test for critical tape alert flags | |
801 | if let Ok(alert_flags) = handle.tape_alert_flags() { | |
802 | if !alert_flags.is_empty() { | |
803 | worker.log(format!("TapeAlertFlags: {:?}", alert_flags)); | |
804 | if tape_alert_flags_critical(alert_flags) { | |
805 | bail!("found critical tape alert flags: {:?}", alert_flags); | |
806 | } | |
807 | } | |
808 | } | |
809 | ||
810 | // test wearout (max. 50 mounts) | |
811 | if let Ok(volume_stats) = handle.volume_statistics() { | |
812 | worker.log(format!("Volume mounts: {}", volume_stats.volume_mounts)); | |
813 | let wearout = volume_stats.volume_mounts * 2; // (*100.0/50.0); | |
814 | worker.log(format!("Cleaning tape wearout: {}%", wearout)); | |
815 | } | |
816 | } | |
817 | ||
d1d74c43 | 818 | worker.log("Drive cleaned successfully"); |
df69a4fc DM |
819 | |
820 | Ok(()) | |
a1c55753 DC |
821 | }, |
822 | )?; | |
df69a4fc DM |
823 | |
824 | Ok(upid_str.into()) | |
825 | } | |
826 | ||
83abc749 DM |
827 | #[api( |
828 | input: { | |
829 | properties: { | |
830 | drive: { | |
49c965a4 | 831 | schema: DRIVE_NAME_SCHEMA, |
83abc749 | 832 | }, |
83abc749 DM |
833 | }, |
834 | }, | |
835 | returns: { | |
836 | description: "The list of media labels with associated media Uuid (if any).", | |
837 | type: Array, | |
838 | items: { | |
839 | type: LabelUuidMap, | |
840 | }, | |
841 | }, | |
b4975d31 DM |
842 | access: { |
843 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
844 | }, | |
83abc749 | 845 | )] |
e92c7581 | 846 | /// List known media labels (Changer Inventory) |
83abc749 DM |
847 | /// |
848 | /// Note: Only useful for drives with associated changer device. | |
849 | /// | |
e92c7581 DM |
850 | /// This method queries the changer to get a list of media labels. |
851 | /// | |
852 | /// Note: This updates the media online status. | |
66dbe563 | 853 | pub async fn inventory( |
83abc749 | 854 | drive: String, |
83abc749 | 855 | ) -> Result<Vec<LabelUuidMap>, Error> { |
47a72414 DC |
856 | run_drive_blocking_task( |
857 | drive.clone(), | |
858 | "inventorize".to_string(), | |
859 | move |config| { | |
860 | let (mut changer, changer_name) = required_media_changer(&config, &drive)?; | |
83abc749 | 861 | |
47a72414 | 862 | let label_text_list = changer.online_media_label_texts()?; |
25aa55b5 | 863 | |
47a72414 | 864 | let state_path = Path::new(TAPE_STATUS_DIR); |
83abc749 | 865 | |
47a72414 | 866 | let mut inventory = Inventory::load(state_path)?; |
83abc749 | 867 | |
47a72414 DC |
868 | update_changer_online_status( |
869 | &config, | |
870 | &mut inventory, | |
871 | &changer_name, | |
872 | &label_text_list, | |
873 | )?; | |
83abc749 | 874 | |
47a72414 | 875 | let mut list = Vec::new(); |
83abc749 | 876 | |
47a72414 DC |
877 | for label_text in label_text_list.iter() { |
878 | if label_text.starts_with("CLN") { | |
879 | // skip cleaning unit | |
880 | continue; | |
881 | } | |
83abc749 | 882 | |
47a72414 | 883 | let label_text = label_text.to_string(); |
83abc749 | 884 | |
47a72414 DC |
885 | if let Some(media_id) = inventory.find_media_by_label_text(&label_text) { |
886 | list.push(LabelUuidMap { label_text, uuid: Some(media_id.label.uuid.clone()) }); | |
887 | } else { | |
888 | list.push(LabelUuidMap { label_text, uuid: None }); | |
889 | } | |
66dbe563 | 890 | } |
83abc749 | 891 | |
47a72414 | 892 | Ok(list) |
83abc749 | 893 | } |
47a72414 DC |
894 | ) |
895 | .await | |
e92c7581 | 896 | } |
83abc749 | 897 | |
e92c7581 DM |
898 | #[api( |
899 | input: { | |
900 | properties: { | |
901 | drive: { | |
49c965a4 | 902 | schema: DRIVE_NAME_SCHEMA, |
e92c7581 DM |
903 | }, |
904 | "read-all-labels": { | |
905 | description: "Load all tapes and try read labels (even if already inventoried)", | |
906 | type: bool, | |
907 | optional: true, | |
908 | }, | |
909 | }, | |
910 | }, | |
911 | returns: { | |
912 | schema: UPID_SCHEMA, | |
913 | }, | |
b4975d31 DM |
914 | access: { |
915 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
916 | }, | |
e92c7581 DM |
917 | )] |
918 | /// Update inventory | |
919 | /// | |
920 | /// Note: Only useful for drives with associated changer device. | |
921 | /// | |
922 | /// This method queries the changer to get a list of media labels. It | |
923 | /// then loads any unknown media into the drive, reads the label, and | |
924 | /// store the result to the media database. | |
925 | /// | |
926 | /// Note: This updates the media online status. | |
927 | pub fn update_inventory( | |
928 | drive: String, | |
929 | read_all_labels: Option<bool>, | |
930 | rpcenv: &mut dyn RpcEnvironment, | |
931 | ) -> Result<Value, Error> { | |
a1c55753 DC |
932 | let upid_str = run_drive_worker( |
933 | rpcenv, | |
934 | drive.clone(), | |
e92c7581 DM |
935 | "inventory-update", |
936 | Some(drive.clone()), | |
a1c55753 | 937 | move |worker, config| { |
284eb5da | 938 | let (mut changer, changer_name) = required_media_changer(&config, &drive)?; |
83abc749 | 939 | |
8446fbca DM |
940 | let label_text_list = changer.online_media_label_texts()?; |
941 | if label_text_list.is_empty() { | |
3b82f3ee | 942 | worker.log("changer device does not list any media labels".to_string()); |
83abc749 | 943 | } |
e92c7581 DM |
944 | |
945 | let state_path = Path::new(TAPE_STATUS_DIR); | |
946 | ||
947 | let mut inventory = Inventory::load(state_path)?; | |
e92c7581 | 948 | |
8446fbca | 949 | update_changer_online_status(&config, &mut inventory, &changer_name, &label_text_list)?; |
e92c7581 | 950 | |
8446fbca DM |
951 | for label_text in label_text_list.iter() { |
952 | if label_text.starts_with("CLN") { | |
953 | worker.log(format!("skip cleaning unit '{}'", label_text)); | |
e92c7581 DM |
954 | continue; |
955 | } | |
956 | ||
8446fbca | 957 | let label_text = label_text.to_string(); |
e92c7581 | 958 | |
3984a5fd FG |
959 | if !read_all_labels.unwrap_or(false) && inventory.find_media_by_label_text(&label_text).is_some() { |
960 | worker.log(format!("media '{}' already inventoried", label_text)); | |
961 | continue; | |
e92c7581 DM |
962 | } |
963 | ||
8446fbca DM |
964 | if let Err(err) = changer.load_media(&label_text) { |
965 | worker.warn(format!("unable to load media '{}' - {}", label_text, err)); | |
83abc749 DM |
966 | continue; |
967 | } | |
e92c7581 DM |
968 | |
969 | let mut drive = open_drive(&config, &drive)?; | |
970 | match drive.read_label() { | |
971 | Err(err) => { | |
8446fbca | 972 | worker.warn(format!("unable to read label form media '{}' - {}", label_text, err)); |
e92c7581 | 973 | } |
feb1645f | 974 | Ok((None, _)) => { |
8446fbca | 975 | worker.log(format!("media '{}' is empty", label_text)); |
e92c7581 | 976 | } |
feb1645f | 977 | Ok((Some(media_id), _key_config)) => { |
8446fbca | 978 | if label_text != media_id.label.label_text { |
d1d74c43 | 979 | worker.warn(format!("label text mismatch ({} != {})", label_text, media_id.label.label_text)); |
e92c7581 DM |
980 | continue; |
981 | } | |
8446fbca | 982 | worker.log(format!("inventorize media '{}' with uuid '{}'", label_text, media_id.label.uuid)); |
30316192 DM |
983 | |
984 | if let Some(MediaSetLabel { ref pool, ref uuid, ..}) = media_id.media_set_label { | |
985 | let _pool_lock = lock_media_pool(state_path, pool)?; | |
986 | let _lock = lock_media_set(state_path, uuid, None)?; | |
987 | MediaCatalog::destroy_unrelated_catalog(state_path, &media_id)?; | |
988 | inventory.store(media_id, false)?; | |
989 | } else { | |
990 | let _lock = lock_unassigned_media_pool(state_path)?; | |
991 | MediaCatalog::destroy(state_path, &media_id.label.uuid)?; | |
992 | inventory.store(media_id, false)?; | |
993 | }; | |
e92c7581 DM |
994 | } |
995 | } | |
df69a4fc | 996 | changer.unload_media(None)?; |
83abc749 | 997 | } |
e92c7581 | 998 | Ok(()) |
a1c55753 | 999 | }, |
e92c7581 | 1000 | )?; |
83abc749 | 1001 | |
e92c7581 | 1002 | Ok(upid_str.into()) |
83abc749 DM |
1003 | } |
1004 | ||
1005 | ||
bff7e3f3 DM |
1006 | #[api( |
1007 | input: { | |
1008 | properties: { | |
1009 | drive: { | |
49c965a4 | 1010 | schema: DRIVE_NAME_SCHEMA, |
bff7e3f3 DM |
1011 | }, |
1012 | pool: { | |
1013 | schema: MEDIA_POOL_NAME_SCHEMA, | |
1014 | optional: true, | |
1015 | }, | |
1016 | }, | |
1017 | }, | |
6dbad5b4 DM |
1018 | returns: { |
1019 | schema: UPID_SCHEMA, | |
1020 | }, | |
b4975d31 DM |
1021 | access: { |
1022 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false), | |
1023 | }, | |
bff7e3f3 DM |
1024 | )] |
1025 | /// Label media with barcodes from changer device | |
1026 | pub fn barcode_label_media( | |
1027 | drive: String, | |
1028 | pool: Option<String>, | |
6dbad5b4 DM |
1029 | rpcenv: &mut dyn RpcEnvironment, |
1030 | ) -> Result<Value, Error> { | |
bff7e3f3 DM |
1031 | if let Some(ref pool) = pool { |
1032 | let (pool_config, _digest) = config::media_pool::config()?; | |
1033 | ||
1034 | if pool_config.sections.get(pool).is_none() { | |
1035 | bail!("no such pool ('{}')", pool); | |
1036 | } | |
1037 | } | |
1038 | ||
a1c55753 DC |
1039 | let upid_str = run_drive_worker( |
1040 | rpcenv, | |
1041 | drive.clone(), | |
6dbad5b4 DM |
1042 | "barcode-label-media", |
1043 | Some(drive.clone()), | |
a1c55753 | 1044 | move |worker, config| barcode_label_media_worker(worker, drive, &config, pool), |
6dbad5b4 DM |
1045 | )?; |
1046 | ||
1047 | Ok(upid_str.into()) | |
1048 | } | |
1049 | ||
1050 | fn barcode_label_media_worker( | |
1051 | worker: Arc<WorkerTask>, | |
1052 | drive: String, | |
25aa55b5 | 1053 | drive_config: &SectionConfigData, |
6dbad5b4 DM |
1054 | pool: Option<String>, |
1055 | ) -> Result<(), Error> { | |
25aa55b5 | 1056 | let (mut changer, changer_name) = required_media_changer(drive_config, &drive)?; |
bff7e3f3 | 1057 | |
af762341 DM |
1058 | let mut label_text_list = changer.online_media_label_texts()?; |
1059 | ||
1060 | // make sure we label them in the right order | |
1061 | label_text_list.sort(); | |
bff7e3f3 DM |
1062 | |
1063 | let state_path = Path::new(TAPE_STATUS_DIR); | |
1064 | ||
1065 | let mut inventory = Inventory::load(state_path)?; | |
bff7e3f3 | 1066 | |
25aa55b5 | 1067 | update_changer_online_status(drive_config, &mut inventory, &changer_name, &label_text_list)?; |
bff7e3f3 | 1068 | |
8446fbca | 1069 | if label_text_list.is_empty() { |
bff7e3f3 DM |
1070 | bail!("changer device does not list any media labels"); |
1071 | } | |
1072 | ||
8446fbca DM |
1073 | for label_text in label_text_list { |
1074 | if label_text.starts_with("CLN") { continue; } | |
bff7e3f3 DM |
1075 | |
1076 | inventory.reload()?; | |
8446fbca DM |
1077 | if inventory.find_media_by_label_text(&label_text).is_some() { |
1078 | worker.log(format!("media '{}' already inventoried (already labeled)", label_text)); | |
bff7e3f3 DM |
1079 | continue; |
1080 | } | |
1081 | ||
8446fbca | 1082 | worker.log(format!("checking/loading media '{}'", label_text)); |
bff7e3f3 | 1083 | |
8446fbca DM |
1084 | if let Err(err) = changer.load_media(&label_text) { |
1085 | worker.warn(format!("unable to load media '{}' - {}", label_text, err)); | |
bff7e3f3 DM |
1086 | continue; |
1087 | } | |
1088 | ||
25aa55b5 | 1089 | let mut drive = open_drive(drive_config, &drive)?; |
bff7e3f3 DM |
1090 | drive.rewind()?; |
1091 | ||
1092 | match drive.read_next_file() { | |
318b3106 | 1093 | Ok(_reader) => { |
e29f456e | 1094 | worker.log(format!("media '{}' is not empty (format it first)", label_text)); |
bff7e3f3 DM |
1095 | continue; |
1096 | } | |
318b3106 DM |
1097 | Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ }, |
1098 | Err(BlockReadError::EndOfStream) => { /* tape is empty */ }, | |
1099 | Err(_err) => { | |
1100 | worker.warn(format!("media '{}' read error (maybe not empty - format it first)", label_text)); | |
1101 | continue; | |
bff7e3f3 DM |
1102 | } |
1103 | } | |
1104 | ||
1105 | let ctime = proxmox::tools::time::epoch_i64(); | |
a78348ac | 1106 | let label = MediaLabel { |
8446fbca | 1107 | label_text: label_text.to_string(), |
bff7e3f3 DM |
1108 | uuid: Uuid::generate(), |
1109 | ctime, | |
1110 | }; | |
1111 | ||
6dbad5b4 | 1112 | write_media_label(worker.clone(), &mut drive, label, pool.clone())? |
bff7e3f3 DM |
1113 | } |
1114 | ||
1115 | Ok(()) | |
1116 | } | |
1117 | ||
1e20f819 DM |
1118 | #[api( |
1119 | input: { | |
1120 | properties: { | |
1121 | drive: { | |
1122 | schema: DRIVE_NAME_SCHEMA, | |
1123 | }, | |
1124 | }, | |
1125 | }, | |
1126 | returns: { | |
1127 | description: "A List of medium auxiliary memory attributes.", | |
1128 | type: Array, | |
1129 | items: { | |
1130 | type: MamAttribute, | |
1131 | }, | |
1132 | }, | |
b4975d31 DM |
1133 | access: { |
1134 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false), | |
1135 | }, | |
1e20f819 | 1136 | )] |
ee01737e | 1137 | /// Read Cartridge Memory (Medium auxiliary memory attributes) |
41e66bfa DC |
1138 | pub async fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error> { |
1139 | run_drive_blocking_task( | |
1140 | drive.clone(), | |
1141 | "reading cartridge memory".to_string(), | |
1142 | move |config| { | |
a79082a0 | 1143 | let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?; |
1ce8e905 | 1144 | let mut handle = open_lto_tape_drive(&drive_config)?; |
1e20f819 | 1145 | |
41e66bfa DC |
1146 | handle.cartridge_memory() |
1147 | } | |
1148 | ) | |
1149 | .await | |
1e20f819 DM |
1150 | } |
1151 | ||
5f34d69b DM |
1152 | #[api( |
1153 | input: { | |
1154 | properties: { | |
1155 | drive: { | |
1156 | schema: DRIVE_NAME_SCHEMA, | |
1157 | }, | |
1158 | }, | |
1159 | }, | |
1160 | returns: { | |
1161 | type: Lp17VolumeStatistics, | |
1162 | }, | |
b4975d31 DM |
1163 | access: { |
1164 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false), | |
1165 | }, | |
5f34d69b DM |
1166 | )] |
1167 | /// Read Volume Statistics (SCSI log page 17h) | |
41e66bfa DC |
1168 | pub async fn volume_statistics(drive: String) -> Result<Lp17VolumeStatistics, Error> { |
1169 | run_drive_blocking_task( | |
1170 | drive.clone(), | |
1171 | "reading volume statistics".to_string(), | |
1172 | move |config| { | |
a79082a0 | 1173 | let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?; |
1ce8e905 | 1174 | let mut handle = open_lto_tape_drive(&drive_config)?; |
5f34d69b | 1175 | |
41e66bfa DC |
1176 | handle.volume_statistics() |
1177 | } | |
1178 | ) | |
1179 | .await | |
5f34d69b DM |
1180 | } |
1181 | ||
cb80d900 DM |
1182 | #[api( |
1183 | input: { | |
1184 | properties: { | |
1185 | drive: { | |
1186 | schema: DRIVE_NAME_SCHEMA, | |
1187 | }, | |
1188 | }, | |
1189 | }, | |
1190 | returns: { | |
a79082a0 | 1191 | type: LtoDriveAndMediaStatus, |
cb80d900 | 1192 | }, |
b4975d31 DM |
1193 | access: { |
1194 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false), | |
1195 | }, | |
cb80d900 | 1196 | )] |
5ae86dfa | 1197 | /// Get drive/media status |
a79082a0 | 1198 | pub async fn status(drive: String) -> Result<LtoDriveAndMediaStatus, Error> { |
41e66bfa DC |
1199 | run_drive_blocking_task( |
1200 | drive.clone(), | |
1201 | "reading drive status".to_string(), | |
1202 | move |config| { | |
a79082a0 | 1203 | let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?; |
cb80d900 | 1204 | |
a79082a0 DM |
1205 | // Note: use open_lto_tape_device, because this also works if no medium loaded |
1206 | let file = open_lto_tape_device(&drive_config.path)?; | |
cb80d900 | 1207 | |
a79082a0 | 1208 | let mut handle = LtoTapeHandle::new(file)?; |
5ae86dfa | 1209 | |
41e66bfa DC |
1210 | handle.get_drive_and_media_status() |
1211 | } | |
1212 | ) | |
1213 | .await | |
cb80d900 DM |
1214 | } |
1215 | ||
b017bbc4 DM |
1216 | #[api( |
1217 | input: { | |
1218 | properties: { | |
1219 | drive: { | |
1220 | schema: DRIVE_NAME_SCHEMA, | |
1221 | }, | |
1222 | force: { | |
1223 | description: "Force overriding existing index.", | |
1224 | type: bool, | |
1225 | optional: true, | |
1226 | }, | |
c553407e DM |
1227 | scan: { |
1228 | description: "Re-read the whole tape to reconstruct the catalog instead of restoring saved versions.", | |
1229 | type: bool, | |
1230 | optional: true, | |
1231 | }, | |
b017bbc4 DM |
1232 | verbose: { |
1233 | description: "Verbose mode - log all found chunks.", | |
1234 | type: bool, | |
1235 | optional: true, | |
1236 | }, | |
1237 | }, | |
1238 | }, | |
1239 | returns: { | |
1240 | schema: UPID_SCHEMA, | |
1241 | }, | |
b4975d31 DM |
1242 | access: { |
1243 | permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false), | |
1244 | }, | |
b017bbc4 DM |
1245 | )] |
1246 | /// Scan media and record content | |
1247 | pub fn catalog_media( | |
1248 | drive: String, | |
1249 | force: Option<bool>, | |
c553407e | 1250 | scan: Option<bool>, |
b017bbc4 DM |
1251 | verbose: Option<bool>, |
1252 | rpcenv: &mut dyn RpcEnvironment, | |
1253 | ) -> Result<Value, Error> { | |
b017bbc4 DM |
1254 | let verbose = verbose.unwrap_or(false); |
1255 | let force = force.unwrap_or(false); | |
c553407e | 1256 | let scan = scan.unwrap_or(false); |
b017bbc4 | 1257 | |
a1c55753 DC |
1258 | let upid_str = run_drive_worker( |
1259 | rpcenv, | |
1260 | drive.clone(), | |
b017bbc4 DM |
1261 | "catalog-media", |
1262 | Some(drive.clone()), | |
a1c55753 | 1263 | move |worker, config| { |
b017bbc4 DM |
1264 | let mut drive = open_drive(&config, &drive)?; |
1265 | ||
1266 | drive.rewind()?; | |
1267 | ||
1268 | let media_id = match drive.read_label()? { | |
feb1645f | 1269 | (Some(media_id), key_config) => { |
b017bbc4 DM |
1270 | worker.log(format!( |
1271 | "found media label: {}", | |
1272 | serde_json::to_string_pretty(&serde_json::to_value(&media_id)?)? | |
1273 | )); | |
feb1645f DM |
1274 | if key_config.is_some() { |
1275 | worker.log(format!( | |
1276 | "encryption key config: {}", | |
1277 | serde_json::to_string_pretty(&serde_json::to_value(&key_config)?)? | |
1278 | )); | |
1279 | } | |
b017bbc4 DM |
1280 | media_id |
1281 | }, | |
feb1645f | 1282 | (None, _) => bail!("media is empty (no media label found)"), |
b017bbc4 DM |
1283 | }; |
1284 | ||
1285 | let status_path = Path::new(TAPE_STATUS_DIR); | |
1286 | ||
30316192 | 1287 | let mut inventory = Inventory::new(status_path); |
b017bbc4 | 1288 | |
074503f2 | 1289 | let (_media_set_lock, media_set_uuid) = match media_id.media_set_label { |
b017bbc4 DM |
1290 | None => { |
1291 | worker.log("media is empty"); | |
30316192 | 1292 | let _lock = lock_unassigned_media_pool(status_path)?; |
b017bbc4 | 1293 | MediaCatalog::destroy(status_path, &media_id.label.uuid)?; |
30316192 | 1294 | inventory.store(media_id.clone(), false)?; |
b017bbc4 DM |
1295 | return Ok(()); |
1296 | } | |
1297 | Some(ref set) => { | |
1298 | if set.uuid.as_ref() == [0u8;16] { // media is empty | |
1299 | worker.log("media is empty"); | |
30316192 | 1300 | let _lock = lock_unassigned_media_pool(status_path)?; |
b017bbc4 | 1301 | MediaCatalog::destroy(status_path, &media_id.label.uuid)?; |
30316192 | 1302 | inventory.store(media_id.clone(), false)?; |
b017bbc4 DM |
1303 | return Ok(()); |
1304 | } | |
2b191385 DM |
1305 | let encrypt_fingerprint = set.encryption_key_fingerprint.clone() |
1306 | .map(|fp| (fp, set.uuid.clone())); | |
1307 | ||
8a0046f5 DM |
1308 | drive.set_encryption(encrypt_fingerprint)?; |
1309 | ||
30316192 DM |
1310 | let _pool_lock = lock_media_pool(status_path, &set.pool)?; |
1311 | let media_set_lock = lock_media_set(status_path, &set.uuid, None)?; | |
1312 | ||
1313 | MediaCatalog::destroy_unrelated_catalog(status_path, &media_id)?; | |
1314 | ||
1315 | inventory.store(media_id.clone(), false)?; | |
1316 | ||
074503f2 | 1317 | (media_set_lock, &set.uuid) |
b017bbc4 DM |
1318 | } |
1319 | }; | |
1320 | ||
6334bdc1 FG |
1321 | if MediaCatalog::exists(status_path, &media_id.label.uuid) && !force { |
1322 | bail!("media catalog exists (please use --force to overwrite)"); | |
b017bbc4 DM |
1323 | } |
1324 | ||
c553407e DM |
1325 | if !scan { |
1326 | let media_set = inventory.compute_media_set_members(media_set_uuid)?; | |
1327 | ||
1328 | if fast_catalog_restore(&worker, &mut drive, &media_set, &media_id.label.uuid)? { | |
1329 | return Ok(()) | |
1330 | } | |
074503f2 | 1331 | |
c553407e | 1332 | task_log!(worker, "no catalog found"); |
074503f2 DM |
1333 | } |
1334 | ||
c553407e | 1335 | task_log!(worker, "scanning entire media to reconstruct catalog"); |
074503f2 | 1336 | |
87068101 DM |
1337 | drive.rewind()?; |
1338 | drive.read_label()?; // skip over labels - we already read them above | |
1339 | ||
28570d19 | 1340 | let mut checked_chunks = HashMap::new(); |
49f9aca6 | 1341 | restore_media(worker, &mut drive, &media_id, None, &mut checked_chunks, verbose)?; |
b017bbc4 DM |
1342 | |
1343 | Ok(()) | |
a1c55753 | 1344 | }, |
b017bbc4 DM |
1345 | )?; |
1346 | ||
1347 | Ok(upid_str.into()) | |
1348 | } | |
1349 | ||
5fdaecf6 DC |
1350 | #[api( |
1351 | input: { | |
18262a88 DC |
1352 | properties: { |
1353 | changer: { | |
1354 | schema: CHANGER_NAME_SCHEMA, | |
1355 | optional: true, | |
1356 | }, | |
1357 | }, | |
5fdaecf6 DC |
1358 | }, |
1359 | returns: { | |
1360 | description: "The list of configured drives with model information.", | |
1361 | type: Array, | |
1362 | items: { | |
1363 | type: DriveListEntry, | |
1364 | }, | |
1365 | }, | |
8cd63df0 DM |
1366 | access: { |
1367 | description: "List configured tape drives filtered by Tape.Audit privileges", | |
1368 | permission: &Permission::Anybody, | |
1369 | }, | |
5fdaecf6 DC |
1370 | )] |
1371 | /// List drives | |
1372 | pub fn list_drives( | |
18262a88 | 1373 | changer: Option<String>, |
5fdaecf6 | 1374 | _param: Value, |
8cd63df0 | 1375 | rpcenv: &mut dyn RpcEnvironment, |
5fdaecf6 | 1376 | ) -> Result<Vec<DriveListEntry>, Error> { |
8cd63df0 DM |
1377 | let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; |
1378 | let user_info = CachedUserInfo::new()?; | |
5fdaecf6 | 1379 | |
1ce8e905 | 1380 | let (config, _) = pbs_config::drive::config()?; |
5fdaecf6 | 1381 | |
a79082a0 | 1382 | let lto_drives = lto_tape_device_list(); |
5fdaecf6 | 1383 | |
a79082a0 | 1384 | let drive_list: Vec<LtoTapeDrive> = config.convert_to_typed_array("lto")?; |
5fdaecf6 DC |
1385 | |
1386 | let mut list = Vec::new(); | |
1387 | ||
1388 | for drive in drive_list { | |
18262a88 DC |
1389 | if changer.is_some() && drive.changer != changer { |
1390 | continue; | |
1391 | } | |
1392 | ||
8cd63df0 DM |
1393 | let privs = user_info.lookup_privs(&auth_id, &["tape", "drive", &drive.name]); |
1394 | if (privs & PRIV_TAPE_AUDIT) == 0 { | |
1395 | continue; | |
1396 | } | |
1397 | ||
a79082a0 | 1398 | let info = lookup_device_identification(<o_drives, &drive.path); |
8bf57693 DC |
1399 | let state = get_tape_device_state(&config, &drive.name)?; |
1400 | let entry = DriveListEntry { config: drive, info, state }; | |
5fdaecf6 DC |
1401 | list.push(entry); |
1402 | } | |
1403 | ||
1404 | Ok(list) | |
1405 | } | |
1406 | ||
55118ca1 DM |
1407 | #[sortable] |
1408 | pub const SUBDIRS: SubdirMap = &sorted!([ | |
bff7e3f3 DM |
1409 | ( |
1410 | "barcode-label-media", | |
1411 | &Router::new() | |
c297835b | 1412 | .post(&API_METHOD_BARCODE_LABEL_MEDIA) |
bff7e3f3 | 1413 | ), |
b017bbc4 DM |
1414 | ( |
1415 | "catalog", | |
1416 | &Router::new() | |
c297835b | 1417 | .post(&API_METHOD_CATALOG_MEDIA) |
b017bbc4 | 1418 | ), |
df69a4fc DM |
1419 | ( |
1420 | "clean", | |
1421 | &Router::new() | |
1422 | .put(&API_METHOD_CLEAN_DRIVE) | |
1423 | ), | |
583a68a4 | 1424 | ( |
55118ca1 | 1425 | "eject-media", |
583a68a4 | 1426 | &Router::new() |
41dacd5d | 1427 | .post(&API_METHOD_EJECT_MEDIA) |
583a68a4 | 1428 | ), |
0098b712 | 1429 | ( |
e29f456e | 1430 | "format-media", |
0098b712 | 1431 | &Router::new() |
e29f456e | 1432 | .post(&API_METHOD_FORMAT_MEDIA) |
0098b712 | 1433 | ), |
5243df47 DM |
1434 | ( |
1435 | "export-media", | |
1436 | &Router::new() | |
1437 | .put(&API_METHOD_EXPORT_MEDIA) | |
1438 | ), | |
83abc749 DM |
1439 | ( |
1440 | "inventory", | |
1441 | &Router::new() | |
1442 | .get(&API_METHOD_INVENTORY) | |
e92c7581 | 1443 | .put(&API_METHOD_UPDATE_INVENTORY) |
83abc749 | 1444 | ), |
7bb720cb DM |
1445 | ( |
1446 | "label-media", | |
1447 | &Router::new() | |
5243df47 | 1448 | .post(&API_METHOD_LABEL_MEDIA) |
7bb720cb | 1449 | ), |
f45dceeb DC |
1450 | ( |
1451 | "load-media", | |
1452 | &Router::new() | |
d0647e5a | 1453 | .post(&API_METHOD_LOAD_MEDIA) |
f45dceeb | 1454 | ), |
5d908606 DM |
1455 | ( |
1456 | "load-slot", | |
1457 | &Router::new() | |
d1bee434 | 1458 | .post(&API_METHOD_LOAD_SLOT) |
5d908606 | 1459 | ), |
1e20f819 | 1460 | ( |
ee01737e | 1461 | "cartridge-memory", |
1e20f819 | 1462 | &Router::new() |
cef4654f | 1463 | .get(&API_METHOD_CARTRIDGE_MEMORY) |
1e20f819 | 1464 | ), |
5f34d69b DM |
1465 | ( |
1466 | "volume-statistics", | |
1467 | &Router::new() | |
cef4654f | 1468 | .get(&API_METHOD_VOLUME_STATISTICS) |
5f34d69b | 1469 | ), |
83abc749 DM |
1470 | ( |
1471 | "read-label", | |
1472 | &Router::new() | |
1473 | .get(&API_METHOD_READ_LABEL) | |
1474 | ), | |
1552d969 DM |
1475 | ( |
1476 | "restore-key", | |
1477 | &Router::new() | |
1478 | .post(&API_METHOD_RESTORE_KEY) | |
1479 | ), | |
f70d8091 DM |
1480 | ( |
1481 | "rewind", | |
1482 | &Router::new() | |
eb1dfb02 | 1483 | .post(&API_METHOD_REWIND) |
f70d8091 | 1484 | ), |
cb80d900 DM |
1485 | ( |
1486 | "status", | |
1487 | &Router::new() | |
1488 | .get(&API_METHOD_STATUS) | |
1489 | ), | |
5d908606 DM |
1490 | ( |
1491 | "unload", | |
1492 | &Router::new() | |
d0647e5a | 1493 | .post(&API_METHOD_UNLOAD) |
5d908606 | 1494 | ), |
55118ca1 | 1495 | ]); |
5d908606 | 1496 | |
5fdaecf6 | 1497 | const ITEM_ROUTER: Router = Router::new() |
5d908606 | 1498 | .get(&list_subdirs_api_method!(SUBDIRS)) |
5fdaecf6 DC |
1499 | .subdirs(&SUBDIRS); |
1500 | ||
1501 | pub const ROUTER: Router = Router::new() | |
1502 | .get(&API_METHOD_LIST_DRIVES) | |
1503 | .match_all("drive", &ITEM_ROUTER); |