switch to configfile instead of env

This commit is contained in:
Robin Appelman 2024-02-29 23:39:46 +01:00
commit c4d579b033
9 changed files with 629 additions and 274 deletions

68
src/config.rs Normal file
View file

@ -0,0 +1,68 @@
use crate::error::Error;
use secretfile::load;
use serde::Deserialize;
use std::fs::read_to_string;
use std::net::{IpAddr, Ipv4Addr};
use std::path::Path;
#[derive(Debug, Deserialize)]
pub struct Config {
pub mqtt: Option<MqttConfig>,
pub ssh: SshConfig,
pub exporter: ExporterConfig,
}
impl Config {
pub fn load(path: &Path) -> Result<Self, Error> {
let content = read_to_string(path).map_err(Error::ReadConfig)?;
toml::from_str(&content).map_err(Error::ParseConfig)
}
}
#[derive(Debug, Deserialize)]
pub struct MqttConfig {
pub hostname: String,
#[serde(default = "default_mqtt_port")]
pub port: u16,
pub username: String,
password_file: String,
}
fn default_mqtt_port() -> u16 {
1883
}
impl MqttConfig {
pub fn password(&self) -> Result<String, Error> {
Ok(load(&self.password_file)?)
}
}
#[derive(Debug, Deserialize)]
pub struct SshConfig {
pub address: String,
pubkey_file: String,
key_file: String,
}
impl SshConfig {
pub fn key(&self) -> Result<String, Error> {
Ok(load(&self.key_file)?)
}
pub fn pubkey(&self) -> Result<String, Error> {
Ok(load(&self.pubkey_file)?)
}
}
#[derive(Debug, Deserialize)]
pub struct ExporterConfig {
#[serde(default = "default_address")]
pub address: IpAddr,
pub port: u16,
pub interfaces: Vec<String>,
}
fn default_address() -> IpAddr {
Ipv4Addr::new(127, 0, 0, 1).into()
}

18
src/error.rs Normal file
View file

@ -0,0 +1,18 @@
use secretfile::SecretError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("failed to read config: {0}")]
ReadConfig(std::io::Error),
#[error("failed to parse config: {0}")]
ParseConfig(toml::de::Error),
#[error("failed to secret: {0:#}")]
Secret(#[from] SecretError),
#[error("failed to connect to ssh server: {0}")]
SshConnect(std::io::Error),
#[error("failed to start ssh session: {0}")]
SshSession(ssh2::Error),
#[error("failed to authenticate ssh session: {0}")]
SshAuth(ssh2::Error),
}

61
src/listener.rs Normal file
View file

@ -0,0 +1,61 @@
use crate::error::Error;
use ssh2::{ErrorCode, Session};
use std::fmt::Debug;
use std::io::Read;
use std::net::{TcpStream, ToSocketAddrs};
use tracing::{debug, info};
pub struct WifiLister {
command: String,
session: Session,
}
impl WifiLister {
pub fn new<A: ToSocketAddrs + Debug>(
addr: A,
key: &str,
pubkey: &str,
interfaces: &[String],
) -> Result<Self, Error> {
debug!(address = ?addr, "connecting to ssh");
let tcp = TcpStream::connect(&addr).map_err(Error::SshConnect)?;
let mut session = Session::new().map_err(Error::SshSession)?;
session.set_tcp_stream(tcp);
session.handshake().map_err(Error::SshSession)?;
session
.userauth_pubkey_memory("admin", Some(pubkey), key, None)
.map_err(Error::SshAuth)?;
let command = if interfaces.is_empty() {
"wl assoclist".to_string()
} else {
let commands: Vec<String> = interfaces
.iter()
.map(|interface| format!("wl -a {} assoclist", interface))
.collect();
commands.join(" && ")
};
info!("ssh connected");
Ok(WifiLister { session, command })
}
pub fn list_connected_devices(&self) -> Result<Vec<String>, ssh2::Error> {
let mut channel = self.session.channel_session()?;
debug!(command = self.command, "sending ssh command");
channel.exec(&self.command)?;
let mut s = String::new();
channel.read_to_string(&mut s).map_err(|e| {
ssh2::Error::new(
ErrorCode::Session(e.raw_os_error().unwrap_or_default()),
"error reading from ssh stream",
)
})?;
channel.wait_close()?;
Ok(s.lines()
.map(|s| s.trim_start_matches("assoclist ").to_string())
.collect())
}
}

View file

@ -1,109 +1,63 @@
mod config;
mod error;
mod listener;
use crate::config::Config;
use crate::listener::WifiLister;
use clap::Parser;
use main_error::MainError;
use rumqttc::{AsyncClient, ClientError, MqttOptions, QoS};
use ssh2::{ErrorCode, Session};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fmt::{Display, Formatter, Write};
use std::io::prelude::*;
use std::net::{TcpStream, ToSocketAddrs};
use std::path::Path;
use std::str::FromStr;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::{spawn, time::sleep};
use tracing::{error, info};
use warp::Filter;
struct WifiLister {
command: String,
session: Session,
}
impl WifiLister {
pub fn new<A: ToSocketAddrs, Priv: AsRef<OsStr>, Pub: AsRef<OsStr>>(
addr: A,
keyfile: Priv,
pubkey: Pub,
interfaces: &[&str],
) -> Result<Self, MainError> {
let tcp = TcpStream::connect(addr)?;
let mut session = Session::new()?;
session.set_tcp_stream(tcp);
session.handshake()?;
let keyfile = Path::new(keyfile.as_ref());
let pubkey = Path::new(pubkey.as_ref());
session.userauth_pubkey_file("admin", Some(pubkey), keyfile, None)?;
let command = if interfaces.is_empty() {
"wl assoclist".to_string()
} else {
let commands: Vec<String> = interfaces
.iter()
.map(|interface| format!("wl -a {} assoclist", interface))
.collect();
commands.join(" && ")
};
Ok(WifiLister { session, command })
}
pub fn list_connected_devices(&self) -> Result<Vec<String>, ssh2::Error> {
let mut channel = self.session.channel_session()?;
channel.exec(&self.command)?;
let mut s = String::new();
channel.read_to_string(&mut s).map_err(|e| {
ssh2::Error::new(
ErrorCode::Session(e.raw_os_error().unwrap_or_default()),
"error reading from ssh stream",
)
})?;
channel.wait_close()?;
Ok(s.lines()
.map(|s| s.trim_start_matches("assoclist ").to_string())
.collect())
}
#[derive(Parser, Debug)]
struct Args {
/// Path to config file
config: PathBuf,
}
#[tokio::main]
async fn main() -> Result<(), MainError> {
let mut env: HashMap<String, String> = dotenvy::vars().collect();
let addr = env.remove("ADDR").ok_or("No ADDR set")?;
let keyfile = env.remove("KEYFILE").ok_or("No KEYFILE set")?;
let pubfile = env.remove("PUBFILE").ok_or("No PUBFILE set")?;
let port = env
.get("PORT")
.and_then(|s| u16::from_str(s).ok())
.unwrap_or(80);
let mqtt_host = env.remove("MQTT_HOSTNAME");
let mqtt_user = env.remove("MQTT_USERNAME");
let mqtt_pass = env.remove("MQTT_PASSWORD");
let interfaces: Vec<&str> = env
.get("INTERFACES")
.map(|interfaces| interfaces.split(' ').collect())
.unwrap_or_default();
let mqtt_options = match (mqtt_host, mqtt_user, mqtt_pass) {
(Some(host), Some(user), Some(pass)) => {
let mut mqtt_options = MqttOptions::new("wifi-exporter", host, 1883);
tracing_subscriber::fmt::init();
let args = Args::parse();
let config = Config::load(&args.config)?;
let mqtt_options = match &config.mqtt {
Some(mqtt_config) => {
let mut mqtt_options =
MqttOptions::new("wifi-exporter", &mqtt_config.hostname, mqtt_config.port);
mqtt_options.set_keep_alive(Duration::from_secs(5));
mqtt_options.set_credentials(user, pass);
println!("mqtt enabled");
mqtt_options.set_credentials(&mqtt_config.username, mqtt_config.password()?);
info!("mqtt enabled");
Some(mqtt_options)
}
_ => {
println!("mqtt disabled");
info!("mqtt disabled");
None
}
};
if interfaces.is_empty() {
println!("Listening on default interface");
if config.exporter.interfaces.is_empty() {
info!("Listening on default interface");
} else {
println!("Listening on interfaces: {}", interfaces.join(", "));
info!(
"Listening on interfaces: {}",
config.exporter.interfaces.join(", ")
);
}
let connected: Arc<Mutex<DeviceStates>> = Default::default();
let wifi_listener = WifiLister::new(addr, &keyfile, &pubfile, &interfaces)?;
let wifi_listener = WifiLister::new(
&config.ssh.address,
&config.ssh.key()?,
&config.ssh.pubkey()?,
&config.exporter.interfaces,
)?;
spawn(listener(wifi_listener, connected.clone(), mqtt_options));
@ -114,7 +68,9 @@ async fn main() -> Result<(), MainError> {
})
.expect("Error setting Ctrl-C handler");
warp::serve(metrics).run(([0, 0, 0, 0], port)).await;
warp::serve(metrics)
.run((config.exporter.address, config.exporter.port))
.await;
Ok(())
}
@ -207,15 +163,15 @@ async fn listener(
Ok(devices) => {
let updates = connected.lock().unwrap().update(devices);
for (mac, update) in updates {
println!("{} {}", mac, update);
info!(mac, %update, "change detected");
if let Some(client) = client.as_mut() {
if let Err(e) = send_update(client, mac, update).await {
eprintln!("Error while sending mqtt update: {:?}", e);
error!(e = ?e, "Error while sending mqtt update: {e}");
}
}
}
}
Err(e) => eprintln!("Error while listing devices {:#?}", e),
Err(e) => error!(e = ?e, "Error while listing devices {e}"),
}
sleep(Duration::from_secs(5)).await;
}