Implement rupsc, clone of upsc (#5)

Fixes #4
This commit is contained in:
Aram Peres 2021-07-31 04:18:39 -04:00 committed by GitHub
parent 43121ce2ea
commit 8556a7ca0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 676 additions and 82 deletions

21
rupsc/Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "rupsc"
version = "0.0.4"
authors = ["Aram Peres <aram.peres@wavy.fm>"]
edition = "2018"
description = "A demo program to display UPS variables"
categories = ["network-programming"]
keywords = ["ups", "nut"]
repository = "https://github.com/aramperes/nut-client-rs"
readme = "README.md"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = "2.33.3"
anyhow = "1"
[dependencies.nut-client]
version = "0.0.4"
path = "../nut-client"

21
rupsc/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2021 Aram Peres
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

65
rupsc/README.md Normal file
View file

@ -0,0 +1,65 @@
# rupsc
[![crates.io](https://img.shields.io/crates/v/rupsc.svg)](https://crates.io/crates/rupsc)
[![Documentation](https://docs.rs/nut-client/badge.svg)](https://docs.rs/nut-client)
[![MIT licensed](https://img.shields.io/crates/l/rupsc.svg)](./LICENSE)
[![CI](https://github.com/aramperes/nut-client-rs/workflows/CI/badge.svg)](https://github.com/aramperes/nut-client-rs/actions?query=workflow%3ACI)
A Rust clone of [upsc](https://networkupstools.org/docs/man/upsc.html),
the [Network UPS Tools](https://github.com/networkupstools/nut) (NUT) demo program to display UPS variables.
Written using the [nut-client](https://github.com/aramperes/nut-client-rs) crate.
- Connect to `upsd`/`nut-server` using TCP
- List UPS devices
- List variables for a UPS device
- Get variable value of a UPS device
- List clients connected to a UPS device
## Installation
```bash
# Using cargo
cargo install rupsc
# Or, build for other targets
# (make sure you install the appropriate toolchain & gcc linker)
cargo build --release --target armv7-unknown-linux-gnueabihf
cargo build --release --target aarch64-unknown-linux-gnu
cargo build --release --target arm-unknown-linux-gnueabihf
```
## Usage
This is a clone of [`upsc`](https://networkupstools.org/docs/man/upsc.html), so the usage is the same:
```bash
# Show usage
rupsc -h
# List variables on UPS device "nutdev1" (assumes upsd running on 127.0.0.1:3493)
rupsc nutdev1
# List variables on UPS device "nutdev1" (remove upsd)
rupsc nutdev1@192.168.1.2:3493
# List available UPS devices
rupsc -l
# List available UPS devices, with description
rupsc -L
# List clients connected to UPS device "nutdev1"
rupsc -c nutdev1
```
However, there are also some additions:
```bash
# Enable network debugging (global flag).
ruspc -D
```
## Pronunciation
> r-oopsie

66
rupsc/src/cmd.rs Normal file
View file

@ -0,0 +1,66 @@
use crate::parser::UpsdName;
use anyhow::Context;
use core::convert::TryInto;
use nut_client::blocking::Connection;
/// Lists each UPS on the upsd server, one per line.
pub fn list_devices(server: UpsdName, with_description: bool, debug: bool) -> anyhow::Result<()> {
let mut conn = connect(server, debug)?;
for (name, description) in conn.list_ups()? {
if with_description {
println!("{}: {}", name, description);
} else {
println!("{}", name);
}
}
Ok(())
}
pub fn print_variable(server: UpsdName, variable: &str, debug: bool) -> anyhow::Result<()> {
let ups_name = server
.upsname
.with_context(|| "ups name must be specified: <upsname>[@<hostname>[:<port>]]")?;
let mut conn = connect(server, debug)?;
let variable = conn.get_var(ups_name, variable)?;
println!("{}", variable.value());
Ok(())
}
pub fn list_variables(server: UpsdName, debug: bool) -> anyhow::Result<()> {
let ups_name = server
.upsname
.with_context(|| "ups name must be specified: <upsname>[@<hostname>[:<port>]]")?;
let mut conn = connect(server, debug)?;
for var in conn.list_vars(ups_name)? {
println!("{}", var);
}
Ok(())
}
pub fn list_clients(server: UpsdName, debug: bool) -> anyhow::Result<()> {
let ups_name = server
.upsname
.with_context(|| "ups name must be specified: <upsname>[@<hostname>[:<port>]]")?;
let mut conn = connect(server, debug)?;
for client_ip in conn.list_clients(ups_name)? {
println!("{}", client_ip);
}
Ok(())
}
fn connect(server: UpsdName, debug: bool) -> anyhow::Result<Connection> {
let host = server.try_into()?;
let config = nut_client::ConfigBuilder::new()
.with_host(host)
.with_debug(debug)
.build();
Connection::new(config).with_context(|| format!("Failed to connect to upsd: {}", server))
}

85
rupsc/src/main.rs Normal file
View file

@ -0,0 +1,85 @@
///! # rupsc
///! A demo program to display UPS variables.
///! This a Rust clone of [upsc](https://github.com/networkupstools/nut/blob/master/clients/upsc.c).
///!
///! P.S.: pronounced "r-oopsie".
mod cmd;
mod parser;
use crate::parser::UpsdName;
use anyhow::Context;
use clap::{App, Arg};
use core::convert::TryInto;
fn main() -> anyhow::Result<()> {
let args = App::new(clap::crate_name!())
.version(clap::crate_version!())
.author(clap::crate_authors!())
.about(clap::crate_description!())
.arg(
Arg::with_name("list")
.short("l")
.conflicts_with_all(&["list-full", "clients"])
.takes_value(false)
.help("Lists each UPS on <hostname>, one per line."),
)
.arg(
Arg::with_name("list-full")
.short("L")
.conflicts_with_all(&["list", "clients"])
.takes_value(false)
.help("Lists each UPS followed by its description (from ups.conf)."),
)
.arg(
Arg::with_name("clients")
.short("c")
.conflicts_with_all(&["list", "list-full"])
.takes_value(false)
.help("Lists each client connected on <upsname>, one per line."),
)
.arg(
Arg::with_name("debug")
.short("D")
.takes_value(false)
.help("Enables debug mode (logs network commands to stderr)."),
)
.arg(
Arg::with_name("upsd-server")
.required(false)
.value_name("[upsname][@<hostname>[:<port>]]")
.help("upsd server (with optional upsname, if applicable)."),
)
.arg(
Arg::with_name("variable")
.required(false)
.value_name("variable")
.help("Optional, display this variable only."),
)
.get_matches();
let server: parser::UpsdName = args.value_of("upsd-server").map_or_else(
|| Ok(UpsdName::default()),
|s| s.try_into().with_context(|| "Invalid upsd server name"),
)?;
let debug = args.is_present("debug");
if args.is_present("list") {
return cmd::list_devices(server, false, debug);
}
if args.is_present("list-full") {
return cmd::list_devices(server, true, debug);
}
if args.is_present("clients") {
return cmd::list_clients(server, debug);
}
// Fallback: prints one variable (or all of them)
if let Some(variable) = args.value_of("variable") {
cmd::print_variable(server, variable, debug)
} else {
cmd::list_variables(server, debug)
}
}

150
rupsc/src/parser.rs Normal file
View file

@ -0,0 +1,150 @@
use anyhow::Context;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::net::ToSocketAddrs;
pub const DEFAULT_HOSTNAME: &str = "127.0.0.1";
pub const DEFAULT_PORT: u16 = 3493;
/// Connection information for a upsd server.
///
/// The upsname is optional depending on context.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct UpsdName<'a> {
pub upsname: Option<&'a str>,
pub hostname: &'a str,
pub port: u16,
}
impl<'a> Default for UpsdName<'a> {
fn default() -> Self {
UpsdName {
upsname: None,
hostname: DEFAULT_HOSTNAME,
port: DEFAULT_PORT,
}
}
}
impl<'a> TryFrom<&'a str> for UpsdName<'a> {
type Error = anyhow::Error;
fn try_from(value: &'a str) -> anyhow::Result<UpsdName<'a>> {
let mut upsname: Option<&str> = None;
let mut hostname = DEFAULT_HOSTNAME;
let mut port = DEFAULT_PORT;
if value.contains(':') {
let mut split = value.splitn(2, ':');
let prefix = split.next().unwrap();
port = split
.next()
.unwrap()
.parse::<u16>()
.with_context(|| "invalid port number")?;
if prefix.contains('@') {
let mut split = prefix.splitn(2, '@');
upsname = Some(split.next().unwrap());
hostname = split.next().unwrap();
} else {
hostname = prefix;
}
} else if value.contains('@') {
let mut split = value.splitn(2, '@');
upsname = Some(split.next().unwrap());
hostname = split.next().unwrap();
} else {
upsname = Some(value);
}
Ok(UpsdName {
upsname,
hostname,
port,
})
}
}
impl<'a> TryInto<nut_client::Host> for UpsdName<'a> {
type Error = anyhow::Error;
fn try_into(self) -> anyhow::Result<nut_client::Host> {
Ok((String::from(self.hostname), self.port)
.to_socket_addrs()
.with_context(|| "Failed to convert to SocketAddr")?
.next()
.with_context(|| "Failed to convert to SocketAddr")?
.into())
}
}
impl<'a> fmt::Display for UpsdName<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(upsname) = self.upsname {
write!(f, "{}@", upsname)?;
}
write!(f, "{}:{}", self.hostname, self.port)
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::convert::TryInto;
#[test]
fn test_upsdname_parser_full() {
let name: UpsdName = "ups@notlocal:1234".try_into().unwrap();
assert_eq!(
name,
UpsdName {
upsname: Some("ups"),
hostname: "notlocal",
port: 1234
}
);
assert_eq!(format!("{}", name), "ups@notlocal:1234");
}
#[test]
fn test_upsdname_parser_no_name() {
let name: UpsdName = "notlocal:5678".try_into().unwrap();
assert_eq!(
name,
UpsdName {
upsname: None,
hostname: "notlocal",
port: 5678
}
);
assert_eq!(format!("{}", name), "notlocal:5678");
}
#[test]
fn test_upsdname_parser_no_port_no_hostname() {
let name: UpsdName = "ups0".try_into().unwrap();
assert_eq!(
name,
UpsdName {
upsname: Some("ups0"),
hostname: DEFAULT_HOSTNAME,
port: DEFAULT_PORT
}
);
assert_eq!(format!("{}", name), "ups0@127.0.0.1:3493");
}
#[test]
fn test_upsdname_parser_no_port() {
let name: UpsdName = "ups@notlocal".try_into().unwrap();
assert_eq!(
name,
UpsdName {
upsname: Some("ups"),
hostname: "notlocal",
port: DEFAULT_PORT
}
);
assert_eq!(format!("{}", name), "ups@notlocal:3493");
}
}