Skip to content

Commit 5050964

Browse files
[ddm-api] PUT /peer for state-machine-bypassing peer injection
Fills out the testing work for `ddmd` in Omicron. Includes: - Adds a `PUT /peer` admin endpoint, gated at v2.0.0 (MULTICAST_SUPPORT), that injects a `PeerInfo` directly into the in-memory peer table at a supplied interface index. Intended for fixtures running ddmd with `--no-state-machine`. Note that in typical operations the discovery handler will overwrite any directly-injected entry the next time a peer is observed on that interface. - Extracts the `get_peers` and `put_peer` impl bodies into `do_get_peers` / `do_put_peer` free functions in `ddm/src/admin.rs`, mirroring `mgd::bgp_admin::do_bgp_apply`, so the endpoints can be exercised in-process. - Adds `tests::put_peer_round_trips` over a tempdir-backed setup covering the round-trip and same interface-index overwrite invariant. - Adds a `put_peer` synthetic injection into the `run_trio_tests` smoke test that exercises the full HTTP path through the generated client against a running ddmd.
1 parent 8bcd5fe commit 5050964

10 files changed

Lines changed: 215 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ddm-api/src/lib.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,23 @@ pub trait DdmAdminApi {
7272
params: Path<latest::admin::ExpirePathParams>,
7373
) -> Result<HttpResponseUpdatedNoContent, HttpError>;
7474

75+
/// Set peer information for a given interface index, bypassing the state machine.
76+
///
77+
/// Intended for test fixtures that run `ddmd` with `--no-state-machine`.
78+
/// In a normal run, discovery writes peer entries keyed by interface
79+
/// index whenever it processes an advertisement, so any directly-injected
80+
/// entry for an active interface will be overwritten the next time a
81+
/// peer is observed there.
82+
#[endpoint {
83+
method = PUT,
84+
path = "/peer",
85+
versions = VERSION_MULTICAST_SUPPORT..
86+
}]
87+
async fn put_peer(
88+
ctx: RequestContext<Self::Context>,
89+
request: TypedBody<latest::admin::PutPeerRequest>,
90+
) -> Result<HttpResponseUpdatedNoContent, HttpError>;
91+
7592
#[endpoint { method = GET, path = "/originated" }]
7693
async fn get_originated(
7794
ctx: RequestContext<Self::Context>,

ddm-types/versions/src/latest.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod admin {
88
pub use crate::v1::admin::EnableStatsRequest;
99
pub use crate::v1::admin::ExpirePathParams;
1010
pub use crate::v1::admin::PrefixMap;
11+
pub use crate::v2::admin::PutPeerRequest;
1112
}
1213

1314
pub mod db {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
use schemars::JsonSchema;
6+
use serde::{Deserialize, Serialize};
7+
8+
use super::db::PeerInfo;
9+
10+
/// Body for `PUT /peer`. Sets `info` at the slot keyed by `if_index`
11+
/// (interface index) in the in-memory peer map.
12+
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
13+
pub struct PutPeerRequest {
14+
pub if_index: u32,
15+
pub info: PeerInfo,
16+
}

ddm-types/versions/src/multicast_support/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
//! Types from API version 2 (MULTICAST_SUPPORT) that add multicast
66
//! group management to the DDM admin API.
77
8+
pub mod admin;
89
pub mod db;
910
pub mod exchange;

ddm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition = "2024"
55

66
[dev-dependencies]
77
pretty_assertions.workspace = true
8+
tempfile = "3"
89

910
[dependencies]
1011
slog.workspace = true

ddm/src/admin.rs

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use crate::db::Db;
66
use crate::sm::{AdminEvent, Event, PrefixSet, SmContext};
77
use ddm_api::DdmAdminApi;
88
use ddm_api::ddm_admin_api_mod;
9-
use ddm_types::admin::{EnableStatsRequest, ExpirePathParams, PrefixMap};
9+
use ddm_types::admin::{
10+
EnableStatsRequest, ExpirePathParams, PrefixMap, PutPeerRequest,
11+
};
1012
use ddm_types::db::{MulticastRoute, PeerInfo, TunnelRoute};
1113
use ddm_types::exchange::PathVector;
1214
use dropshot::ApiDescription;
@@ -112,8 +114,7 @@ impl DdmAdminApi for DdmAdminApiImpl {
112114
async fn get_peers(
113115
ctx: RequestContext<Self::Context>,
114116
) -> Result<HttpResponseOk<HashMap<u32, PeerInfo>>, HttpError> {
115-
let ctx = lock!(ctx.context());
116-
Ok(HttpResponseOk(ctx.db.peers()))
117+
Ok(HttpResponseOk(do_get_peers(ctx.context())))
117118
}
118119

119120
async fn get_peers_v1(
@@ -151,6 +152,14 @@ impl DdmAdminApi for DdmAdminApiImpl {
151152
Ok(HttpResponseUpdatedNoContent())
152153
}
153154

155+
async fn put_peer(
156+
ctx: RequestContext<Self::Context>,
157+
request: TypedBody<PutPeerRequest>,
158+
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
159+
do_put_peer(ctx.context(), request.into_inner());
160+
Ok(HttpResponseUpdatedNoContent())
161+
}
162+
154163
async fn get_originated(
155164
ctx: RequestContext<Self::Context>,
156165
) -> Result<HttpResponseOk<HashSet<Ipv6Net>>, HttpError> {
@@ -481,3 +490,93 @@ pub fn api_description()
481490
{
482491
ddm_admin_api_mod::api_description::<DdmAdminApiImpl>()
483492
}
493+
494+
/// Snapshot the current peer table, keyed by interface index.
495+
pub(crate) fn do_get_peers(
496+
ctx: &Arc<Mutex<HandlerContext>>,
497+
) -> HashMap<u32, PeerInfo> {
498+
let ctx = lock!(ctx);
499+
ctx.db.peers()
500+
}
501+
502+
/// Insert or replace the peer entry at `request.if_index`. Tests bypass
503+
/// the dropshot endpoint and call this directly; production goes through
504+
/// [`DdmAdminApiImpl::put_peer`].
505+
pub(crate) fn do_put_peer(
506+
ctx: &Arc<Mutex<HandlerContext>>,
507+
request: PutPeerRequest,
508+
) {
509+
let PutPeerRequest { if_index, info } = request;
510+
let ctx = lock!(ctx);
511+
ctx.db.set_peer(if_index, info);
512+
}
513+
514+
#[cfg(test)]
515+
mod tests {
516+
use super::{HandlerContext, RouterStats, do_get_peers, do_put_peer};
517+
use crate::db::Db;
518+
use ddm_types::admin::PutPeerRequest;
519+
use ddm_types::db::{PeerInfo, PeerStatus, RouterKind};
520+
use slog::{Discard, Logger, o};
521+
use std::sync::{Arc, Mutex};
522+
use tempfile::TempDir;
523+
524+
fn build_context(tmpdir: &TempDir) -> Arc<Mutex<HandlerContext>> {
525+
let log = Logger::root(Discard, o!());
526+
let db_path = tmpdir.path().join("ddm").to_str().unwrap().to_string();
527+
let db = Db::new(&db_path, log.clone()).expect("open db");
528+
Arc::new(Mutex::new(HandlerContext {
529+
event_channels: vec![],
530+
db,
531+
stats: Arc::new(RouterStats::default()),
532+
peers: vec![],
533+
stats_handler: Arc::new(Mutex::new(None)),
534+
log,
535+
}))
536+
}
537+
538+
#[test]
539+
fn put_peer_round_trips() {
540+
let tmpdir = TempDir::new().expect("tempdir");
541+
let ctx = build_context(&tmpdir);
542+
543+
let info = PeerInfo {
544+
status: PeerStatus::Active,
545+
addr: "fd00::1".parse().unwrap(),
546+
host: "test-sled-1".to_string(),
547+
kind: RouterKind::Server,
548+
if_name: Some("tfportrear0_0".to_string()),
549+
};
550+
551+
do_put_peer(
552+
&ctx,
553+
PutPeerRequest {
554+
if_index: 7,
555+
info: info.clone(),
556+
},
557+
);
558+
559+
let peers = do_get_peers(&ctx);
560+
assert_eq!(peers.len(), 1);
561+
let got = peers.get(&7).expect("peer at if_index 7");
562+
assert_eq!(got, &info);
563+
564+
// Overwriting at the same `if_index` replaces the entry rather
565+
// than creating a second one.
566+
let info2 = PeerInfo {
567+
addr: "fd00::2".parse().unwrap(),
568+
host: "test-sled-1-replaced".to_string(),
569+
..info
570+
};
571+
do_put_peer(
572+
&ctx,
573+
PutPeerRequest {
574+
if_index: 7,
575+
info: info2.clone(),
576+
},
577+
);
578+
let peers = do_get_peers(&ctx);
579+
assert_eq!(peers.len(), 1, "overwrite at same if_index keeps map size",);
580+
assert_eq!(peers[&7].addr, info2.addr);
581+
}
582+
}

openapi/ddm-admin/ddm-admin-2.0.0-8aeda2.json renamed to openapi/ddm-admin/ddm-admin-2.0.0-0cfd90.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,34 @@
223223
}
224224
}
225225
},
226+
"/peer": {
227+
"put": {
228+
"summary": "Set peer information for a given interface index, bypassing the state machine.",
229+
"description": "Intended for test fixtures that run `ddmd` with `--no-state-machine`. In a normal run, discovery writes peer entries keyed by interface index whenever it processes an advertisement, so any directly-injected entry for an active interface will be overwritten the next time a peer is observed there.",
230+
"operationId": "put_peer",
231+
"requestBody": {
232+
"content": {
233+
"application/json": {
234+
"schema": {
235+
"$ref": "#/components/schemas/PutPeerRequest"
236+
}
237+
}
238+
},
239+
"required": true
240+
},
241+
"responses": {
242+
"204": {
243+
"description": "resource updated"
244+
},
245+
"4XX": {
246+
"$ref": "#/components/responses/Error"
247+
},
248+
"5XX": {
249+
"$ref": "#/components/responses/Error"
250+
}
251+
}
252+
}
253+
},
226254
"/peers": {
227255
"get": {
228256
"operationId": "get_peers",
@@ -717,6 +745,24 @@
717745
"Expired"
718746
]
719747
},
748+
"PutPeerRequest": {
749+
"description": "Body for `PUT /peer`. Sets `info` at the slot keyed by `if_index` (interface index) in the in-memory peer map.",
750+
"type": "object",
751+
"properties": {
752+
"if_index": {
753+
"type": "integer",
754+
"format": "uint32",
755+
"minimum": 0
756+
},
757+
"info": {
758+
"$ref": "#/components/schemas/PeerInfo"
759+
}
760+
},
761+
"required": [
762+
"if_index",
763+
"info"
764+
]
765+
},
720766
"RouterKind": {
721767
"type": "integer",
722768
"enum": [
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ddm-admin-2.0.0-8aeda2.json
1+
ddm-admin-2.0.0-0cfd90.json

tests/src/ddm.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
use anyhow::{Result, anyhow};
66
use ddm_admin_client::Client;
7-
use ddm_admin_client::types::{MulticastOrigin, TunnelOrigin, Vni};
7+
use ddm_admin_client::types::{
8+
MulticastOrigin, PeerInfo, PeerStatus, PutPeerRequest, RouterKind,
9+
TunnelOrigin, Vni,
10+
};
811
use slog::{Drain, Logger};
912
use std::env;
1013
use std::net::Ipv6Addr;
@@ -462,6 +465,31 @@ async fn run_trio_tests(
462465

463466
println!("initial peering test passed");
464467

468+
// PUT /peer smoke against a running ddmd. Use an unused interface
469+
// index so the live discovery handler does not race the injection
470+
// on a real interface.
471+
let synthetic = PeerInfo {
472+
status: PeerStatus::Active,
473+
addr: "fd00::dead:beef".parse().unwrap(),
474+
host: "synthetic".to_string(),
475+
// RouterKind is integer-encoded in the generated client schema;
476+
// 0 is `Server`. See ddm-types::initial::db::RouterKind.
477+
kind: RouterKind::try_from(0_i64).unwrap(),
478+
if_name: Some("synthetic0".to_string()),
479+
};
480+
481+
t1.put_peer(&PutPeerRequest {
482+
if_index: 9999,
483+
info: synthetic.clone(),
484+
})
485+
.await?;
486+
487+
wait_for_eq!(t1.get_peers().await.map_or(99, |x| x.len()), 3);
488+
let peers = t1.get_peers().await?;
489+
assert_eq!(peers["9999"].host, "synthetic");
490+
491+
println!("put_peer synthetic injection passed");
492+
465493
s1.advertise_prefixes(&vec!["fd00:1::/64".parse().unwrap()])
466494
.await?;
467495

0 commit comments

Comments
 (0)