2021-01-25 16:44:28 +00:00
|
|
|
/// SCSI changer command implemented using scsi-generic raw commands
|
|
|
|
///
|
|
|
|
/// This is a Rust implementation, meant to replace the 'mtx' command
|
|
|
|
/// line tool.
|
|
|
|
///
|
|
|
|
/// Features:
|
|
|
|
///
|
|
|
|
/// - written in Rust
|
|
|
|
///
|
|
|
|
/// - json output
|
|
|
|
///
|
|
|
|
/// - list serial number for attached drives, so that it is possible
|
|
|
|
/// to associate drive numbers with drives.
|
|
|
|
|
|
|
|
use std::fs::File;
|
|
|
|
|
|
|
|
use anyhow::{bail, Error};
|
|
|
|
use serde_json::Value;
|
|
|
|
|
|
|
|
use proxmox::{
|
|
|
|
api::{
|
|
|
|
api,
|
|
|
|
cli::*,
|
|
|
|
RpcEnvironment,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
use proxmox_backup::{
|
|
|
|
tools::sgutils2::{
|
|
|
|
scsi_inquiry,
|
|
|
|
},
|
|
|
|
api2::types::{
|
|
|
|
SCSI_CHANGER_PATH_SCHEMA,
|
|
|
|
CHANGER_NAME_SCHEMA,
|
|
|
|
ScsiTapeChanger,
|
2021-03-30 15:07:59 +00:00
|
|
|
LtoTapeDrive,
|
2021-01-25 16:44:28 +00:00
|
|
|
},
|
|
|
|
tape::{
|
2021-01-27 11:40:51 +00:00
|
|
|
linux_tape_changer_list,
|
2021-01-25 16:44:28 +00:00
|
|
|
complete_changer_path,
|
|
|
|
changer::{
|
|
|
|
ElementStatus,
|
|
|
|
sg_pt_changer,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
config::{
|
|
|
|
self,
|
|
|
|
drive::{
|
|
|
|
complete_changer_name,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
fn get_changer_handle(param: &Value) -> Result<File, Error> {
|
|
|
|
|
|
|
|
if let Some(name) = param["changer"].as_str() {
|
|
|
|
let (config, _digest) = config::drive::config()?;
|
|
|
|
let changer_config: ScsiTapeChanger = config.lookup("changer", &name)?;
|
|
|
|
eprintln!("using device {}", changer_config.path);
|
|
|
|
return sg_pt_changer::open(&changer_config.path);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(device) = param["device"].as_str() {
|
|
|
|
eprintln!("using device {}", device);
|
|
|
|
return sg_pt_changer::open(device);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Ok(name) = std::env::var("PROXMOX_TAPE_DRIVE") {
|
|
|
|
let (config, _digest) = config::drive::config()?;
|
2021-03-30 15:07:59 +00:00
|
|
|
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
|
2021-01-25 16:44:28 +00:00
|
|
|
if let Some(changer) = drive.changer {
|
|
|
|
let changer_config: ScsiTapeChanger = config.lookup("changer", &changer)?;
|
|
|
|
eprintln!("using device {}", changer_config.path);
|
|
|
|
return sg_pt_changer::open(&changer_config.path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Ok(device) = std::env::var("CHANGER") {
|
|
|
|
eprintln!("using device {}", device);
|
|
|
|
return sg_pt_changer::open(device);
|
|
|
|
}
|
|
|
|
|
|
|
|
bail!("no changer device specified");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[api(
|
|
|
|
input: {
|
|
|
|
properties: {
|
|
|
|
changer: {
|
|
|
|
schema: CHANGER_NAME_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
device: {
|
|
|
|
schema: SCSI_CHANGER_PATH_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
"output-format": {
|
|
|
|
schema: OUTPUT_FORMAT,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)]
|
|
|
|
/// Inquiry
|
|
|
|
fn inquiry(
|
|
|
|
param: Value,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
|
|
|
|
let output_format = get_output_format(¶m);
|
|
|
|
|
|
|
|
let result: Result<_, Error> = proxmox::try_block!({
|
|
|
|
let mut file = get_changer_handle(¶m)?;
|
|
|
|
let info = scsi_inquiry(&mut file)?;
|
|
|
|
Ok(info)
|
|
|
|
});
|
|
|
|
|
|
|
|
if output_format == "json-pretty" {
|
|
|
|
let result = result.map_err(|err: Error| err.to_string());
|
|
|
|
println!("{}", serde_json::to_string_pretty(&result)?);
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
if output_format == "json" {
|
|
|
|
let result = result.map_err(|err: Error| err.to_string());
|
|
|
|
println!("{}", serde_json::to_string(&result)?);
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
if output_format != "text" {
|
|
|
|
bail!("unknown output format '{}'", output_format);
|
|
|
|
}
|
|
|
|
|
|
|
|
let info = result?;
|
|
|
|
|
|
|
|
println!("Type: {} ({})", info.peripheral_type_text, info.peripheral_type);
|
|
|
|
println!("Vendor: {}", info.vendor);
|
|
|
|
println!("Product: {}", info.product);
|
|
|
|
println!("Revision: {}", info.revision);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[api(
|
|
|
|
input: {
|
|
|
|
properties: {
|
|
|
|
changer: {
|
|
|
|
schema: CHANGER_NAME_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
device: {
|
|
|
|
schema: SCSI_CHANGER_PATH_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)]
|
|
|
|
/// Inventory
|
|
|
|
fn inventory(
|
|
|
|
param: Value,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
|
|
|
|
let mut file = get_changer_handle(¶m)?;
|
|
|
|
sg_pt_changer::initialize_element_status(&mut file)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[api(
|
|
|
|
input: {
|
|
|
|
properties: {
|
|
|
|
changer: {
|
|
|
|
schema: CHANGER_NAME_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
device: {
|
|
|
|
schema: SCSI_CHANGER_PATH_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
slot: {
|
|
|
|
description: "Storage slot number (source).",
|
|
|
|
type: u64,
|
|
|
|
},
|
|
|
|
drivenum: {
|
|
|
|
description: "Target drive number (defaults to Drive 0)",
|
|
|
|
type: u64,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)]
|
|
|
|
/// Load
|
|
|
|
fn load(
|
|
|
|
param: Value,
|
|
|
|
slot: u64,
|
|
|
|
drivenum: Option<u64>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
|
|
|
|
let mut file = get_changer_handle(¶m)?;
|
|
|
|
|
|
|
|
let drivenum = drivenum.unwrap_or(0);
|
|
|
|
|
|
|
|
sg_pt_changer::load_slot(&mut file, slot, drivenum)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[api(
|
|
|
|
input: {
|
|
|
|
properties: {
|
|
|
|
changer: {
|
|
|
|
schema: CHANGER_NAME_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
device: {
|
|
|
|
schema: SCSI_CHANGER_PATH_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
slot: {
|
|
|
|
description: "Storage slot number (target). If omitted, defaults to the slot that the drive was loaded from.",
|
|
|
|
type: u64,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
drivenum: {
|
|
|
|
description: "Target drive number (defaults to Drive 0)",
|
|
|
|
type: u64,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)]
|
|
|
|
/// Unload
|
|
|
|
fn unload(
|
|
|
|
param: Value,
|
|
|
|
slot: Option<u64>,
|
|
|
|
drivenum: Option<u64>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
|
|
|
|
let mut file = get_changer_handle(¶m)?;
|
|
|
|
|
|
|
|
let drivenum = drivenum.unwrap_or(0);
|
|
|
|
|
|
|
|
if let Some(to_slot) = slot {
|
|
|
|
sg_pt_changer::unload(&mut file, to_slot, drivenum)?;
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
let status = sg_pt_changer::read_element_status(&mut file)?;
|
|
|
|
|
|
|
|
if let Some(info) = status.drives.get(drivenum as usize) {
|
|
|
|
if let ElementStatus::Empty = info.status {
|
|
|
|
bail!("Drive {} is empty.", drivenum);
|
|
|
|
}
|
|
|
|
if let Some(to_slot) = info.loaded_slot {
|
|
|
|
// check if original slot is empty/usable
|
|
|
|
if let Some(slot_info) = status.slots.get(to_slot as usize - 1) {
|
|
|
|
if let ElementStatus::Empty = slot_info.status {
|
|
|
|
sg_pt_changer::unload(&mut file, to_slot, drivenum)?;
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(to_slot) = status.find_free_slot(false) {
|
|
|
|
sg_pt_changer::unload(&mut file, to_slot, drivenum)?;
|
|
|
|
return Ok(());
|
|
|
|
} else {
|
|
|
|
bail!("Drive '{}' unload failure - no free slot", drivenum);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
bail!("Drive {} does not exist.", drivenum);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[api(
|
|
|
|
input: {
|
|
|
|
properties: {
|
|
|
|
changer: {
|
|
|
|
schema: CHANGER_NAME_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
device: {
|
|
|
|
schema: SCSI_CHANGER_PATH_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
"output-format": {
|
|
|
|
schema: OUTPUT_FORMAT,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)]
|
|
|
|
/// Changer Status
|
|
|
|
fn status(
|
|
|
|
param: Value,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
|
|
|
|
let output_format = get_output_format(¶m);
|
|
|
|
|
|
|
|
let result: Result<_, Error> = proxmox::try_block!({
|
|
|
|
let mut file = get_changer_handle(¶m)?;
|
|
|
|
let status = sg_pt_changer::read_element_status(&mut file)?;
|
|
|
|
Ok(status)
|
|
|
|
});
|
|
|
|
|
|
|
|
if output_format == "json-pretty" {
|
|
|
|
let result = result.map_err(|err: Error| err.to_string());
|
|
|
|
println!("{}", serde_json::to_string_pretty(&result)?);
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
if output_format == "json" {
|
|
|
|
let result = result.map_err(|err: Error| err.to_string());
|
|
|
|
println!("{}", serde_json::to_string(&result)?);
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
if output_format != "text" {
|
|
|
|
bail!("unknown output format '{}'", output_format);
|
|
|
|
}
|
|
|
|
|
|
|
|
let status = result?;
|
|
|
|
|
|
|
|
for (i, transport) in status.transports.iter().enumerate() {
|
|
|
|
println!("Transport Element (Griper) {:>3}: {:?}",i, transport.status);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (i, drive) in status.drives.iter().enumerate() {
|
|
|
|
let loaded_txt = match drive.loaded_slot {
|
|
|
|
Some(slot) => format!(", Source: {}", slot),
|
|
|
|
None => String::new(),
|
|
|
|
};
|
|
|
|
let serial_txt = match drive.drive_serial_number {
|
|
|
|
Some(ref serial) => format!(", Serial: {}", serial),
|
|
|
|
None => String::new(),
|
|
|
|
};
|
|
|
|
|
|
|
|
println!(
|
|
|
|
"Data Transfer Element (Drive) {:>3}: {:?}{}{}",
|
|
|
|
i, drive.status, loaded_txt, serial_txt,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (i, slot) in status.slots.iter().enumerate() {
|
|
|
|
if slot.import_export {
|
|
|
|
println!(" Import/Export {:>3}: {:?}", i+1, slot.status);
|
|
|
|
} else {
|
|
|
|
println!(" Storage Element {:>3}: {:?}", i+1, slot.status);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[api(
|
|
|
|
input: {
|
|
|
|
properties: {
|
|
|
|
changer: {
|
|
|
|
schema: CHANGER_NAME_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
device: {
|
|
|
|
schema: SCSI_CHANGER_PATH_SCHEMA,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
from: {
|
|
|
|
description: "Source storage slot number.",
|
|
|
|
type: u64,
|
|
|
|
},
|
|
|
|
to: {
|
|
|
|
description: "Target storage slot number.",
|
|
|
|
type: u64,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)]
|
|
|
|
/// Transfer
|
|
|
|
fn transfer(
|
|
|
|
param: Value,
|
|
|
|
from: u64,
|
|
|
|
to: u64,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
|
|
|
|
let mut file = get_changer_handle(¶m)?;
|
|
|
|
|
|
|
|
sg_pt_changer::transfer_medium(&mut file, from, to)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-01-27 11:40:51 +00:00
|
|
|
#[api(
|
|
|
|
input: {
|
|
|
|
properties: {
|
|
|
|
"output-format": {
|
|
|
|
schema: OUTPUT_FORMAT,
|
|
|
|
optional: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)]
|
|
|
|
/// Scan for existing tape changer devices
|
|
|
|
fn scan(param: Value) -> Result<(), Error> {
|
|
|
|
|
|
|
|
let output_format = get_output_format(¶m);
|
|
|
|
|
|
|
|
let list = linux_tape_changer_list();
|
|
|
|
|
|
|
|
if output_format == "json-pretty" {
|
|
|
|
println!("{}", serde_json::to_string_pretty(&list)?);
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
if output_format == "json" {
|
|
|
|
println!("{}", serde_json::to_string(&list)?);
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
if output_format != "text" {
|
|
|
|
bail!("unknown output format '{}'", output_format);
|
|
|
|
}
|
|
|
|
|
|
|
|
for item in list.iter() {
|
|
|
|
println!("{} ({}/{}/{})", item.path, item.vendor, item.model, item.serial);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-01-25 16:44:28 +00:00
|
|
|
fn main() -> Result<(), Error> {
|
|
|
|
|
|
|
|
let uid = nix::unistd::Uid::current();
|
|
|
|
|
|
|
|
let username = match nix::unistd::User::from_uid(uid)? {
|
|
|
|
Some(user) => user.name,
|
|
|
|
None => bail!("unable to get user name"),
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let cmd_def = CliCommandMap::new()
|
2021-02-06 15:50:56 +00:00
|
|
|
.usage_skip_options(&["device", "changer", "output-format"])
|
2021-01-25 16:44:28 +00:00
|
|
|
.insert(
|
|
|
|
"inquiry",
|
|
|
|
CliCommand::new(&API_METHOD_INQUIRY)
|
|
|
|
.completion_cb("changer", complete_changer_name)
|
|
|
|
.completion_cb("device", complete_changer_path)
|
|
|
|
)
|
|
|
|
.insert(
|
|
|
|
"inventory",
|
|
|
|
CliCommand::new(&API_METHOD_INVENTORY)
|
|
|
|
.completion_cb("changer", complete_changer_name)
|
|
|
|
.completion_cb("device", complete_changer_path)
|
|
|
|
)
|
|
|
|
.insert(
|
|
|
|
"load",
|
|
|
|
CliCommand::new(&API_METHOD_LOAD)
|
|
|
|
.arg_param(&["slot"])
|
|
|
|
.completion_cb("changer", complete_changer_name)
|
|
|
|
.completion_cb("device", complete_changer_path)
|
|
|
|
)
|
|
|
|
.insert(
|
|
|
|
"unload",
|
|
|
|
CliCommand::new(&API_METHOD_UNLOAD)
|
|
|
|
.completion_cb("changer", complete_changer_name)
|
|
|
|
.completion_cb("device", complete_changer_path)
|
|
|
|
)
|
2021-01-27 11:40:51 +00:00
|
|
|
.insert("scan", CliCommand::new(&API_METHOD_SCAN))
|
2021-01-25 16:44:28 +00:00
|
|
|
.insert(
|
|
|
|
"status",
|
|
|
|
CliCommand::new(&API_METHOD_STATUS)
|
|
|
|
.completion_cb("changer", complete_changer_name)
|
|
|
|
.completion_cb("device", complete_changer_path)
|
|
|
|
)
|
|
|
|
.insert(
|
|
|
|
"transfer",
|
|
|
|
CliCommand::new(&API_METHOD_TRANSFER)
|
|
|
|
.arg_param(&["from", "to"])
|
|
|
|
.completion_cb("changer", complete_changer_name)
|
|
|
|
.completion_cb("device", complete_changer_path)
|
|
|
|
)
|
|
|
|
;
|
|
|
|
|
|
|
|
let mut rpcenv = CliEnvironment::new();
|
|
|
|
rpcenv.set_auth_id(Some(format!("{}@pam", username)));
|
|
|
|
|
|
|
|
run_cli_command(cmd_def, rpcenv, None);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|