From e27202d63bb0ad2fc3f6c6374b1504558e0678a6 Mon Sep 17 00:00:00 2001 From: clerie Date: Tue, 9 May 2023 08:40:39 +0200 Subject: [PATCH] Split in multiple binaries --- Cargo.toml | 2 +- src/bin/nixos-validator.rs | 162 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 164 ++----------------------------------- src/nixos.rs | 40 +++++++++ 5 files changed, 209 insertions(+), 160 deletions(-) create mode 100644 src/bin/nixos-validator.rs create mode 100644 src/lib.rs create mode 100644 src/nixos.rs diff --git a/Cargo.toml b/Cargo.toml index 9299d4e..e78f8de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nixos-exporter" -version = "0.5.0" +version = "0.6.0" edition = "2021" [dependencies] diff --git a/src/bin/nixos-validator.rs b/src/bin/nixos-validator.rs new file mode 100644 index 0000000..ae3435e --- /dev/null +++ b/src/bin/nixos-validator.rs @@ -0,0 +1,162 @@ +use axum::{ + extract::{State, Query}, + http::StatusCode, + response::IntoResponse, + routing::get, + Router, +}; + +use std::collections::HashMap; +use std::net::SocketAddr; +use std::str::FromStr; + +use nixos_exporter::nixos::NixStorePath; + +#[derive(Clone)] +struct AppState { + listen: String, + prometheus_url: String, + prometheus_query_tag_template: String, + hydra_url: String, + hydra_job_template: String, +} + +impl AppState{ + pub fn new() -> Self { + Self { + listen: String::from("[::]:9152"), + prometheus_url: String::from(""), + prometheus_query_tag_template: String::from("instance=\"{}\""), + hydra_url: String::from(""), + hydra_job_template: String::from("nixfiles/nixfiles/nixosConfigurations.{}"), + } + } + + pub fn is_valid(self) -> bool { + let mut valid = true; + if self.prometheus_url == String::from("") { + println!("Prometheus url is not specified"); + valid = false; + } + if self.hydra_url == String::from("") { + println!("Hydra url is not specified"); + valid = false; + } + + return valid; + } +} + +#[tokio::main] +async fn main() { + let mut app_state = AppState::new(); + + let mut args = std::env::args(); + let _name = args.next().unwrap(); + loop { + let arg = if let Some(arg) = args.next() { + arg + } else { + break; + }; + + match arg.as_str() { + "--help" | "-h" => { + println!("Prometheus exporter for NixOS systems"); + println!("Use --listen bind the web service."); + println!("Output will be on /metrics endpoint. HTTP 500 if something broke while scraping."); + std::process::exit(0); + } + "--listen" => { + app_state.listen = args.next().unwrap(); + } + "--prometheus-url" => { + app_state.prometheus_url = args.next().unwrap(); + } + "--prometheus-query-tag-template" => { + app_state.prometheus_query_tag_template = args.next().unwrap(); + } + "--hydra-url" => { + app_state.hydra_url = args.next().unwrap(); + } + "--hydra-job-template" => { + app_state.hydra_job_template = args.next().unwrap(); + } + unknown => { + println!("unknown option: {}", unknown); + std::process::exit(1) + } + } + } + + if !app_state.clone().is_valid() { + std::process::exit(1); + } + + let app = Router::new(); + let app = app.route("/metrics", get(check)); + let app = app.with_state(app_state.clone()); + + let addr = SocketAddr::from_str(&app_state.listen.clone()).unwrap(); + println!("listening on http://{}", addr); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +async fn check(State(app_state): State, Query(params): Query>) -> Result<(StatusCode, impl IntoResponse), (StatusCode, impl IntoResponse)> { + let target = params.get("target") + .ok_or_else(|| (StatusCode::NOT_FOUND, "specify target"))?; + + if target.contains("\"") { + return Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid target name")); + } + + let client = reqwest::Client::new(); + + let prometheus_req = client.get(format!("{}/api/v1/query?query=nixos_current_system_hash{{{}}}", app_state.prometheus_url, app_state.prometheus_query_tag_template.clone().replace("{}", target))) + .header("Accept", "application/json") + .send().await + .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Promehteus can't get reached"))?; + + if prometheus_req.status() != reqwest::StatusCode::OK { + return Err((StatusCode::NOT_FOUND, "Target does not exist in Hydra")); + } + + let prometheus_body = prometheus_req.json::().await + .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid response from Prometheus"))?; + + let current_system_hash = prometheus_body["data"]["result"][0]["metric"]["hash"].as_str() + .ok_or_else(|| (StatusCode::INTERNAL_SERVER_ERROR, "No current metric found in Prometheus"))?; + + let hydra_req = client.get(format!("{}/job/{}/latest", app_state.hydra_url, app_state.hydra_job_template.clone().replace("{}", target))) + .header("Accept", "application/json") + .send().await + .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Hydra can't get reached"))?; + + if hydra_req.status() != reqwest::StatusCode::OK { + return Err((StatusCode::NOT_FOUND, "Target does not exist in Hydra")); + } + + let hydra_body = hydra_req.json::().await + .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid response from Hydra"))?; + + let nix_store_path = hydra_body["buildoutputs"]["out"]["path"].as_str() + .ok_or_else(|| (StatusCode::INTERNAL_SERVER_ERROR, "No buildoutput found in Hydra"))?; + + let hydra_system_hash = NixStorePath::from_path_buf(std::path::PathBuf::from(nix_store_path)) + .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid store path returned by Hydra"))? + .hash; + + let mut status = "0"; + + if current_system_hash == hydra_system_hash { + status = "1"; + } + + return Ok(( + StatusCode::OK, + format!("nixos_current_system_is_sync{{}} {}\n", status) + )); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4c990e7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod nixos; diff --git a/src/main.rs b/src/main.rs index c5cab96..1eb9791 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use axum::{ - extract::{State, Query}, http::StatusCode, response::IntoResponse, routing::get, @@ -9,92 +8,23 @@ use axum::{ use std::collections::HashMap; use std::net::SocketAddr; use std::str::FromStr; -use std::path::PathBuf; -#[derive(Clone, PartialEq)] -enum OperationMode { - None, - Exporter, - Validator, -} +use nixos_exporter::nixos::NixStorePath; #[derive(Clone)] struct AppState { listen: String, - operationmode: OperationMode, - prometheus_url: String, - prometheus_query_tag_template: String, - hydra_url: String, - hydra_job_template: String, } impl AppState{ pub fn new() -> Self { Self { listen: String::from("[::]:9152"), - operationmode: OperationMode::None, - prometheus_url: String::from(""), - prometheus_query_tag_template: String::from("instance=\"{}\""), - hydra_url: String::from(""), - hydra_job_template: String::from("nixfiles/nixfiles/nixosConfigurations.{}"), } } pub fn is_valid(self) -> bool { - let mut valid = true; - if self.operationmode == OperationMode::None { - println!("operationmode is not set"); - valid = false; - } - if self.prometheus_url == String::from("") && self.operationmode == OperationMode::Validator { - println!("Prometheus url is not specified"); - valid = false; - } - if self.hydra_url == String::from("") && self.operationmode == OperationMode::Validator { - println!("Hydra url is not specified"); - valid = false; - } - - return valid; - } -} - -#[derive(Clone)] -struct NixStorePath { - hash: String, - name: String, -} - -impl NixStorePath { - pub fn from_str_symlink(path: &str) -> Result { - Ok(Self::from_path_buf_symlink(PathBuf::from(path))?) - } - - pub fn from_path_buf_symlink(path: PathBuf) -> Result { - Ok(Self::from_path_buf(path.read_link().map_err(|err| err.to_string())?)?) - } - - pub fn from_path_buf(path: PathBuf) -> Result { - let store_path_name = path.iter().nth(3) - .ok_or_else(|| String::from("Can't read store path name"))? - .to_str() - .ok_or_else(|| String::from("Failed converting store path name to string"))? - .to_string(); - Ok(Self::from_store_path_name(store_path_name)?) - } - - pub fn from_store_path_name(store_path_name: String) -> Result { - let (hash, name) = store_path_name - .split_once("-") - .ok_or_else(|| String::from("Failed splitting store path name for hash and name"))?; - Ok(Self { - hash: hash.to_string(), - name: name.to_string(), - }) - } - - pub fn to_prometheus_metric(self, infix: String) -> Result { - return Ok(format!("nixos_{}_hash{{hash=\"{}\"}} 1\nnixos_{}_name{{name=\"{}\"}} 1\n", infix, self.hash, infix, self.name)); + return true; } } @@ -103,7 +33,7 @@ async fn main() { let mut app_state = AppState::new(); let mut args = std::env::args(); - let name = args.next().unwrap(); + let _name = args.next().unwrap(); loop { let arg = if let Some(arg) = args.next() { arg @@ -121,24 +51,6 @@ async fn main() { "--listen" => { app_state.listen = args.next().unwrap(); } - "--prometheus-url" => { - app_state.prometheus_url = args.next().unwrap(); - } - "--prometheus-query-tag-template" => { - app_state.prometheus_query_tag_template = args.next().unwrap(); - } - "--hydra-url" => { - app_state.hydra_url = args.next().unwrap(); - } - "--hydra-job-template" => { - app_state.hydra_job_template = args.next().unwrap(); - } - "exporter" => { - app_state.operationmode = OperationMode::Exporter; - } - "validator" => { - app_state.operationmode = OperationMode::Validator; - } unknown => { println!("unknown option: {}", unknown); std::process::exit(1) @@ -150,18 +62,8 @@ async fn main() { std::process::exit(1); } - let mut app = Router::new(); - if app_state.operationmode == OperationMode::Exporter { - println!("Running NixOS Exporter in Exporter mode"); - app = app.route("/metrics", get(metrics)); - } else if app_state.operationmode == OperationMode::Validator { - println!("Running NixOS Exporter in Validator mode"); - app = app.route("/metrics", get(check)); - } else { - println!("Run mode not specified, do {} --help", name); - std::process::exit(1); - }; - + let app = Router::new(); + let app = app.route("/metrics", get(metrics)); let app = app.with_state(app_state.clone()); let addr = SocketAddr::from_str(&app_state.listen.clone()).unwrap(); @@ -201,59 +103,3 @@ async fn metrics() -> Result<(StatusCode, impl IntoResponse), (StatusCode, impl out, )); } - -async fn check(State(app_state): State, Query(params): Query>) -> Result<(StatusCode, impl IntoResponse), (StatusCode, impl IntoResponse)> { - let target = params.get("target") - .ok_or_else(|| (StatusCode::NOT_FOUND, "specify target"))?; - - if target.contains("\"") { - return Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid target name")); - } - - let client = reqwest::Client::new(); - - let prometheus_req = client.get(format!("{}/api/v1/query?query=nixos_current_system_hash{{{}}}", app_state.prometheus_url, app_state.prometheus_query_tag_template.clone().replace("{}", target))) - .header("Accept", "application/json") - .send().await - .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Promehteus can't get reached"))?; - - if prometheus_req.status() != reqwest::StatusCode::OK { - return Err((StatusCode::NOT_FOUND, "Target does not exist in Hydra")); - } - - let prometheus_body = prometheus_req.json::().await - .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid response from Hydra"))?; - - let current_system_hash = prometheus_body["data"]["result"][0]["metric"]["hash"].as_str() - .ok_or_else(|| (StatusCode::INTERNAL_SERVER_ERROR, "No buildoutput found in Hydra"))?; - - let hydra_req = client.get(format!("{}/job/{}/latest", app_state.hydra_url, app_state.hydra_job_template.clone().replace("{}", target))) - .header("Accept", "application/json") - .send().await - .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Hydra can't get reached"))?; - - if hydra_req.status() != reqwest::StatusCode::OK { - return Err((StatusCode::NOT_FOUND, "Target does not exist in Hydra")); - } - - let hydra_body = hydra_req.json::().await - .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid response from Hydra"))?; - - let nix_store_path = hydra_body["buildoutputs"]["out"]["path"].as_str() - .ok_or_else(|| (StatusCode::INTERNAL_SERVER_ERROR, "No buildoutput found in Hydra"))?; - - let hydra_system_hash = NixStorePath::from_path_buf(std::path::PathBuf::from(nix_store_path)) - .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid store path returned by Hydra"))? - .hash; - - let mut status = "0"; - - if current_system_hash == hydra_system_hash { - status = "1"; - } - - return Ok(( - StatusCode::OK, - format!("nixos_current_system_is_sync{{}} {}\n", status) - )); -} diff --git a/src/nixos.rs b/src/nixos.rs new file mode 100644 index 0000000..107e0b1 --- /dev/null +++ b/src/nixos.rs @@ -0,0 +1,40 @@ +use std::path::PathBuf; + +#[derive(Clone)] +pub struct NixStorePath { + pub hash: String, + pub name: String, +} + +impl NixStorePath { + pub fn from_str_symlink(path: &str) -> Result { + Ok(Self::from_path_buf_symlink(PathBuf::from(path))?) + } + + pub fn from_path_buf_symlink(path: PathBuf) -> Result { + Ok(Self::from_path_buf(path.read_link().map_err(|err| err.to_string())?)?) + } + + pub fn from_path_buf(path: PathBuf) -> Result { + let store_path_name = path.iter().nth(3) + .ok_or_else(|| String::from("Can't read store path name"))? + .to_str() + .ok_or_else(|| String::from("Failed converting store path name to string"))? + .to_string(); + Ok(Self::from_store_path_name(store_path_name)?) + } + + pub fn from_store_path_name(store_path_name: String) -> Result { + let (hash, name) = store_path_name + .split_once("-") + .ok_or_else(|| String::from("Failed splitting store path name for hash and name"))?; + Ok(Self { + hash: hash.to_string(), + name: name.to_string(), + }) + } + + pub fn to_prometheus_metric(self, infix: String) -> Result { + return Ok(format!("nixos_{}_hash{{hash=\"{}\"}} 1\nnixos_{}_name{{name=\"{}\"}} 1\n", infix, self.hash, infix, self.name)); + } +}