diff --git a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs index 80082e01377..16f39025729 100644 --- a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs +++ b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs @@ -220,14 +220,14 @@ mod api_impl { use omicron_common::api::internal::shared::{ ResolvedVpcRouteSet, ResolvedVpcRouteState, SwitchPorts, }; - use sled_agent_api::v7::InstanceEnsureBody; - use sled_agent_api::v7::InstanceMulticastBody; use sled_agent_api::*; use sled_agent_types::bootstore::BootstoreStatus; use sled_agent_types::disk::DiskEnsureBody; use sled_agent_types::early_networking::EarlyNetworkConfig; use sled_agent_types::firewall_rules::VpcFirewallRulesEnsureBody; + use sled_agent_types::instance::InstanceEnsureBody; use sled_agent_types::instance::InstanceExternalIpBody; + use sled_agent_types::instance::InstanceMulticastBody; use sled_agent_types::instance::VmmPutStateBody; use sled_agent_types::instance::VmmPutStateResponse; use sled_agent_types::instance::VmmUnregisterResponse; @@ -531,15 +531,7 @@ mod api_impl { unimplemented!() } - async fn vmm_register_v1( - _rqctx: RequestContext, - _path_params: Path, - _body: TypedBody, - ) -> Result, HttpError> { - unimplemented!() - } - - async fn vmm_register_v7( + async fn vmm_register( _rqctx: RequestContext, _path_params: Path, _body: TypedBody, diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index d37f406af54..b375d326749 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -40,8 +40,8 @@ use sled_agent_types::{ early_networking::EarlyNetworkConfig, firewall_rules::VpcFirewallRulesEnsureBody, instance::{ - InstanceExternalIpBody, VmmPutStateBody, VmmPutStateResponse, - VmmUnregisterResponse, + InstanceEnsureBody, InstanceExternalIpBody, InstanceMulticastBody, + VmmPutStateBody, VmmPutStateResponse, VmmUnregisterResponse, }, sled::AddSledRequest, zone_bundle::{ @@ -55,8 +55,8 @@ use uuid::Uuid; /// Copies of data types that changed between v3 and v4. mod v3; -/// Copies of data types that changed between previous versions and v7. -pub mod v7; +/// Copies of data types that changed between previous versions and v6. +mod v6; api_versions!([ // WHEN CHANGING THE API (part 1 of 2): @@ -368,8 +368,10 @@ pub trait SledAgentApi { async fn vmm_register_v1( rqctx: RequestContext, path_params: Path, - body: TypedBody, - ) -> Result, HttpError>; + body: TypedBody, + ) -> Result, HttpError> { + Self::vmm_register(rqctx, path_params, body.map(Into::into)).await + } #[endpoint { method = PUT, @@ -377,10 +379,10 @@ pub trait SledAgentApi { operation_id = "vmm_register", versions = VERSION_MULTICAST_SUPPORT.. }] - async fn vmm_register_v7( + async fn vmm_register( rqctx: RequestContext, path_params: Path, - body: TypedBody, + body: TypedBody, ) -> Result, HttpError>; #[endpoint { @@ -439,7 +441,7 @@ pub trait SledAgentApi { async fn vmm_join_multicast_group( rqctx: RequestContext, path_params: Path, - body: TypedBody, + body: TypedBody, ) -> Result; #[endpoint { @@ -450,7 +452,7 @@ pub trait SledAgentApi { async fn vmm_leave_multicast_group( rqctx: RequestContext, path_params: Path, - body: TypedBody, + body: TypedBody, ) -> Result; #[endpoint { diff --git a/sled-agent/api/src/v7.rs b/sled-agent/api/src/v6.rs similarity index 69% rename from sled-agent/api/src/v7.rs rename to sled-agent/api/src/v6.rs index 4e096e5415f..06e6b995d7e 100644 --- a/sled-agent/api/src/v7.rs +++ b/sled-agent/api/src/v6.rs @@ -26,7 +26,7 @@ use uuid::Uuid; use sled_agent_types::instance::{InstanceMetadata, VmmSpec}; /// The body of a request to ensure that a instance and VMM are known to a sled -/// agent (version 7, with multicast support). +/// agent. #[derive(Serialize, Deserialize, JsonSchema)] pub struct InstanceEnsureBody { /// The virtual hardware configuration this virtual machine should have when @@ -56,7 +56,7 @@ pub struct InstanceEnsureBody { } /// Describes sled-local configuration that a sled-agent must establish to make -/// the instance's virtual hardware fully functional (version 7, with multicast). +/// the instance's virtual hardware fully functional. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct InstanceSledLocalConfig { pub hostname: Hostname, @@ -66,25 +66,33 @@ pub struct InstanceSledLocalConfig { /// provided to an instance to allow inbound connectivity. pub ephemeral_ip: Option, pub floating_ips: Vec, - pub multicast_groups: Vec, pub firewall_rules: Vec, pub dhcp_config: DhcpConfig, } -/// Represents a multicast group membership for an instance. -#[derive( - Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq, Hash, -)] -pub struct InstanceMulticastMembership { - pub group_ip: IpAddr, - // For Source-Specific Multicast (SSM) - pub sources: Vec, -} - -/// Request body for multicast group operations. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum InstanceMulticastBody { - Join(InstanceMulticastMembership), - Leave(InstanceMulticastMembership), +impl From + for sled_agent_types::instance::InstanceEnsureBody +{ + fn from( + v6: InstanceEnsureBody, + ) -> sled_agent_types::instance::InstanceEnsureBody { + sled_agent_types::instance::InstanceEnsureBody { + vmm_spec: v6.vmm_spec, + local_config: sled_agent_types::instance::InstanceSledLocalConfig { + hostname: v6.local_config.hostname, + nics: v6.local_config.nics, + source_nat: v6.local_config.source_nat, + ephemeral_ip: v6.local_config.ephemeral_ip, + floating_ips: v6.local_config.floating_ips, + multicast_groups: Vec::new(), + firewall_rules: v6.local_config.firewall_rules, + dhcp_config: v6.local_config.dhcp_config, + }, + vmm_runtime: v6.vmm_runtime, + instance_id: v6.instance_id, + migration_id: v6.migration_id, + propolis_addr: v6.propolis_addr, + metadata: v6.metadata, + } + } } diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index a3c6a0ec92f..64dc41becc0 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -32,8 +32,8 @@ use sled_agent_types::disk::DiskEnsureBody; use sled_agent_types::early_networking::EarlyNetworkConfig; use sled_agent_types::firewall_rules::VpcFirewallRulesEnsureBody; use sled_agent_types::instance::{ - InstanceExternalIpBody, VmmPutStateBody, VmmPutStateResponse, - VmmUnregisterResponse, + InstanceEnsureBody, InstanceExternalIpBody, InstanceMulticastBody, + VmmPutStateBody, VmmPutStateResponse, VmmUnregisterResponse, }; use sled_agent_types::probes::ProbeSet; use sled_agent_types::sled::AddSledRequest; @@ -489,29 +489,16 @@ impl SledAgentApi for SledAgentImpl { Ok(HttpResponseOk(sa.get_role())) } - async fn vmm_register_v1( + async fn vmm_register( rqctx: RequestContext, path_params: Path, - body: TypedBody, + body: TypedBody, ) -> Result, HttpError> { let sa = rqctx.context(); let propolis_id = path_params.into_inner().propolis_id; let body_args = body.into_inner(); Ok(HttpResponseOk( - sa.instance_ensure_registered_v1(propolis_id, body_args).await?, - )) - } - - async fn vmm_register_v7( - rqctx: RequestContext, - path_params: Path, - body: TypedBody, - ) -> Result, HttpError> { - let sa = rqctx.context(); - let propolis_id = path_params.into_inner().propolis_id; - let body_args = body.into_inner(); - Ok(HttpResponseOk( - sa.instance_ensure_registered_v7(propolis_id, body_args).await?, + sa.instance_ensure_registered(propolis_id, body_args).await?, )) } @@ -571,7 +558,7 @@ impl SledAgentApi for SledAgentImpl { async fn vmm_join_multicast_group( rqctx: RequestContext, path_params: Path, - body: TypedBody, + body: TypedBody, ) -> Result { let sa = rqctx.context(); let id = path_params.into_inner().propolis_id; @@ -583,7 +570,7 @@ impl SledAgentApi for SledAgentImpl { async fn vmm_leave_multicast_group( rqctx: RequestContext, path_params: Path, - body: TypedBody, + body: TypedBody, ) -> Result { let sa = rqctx.context(); let id = path_params.into_inner().propolis_id; diff --git a/sled-agent/src/instance.rs b/sled-agent/src/instance.rs index 9bb95964804..4f90ac7c4d0 100644 --- a/sled-agent/src/instance.rs +++ b/sled-agent/src/instance.rs @@ -37,9 +37,6 @@ use propolis_client::Client as PropolisClient; use propolis_client::instance_spec::{ComponentV0, SpecKey}; use rand::SeedableRng; use rand::prelude::IteratorRandom; -use sled_agent_api::v7::{ - InstanceMulticastMembership, InstanceSledLocalConfig, -}; use sled_agent_config_reconciler::AvailableDatasetsReceiver; use sled_agent_types::instance::*; use sled_agent_types::zone_bundle::ZoneBundleCause; @@ -2491,7 +2488,6 @@ mod tests { use propolis_client::types::{ InstanceMigrateStatusResponse, InstanceStateMonitorResponse, }; - use sled_agent_api::v7::InstanceEnsureBody; use sled_agent_config_reconciler::{ CurrentlyManagedZpoolsReceiver, InternalDiskDetails, InternalDisksReceiver, diff --git a/sled-agent/src/instance_manager.rs b/sled-agent/src/instance_manager.rs index 540b71195a7..e10702a1193 100644 --- a/sled-agent/src/instance_manager.rs +++ b/sled-agent/src/instance_manager.rs @@ -20,7 +20,6 @@ use omicron_common::api::external::ByteCount; use omicron_common::api::internal::nexus::SledVmmState; use omicron_common::api::internal::shared::SledIdentifiers; use omicron_uuid_kinds::PropolisUuid; -use sled_agent_api::v7::{InstanceEnsureBody, InstanceMulticastBody}; use sled_agent_config_reconciler::AvailableDatasetsReceiver; use sled_agent_config_reconciler::CurrentlyManagedZpoolsReceiver; use sled_agent_types::instance::*; diff --git a/sled-agent/src/sim/http_entrypoints.rs b/sled-agent/src/sim/http_entrypoints.rs index 94d0903f7a7..9b7ea9b0f1f 100644 --- a/sled-agent/src/sim/http_entrypoints.rs +++ b/sled-agent/src/sim/http_entrypoints.rs @@ -36,13 +36,14 @@ use omicron_common::api::internal::shared::{ ResolvedVpcRouteSet, ResolvedVpcRouteState, SwitchPorts, }; use range_requests::PotentialRange; -use sled_agent_api::v7::InstanceMulticastBody; use sled_agent_api::*; use sled_agent_types::bootstore::BootstoreStatus; use sled_agent_types::disk::DiskEnsureBody; use sled_agent_types::early_networking::EarlyNetworkConfig; use sled_agent_types::firewall_rules::VpcFirewallRulesEnsureBody; +use sled_agent_types::instance::InstanceEnsureBody; use sled_agent_types::instance::InstanceExternalIpBody; +use sled_agent_types::instance::InstanceMulticastBody; use sled_agent_types::instance::VmmPutStateBody; use sled_agent_types::instance::VmmPutStateResponse; use sled_agent_types::instance::VmmUnregisterResponse; @@ -82,23 +83,10 @@ enum SledAgentSimImpl {} impl SledAgentApi for SledAgentSimImpl { type Context = Arc; - async fn vmm_register_v1( + async fn vmm_register( rqctx: RequestContext, path_params: Path, - body: TypedBody, - ) -> Result, HttpError> { - let sa = rqctx.context(); - let propolis_id = path_params.into_inner().propolis_id; - let body_args = body.into_inner(); - Ok(HttpResponseOk( - sa.instance_register_v1(propolis_id, body_args).await?, - )) - } - - async fn vmm_register_v7( - rqctx: RequestContext, - path_params: Path, - body: TypedBody, + body: TypedBody, ) -> Result, HttpError> { let sa = rqctx.context(); let propolis_id = path_params.into_inner().propolis_id; diff --git a/sled-agent/src/sim/sled_agent.rs b/sled-agent/src/sim/sled_agent.rs index efda700b747..201509bf02c 100644 --- a/sled-agent/src/sim/sled_agent.rs +++ b/sled-agent/src/sim/sled_agent.rs @@ -56,14 +56,13 @@ use propolis_client::{ }; use range_requests::PotentialRange; use sled_agent_api::SupportBundleMetadata; -use sled_agent_api::v7::InstanceMulticastMembership; use sled_agent_types::disk::DiskStateRequested; use sled_agent_types::early_networking::{ EarlyNetworkConfig, EarlyNetworkConfigBody, }; use sled_agent_types::instance::{ - InstanceExternalIpBody, VmmPutStateResponse, VmmStateRequested, - VmmUnregisterResponse, + InstanceEnsureBody, InstanceExternalIpBody, InstanceMulticastMembership, + VmmPutStateResponse, VmmStateRequested, VmmUnregisterResponse, }; use slog::Logger; @@ -200,43 +199,12 @@ impl SledAgent { }) } - /// Idempotently ensures that the given API Instance (described by - /// `api_instance`) exists on this server in the given runtime state - /// (described by `target`). - // Keep the v1 method for compatibility but it just delegates to v2 - pub async fn instance_register_v1( - self: &Arc, - propolis_id: PropolisUuid, - instance: sled_agent_types::instance::InstanceEnsureBody, - ) -> Result { - // Convert v1 to v7 for internal processing - let v5_instance = sled_agent_api::v7::InstanceEnsureBody { - vmm_spec: instance.vmm_spec, - local_config: sled_agent_api::v7::InstanceSledLocalConfig { - hostname: instance.local_config.hostname, - nics: instance.local_config.nics, - source_nat: instance.local_config.source_nat, - ephemeral_ip: instance.local_config.ephemeral_ip, - floating_ips: instance.local_config.floating_ips, - multicast_groups: Vec::new(), // v1 doesn't support multicast - firewall_rules: instance.local_config.firewall_rules, - dhcp_config: instance.local_config.dhcp_config, - }, - vmm_runtime: instance.vmm_runtime, - instance_id: instance.instance_id, - migration_id: instance.migration_id, - propolis_addr: instance.propolis_addr, - metadata: instance.metadata, - }; - self.instance_register(propolis_id, v5_instance).await - } - pub async fn instance_register( self: &Arc, propolis_id: PropolisUuid, - instance: sled_agent_api::v7::InstanceEnsureBody, + instance: InstanceEnsureBody, ) -> Result { - let sled_agent_api::v7::InstanceEnsureBody { + let InstanceEnsureBody { vmm_spec, local_config, instance_id, @@ -722,7 +690,7 @@ impl SledAgent { pub async fn instance_join_multicast_group( &self, propolis_id: PropolisUuid, - membership: &sled_agent_api::v7::InstanceMulticastMembership, + membership: &InstanceMulticastMembership, ) -> Result<(), Error> { if !self.vmms.contains_key(&propolis_id.into_untyped_uuid()).await { return Err(Error::internal_error( @@ -741,7 +709,7 @@ impl SledAgent { pub async fn instance_leave_multicast_group( &self, propolis_id: PropolisUuid, - membership: &sled_agent_api::v7::InstanceMulticastMembership, + membership: &InstanceMulticastMembership, ) -> Result<(), Error> { if !self.vmms.contains_key(&propolis_id.into_untyped_uuid()).await { return Err(Error::internal_error( diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index d9a3e7c89b2..e2dbcffc57d 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -54,7 +54,6 @@ use omicron_ddm_admin_client::Client as DdmAdminClient; use omicron_uuid_kinds::{ GenericUuid, MupdateOverrideUuid, PropolisUuid, SledUuid, }; -use sled_agent_api::v7::{InstanceEnsureBody, InstanceMulticastBody}; use sled_agent_config_reconciler::{ ConfigReconcilerHandle, ConfigReconcilerSpawnToken, InternalDisks, InternalDisksReceiver, LedgerNewConfigError, LedgerTaskError, @@ -63,8 +62,8 @@ use sled_agent_config_reconciler::{ use sled_agent_types::disk::DiskStateRequested; use sled_agent_types::early_networking::EarlyNetworkConfig; use sled_agent_types::instance::{ - InstanceExternalIpBody, VmmPutStateResponse, VmmStateRequested, - VmmUnregisterResponse, + InstanceEnsureBody, InstanceExternalIpBody, InstanceMulticastBody, + VmmPutStateResponse, VmmStateRequested, VmmUnregisterResponse, }; use sled_agent_types::probes::ProbeCreate; use sled_agent_types::sled::{BaseboardId, StartSledAgentRequest}; @@ -839,45 +838,7 @@ impl SledAgent { } } - /// Idempotently ensures that a given instance is registered with this sled, - /// i.e., that it can be addressed by future calls to - /// [`Self::instance_ensure_state`]. - pub async fn instance_ensure_registered_v1( - &self, - propolis_id: PropolisUuid, - instance: sled_agent_types::instance::InstanceEnsureBody, - ) -> Result { - // Convert v1 to v7 - let v5_instance = sled_agent_api::v7::InstanceEnsureBody { - vmm_spec: instance.vmm_spec, - local_config: sled_agent_api::v7::InstanceSledLocalConfig { - hostname: instance.local_config.hostname, - nics: instance.local_config.nics, - source_nat: instance.local_config.source_nat, - ephemeral_ip: instance.local_config.ephemeral_ip, - floating_ips: instance.local_config.floating_ips, - multicast_groups: Vec::new(), // v1 doesn't support multicast - firewall_rules: instance.local_config.firewall_rules, - dhcp_config: instance.local_config.dhcp_config, - }, - vmm_runtime: instance.vmm_runtime, - instance_id: instance.instance_id, - migration_id: instance.migration_id, - propolis_addr: instance.propolis_addr, - metadata: instance.metadata, - }; - self.instance_ensure_registered_v7(propolis_id, v5_instance).await - } - - pub async fn instance_ensure_registered_v7( - &self, - propolis_id: PropolisUuid, - instance: InstanceEnsureBody, - ) -> Result { - self.instance_ensure_registered(propolis_id, instance).await - } - - async fn instance_ensure_registered( + pub async fn instance_ensure_registered( &self, propolis_id: PropolisUuid, instance: InstanceEnsureBody, diff --git a/sled-agent/tests/multicast_cross_version_test.rs b/sled-agent/tests/multicast_cross_version_test.rs deleted file mode 100644 index b869e5a76e9..00000000000 --- a/sled-agent/tests/multicast_cross_version_test.rs +++ /dev/null @@ -1,118 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// 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/. - -//! Cross-version compatibility tests for sled-agent multicast APIs. -//! -//! This test verifies that v4 and v5 instance configurations work correctly -//! together, specifically around multicast group support. It follows the same -//! pattern as the DNS cross-version tests. - -use anyhow::Result; -use std::net::IpAddr; - -use omicron_common::api::internal::shared::DhcpConfig; -use sled_agent_api::v7; - -// Generate v5 client from v5 OpenAPI spec (with enhanced multicast support) -mod v5_client { - progenitor::generate_api!( - spec = "../openapi/sled-agent/sled-agent-5.0.0-253577.json", - interface = Positional, - inner_type = slog::Logger, - derives = [schemars::JsonSchema, Clone, Eq, PartialEq], - pre_hook = (|log: &slog::Logger, request: &reqwest::Request| { - slog::debug!(log, "client request"; - "method" => %request.method(), - "uri" => %request.url(), - "body" => ?&request.body(), - ); - }), - post_hook = (|log: &slog::Logger, result: &Result<_, _>| { - slog::debug!(log, "client response"; "result" => ?result); - }) - ); -} - -// A v5 server can productively handle requests from a v4 client, and a v4 -// client can provide instance configurations to a v5 server (backwards compatible). -// This follows the same pattern as DNS cross-version compatibility. -#[tokio::test] -pub async fn multicast_cross_version_works() -> Result<(), anyhow::Error> { - use omicron_test_utils::dev::test_setup_log; - let logctx = test_setup_log("multicast_cross_version_works"); - - let multicast_addr = "239.1.1.1".parse::().unwrap(); - let source_addr = "192.168.1.10".parse::().unwrap(); - - // Focus on the local_config field since that's where multicast_groups lives - - // Create v4 local config JSON (won't have multicast_groups field) - let v4_local_config_json = serde_json::json!({ - "hostname": "test-v4", - "nics": [], - "source_nat": { - "ip": "10.1.1.1", - "first_port": 0, - "last_port": 16383 - }, - "ephemeral_ip": null, - "floating_ips": [], - "firewall_rules": [], - "dhcp_config": { - "dns_servers": [], - "host_domain": null, - "search_domains": [] - } - }); - - // Create v5 local config with multicast_groups - let v5_local_config = v7::InstanceSledLocalConfig { - hostname: omicron_common::api::external::Hostname::try_from("test-v5") - .unwrap(), - nics: vec![], - source_nat: nexus_types::deployment::SourceNatConfig::new( - "10.1.1.1".parse().unwrap(), - 0, - 16383, - ) - .unwrap(), - ephemeral_ip: None, - floating_ips: vec![], - multicast_groups: vec![v7::InstanceMulticastMembership { - group_ip: multicast_addr, - sources: vec![source_addr], - }], - firewall_rules: vec![], - dhcp_config: DhcpConfig { - dns_servers: vec![], - host_domain: None, - search_domains: vec![], - }, - }; - - // Test that v4 can be parsed by v5 (with empty multicast_groups) - let v4_as_v5_json = serde_json::to_string(&v4_local_config_json)?; - let v5_json = serde_json::to_string(&v5_local_config)?; - - // v4 should NOT have multicast_groups in the JSON - assert!( - !v4_as_v5_json.contains("multicast_groups"), - "v4 InstanceSledLocalConfig should not contain multicast_groups field" - ); - - // v5 should HAVE multicast_groups in the JSON - assert!( - v5_json.contains("multicast_groups"), - "v5 InstanceSledLocalConfig should contain multicast_groups field" - ); - - // Verify v5 has the multicast group we added - assert!( - v5_json.contains(&format!("\"group_ip\":\"{multicast_addr}\"")), - "v5 should contain the multicast group IP" - ); - - logctx.cleanup_successful(); - Ok(()) -} diff --git a/sled-agent/types/src/instance.rs b/sled-agent/types/src/instance.rs index b2de8311dbb..a825f9e2690 100644 --- a/sled-agent/types/src/instance.rs +++ b/sled-agent/types/src/instance.rs @@ -28,7 +28,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; /// The body of a request to ensure that a instance and VMM are known to a sled -/// agent. +/// agent (version 7, with multicast support). #[derive(Serialize, Deserialize, JsonSchema)] pub struct InstanceEnsureBody { /// The virtual hardware configuration this virtual machine should have when @@ -58,7 +58,7 @@ pub struct InstanceEnsureBody { } /// Describes sled-local configuration that a sled-agent must establish to make -/// the instance's virtual hardware fully functional. +/// the instance's virtual hardware fully functional (version 7, with multicast). #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct InstanceSledLocalConfig { pub hostname: Hostname, @@ -68,10 +68,29 @@ pub struct InstanceSledLocalConfig { /// provided to an instance to allow inbound connectivity. pub ephemeral_ip: Option, pub floating_ips: Vec, + pub multicast_groups: Vec, pub firewall_rules: Vec, pub dhcp_config: DhcpConfig, } +/// Represents a multicast group membership for an instance. +#[derive( + Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq, Hash, +)] +pub struct InstanceMulticastMembership { + pub group_ip: IpAddr, + // For Source-Specific Multicast (SSM) + pub sources: Vec, +} + +/// Request body for multicast group operations. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum InstanceMulticastBody { + Join(InstanceMulticastMembership), + Leave(InstanceMulticastMembership), +} + /// Metadata used to track statistics about an instance. /// // NOTE: The instance ID is not here, since it's already provided in other