diff --git a/Cargo.lock b/Cargo.lock index 61c7412f..f63c0275 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -780,6 +780,7 @@ dependencies = [ "colored", "ddm", "ddm-admin-client", + "humantime", "mg-common", "slog", "slog-async", diff --git a/ddm-admin-client/src/lib.rs b/ddm-admin-client/src/lib.rs index 38ee46fe..304d52ae 100644 --- a/ddm-admin-client/src/lib.rs +++ b/ddm-admin-client/src/lib.rs @@ -14,7 +14,8 @@ progenitor::generate_api!( }), post_hook = (|log: &slog::Logger, result: &Result<_, _>| { slog::trace!(log, "client response"; "result" => ?result); - }) + }), + replace = { Duration = std::time::Duration } ); impl Copy for types::Ipv4Prefix {} diff --git a/ddm/src/admin.rs b/ddm/src/admin.rs index 918a300d..226642e1 100644 --- a/ddm/src/admin.rs +++ b/ddm/src/admin.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use crate::db::{Db, PeerInfo, TunnelRoute}; +use crate::db::{Db, RouterKind, TunnelRoute}; use crate::exchange::PathVector; use crate::sm::{AdminEvent, Event, PrefixSet, SmContext}; use dropshot::endpoint; @@ -27,6 +27,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::mpsc::Sender; use std::sync::Arc; use std::sync::Mutex; +use std::time::{Duration, Instant}; use tokio::spawn; use tokio::task::JoinHandle; use uuid::Uuid; @@ -103,12 +104,130 @@ pub fn handler( Ok(()) } +/// Status of a DDM peer with state expressed as durations. +#[derive( + Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema, +)] +#[serde(tag = "type", content = "value")] +pub enum PeerStatusV2 { + NoContact, + Init(Duration), + Solicit(Duration), + Exchange(Duration), + Expired(Duration), +} + +// Translate internal peer status which is based on instants, to API +// representation which is based on durations. +impl From for PeerStatusV2 { + fn from(value: crate::db::PeerStatus) -> Self { + match value { + crate::db::PeerStatus::NoContact => Self::NoContact, + crate::db::PeerStatus::Init(t) => { + Self::Init(Instant::now().duration_since(t)) + } + crate::db::PeerStatus::Solicit(t) => { + Self::Solicit(Instant::now().duration_since(t)) + } + crate::db::PeerStatus::Exchange(t) => { + Self::Exchange(Instant::now().duration_since(t)) + } + crate::db::PeerStatus::Expired(t) => { + Self::Expired(Instant::now().duration_since(t)) + } + } + } +} + +/// Information about a DDM peer. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +pub struct PeerInfoV2 { + pub status: PeerStatusV2, + pub addr: Ipv6Addr, + pub host: String, + pub kind: RouterKind, +} + +#[derive( + Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, +)] +pub enum PeerStatus { + NoContact, + Active, + Expired, +} + +// Translate internal peer status which is based on instants, to API +// representation which is based on durations. +impl From for PeerStatus { + fn from(value: crate::db::PeerStatus) -> Self { + match value { + crate::db::PeerStatus::NoContact => Self::NoContact, + crate::db::PeerStatus::Init(_) + | crate::db::PeerStatus::Solicit(_) + | crate::db::PeerStatus::Exchange(_) => Self::Active, + crate::db::PeerStatus::Expired(_) => Self::Expired, + } + } +} + +impl From for PeerInfo { + fn from(value: crate::db::PeerInfo) -> Self { + Self { + status: value.status.into(), + addr: value.addr, + host: value.host, + kind: value.kind, + } + } +} + +/// Information about a DDM peer. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +pub struct PeerInfo { + pub status: PeerStatus, + pub addr: Ipv6Addr, + pub host: String, + pub kind: RouterKind, +} + +impl From for PeerInfoV2 { + fn from(value: crate::db::PeerInfo) -> Self { + Self { + status: value.status.into(), + addr: value.addr, + host: value.host, + kind: value.kind, + } + } +} + #[endpoint { method = GET, path = "/peers" }] async fn get_peers( ctx: RequestContext>>, ) -> Result>, HttpError> { let ctx = ctx.context().lock().unwrap(); - Ok(HttpResponseOk(ctx.db.peers())) + let peers = ctx + .db + .peers() + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(); + Ok(HttpResponseOk(peers)) +} + +#[endpoint { method = GET, path = "/peers_v2" }] +async fn get_peers_v2( + ctx: RequestContext>>, +) -> Result>, HttpError> { + let ctx = ctx.context().lock().unwrap(); + let peers = ctx + .db + .peers() + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(); + Ok(HttpResponseOk(peers)) } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] @@ -422,6 +541,7 @@ pub fn api_description( ) -> Result>>, String> { let mut api = ApiDescription::new(); api.register(get_peers)?; + api.register(get_peers_v2)?; api.register(expire_peer)?; api.register(advertise_prefixes)?; api.register(advertise_tunnel_endpoints)?; diff --git a/ddm/src/db.rs b/ddm/src/db.rs index f3edc674..b8639af6 100644 --- a/ddm/src/db.rs +++ b/ddm/src/db.rs @@ -6,10 +6,11 @@ use mg_common::net::{IpPrefix, Ipv6Prefix, TunnelOrigin}; use schemars::{JsonSchema, JsonSchema_repr}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use slog::{error, Logger}; +use slog::{debug, error, Logger}; use std::collections::{HashMap, HashSet}; use std::net::Ipv6Addr; use std::sync::{Arc, Mutex}; +use std::time::Instant; /// The handle used to open a persistent key-value tree for originated /// prefixes. @@ -227,10 +228,34 @@ impl Db { /// Set peer info at the given index. Returns true if peer information was /// changed. - pub fn set_peer(&self, index: u32, info: PeerInfo) -> bool { - match self.data.lock().unwrap().peers.insert(index, info.clone()) { - Some(previous) => previous == info, - None => true, + pub fn set_peer_info( + &self, + index: u32, + addr: Ipv6Addr, + host: String, + kind: RouterKind, + ) -> bool { + let mut data = self.data.lock().unwrap(); + if let Some(peer) = data.peers.get_mut(&index) { + if peer.addr == addr && peer.host == host && peer.kind == kind { + false + } else { + peer.addr = addr; + peer.host = host; + peer.kind = kind; + true + } + } else { + data.peers.insert( + index, + PeerInfo { + addr, + host, + kind, + status: PeerStatus::Init(Instant::now()), + }, + ); + true } } @@ -267,6 +292,19 @@ impl Db { self.data.lock().unwrap().peers.remove(&index); } + pub fn peer_status_transition(&self, index: u32, status: PeerStatus) { + if let Some(info) = self.data.lock().unwrap().peers.get_mut(&index) { + info.status = status; + } else { + // This is expected to happen during initialization as we don't + // add a peer to the db until an advertisement is received. + debug!( + self.log, + "status update: peer with index {} does not exist", index + ); + } + } + pub fn routes_by_vector( &self, dst: Ipv6Prefix, @@ -283,16 +321,16 @@ impl Db { } } -#[derive( - Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema, -)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum PeerStatus { NoContact, - Active, - Expired, + Init(Instant), + Solicit(Instant), + Exchange(Instant), + Expired(Instant), } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct PeerInfo { pub status: PeerStatus, pub addr: Ipv6Addr, diff --git a/ddm/src/discovery.rs b/ddm/src/discovery.rs index f4d00c81..035dc69f 100644 --- a/ddm/src/discovery.rs +++ b/ddm/src/discovery.rs @@ -85,7 +85,7 @@ //! and 1 for a transit routers. The fourth byte is a hostname length followed //! directly by a hostname of up to 255 bytes in length. -use crate::db::{Db, PeerInfo, PeerStatus, RouterKind}; +use crate::db::{Db, RouterKind}; use crate::sm::{Config, Event, NeighborEvent, SessionStats}; use crate::util::u8_slice_assume_init_ref; use crate::{dbg, err, inf, trc, wrn}; @@ -504,15 +504,9 @@ fn handle_advertisement( } }; drop(guard); - let updated = ctx.db.set_peer( - ctx.config.if_index, - PeerInfo { - status: PeerStatus::Active, - addr: *sender, - host: hostname, - kind, - }, - ); + let updated = + ctx.db + .set_peer_info(ctx.config.if_index, *sender, hostname, kind); if updated { stats.peer_address.lock().unwrap().replace(*sender); emit_nbr_update(ctx, sender, version); diff --git a/ddm/src/sm.rs b/ddm/src/sm.rs index 524f0c3e..7ce30ed5 100644 --- a/ddm/src/sm.rs +++ b/ddm/src/sm.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use crate::db::{Db, RouterKind}; +use crate::db::{Db, PeerStatus, RouterKind}; use crate::discovery::Version; use crate::exchange::{PathVector, TunnelUpdate, UnderlayUpdate, Update}; use crate::{dbg, discovery, err, exchange, inf, wrn}; @@ -16,7 +16,7 @@ use std::sync::mpsc::{Receiver, Sender}; use std::sync::{Arc, Mutex}; use std::thread::sleep; use std::thread::spawn; -use std::time::Duration; +use std::time::{Duration, Instant}; use thiserror::Error; #[derive(Debug)] @@ -228,6 +228,10 @@ impl State for Init { &mut self, event: Receiver, ) -> (Box, Receiver) { + self.ctx.db.peer_status_transition( + self.ctx.config.if_index, + PeerStatus::Init(Instant::now()), + ); loop { let info = match get_ipaddr_info(&self.ctx.config.aobj_name) { Ok(info) => info, @@ -303,6 +307,10 @@ impl State for Solicit { &mut self, event: Receiver, ) -> (Box, Receiver) { + self.ctx.db.peer_status_transition( + self.ctx.config.if_index, + PeerStatus::Solicit(Instant::now()), + ); loop { let e = match event.recv() { Ok(e) => e, @@ -529,6 +537,10 @@ impl State for Exchange { &mut self, event: Receiver, ) -> (Box, Receiver) { + self.ctx.db.peer_status_transition( + self.ctx.config.if_index, + PeerStatus::Exchange(Instant::now()), + ); let exchange_thread = loop { match exchange::handler( self.ctx.clone(), @@ -759,7 +771,6 @@ impl State for Exchange { ); } } - // TODO tunnel Event::Peer(PeerEvent::Push(update)) => { inf!( self.log, diff --git a/ddmadm/Cargo.toml b/ddmadm/Cargo.toml index 703629e4..70153ce0 100644 --- a/ddmadm/Cargo.toml +++ b/ddmadm/Cargo.toml @@ -17,3 +17,4 @@ tabwriter.workspace = true colored.workspace = true anyhow.workspace = true anstyle.workspace = true +humantime.workspace = true diff --git a/ddmadm/src/main.rs b/ddmadm/src/main.rs index 46005cb5..268e9259 100644 --- a/ddmadm/src/main.rs +++ b/ddmadm/src/main.rs @@ -5,7 +5,9 @@ use anyhow::Result; use clap::Parser; use colored::*; +use ddm_admin_client::types::PeerStatusV2; use ddm_admin_client::{types, Client}; +use humantime::Duration; use mg_common::cli::oxide_cli_style; use mg_common::net::{IpPrefix, Ipv4Prefix, Ipv6Prefix}; use slog::{Drain, Logger}; @@ -106,7 +108,7 @@ async fn run() -> Result<()> { match arg.subcmd { SubCommand::GetPeers => { - let msg = client.get_peers().await?; + let msg = client.get_peers_v2().await?; let mut tw = TabWriter::new(stdout()); writeln!( &mut tw, @@ -120,7 +122,7 @@ async fn run() -> Result<()> { for (index, info) in &msg.into_inner() { writeln!( &mut tw, - "{}\t{}\t{}\t{}\t{:?}", + "{}\t{}\t{}\t{}\t{}", index, info.host, info.addr, @@ -129,7 +131,34 @@ async fn run() -> Result<()> { 1 => "Transit", _ => "?", }, - info.status, + match info.status { + PeerStatusV2::NoContact => "no contact".into(), + PeerStatusV2::Init(t) => format!( + "Init {}", + // Don't care about precision beyond milliseconds + Duration::from(std::time::Duration::from_millis( + t.as_millis() as u64 + )) + ), + PeerStatusV2::Solicit(t) => format!( + "Solicit {}", + Duration::from(std::time::Duration::from_millis( + t.as_millis() as u64 + )) + ), + PeerStatusV2::Exchange(t) => format!( + "Exchange {}", + Duration::from(std::time::Duration::from_millis( + t.as_millis() as u64 + )) + ), + PeerStatusV2::Expired(t) => format!( + "Expired {}", + Duration::from(std::time::Duration::from_millis( + t.as_millis() as u64 + )) + ), + } )?; } tw.flush()?; diff --git a/openapi/ddm-admin.json b/openapi/ddm-admin.json index 0fa17ba7..2fdfc076 100644 --- a/openapi/ddm-admin.json +++ b/openapi/ddm-admin.json @@ -157,6 +157,33 @@ } } }, + "/peers_v2": { + "get": { + "operationId": "get_peers_v2", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_PeerInfoV2", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PeerInfoV2" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/prefix": { "put": { "operationId": "advertise_prefixes", @@ -355,6 +382,25 @@ }, "components": { "schemas": { + "Duration": { + "type": "object", + "properties": { + "nanos": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "secs": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "nanos", + "secs" + ] + }, "EnableStatsRequest": { "type": "object", "properties": { @@ -486,6 +532,7 @@ ] }, "PeerInfo": { + "description": "Information about a DDM peer.", "type": "object", "properties": { "addr": { @@ -509,6 +556,31 @@ "status" ] }, + "PeerInfoV2": { + "description": "Information about a DDM peer.", + "type": "object", + "properties": { + "addr": { + "type": "string", + "format": "ipv6" + }, + "host": { + "type": "string" + }, + "kind": { + "$ref": "#/components/schemas/RouterKind" + }, + "status": { + "$ref": "#/components/schemas/PeerStatusV2" + } + }, + "required": [ + "addr", + "host", + "kind", + "status" + ] + }, "PeerStatus": { "type": "string", "enum": [ @@ -517,6 +589,97 @@ "Expired" ] }, + "PeerStatusV2": { + "description": "Status of a DDM peer with state expressed as durations.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "NoContact" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Init" + ] + }, + "value": { + "$ref": "#/components/schemas/Duration" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Solicit" + ] + }, + "value": { + "$ref": "#/components/schemas/Duration" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Exchange" + ] + }, + "value": { + "$ref": "#/components/schemas/Duration" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Expired" + ] + }, + "value": { + "$ref": "#/components/schemas/Duration" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, "RouterKind": { "type": "integer", "enum": [