diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 3d5d3d42201..e7b2100a3b6 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -728,7 +728,6 @@ impl super::Nexus { organization_name: &Name, project_name: &Name, instance_name: &Name, - pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { let (.., authz_instance) = LookupPath::new(opctx, &self.db_datastore) .organization_name(organization_name) @@ -737,7 +736,7 @@ impl super::Nexus { .lookup_for(authz::Action::ListChildren) .await?; self.db_datastore - .instance_list_network_interfaces(opctx, &authz_instance, pagparams) + .instance_list_network_interfaces(opctx, &authz_instance) .await } diff --git a/nexus/src/db/datastore.rs b/nexus/src/db/datastore.rs index 499eee458bc..73fd151c67e 100644 --- a/nexus/src/db/datastore.rs +++ b/nexus/src/db/datastore.rs @@ -1932,16 +1932,16 @@ impl DataStore { &self, opctx: &OpContext, authz_instance: &authz::Instance, - pagparams: &DataPageParams<'_, Name>, ) -> ListResultVec { opctx.authorize(authz::Action::ListChildren, authz_instance).await?; - use db::schema::network_interface::dsl; - paginated(dsl::network_interface, dsl::name, &pagparams) + + dsl::network_interface .filter(dsl::time_deleted.is_null()) .filter(dsl::instance_id.eq(authz_instance.id())) + .order(dsl::slot.asc()) .select(NetworkInterface::as_select()) - .load_async::(self.pool_authorized(opctx).await?) + .load_async(self.pool_authorized(opctx).await?) .await .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) } diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index b47e6ecf12c..be918c857bc 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -7,8 +7,9 @@ use super::{ console_api, params, views, views::{ - GlobalImage, IdentityProvider, Image, Organization, Project, Rack, - Role, Silo, Sled, Snapshot, SshKey, User, Vpc, VpcRouter, VpcSubnet, + GlobalImage, IdentityProvider, Image, NetworkInterfaces, Organization, + Project, Rack, Role, Silo, Sled, Snapshot, SshKey, User, Vpc, + VpcRouter, VpcSubnet, }, }; use crate::authz; @@ -1833,12 +1834,10 @@ async fn project_images_delete_image( }] async fn instance_network_interfaces_get( rqctx: Arc>>, - query_params: Query, path_params: Path, -) -> Result>, HttpError> { +) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; - let query = query_params.into_inner(); let path = path_params.into_inner(); let organization_name = &path.organization_name; let project_name = &path.project_name; @@ -1851,14 +1850,14 @@ async fn instance_network_interfaces_get( &organization_name, &project_name, &instance_name, - &data_page_params_for(&rqctx, &query)? - .map_name(|n| Name::ref_cast(n)), ) - .await? - .into_iter() - .map(|d| d.into()) - .collect(); - Ok(HttpResponseOk(ScanByName::results_page(&query, interfaces)?)) + .await?; + Ok(HttpResponseOk(NetworkInterfaces { + interfaces: interfaces + .into_iter() + .map(|interface| interface.into()) + .collect(), + })) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } diff --git a/nexus/src/external_api/views.rs b/nexus/src/external_api/views.rs index f758e8f4e16..977a4ee701b 100644 --- a/nexus/src/external_api/views.rs +++ b/nexus/src/external_api/views.rs @@ -10,7 +10,7 @@ use crate::db::model; use api_identity::ObjectIdentity; use omicron_common::api::external::{ ByteCount, Digest, IdentityMetadata, Ipv4Net, Ipv6Net, Name, - ObjectIdentity, RoleName, + NetworkInterface, ObjectIdentity, RoleName, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -419,3 +419,11 @@ impl From for SshKey { } } } + +// NETWORK INTERFACES + +/// Collection of an [`Instance`]'s network interfaces +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct NetworkInterfaces { + pub interfaces: Vec, +} diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index 9aeafee445f..985e72b4325 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -11,17 +11,17 @@ use dropshot::test_util::ClientTestContext; use dropshot::HttpErrorResponseBody; use dropshot::Method; use http::StatusCode; -use omicron_common::api::external::ByteCount; use omicron_common::api::external::Disk; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::Instance; use omicron_common::api::external::InstanceCpuCount; +use omicron_common::api::external::{ByteCount, NetworkInterface}; use omicron_nexus::crucible_agent_client::types::State as RegionState; use omicron_nexus::external_api::params; use omicron_nexus::external_api::shared; use omicron_nexus::external_api::shared::IdentityType; use omicron_nexus::external_api::views::{ - Organization, Project, Silo, Vpc, VpcRouter, + NetworkInterfaces, Organization, Project, Silo, Vpc, VpcRouter, }; use omicron_sled_agent::sim::SledAgent; use std::sync::Arc; @@ -340,6 +340,21 @@ pub async fn project_get( .expect("failed to parse Project") } +pub async fn instance_network_interfaces_get( + client: &ClientTestContext, + interfaces_url: &str, +) -> Vec { + let request: NetworkInterfaces = + NexusRequest::object_get(client, interfaces_url) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + request.interfaces +} + pub struct DiskTest { pub sled_agent: Arc, pub zpool_id: Uuid, diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 5ba02e0a6f7..33e2f0b2c95 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -9,9 +9,11 @@ use http::StatusCode; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; -use nexus_test_utils::resource_helpers::create_disk; use nexus_test_utils::resource_helpers::objects_list_page_authz; use nexus_test_utils::resource_helpers::DiskTest; +use nexus_test_utils::resource_helpers::{ + create_disk, instance_network_interfaces_get, +}; use omicron_common::api::external::ByteCount; use omicron_common::api::external::Disk; use omicron_common::api::external::DiskState; @@ -940,16 +942,12 @@ async fn test_instance_create_delete_network_interface( "/organizations/{}/projects/{}/instances/{}/network-interfaces", ORGANIZATION_NAME, PROJECT_NAME, instance.identity.name, ); - let interfaces = NexusRequest::iter_collection_authn::( - client, - url_interfaces.as_str(), - "", - Some(100), - ) - .await - .expect("Failed to get interfaces for instance"); + + // Get network interface(s) attached to the instance + let interfaces = + instance_network_interfaces_get(client, &url_interfaces).await; assert!( - interfaces.all_items.is_empty(), + interfaces.is_empty(), "Expected no network interfaces for instance" ); @@ -1034,9 +1032,7 @@ async fn test_instance_create_delete_network_interface( // Get all interfaces in one request. let other_interfaces = - objects_list_page_authz::(client, &url_interfaces) - .await - .items; + instance_network_interfaces_get(client, &url_interfaces).await; for (iface0, iface1) in interfaces.iter().zip(other_interfaces) { assert_eq!(iface0.identity.id, iface1.identity.id); assert_eq!(iface0.vpc_id, iface1.vpc_id); @@ -1164,7 +1160,7 @@ async fn test_instance_update_network_interfaces( let instance_params = params::InstanceCreate { identity: IdentityMetadataCreateParams { name: Name::try_from(String::from("nic-update-test-inst")).unwrap(), - description: String::from("instance to test updatin nics"), + description: String::from("instance to test updating nics"), }, ncpus: InstanceCpuCount::try_from(2).unwrap(), memory: ByteCount::from_gibibytes_u32(4), @@ -1468,7 +1464,7 @@ async fn test_instance_update_network_interfaces( // The now-secondary interface should be, well, secondary assert!( !new_secondary_iface.primary, - "The old primary interface should now be a seconary" + "The old primary interface should now be a secondary" ); // Nothing else about the old primary should have changed @@ -1492,9 +1488,7 @@ async fn test_instance_update_network_interfaces( .await .expect("Failed to delete original secondary interface"); let all_interfaces = - objects_list_page_authz::(client, &url_interfaces) - .await - .items; + instance_network_interfaces_get(client, &url_interfaces).await; assert_eq!( all_interfaces.len(), 1, @@ -2364,6 +2358,39 @@ async fn test_instance_serial(cptestctx: &ControlPlaneTestContext) { .unwrap(); } +// Test getting network interfaces attached to an instance +#[nexus_test] +async fn test_instance_get_network_interfaces( + cptestctx: &ControlPlaneTestContext, +) { + static INSTANCE_NAME: &str = "just-rainsticks"; + let client = &cptestctx.external_client; + let path = &format!( + "/organizations/{}/projects/{}/instances/{}/network-interfaces", + ORGANIZATION_NAME, PROJECT_NAME, INSTANCE_NAME + ); + create_org_and_project(client).await; + + // Create instance with default network interface + let instance = create_instance( + &client, + ORGANIZATION_NAME, + PROJECT_NAME, + INSTANCE_NAME, + ) + .await; + + // Get network interface(s) attached to the instance + let interface = instance_network_interfaces_get(client, path).await; + + assert_eq!(interface[0].identity.name, "net0"); + assert_eq!( + interface[0].identity.description, + format!("default primary interface for {}", INSTANCE_NAME) + ); + assert_eq!(interface[0].instance_id, instance.identity.id); +} + async fn instance_get( client: &ClientTestContext, instance_url: &str, diff --git a/openapi/nexus.json b/openapi/nexus.json index 3a9c4e71060..d209003689f 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -2075,36 +2075,6 @@ "summary": "List network interfaces attached to this instance.", "operationId": "instance_network_interfaces_get", "parameters": [ - { - "in": "query", - "name": "limit", - "description": "Maximum number of items returned by a single call", - "schema": { - "nullable": true, - "type": "integer", - "format": "uint32", - "minimum": 1 - }, - "style": "form" - }, - { - "in": "query", - "name": "page_token", - "description": "Token returned by previous call to retrieve the subsequent page", - "schema": { - "nullable": true, - "type": "string" - }, - "style": "form" - }, - { - "in": "query", - "name": "sort_by", - "schema": { - "$ref": "#/components/schemas/NameSortMode" - }, - "style": "form" - }, { "in": "path", "name": "instance_name", @@ -2139,7 +2109,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NetworkInterfaceResultsPage" + "$ref": "#/components/schemas/NetworkInterfaces" } } } @@ -2150,8 +2120,7 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "x-dropshot-pagination": true + } }, "post": { "tags": [ @@ -7234,6 +7203,21 @@ } } }, + "NetworkInterfaces": { + "description": "Collection of an [`Instance`]'s network interfaces", + "type": "object", + "properties": { + "interfaces": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkInterface" + } + } + }, + "required": [ + "interfaces" + ] + }, "Organization": { "description": "Client view of an [`Organization`]", "type": "object",