#[macro_use]
extern crate log;
#[macro_use]
extern crate prettytable;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate snafu;
extern crate crossbeam_channel;
extern crate env_logger;
extern crate indexmap;
extern crate ommui_file_loading;
extern crate serde_json;
extern crate structopt;
extern crate uuid;
extern crate xio_base_datatypes;
extern crate xio_jobset;
extern crate xio_webapi;
extern crate xio_webclient;
use indexmap::IndexMap;
use ommui_file_loading::PathLoad;
use snafu::{Backtrace, ErrorCompat, ResultExt, Snafu};
use std::path::Path;
use structopt::StructOpt;
use uuid::Uuid;
use xio_base_datatypes as base;
use xio_jobset as jobset;
use xio_webapi as webapi;
use xio_webclient as webclient;
#[derive(Debug, Snafu)]
enum Error {
#[snafu(display("{}", source))]
OmmuiFileLoading { source: ommui_file_loading::Error },
#[snafu(display(
"API version must be {:?}, found: {:?}",
required,
found,
))]
ApiVersionMismatch {
found: webapi::ApiVersion,
required: webapi::ApiVersion,
backtrace: Backtrace,
},
#[snafu(display(
"Web API uuid must be {:?}, found: {:?}",
required,
found
))]
ApiUuidMismatch {
found: Uuid,
required: Uuid,
backtrace: Backtrace,
},
#[snafu(display("{}", source))]
WebClient { source: xio_webclient::Error },
#[snafu(display("Couldn't decode JSON: {}", source))]
SerdeJson {
source: serde_json::Error,
backtrace: Backtrace,
},
}
type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(StructOpt, Debug)]
enum ControllersAction {
#[structopt(
name = "list",
about = "Print a list of available controllers"
)]
List,
#[structopt(name = "monitor", about = "Monitor controller events")]
Monitor,
}
#[derive(StructOpt, Debug)]
struct LoadJobSetParameters {
#[structopt(long = "metadata")]
metadata: String,
#[structopt(required = true, long = "parameters")]
parameters: Vec<String>,
#[structopt(long = "mapping")]
mapping: String,
#[structopt(required = true, name = "jobs")]
jobs: Vec<String>,
}
#[derive(StructOpt, Debug)]
struct StartJobParameters {
#[structopt(required = true, name = "job")]
job: String,
}
#[derive(StructOpt, Debug)]
enum ModuleAction {
#[structopt(name = "initialize", about = "Initialize the module")]
Initialize,
#[structopt(name = "start", about = "Start the module")]
Start,
#[structopt(name = "stop", about = "Stop the module")]
Stop,
#[structopt(name = "uninitialize", about = "Uninitialize the module")]
Uninitialize,
}
#[derive(StructOpt, Debug)]
enum ControllerAction {
#[structopt(
name = "details",
about = "Print details of a controller"
)]
Details,
#[structopt(
name = "modules",
about = "Print module details of a controller"
)]
Modules,
#[structopt(
name = "module",
about = "Perform actions on a controller module"
)]
Module {
module_id: String,
#[structopt(subcommand)]
action: ModuleAction,
},
#[structopt(
name = "clear-jobset",
about = "Clear the XIO jobset from the controller"
)]
ClearJobSet,
#[structopt(
name = "load-jobset",
about = "Load a XIO jobset onto the controller"
)]
LoadJobSet(LoadJobSetParameters),
#[structopt(name = "start-job", about = "Start a job")]
StartJob(StartJobParameters),
#[structopt(
name = "stop-job",
about = "Stop the currently running job"
)]
StopJob,
#[structopt(
name = "monitor",
about = "Monitor events of the controller"
)]
Monitor,
#[structopt(name = "joblog", about = "Monitor the job log")]
JobLog,
#[structopt(
name = "flash-firmware",
about = "Flash the latest available version on a controller"
)]
FlashFirmware,
}
fn table_format() -> prettytable::format::TableFormat {
use prettytable::format;
format::FormatBuilder::new()
.column_separator('│')
.padding(1, 1)
.borders(' ')
.separator(
format::LinePosition::Title,
format::LineSeparator::new('─', '┼', '╶', '╴'),
)
.separator(
format::LinePosition::Top,
format::LineSeparator::new(' ', 'â•·', ' ', ' '),
)
.separator(
format::LinePosition::Bottom,
format::LineSeparator::new(' ', '╵', ' ', ' '),
)
.build()
}
impl ModuleAction {
fn run(
&self,
client: &webclient::Client,
uuid: &Uuid,
module_id: &str,
) -> Result<()> {
use ModuleAction::*;
let message = match *self {
Initialize => webapi::ModuleAction::Initialize,
Start => webapi::ModuleAction::Start,
Stop => webapi::ModuleAction::Stop,
Uninitialize => webapi::ModuleAction::Uninitialize,
};
info!("Module {:?}: {:?}", module_id, message);
println!(
"Module action request: {:?}",
client
.post_module_statecontrol(uuid, module_id, &message)
.context(WebClient)?
);
Ok(())
}
}
impl ControllerAction {
fn run(&self, client: &webclient::Client, uuid: &Uuid) -> Result<()> {
match *self {
ControllerAction::Details => Self::details(client, uuid),
ControllerAction::Modules => Self::modules(client, uuid),
ControllerAction::Module {
ref module_id,
ref action,
} => action.run(client, uuid, module_id),
ControllerAction::ClearJobSet => {
Self::clear_job_set(client, uuid)
}
ControllerAction::LoadJobSet(ref params) => {
Self::load_job_set(client, uuid, params)
}
ControllerAction::StartJob(ref params) => {
Self::start_job(client, uuid, params)
}
ControllerAction::StopJob => Self::stop_job(client, uuid),
ControllerAction::Monitor => Self::monitor(client, uuid),
ControllerAction::JobLog => Self::joblog(client, uuid),
ControllerAction::FlashFirmware => {
Self::flash_firmware(client, &uuid)
}
}
}
fn details(client: &webclient::Client, uuid: &Uuid) -> Result<()> {
let controller = client.controller(uuid).context(WebClient)?;
let mut table = prettytable::Table::new();
table.set_titles(row!["Details"]);
table.add_row(row!["Model", controller.model_id]);
table.add_row(row!["Uuid", controller.uuid]);
table.add_row(row!["State", controller.state]);
table.add_row(row![
"Manufacturer",
if let Some(m) = controller.manufacturer {
m.to_string()
} else {
"‒".to_string()
}
]);
table.add_row(row![
"Firmware version",
if let Some(v) = controller.firmware_version {
v.to_string()
} else {
"‒".to_string()
}
]);
table.set_format(table_format());
table.print_tty(false);
Ok(())
}
fn modules(client: &webclient::Client, uuid: &Uuid) -> Result<()> {
let controller = client.controller(uuid).context(WebClient)?;
let description = client
.description_controllers()
.context(WebClient)?
.remove(&controller.model_id);
let modules = client.description_modules().context(WebClient)?;
let mut table = prettytable::Table::new();
table.set_titles(row![
"Module",
"Type",
"State",
"Assigned\nto jobset"
]);
for (id, details) in controller.modules {
let (state, assigned) = {
use webapi::ModuleState::*;
match details {
Unknown => ("Unknown", "‒".to_string()),
Uninitialized => ("Uninitialized", "‒".to_string()),
Ready { assigned_to_jobset } => {
("Ready", assigned_to_jobset.to_string())
}
Running { assigned_to_jobset } => {
("Running", assigned_to_jobset.to_string())
}
Error => ("Error", "‒".to_string()),
}
};
let module_name = if let Some(ref description) = description {
if let Some(ref module_details) =
description.capabilities.get(&id)
{
if let Some(ref module_description) =
modules.get(&module_details.module)
{
module_description.caption.to_string()
} else {
"‒".to_string()
}
} else {
"‒".to_string()
}
} else {
"‒".to_string()
};
table.add_row(row![id, module_name, state, assigned]);
}
table.set_format(table_format());
table.print_tty(false);
Ok(())
}
fn clear_job_set(
client: &webclient::Client,
uuid: &Uuid,
) -> Result<()> {
println!(
"CLEAR REQUEST: {:?}",
client.clear_job_set(uuid).context(WebClient)?
);
Ok(())
}
fn load_job_set(
client: &webclient::Client,
uuid: &Uuid,
params: &LoadJobSetParameters,
) -> Result<()> {
info!("Loading job set");
let mut parameters = Vec::new();
for path_name in ¶ms.parameters {
let path = Path::new(&path_name);
let description_path =
path.join("parameters_description.json");
let parameter_descriptions_path = path.join("parameters.json");
let parameter_values_path = path.join("parameterset.json");
let metadata = if description_path.is_file() {
ommui_file_loading::load_json::<
jobset::ParameterSetMetadata,
_,
>(description_path)
.context(OmmuiFileLoading)?
} else {
jobset::ParameterSetMetadata {
name: path_name.to_string(),
}
};
type ParameterDescriptions =
IndexMap<String, jobset::ParameterDescription>;
type ParameterValues =
IndexMap<String, base::DataValueDescriptive>;
let descriptions = ParameterDescriptions::load_from_path(
parameter_descriptions_path,
)
.context(OmmuiFileLoading)?;
let values =
ParameterValues::load_from_path(parameter_values_path)
.context(OmmuiFileLoading)?;
println!("Parameters: {:?} ({:?})", path, metadata.name);
parameters.push(jobset::ParameterSet {
metadata,
descriptions,
values,
});
}
let mut jobs = IndexMap::new();
for path_name in ¶ms.jobs {
let path = Path::new(&path_name);
let id = {
let base_name = if let Some(stem) = path.file_stem() {
stem.to_string_lossy().to_string()
} else {
"unknown_job".to_string()
};
let mut iter =
vec![base_name.to_string()].into_iter().chain(
(1usize..).map(|i| format!("{}_{}", base_name, i)),
);
iter.find(|s| !jobs.contains_key(s)).unwrap()
};
let job = jobset::Job::load_from_path(path)
.context(OmmuiFileLoading)?;
jobs.insert(id.to_string(), job);
}
type Mapping = IndexMap<String, jobset::ChannelAssignment>;
let channels = Mapping::load_from_path(¶ms.mapping)
.context(OmmuiFileLoading)?;
let metadata = jobset::Metadata::load_from_path(¶ms.metadata)
.context(OmmuiFileLoading)?;
let jobset = jobset::JobSet {
metadata,
jobs,
parameters,
channels,
};
println!(
"LOAD REQUEST: {:?}",
client.post_job_set(uuid, &jobset).context(WebClient)?
);
Ok(())
}
fn start_job(
client: &webclient::Client,
uuid: &Uuid,
params: &StartJobParameters,
) -> Result<()> {
info!("Starting job");
let job = params.job.to_string();
let message = webapi::JobControlAction::StartJob { job };
println!(
"LOAD REQUEST: {:?}",
client.post_job_control(uuid, &message).context(WebClient)?
);
Ok(())
}
fn stop_job(client: &webclient::Client, uuid: &Uuid) -> Result<()> {
info!("Stopping job");
println!(
"LOAD REQUEST: {:?}",
client
.post_job_control(uuid, &webapi::JobControlAction::StopJob)
.context(WebClient)?
);
Ok(())
}
fn monitor(client: &webclient::Client, uuid: &Uuid) -> Result<()> {
let events = client.controller_events(uuid).context(WebClient)?;
for event in events {
println!(
"{}",
serde_json::to_string(&event).context(SerdeJson)?
);
}
Ok(())
}
fn joblog(client: &webclient::Client, uuid: &Uuid) -> Result<()> {
let events = client.joblog_events(uuid).context(WebClient)?;
for event in events {
println!(
"{}",
serde_json::to_string(&event).context(SerdeJson)?
);
}
Ok(())
}
fn flash_firmware(
client: &webclient::Client,
device_uuid: &Uuid,
) -> Result<()> {
info!("Flashing latest firmware");
let events =
client.controller_events(device_uuid).context(WebClient)?;
client.post_flash_firmware(device_uuid).context(WebClient)?;
for event in events {
use webapi::ControllerEvent::*;
match event {
FlashingProgressMessage { ref message, .. } => {
info!("{} → {}", device_uuid, message);
}
FlashingErrorMessage { ref message } => {
warn!("{} → {}", device_uuid, message);
}
FlashingFinished {
ref success,
ref message,
} => {
if *success {
info!("{} → success. {}", device_uuid, message);
} else {
info!("{} → failed. {}", device_uuid, message);
}
return Ok(());
}
_ => { }
}
}
Ok(())
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "snake_case")]
struct ParametersDescription {
name: String,
}
impl ControllersAction {
fn run(&self, client: &webclient::Client) -> Result<()> {
match *self {
ControllersAction::List => Self::list(client),
ControllersAction::Monitor => Self::monitor(client),
}
}
fn list(client: &webclient::Client) -> Result<()> {
let list = client.controllers().context(WebClient)?;
if !list.is_empty() {
let mut table = prettytable::Table::new();
table.set_titles(row!["Model", "Uuid", "Status", "Firmware"]);
for controller in list.values() {
let firmware = match (
&controller.firmware_version,
&controller.firmware_version_available,
) {
(None, None) => "Unknown".to_string(),
(Some(ref i), None) => {
format!("{} (no update available)", i)
}
(None, Some(ref a)) => {
format!("unknown ({} available)", a)
}
(Some(ref i), Some(ref a)) if i == a => {
format!("{} (up-to-date)", i)
}
(Some(ref i), Some(ref a)) => {
format!("{} ({} available)", i, a)
}
};
table.add_row(row![
controller.model_id,
controller.uuid,
controller.state,
firmware
]);
}
table.set_format(table_format());
table.print_tty(false);
} else {
println!("No controllers present");
}
Ok(())
}
fn monitor(client: &webclient::Client) -> Result<()> {
let events = client.controllers_events().context(WebClient)?;
for event in events {
println!(
"{}",
serde_json::to_string(&event).context(SerdeJson)?
);
}
Ok(())
}
}
#[derive(StructOpt, Debug)]
enum Action {
#[structopt(
name = "check",
about = "Check that the connection to the XIO daemon works"
)]
Check,
#[structopt(
name = "controllers",
about = "Commands for all controllers"
)]
Controllers(ControllersAction),
#[structopt(name = "controller", about = "Controller actions")]
Controller {
uuid: Uuid,
#[structopt(subcommand)]
action: ControllerAction,
},
}
impl Action {
fn run(&self, client: &webclient::Client) -> Result<()> {
match *self {
Action::Check => self.run_check(client),
Action::Controllers(ref action) => action.run(client),
Action::Controller { uuid, ref action } => {
action.run(client, &uuid)
}
}
}
fn run_check(&self, client: &webclient::Client) -> Result<()> {
let description = client.description_api().context(WebClient)?;
ensure!(
description.version == webapi::ApiVersion::V1,
ApiVersionMismatch {
found: description.version,
required: webapi::ApiVersion::V1
}
);
ensure!(
description.uuid == webapi::api_uuid(),
ApiUuidMismatch {
found: description.uuid,
required: webapi::api_uuid()
}
);
let mut table = prettytable::Table::new();
table.set_titles(row!["Info", "Value"]);
table.add_row(row![
"API Version",
format!("{:?}", description.version),
]);
table.add_row(row!["API UUID", format!("{}", description.uuid),]);
table.add_row(row!["Description", description.description]);
table.set_format(table_format());
table.print_tty(false);
Ok(())
}
}
#[derive(StructOpt, Debug)]
#[structopt(name = "xioclient", about = "XIO client")]
pub struct Opt {
#[structopt(long = "verbose", short = "v")]
verbose: bool,
#[structopt(long = "url", default_value = "http://localhost:4488/")]
url: String,
#[structopt(subcommand)]
action: Action,
}
impl Opt {
fn run(&self) -> Result<()> {
let client = webclient::Client::new(&self.url);
self.action.run(&client)
}
}
fn run(opt: &Opt) -> Result<()> {
opt.run()
}
fn main() {
let opt = Opt::from_args();
let mut builder = env_logger::Builder::new();
builder.parse_filters("xio_webclient=info");
builder.parse_filters("ommui_file_loading=info");
builder.parse_filters("xioclient=info");
if opt.verbose {
builder.parse_filters("xio_webclient=debug");
builder.parse_filters("xioclient=debug");
}
if let Ok(rust_log) = std::env::var("RUST_LOG") {
builder.parse_filters(&rust_log);
}
builder.init();
if let Err(s) = run(&opt) {
warn!("{:#?}", s);
if let Some(backtrace) = ErrorCompat::backtrace(&s) {
warn!("{}", backtrace);
}
std::process::exit(2);
}
}