From a05075bd9dc8e5ac754e875071f6ed867dfedc6c Mon Sep 17 00:00:00 2001 From: Evan Cameron Date: Thu, 9 May 2024 22:43:25 -0400 Subject: [PATCH 1/4] ext_api: add leases endpoint --- Cargo.lock | 17 +++--- bin/src/main.rs | 6 +- external-api/Cargo.toml | 1 + external-api/src/lib.rs | 95 ++++++++++++++++++++++++------- libs/config/sample/config_v4.json | 52 +++++++++++++++++ libs/config/src/lib.rs | 2 +- libs/config/src/v4.rs | 18 +++++- libs/config/src/v6.rs | 1 - 8 files changed, 158 insertions(+), 34 deletions(-) create mode 100644 libs/config/sample/config_v4.json diff --git a/Cargo.lock b/Cargo.lock index fe120cc..5f810b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,9 +307,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bit-set" @@ -1067,6 +1067,7 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "config", "dora-core", "ip-manager", "parking_lot 0.12.1", @@ -2876,7 +2877,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "bytes", "futures-core", "futures-util", @@ -3026,21 +3027,21 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring 0.17.8", "rustls-pki-types", diff --git a/bin/src/main.rs b/bin/src/main.rs index d821344..caa8932 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs @@ -72,7 +72,11 @@ async fn start(config: cli::Config) -> Result<()> { debug!("starting database"); let ip_mgr = Arc::new(IpManager::new(SqliteDb::new(database_url).await?)?); // start external api for healthchecks - let api = ExternalApi::new(config.external_api, Arc::clone(&ip_mgr)); + let api = ExternalApi::new( + config.external_api, + Arc::clone(&dhcp_cfg), + Arc::clone(&ip_mgr), + ); // start v4 server debug!("starting v4 server"); let mut v4: Server = diff --git a/external-api/Cargo.toml b/external-api/Cargo.toml index e169f36..7c0e87e 100644 --- a/external-api/Cargo.toml +++ b/external-api/Cargo.toml @@ -10,6 +10,7 @@ license = "MPL-2.0" [dependencies] dora-core = { path = "../dora-core" } ip-manager = { path = "../libs/ip-manager" } +config = { path = "../libs/config" } # libs anyhow = { workspace = true } diff --git a/external-api/src/lib.rs b/external-api/src/lib.rs index ea2a65e..c0b0b9e 100644 --- a/external-api/src/lib.rs +++ b/external-api/src/lib.rs @@ -16,15 +16,16 @@ #![deny(rustdoc::broken_intra_doc_links)] #![allow(clippy::cognitive_complexity, clippy::too_many_arguments)] +use std::{net::SocketAddr, sync::Arc}; + use anyhow::{bail, Result}; use axum::{extract::Extension, routing, Router}; use ip_manager::{IpManager, Storage}; use tokio::{net::TcpListener, sync::mpsc, task::JoinHandle}; use tracing::{error, info, trace}; -use std::{net::SocketAddr, sync::Arc}; - pub use crate::models::{Health, State}; +use config::DhcpConfig; /// The task runner for the [`ExternalApi`] /// @@ -50,11 +51,12 @@ pub struct ExternalApi { addr: SocketAddr, state: State, ip_mgr: Arc>, + cfg: Arc, } impl ExternalApi { /// Create a new ExternalApi instance - pub fn new(addr: SocketAddr, ip_mgr: Arc>) -> Self { + pub fn new(addr: SocketAddr, cfg: Arc, ip_mgr: Arc>) -> Self { trace!("starting external api"); let (tx, rx) = mpsc::channel(10); let state = models::blank_health(); @@ -64,6 +66,7 @@ impl ExternalApi { addr, state, ip_mgr, + cfg, } } @@ -90,20 +93,28 @@ impl ExternalApi { } /// serve the HTTP external api - async fn run(addr: SocketAddr, state: State, ip_mgr: Arc>) -> Result<()> { + async fn run( + addr: SocketAddr, + state: State, + cfg: Arc, + ip_mgr: Arc>, + ) -> Result<()> { let tcp = TcpListener::bind(&addr).await?; // Provides: // /health // /ping // /metrics // /metrics-text + // /leases let app = Router::new() .route("/health", routing::get(handlers::ok)) .route("/ping", routing::get(handlers::ping)) .route("/metrics", routing::get(handlers::metrics)) .route("/metrics-text", routing::get(handlers::metrics_text)) + .route("/leases", routing::get(handlers::leases::)) .layer(Extension(state)) - .layer(Extension(ip_mgr)); + .layer(Extension(ip_mgr)) + .layer(Extension(cfg)); tracing::debug!("external API listening on {}", addr); @@ -117,12 +128,14 @@ impl ExternalApi { let state = self.state.clone(); let addr = self.addr; let ip_mgr = self.ip_mgr.clone(); + let cfg = self.cfg.clone(); // if tx is not cloned, health listen will never update since ExternalApi is owner tokio::spawn(async move { - if let Err(err) = - tokio::try_join!(ExternalApi::run(addr, state, ip_mgr), self.listen_status()) - { + if let Err(err) = tokio::try_join!( + ExternalApi::run(addr, state, cfg, ip_mgr), + self.listen_status() + ) { error!(?err, "health task returning, this should not happen") } }) @@ -138,7 +151,8 @@ impl ExternalApi { mod handlers { - use crate::models::{Health, State}; + use std::sync::Arc; + use axum::{ body::Body, extract::Extension, @@ -146,20 +160,30 @@ mod handlers { http::{Response, StatusCode}, response::IntoResponse, }; + use config::DhcpConfig; use dora_core::metrics::{START_TIME, UPTIME}; + use ip_manager::{IpManager, Storage}; use prometheus::{Encoder, ProtobufEncoder, TextEncoder}; use tracing::error; - pub(crate) async fn ok( - Extension(state): Extension, - ) -> Result { + use crate::models::{Health, ServerResult, State}; + + pub(crate) async fn ok(Extension(state): Extension) -> ServerResult { Ok(match *state.lock() { Health::Good => StatusCode::OK, Health::Bad => StatusCode::INTERNAL_SERVER_ERROR, }) } - pub(crate) async fn metrics() -> Result { + pub(crate) async fn leases( + Extension(cfg): Extension>, + Extension(ip_mgr): Extension>>, + ) -> ServerResult { + todo!(); + Ok(()) + } + + pub(crate) async fn metrics() -> ServerResult { UPTIME.set(START_TIME.elapsed().as_secs() as i64); let encoder = ProtobufEncoder::new(); let mut buf = Vec::new(); @@ -171,14 +195,13 @@ mod handlers { error!(?err, "error protobuf encoding prometheus metrics"); Ok(resp .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .unwrap()) + .body(Body::empty())?) } - Ok(_) => Ok(resp.status(StatusCode::OK).body(Body::from(buf)).unwrap()), + Ok(_) => Ok(resp.status(StatusCode::OK).body(Body::from(buf))?), } } - pub(crate) async fn metrics_text() -> Result { + pub(crate) async fn metrics_text() -> ServerResult { UPTIME.set(START_TIME.elapsed().as_secs() as i64); let encoder = TextEncoder::new(); let mut buf = String::new(); @@ -190,10 +213,9 @@ mod handlers { error!(?err, "error text encoding prometheus metrics"); Ok(resp .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .unwrap()) + .body(Body::empty())?) } - Ok(_) => Ok(resp.status(StatusCode::OK).body(Body::from(buf)).unwrap()), + Ok(_) => Ok(resp.status(StatusCode::OK).body(Body::from(buf))?), } } @@ -204,6 +226,7 @@ mod handlers { /// Various models for API responses pub mod models { + use axum::response::IntoResponse; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::{fmt, sync::Arc}; @@ -236,6 +259,32 @@ pub mod models { pub(crate) fn blank_health() -> State { Arc::new(Mutex::new(Health::Bad)) } + + // error type + /// Make our own error that wraps `anyhow::Error`. + #[derive(Debug)] + pub struct ServerError(anyhow::Error); + /// return error result + pub type ServerResult = Result; + + impl IntoResponse for ServerError { + fn into_response(self) -> axum::response::Response { + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + format!("{}", self.0), + ) + .into_response() + } + } + + impl From for ServerError + where + E: Into, + { + fn from(err: E) -> Self { + Self(err.into()) + } + } } #[cfg(test)] @@ -248,7 +297,8 @@ mod tests { #[tokio::test] async fn test_health() -> anyhow::Result<()> { let mgr = Arc::new(IpManager::new(SqliteDb::new("sqlite::memory:").await?)?); - let api = ExternalApi::new("0.0.0.0:8889".parse().unwrap(), mgr); + let cfg = Arc::new(DhcpConfig::default()); + let api = ExternalApi::new("0.0.0.0:8889".parse().unwrap(), cfg, mgr); let _handle = api.serve(); // wait for server to come up tokio::time::sleep(Duration::from_secs(1)).await; @@ -271,7 +321,8 @@ mod tests { #[tokio::test] async fn test_metrics() -> anyhow::Result<()> { let mgr = Arc::new(IpManager::new(SqliteDb::new("sqlite::memory:").await?)?); - let api = ExternalApi::new("0.0.0.0:8888".parse().unwrap(), mgr); + let cfg = Arc::new(DhcpConfig::default()); + let api = ExternalApi::new("0.0.0.0:8888".parse().unwrap(), cfg, mgr); let _handle = api.serve(); // wait for server to come up tokio::time::sleep(Duration::from_secs(1)).await; diff --git a/libs/config/sample/config_v4.json b/libs/config/sample/config_v4.json new file mode 100644 index 0000000..90c0456 --- /dev/null +++ b/libs/config/sample/config_v4.json @@ -0,0 +1,52 @@ +{ + "chaddr_only": false, + "flood_protection_threshold": { + "packets": 3, + "secs": 5 + }, + "cache_threshold": 25, + "networks": { + "192.168.1.100/30": { + "probation_period": 86400, + "server_id": "192.168.1.1", + "ranges": [ + { + "start": "192.168.1.100", + "end": "192.168.1.103", + "config": { + "lease_time": { + "default": 3600, + "min": 1200, + "max": 4800 + } + }, + "options": { + "values": { + "1": { + "type": "ip", + "value": "192.168.1.1" + }, + "3": { + "type": "ip", + "value": ["192.168.1.1"] + }, + "43": { + "type": "sub_option", + "value": { + "1": { + "type": "str", + "value": "foobar" + }, + "2": { + "type": "ip", + "value": "1.2.3.4" + } + } + } + } + } + } + ] + } + } +} diff --git a/libs/config/src/lib.rs b/libs/config/src/lib.rs index dde8b1b..e4c278b 100644 --- a/libs/config/src/lib.rs +++ b/libs/config/src/lib.rs @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize}; use tracing::debug; use wire::v6::ServerDuidInfo; /// server config -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct DhcpConfig { v4: v4::Config, } diff --git a/libs/config/src/v4.rs b/libs/config/src/v4.rs index 6f8c0c1..173fe67 100644 --- a/libs/config/src/v4.rs +++ b/libs/config/src/v4.rs @@ -29,7 +29,7 @@ pub use wire::v4::ddns::Ddns; pub const DEFAULT_LEASE_TIME: Duration = Duration::from_secs(86_400); /// server config for dhcpv4 -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Config { /// interfaces that are either explicitly bound by the config or /// are up & ipv4 @@ -672,6 +672,7 @@ mod tests { use super::*; pub static SAMPLE_YAML: &str = include_str!("../sample/config.yaml"); + pub static V4_JSON: &str = include_str!("../sample/config_v4.json"); pub static CIRC_YAML: &str = include_str!("../sample/circular_deps.yaml"); // test we can decode from wire @@ -689,6 +690,21 @@ mod tests { ); } + // test json sample + #[test] + fn test_sample_json() { + let cfg = Config::new(V4_JSON).unwrap(); + // test a range decoded properly + let net = cfg.network([192, 168, 1, 100]).unwrap(); + assert_eq!(net.ranges()[0].start(), Ipv4Addr::from([192, 168, 1, 100])); + assert_eq!( + net.ranges()[0].opts().get(v4::OptionCode::Router), + Some(&v4::DhcpOption::Router(vec![Ipv4Addr::from([ + 192, 168, 1, 1 + ])])) + ); + } + #[test] fn test_circular() { let cfg = Config::new(CIRC_YAML); diff --git a/libs/config/src/v6.rs b/libs/config/src/v6.rs index 9da42dc..9591768 100644 --- a/libs/config/src/v6.rs +++ b/libs/config/src/v6.rs @@ -1,4 +1,3 @@ -use hex; use std::{ collections::HashMap, net::Ipv6Addr, From 3d9688a68861442e587f41ed1deba8d7287d5171 Mon Sep 17 00:00:00 2001 From: Evan Cameron Date: Sun, 9 Jun 2024 23:17:36 -0400 Subject: [PATCH 2/4] add select_all method to sqlite db --- external-api/src/lib.rs | 15 +++++++ libs/config/src/lib.rs | 34 ++++++++++---- libs/ip-manager/src/lib.rs | 84 ++++++++++++++++++++++++++++++++++- libs/ip-manager/src/sqlite.rs | 36 ++++++++++++--- 4 files changed, 151 insertions(+), 18 deletions(-) diff --git a/external-api/src/lib.rs b/external-api/src/lib.rs index c0b0b9e..9e97b48 100644 --- a/external-api/src/lib.rs +++ b/external-api/src/lib.rs @@ -112,6 +112,7 @@ impl ExternalApi { .route("/metrics", routing::get(handlers::metrics)) .route("/metrics-text", routing::get(handlers::metrics_text)) .route("/leases", routing::get(handlers::leases::)) + .route("/config", routing::get(handlers::config)) .layer(Extension(state)) .layer(Extension(ip_mgr)) .layer(Extension(cfg)); @@ -153,6 +154,7 @@ mod handlers { use std::sync::Arc; + use anyhow::Context; use axum::{ body::Body, extract::Extension, @@ -179,10 +181,23 @@ mod handlers { Extension(cfg): Extension>, Extension(ip_mgr): Extension>>, ) -> ServerResult { + let leases = ip_mgr.select_all().await; todo!(); Ok(()) } + pub(crate) async fn config( + Extension(cfg): Extension>, + ) -> ServerResult { + // TODO: if serializing worked we could get DhcpConfig back into JSON/YAML but there's + // a lot of logic left to make that particular transform. So just read from disk + let path = cfg.path().context("no path specified for config")?; + let cfg = tokio::fs::read_to_string(path) + .await + .with_context(|| format!("failed to find config at {}", path.display()))?; + Ok(serde_json::to_string_pretty(&cfg)?) + } + pub(crate) async fn metrics() -> ServerResult { UPTIME.set(START_TIME.elapsed().as_secs() as i64); let encoder = ProtobufEncoder::new(); diff --git a/libs/config/src/lib.rs b/libs/config/src/lib.rs index e4c278b..7e6c224 100644 --- a/libs/config/src/lib.rs +++ b/libs/config/src/lib.rs @@ -1,25 +1,32 @@ +use std::{ + env, + path::{Path, PathBuf}, + time::Duration, +}; + +use anyhow::{bail, Context, Result}; +use rand::{self, RngCore}; +use serde::{Deserialize, Serialize}; +use tracing::debug; +use wire::v6::ServerDuidInfo; + pub mod client_classes; pub mod v4; pub mod v6; pub mod wire; -use std::{env, path::Path, time::Duration}; - -use anyhow::{bail, Context, Result}; use dora_core::dhcproto::v6::duid::Duid; use dora_core::pnet::{ self, datalink::NetworkInterface, ipnetwork::{IpNetwork, Ipv4Network}, }; -use rand::{self, RngCore}; -use serde::{Deserialize, Serialize}; -use tracing::debug; -use wire::v6::ServerDuidInfo; + /// server config #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct DhcpConfig { v4: v4::Config, + path: Option, } impl DhcpConfig { @@ -32,6 +39,9 @@ impl DhcpConfig { pub fn v6(&self) -> &v6::Config { self.v4.v6().unwrap() // v6 existence checked before starting plugins } + pub fn path(&self) -> Option<&Path> { + self.path.as_deref() + } } /// server instance config @@ -64,14 +74,20 @@ impl DhcpConfig { )?; debug!(?config); - Ok(Self { v4: config }) + Ok(Self { + v4: config, + path: Some(path.to_path_buf()), + }) } /// attempts to decode the config first as JSON, then YAML, finally erroring if neither work pub fn parse_str>(s: S) -> Result { let config = v4::Config::new(s.as_ref())?; debug!(?config); - Ok(Self { v4: config }) + Ok(Self { + v4: config, + path: None, + }) } } diff --git a/libs/ip-manager/src/lib.rs b/libs/ip-manager/src/lib.rs index d7d6fa5..9c3d505 100644 --- a/libs/ip-manager/src/lib.rs +++ b/libs/ip-manager/src/lib.rs @@ -38,7 +38,7 @@ use std::{ const PING_TTL: u64 = 60; pub type ClientId = Option>; -#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, sqlx::FromRow)] pub struct ClientInfo { ip: IpAddr, id: ClientId, @@ -46,6 +46,21 @@ pub struct ClientInfo { expires_at: SystemTime, } +impl ClientInfo { + pub fn ip(&self) -> IpAddr { + self.ip + } + pub fn id(&self) -> Option<&[u8]> { + self.id.as_deref() + } + pub fn network(&self) -> IpAddr { + self.network + } + pub fn expires_at(&self) -> SystemTime { + self.expires_at + } +} + #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum IpState { Lease, @@ -88,6 +103,7 @@ pub trait Storage: Send + Sync + 'static { async fn get(&self, ip: IpAddr) -> Result, Self::Error>; async fn get_id(&self, id: &[u8]) -> Result, Self::Error>; + async fn select_all(&self) -> Result, Self::Error>; async fn release_ip(&self, ip: IpAddr, id: &[u8]) -> Result, Self::Error>; async fn delete(&self, ip: IpAddr) -> Result<(), Self::Error>; @@ -129,7 +145,7 @@ pub trait Storage: Send + Sync + 'static { async fn count(&self, state: IpState) -> Result; } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum State { Reserved(ClientInfo), Leased(ClientInfo), @@ -405,6 +421,15 @@ where Ok(()) } + /// select all leases in array, returning as a vec + pub async fn select_all(&self) -> Result, IpError> { + Ok(self.store.select_all().await?) + } + + pub async fn get(&self, ip: IpAddr) -> Result, IpError> { + Ok(self.store.get(ip).await?) + } + /// sees if there is an un-expired IP associated with this ID /// Returns /// Err if expired or id not found @@ -762,6 +787,61 @@ mod tests { Ok(()) } + // add some leases then select * + #[tokio::test] + #[traced_test] + async fn test_select_all() -> Result<()> { + let mgr = IpManager::new(SqliteDb::new("sqlite::memory:").await?)?; + let range = NetRange::new( + Ipv4Addr::new(192, 168, 1, 100)..=Ipv4Addr::new(192, 168, 1, 255), + LeaseTime::new( + Duration::from_secs(5), + Duration::from_secs(3), + Duration::from_secs(10), + ), + ); + let mut network = Network::default(); + network + .set_subnet("192.168.1.0/24".parse()?) + .set_ranges(vec![range.clone()]); + let mut res = vec![]; + + let expires_at = SystemTime::now() + Duration::from_secs(30); + for i in 0..5 { + let client_id = &[1, 1, 1, 1, 1, 1 + i]; + // reserve from range + let ip = mgr + .reserve_first(&range, &network, client_id, expires_at, None) + .await?; + assert_eq!(ip, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100 + i))); + + // make lease + mgr.try_lease(ip, client_id, expires_at, &network).await?; + let state = mgr.get(ip).await?.expect("not found"); + assert_eq!( + state.as_ref().ip(), + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100 + i)) + ); + assert_eq!(state.as_ref().id().unwrap(), &[1, 1, 1, 1, 1, 1 + i]); + + res.push(State::Leased(ClientInfo { + ip: state.as_ref().ip(), + id: Some(client_id.to_vec().clone()), + // systtime we get back has no nano seconds + expires_at: state.as_ref().expires_at(), + network: network.subnet().into(), + })); + } + + let leases = mgr.select_all().await?; + assert_eq!( + leases.into_iter().collect::>(), + res.into_iter().collect() + ); + + Ok(()) + } + // do reserve and lease in 2 steps like usual #[tokio::test] #[traced_test] diff --git a/libs/ip-manager/src/sqlite.rs b/libs/ip-manager/src/sqlite.rs index d540461..af4c6ed 100644 --- a/libs/ip-manager/src/sqlite.rs +++ b/libs/ip-manager/src/sqlite.rs @@ -301,6 +301,10 @@ impl Storage for SqliteDb { ) .await } + + async fn select_all(&self) -> Result, Self::Error> { + util::select_all(&self.inner).await + } } mod util { @@ -426,6 +430,24 @@ mod util { })) } + /// select all values in leases table and return them + pub async fn select_all(pool: &SqlitePool) -> Result, sqlx::Error> { + Ok(sqlx::query!("SELECT * FROM leases") + .fetch_all(pool) + .await? + .into_iter() + .map(|cur| { + let info = ClientInfo { + ip: IpAddr::V4(Ipv4Addr::from(cur.ip as u32)), + id: cur.client_id.map(|v| v.to_vec()), + network: IpAddr::V4(Ipv4Addr::from(cur.network as u32)), + expires_at: to_systime(cur.expires_at), + }; + into_clientinfo(info, cur.leased, cur.probation) + }) + .collect()) + } + /// return a count of all rows where leased & probation & un-expired pub async fn count( pool: &SqlitePool, @@ -450,11 +472,11 @@ mod util { now: i64, ) -> Result, sqlx::Error> { Ok(sqlx::query!( - "SELECT ip - FROM - leases - WHERE - client_id = ?1 AND expires_at > ?2 + "SELECT ip + FROM + leases + WHERE + client_id = ?1 AND expires_at > ?2 LIMIT 1", id, now @@ -575,7 +597,7 @@ mod util { SELECT ip FROM leases WHERE - ((client_id = ?2 AND ip = ?3) + ((client_id = ?2 AND ip = ?3) OR (expires_at < ?1 AND ip = ?3)) ORDER BY ip LIMIT 1 ) @@ -671,7 +693,7 @@ mod util { UPDATE leases SET client_id = ?2, expires_at = ?3, leased = ?4, probation = ?5 - WHERE + WHERE ip = ?1 RETURNING * "#, From dd45079567ec4df30c2dd00c47fdfc6a31a1af17 Mon Sep 17 00:00:00 2001 From: Evan Cameron Date: Sun, 9 Jun 2024 23:41:42 -0400 Subject: [PATCH 3/4] replace offline sqlx --- libs/ip-manager/sqlx-data.json | 204 ++++++++++++++++++++------------- 1 file changed, 126 insertions(+), 78 deletions(-) diff --git a/libs/ip-manager/sqlx-data.json b/libs/ip-manager/sqlx-data.json index 6db2f72..af45f50 100644 --- a/libs/ip-manager/sqlx-data.json +++ b/libs/ip-manager/sqlx-data.json @@ -1,7 +1,24 @@ { "db": "SQLite", + "0ed87a7db669b203bea70d30069fd27c97d74931053e69d39d5487075d575976": { + "describe": { + "columns": [ + { + "name": "ip", + "ordinal": 0, + "type_info": "Int64" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT ip\n FROM\n leases\n WHERE\n client_id = ?1 AND expires_at > ?2\n LIMIT 1" + }, "47c078186e966aa9ce236a2e0e54edf870cf6cbca6fbb20eb0896675be9347f3": { - "query": "SELECT * FROM leases WHERE ip = ?1", "describe": { "columns": [ { @@ -35,9 +52,6 @@ "type_info": "Bool" } ], - "parameters": { - "Right": 1 - }, "nullable": [ false, true, @@ -45,11 +59,14 @@ false, false, false - ] - } + ], + "parameters": { + "Right": 1 + } + }, + "query": "SELECT * FROM leases WHERE ip = ?1" }, "793f1692b36d96a6815c6bfb21d5de88c4f64c020af406405a2ed8c9535ffff3": { - "query": "SELECT COUNT(ip) as count_ip FROM leases WHERE leased = ?1 AND probation = ?2 AND expires_at > ?3", "describe": { "columns": [ { @@ -58,26 +75,74 @@ "type_info": "Int" } ], + "nullable": [ + false + ], "parameters": { "Right": 3 - }, + } + }, + "query": "SELECT COUNT(ip) as count_ip FROM leases WHERE leased = ?1 AND probation = ?2 AND expires_at > ?3" + }, + "79f533ff7a523b1e305075de57b772e8728a5c76e21cd256d88303b86585ba0d": { + "describe": { + "columns": [ + { + "name": "ip", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "client_id", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "leased", + "ordinal": 2, + "type_info": "Bool" + }, + { + "name": "expires_at", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "network", + "ordinal": 4, + "type_info": "Int64" + }, + { + "name": "probation", + "ordinal": 5, + "type_info": "Bool" + } + ], "nullable": [ + false, + true, + false, + false, + false, false - ] - } + ], + "parameters": { + "Right": 5 + } + }, + "query": "\n UPDATE leases\n SET\n client_id = ?2, expires_at = ?3, leased = ?4, probation = ?5\n WHERE\n ip = ?1\n RETURNING *\n " }, "7a75f6b16faff6ed52e5d49bf639d27e8cbabd72922a6ecebecddc5810ad5e2d": { - "query": "INSERT INTO leases\n (ip, client_id, expires_at, network, leased, probation)\n VALUES\n (?1, ?2, ?3, ?4, ?5, ?6)", "describe": { "columns": [], + "nullable": [], "parameters": { "Right": 6 - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO leases\n (ip, client_id, expires_at, network, leased, probation)\n VALUES\n (?1, ?2, ?3, ?4, ?5, ?6)" }, "882703a86e74138d6b6f7e58219890fbf40508e6212ba0aa6e507b1f1c9ad1f4": { - "query": "\n UPDATE leases\n SET\n client_id = ?4, leased = ?5, expires_at = ?6, probation = FALSE\n WHERE ip in\n (\n SELECT ip\n FROM leases\n WHERE\n ((expires_at < ?1) AND (ip >= ?2 AND ip <= ?3)) OR (client_id = ?4)\n ORDER BY ip LIMIT 1\n )\n RETURNING ip\n ", "describe": { "columns": [ { @@ -86,16 +151,16 @@ "type_info": "Int64" } ], - "parameters": { - "Right": 6 - }, "nullable": [ false - ] - } + ], + "parameters": { + "Right": 6 + } + }, + "query": "\n UPDATE leases\n SET\n client_id = ?4, leased = ?5, expires_at = ?6, probation = FALSE\n WHERE ip in\n (\n SELECT ip\n FROM leases\n WHERE\n ((expires_at < ?1) AND (ip >= ?2 AND ip <= ?3)) OR (client_id = ?4)\n ORDER BY ip LIMIT 1\n )\n RETURNING ip\n " }, - "a9016b83146975088fb6d81b9e9f279cd5f8f697a3720e4a0a9394ecb91a0a5e": { - "query": "\n UPDATE leases\n SET\n client_id = ?2, leased = ?4, expires_at = ?5, probation = ?6\n WHERE ip in\n (\n SELECT ip\n FROM leases\n WHERE\n ((client_id = ?2 AND ip = ?3) \n OR (expires_at < ?1 AND ip = ?3))\n ORDER BY ip LIMIT 1\n )\n RETURNING ip\n ", + "891fc8512be79fc824f7ff618c343d21ad22ebc067474065839d143257bee20c": { "describe": { "columns": [ { @@ -104,16 +169,16 @@ "type_info": "Int64" } ], - "parameters": { - "Right": 6 - }, "nullable": [ false - ] - } + ], + "parameters": { + "Right": 6 + } + }, + "query": "\n UPDATE leases\n SET\n client_id = ?2, leased = ?4, expires_at = ?5, probation = ?6\n WHERE ip in\n (\n SELECT ip\n FROM leases\n WHERE\n ((client_id = ?2 AND ip = ?3)\n OR (expires_at < ?1 AND ip = ?3))\n ORDER BY ip LIMIT 1\n )\n RETURNING ip\n " }, "ac1e7d2c911b8205f797601c1106d8dd455ad37d7805bb5e708cc389fef971e2": { - "query": "\n UPDATE leases\n SET\n leased = ?4, expires_at = ?5, probation = ?6, client_id = ?7\n WHERE ip in\n (\n SELECT ip\n FROM leases\n WHERE\n ((expires_at > ?1) AND (client_id = ?2) AND (ip = ?3))\n ORDER BY ip LIMIT 1\n )\n RETURNING ip\n ", "describe": { "columns": [ { @@ -122,16 +187,16 @@ "type_info": "Int64" } ], - "parameters": { - "Right": 7 - }, "nullable": [ false - ] - } + ], + "parameters": { + "Right": 7 + } + }, + "query": "\n UPDATE leases\n SET\n leased = ?4, expires_at = ?5, probation = ?6, client_id = ?7\n WHERE ip in\n (\n SELECT ip\n FROM leases\n WHERE\n ((expires_at > ?1) AND (client_id = ?2) AND (ip = ?3))\n ORDER BY ip LIMIT 1\n )\n RETURNING ip\n " }, "adbd99769bd732ad04fbe219c0b4867094f50d3febba9e8342153afb3f1d0bf7": { - "query": "SELECT * FROM leases WHERE ip = ?1 AND client_id = ?2", "describe": { "columns": [ { @@ -165,9 +230,6 @@ "type_info": "Bool" } ], - "parameters": { - "Right": 2 - }, "nullable": [ false, true, @@ -175,21 +237,24 @@ false, false, false - ] - } + ], + "parameters": { + "Right": 2 + } + }, + "query": "SELECT * FROM leases WHERE ip = ?1 AND client_id = ?2" }, "aef8d003e1661e7b3ea3a74535cfb94451e91e6a3af65c2fb452cc56a512ac0d": { - "query": "INSERT INTO leases (ip, client_id, expires_at, network) VALUES (?1, ?2, ?3, ?4)", "describe": { "columns": [], + "nullable": [], "parameters": { "Right": 4 - }, - "nullable": [] - } + } + }, + "query": "INSERT INTO leases (ip, client_id, expires_at, network) VALUES (?1, ?2, ?3, ?4)" }, "b2234ad91400eac3cc40103cde969bb8acb66cd866a25515815c187d2b959c52": { - "query": "\n SELECT\n *\n FROM\n leases\n WHERE\n ip >= ?1 AND ip <= ?2\n ORDER BY\n ip DESC\n LIMIT 1\n ", "describe": { "columns": [ { @@ -223,9 +288,6 @@ "type_info": "Bool" } ], - "parameters": { - "Right": 2 - }, "nullable": [ false, true, @@ -233,11 +295,14 @@ false, false, false - ] - } + ], + "parameters": { + "Right": 2 + } + }, + "query": "\n SELECT\n *\n FROM\n leases\n WHERE\n ip >= ?1 AND ip <= ?2\n ORDER BY\n ip DESC\n LIMIT 1\n " }, - "d69709c14c3d5077d08ee45219811df9460eb5ba305097440d953a3c69edcdc0": { - "query": "\n UPDATE leases\n SET\n client_id = ?2, expires_at = ?3, leased = ?4, probation = ?5\n WHERE \n ip = ?1\n RETURNING *\n ", + "b956acaf5fae340ca8d4f050a8d5825fd77dd03d68ccfec6f5c97bc49178bfce": { "describe": { "columns": [ { @@ -271,9 +336,6 @@ "type_info": "Bool" } ], - "parameters": { - "Right": 5 - }, "nullable": [ false, true, @@ -281,35 +343,21 @@ false, false, false - ] - } + ], + "parameters": { + "Right": 0 + } + }, + "query": "SELECT * FROM leases" }, "d936276b3e7ea7fd4e26597791388e43779d3b87835573709227673ab8d49847": { - "query": "DELETE FROM leases WHERE ip = ?1", "describe": { "columns": [], + "nullable": [], "parameters": { "Right": 1 - }, - "nullable": [] - } - }, - "f142683f0ea01af776e17015553be7f6b0b49e030f954bd1a80a38a8de72147d": { - "query": "SELECT ip \n FROM \n leases \n WHERE \n client_id = ?1 AND expires_at > ?2 \n LIMIT 1", - "describe": { - "columns": [ - { - "name": "ip", - "ordinal": 0, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false - ] - } + } + }, + "query": "DELETE FROM leases WHERE ip = ?1" } } \ No newline at end of file From 3b9f9421f85a39ffbf38de45868e28f5556d03d6 Mon Sep 17 00:00:00 2001 From: Evan Cameron Date: Wed, 19 Jun 2024 23:32:11 -0400 Subject: [PATCH 4/4] return some leases --- Cargo.lock | 3 ++ external-api/Cargo.toml | 3 ++ external-api/src/lib.rs | 101 +++++++++++++++++++++++++++++++++++++--- libs/config/src/v4.rs | 8 ++++ 4 files changed, 108 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f810b1..2079775 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1067,9 +1067,12 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "chrono", "config", "dora-core", + "hex", "ip-manager", + "ipnet", "parking_lot 0.12.1", "prometheus", "reqwest 0.12.4", diff --git a/external-api/Cargo.toml b/external-api/Cargo.toml index 7c0e87e..debb95b 100644 --- a/external-api/Cargo.toml +++ b/external-api/Cargo.toml @@ -15,6 +15,8 @@ config = { path = "../libs/config" } # libs anyhow = { workspace = true } axum = "0.7.5" +chrono = "0.4.38" +hex = "0.4.3" tokio = { workspace = true } tracing-futures = { workspace = true } tracing = { workspace = true } @@ -22,6 +24,7 @@ parking_lot = "0.12" serde = { workspace = true } serde_json = { workspace = true } prometheus = { workspace = true } +ipnet = { workspace = true } [dev-dependencies] diff --git a/external-api/src/lib.rs b/external-api/src/lib.rs index 9e97b48..3cdcf80 100644 --- a/external-api/src/lib.rs +++ b/external-api/src/lib.rs @@ -111,7 +111,7 @@ impl ExternalApi { .route("/ping", routing::get(handlers::ping)) .route("/metrics", routing::get(handlers::metrics)) .route("/metrics-text", routing::get(handlers::metrics_text)) - .route("/leases", routing::get(handlers::leases::)) + .route("/v1/leases", routing::get(handlers::leases::)) .route("/config", routing::get(handlers::config)) .layer(Extension(state)) .layer(Extension(ip_mgr)) @@ -152,7 +152,7 @@ impl ExternalApi { mod handlers { - use std::sync::Arc; + use std::{collections::HashMap, sync::Arc, time::UNIX_EPOCH}; use anyhow::Context; use axum::{ @@ -162,9 +162,11 @@ mod handlers { http::{Response, StatusCode}, response::IntoResponse, }; + use chrono::{DateTime, Utc}; use config::DhcpConfig; use dora_core::metrics::{START_TIME, UPTIME}; use ip_manager::{IpManager, Storage}; + use ipnet::Ipv4Net; use prometheus::{Encoder, ProtobufEncoder, TextEncoder}; use tracing::error; @@ -181,9 +183,67 @@ mod handlers { Extension(cfg): Extension>, Extension(ip_mgr): Extension>>, ) -> ServerResult { - let leases = ip_mgr.select_all().await; - todo!(); - Ok(()) + use crate::models::{LeaseInfo, LeaseNetworks, LeaseState, Leases}; + use ip_manager::State as S; + + let mut cfg = (*cfg).clone(); + let networks = ip_mgr + .select_all() + .await? + .into_iter() + .map(|lease| { + let info = lease.as_ref(); + let ip = info.ip(); + let id = info.id().map(hex::encode); + let secs = info.expires_at().duration_since(UNIX_EPOCH)?.as_secs(); + let network = info.network(); + let expires_at_epoch = secs; + let expires_at_utc = DateTime::::from_timestamp( + info.expires_at().duration_since(UNIX_EPOCH)?.as_secs() as i64, + 0, + ) + .context("failed to create UTC datetime")? + .to_rfc3339(); + let lease_info = LeaseInfo { + ip, + id, + network, + expires_at_epoch, + expires_at_utc, + }; + + let netv4 = match network { + std::net::IpAddr::V4(ip) => ip, + std::net::IpAddr::V6(_) => { + return Err(anyhow::anyhow!("no dynamic ipv6 at this time")) + } + }; + if let Some(net) = cfg.v4().network(netv4) { + let lease = match lease { + S::Reserved(_) => LeaseState::Reserved(lease_info), + S::Leased(_) => LeaseState::Leased(lease_info), + S::Probated(_) => LeaseState::Probated(lease_info), + }; + Ok((net, lease)) + } else { + Err(anyhow::anyhow!( + "failed to find network in cfg for {lease_info:?}" + )) + } + }) + .collect::, _>>()? + .into_iter() + .fold( + HashMap::::new(), + |mut map, (net, lease)| { + let entry = map.entry(net.full_subnet()).or_default(); + entry.ips.push(lease); + + map + }, + ); + + Ok(axum::Json(Leases { networks })) } pub(crate) async fn config( @@ -195,7 +255,7 @@ mod handlers { let cfg = tokio::fs::read_to_string(path) .await .with_context(|| format!("failed to find config at {}", path.display()))?; - Ok(serde_json::to_string_pretty(&cfg)?) + Ok(axum::Json(cfg)) } pub(crate) async fn metrics() -> ServerResult { @@ -241,10 +301,12 @@ mod handlers { /// Various models for API responses pub mod models { + use std::{collections::HashMap, fmt, net::IpAddr, sync::Arc}; + use axum::response::IntoResponse; + use ipnet::Ipv4Net; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; - use std::{fmt, sync::Arc}; /// The overall health of the system pub type State = Arc>; @@ -271,6 +333,31 @@ pub mod models { } } + #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Eq)] + pub struct Leases { + pub networks: HashMap, + } + + #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, Eq)] + pub struct LeaseNetworks { + pub ips: Vec, + } + + #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Eq)] + pub enum LeaseState { + Reserved(LeaseInfo), + Leased(LeaseInfo), + Probated(LeaseInfo), + } + #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Eq)] + pub struct LeaseInfo { + pub ip: IpAddr, + pub id: Option, + pub network: IpAddr, + pub expires_at_epoch: u64, + pub expires_at_utc: String, + } + pub(crate) fn blank_health() -> State { Arc::new(Mutex::new(Health::Bad)) } diff --git a/libs/config/src/v4.rs b/libs/config/src/v4.rs index 173fe67..57adb13 100644 --- a/libs/config/src/v4.rs +++ b/libs/config/src/v4.rs @@ -205,6 +205,11 @@ impl Config { }) } + /// return hashmap of networks + pub fn networks(&self) -> &HashMap { + &self.networks + } + /// find the interface at the index `iface_index` pub fn find_interface(&self, iface_index: u32) -> Option<&NetworkInterface> { self.interfaces.iter().find(|e| e.index == iface_index) @@ -356,6 +361,9 @@ impl Network { pub fn subnet(&self) -> Ipv4Addr { self.subnet.network() } + pub fn full_subnet(&self) -> Ipv4Net { + self.subnet + } pub fn authoritative(&self) -> bool { self.authoritative }