diff --git a/nexus/db-model/src/multicast_group.rs b/nexus/db-model/src/multicast_group.rs index 6c9deea3de4..32feafb0ec5 100644 --- a/nexus/db-model/src/multicast_group.rs +++ b/nexus/db-model/src/multicast_group.rs @@ -91,7 +91,6 @@ use nexus_db_schema::schema::{ use nexus_types::external_api::views; use nexus_types::identity::Resource as IdentityResource; use omicron_common::api::external::{self, IdentityMetadata}; -use omicron_common::vlan::VlanID; use omicron_uuid_kinds::SledKind; use crate::typed_uuid::DbTypedUuid; @@ -180,28 +179,6 @@ pub struct ExternalMulticastGroup { /// Source IP addresses for Source-Specific Multicast (SSM). /// Empty array means any source is allowed. pub source_ips: Vec, - /// Multicast VLAN (MVLAN) for egress multicast traffic to upstream networks. - /// - /// When specified, this VLAN ID is passed to switches (via DPD) as part of - /// the `ExternalForwarding` configuration to tag multicast packets leaving - /// the rack. This enables multicast traffic to traverse VLAN-segmented - /// upstream networks (e.g., peering with external multicast sources/receivers - /// on specific VLANs). - /// - /// The MVLAN value is sent to switches during group creation/updates and - /// controls VLAN tagging for egress traffic only; it does not affect ingress - /// multicast traffic received by the rack. Switch port selection for egress - /// traffic remains pending (see TODOs in `nexus/src/app/multicast/dataplane.rs`). - /// - /// Valid range when specified: 2-4094 (IEEE 802.1Q; Dendrite requires >= 2). - /// - /// Database Type: i16 (INT2) - this field uses `i16` (INT2) for storage - /// efficiency, unlike other VLAN columns in the schema which use `SqlU16` - /// (forcing INT4). Direct `i16` is appropriate here since VLANs fit in - /// INT2's range. - /// - /// TODO(multicast): Remove mvlan field - being deprecated from multicast groups - pub mvlan: Option, /// Associated underlay group for NAT. /// Initially None in ["Creating"](MulticastGroupState::Creating) state, /// populated by reconciler when group becomes ["Active"](MulticastGroupState::Active). @@ -300,21 +277,9 @@ pub struct MulticastGroupMember { // Conversions to external API views -impl TryFrom for views::MulticastGroup { - type Error = external::Error; - - fn try_from(group: ExternalMulticastGroup) -> Result { - let mvlan = group - .mvlan - .map(|vlan| VlanID::new(vlan as u16)) - .transpose() - .map_err(|e| { - external::Error::internal_error(&format!( - "invalid VLAN ID: {e:#}" - )) - })?; - - Ok(views::MulticastGroup { +impl From for views::MulticastGroup { + fn from(group: ExternalMulticastGroup) -> Self { + views::MulticastGroup { identity: group.identity(), multicast_ip: group.multicast_ip.ip(), source_ips: group @@ -322,10 +287,9 @@ impl TryFrom for views::MulticastGroup { .into_iter() .map(|ip| ip.ip()) .collect(), - mvlan, ip_pool_id: group.ip_pool_id, state: group.state.to_string(), - }) + } } } @@ -367,7 +331,6 @@ pub struct IncompleteExternalMulticastGroup { // Optional address requesting that a specific multicast IP address be // allocated or provided pub explicit_address: Option, - pub mvlan: Option, pub vni: Vni, pub tag: Option, } @@ -381,7 +344,6 @@ pub struct IncompleteExternalMulticastGroupParams { pub ip_pool_id: Uuid, pub explicit_address: Option, pub source_ips: Vec, - pub mvlan: Option, pub vni: Vni, pub tag: Option, } @@ -397,7 +359,6 @@ impl IncompleteExternalMulticastGroup { ip_pool_id: params.ip_pool_id, source_ips: params.source_ips, explicit_address: params.explicit_address.map(|ip| ip.into()), - mvlan: params.mvlan, vni: params.vni, tag: params.tag, } diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index ad43b14b5f9..b59611a57e9 100644 --- a/nexus/db-model/src/schema_versions.rs +++ b/nexus/db-model/src/schema_versions.rs @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock}; /// /// This must be updated when you change the database schema. Refer to /// schema/crdb/README.adoc in the root of this repository for details. -pub const SCHEMA_VERSION: Version = Version::new(213, 0, 0); +pub const SCHEMA_VERSION: Version = Version::new(214, 0, 0); /// List of all past database schema versions, in *reverse* order /// @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock> = LazyLock::new(|| { // | leaving the first copy as an example for the next person. // v // KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"), + KnownVersion::new(214, "multicast-drop-mvlan"), KnownVersion::new(213, "multicast-member-ip-and-indexes"), KnownVersion::new(212, "local-storage-disk-type"), KnownVersion::new(211, "blueprint-sled-config-subnet"), diff --git a/nexus/db-queries/src/db/datastore/multicast/groups.rs b/nexus/db-queries/src/db/datastore/multicast/groups.rs index 74859f4f2bd..bf06759bc8c 100644 --- a/nexus/db-queries/src/db/datastore/multicast/groups.rs +++ b/nexus/db-queries/src/db/datastore/multicast/groups.rs @@ -34,7 +34,6 @@ use omicron_common::api::external::{ IdentityMetadataCreateParams, ListResultVec, LookupResult, LookupType, ResourceType, UpdateResult, }; -use omicron_common::vlan::VlanID; use omicron_uuid_kinds::{GenericUuid, MulticastGroupUuid}; use crate::authz; @@ -56,7 +55,6 @@ pub(crate) struct MulticastGroupAllocationParams { pub ip: Option, pub pool: Option, pub source_ips: Option>, - pub mvlan: Option, } impl DataStore { @@ -165,7 +163,6 @@ impl DataStore { ip: params.multicast_ip, pool: authz_pool, source_ips: params.source_ips.clone(), - mvlan: params.mvlan, }, ) .await @@ -464,7 +461,6 @@ impl DataStore { ip_pool_id: authz_pool.id(), explicit_address: params.ip, source_ips: source_ip_networks, - mvlan: params.mvlan.map(|vlan_id| u16::from(vlan_id) as i16), vni, // Set DPD tag to the group UUID to ensure uniqueness across lifecycle. // This prevents tag collision when group names are reused. @@ -826,7 +822,6 @@ mod tests { }, multicast_ip: None, source_ips: None, - mvlan: None, }; datastore .multicast_group_create(&opctx, ¶ms1, Some(authz_pool.clone())) @@ -841,7 +836,6 @@ mod tests { }, multicast_ip: None, source_ips: None, - mvlan: None, }; datastore .multicast_group_create(&opctx, ¶ms2, Some(authz_pool.clone())) @@ -856,7 +850,6 @@ mod tests { }, multicast_ip: None, source_ips: None, - mvlan: None, }; let result3 = datastore .multicast_group_create(&opctx, ¶ms3, Some(authz_pool.clone())) @@ -929,7 +922,6 @@ mod tests { }, multicast_ip: None, source_ips: None, - mvlan: None, }; let group_default = datastore @@ -954,7 +946,6 @@ mod tests { }, multicast_ip: None, source_ips: None, - mvlan: None, }; let group_explicit = datastore .multicast_group_create(&opctx, ¶ms_explicit, None) @@ -1081,7 +1072,6 @@ mod tests { }, multicast_ip: Some("224.1.3.3".parse().unwrap()), source_ips: None, - mvlan: None, }; let external_group = datastore @@ -1178,7 +1168,6 @@ mod tests { }, multicast_ip: Some("224.3.1.5".parse().unwrap()), source_ips: None, - mvlan: None, }; let group = datastore @@ -1643,7 +1632,6 @@ mod tests { }, multicast_ip: Some("224.3.1.5".parse().unwrap()), source_ips: None, - mvlan: None, }; let group = datastore @@ -1775,7 +1763,6 @@ mod tests { }, multicast_ip: None, // Let it allocate from pool source_ips: None, - mvlan: None, }; let group = datastore .multicast_group_create( @@ -1987,7 +1974,6 @@ mod tests { }, multicast_ip: Some(target_ip), source_ips: None, - mvlan: None, }; let group1 = datastore @@ -2014,7 +2000,6 @@ mod tests { }, multicast_ip: Some(target_ip), source_ips: None, - mvlan: None, }; let group2 = datastore @@ -2099,7 +2084,6 @@ mod tests { }, multicast_ip: None, source_ips: None, - mvlan: None, }; let group1 = datastore @@ -2116,7 +2100,6 @@ mod tests { }, multicast_ip: None, source_ips: None, - mvlan: None, }; let result2 = datastore @@ -2146,7 +2129,6 @@ mod tests { }, multicast_ip: None, source_ips: None, - mvlan: None, }; let group3 = datastore @@ -2232,7 +2214,6 @@ mod tests { }, multicast_ip: None, source_ips: None, - mvlan: None, }; let group = datastore @@ -2359,7 +2340,6 @@ mod tests { "10.0.0.1".parse().unwrap(), "10.0.0.2".parse().unwrap(), ]), - mvlan: None, }; let group = datastore @@ -2466,7 +2446,6 @@ mod tests { }, multicast_ip: Some("224.100.20.10".parse().unwrap()), source_ips: None, - mvlan: None, }; let params_2 = MulticastGroupCreate { @@ -2476,7 +2455,6 @@ mod tests { }, multicast_ip: Some("224.100.20.11".parse().unwrap()), source_ips: None, - mvlan: None, }; let params_3 = MulticastGroupCreate { @@ -2486,7 +2464,6 @@ mod tests { }, multicast_ip: Some("224.100.20.12".parse().unwrap()), source_ips: None, - mvlan: None, }; // Create groups (all are fleet-scoped) @@ -2594,7 +2571,6 @@ mod tests { }, multicast_ip: Some("224.100.30.5".parse().unwrap()), source_ips: None, - mvlan: None, }; // Create group - starts in "Creating" state diff --git a/nexus/db-queries/src/db/datastore/multicast/members.rs b/nexus/db-queries/src/db/datastore/multicast/members.rs index 6a74525ea0e..7df8909aea9 100644 --- a/nexus/db-queries/src/db/datastore/multicast/members.rs +++ b/nexus/db-queries/src/db/datastore/multicast/members.rs @@ -926,7 +926,6 @@ mod tests { multicast_ip: Some("224.10.1.6".parse().unwrap()), source_ips: None, // Pool resolved via authz_pool argument to datastore call - mvlan: None, }; let creating_group = datastore diff --git a/nexus/db-queries/src/db/pub_test_utils/multicast.rs b/nexus/db-queries/src/db/pub_test_utils/multicast.rs index 56934b90324..f6aa80dbe9e 100644 --- a/nexus/db-queries/src/db/pub_test_utils/multicast.rs +++ b/nexus/db-queries/src/db/pub_test_utils/multicast.rs @@ -194,7 +194,6 @@ pub async fn create_test_group_with_state( }, multicast_ip: Some(multicast_ip.parse().unwrap()), source_ips: None, - mvlan: None, }; let group = datastore diff --git a/nexus/db-queries/src/db/queries/external_multicast_group.rs b/nexus/db-queries/src/db/queries/external_multicast_group.rs index 833c13c94b7..e6de0808540 100644 --- a/nexus/db-queries/src/db/queries/external_multicast_group.rs +++ b/nexus/db-queries/src/db/queries/external_multicast_group.rs @@ -118,10 +118,6 @@ impl NextExternalMulticastGroup { } out.push_sql("]::inet[] AS source_ips, "); - // MVLAN for external uplink forwarding - out.push_bind_param::, Option>(&self.group.mvlan)?; - out.push_sql(" AS mvlan, "); - out.push_bind_param::, Option>(&None)?; out.push_sql(" AS underlay_group_id, "); @@ -274,10 +270,10 @@ impl QueryFragment for NextExternalMulticastGroup { out.push_sql("INSERT INTO "); schema::multicast_group::table.walk_ast(out.reborrow())?; out.push_sql( - " (id, name, description, time_created, time_modified, time_deleted, ip_pool_id, ip_pool_range_id, vni, multicast_ip, source_ips, mvlan, underlay_group_id, tag, state, version_added, version_removed) - SELECT id, name, description, time_created, time_modified, time_deleted, ip_pool_id, ip_pool_range_id, vni, multicast_ip, source_ips, mvlan, underlay_group_id, tag, state, version_added, version_removed FROM next_external_multicast_group + " (id, name, description, time_created, time_modified, time_deleted, ip_pool_id, ip_pool_range_id, vni, multicast_ip, source_ips, underlay_group_id, tag, state, version_added, version_removed) + SELECT id, name, description, time_created, time_modified, time_deleted, ip_pool_id, ip_pool_range_id, vni, multicast_ip, source_ips, underlay_group_id, tag, state, version_added, version_removed FROM next_external_multicast_group WHERE NOT EXISTS (SELECT 1 FROM previously_allocated_group) - RETURNING id, name, description, time_created, time_modified, time_deleted, ip_pool_id, ip_pool_range_id, vni, multicast_ip, source_ips, mvlan, underlay_group_id, tag, state, version_added, version_removed", + RETURNING id, name, description, time_created, time_modified, time_deleted, ip_pool_id, ip_pool_range_id, vni, multicast_ip, source_ips, underlay_group_id, tag, state, version_added, version_removed", ); out.push_sql("), "); @@ -288,9 +284,9 @@ impl QueryFragment for NextExternalMulticastGroup { // Return either the newly inserted or previously allocated group out.push_sql( - "SELECT id, name, description, time_created, time_modified, time_deleted, ip_pool_id, ip_pool_range_id, vni, multicast_ip, source_ips, mvlan, underlay_group_id, tag, state, version_added, version_removed FROM previously_allocated_group + "SELECT id, name, description, time_created, time_modified, time_deleted, ip_pool_id, ip_pool_range_id, vni, multicast_ip, source_ips, underlay_group_id, tag, state, version_added, version_removed FROM previously_allocated_group UNION ALL - SELECT id, name, description, time_created, time_modified, time_deleted, ip_pool_id, ip_pool_range_id, vni, multicast_ip, source_ips, mvlan, underlay_group_id, tag, state, version_added, version_removed FROM multicast_group", + SELECT id, name, description, time_created, time_modified, time_deleted, ip_pool_id, ip_pool_range_id, vni, multicast_ip, source_ips, underlay_group_id, tag, state, version_added, version_removed FROM multicast_group", ); Ok(()) diff --git a/nexus/db-schema/src/schema.rs b/nexus/db-schema/src/schema.rs index b6cc91c53db..2dd4c30b4e9 100644 --- a/nexus/db-schema/src/schema.rs +++ b/nexus/db-schema/src/schema.rs @@ -2778,7 +2778,6 @@ table! { vni -> Int4, multicast_ip -> Inet, source_ips -> Array, - mvlan -> Nullable, underlay_group_id -> Nullable, tag -> Nullable, state -> crate::enums::MulticastGroupStateEnum, diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index 436c3c7dbd8..49bcc0de045 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -1306,10 +1306,20 @@ pub trait NexusExternalApi { async fn v2025120300_multicast_group_list( rqctx: RequestContext, query_params: Query, - ) -> Result>, HttpError> - { - // Types are identical, delegate directly - Self::multicast_group_list(rqctx, query_params).await + ) -> Result< + HttpResponseOk>, + HttpError, + > { + match Self::multicast_group_list(rqctx, query_params).await { + Ok(page) => { + let new_page = ResultsPage { + next_page: page.0.next_page, + items: page.0.items.into_iter().map(Into::into).collect(), + }; + Ok(HttpResponseOk(new_page)) + } + Err(e) => Err(e), + } } /// List multicast groups. @@ -1337,7 +1347,7 @@ pub trait NexusExternalApi { async fn v2025120300_multicast_group_create( rqctx: RequestContext, new_group: TypedBody, - ) -> Result, HttpError>; + ) -> Result, HttpError>; /// Fetch a multicast group. /// @@ -1352,7 +1362,7 @@ pub trait NexusExternalApi { async fn v2025120300_multicast_group_view( rqctx: RequestContext, path_params: Path, - ) -> Result, HttpError>; + ) -> Result, HttpError>; /// Fetch a multicast group. /// @@ -1382,7 +1392,7 @@ pub trait NexusExternalApi { rqctx: RequestContext, path_params: Path, update_params: TypedBody, - ) -> Result, HttpError>; + ) -> Result, HttpError>; /// Delete a multicast group. /// @@ -1545,7 +1555,7 @@ pub trait NexusExternalApi { async fn v2025120300_lookup_multicast_group_by_ip( rqctx: RequestContext, path_params: Path, - ) -> Result, HttpError>; + ) -> Result, HttpError>; // Disks diff --git a/nexus/external-api/src/v2025120300.rs b/nexus/external-api/src/v2025120300.rs index 1e5c19f8fc2..0f9772c4acd 100644 --- a/nexus/external-api/src/v2025120300.rs +++ b/nexus/external-api/src/v2025120300.rs @@ -32,9 +32,9 @@ use nexus_types::external_api::params::UserData; use nexus_types::external_api::{params, views}; use nexus_types::multicast::MulticastGroupCreate as InternalMulticastGroupCreate; use omicron_common::api::external::{ - ByteCount, Hostname, IdentityMetadataCreateParams, + ByteCount, Hostname, IdentityMetadata, IdentityMetadataCreateParams, InstanceAutoRestartPolicy, InstanceCpuCount, InstanceCpuPlatform, Name, - NameOrId, Nullable, + NameOrId, Nullable, ObjectIdentity, }; use omicron_common::vlan::VlanID; use params::{ @@ -102,18 +102,18 @@ impl From for params::InstanceMulticastGroupPath { pub struct MulticastGroupCreate { pub name: Name, pub description: String, - /// The multicast IP address to allocate. If None, one will be allocated + /// The multicast IP address to allocate. If `None`, one will be allocated /// from the default pool. #[serde(default)] pub multicast_ip: Option, /// Source IP addresses for Source-Specific Multicast (SSM). /// - /// None uses default behavior (Any-Source Multicast). + /// `None` uses default behavior (Any-Source Multicast). /// Empty list explicitly allows any source (Any-Source Multicast). /// Non-empty list restricts to specific sources (SSM). #[serde(default)] pub source_ips: Option>, - /// Name or ID of the IP pool to allocate from. If None, uses the default + /// Name or ID of the IP pool to allocate from. If `None`, uses the default /// multicast pool. #[serde(default)] pub pool: Option, @@ -173,7 +173,6 @@ impl From for InternalMulticastGroupCreate { }, multicast_ip: old.multicast_ip, source_ips: old.source_ips, - mvlan: old.mvlan, } } } @@ -216,6 +215,44 @@ impl From for MulticastGroupMember { } } +/// View of a Multicast Group +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroup { + #[serde(flatten)] + pub identity: IdentityMetadata, + /// The multicast IP address held by this resource. + pub multicast_ip: IpAddr, + /// Source IP addresses for Source-Specific Multicast (SSM). + /// Empty array means any source is allowed. + pub source_ips: Vec, + /// Multicast VLAN (MVLAN) for egress multicast traffic to upstream networks. + /// `None` means no VLAN tagging on egress. + pub mvlan: Option, + /// The ID of the IP pool this resource belongs to. + pub ip_pool_id: Uuid, + /// Current state of the multicast group. + pub state: String, +} + +impl From for MulticastGroup { + fn from(v: views::MulticastGroup) -> Self { + Self { + identity: v.identity, + multicast_ip: v.multicast_ip, + source_ips: v.source_ips, + mvlan: None, // Field removed, always `None` for backwards compat + ip_pool_id: v.ip_pool_id, + state: v.state, + } + } +} + +impl ObjectIdentity for MulticastGroup { + fn identity(&self) -> &IdentityMetadata { + &self.identity + } +} + /// Create-time parameters for an `Instance`. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct InstanceCreate { @@ -333,7 +370,7 @@ pub struct InstanceUpdate { /// membership with the new set of groups. The instance will leave any /// groups not listed here and join any new groups that are specified. /// - /// If not provided (None), the instance's multicast group membership + /// If not provided (`None`), the instance's multicast group membership /// will not be changed. /// /// Accepts group names or UUIDs. Newer API versions also accept multicast diff --git a/nexus/src/app/background/tasks/multicast/groups.rs b/nexus/src/app/background/tasks/multicast/groups.rs index 1b094bff348..2adc56441fc 100644 --- a/nexus/src/app/background/tasks/multicast/groups.rs +++ b/nexus/src/app/background/tasks/multicast/groups.rs @@ -128,15 +128,6 @@ fn dpd_state_matches_sources( dpd_ips == db_sources_sorted } -/// Check if DPD vlan_id matches database mvlan. -fn dpd_state_matches_mvlan( - dpd_group: &dpd_client::types::MulticastGroupExternalResponse, - db_group: &MulticastGroup, -) -> bool { - let db_mvlan = db_group.mvlan.map(|v| v as u16); - dpd_group.external_forwarding.vlan_id == db_mvlan -} - /// Trait for processing different types of multicast groups trait GroupStateProcessor { /// Process a group in "Creating" state. @@ -513,10 +504,8 @@ impl MulticastGroupReconciler { let tag_matches = dpd_state_matches_tag(&dpd_group, group); let sources_match = dpd_state_matches_sources(&dpd_group, group); - let mvlan_matches = dpd_state_matches_mvlan(&dpd_group, group); - let needs_update = - !tag_matches || !sources_match || !mvlan_matches; + let needs_update = !tag_matches || !sources_match; if needs_update { debug!( @@ -524,8 +513,7 @@ impl MulticastGroupReconciler { "detected DPD state mismatch for active group"; "group_id" => %group.id(), "tag_matches" => tag_matches, - "sources_match" => sources_match, - "mvlan_matches" => mvlan_matches + "sources_match" => sources_match ); } diff --git a/nexus/src/app/multicast/dataplane.rs b/nexus/src/app/multicast/dataplane.rs index 047ab58b573..eef20d350bf 100644 --- a/nexus/src/app/multicast/dataplane.rs +++ b/nexus/src/app/multicast/dataplane.rs @@ -43,7 +43,6 @@ use internal_dns_resolver::Resolver; use nexus_db_model::{ExternalMulticastGroup, UnderlayMulticastGroup}; use nexus_types::identity::Resource; use omicron_common::api::external::{Error, SwitchLocation}; -use omicron_common::vlan::VlanID; use crate::app::dpd_clients; @@ -363,15 +362,6 @@ impl MulticastDataplaneClient { let dpd_clients = &self.dpd_clients; let tag = external_group.dpd_tag(); - // Convert MVLAN to u16 for DPD, validating through VlanID - let vlan_id = external_group - .mvlan - .map(|v| VlanID::new(v as u16)) - .transpose() - .map_err(|e| { - Error::internal_error(&format!("invalid VLAN ID: {e:#}")) - })? - .map(u16::from); let underlay_ip_admin = underlay_group.multicast_ip.ip().into_admin_scoped()?; let underlay_ipv6 = match underlay_group.multicast_ip.ip() { @@ -414,9 +404,14 @@ impl MulticastDataplaneClient { ) .await?; + // TODO: `vlan_id` is always `None` since we're not doing + // egress in MVP. When egress support is added, this will + // need to be populated from a dynamic configuration. let external_entry = MulticastGroupCreateExternalEntry { group_ip: external_group_ip, - external_forwarding: ExternalForwarding { vlan_id }, + external_forwarding: ExternalForwarding { + vlan_id: None, + }, internal_forwarding: InternalForwarding { nat_target: Some(nat_target), }, @@ -502,16 +497,6 @@ impl MulticastDataplaneClient { let dpd_clients = &self.dpd_clients; // Pre-compute shared data once - // Convert MVLAN to u16 for DPD, validating through VlanID - let vlan_id = params - .external_group - .mvlan - .map(|v| VlanID::new(v as u16)) - .transpose() - .map_err(|e| { - Error::internal_error(&format!("invalid VLAN ID: {e:#}")) - })? - .map(u16::from); let underlay_ip_admin = params.underlay_group.multicast_ip.ip().into_admin_scoped()?; let underlay_ipv6 = match params.underlay_group.multicast_ip.ip() { @@ -603,7 +588,12 @@ impl MulticastDataplaneClient { })?; // Prepare external update/create entries with pre-computed data - let external_forwarding = ExternalForwarding { vlan_id }; + // + // TODO: `vlan_id` is always `None` since we're not doing + // egress in MVP. When egress support is added, this will + // need to be populated from a dynamic configuration. + let external_forwarding = + ExternalForwarding { vlan_id: None }; let internal_forwarding = InternalForwarding { nat_target: Some(nat_target) }; diff --git a/nexus/src/app/multicast/mod.rs b/nexus/src/app/multicast/mod.rs index 009f3d99c7d..0e04419bcbf 100644 --- a/nexus/src/app/multicast/mod.rs +++ b/nexus/src/app/multicast/mod.rs @@ -37,6 +37,16 @@ //! //! All multicast groups use `DEFAULT_MULTICAST_VNI` (77), which is reserved for //! multicast and below the guest VNI range. +//! +//! # TODO: Scope +//! +//! The current implementation supports **ingress-into-the-rack multicast**: +//! external multicast sources sending traffic to instances within the rack. +//! Egress multicast (instances sending to external receivers out-of-the-rack) +//! is not yet supported. +//! +//! When egress support is added, (M)VLAN tagging will be introduced to enable +//! multicast traffic to traverse VLAN-segmented upstream networks. use std::collections::HashSet; use std::net::IpAddr; @@ -343,7 +353,6 @@ impl super::Nexus { }, multicast_ip: Some(ip), source_ips: source_ips.clone(), - mvlan: None, }; // Create the group; on conflict -> re-lookup @@ -412,7 +421,6 @@ impl super::Nexus { }, multicast_ip: None, source_ips: source_ips.clone(), - mvlan: None, }; // Create the group; on conflict -> re-lookup diff --git a/nexus/src/app/sagas/multicast_group_dpd_ensure.rs b/nexus/src/app/sagas/multicast_group_dpd_ensure.rs index 20f82973a01..5c502cc8db3 100644 --- a/nexus/src/app/sagas/multicast_group_dpd_ensure.rs +++ b/nexus/src/app/sagas/multicast_group_dpd_ensure.rs @@ -475,7 +475,6 @@ mod test { }, multicast_ip: Some(IpAddr::V4(Ipv4Addr::new(224, 70, 0, 100))), source_ips: None, - mvlan: None, }; let external_group = datastore diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index d16604e7ccf..09c17f870ef 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -2375,10 +2375,7 @@ impl NexusExternalApi for NexusExternalApiImpl { nexus.multicast_groups_list(&opctx, &paginated_by).await?; let results_page = ScanByNameOrId::results_page( &query, - groups - .into_iter() - .map(views::MulticastGroup::try_from) - .collect::, _>>()?, + groups.into_iter().map(views::MulticastGroup::from).collect(), &marker_for_name_or_id, )?; Ok(HttpResponseOk(results_page)) @@ -2393,7 +2390,8 @@ impl NexusExternalApi for NexusExternalApiImpl { async fn v2025120300_multicast_group_create( rqctx: RequestContext, new_group: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> + { let apictx = rqctx.context(); let handler = async { let opctx = @@ -2418,7 +2416,10 @@ impl NexusExternalApi for NexusExternalApiImpl { v1_params.into(); let group = nexus.multicast_group_create(&opctx, &internal_params).await?; - Ok(HttpResponseCreated(views::MulticastGroup::try_from(group)?)) + // Convert to v1 type (includes mvlan field) + Ok(HttpResponseCreated(v2025120300::MulticastGroup::from( + views::MulticastGroup::from(group), + ))) }; apictx .context @@ -2430,7 +2431,7 @@ impl NexusExternalApi for NexusExternalApiImpl { async fn v2025120300_multicast_group_view( rqctx: RequestContext, path_params: Path, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let apictx = rqctx.context(); let handler = async { let path: params::MulticastGroupPath = @@ -2445,7 +2446,10 @@ impl NexusExternalApi for NexusExternalApiImpl { .nexus .multicast_group_view(&opctx, &group_selector) .await?; - Ok(HttpResponseOk(views::MulticastGroup::try_from(group)?)) + // Convert to v1 type (includes mvlan field) + Ok(HttpResponseOk(v2025120300::MulticastGroup::from( + views::MulticastGroup::from(group), + ))) }; apictx .context @@ -2471,7 +2475,7 @@ impl NexusExternalApi for NexusExternalApiImpl { .nexus .multicast_group_view(&opctx, &group_selector) .await?; - Ok(HttpResponseOk(views::MulticastGroup::try_from(group)?)) + Ok(HttpResponseOk(views::MulticastGroup::from(group))) }; apictx .context @@ -2484,7 +2488,7 @@ impl NexusExternalApi for NexusExternalApiImpl { _rqctx: RequestContext, _path_params: Path, _update_params: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { // Multicast group update is deprecated in the implicit lifecycle model. // Groups are now created implicitly when members join and deleted when // all members leave. Properties like `source_ips` should be set when @@ -2806,7 +2810,7 @@ impl NexusExternalApi for NexusExternalApiImpl { async fn v2025120300_lookup_multicast_group_by_ip( rqctx: RequestContext, path_params: Path, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let apictx = rqctx.context(); let handler = async { let opctx = @@ -2822,7 +2826,10 @@ impl NexusExternalApi for NexusExternalApiImpl { }; let group = nexus.multicast_group_view(&opctx, &group_selector).await?; - Ok(HttpResponseOk(views::MulticastGroup::try_from(group)?)) + // Convert to v1 type (includes mvlan field) + Ok(HttpResponseOk(v2025120300::MulticastGroup::from( + views::MulticastGroup::from(group), + ))) }; apictx .context diff --git a/nexus/tests/integration_tests/multicast/groups.rs b/nexus/tests/integration_tests/multicast/groups.rs index 172323b8074..1aa31ee5bbb 100644 --- a/nexus/tests/integration_tests/multicast/groups.rs +++ b/nexus/tests/integration_tests/multicast/groups.rs @@ -1010,7 +1010,6 @@ fn assert_groups_eq(left: &MulticastGroup, right: &MulticastGroup) { assert_eq!(left.identity.description, right.identity.description); assert_eq!(left.multicast_ip, right.multicast_ip); assert_eq!(left.source_ips, right.source_ips); - assert_eq!(left.mvlan, right.mvlan); assert_eq!(left.ip_pool_id, right.ip_pool_id); } diff --git a/nexus/types/src/external_api/views.rs b/nexus/types/src/external_api/views.rs index 4f8a3cdc8db..ff74d96bd72 100644 --- a/nexus/types/src/external_api/views.rs +++ b/nexus/types/src/external_api/views.rs @@ -19,7 +19,6 @@ use omicron_common::api::external::{ Digest, Error, FailureDomain, IdentityMetadata, InstanceState, Name, Nullable, ObjectIdentity, SimpleIdentity, SimpleIdentityOrName, }; -use omicron_common::vlan::VlanID; use omicron_uuid_kinds::*; use oxnet::{Ipv4Net, Ipv6Net}; use schemars::JsonSchema; @@ -550,10 +549,6 @@ pub struct MulticastGroup { /// Source IP addresses for Source-Specific Multicast (SSM). /// Empty array means any source is allowed. pub source_ips: Vec, - /// Multicast VLAN (MVLAN) for egress multicast traffic to upstream networks. - /// None means no VLAN tagging on egress. - // TODO(multicast): Remove mvlan field - being deprecated from multicast groups - pub mvlan: Option, /// The ID of the IP pool this resource belongs to. pub ip_pool_id: Uuid, /// Current state of the multicast group. diff --git a/nexus/types/src/multicast.rs b/nexus/types/src/multicast.rs index 30d28499802..47f631d05c4 100644 --- a/nexus/types/src/multicast.rs +++ b/nexus/types/src/multicast.rs @@ -5,7 +5,6 @@ //! Internal multicast types used by Nexus. use omicron_common::api::external::IdentityMetadataCreateParams; -use omicron_common::vlan::VlanID; use std::net::IpAddr; /// Internal parameters for creating a multicast group. @@ -25,10 +24,4 @@ pub struct MulticastGroupCreate { /// Empty list explicitly allows any source (Any-Source Multicast). /// Non-empty list restricts to specific sources (SSM). pub source_ips: Option>, - /// Multicast VLAN (MVLAN) for egress multicast traffic to upstream networks. - /// Tags packets leaving the rack to traverse VLAN-segmented upstream networks. - /// - /// Valid range: 2-4094 (VLAN IDs 0-1 are reserved by IEEE 802.1Q standard). - // TODO(multicast): Remove mvlan field - being deprecated from multicast groups - pub mvlan: Option, } diff --git a/openapi/nexus/nexus-2025120500.0.0-c28237.json b/openapi/nexus/nexus-2025120500.0.0-95be0e.json similarity index 99% rename from openapi/nexus/nexus-2025120500.0.0-c28237.json rename to openapi/nexus/nexus-2025120500.0.0-95be0e.json index 78dc21a3d41..114dfbed127 100644 --- a/openapi/nexus/nexus-2025120500.0.0-c28237.json +++ b/openapi/nexus/nexus-2025120500.0.0-95be0e.json @@ -22904,13 +22904,6 @@ "type": "string", "format": "ip" }, - "mvlan": { - "nullable": true, - "description": "Multicast VLAN (MVLAN) for egress multicast traffic to upstream networks. None means no VLAN tagging on egress.", - "type": "integer", - "format": "uint16", - "minimum": 0 - }, "name": { "description": "unique, mutable, user-controlled identifier for each resource", "allOf": [ diff --git a/openapi/nexus/nexus-latest.json b/openapi/nexus/nexus-latest.json index 8aab5651cba..bbf34f7eee9 120000 --- a/openapi/nexus/nexus-latest.json +++ b/openapi/nexus/nexus-latest.json @@ -1 +1 @@ -nexus-2025120500.0.0-c28237.json \ No newline at end of file +nexus-2025120500.0.0-95be0e.json \ No newline at end of file diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index f90302c0755..288a548ac75 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -7053,11 +7053,6 @@ CREATE TABLE IF NOT EXISTS omicron.public.multicast_group ( /* Source-Specific Multicast (SSM) support */ source_ips INET[] DEFAULT ARRAY[]::INET[], - /* Multicast VLAN (MVLAN) for egress to upstream networks */ - /* Tags packets leaving the rack to traverse VLAN-segmented upstream networks */ - /* Internal rack traffic uses VNI-based underlay forwarding */ - mvlan INT2, - /* Associated underlay group for NAT */ /* We fill this as part of the RPW */ underlay_group_id UUID, @@ -7099,11 +7094,6 @@ CREATE TABLE IF NOT EXISTS omicron.public.multicast_group ( NOT multicast_ip << 'ff01::/16' AND -- Interface-local scope NOT multicast_ip << 'ff02::/16' -- Link-local scope ) - ), - - -- MVLAN validation (Dendrite requires >= 2) - CONSTRAINT mvlan_valid_range CHECK ( - mvlan IS NULL OR (mvlan >= 2 AND mvlan <= 4094) ) ); @@ -7397,7 +7387,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '213.0.0', NULL) + (TRUE, NOW(), NOW(), '214.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/schema/crdb/multicast-drop-mvlan/up01.sql b/schema/crdb/multicast-drop-mvlan/up01.sql new file mode 100644 index 00000000000..a6ed8fbd2b1 --- /dev/null +++ b/schema/crdb/multicast-drop-mvlan/up01.sql @@ -0,0 +1,6 @@ +-- Drop mvlan column and constraint from multicast_group +-- This field was for egress multicast which is not in MVP scope + +-- First drop the constraint, then the column +ALTER TABLE omicron.public.multicast_group DROP CONSTRAINT IF EXISTS mvlan_valid_range; +ALTER TABLE omicron.public.multicast_group DROP COLUMN IF EXISTS mvlan;