tape: add tape changer support using 'mtx' command
This commit is contained in:
		
							
								
								
									
										57
									
								
								src/tape/changer/email.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/tape/changer/email.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | |||||||
|  | use anyhow::Error; | ||||||
|  |  | ||||||
|  | use proxmox::tools::email::sendmail; | ||||||
|  |  | ||||||
|  | use super::MediaChange; | ||||||
|  |  | ||||||
|  | /// Send email to a person to request a manual media change | ||||||
|  | pub struct ChangeMediaEmail { | ||||||
|  |     drive: String, | ||||||
|  |     to: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ChangeMediaEmail { | ||||||
|  |  | ||||||
|  |     pub fn new(drive: &str, to: &str) -> Self { | ||||||
|  |         Self { | ||||||
|  |             drive: String::from(drive), | ||||||
|  |             to: String::from(to), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl MediaChange for ChangeMediaEmail { | ||||||
|  |  | ||||||
|  |     fn load_media(&mut self, changer_id: &str) -> Result<(), Error> { | ||||||
|  |  | ||||||
|  |         let subject = format!("Load Media '{}' request for drive '{}'", changer_id, self.drive); | ||||||
|  |  | ||||||
|  |         let mut text = String::new(); | ||||||
|  |  | ||||||
|  |         text.push_str("Please insert the requested media into the backup drive.\n\n"); | ||||||
|  |  | ||||||
|  |         text.push_str(&format!("Drive: {}\n", self.drive)); | ||||||
|  |         text.push_str(&format!("Media: {}\n", changer_id)); | ||||||
|  |  | ||||||
|  |         sendmail( | ||||||
|  |             &[&self.to], | ||||||
|  |             &subject, | ||||||
|  |             Some(&text), | ||||||
|  |             None, | ||||||
|  |             None, | ||||||
|  |             None, | ||||||
|  |         )?; | ||||||
|  |  | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn unload_media(&mut self) -> Result<(), Error> { | ||||||
|  |         /* ignore ? */ | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn list_media_changer_ids(&self) -> Result<Vec<String>, Error> { | ||||||
|  |         Ok(Vec::new()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										145
									
								
								src/tape/changer/linux_tape.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/tape/changer/linux_tape.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,145 @@ | |||||||
|  | use anyhow::{bail, Error}; | ||||||
|  |  | ||||||
|  | use crate::{ | ||||||
|  |     tape::changer::{ | ||||||
|  |         MediaChange, | ||||||
|  |         MtxStatus, | ||||||
|  |         ElementStatus, | ||||||
|  |         mtx_status, | ||||||
|  |         mtx_load, | ||||||
|  |         mtx_unload, | ||||||
|  |     }, | ||||||
|  |     api2::types::{ | ||||||
|  |         ScsiTapeChanger, | ||||||
|  |         LinuxTapeDrive, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | fn unload_to_free_slot(drive_name: &str, path: &str, status: &MtxStatus, drivenum: u64) -> Result<(), Error> { | ||||||
|  |  | ||||||
|  |     if drivenum >= status.drives.len() as u64 { | ||||||
|  |         bail!("unload drive '{}' got unexpected drive number '{}' - changer only has '{}' drives", | ||||||
|  |               drive_name, drivenum, status.drives.len()); | ||||||
|  |     } | ||||||
|  |     let drive_status = &status.drives[drivenum as usize]; | ||||||
|  |     if let Some(slot) = drive_status.loaded_slot { | ||||||
|  |         mtx_unload(path, slot, drivenum) | ||||||
|  |     } else { | ||||||
|  |         let mut free_slot = None; | ||||||
|  |         for i in 0..status.slots.len() { | ||||||
|  |             if let ElementStatus::Empty = status.slots[i] { | ||||||
|  |                 free_slot = Some((i+1) as u64); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if let Some(slot) = free_slot { | ||||||
|  |             mtx_unload(path, slot, drivenum) | ||||||
|  |         } else { | ||||||
|  |             bail!("drive '{}' unload failure - no free slot", drive_name); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl MediaChange for LinuxTapeDrive { | ||||||
|  |  | ||||||
|  |     fn load_media(&mut self, changer_id: &str) -> Result<(), Error> { | ||||||
|  |  | ||||||
|  |         if changer_id.starts_with("CLN") { | ||||||
|  |             bail!("unable to load media '{}' (seems top be a a cleaning units)", changer_id); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let (config, _digest) = crate::config::drive::config()?; | ||||||
|  |  | ||||||
|  |         let changer: ScsiTapeChanger = match self.changer { | ||||||
|  |             Some(ref changer) => config.lookup("changer", changer)?, | ||||||
|  |             None => bail!("drive '{}' has no associated changer", self.name), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         let status = mtx_status(&changer.path)?; | ||||||
|  |  | ||||||
|  |         let drivenum = 0; // fixme: read from drive config | ||||||
|  |  | ||||||
|  |         // already loaded? | ||||||
|  |         for (i, drive_status) in status.drives.iter().enumerate() { | ||||||
|  |             if let ElementStatus::VolumeTag(ref tag) = drive_status.status { | ||||||
|  |                 if *tag == changer_id { | ||||||
|  |                     if i != drivenum { | ||||||
|  |                         bail!("unable to load media '{}' - media in wrong drive ({} != {})", | ||||||
|  |                               changer_id, i, drivenum); | ||||||
|  |                     } | ||||||
|  |                     return Ok(()) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             if i == drivenum { | ||||||
|  |                 match  drive_status.status { | ||||||
|  |                     ElementStatus::Empty => { /* OK */ }, | ||||||
|  |                     _ => unload_to_free_slot(&self.name, &changer.path, &status, drivenum as u64)?, | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let mut slot = None; | ||||||
|  |         for (i, element_status) in status.slots.iter().enumerate() { | ||||||
|  |             if let ElementStatus::VolumeTag(tag) = element_status { | ||||||
|  |                 if *tag == changer_id { | ||||||
|  |                     slot = Some(i+1); | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let slot = match slot { | ||||||
|  |             None => bail!("unable to find media '{}' (offline?)", changer_id), | ||||||
|  |             Some(slot) => slot, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         mtx_load(&changer.path, slot as u64, drivenum as u64) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn unload_media(&mut self) -> Result<(), Error> { | ||||||
|  |         let (config, _digest) = crate::config::drive::config()?; | ||||||
|  |  | ||||||
|  |         let changer: ScsiTapeChanger = match self.changer { | ||||||
|  |             Some(ref changer) => config.lookup("changer", changer)?, | ||||||
|  |             None => return Ok(()), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         let drivenum: u64 = 0; | ||||||
|  |  | ||||||
|  |         let status = mtx_status(&changer.path)?; | ||||||
|  |  | ||||||
|  |         unload_to_free_slot(&self.name, &changer.path, &status, drivenum) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn eject_on_unload(&self) -> bool { | ||||||
|  |         true | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn list_media_changer_ids(&self) -> Result<Vec<String>, Error> { | ||||||
|  |         let (config, _digest) = crate::config::drive::config()?; | ||||||
|  |  | ||||||
|  |         let changer: ScsiTapeChanger = match self.changer { | ||||||
|  |             Some(ref changer) => config.lookup("changer", changer)?, | ||||||
|  |             None => return Ok(Vec::new()), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         let status = mtx_status(&changer.path)?; | ||||||
|  |  | ||||||
|  |         let mut list = Vec::new(); | ||||||
|  |  | ||||||
|  |         for drive_status in status.drives.iter() { | ||||||
|  |             if let ElementStatus::VolumeTag(ref tag) = drive_status.status { | ||||||
|  |                 list.push(tag.clone()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for element_status in status.slots.iter() { | ||||||
|  |             if let ElementStatus::VolumeTag(ref tag) = element_status { | ||||||
|  |                 list.push(tag.clone()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Ok(list) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								src/tape/changer/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/tape/changer/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | mod email; | ||||||
|  | pub use email::*; | ||||||
|  |  | ||||||
|  | mod parse_mtx_status; | ||||||
|  | pub use parse_mtx_status::*; | ||||||
|  |  | ||||||
|  | mod mtx_wrapper; | ||||||
|  | pub use mtx_wrapper::*; | ||||||
|  |  | ||||||
|  | mod linux_tape; | ||||||
|  | pub use linux_tape::*; | ||||||
|  |  | ||||||
|  | use anyhow::Error; | ||||||
|  |  | ||||||
|  | /// Interface to media change devices | ||||||
|  | pub trait MediaChange { | ||||||
|  |  | ||||||
|  |     /// Load media into drive | ||||||
|  |     /// | ||||||
|  |     /// This unloads first if the drive is already loaded with another media. | ||||||
|  |     fn load_media(&mut self, changer_id: &str) -> Result<(), Error>; | ||||||
|  |  | ||||||
|  |     /// Unload media from drive | ||||||
|  |     /// | ||||||
|  |     /// This is a nop on drives without autoloader. | ||||||
|  |     fn unload_media(&mut self) -> Result<(), Error>; | ||||||
|  |  | ||||||
|  |     /// Returns true if unload_media automatically ejects drive media | ||||||
|  |     fn eject_on_unload(&self) -> bool { | ||||||
|  |         false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// List media changer IDs (barcodes) | ||||||
|  |     fn list_media_changer_ids(&self) -> Result<Vec<String>, Error>; | ||||||
|  | } | ||||||
							
								
								
									
										84
									
								
								src/tape/changer/mtx_wrapper.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/tape/changer/mtx_wrapper.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | |||||||
|  | use std::collections::HashSet; | ||||||
|  |  | ||||||
|  | use anyhow::Error; | ||||||
|  |  | ||||||
|  | use proxmox::tools::Uuid; | ||||||
|  |  | ||||||
|  | use crate::{ | ||||||
|  |     tools::run_command, | ||||||
|  |     tape::{ | ||||||
|  |         Inventory, | ||||||
|  |         changer::{ | ||||||
|  |             MtxStatus, | ||||||
|  |             ElementStatus, | ||||||
|  |             parse_mtx_status, | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /// Run 'mtx status' and return parsed result. | ||||||
|  | pub fn mtx_status(path: &str) -> Result<MtxStatus, Error> { | ||||||
|  |  | ||||||
|  |     let mut command = std::process::Command::new("mtx"); | ||||||
|  |     command.args(&["-f", path, "status"]); | ||||||
|  |  | ||||||
|  |     let output = run_command(command, None)?; | ||||||
|  |  | ||||||
|  |     let status = parse_mtx_status(&output)?; | ||||||
|  |  | ||||||
|  |     Ok(status) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Run 'mtx load' | ||||||
|  | pub fn mtx_load( | ||||||
|  |     path: &str, | ||||||
|  |     slot: u64, | ||||||
|  |     drivenum: u64, | ||||||
|  | ) -> Result<(), Error> { | ||||||
|  |  | ||||||
|  |     let mut command = std::process::Command::new("mtx"); | ||||||
|  |     command.args(&["-f", path, "load", &slot.to_string(), &drivenum.to_string()]); | ||||||
|  |     run_command(command, None)?; | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Run 'mtx unload' | ||||||
|  | pub fn mtx_unload( | ||||||
|  |     path: &str, | ||||||
|  |     slot: u64, | ||||||
|  |     drivenum: u64, | ||||||
|  | ) -> Result<(), Error> { | ||||||
|  |  | ||||||
|  |     let mut command = std::process::Command::new("mtx"); | ||||||
|  |     command.args(&["-f", path, "unload", &slot.to_string(), &drivenum.to_string()]); | ||||||
|  |     run_command(command, None)?; | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Extract the list of online media from MtxStatus | ||||||
|  | /// | ||||||
|  | /// Returns a HashSet containing all found media Uuid | ||||||
|  | pub fn mtx_status_to_online_set(status: &MtxStatus, inventory: &Inventory) -> HashSet<Uuid> { | ||||||
|  |  | ||||||
|  |     let mut online_set = HashSet::new(); | ||||||
|  |  | ||||||
|  |     for drive_status in status.drives.iter() { | ||||||
|  |         if let ElementStatus::VolumeTag(ref changer_id) = drive_status.status { | ||||||
|  |             if let Some(media_id) = inventory.find_media_by_changer_id(changer_id) { | ||||||
|  |                 online_set.insert(media_id.label.uuid.clone()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for slot_status in status.slots.iter() { | ||||||
|  |         if let ElementStatus::VolumeTag(ref changer_id) = slot_status { | ||||||
|  |             if let Some(media_id) = inventory.find_media_by_changer_id(changer_id) { | ||||||
|  |                 online_set.insert(media_id.label.uuid.clone()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     online_set | ||||||
|  | } | ||||||
							
								
								
									
										161
									
								
								src/tape/changer/parse_mtx_status.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/tape/changer/parse_mtx_status.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,161 @@ | |||||||
|  | use anyhow::Error; | ||||||
|  |  | ||||||
|  | use nom::{ | ||||||
|  |     bytes::complete::{take_while, tag}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | use crate::tools::nom::{ | ||||||
|  |     parse_complete, multispace0, multispace1, parse_u64, | ||||||
|  |     parse_failure, parse_error, IResult, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | pub enum ElementStatus { | ||||||
|  |     Empty, | ||||||
|  |     Full, | ||||||
|  |     VolumeTag(String), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct DriveStatus { | ||||||
|  |     pub loaded_slot: Option<u64>, | ||||||
|  |     pub status: ElementStatus, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct MtxStatus { | ||||||
|  |     pub drives: Vec<DriveStatus>, | ||||||
|  |     pub slots: Vec<ElementStatus>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Recognizes one line | ||||||
|  | fn next_line(i: &str)  -> IResult<&str, &str> { | ||||||
|  |     let (i, line) = take_while(|c| (c != '\n'))(i)?; | ||||||
|  |     if i.is_empty() { | ||||||
|  |         Ok((i, line)) | ||||||
|  |     } else { | ||||||
|  |         Ok((&i[1..], line)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn parse_storage_changer(i: &str) -> IResult<&str, ()> { | ||||||
|  |  | ||||||
|  |     let (i, _) = multispace0(i)?; | ||||||
|  |     let (i, _) = tag("Storage Changer")(i)?; | ||||||
|  |     let (i, _) = next_line(i)?; // skip | ||||||
|  |  | ||||||
|  |     Ok((i, ())) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn parse_drive_status(i: &str) -> IResult<&str, DriveStatus> { | ||||||
|  |  | ||||||
|  |     let mut loaded_slot = None; | ||||||
|  |  | ||||||
|  |     if i.starts_with("Empty") { | ||||||
|  |         return Ok((&i[5..], DriveStatus { loaded_slot, status: ElementStatus::Empty })); | ||||||
|  |     } | ||||||
|  |     let (mut i, _) = tag("Full (")(i)?; | ||||||
|  |  | ||||||
|  |     if i.starts_with("Storage Element ") { | ||||||
|  |         let n = &i[16..]; | ||||||
|  |         let (n, id) = parse_u64(n)?; | ||||||
|  |         loaded_slot = Some(id); | ||||||
|  |         let (n, _) = tag(" Loaded")(n)?; | ||||||
|  |         i = n; | ||||||
|  |     } else { | ||||||
|  |         let (n, _) = take_while(|c| !(c == ')' || c == '\n'))(i)?; // skip to ')' | ||||||
|  |         i = n; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let (i, _) = tag(")")(i)?; | ||||||
|  |  | ||||||
|  |     if i.starts_with(":VolumeTag = ") { | ||||||
|  |         let i = &i[13..]; | ||||||
|  |         let (i, tag) = take_while(|c| !(c == ' ' || c == ':' || c == '\n'))(i)?; | ||||||
|  |         let (i, _) = take_while(|c| c != '\n')(i)?; // skip to eol | ||||||
|  |         return Ok((i, DriveStatus { loaded_slot, status: ElementStatus::VolumeTag(tag.to_string()) })); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let (i, _) = take_while(|c| c != '\n')(i)?; // skip | ||||||
|  |  | ||||||
|  |     Ok((i, DriveStatus { loaded_slot, status: ElementStatus::Full })) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn parse_slot_status(i: &str) -> IResult<&str, ElementStatus> { | ||||||
|  |     if i.starts_with("Empty") { | ||||||
|  |         return Ok((&i[5..],  ElementStatus::Empty)); | ||||||
|  |     } | ||||||
|  |     if i.starts_with("Full ") { | ||||||
|  |         let mut n = &i[5..]; | ||||||
|  |  | ||||||
|  |         if n.starts_with(":VolumeTag=") { | ||||||
|  |             n = &n[11..]; | ||||||
|  |             let (n, tag) = take_while(|c| !(c == ' ' || c == ':' || c == '\n'))(n)?; | ||||||
|  |             let (n, _) = take_while(|c| c != '\n')(n)?; // skip to eol | ||||||
|  |             return Ok((n, ElementStatus::VolumeTag(tag.to_string()))); | ||||||
|  |  | ||||||
|  |         } | ||||||
|  |         let (n, _) = take_while(|c| c != '\n')(n)?; // skip | ||||||
|  |  | ||||||
|  |         return Ok((n, ElementStatus::Full)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Err(parse_error(i, "unexptected element status")) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn parse_data_transfer_element(i: &str) -> IResult<&str, (u64, DriveStatus)> { | ||||||
|  |  | ||||||
|  |     let (i, _) = tag("Data Transfer Element")(i)?; | ||||||
|  |     let (i, _) = multispace1(i)?; | ||||||
|  |     let (i, id) = parse_u64(i)?; | ||||||
|  |     let (i, _) = nom::character::complete::char(':')(i)?; | ||||||
|  |     let (i, element_status) = parse_drive_status(i)?; | ||||||
|  |     let (i, _) = nom::character::complete::newline(i)?; | ||||||
|  |  | ||||||
|  |     Ok((i, (id, element_status))) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn parse_storage_element(i: &str) -> IResult<&str, (u64, ElementStatus)> { | ||||||
|  |  | ||||||
|  |     let (i, _) = multispace1(i)?; | ||||||
|  |     let (i, _) = tag("Storage Element")(i)?; | ||||||
|  |     let (i, _) = multispace1(i)?; | ||||||
|  |     let (i, id) = parse_u64(i)?; | ||||||
|  |     let (i, _) = nom::character::complete::char(':')(i)?; | ||||||
|  |     let (i, element_status) = parse_slot_status(i)?; | ||||||
|  |     let (i, _) = nom::character::complete::newline(i)?; | ||||||
|  |  | ||||||
|  |     Ok((i, (id, element_status))) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn parse_status(i: &str) ->  IResult<&str, MtxStatus> { | ||||||
|  |  | ||||||
|  |     let (mut i, _) = parse_storage_changer(i)?; | ||||||
|  |  | ||||||
|  |     let mut drives = Vec::new(); | ||||||
|  |     while let Ok((n, (id, drive_status))) = parse_data_transfer_element(i) { | ||||||
|  |         if id != drives.len() as u64 { | ||||||
|  |             return Err(parse_failure(i, "unexpected drive number")); | ||||||
|  |         } | ||||||
|  |         i = n; | ||||||
|  |         drives.push(drive_status); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let mut slots = Vec::new(); | ||||||
|  |     while let Ok((n, (id, element_status))) = parse_storage_element(i) { | ||||||
|  |         if id != (slots.len() as u64 + 1) { | ||||||
|  |             return Err(parse_failure(i, "unexpected slot number")); | ||||||
|  |         } | ||||||
|  |         i = n; | ||||||
|  |         slots.push(element_status); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let status = MtxStatus { drives, slots }; | ||||||
|  |  | ||||||
|  |     Ok((i, status)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Parses the output from 'mtx status' | ||||||
|  | pub fn parse_mtx_status(i: &str) -> Result<MtxStatus, Error> { | ||||||
|  |  | ||||||
|  |     let status = parse_complete("mtx status", i, parse_status)?; | ||||||
|  |  | ||||||
|  |     Ok(status) | ||||||
|  | } | ||||||
| @ -9,6 +9,9 @@ pub use tape_read::*; | |||||||
| mod inventory; | mod inventory; | ||||||
| pub use inventory::*; | pub use inventory::*; | ||||||
|  |  | ||||||
|  | mod changer; | ||||||
|  | pub use changer::*; | ||||||
|  |  | ||||||
| /// Directory path where we stora all status information | /// Directory path where we stora all status information | ||||||
| pub const MEDIA_POOL_STATUS_DIR: &str = "/var/lib/proxmox-backup/mediapool"; | pub const MEDIA_POOL_STATUS_DIR: &str = "/var/lib/proxmox-backup/mediapool"; | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user