From d642b5e7a51a220d74809cd7d949ae3aea141a36 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Thu, 10 Mar 2022 17:11:19 -0500 Subject: [PATCH 1/7] [nexus] Add stubs for snapshot APIs --- common/src/api/external/mod.rs | 15 +++ common/src/sql/dbinit.sql | 29 ++++ nexus/src/db/model.rs | 36 ++++- nexus/src/db/schema.rs | 16 +++ nexus/src/external_api/http_entrypoints.rs | 150 +++++++++++++++++++++ nexus/src/external_api/params.rs | 12 ++ nexus/src/nexus.rs | 41 ++++++ 7 files changed, 298 insertions(+), 1 deletion(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index a890106e10..b103fa5e78 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -935,6 +935,21 @@ impl DiskState { } } +/* + * SNAPSHOTS + */ + +/// Client view of a Snapshot +#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct Snapshot { + #[serde(flatten)] + pub identity: IdentityMetadata, + + pub project_id: Uuid, + pub disk_id: Uuid, + pub size: ByteCount, +} + /* * Sagas * diff --git a/common/src/sql/dbinit.sql b/common/src/sql/dbinit.sql index 531f37f26f..1453bbea8e 100644 --- a/common/src/sql/dbinit.sql +++ b/common/src/sql/dbinit.sql @@ -371,6 +371,35 @@ CREATE INDEX ON omicron.public.disk ( time_deleted IS NULL AND attach_instance_id IS NOT NULL; +CREATE TABLE omicron.public.snapshot ( + /* Identity metadata (resource) */ + id UUID PRIMARY KEY, + name STRING(63) NOT NULL, + description STRING(512) NOT NULL, + time_created TIMESTAMPTZ NOT NULL, + time_modified TIMESTAMPTZ NOT NULL, + /* Indicates that the object has been deleted */ + time_deleted TIMESTAMPTZ, + + /* Every Snapshot is in exactly one Project at a time. */ + project_id UUID NOT NULL, + + /* Every Snapshot originated from a single disk */ + disk_id UUID NOT NULL, + + /* Every Snapshot consists of a root volume */ + volume_id UUID NOT NULL, + + /* Disk configuration (from the time the snapshot was taken) */ + size_bytes INT NOT NULL +); + +CREATE UNIQUE INDEX ON omicron.public.snapshot ( + project_id, + name +) WHERE + time_deleted IS NULL; + /* * Oximeter collector servers. */ diff --git a/nexus/src/db/model.rs b/nexus/src/db/model.rs index 8870ff0b6f..ed66c63675 100644 --- a/nexus/src/db/model.rs +++ b/nexus/src/db/model.rs @@ -9,7 +9,7 @@ use crate::db::identity::{Asset, Resource}; use crate::db::schema::{ console_session, dataset, disk, instance, metric_producer, network_interface, organization, oximeter, project, rack, region, - role_assignment_builtin, role_builtin, router_route, sled, + role_assignment_builtin, role_builtin, router_route, sled, snapshot, update_available_artifact, user_builtin, volume, vpc, vpc_firewall_rule, vpc_router, vpc_subnet, zpool, }; @@ -1231,6 +1231,40 @@ impl Into for DiskState { } } +#[derive( + Queryable, + Insertable, + Selectable, + Clone, + Debug, + Resource, + Serialize, + Deserialize, +)] +#[table_name = "snapshot"] +pub struct Snapshot { + #[diesel(embed)] + identity: SnapshotIdentity, + + project_id: Uuid, + disk_id: Uuid, + volume_id: Uuid, + + #[column_name = "size_bytes"] + pub size: ByteCount, +} + +impl From for external::Snapshot { + fn from(snapshot: Snapshot) -> Self { + Self { + identity: snapshot.identity(), + project_id: snapshot.project_id, + disk_id: snapshot.disk_id, + size: snapshot.size.into(), + } + } +} + /// Information announced by a metric server, used so that clients can contact it and collect /// available metric data from it. #[derive(Queryable, Insertable, Debug, Clone, Selectable, Asset)] diff --git a/nexus/src/db/schema.rs b/nexus/src/db/schema.rs index ba665e533f..1cab17dd90 100644 --- a/nexus/src/db/schema.rs +++ b/nexus/src/db/schema.rs @@ -26,6 +26,22 @@ table! { } } +table! { + snapshot (id) { + id -> Uuid, + name -> Text, + description -> Text, + time_created -> Timestamptz, + time_modified -> Timestamptz, + time_deleted -> Nullable, + + project_id -> Uuid, + disk_id -> Uuid, + volume_id -> Uuid, + size_bytes -> Int8, + } +} + table! { instance (id) { id -> Uuid, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index b4b481f860..8b9016774a 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -52,6 +52,7 @@ use omicron_common::api::external::RouterRouteCreateParams; use omicron_common::api::external::RouterRouteKind; use omicron_common::api::external::RouterRouteUpdateParams; use omicron_common::api::external::Saga; +use omicron_common::api::external::Snapshot; use omicron_common::api::external::VpcFirewallRuleUpdateParams; use omicron_common::api::external::VpcFirewallRules; use omicron_common::api::external::VpcRouter; @@ -86,6 +87,7 @@ pub fn external_api() -> NexusApiDescription { api.register(project_disks_post)?; api.register(project_disks_get_disk)?; api.register(project_disks_delete_disk)?; + api.register(project_disks_snapshot_post)?; api.register(project_instances_get)?; api.register(project_instances_post)?; @@ -100,6 +102,10 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_disks_attach)?; api.register(instance_disks_detach)?; + api.register(project_snapshots_get)?; + api.register(project_snapshots_get_snapshot)?; + api.register(project_snapshots_delete_snapshot)?; + api.register(project_vpcs_get)?; api.register(project_vpcs_post)?; api.register(project_vpcs_get_vpc)?; @@ -704,6 +710,40 @@ async fn project_disks_delete_disk( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Create a snapshot of a disk. +#[endpoint { + method = POST, + path = "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/snapshot", + tags = ["disks"], +}] +async fn project_disks_snapshot_post( + rqctx: Arc>>, + path_params: Path, + new_snapshot: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let organization_name = &path.organization_name; + let project_name = &path.project_name; + let disk_name = &path.disk_name; + let new_snapshot_params = &new_snapshot.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let snapshot = nexus + .disk_create_snapshot( + &opctx, + &organization_name, + &project_name, + &disk_name, + &new_snapshot_params, + ) + .await?; + Ok(HttpResponseCreated(snapshot.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /* * Instances */ @@ -1106,6 +1146,116 @@ async fn instance_disks_detach( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/* + * Snapshots + */ + +/// List snapshots in a project. +#[endpoint { + method = GET, + path = "/organizations/{organization_name}/projects/{project_name}/snapshots", + tags = ["snapshots"], +}] +async fn project_snapshots_get( + rqctx: Arc>>, + query_params: Query, + path_params: Path, +) -> 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; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let snapshots = nexus + .project_list_snapshots( + &opctx, + organization_name, + project_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, snapshots)?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Path parameters for Snapshot requests +#[derive(Deserialize, JsonSchema)] +struct SnapshotPathParam { + organization_name: Name, + project_name: Name, + snapshot_name: Name, +} + +/// Get a snapshot in a project. +#[endpoint { + method = GET, + path = "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}", + tags = ["snapshots"], +}] +async fn project_snapshots_get_snapshot( + rqctx: Arc>>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let organization_name = &path.organization_name; + let project_name = &path.project_name; + let snapshot_name = &path.snapshot_name; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let snapshot = nexus + .snapshot_fetch( + &opctx, + &organization_name, + &project_name, + &snapshot_name, + ) + .await?; + Ok(HttpResponseOk(snapshot.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Delete a snapshot from a project. +#[endpoint { + method = DELETE, + path = "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}", + tags = ["snapshots"], +}] +async fn project_snapshots_delete_snapshot( + rqctx: Arc>>, + path_params: Path, +) -> Result { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let organization_name = &path.organization_name; + let project_name = &path.project_name; + let snapshot_name = &path.snapshot_name; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + nexus + .project_delete_snapshot( + &opctx, + &organization_name, + &project_name, + &snapshot_name, + ) + .await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /* * VPCs */ diff --git a/nexus/src/external_api/params.rs b/nexus/src/external_api/params.rs index 40f37ec63e..708b62476d 100644 --- a/nexus/src/external_api/params.rs +++ b/nexus/src/external_api/params.rs @@ -229,6 +229,18 @@ pub struct DiskIdentifier { pub disk: Name, } +/* + * SNAPSHOTS + */ + +/// Create-time parameters for a [`Snapshot`](omicron_common::api::external::Snapshot) +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct SnapshotCreate { + /// common identifying metadata + #[serde(flatten)] + pub identity: IdentityMetadataCreateParams, +} + /* * BUILT-IN USERS * diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 4adb092b7d..c1b81998b5 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -791,6 +791,47 @@ impl Nexus { Ok(()) } + pub async fn disk_create_snapshot( + self: &Arc, + _opctx: &OpContext, + _organization_name: &Name, + _project_name: &Name, + _disk_name: &Name, + _params: ¶ms::SnapshotCreate, + ) -> CreateResult { + unimplemented!(); + } + + pub async fn project_list_snapshots( + &self, + _opctx: &OpContext, + _organization_name: &Name, + _project_name: &Name, + _pagparams: &DataPageParams<'_, Name>, + ) -> ListResultVec { + unimplemented!(); + } + + pub async fn snapshot_fetch( + &self, + _opctx: &OpContext, + _organization_name: &Name, + _project_name: &Name, + _snapshot_name: &Name, + ) -> LookupResult { + unimplemented!(); + } + + pub async fn project_delete_snapshot( + self: &Arc, + _opctx: &OpContext, + _organization_name: &Name, + _project_name: &Name, + _snapshot_name: &Name, + ) -> DeleteResult { + unimplemented!(); + } + /* * Instances */ From 0ab8af324b8616b56fd0a4691f7cf9e6acb0a7c1 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Thu, 10 Mar 2022 17:12:40 -0500 Subject: [PATCH 2/7] openapi --- nexus/tests/output/nexus_tags.txt | 7 + openapi/nexus.json | 340 ++++++++++++++++++++++++++++++ 2 files changed, 347 insertions(+) diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index e7f4fa0fe1..b51b9aaa1b 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -4,6 +4,7 @@ project_disks_delete_disk /organizations/{organization_name}/proj project_disks_get /organizations/{organization_name}/projects/{project_name}/disks project_disks_get_disk /organizations/{organization_name}/projects/{project_name}/disks/{disk_name} project_disks_post /organizations/{organization_name}/projects/{project_name}/disks +project_disks_snapshot_post /organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/snapshot API operations found with tag "firewall" OPERATION ID URL PATH @@ -86,6 +87,12 @@ OPERATION ID URL PATH hardware_sleds_get /hardware/sleds hardware_sleds_get_sled /hardware/sleds/{sled_id} +API operations found with tag "snapshots" +OPERATION ID URL PATH +project_snapshots_delete_snapshot /organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name} +project_snapshots_get /organizations/{organization_name}/projects/{project_name}/snapshots +project_snapshots_get_snapshot /organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name} + API operations found with tag "subnets" OPERATION ID URL PATH subnets_ips_get /organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}/ips diff --git a/openapi/nexus.json b/openapi/nexus.json index a88e86dd09..f35bfd3dc5 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -960,6 +960,72 @@ } } }, + "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/snapshot": { + "post": { + "tags": [ + "disks" + ], + "summary": "Create a snapshot of a disk.", + "operationId": "project_disks_snapshot_post", + "parameters": [ + { + "in": "path", + "name": "disk_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + }, + { + "in": "path", + "name": "organization_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + }, + { + "in": "path", + "name": "project_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SnapshotCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Snapshot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/organizations/{organization_name}/projects/{project_name}/instances": { "get": { "tags": [ @@ -1651,6 +1717,189 @@ } } }, + "/organizations/{organization_name}/projects/{project_name}/snapshots": { + "get": { + "tags": [ + "snapshots" + ], + "summary": "List snapshots in a project.", + "operationId": "project_snapshots_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 retreive 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": "organization_name", + "description": "The organization's unique name.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + }, + { + "in": "path", + "name": "project_name", + "description": "The project's unique name within the organization.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SnapshotResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + } + }, + "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}": { + "get": { + "tags": [ + "snapshots" + ], + "summary": "Get a snapshot in a project.", + "operationId": "project_snapshots_get_snapshot", + "parameters": [ + { + "in": "path", + "name": "organization_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + }, + { + "in": "path", + "name": "project_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + }, + { + "in": "path", + "name": "snapshot_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Snapshot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "snapshots" + ], + "summary": "Delete a snapshot from a project.", + "operationId": "project_snapshots_delete_snapshot", + "parameters": [ + { + "in": "path", + "name": "organization_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + }, + { + "in": "path", + "name": "project_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + }, + { + "in": "path", + "name": "snapshot_name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/organizations/{organization_name}/projects/{project_name}/vpcs": { "get": { "tags": [ @@ -5119,6 +5368,97 @@ "items" ] }, + "Snapshot": { + "description": "Client view of a Snapshot", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "disk_id": { + "type": "string", + "format": "uuid" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "project_id": { + "type": "string", + "format": "uuid" + }, + "size": { + "$ref": "#/components/schemas/ByteCount" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "disk_id", + "id", + "name", + "project_id", + "size", + "time_created", + "time_modified" + ] + }, + "SnapshotCreate": { + "description": "Create-time parameters for a [`Snapshot`](omicron_common::api::external::Snapshot)", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + } + }, + "required": [ + "description", + "name" + ] + }, + "SnapshotResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Snapshot" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, "TimeseriesName": { "title": "The name of a timeseries", "description": "Names are constructed by concatenating the target and metric names with ':'. Target and metric names must be lowercase alphanumeric characters with '_' separating words.", From 9c5a1a7d286195fe28487189a7f1a4b9f4cede67 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Thu, 10 Mar 2022 18:32:27 -0500 Subject: [PATCH 3/7] move things to views --- common/src/api/external/mod.rs | 15 --------------- nexus/src/db/model.rs | 3 ++- nexus/src/external_api/http_entrypoints.rs | 3 +-- nexus/src/external_api/views.rs | 18 +++++++++++++++++- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index b103fa5e78..a890106e10 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -935,21 +935,6 @@ impl DiskState { } } -/* - * SNAPSHOTS - */ - -/// Client view of a Snapshot -#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct Snapshot { - #[serde(flatten)] - pub identity: IdentityMetadata, - - pub project_id: Uuid, - pub disk_id: Uuid, - pub size: ByteCount, -} - /* * Sagas * diff --git a/nexus/src/db/model.rs b/nexus/src/db/model.rs index ed66c63675..922f84e064 100644 --- a/nexus/src/db/model.rs +++ b/nexus/src/db/model.rs @@ -15,6 +15,7 @@ use crate::db::schema::{ }; use crate::defaults; use crate::external_api::params; +use crate::external_api::views; use crate::internal_api; use chrono::{DateTime, Utc}; use db_macros::{Asset, Resource}; @@ -1254,7 +1255,7 @@ pub struct Snapshot { pub size: ByteCount, } -impl From for external::Snapshot { +impl From for views::Snapshot { fn from(snapshot: Snapshot) -> Self { Self { identity: snapshot.identity(), diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 8b9016774a..1120a29ddd 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -12,7 +12,7 @@ use crate::ServerContext; use super::{ console_api, params, - views::{Organization, Project, Rack, Role, Sled, User, Vpc, VpcSubnet}, + views::{Organization, Project, Rack, Role, Sled, Snapshot, User, Vpc, VpcSubnet}, }; use crate::context::OpContext; use dropshot::ApiDescription; @@ -52,7 +52,6 @@ use omicron_common::api::external::RouterRouteCreateParams; use omicron_common::api::external::RouterRouteKind; use omicron_common::api::external::RouterRouteUpdateParams; use omicron_common::api::external::Saga; -use omicron_common::api::external::Snapshot; use omicron_common::api::external::VpcFirewallRuleUpdateParams; use omicron_common::api::external::VpcFirewallRules; use omicron_common::api::external::VpcRouter; diff --git a/nexus/src/external_api/views.rs b/nexus/src/external_api/views.rs index 3958a82508..7d4b6b1b92 100644 --- a/nexus/src/external_api/views.rs +++ b/nexus/src/external_api/views.rs @@ -11,7 +11,8 @@ use crate::db::identity::{Asset, Resource}; use crate::db::model; use api_identity::ObjectIdentity; use omicron_common::api::external::{ - IdentityMetadata, Ipv4Net, Ipv6Net, Name, ObjectIdentity, RoleName, + ByteCount, IdentityMetadata, Ipv4Net, Ipv6Net, Name, ObjectIdentity, + RoleName, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -64,6 +65,21 @@ impl Into for model::Project { } } +/* + * SNAPSHOTS + */ + +/// Client view of a Snapshot +#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct Snapshot { + #[serde(flatten)] + pub identity: IdentityMetadata, + + pub project_id: Uuid, + pub disk_id: Uuid, + pub size: ByteCount, +} + /* * VPCs */ From 2decd76c24e23bb51c59dd6c7cd92e9498744824 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Thu, 10 Mar 2022 18:44:14 -0500 Subject: [PATCH 4/7] Update tags --- nexus/src/external_api/http_entrypoints.rs | 2 +- nexus/tests/output/nexus_tags.txt | 2 +- openapi/nexus.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 1120a29ddd..648658f25a 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -713,7 +713,7 @@ async fn project_disks_delete_disk( #[endpoint { method = POST, path = "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/snapshot", - tags = ["disks"], + tags = ["snapshots"], }] async fn project_disks_snapshot_post( rqctx: Arc>>, diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index b51b9aaa1b..ad8d77dfd6 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -4,7 +4,6 @@ project_disks_delete_disk /organizations/{organization_name}/proj project_disks_get /organizations/{organization_name}/projects/{project_name}/disks project_disks_get_disk /organizations/{organization_name}/projects/{project_name}/disks/{disk_name} project_disks_post /organizations/{organization_name}/projects/{project_name}/disks -project_disks_snapshot_post /organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/snapshot API operations found with tag "firewall" OPERATION ID URL PATH @@ -89,6 +88,7 @@ hardware_sleds_get_sled /hardware/sleds/{sled_id} API operations found with tag "snapshots" OPERATION ID URL PATH +project_disks_snapshot_post /organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/snapshot project_snapshots_delete_snapshot /organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name} project_snapshots_get /organizations/{organization_name}/projects/{project_name}/snapshots project_snapshots_get_snapshot /organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name} diff --git a/openapi/nexus.json b/openapi/nexus.json index f35bfd3dc5..863da3105e 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -963,7 +963,7 @@ "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/snapshot": { "post": { "tags": [ - "disks" + "snapshots" ], "summary": "Create a snapshot of a disk.", "operationId": "project_disks_snapshot_post", From 3f91513fd66a0cb60552f40779255ec593fbd09f Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Thu, 10 Mar 2022 18:46:08 -0500 Subject: [PATCH 5/7] fmt --- nexus/src/external_api/http_entrypoints.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 648658f25a..059ef48074 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -12,7 +12,9 @@ use crate::ServerContext; use super::{ console_api, params, - views::{Organization, Project, Rack, Role, Sled, Snapshot, User, Vpc, VpcSubnet}, + views::{ + Organization, Project, Rack, Role, Sled, Snapshot, User, Vpc, VpcSubnet, + }, }; use crate::context::OpContext; use dropshot::ApiDescription; From 9d666cf360454188a9c4321615107938dc472452 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 15 Mar 2022 15:15:12 -0400 Subject: [PATCH 6/7] Snapshots POST operates on projects, not disks --- nexus/src/external_api/http_entrypoints.rs | 68 +++++++++++----------- nexus/src/external_api/params.rs | 3 + nexus/src/nexus.rs | 3 +- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 194db397f5..a46afa8c0a 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -91,7 +91,6 @@ pub fn external_api() -> NexusApiDescription { api.register(project_disks_post)?; api.register(project_disks_get_disk)?; api.register(project_disks_delete_disk)?; - api.register(project_disks_snapshot_post)?; api.register(project_instances_get)?; api.register(project_instances_post)?; @@ -107,6 +106,7 @@ pub fn external_api() -> NexusApiDescription { api.register(instance_disks_detach)?; api.register(project_snapshots_get)?; + api.register(project_snapshots_post)?; api.register(project_snapshots_get_snapshot)?; api.register(project_snapshots_delete_snapshot)?; @@ -721,40 +721,6 @@ async fn project_disks_delete_disk( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Create a snapshot of a disk. -#[endpoint { - method = POST, - path = "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/snapshot", - tags = ["snapshots"], -}] -async fn project_disks_snapshot_post( - rqctx: Arc>>, - path_params: Path, - new_snapshot: TypedBody, -) -> Result, HttpError> { - let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let organization_name = &path.organization_name; - let project_name = &path.project_name; - let disk_name = &path.disk_name; - let new_snapshot_params = &new_snapshot.into_inner(); - let handler = async { - let opctx = OpContext::for_external_api(&rqctx).await?; - let snapshot = nexus - .disk_create_snapshot( - &opctx, - &organization_name, - &project_name, - &disk_name, - &new_snapshot_params, - ) - .await?; - Ok(HttpResponseCreated(snapshot.into())) - }; - apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await -} - /* * Instances */ @@ -1342,6 +1308,38 @@ async fn project_snapshots_get( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Create a snapshot of a disk. +#[endpoint { + method = POST, + path = "/organizations/{organization_name}/projects/{project_name}/snapshot", + tags = ["snapshots"], +}] +async fn project_snapshots_post( + rqctx: Arc>>, + path_params: Path, + new_snapshot: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let organization_name = &path.organization_name; + let project_name = &path.project_name; + let new_snapshot_params = &new_snapshot.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let snapshot = nexus + .project_create_snapshot( + &opctx, + &organization_name, + &project_name, + &new_snapshot_params, + ) + .await?; + Ok(HttpResponseCreated(snapshot.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for Snapshot requests #[derive(Deserialize, JsonSchema)] struct SnapshotPathParam { diff --git a/nexus/src/external_api/params.rs b/nexus/src/external_api/params.rs index 615b8e9051..3029623a80 100644 --- a/nexus/src/external_api/params.rs +++ b/nexus/src/external_api/params.rs @@ -301,6 +301,9 @@ pub struct SnapshotCreate { /// common identifying metadata #[serde(flatten)] pub identity: IdentityMetadataCreateParams, + + /// The name of the disk to be snapshotted + pub disk: Name, } /* diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index a530178853..276b8b8434 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -794,12 +794,11 @@ impl Nexus { Ok(()) } - pub async fn disk_create_snapshot( + pub async fn project_create_snapshot( self: &Arc, _opctx: &OpContext, _organization_name: &Name, _project_name: &Name, - _disk_name: &Name, _params: ¶ms::SnapshotCreate, ) -> CreateResult { unimplemented!(); From e47f3e9f4b8ee90bcc4af23a4bd2ca367f941b12 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 15 Mar 2022 16:17:54 -0400 Subject: [PATCH 7/7] Fix tests, endpoints, tags --- nexus/src/external_api/http_entrypoints.rs | 2 +- nexus/src/external_api/tag-config.json | 8 +- nexus/tests/output/nexus_tags.txt | 2 +- openapi/nexus.json | 139 +++++++++++---------- 4 files changed, 82 insertions(+), 69 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index a46afa8c0a..70448a4990 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1311,7 +1311,7 @@ async fn project_snapshots_get( /// Create a snapshot of a disk. #[endpoint { method = POST, - path = "/organizations/{organization_name}/projects/{project_name}/snapshot", + path = "/organizations/{organization_name}/projects/{project_name}/snapshots", tags = ["snapshots"], }] async fn project_snapshots_post( diff --git a/nexus/src/external_api/tag-config.json b/nexus/src/external_api/tag-config.json index a84bd660e4..0a9cea9c55 100644 --- a/nexus/src/external_api/tag-config.json +++ b/nexus/src/external_api/tag-config.json @@ -80,6 +80,12 @@ "url": "http://oxide.computer/docs/#xxx" } }, + "snapshots": { + "description": "Snapshots of Virtual Disks at a particular point in time.", + "external_docs": { + "url": "http://oxide.computer/docs/#xxx" + } + }, "subnets": { "description": "This tag should be moved into a generic network tag", "external_docs": { @@ -105,4 +111,4 @@ } } } -} \ No newline at end of file +} diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 9d13f83f43..2ddef25a4c 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -92,10 +92,10 @@ hardware_sleds_get_sled /hardware/sleds/{sled_id} API operations found with tag "snapshots" OPERATION ID URL PATH -project_disks_snapshot_post /organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/snapshot project_snapshots_delete_snapshot /organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name} project_snapshots_get /organizations/{organization_name}/projects/{project_name}/snapshots project_snapshots_get_snapshot /organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name} +project_snapshots_post /organizations/{organization_name}/projects/{project_name}/snapshots API operations found with tag "subnets" OPERATION ID URL PATH diff --git a/openapi/nexus.json b/openapi/nexus.json index 73b75ee58f..35f9c5f5f5 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -960,72 +960,6 @@ } } }, - "/organizations/{organization_name}/projects/{project_name}/disks/{disk_name}/snapshot": { - "post": { - "tags": [ - "snapshots" - ], - "summary": "Create a snapshot of a disk.", - "operationId": "project_disks_snapshot_post", - "parameters": [ - { - "in": "path", - "name": "disk_name", - "required": true, - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "simple" - }, - { - "in": "path", - "name": "organization_name", - "required": true, - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "simple" - }, - { - "in": "path", - "name": "project_name", - "required": true, - "schema": { - "$ref": "#/components/schemas/Name" - }, - "style": "simple" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SnapshotCreate" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "successful creation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Snapshot" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, "/organizations/{organization_name}/projects/{project_name}/instances": { "get": { "tags": [ @@ -2067,6 +2001,63 @@ } }, "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "snapshots" + ], + "summary": "Create a snapshot of a disk.", + "operationId": "project_snapshots_post", + "parameters": [ + { + "in": "path", + "name": "organization_name", + "description": "The organization's unique name.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + }, + { + "in": "path", + "name": "project_name", + "description": "The project's unique name within the organization.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SnapshotCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Snapshot" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } } }, "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}": { @@ -5887,12 +5878,21 @@ "description": { "type": "string" }, + "disk": { + "description": "The name of the disk to be snapshotted", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, "name": { "$ref": "#/components/schemas/Name" } }, "required": [ "description", + "disk", "name" ] }, @@ -7023,6 +7023,13 @@ "url": "http://oxide.computer/docs/#xxx" } }, + { + "name": "snapshots", + "description": "Snapshots of Virtual Disks at a particular point in time.", + "externalDocs": { + "url": "http://oxide.computer/docs/#xxx" + } + }, { "name": "subnets", "description": "This tag should be moved into a generic network tag",