]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/tape/drive.rs
tape: implement inventory command
[proxmox-backup.git] / src / api2 / tape / drive.rs
1 use std::path::Path;
2 use anyhow::{bail, Error};
3 use serde_json::Value;
4
5 use proxmox::{
6 sortable,
7 identity,
8 list_subdirs_api_method,
9 tools::Uuid,
10 sys::error::SysError,
11 api::{
12 api,
13 Router,
14 SubdirMap,
15 },
16 };
17
18 use crate::{
19 config,
20 api2::types::{
21 DRIVE_ID_SCHEMA,
22 MEDIA_LABEL_SCHEMA,
23 MEDIA_POOL_NAME_SCHEMA,
24 LinuxTapeDrive,
25 ScsiTapeChanger,
26 TapeDeviceInfo,
27 MediaLabelInfoFlat,
28 LabelUuidMap,
29 },
30 tape::{
31 TAPE_STATUS_DIR,
32 TapeDriver,
33 MediaChange,
34 Inventory,
35 MediaStateDatabase,
36 MediaId,
37 mtx_load,
38 mtx_unload,
39 linux_tape_device_list,
40 open_drive,
41 media_changer,
42 update_changer_online_status,
43 file_formats::{
44 DriveLabel,
45 MediaSetLabel,
46 },
47 },
48 };
49
50 #[api(
51 input: {
52 properties: {
53 drive: {
54 schema: DRIVE_ID_SCHEMA,
55 },
56 slot: {
57 description: "Source slot number",
58 minimum: 1,
59 },
60 },
61 },
62 )]
63 /// Load media via changer from slot
64 pub fn load_slot(
65 drive: String,
66 slot: u64,
67 _param: Value,
68 ) -> Result<(), Error> {
69
70 let (config, _digest) = config::drive::config()?;
71
72 let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
73
74 let changer: ScsiTapeChanger = match drive_config.changer {
75 Some(ref changer) => config.lookup("changer", changer)?,
76 None => bail!("drive '{}' has no associated changer", drive),
77 };
78
79 let drivenum = drive_config.changer_drive_id.unwrap_or(0);
80
81 mtx_load(&changer.path, slot, drivenum)
82 }
83
84 #[api(
85 input: {
86 properties: {
87 drive: {
88 schema: DRIVE_ID_SCHEMA,
89 },
90 "changer-id": {
91 schema: MEDIA_LABEL_SCHEMA,
92 },
93 },
94 },
95 )]
96 /// Load media with specified label
97 ///
98 /// Issue a media load request to the associated changer device.
99 pub fn load_media(drive: String, changer_id: String) -> Result<(), Error> {
100
101 let (config, _digest) = config::drive::config()?;
102
103 let (mut changer, _) = media_changer(&config, &drive, false)?;
104
105 changer.load_media(&changer_id)?;
106
107 Ok(())
108 }
109
110 #[api(
111 input: {
112 properties: {
113 drive: {
114 schema: DRIVE_ID_SCHEMA,
115 },
116 slot: {
117 description: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
118 minimum: 1,
119 optional: true,
120 },
121 },
122 },
123 )]
124 /// Unload media via changer
125 pub fn unload(
126 drive: String,
127 slot: Option<u64>,
128 _param: Value,
129 ) -> Result<(), Error> {
130
131 let (config, _digest) = config::drive::config()?;
132
133 let mut drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
134
135 let changer: ScsiTapeChanger = match drive_config.changer {
136 Some(ref changer) => config.lookup("changer", changer)?,
137 None => bail!("drive '{}' has no associated changer", drive),
138 };
139
140 let drivenum: u64 = 0;
141
142 if let Some(slot) = slot {
143 mtx_unload(&changer.path, slot, drivenum)
144 } else {
145 drive_config.unload_media()
146 }
147 }
148
149 #[api(
150 input: {
151 properties: {},
152 },
153 returns: {
154 description: "The list of autodetected tape drives.",
155 type: Array,
156 items: {
157 type: TapeDeviceInfo,
158 },
159 },
160 )]
161 /// Scan tape drives
162 pub fn scan_drives(_param: Value) -> Result<Vec<TapeDeviceInfo>, Error> {
163
164 let list = linux_tape_device_list();
165
166 Ok(list)
167 }
168
169 #[api(
170 input: {
171 properties: {
172 drive: {
173 schema: DRIVE_ID_SCHEMA,
174 },
175 fast: {
176 description: "Use fast erase.",
177 type: bool,
178 optional: true,
179 default: true,
180 },
181 },
182 },
183 )]
184 /// Erase media
185 pub fn erase_media(drive: String, fast: Option<bool>) -> Result<(), Error> {
186
187 let (config, _digest) = config::drive::config()?;
188
189 let mut drive = open_drive(&config, &drive)?;
190
191 drive.erase_media(fast.unwrap_or(true))?;
192
193 Ok(())
194 }
195
196 #[api(
197 input: {
198 properties: {
199 drive: {
200 schema: DRIVE_ID_SCHEMA,
201 },
202 },
203 },
204 )]
205 /// Rewind tape
206 pub fn rewind(drive: String) -> Result<(), Error> {
207
208 let (config, _digest) = config::drive::config()?;
209
210 let mut drive = open_drive(&config, &drive)?;
211
212 drive.rewind()?;
213
214 Ok(())
215 }
216
217 #[api(
218 input: {
219 properties: {
220 drive: {
221 schema: DRIVE_ID_SCHEMA,
222 },
223 },
224 },
225 )]
226 /// Eject/Unload drive media
227 pub fn eject_media(drive: String) -> Result<(), Error> {
228
229 let (config, _digest) = config::drive::config()?;
230
231 let (mut changer, _) = media_changer(&config, &drive, false)?;
232
233 if !changer.eject_on_unload() {
234 let mut drive = open_drive(&config, &drive)?;
235 drive.eject_media()?;
236 }
237
238 changer.unload_media()?;
239
240 Ok(())
241 }
242
243 #[api(
244 input: {
245 properties: {
246 drive: {
247 schema: DRIVE_ID_SCHEMA,
248 },
249 "changer-id": {
250 schema: MEDIA_LABEL_SCHEMA,
251 },
252 pool: {
253 schema: MEDIA_POOL_NAME_SCHEMA,
254 optional: true,
255 },
256 },
257 },
258 )]
259 /// Label media
260 ///
261 /// Write a new media label to the media in 'drive'. The media is
262 /// assigned to the specified 'pool', or else to the free media pool.
263 ///
264 /// Note: The media need to be empty (you may want to erase it first).
265 pub fn label_media(
266 drive: String,
267 pool: Option<String>,
268 changer_id: String,
269 ) -> Result<(), Error> {
270
271 if let Some(ref pool) = pool {
272 let (pool_config, _digest) = config::media_pool::config()?;
273
274 if pool_config.sections.get(pool).is_none() {
275 bail!("no such pool ('{}')", pool);
276 }
277 }
278
279 let (config, _digest) = config::drive::config()?;
280
281 let mut drive = open_drive(&config, &drive)?;
282
283 drive.rewind()?;
284
285 match drive.read_next_file() {
286 Ok(Some(_file)) => bail!("media is not empty (erase first)"),
287 Ok(None) => { /* EOF mark at BOT, assume tape is empty */ },
288 Err(err) => {
289 if err.is_errno(nix::errno::Errno::ENOSPC) || err.is_errno(nix::errno::Errno::EIO) {
290 /* assume tape is empty */
291 } else {
292 bail!("media read error - {}", err);
293 }
294 }
295 }
296
297 let ctime = proxmox::tools::time::epoch_i64();
298 let label = DriveLabel {
299 changer_id: changer_id.to_string(),
300 uuid: Uuid::generate(),
301 ctime,
302 };
303
304 write_media_label(&mut drive, label, pool)
305 }
306
307 fn write_media_label(
308 drive: &mut Box<dyn TapeDriver>,
309 label: DriveLabel,
310 pool: Option<String>,
311 ) -> Result<(), Error> {
312
313 drive.label_tape(&label)?;
314
315 let mut media_set_label = None;
316
317 if let Some(ref pool) = pool {
318 // assign media to pool by writing special media set label
319 println!("Label media '{}' for pool '{}'", label.changer_id, pool);
320 let set = MediaSetLabel::with_data(&pool, [0u8; 16].into(), 0, label.ctime);
321
322 drive.write_media_set_label(&set)?;
323 media_set_label = Some(set);
324 } else {
325 println!("Label media '{}' (no pool assignment)", label.changer_id);
326 }
327
328 let media_id = MediaId { label, media_set_label };
329
330 let mut inventory = Inventory::load(Path::new(TAPE_STATUS_DIR))?;
331 inventory.store(media_id.clone())?;
332
333 drive.rewind()?;
334
335 match drive.read_label() {
336 Ok(Some(info)) => {
337 if info.label.uuid != media_id.label.uuid {
338 bail!("verify label failed - got wrong label uuid");
339 }
340 if let Some(ref pool) = pool {
341 match info.media_set_label {
342 Some((set, _)) => {
343 if set.uuid != [0u8; 16].into() {
344 bail!("verify media set label failed - got wrong set uuid");
345 }
346 if &set.pool != pool {
347 bail!("verify media set label failed - got wrong pool");
348 }
349 }
350 None => {
351 bail!("verify media set label failed (missing set label)");
352 }
353 }
354 }
355 },
356 Ok(None) => bail!("verify label failed (got empty media)"),
357 Err(err) => bail!("verify label failed - {}", err),
358 };
359
360 drive.rewind()?;
361
362 Ok(())
363 }
364
365 #[api(
366 input: {
367 properties: {
368 drive: {
369 schema: DRIVE_ID_SCHEMA,
370 },
371 },
372 },
373 returns: {
374 type: MediaLabelInfoFlat,
375 },
376 )]
377 /// Read media label
378 pub fn read_label(drive: String) -> Result<MediaLabelInfoFlat, Error> {
379
380 let (config, _digest) = config::drive::config()?;
381
382 let mut drive = open_drive(&config, &drive)?;
383
384 let info = drive.read_label()?;
385
386 let info = match info {
387 Some(info) => {
388 let mut flat = MediaLabelInfoFlat {
389 uuid: info.label.uuid.to_string(),
390 changer_id: info.label.changer_id.clone(),
391 ctime: info.label.ctime,
392 media_set_ctime: None,
393 media_set_uuid: None,
394 pool: None,
395 seq_nr: None,
396 };
397 if let Some((set, _)) = info.media_set_label {
398 flat.pool = Some(set.pool.clone());
399 flat.seq_nr = Some(set.seq_nr);
400 flat.media_set_uuid = Some(set.uuid.to_string());
401 flat.media_set_ctime = Some(set.ctime);
402 }
403 flat
404 }
405 None => {
406 bail!("Media is empty (no label).");
407 }
408 };
409
410 Ok(info)
411 }
412
413 #[api(
414 input: {
415 properties: {
416 drive: {
417 schema: DRIVE_ID_SCHEMA,
418 },
419 "read-labels": {
420 description: "Load unknown tapes and try read labels",
421 type: bool,
422 optional: true,
423 },
424 "read-all-labels": {
425 description: "Load all tapes and try read labels (even if already inventoried)",
426 type: bool,
427 optional: true,
428 },
429 },
430 },
431 returns: {
432 description: "The list of media labels with associated media Uuid (if any).",
433 type: Array,
434 items: {
435 type: LabelUuidMap,
436 },
437 },
438 )]
439 /// List (and update) media labels (Changer Inventory)
440 ///
441 /// Note: Only useful for drives with associated changer device.
442 ///
443 /// This method queries the changer to get a list of media labels. It
444 /// 'read-labels' is set, it then loads any unknown media into the
445 /// drive, reads the label, and store the result to the media
446 /// database.
447 pub fn inventory(
448 drive: String,
449 read_labels: Option<bool>,
450 read_all_labels: Option<bool>,
451 ) -> Result<Vec<LabelUuidMap>, Error> {
452
453 let (config, _digest) = config::drive::config()?;
454
455 let (mut changer, changer_name) = media_changer(&config, &drive, false)?;
456
457 let changer_id_list = changer.list_media_changer_ids()?;
458
459 let state_path = Path::new(TAPE_STATUS_DIR);
460
461 let mut inventory = Inventory::load(state_path)?;
462 let mut state_db = MediaStateDatabase::load(state_path)?;
463
464 update_changer_online_status(&config, &mut inventory, &mut state_db, &changer_name, &changer_id_list)?;
465
466 let mut list = Vec::new();
467
468 let do_read = read_labels.unwrap_or(false) || read_all_labels.unwrap_or(false);
469
470 for changer_id in changer_id_list.iter() {
471 if changer_id.starts_with("CLN") {
472 // skip cleaning unit
473 continue;
474 }
475
476 let changer_id = changer_id.to_string();
477
478 if !read_all_labels.unwrap_or(false) {
479 if let Some(media_id) = inventory.find_media_by_changer_id(&changer_id) {
480 list.push(LabelUuidMap { changer_id, uuid: Some(media_id.label.uuid.to_string()) });
481 continue;
482 }
483 }
484
485 if !do_read {
486 list.push(LabelUuidMap { changer_id, uuid: None });
487 continue;
488 }
489
490 if let Err(err) = changer.load_media(&changer_id) {
491 eprintln!("unable to load media '{}' - {}", changer_id, err);
492 list.push(LabelUuidMap { changer_id, uuid: None });
493 continue;
494 }
495
496 let mut drive = open_drive(&config, &drive)?;
497 match drive.read_label() {
498 Err(err) => {
499 eprintln!("unable to read label form media '{}' - {}", changer_id, err);
500 list.push(LabelUuidMap { changer_id, uuid: None });
501
502 }
503 Ok(None) => {
504 // no label on media (empty)
505 list.push(LabelUuidMap { changer_id, uuid: None });
506
507 }
508 Ok(Some(info)) => {
509 if changer_id != info.label.changer_id {
510 eprintln!("label changer ID missmatch ({} != {})", changer_id, info.label.changer_id);
511 list.push(LabelUuidMap { changer_id, uuid: None });
512 continue;
513 }
514 let uuid = info.label.uuid.to_string();
515 inventory.store(info.into())?;
516 list.push(LabelUuidMap { changer_id, uuid: Some(uuid) });
517 }
518 }
519 }
520
521 Ok(list)
522 }
523
524
525 #[sortable]
526 pub const SUBDIRS: SubdirMap = &sorted!([
527 (
528 "eject-media",
529 &Router::new()
530 .put(&API_METHOD_EJECT_MEDIA)
531 ),
532 (
533 "erase-media",
534 &Router::new()
535 .put(&API_METHOD_ERASE_MEDIA)
536 ),
537 (
538 "inventory",
539 &Router::new()
540 .get(&API_METHOD_INVENTORY)
541 ),
542 (
543 "label-media",
544 &Router::new()
545 .put(&API_METHOD_LABEL_MEDIA)
546 ),
547 (
548 "load-slot",
549 &Router::new()
550 .put(&API_METHOD_LOAD_SLOT)
551 ),
552 (
553 "read-label",
554 &Router::new()
555 .get(&API_METHOD_READ_LABEL)
556 ),
557 (
558 "rewind",
559 &Router::new()
560 .put(&API_METHOD_REWIND)
561 ),
562 (
563 "scan",
564 &Router::new()
565 .get(&API_METHOD_SCAN_DRIVES)
566 ),
567 (
568 "unload",
569 &Router::new()
570 .put(&API_METHOD_UNLOAD)
571 ),
572 ]);
573
574 pub const ROUTER: Router = Router::new()
575 .get(&list_subdirs_api_method!(SUBDIRS))
576 .subdirs(SUBDIRS);