Skip to content

Commit 183f02d

Browse files
[multicast, refactor]: add ddm-peers omdb view, shift member writes to mg-lower
Under RFD 488 (incoming changes), and akin to unicast tunnel routes, mg-lower and ddmd derive underlay members directly from DDM peer subscriptions, so Nexus no longer resolves live sled-to-switch-port from inventory or DDM peers. The DDM client, instead, becomes a read-only peer view, and the reconciler's member->"Joined" path is a pure DB CAS plus best-effort sled-agent propagation, decoupled from MGS, the switch zone, and DPD member updates. Includes: - Adds the `omdb nexus multicast ddm-peers` view backed by the read-only DDM client, with a new internal-dns resolver helper (`lookup_all_socket_v6_by_target`) so per-switch clients correlate sockets back to their SRV targets - Adds reconciler config knobs to `MulticastGroupReconcilerConfig`: `group_concurrency_limit`, `member_concurrency_limit`, and `orphan_grace_secs` (grace before an orphaned empty "Creating" group is reaped), wired through the example, SMF, and test configs - Shifts underlay member writes to mg-lower, trimming the reconciler's member state machine and switch-zone coordination accordingly - Splits the multicast dataplane client into focused modules: `mrib.rs` for per-pass MRIB route reconciliation and `switch_zone.rs` for the MGD/DDM switch-zone clients, leaving `dataplane.rs` to DPD group operations - Adds schema migration `multicast-member-origin` (schema version 267): a new `multicast_group_member_origin` enum (`static` / `igmp_snooped`) and a `membership_origin` column on multicast group members defaulting to `static`. Groundwork for the RFD 488 static and IGMP/MLD paths, distinguishing which rows a future soft-state reaper may expire - Aligns the simulated sled-agent's `set_mcast_fwd` with real OPTE's additive next-hop semantics, and reads back through the `list_mcast_fwd` API in the multi-sled test - Also, the sled-agent multicast membership API was renamed `instance_join/leave_multicast_group` → `vmm_join/leave_multicast_group` (for sim + client/HTTP surface). - Corrects stale reconciler, switch-zone, and API-version docs throughout - Documents the test boundary: coverage stops at the Omicron-owned edge, while DDM exchange and DPD member programming are maghemite/dendrite's domain - Cleans up the now-dead inventory-based test scaffolding
1 parent 66658c1 commit 183f02d

77 files changed

Lines changed: 5321 additions & 4090 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 11 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -615,9 +615,10 @@ ntp-admin-client = { path = "clients/ntp-admin-client" }
615615
ntp-admin-v1-client = { path = "clients/ntp-admin-v1-client" }
616616
ntp-admin-types = { path = "ntp-admin/types" }
617617
ntp-admin-types-versions = { path = "ntp-admin/types/versions" }
618-
mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "5aaa7c9bc2d0e541d29604453dd98cb32676b347" }
619-
mg-api-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "5aaa7c9bc2d0e541d29604453dd98cb32676b347" }
620-
ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "5aaa7c9bc2d0e541d29604453dd98cb32676b347" }
618+
mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "4cc569836dc1c5d13a5f2396de310220eafee2fe" }
619+
mg-api-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "4cc569836dc1c5d13a5f2396de310220eafee2fe" }
620+
ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "4cc569836dc1c5d13a5f2396de310220eafee2fe" }
621+
ddm-api-types-versions = { git = "https://github.com/oxidecomputer/maghemite", rev = "4cc569836dc1c5d13a5f2396de310220eafee2fe" }
621622
multimap = "0.10.1"
622623
nexus-auth = { path = "nexus/auth" }
623624
nexus-background-task-interface = { path = "nexus/background-task-interface" }

clients/ddm-admin-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ omicron-common.workspace = true
2020
sled-hardware-types.workspace = true
2121
omicron-workspace-hack.workspace = true
2222
ddm-admin-client.workspace = true
23+
ddm-api-types-versions.workspace = true
2324
uuid.workspace = true
2425
slog-error-chain.workspace = true

clients/ddm-admin-client/src/lib.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
// Copyright 2023 Oxide Computer Company
5+
// Copyright 2026 Oxide Computer Company
66

77
#![allow(clippy::redundant_closure_call)]
88
#![allow(clippy::needless_lifetimes)]
99
#![allow(clippy::match_single_binding)]
1010
#![allow(clippy::clone_on_copy)]
1111
pub use ddm_admin_client::Error;
1212
pub use ddm_admin_client::types;
13+
pub use ddm_api_types_versions::latest::db::{
14+
MulticastRoute, PeerInfo, PeerStatus,
15+
};
16+
pub use ddm_api_types_versions::latest::net::MulticastOrigin;
1317

1418
use ddm_admin_client::Client as InnerClient;
1519
use either::Either;
@@ -105,6 +109,38 @@ impl Client {
105109
self.inner.enable_stats(request).await.map(|resp| resp.into_inner())
106110
}
107111

112+
/// Returns DDM peer information including interface names.
113+
///
114+
/// The `if_name` field on each peer provides a live sled-to-port
115+
/// mapping, identifying which switch port a peer sled is connected
116+
/// through (e.g., `"tfportrear0_0"`).
117+
pub async fn get_peers(
118+
&self,
119+
) -> Result<std::collections::HashMap<String, PeerInfo>, Error<types::Error>>
120+
{
121+
self.inner.get_peers().await.map(|resp| resp.into_inner())
122+
}
123+
124+
/// Returns multicast routes learned from DDM peers.
125+
///
126+
/// Each route includes the origin (overlay/underlay mapping),
127+
/// the nexthop peer that advertised it, and the path vector.
128+
pub async fn get_multicast_groups(
129+
&self,
130+
) -> Result<Vec<MulticastRoute>, Error<types::Error>> {
131+
self.inner.get_multicast_groups().await.map(|resp| resp.into_inner())
132+
}
133+
134+
/// Returns multicast origins that this DDM instance is advertising.
135+
pub async fn get_originated_multicast_groups(
136+
&self,
137+
) -> Result<Vec<MulticastOrigin>, Error<types::Error>> {
138+
self.inner
139+
.get_originated_multicast_groups()
140+
.await
141+
.map(|resp| resp.into_inner())
142+
}
143+
108144
/// Returns the addresses of connected sleds.
109145
///
110146
/// Note: These sleds have not yet been verified.

common/src/api/external/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2538,6 +2538,8 @@ impl Vni {
25382538
///
25392539
/// This is a low-numbered VNI to avoid colliding with user VNIs.
25402540
/// However, it is not in the Oxide-reserved range yet.
2541+
///
2542+
/// Should match `oxide_vpc::api::DEFAULT_MULTICAST_VNI`.
25412543
pub const DEFAULT_MULTICAST_VNI: Self = Self(77);
25422544

25432545
/// Oxide reserves a slice of initial VNIs for its own use.

dev-tools/ls-apis/tests/api_dependencies.out

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Crucible Pantry (client: crucible-pantry-client)
2929
Maghemite DDM Admin (client: ddm-admin-client)
3030
consumed by: installinator (omicron/installinator) via 1 path
3131
consumed by: mgd (maghemite/mgd) via 1 path
32+
consumed by: omicron-nexus (omicron/nexus) via 1 path
3233
consumed by: omicron-sled-agent (omicron/sled-agent) via 1 path
3334
consumed by: sled-agent-rack-setup (omicron/sled-agent/rack-setup) via 1 path [embedded in omicron-sled-agent; rack-init only]
3435
consumed by: wicketd (omicron/wicketd) via 1 path

dev-tools/omdb/src/bin/omdb/nexus.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ enum NexusCommands {
167167
FetchOmdb(FetchOmdbArgs),
168168
/// print information about pending MGS updates
169169
MgsUpdates,
170+
/// inspect multicast state
171+
Multicast(MulticastArgs),
170172
/// interact with oximeter read policy
171173
OximeterReadPolicy(OximeterReadPolicyArgs),
172174
/// view or modify the quiesce status
@@ -450,6 +452,26 @@ struct FetchOmdbArgs {
450452
output: Utf8PathBuf,
451453
}
452454

455+
#[derive(Debug, Args)]
456+
struct MulticastArgs {
457+
#[command(subcommand)]
458+
command: MulticastCommands,
459+
}
460+
461+
#[derive(Debug, Subcommand)]
462+
enum MulticastCommands {
463+
/// List DDM underlay peers observed across switch zones
464+
DdmPeers(MulticastDdmPeersArgs),
465+
}
466+
467+
#[derive(Debug, Args)]
468+
struct MulticastDdmPeersArgs {
469+
/// Show only peers that become multicast underlay members
470+
/// (DDM session in `Exchange` with a switch rear-port interface).
471+
#[arg(long)]
472+
mcast: bool,
473+
}
474+
453475
#[derive(Debug, Args)]
454476
struct OximeterReadPolicyArgs {
455477
#[command(subcommand)]
@@ -825,6 +847,14 @@ impl NexusArgs {
825847

826848
NexusCommands::MgsUpdates => cmd_nexus_mgs_updates(&client).await,
827849

850+
NexusCommands::Multicast(MulticastArgs { command }) => {
851+
match command {
852+
MulticastCommands::DdmPeers(args) => {
853+
cmd_nexus_multicast_ddm_peers(&client, args).await
854+
}
855+
}
856+
}
857+
828858
NexusCommands::OximeterReadPolicy(OximeterReadPolicyArgs {
829859
command,
830860
}) => match command {
@@ -4713,6 +4743,68 @@ async fn cmd_nexus_mgs_updates(
47134743
Ok(())
47144744
}
47154745

4746+
async fn cmd_nexus_multicast_ddm_peers(
4747+
client: &nexus_lockstep_client::Client,
4748+
args: &MulticastDdmPeersArgs,
4749+
) -> Result<(), anyhow::Error> {
4750+
let view = client
4751+
.multicast_ddm_peers()
4752+
.await
4753+
.context("fetching multicast DDM peers")?
4754+
.into_inner();
4755+
4756+
let mut peers = view.peers;
4757+
if args.mcast {
4758+
// Members are derived only from peers in DDM `Exchange` on a switch
4759+
// rear-port interface (see the multicast members background task).
4760+
peers.retain(|peer| {
4761+
peer.status
4762+
== nexus_lockstep_client::types::MulticastDdmPeerStatus::Exchange
4763+
&& peer.if_name.is_some()
4764+
});
4765+
}
4766+
4767+
if peers.is_empty() {
4768+
println!("no DDM peers reported by any switch zone");
4769+
return Ok(());
4770+
}
4771+
4772+
#[derive(Tabled)]
4773+
#[tabled(rename_all = "SCREAMING_SNAKE_CASE")]
4774+
struct PeerRow {
4775+
switch: String,
4776+
addr: String,
4777+
host: String,
4778+
status: String,
4779+
duration: String,
4780+
if_name: String,
4781+
}
4782+
4783+
peers.sort_by(|a, b| (&a.switch, &a.addr).cmp(&(&b.switch, &b.addr)));
4784+
let rows = peers.into_iter().map(|peer| PeerRow {
4785+
switch: peer.switch,
4786+
addr: peer.addr.to_string(),
4787+
host: peer.host,
4788+
status: peer.status.to_string(),
4789+
duration: format!(
4790+
"{:?}",
4791+
std::time::Duration::new(
4792+
peer.status_duration.secs,
4793+
peer.status_duration.nanos,
4794+
)
4795+
),
4796+
if_name: peer.if_name.unwrap_or_else(|| "-".to_string()),
4797+
});
4798+
4799+
let table = tabled::Table::new(rows)
4800+
.with(tabled::settings::Style::empty())
4801+
.with(tabled::settings::Padding::new(0, 1, 0, 0))
4802+
.to_string();
4803+
println!("{table}");
4804+
4805+
Ok(())
4806+
}
4807+
47164808
async fn cmd_nexus_clickhouse_policy_set(
47174809
client: &nexus_lockstep_client::Client,
47184810
args: &ClickhousePolicySetArgs,

dev-tools/omdb/tests/test_multicast.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ use nexus_test_utils::resource_helpers::{
2222
create_project, link_ip_pool, object_put_upsert, objects_list_page_authz,
2323
};
2424
use nexus_test_utils_macros::nexus_test;
25-
use nexus_types::external_api::instance::Instance;
26-
use nexus_types::external_api::instance::InstanceNetworkInterfaceAttachment;
25+
use nexus_types::external_api::instance::{
26+
Instance, InstanceNetworkInterfaceAttachment,
27+
};
2728
use nexus_types::external_api::multicast::{
2829
InstanceMulticastGroupJoin, MulticastGroup, MulticastGroupMember,
2930
};

dev-tools/omdb/tests/usage_errors.out

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,7 @@ Commands:
10311031
clickhouse-policy interact with clickhouse policy
10321032
fetch-omdb fetch an omdb binary associated with an active Nexus
10331033
mgs-updates print information about pending MGS updates
1034+
multicast inspect multicast state
10341035
oximeter-read-policy interact with oximeter read policy
10351036
quiesce view or modify the quiesce status
10361037
reconfigurator-config interact with reconfigurator config

illumos-utils/src/opte/illumos.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ pub enum Error {
7676
"address {0} is not within the underlay multicast subnet (ff04::/16)"
7777
)]
7878
InvalidMcastUnderlay(Ipv6Addr),
79+
80+
#[error(
81+
"failed to install NIC multicast MAC filter for underlay {0}, \
82+
caller should retry"
83+
)]
84+
UnderlayMcastJoinFailed(Ipv6Addr),
7985
}
8086

8187
/// Delete all xde devices on the system.

0 commit comments

Comments
 (0)