2017-11-17 00:18:59 +01:00
|
|
|
#[macro_use]
|
|
|
|
extern crate serde_derive;
|
|
|
|
#[macro_use]
|
|
|
|
extern crate derive_error_chain;
|
|
|
|
|
|
|
|
extern crate serde_yaml;
|
|
|
|
|
|
|
|
use std::fs::File;
|
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
//read the files
|
|
|
|
//let paths = [Path::new("test.yml")];
|
|
|
|
let filenames: Vec<_> = std::env::args_os().skip(1).collect();
|
|
|
|
let hosts: Vec<Host> = filenames.iter()
|
|
|
|
.map(Path::new)
|
|
|
|
.map(|path| Host::from_file(&path))
|
|
|
|
.map(|result| match result {
|
|
|
|
Ok(val) => val,
|
|
|
|
Err(err) => panic!("{:#}", err),
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
//build dot-graph
|
|
|
|
let nodes = hosts.iter().map(Host::to_dot_node);
|
|
|
|
let edges = hosts.iter().flat_map(Host::to_dot_edge);
|
|
|
|
|
|
|
|
//render it
|
2017-11-17 02:14:12 +01:00
|
|
|
println!("graph antennen {{");
|
2017-11-17 00:18:59 +01:00
|
|
|
for node in nodes {
|
|
|
|
println!("{}", node);
|
|
|
|
}
|
|
|
|
for edge in edges {
|
|
|
|
println!("{}", edge);
|
|
|
|
}
|
|
|
|
println!("}}");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, ErrorChain)]
|
|
|
|
pub enum ParseError {
|
|
|
|
#[error_chain(foreign)]
|
|
|
|
IO(std::io::Error),
|
|
|
|
|
|
|
|
|
|
|
|
#[error_chain(custom)]
|
|
|
|
#[error_chain(description = r#"|_| "File invalid/incomplete yaml""#)]
|
|
|
|
#[error_chain(display = r#"|&(ref path, ref err)| write!(f, "failed at file {:?} with {}", path, err)"#)]
|
|
|
|
Yaml((std::ffi::OsString, serde_yaml::Error)),
|
|
|
|
|
|
|
|
#[error_chain(custom)]
|
|
|
|
#[error_chain(description = r#"|_| "Host file structure broken""#)]
|
2017-11-17 02:14:12 +01:00
|
|
|
#[error_chain(display = r#"|&(ref name, ref err)| write!(f, "the antenna config of {} is wrong: {}", name, err)"#)]
|
|
|
|
Structure((String, String)),
|
2017-11-17 00:18:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct Host {
|
|
|
|
name: String,
|
|
|
|
ip: String,
|
2017-11-17 02:14:12 +01:00
|
|
|
mac: Option<String>,
|
2017-11-17 00:18:59 +01:00
|
|
|
kind: HostKind,
|
|
|
|
}
|
|
|
|
impl Host {
|
|
|
|
fn from_file(path: &Path) -> Result<Self> {
|
|
|
|
RawHost::from_file(path)?.parse()
|
|
|
|
}
|
|
|
|
fn to_dot_node(&self) -> String {
|
2017-11-17 02:14:12 +01:00
|
|
|
let mac = self.mac.clone().unwrap_or("no mac".into());
|
2017-11-17 00:18:59 +01:00
|
|
|
let mut attributes = vec!
|
|
|
|
[ "shape=record".to_string()
|
|
|
|
, format!("label=\"{{\'{name}\'|{typ}|{{{ip}|{mac}}}|{{{ipv6}}}}}\""
|
2017-11-17 02:14:12 +01:00
|
|
|
, name = self.name, typ=self.kind.name(), ip=self.ip
|
|
|
|
, mac=mac, ipv6="no ipv6")
|
2017-11-17 00:18:59 +01:00
|
|
|
, "style=filled".into()
|
|
|
|
];
|
|
|
|
|
|
|
|
// type-specific handling
|
|
|
|
match self.kind {
|
2017-11-17 02:14:12 +01:00
|
|
|
HostKind::Client {coordinates, ref subnet, .. } =>
|
|
|
|
{ attributes.push("fillcolor=lightgray".into())
|
|
|
|
; if let Some(coordinates) = coordinates
|
|
|
|
{ attributes.push(format!("pos=\"{breitengrad},{längengrad}\""
|
|
|
|
, längengrad=coordinates[0], breitengrad=coordinates[1]))
|
|
|
|
}
|
|
|
|
},
|
2017-11-17 00:18:59 +01:00
|
|
|
_ => (),
|
|
|
|
};
|
|
|
|
|
|
|
|
let attributes = attributes.join(", ");
|
|
|
|
|
|
|
|
format!("\"{name}\" [{attributes}]", name=self.name, attributes=attributes)
|
|
|
|
}
|
|
|
|
fn to_dot_edge(&self) -> Option<String> {
|
|
|
|
match self.kind {
|
|
|
|
HostKind::Client { ref parent, .. } =>
|
2017-11-17 02:14:12 +01:00
|
|
|
format!("\"{name}\" -- \"{parent}\"", name=self.name, parent=parent).into(),
|
2017-11-17 00:18:59 +01:00
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
enum HostKind {
|
2017-11-17 02:14:12 +01:00
|
|
|
Client { subnet: String, coordinates: Option<[f64;2]>, parent: String },
|
2017-11-17 00:18:59 +01:00
|
|
|
AccessPoint,
|
|
|
|
Other,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl HostKind {
|
|
|
|
fn name(&self) -> &'static str {
|
|
|
|
use HostKind::*;
|
|
|
|
match *self {
|
|
|
|
Client { .. } => "Client",
|
|
|
|
AccessPoint => "AccessPoint",
|
|
|
|
Other => "Other",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize,Debug)]
|
|
|
|
struct RawHost {
|
|
|
|
#[serde(skip)]
|
|
|
|
name: Option<std::ffi::OsString>,
|
|
|
|
ansible_host: String,
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
kind: String,
|
2017-11-17 02:14:12 +01:00
|
|
|
mac: Option<String>,
|
2017-11-17 00:18:59 +01:00
|
|
|
client_of: Option<String>,
|
|
|
|
coordinate: Option<[f64;2]>,
|
|
|
|
subnet: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RawHost {
|
|
|
|
fn parse(self) -> Result<Host> {
|
2017-11-17 02:14:12 +01:00
|
|
|
let name : String = self.name
|
|
|
|
.ok_or(ParseError::Structure(("???".into(),"got no file name? weird.".into())))?
|
2017-11-17 00:18:59 +01:00
|
|
|
.into_string().unwrap()
|
|
|
|
.split('.')
|
|
|
|
.next().unwrap().into();
|
|
|
|
Ok(Host
|
2017-11-17 02:14:12 +01:00
|
|
|
{ name: name.clone()
|
2017-11-17 00:18:59 +01:00
|
|
|
, ip: self.ansible_host
|
|
|
|
, mac: self.mac
|
|
|
|
, kind: match self.kind.as_ref()
|
|
|
|
{ "wlan-ap" => HostKind::AccessPoint
|
|
|
|
, "client" => HostKind::Client
|
2017-11-17 02:14:12 +01:00
|
|
|
{ subnet: self.subnet.ok_or(ParseError::Structure((name.clone(), "is client, has no subnet".into())))?
|
|
|
|
//, coordinates: self.coordinate.ok_or(ParseError::Structure("is client, has no coordinates".into()))?
|
|
|
|
, coordinates: self.coordinate
|
|
|
|
, parent: self.client_of.ok_or(ParseError::Structure((name.clone(), "is client, has no client_of".into())))?
|
2017-11-17 00:18:59 +01:00
|
|
|
}
|
|
|
|
, _ => HostKind::Other
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
fn from_file(path: &Path) -> Result<Self> {
|
|
|
|
let fd = File::open(path)?;
|
|
|
|
let mut antenna : Self = match serde_yaml::from_reader(&fd) {
|
|
|
|
Ok(val) => val,
|
|
|
|
Err(err) => return Err(ParseError::Yaml((path.as_os_str().to_owned(), err)).into()),
|
|
|
|
};
|
|
|
|
antenna.name = path.file_name().map(|a|a.to_owned());
|
|
|
|
Ok(antenna)
|
|
|
|
}
|
|
|
|
}
|