devguard-genesis 0.1.4

devguard generic iot configuration service
use super::genesis;
use std::io::{Write, Read};
use std::io::{Error,ErrorKind};
use std::collections::HashMap;
use ipnet::IpNet;
use std::process::Command;
use handlebars::Handlebars;

#[derive(Default)]
struct Interface {
    ifname:     Vec<String>,
    dhcp:       bool,
    typ:        Option<String>,
    ipaddr:     Option<(String, String)>,
}

pub struct Device {
}

pub struct Emitter{
    devices:        HashMap<String, Device>,
    interfaces:     HashMap<String, Interface>,
    out_wireless:   Vec<u8>,
    out_network:    Vec<u8>,
    tplout:         HashMap<String, String>,
}

impl Emitter {
    pub fn new() -> Self {
        Self {
            devices:        HashMap::new(),
            interfaces:     Default::default(),
            out_wireless:   Vec::new(),
            out_network:    Vec::new(),
            tplout:         HashMap::new(),
        }
    }


    pub fn load(&mut self, config: &genesis::Genesis) -> Result<(), Error> {
        for (name, device ) in &config.device {
            self.device(name, &device)?;
        }

        for (name, interface) in &config.interface {
            self.interface(name, &interface)?;
        }


        for (name, template) in &config.template {
            self.template(name, &template)?;
        }

        write!(&mut self.out_network, "
config interface   'loopback'
    option ifname  'lo'
    option proto   'static'
    option ipaddr  '127.0.0.1'
    option netmask '255.0.0.0'
\n")?;

        //emit
        for (k,v) in &self.interfaces {
            write!(&mut self.out_network, "config interface   '{}'\n", k)?;
            if let Some(typ) = &v.typ{
                write!(&mut self.out_network,"    option type    '{}'\n", typ)?;
            }
            if v.ifname.len() > 0 {
                write!(&mut self.out_network,"    option ifname  '{}'\n", v.ifname.join(" "))?;
            }

            if v.dhcp {
                write!(&mut self.out_network,"    option proto   'dhcp'\n")?;
            } else if let Some((ipaddr, mask)) = &v.ipaddr {
                write!(&mut self.out_network,"    option proto   'static'\n")?;
                write!(&mut self.out_network,"    option ipaddr  '{}'\n",  ipaddr)?;
                write!(&mut self.out_network,"    option netmask '{}'\n", mask)?;
            }
            write!(&mut self.out_network, "\n")?;
        }


        Ok(())
    }

    fn device(&mut self, name: &str, device: &genesis::Device) -> Result<(),Error> {
        self.devices.insert(name.to_string(), Device{
        });
        match device.class.as_str() {
            "wifi" => {

                let mut path = match &device.path {
                    Some(v) => v.clone(),
                    None => return Err(Error::new(ErrorKind::Other, format!("wifi device must be matched by path '{}'", name))),
                };

                if !path.starts_with("/sys/device") {
                    let path2 = std::path::Path::new(&path);
                    if let Ok(v) = std::fs::read_link(&path2.join("device")).and_then(|v|std::path::Path::new(&path).join(v).canonicalize()) {
                        path = v.clone().to_string_lossy().to_string();
                    }
                }

                if !path.starts_with("/sys/devices/") {
                    return Err(Error::new(ErrorKind::Other, format!("path {} did not resolve to a sysfs device", path)));
                }
                path = path[13..].to_string();


                write!(&mut self.out_wireless , "
config wifi-device '{}'
    option type     'mac80211'
    option path     '{}'
", name, &path)?;

                let w = match device.wifi.as_ref() {
                    Some(v) => v,
                    None => {
                        return Err(Error::new(ErrorKind::Other, format!("missing [device.{}.wifi] section", name)));
                    }
                };

                match &w.channel {
                    Some(toml::value::Value::String(v)) =>  {
                        write!(&mut self.out_wireless , "    option channel  '{}'\n", v)?;
                    }
                    Some(toml::value::Value::Integer(v)) =>  {
                        write!(&mut self.out_wireless , "    option channel  '{}'\n", v)?;
                    },
                    _ => ()
                };

                if let Some(v) = &w.htmode {
                    write!(&mut self.out_wireless , "    option htmode  '{}'\n", v)?;
                }

            },
            a => {
                return Err(Error::new(ErrorKind::Other, format!("invalid class '{}'", a)));
            }
        }
        Ok(())
    }

    fn interface(&mut self, name: &str, interface: &genesis::Interface) -> Result<(),Error> {
        if name.chars().any(|x| !x.is_alphanumeric()) {
            return Err(Error::new(ErrorKind::Other, format!("invalid interface name {}", name)));
        }

        match interface.class.as_ref().map(|x|x.as_ref()) {
            None => {
                return self.create_interface(name, interface);
            },
            Some("bridge") => {
                self.create_interface(name.clone(),interface)?;
                self.interfaces.get_mut(name).unwrap().typ = Some("bridge".to_string());
            },
            Some("wifi") => {
                return self.wifi(name, interface);
            },
            Some("wireguard") => {
                self.wireguard(name, interface)?;
            },
            Some(a) => {
                return Err(Error::new(ErrorKind::Other, format!("invalid class '{}'", a)));
            }
        }
        Ok(())
    }

    fn wifi(&mut self, name: &str, interface: &genesis::Interface) -> Result<(),Error> {

        let device_name = match &interface.device {
            None => return Err(Error::new(ErrorKind::Other, format!("wifi interface missing device {}'", name))),
            Some(v) => match self.devices.get(v) {
                None => return Err(Error::new(ErrorKind::Other, format!("undeclared interface '{}' used in device {}'", v, name))),
                Some(_) => v.to_string(),
            }
        };

        let w = match interface.wifi.as_ref() {
            Some(v) => v,
            None => {
                return Err(Error::new(ErrorKind::Other, format!("missing [interface.{}.wifi] section", name)));
            }
        };

        let mode = match w.mode {
            genesis::WifiMode::Sta      => "sta",
            genesis::WifiMode::Ap       => "ap",
            genesis::WifiMode::Monitor  => "monitor",
        };

        write!(&mut self.out_wireless , "
config wifi-iface   '{}'
    option device   '{}'
    option mode     '{}'
    option ifname   '{}'
", name, device_name, mode, name)?;

        if let Some(br) = &interface.bridge {
            write!(&mut self.out_wireless , "    option network  '{}'\n", br)?;
        };

        if let Some(v) = &w.ssid {
            write!(&mut self.out_wireless , "    option ssid     '{}'\n", v)?;

            match &w.auth {
                Some(genesis::WifiAuth::Psk2) => {
                    write!(&mut self.out_wireless , "    option encryption 'psk2' \n")?;
                },
                Some(genesis::WifiAuth::None) | None  => {
                    write!(&mut self.out_wireless , "    option encryption 'none' \n")?;
                }
            };
        };

        if let Some(v) = &w.key {
            write!(&mut self.out_wireless , "    option key       '{}'\n", v)?;
        };

        Ok(())
    }

    fn create_interface(&mut self, name: &str, interface: &genesis::Interface) -> Result<(),Error> {

        if let Some(br) = &interface.bridge {
            let i = self.interfaces.entry(br.to_string()).or_default();
            if let Some(ifname) = &interface.device {
                i.ifname.push(ifname.to_string());
            }
            return Ok(());
        }

        let i = self.interfaces.entry(name.to_string()).or_default();
        if let Some(ifname) = &interface.device {
            i.ifname.push(ifname.to_string());
        }

        if let Some(ipaddr) = &interface.ipaddr {
            if let Some(addr) = ipaddr.first() {
                let ip : IpNet = addr.parse().map_err(super::map_err)?;
                i.ipaddr = Some((format!("{}", ip.addr()), format!("{}", ip.netmask())));
            }
        }

        match &interface.dhcp {
            Some(toml::value::Value::String(s)) =>  {
                match s.as_str() {
                    "yes" | "true" | "ipv4" => {
                        i.dhcp = true;
                    },
                    "false" | "no" => {
                        i.dhcp = false;
                    },
                    _ => (),
                }
            },
            Some(toml::value::Value::Boolean(v)) => {
                if *v {
                    i.dhcp = true;
                } else {
                    i.dhcp = true;
                }
            },
            _ => (),
        }
        Ok(())
    }


    fn wireguard(&mut self, name: &str, interface: &genesis::Interface) -> Result<(),Error> {
        let wg = match interface.wireguard.as_ref() {
            Some(v) => v,
            None => {
                return Err(Error::new(ErrorKind::Other, format!("missing [interface.{}.wireguard] section", name)));
            }
        };
        write!(&mut self.out_network, "
config interface '{}'
    option proto 'wireguard'
    option private_key '{}'
    option disabled '0'
", name, wg.private_key)?;

        if let Some(ipaddr) = &interface.ipaddr {
            for addr in ipaddr {
                write!(&mut self.out_network, "    list addresses '{}'\n", addr)?;
            }
        }

        let peer = match &wg.peers {
            Some(v) if v.len() == 1 => v.first().unwrap(),
            _ => {
                return Err(Error::new(ErrorKind::Other, format!("need exactly one [interface.{}.wireguard.peers] section", name)));
            }
        };


        write!(&mut self.out_network, "
config wireguard_{}
    option public_key '{}'
    option route_allowed_ips '{}'
", name, peer.public_key, if peer.autoroute.clone().unwrap_or_default() {"1"} else {"0"})?;

        if let Some(psk) = &peer.psk {
            write!(&mut self.out_network, "    option preshared_key '{}'\n", psk)?;
        }

        let endpoint : Vec<&str> = peer.endpoint.split(":").collect();
        if endpoint.len() != 2 {
            return Err(Error::new(ErrorKind::Other, format!("invalid wg endpoint: {}", peer.endpoint)));
        }
        match endpoint[1].parse::<u16>() {
            Err(e) => {
                return Err(Error::new(ErrorKind::Other, format!("invalid wg endpoint port: {}: {}", endpoint[1], e)));
            }
            Ok(v) => {
                write!(&mut self.out_network, "    option endpoint_port '{}'\n", v)?;
            },
        };
        write!(&mut self.out_network, "    option endpoint_host '{}'\n", endpoint[0])?;


        if let Some(v) = &peer.keepalive {
            write!(&mut self.out_network, "    option persistent_keepalive '{}'\n", v)?;
        }

        for route in &peer.routes {
            write!(&mut self.out_network, "    list allowed_ips '{}'\n", route)?;
        }

        Ok(())
    }

    fn template(&mut self, _name: &str, template: &genesis::Template) -> Result<(),Error> {
        let p = std::path::Path::new("/etc/devguard/genesis/templates/").join(&template.template);
        let mut f = std::fs::File::open(p)?;
        let mut s = String::new();
        f.read_to_string(&mut s)?;

        let t = Handlebars::new().render_template(&s, &template.vars).map_err(super::map_err)?;

        self.tplout.insert(template.output.to_string(), t);

        Ok(())
    }

    pub fn commit(&self) ->  Result<(), Error> {

        let mut f = std::fs::File::create("/etc/config/wireless")?;
        f.write_all(&self.out_wireless)?;

        let mut f = std::fs::File::create("/etc/config/network")?;
        f.write_all(&self.out_network)?;


        for (path, s) in &self.tplout {
            if let Err(err) = std::fs::File::create(path).and_then(|mut f|f.write_all(s.as_bytes())) {
                log::warn!("{}: {}", path, err);
            }
        }

        Command::new("/etc/devguard/genesis/post")
            .spawn().ok();

        Ok(())
    }



}