diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +target diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cbf498 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/debug +/target +Cargo.lock diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..975b4cc --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,23 @@ +include: + - project: 'famedly/infra/templates/ci-cd' + ref: 'rust-v1' + file: '/rust.yml' + - project: 'famedly/infra/templates/ci-cd' + ref: 'docker-v1' + file: '/docker.yml' + +stages: + - test + - build + +cargo-check: + extends: .cargo_check + +cargo-build: + extends: .cargo_build + +docker_releases: + extends: .docker_releases + +docker_tags: + extends: .docker_tags diff --git a/Cargo.toml b/Cargo.toml index abf5540..ba807ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,9 @@ edition = "2021" [dependencies] config = { version = "0.13.1", features = ["yaml"] } serde = { version = "1.0", features = ["derive"] } -prometheus-client = "0.19.0" +metrics = "0.21.0" +metrics-exporter-prometheus = { version = "0.12.1", features = ["http-listener"] } futures-util = "0.3.25" -axum = "0.6.12" tokio = { version = "1.21.2", features = ["rt-multi-thread","macros"] } rsvici = "0.1" anyhow = "1.0.70" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ca61e4a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM registry.gitlab.com/famedly/infra/containers/rust:main as builder +COPY . /app +WORKDIR /app + +RUN cargo build --release + +FROM debian:stable-slim +RUN mkdir -p /opt/openmetrics-vici-exporter +WORKDIR /opt/openmetrics-vici-exporter +COPY --from=builder /app/target/release/openmetrics-vici-exporter /usr/local/bin/openmetrics-vici-exporter +CMD ["/usr/local/bin/openmetrics-vici-exporter"] diff --git a/config.yml b/config.yml index 7c42627..1d34255 100644 --- a/config.yml +++ b/config.yml @@ -1,5 +1,7 @@ --- -vici_socket: "/var/run/charon.vici" -actix_bind_addr: "0.0.0.0" -actix_bind_port: "80" -actix_auth_token: "" +vici: + socket: "/var/run/charon.vici" + interval: 10 +server: + address: "0.0.0.0" + port: 8001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4da34ef --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3' + +services: + ove: + restart: "unless-stopped" + environment: + - VICI_EXPORTER_VICI_SOCKET="/var/run/charon.vici" + - VICI_EXPORTER_VICI_INTERVAL=10 + - VICI_EXPORTER_SERVER_ADDRESS=0.0.0.0 + - VICI_EXPORTER_SERVER_PORT=8001 + volumes: + #- ./config.yml:/opt/openmetrics-vici-exporter/config.yml + - /var/run/charon.vici:/var/run/charon.vici + ports: + - 8111:80/tcp diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..7530651 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 120 diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..cb1f424 --- /dev/null +++ b/shell.nix @@ -0,0 +1,5 @@ +{ pkgs ? import {} }: + pkgs.mkShell { + packages = with pkgs; [ rustc cargo gcc rustfmt clippy ]; + name = "rust-env"; +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..fb80dd3 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,41 @@ +use anyhow::Result; +use config::Config; +use serde::Deserialize; +use std::net::{IpAddr, SocketAddr}; + +#[derive(Debug, Deserialize)] +pub struct WebServerConfig { + pub address: IpAddr, + pub port: u16, +} +impl Into for &WebServerConfig { + fn into(self) -> SocketAddr { + SocketAddr::from((self.address, self.port)) + } +} + +#[derive(Debug, Deserialize)] +pub struct VICIConfig { + pub socket: String, + pub interval: u64, +} + +#[derive(Debug, Deserialize)] +pub struct Configuration { + pub server: WebServerConfig, + pub vici: VICIConfig, +} + +impl Configuration { + pub async fn load() -> Result { + let mut s = Config::builder(); + if std::fs::metadata("config").is_ok(){ + s = s.add_source(config::File::with_name("config")); + } else { println!("config file not found. continuing with env vars... ") }; + + s = s.add_source(config::Environment::with_prefix("VICI_EXPORTER").separator("_")); +// s.build().unwrap(); + let conf: Configuration = s.build().unwrap().try_deserialize().unwrap(); + Ok(conf) + } +} diff --git a/src/main.rs b/src/main.rs index 40a3d41..80a12cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,104 +1,56 @@ -#![allow(dead_code)] - -use axum::{ - response::IntoResponse, - http::{ - StatusCode, - header::{self} - }, - extract::State, - routing::get, - Router, -}; - -use prometheus_client::{ - registry::Registry, - metrics::{ - family::Family, - } -}; - -use std::{ -// collections::HashMap, -// error::Error, - sync::Arc, - net::{IpAddr,SocketAddr}, -// path::Path, -}; - -use serde::Deserialize; - -use config::Config; +use metrics::{ + describe_gauge, + gauge, + describe_counter, + counter, + IntoLabels, + Unit}; +use metrics_exporter_prometheus::PrometheusBuilder; +use tokio::time::{interval, Duration, MissedTickBehavior}; +pub mod config; pub mod vici; -pub mod metrics; - - -#[derive(Debug, Deserialize)] -struct Configuration { - vici_socket: String, - axum_bind_addr: IpAddr, - axum_bind_port: u16, -} - -pub async fn metrics_handler(State(state): State>) -> impl IntoResponse { - let state: Arc = state.clone(); - let mut buffer = String::new(); - prometheus_client::encoding::text::encode(&mut buffer, &state.registry).unwrap(); - ( - StatusCode::OK, - [(header::CONTENT_TYPE, "application/openmetrics-text; version=1.0.0; charset=utf-8")], - buffer, - ) -} - -pub struct AppState { - pub registry: Registry, - pub vici: vici::VICIState, -} - #[tokio::main] async fn main() -> anyhow::Result<()> { - let settings = Config::builder() - .add_source(config::File::with_name("config")) - .add_source(config::Environment::with_prefix("VICI_EXPORTER")) - .build() - .unwrap(); - let mut conf: Configuration = settings.try_deserialize().unwrap(); - let mut vici_client = rsvici::unix::connect(conf.vici_socket).await?; - let mut vici_state: vici::VICIState; + let conf = config::Configuration::load().await?; + let mut vici_client = rsvici::unix::connect(conf.vici.socket).await?; - let metrics = Arc::new(metrics::Metrics { - sa_uptime: Family::default(), - }); + let mut interval = interval(Duration::from_secs(conf.vici.interval)); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); - let mut initial_registery = Registry::default(); + PrometheusBuilder::new() + .with_http_listener(&conf.server) + .install() + .expect("failed to install recorder/exporter"); - initial_registery.register( - "sa_uptime", - "How Long a connection has been established", - metrics.sa_uptime.clone(), - ); + describe_gauge!("sa_uptime", Unit::Seconds, ""); + describe_gauge!("sa_rekey_time", Unit::Seconds, ""); - let mut state = Arc::new( - AppState { - registry: initial_registery, - vici: vici::VICIState::update(&mut vici_client).await?, - }, - ); + describe_counter!("sa_child_bytes_out", Unit::Bytes, ""); + describe_counter!("sa_child_bytes_in", Unit::Bytes, ""); + loop { + let vici_state = vici::VICIState::update(&mut vici_client).await?; + for (sa_name, sa_values) in vici_state.security_associations { + let mut labels = sa_values.into_labels(); + labels.push((&("sa_name", sa_name.clone())).into()); - let addr = SocketAddr::from((conf.axum_bind_addr,conf.axum_bind_port)); - let app = Router::new() - .route("/metrics",get(metrics_handler)) - .with_state(state); - - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await - .unwrap(); - - Ok(()) + gauge!("sa_uptime", sa_values.established as f64, labels.clone()); + gauge!("sa_rekey_time", sa_values.rekey_time as f64, labels.clone()); + //gauge!("sa_state") + for (sa_child_name, sa_child_values) in sa_values.child_security_associations { + let mut child_labels = sa_child_values.into_labels(); + child_labels.push((&("sa_name", sa_name.clone())).into()); + child_labels.push((&("sa_child_name", sa_child_name)).into()); + counter!("sa_child_bytes_in", sa_child_values.bytes_in, child_labels.clone()); + counter!("sa_child_bytes_out", sa_child_values.bytes_out, child_labels.clone()); + counter!("sa_child_packets_in", sa_child_values.packets_in, child_labels.clone()); + counter!("sa_child_packets_out", sa_child_values.packets_out, child_labels.clone()); + + } + } + interval.tick().await; + } } diff --git a/src/metrics.rs b/src/metrics.rs deleted file mode 100644 index 726e7ef..0000000 --- a/src/metrics.rs +++ /dev/null @@ -1,25 +0,0 @@ -use prometheus_client::{ - metrics::{ - family::Family, - gauge::Gauge, - } -}; -use anyhow::Result; - -use crate::vici; -pub mod labels; - -pub struct Metrics { - pub sa_uptime: Family, -} - -impl Metrics { - pub async fn sa_uptime(&self, security_associations: vici::SecurityAssociations) -> Result<()>{ - for named_sa in security_associations.into_iter() { - let label_set = labels::SecurityAssociationLabels::set_from_sa(&mut named_sa).await?; - let (_sa_name, sa_value) = named_sa; - self.sa_uptime.get_or_create(&label_set).set(sa_value.established as i64); - } - Ok(()); - } -} diff --git a/src/metrics/labels.rs b/src/metrics/labels.rs deleted file mode 100644 index a2dd2cb..0000000 --- a/src/metrics/labels.rs +++ /dev/null @@ -1,65 +0,0 @@ -use serde::Deserialize; -use prometheus_client::encoding::{EncodeLabelValue,EncodeLabelSet}; - -use crate::vici; -use anyhow::Result; - - -// I don't really wanna define *all* of this here, it's gonna get really tedious and uncomfortable to maintain. - -/* -#[derive(Debug, Deserialize, Clone, Hash, PartialEq, Eq)] -pub struct SecurityAssociationLabels { - pub uniqueid: String, - pub local_id: String, - pub local_host: String, - pub local_port: u16, - pub remote_id: String, - pub remote_host: String, - pub remote_port: u16, -} -*/ -#[derive(Debug, Deserialize, Clone, Hash, PartialEq, Eq)] -pub struct SecurityAssociationInfo { - pub uniqueid: String, - pub version: u8, - pub local_host: String, - pub local_port: u16, - pub local_id: String, - pub remote_host: String, - pub remote_port: u16, - pub remote_id: String, - pub if_id_in: String, - pub if_id_out: String, - pub encr_alg: String, - pub encr_keysize: String, - pub integ_alg: String, - pub integ_keysize: String, - pub prf_alg: String, - pub dh_group: Option, - pub local_vips: Vec, - pub remote_vips: Vec, -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq, EncodeLabelSet)] -pub struct SecurityAssociationLabels { - pub name: String, - pub uniqueid: String, - pub ike_version: u8, - pub local_id: String, - pub remote_id: String, -} - - -impl SecurityAssociationLabels { - pub async fn set_from_sa(sa: &mut vici::NamedSecurityAssociation) -> Result { - let (sa_name, sa_value) = sa; - Ok(SecurityAssociationLabels { - name: sa_name, - uniqueid: sa_value.uniqueid, - ike_version: sa_value.version, - local_id: sa_value.local_id.unwrap(), - remote_id: sa_value.remote_id.unwrap(), - }) - } -} diff --git a/src/vici.rs b/src/vici.rs index 46b87e0..7f22db4 100644 --- a/src/vici.rs +++ b/src/vici.rs @@ -6,47 +6,57 @@ use std::collections::HashMap; use futures_util::stream::StreamExt; use anyhow::Result; +use metrics::{IntoLabels,Label}; #[derive(Debug, Deserialize)] pub struct VICIState { - pub version: Version, - pub statistics: Statistics, - pub policies: Policies, - pub connections: Connections, - pub security_associations: SecurityAssociations, - pub certificates: Certificates, - pub authorities: Authorities, - pub pools: Pools, + pub version: Version, + pub statistics: Statistics, + pub policies: Policies, + pub connections: Connections, + pub security_associations: SecurityAssociations, + pub certificates: Certificates, + pub authorities: Authorities, + pub pools: Pools, } impl VICIState { pub async fn update(client: &mut rsvici::Client) -> Result { Ok(VICIState { - version: client.request("version", ()).await?, - statistics: client.request("statistics", ()).await?, - policies: collected_stream::(client, "list-policies", "list-policy").await, - connections: collected_stream::(client, "list-connections", "list-conn").await, - security_associations: collected_stream::(client, "list-sas", "list-sa").await, - certificates: collected_stream::(client, "list-certs", "list-cert").await, - authorities: collected_stream::(client, "list-authorities", "list-authority").await, - pools: collected_stream::(client, "list-pools", "list-pool").await, + version: client.request("version", ()).await?, + statistics: client.request("statistics", ()).await?, + policies: collected_stream::(client, "list-policies", "list-policy").await?, + connections: collected_stream::(client, "list-connections", "list-conn") + .await?, + security_associations: collected_stream::( + client, "list-sas", "list-sa", + ) + .await?, + certificates: collected_stream::(client, "list-certs", "list-cert").await?, + authorities: collected_stream::(client, "list-authorities", "list-authority") + .await?, + pools: collected_stream::(client, "list-pools", "list-pool").await?, }) } } -async fn collected_stream(client: &mut rsvici::Client, command: &str, event: &str) -> C +async fn collected_stream(client: &mut rsvici::Client, command: &str, event: &str) -> Result where N: for<'de> serde::Deserialize<'de>, C: std::iter::Extend + Default, { - client.stream_request::<(), N>(command, event, ()).filter_map(|event| async move {event.ok()}).collect::().await + Ok(client + .stream_request::<(), N>(command, event, ()) + .filter_map(|event| async move { event.ok() }) + .collect::() + .await) } // Structs for parsing the control interface #[derive(Debug, Deserialize)] pub struct Version { - pub daemon: String, + pub daemon: String, pub version: String, pub sysname: String, pub release: String, @@ -55,46 +65,46 @@ pub struct Version { #[derive(Debug, Deserialize)] pub struct Statistics { - pub uptime: StatisticsUptime, - pub workers: StatisticsWorkers, - pub queues: StatisticsJobPriorities, - pub scheduled: String, - pub ikesecurity_associations: StatisticsIKESecurityAssociations, - pub plugins: Vec, - pub mem: Option, - pub mallinfo: Option, + pub uptime: StatisticsUptime, + pub workers: StatisticsWorkers, + pub queues: StatisticsJobPriorities, + pub scheduled: String, + pub ikesas: StatisticsIKESecurityAssociations, + pub plugins: Vec, + pub mem: Option, + pub mallinfo: Option, } #[derive(Debug, Deserialize)] pub struct StatisticsUptime { pub running: String, - pub since: String, + pub since: String, } #[derive(Debug, Deserialize)] pub struct StatisticsWorkers { - pub total: String, - pub idle: String, + pub total: String, + pub idle: String, pub active: StatisticsJobPriorities, } #[derive(Debug, Deserialize)] pub struct StatisticsJobPriorities { pub critical: String, - pub high: String, - pub medium: String, - pub low: String, + pub high: String, + pub medium: String, + pub low: String, } #[derive(Debug, Deserialize)] pub struct StatisticsIKESecurityAssociations { - pub total: String, + pub total: String, pub half_open: Option, } #[derive(Debug, Deserialize)] pub struct StatisticsMem { - pub total: String, + pub total: String, pub allocs: String, } #[derive(Debug, Deserialize)] @@ -111,19 +121,20 @@ pub type NamedPolicy = (String, Policy); #[derive(Debug, Deserialize)] pub struct Policy { - pub child: String, - pub ike: Option, - pub mode: PolicyMode, - pub local_ts: Option>, + pub child: String, + pub ike: Option, + pub mode: PolicyMode, + pub local_ts: Option>, pub remote_ts: Option>, } #[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum PolicyMode { - tunnel, - transport, - pass, - drop, + Tunnel, + Transport, + Pass, + Drop, } pub type Connections = HashMap; @@ -132,45 +143,46 @@ pub type NamedConnection = (String, Conn); #[derive(Debug, Deserialize)] pub struct Conn { - pub local_addrs: Vec, + pub local_addrs: Vec, pub remote_addrs: Vec, - pub version: String, - pub reauth_time: u32, - pub rekey_time: u32, - pub children: HashMap, + pub version: String, + pub reauth_time: u32, + pub rekey_time: u32, + pub children: HashMap, } #[derive(Debug, Deserialize)] pub struct ConnAuthSection { - pub class: String, - pub eap_type: Option, - pub eap_vendor: Option, - pub xauth: Option, - pub revocation: Option, - pub id: String, - pub aaa_id: Option, - pub eap_id: Option, - pub xauth_id: Option, - pub groups: Option>, + pub class: String, + pub eap_type: Option, + pub eap_vendor: Option, + pub xauth: Option, + pub revocation: Option, + pub id: String, + pub aaa_id: Option, + pub eap_id: Option, + pub xauth_id: Option, + pub groups: Option>, pub certificates: Option>, - pub cacerts: Option>, + pub cacerts: Option>, } #[derive(Debug, Deserialize)] pub struct ConnChildSection { - pub mode: ChildSecurityAssociationMode, - pub rekey_time: u32, - pub rekey_bytes: u64, + pub mode: ChildSecurityAssociationMode, + pub rekey_time: u32, + pub rekey_bytes: u64, pub rekey_packets: u64, - pub local_ts: Option>, - pub remote_ts: Option>, + pub local_ts: Option>, + pub remote_ts: Option>, } #[derive(Debug, Deserialize, Clone, Hash, PartialEq, Eq)] +#[serde(rename_all = "UPPERCASE")] pub enum ChildSecurityAssociationMode { - TUNNEL, - TRANSPORT, - BEET, + Tunnel, + Transport, + Beet, } #[derive(Debug, Deserialize, Clone, Hash, PartialEq, Eq)] pub enum ChildSecurityAssociationProtocol { @@ -182,82 +194,103 @@ pub type SecurityAssociations = HashMap; pub type NamedSecurityAssociation = (String, SecurityAssociation); -#[derive(Debug, Deserialize)] -pub struct SecurityAssociation { - pub uniqueid: String, - pub version: u8, - pub state: String, - pub local_host: Option, - pub local_port: Option, - pub local_id: Option, - pub remote_host: Option, - pub remote_port: Option, - pub remote_id: Option, - pub remote_xauth_id: Option, - pub remote_epa_id: Option, - pub initiator: Option, - pub initiator_spi: Option, - pub responder_spi: Option, - pub nat_local: Option, - pub nat_remote: Option, - pub nat_fake: Option, - pub nat_any: Option, - pub if_id_in: Option, - pub if_id_out: Option, - pub encr_alg: Option, - pub encr_keysize: Option, - pub integ_alg: Option, - pub integ_keysize: Option, - pub prf_alg: Option, - pub dh_group: Option, - pub established: u64, - pub rekey_time: Option, - pub reauth_time: Option, - pub local_vips: Option>, - pub remote_vips: Option>, - pub tasks_queued: Option>, - pub tasks_active: Option>, - pub tasks_passive: Option>, - pub child_security_associations: Option>, +impl IntoLabels for &SecurityAssociation { + fn into_labels(self) -> Vec