nut-rs/nut-client/src/cmd.rs
2021-08-01 00:41:28 -04:00

639 lines
20 KiB
Rust

use core::fmt;
use crate::{ClientError, NutError, Variable};
#[derive(Debug, Clone)]
pub enum Command<'a> {
Get(&'a [&'a str]),
/// Passes the login username.
SetUsername(&'a str),
/// Passes the login password.
SetPassword(&'a str),
/// Queries for a list. Allows for any number of arguments, which forms a single query.
List(&'a [&'a str]),
/// Tells upsd to switch to TLS, so all future communications will be encrypted.
StartTLS,
/// Queries the network version.
NetworkVersion,
/// Queries the server version.
Version,
/// Gracefully shuts down the connection.
Logout,
}
impl<'a> Command<'a> {
/// The network identifier of the command.
pub fn name(&self) -> &'static str {
match self {
Self::Get(_) => "GET",
Self::SetUsername(_) => "USERNAME",
Self::SetPassword(_) => "PASSWORD",
Self::List(_) => "LIST",
Self::StartTLS => "STARTTLS",
Self::NetworkVersion => "NETVER",
Self::Version => "VER",
Self::Logout => "LOGOUT",
}
}
/// The arguments of the command to serialize.
pub fn args(&self) -> Vec<&str> {
match self {
Self::Get(cmd) => cmd.to_vec(),
Self::SetUsername(username) => vec![username],
Self::SetPassword(password) => vec![password],
Self::List(query) => query.to_vec(),
_ => Vec::new(),
}
}
}
impl<'a> fmt::Display for Command<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut args = self.args();
args.insert(0, self.name());
write!(f, "{}", shell_words::join(args))
}
}
#[derive(Debug, Clone)]
pub enum Response {
/// A successful response.
Ok,
/// Marks the beginning of a list response.
BeginList(String),
/// Marks the end of a list response.
EndList(String),
/// A variable (VAR) response.
///
/// Params: (var name, var value)
Var(String, String),
/// A UPS (UPS) response.
///
/// Params: (device name, device description)
Ups(String, String),
/// A client (CLIENT) response.
///
/// Params: (client IP)
Client(String),
/// A command (CMD) response.
///
/// Params: (command name)
Cmd(String),
/// A command description (CMDDESC) response.
///
/// Params: (command description)
CmdDesc(String),
/// A mutable variable (RW) response.
///
/// Params: (var name, var value)
Rw(String, String),
}
impl Response {
pub fn from_args(mut args: Vec<String>) -> crate::Result<Response> {
if args.is_empty() {
return Err(
NutError::Generic("Parsing server response failed: empty line".into()).into(),
);
}
let cmd_name = args.remove(0);
match cmd_name.as_str() {
"OK" => Ok(Self::Ok),
"ERR" => {
if args.is_empty() {
Err(NutError::Generic("Unspecified server error".into()).into())
} else {
let err_type = args.remove(0);
match err_type.as_str() {
"ACCESS-DENIED" => Err(NutError::AccessDenied.into()),
"UNKNOWN-UPS" => Err(NutError::UnknownUps.into()),
"FEATURE-NOT-CONFIGURED" => Err(NutError::FeatureNotConfigured.into()),
_ => Err(NutError::Generic(format!(
"Server error: {} {}",
err_type,
args.join(" ")
))
.into()),
}
}
}
"BEGIN" => {
if args.is_empty() {
Err(NutError::Generic("Unspecified BEGIN type".into()).into())
} else {
let begin_type = args.remove(0);
if &begin_type != "LIST" {
Err(
NutError::Generic(format!("Unexpected BEGIN type: {}", begin_type))
.into(),
)
} else {
let args = shell_words::join(args);
Ok(Response::BeginList(args))
}
}
}
"END" => {
if args.is_empty() {
Err(NutError::Generic("Unspecified END type".into()).into())
} else {
let begin_type = args.remove(0);
if &begin_type != "LIST" {
Err(
NutError::Generic(format!("Unexpected END type: {}", begin_type))
.into(),
)
} else {
let args = shell_words::join(args);
Ok(Response::EndList(args))
}
}
}
"VAR" => {
let _var_device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified VAR device name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let var_name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified VAR name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let var_value = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified VAR value in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Var(var_name, var_value))
}
"RW" => {
let _var_device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified RW device name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let var_name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified RW name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let var_value = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified RW value in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Rw(var_name, var_value))
}
"UPS" => {
let name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified UPS name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let description = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified UPS description in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Ups(name, description))
}
"CLIENT" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CLIENT device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let ip_address = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CLIENT IP in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Client(ip_address))
}
"CMD" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CMD device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CMD name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::Cmd(name))
}
"CMDDESC" => {
let _device = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CMDDESC device in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let _name = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CMDDESC name in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
let desc = if args.is_empty() {
Err(ClientError::from(NutError::Generic(
"Unspecified CMDDESC description in response".into(),
)))
} else {
Ok(args.remove(0))
}?;
Ok(Response::CmdDesc(desc))
}
_ => Err(NutError::UnknownResponseType(cmd_name).into()),
}
}
pub fn expect_ok(&self) -> crate::Result<&Response> {
match self {
Self::Ok => Ok(self),
_ => Err(NutError::UnexpectedResponse.into()),
}
}
pub fn expect_begin_list(self, expected_args: &[&str]) -> crate::Result<Response> {
let expected_args = shell_words::join(expected_args);
if let Self::BeginList(args) = &self {
if &expected_args == args {
Ok(self)
} else {
Err(NutError::UnexpectedResponse.into())
}
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_var(&self) -> crate::Result<Variable> {
if let Self::Var(name, value) = &self {
Ok(Variable::parse(name, value.to_owned()))
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_rw(&self) -> crate::Result<Variable> {
if let Self::Rw(name, value) = &self {
Ok(Variable::parse(name, value.to_owned()))
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_ups(&self) -> crate::Result<(String, String)> {
if let Self::Ups(name, description) = &self {
Ok((name.to_owned(), description.to_owned()))
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_client(&self) -> crate::Result<String> {
if let Self::Client(client_ip) = &self {
Ok(client_ip.to_owned())
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_cmd(&self) -> crate::Result<String> {
if let Self::Cmd(name) = &self {
Ok(name.to_owned())
} else {
Err(NutError::UnexpectedResponse.into())
}
}
pub fn expect_cmddesc(&self) -> crate::Result<String> {
if let Self::CmdDesc(description) = &self {
Ok(description.to_owned())
} else {
Err(NutError::UnexpectedResponse.into())
}
}
}
/// A macro for implementing `LIST` commands.
///
/// Each function should return a 2-tuple with
/// (1) the query to pass to `LIST`
/// (2) a closure for mapping each `Response` row to the return type
macro_rules! implement_list_commands {
(
$(
$(#[$attr:meta])+
$vis:vis fn $name:ident($($argname:ident: $argty:ty),*) -> $retty:ty {
(
$query:block,
$mapper:block,
)
}
)*
) => {
impl crate::blocking::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd(Command::List($query))?;
let list = conn.read_list($query)?;
list.into_iter().map($mapper).collect()
},
}
}
)*
}
#[cfg(feature = "async")]
impl crate::tokio::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis async fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd(Command::List($query)).await?;
let list = conn.read_list($query).await?;
list.into_iter().map($mapper).collect()
},
}
}
)*
}
};
}
/// A macro for implementing `GET` commands.
///
/// Each function should return a 2-tuple with
/// (1) the query to pass to `GET`
/// (2) a closure for mapping the `Response` row to the return type
macro_rules! implement_get_commands {
(
$(
$(#[$attr:meta])+
$vis:vis fn $name:ident($($argname:ident: $argty:ty),*) -> $retty:ty {
(
$query:block,
$mapper:block,
)
}
)*
) => {
impl crate::blocking::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd(Command::Get($query))?;
($mapper)(conn.read_response()?)
},
}
}
)*
}
#[cfg(feature = "async")]
impl crate::tokio::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis async fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd(Command::Get($query)).await?;
($mapper)(conn.read_response().await?)
},
}
}
)*
}
};
}
/// A macro for implementing simple/direct commands.
///
/// Each function should return a 2-tuple with
/// (1) the command to pass
/// (2) a closure for mapping the `String` row to the return type
macro_rules! implement_simple_commands {
(
$(
$(#[$attr:meta])+
$vis:vis fn $name:ident($($argname:ident: $argty:ty),*) -> $retty:ty {
(
$cmd:block,
$mapper:block,
)
}
)*
) => {
impl crate::blocking::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd($cmd)?;
($mapper)(conn.read_plain_response()?)
},
}
}
)*
}
#[cfg(feature = "async")]
impl crate::tokio::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis async fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<$retty> {
match self {
Self::Tcp(conn) => {
conn.write_cmd($cmd).await?;
($mapper)(conn.read_plain_response().await?)
},
}
}
)*
}
};
}
/// A macro for implementing action commands that return `OK`.
///
/// Each function should return the command to pass.
macro_rules! implement_action_commands {
(
$(
$(#[$attr:meta])+
$vis:vis fn $name:ident($($argname:ident: $argty:ty),*) $cmd:block
)*
) => {
impl crate::blocking::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<()> {
match self {
Self::Tcp(conn) => {
conn.write_cmd($cmd)?;
conn.read_response()?.expect_ok()?;
Ok(())
},
}
}
)*
}
#[cfg(feature = "async")]
impl crate::tokio::Connection {
$(
$(#[$attr])*
#[allow(dead_code)]
$vis async fn $name(&mut self$(, $argname: $argty)*) -> crate::Result<()> {
match self {
Self::Tcp(conn) => {
conn.write_cmd($cmd).await?;
conn.read_response().await?.expect_ok()?;
Ok(())
},
}
}
)*
}
};
}
implement_list_commands! {
/// Queries a list of UPS devices.
pub fn list_ups() -> Vec<(String, String)> {
(
{ &["UPS"] },
{ |row: Response| row.expect_ups() },
)
}
/// Queries the list of client IP addresses connected to the given device.
pub fn list_clients(ups_name: &str) -> Vec<String> {
(
{ &["CLIENT", ups_name] },
{ |row: Response| row.expect_client() },
)
}
/// Queries the list of variables for a UPS device.
pub fn list_vars(ups_name: &str) -> Vec<Variable> {
(
{ &["VAR", ups_name] },
{ |row: Response| row.expect_var() },
)
}
/// Queries the list of mutable variables for a UPS device.
pub fn list_mutable_variables(ups_name: &str) -> Vec<Variable> {
(
{ &["RW", ups_name] },
{ |row: Response| row.expect_rw() },
)
}
/// Queries the list of commands available for the given device.
pub fn list_commands(ups_name: &str) -> Vec<String> {
(
{ &["CMD", ups_name] },
{ |row: Response| row.expect_cmd() },
)
}
}
implement_get_commands! {
/// Queries one variable for a UPS device.
pub fn get_var(ups_name: &str, variable: &str) -> Variable {
(
{ &["VAR", ups_name, variable] },
{ |row: Response| row.expect_var() },
)
}
/// Queries the description of a UPS command.
pub fn get_command_description(ups_name: &str, variable: &str) -> String {
(
{ &["CMDDESC", ups_name, variable] },
{ |row: Response| row.expect_cmddesc() },
)
}
}
implement_simple_commands! {
/// Queries the network protocol version.
pub fn get_network_version() -> String {
(
{ Command::NetworkVersion },
{ |row: String| Ok(row) },
)
}
/// Queries the server NUT version.
pub fn get_server_version() -> String {
(
{ Command::Version },
{ |row: String| Ok(row) },
)
}
}
implement_action_commands! {
/// Sends the login username.
pub(crate) fn set_username(username: &str) {
Command::SetUsername(username)
}
/// Sends the login password.
pub(crate) fn set_password(password: &str) {
Command::SetPassword(password)
}
/// Gracefully shuts down the connection.
pub(crate) fn logout() {
Command::Logout
}
}