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