Skip to content

Commit 6db4479

Browse files
authored
[nexus] Add stubs for snapshot APIs (#748)
First part of #735
1 parent a0b2e89 commit 6db4479

File tree

10 files changed

+664
-4
lines changed

10 files changed

+664
-4
lines changed

common/src/sql/dbinit.sql

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,35 @@ CREATE INDEX ON omicron.public.disk (
371371
time_deleted IS NULL AND attach_instance_id IS NOT NULL;
372372

373373

374+
CREATE TABLE omicron.public.snapshot (
375+
/* Identity metadata (resource) */
376+
id UUID PRIMARY KEY,
377+
name STRING(63) NOT NULL,
378+
description STRING(512) NOT NULL,
379+
time_created TIMESTAMPTZ NOT NULL,
380+
time_modified TIMESTAMPTZ NOT NULL,
381+
/* Indicates that the object has been deleted */
382+
time_deleted TIMESTAMPTZ,
383+
384+
/* Every Snapshot is in exactly one Project at a time. */
385+
project_id UUID NOT NULL,
386+
387+
/* Every Snapshot originated from a single disk */
388+
disk_id UUID NOT NULL,
389+
390+
/* Every Snapshot consists of a root volume */
391+
volume_id UUID NOT NULL,
392+
393+
/* Disk configuration (from the time the snapshot was taken) */
394+
size_bytes INT NOT NULL
395+
);
396+
397+
CREATE UNIQUE INDEX ON omicron.public.snapshot (
398+
project_id,
399+
name
400+
) WHERE
401+
time_deleted IS NULL;
402+
374403
/*
375404
* Oximeter collector servers.
376405
*/

nexus/src/db/model.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ use crate::db::identity::{Asset, Resource};
99
use crate::db::schema::{
1010
console_session, dataset, disk, instance, metric_producer,
1111
network_interface, organization, oximeter, project, rack, region,
12-
role_assignment_builtin, role_builtin, router_route, sled,
12+
role_assignment_builtin, role_builtin, router_route, sled, snapshot,
1313
update_available_artifact, user_builtin, volume, vpc, vpc_firewall_rule,
1414
vpc_router, vpc_subnet, zpool,
1515
};
1616
use crate::defaults;
1717
use crate::external_api::params;
18+
use crate::external_api::views;
1819
use crate::internal_api;
1920
use chrono::{DateTime, Utc};
2021
use db_macros::{Asset, Resource};
@@ -1260,6 +1261,40 @@ impl Into<external::DiskState> for DiskState {
12601261
}
12611262
}
12621263

1264+
#[derive(
1265+
Queryable,
1266+
Insertable,
1267+
Selectable,
1268+
Clone,
1269+
Debug,
1270+
Resource,
1271+
Serialize,
1272+
Deserialize,
1273+
)]
1274+
#[table_name = "snapshot"]
1275+
pub struct Snapshot {
1276+
#[diesel(embed)]
1277+
identity: SnapshotIdentity,
1278+
1279+
project_id: Uuid,
1280+
disk_id: Uuid,
1281+
volume_id: Uuid,
1282+
1283+
#[column_name = "size_bytes"]
1284+
pub size: ByteCount,
1285+
}
1286+
1287+
impl From<Snapshot> for views::Snapshot {
1288+
fn from(snapshot: Snapshot) -> Self {
1289+
Self {
1290+
identity: snapshot.identity(),
1291+
project_id: snapshot.project_id,
1292+
disk_id: snapshot.disk_id,
1293+
size: snapshot.size.into(),
1294+
}
1295+
}
1296+
}
1297+
12631298
/// Information announced by a metric server, used so that clients can contact it and collect
12641299
/// available metric data from it.
12651300
#[derive(Queryable, Insertable, Debug, Clone, Selectable, Asset)]

nexus/src/db/schema.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ table! {
2626
}
2727
}
2828

29+
table! {
30+
snapshot (id) {
31+
id -> Uuid,
32+
name -> Text,
33+
description -> Text,
34+
time_created -> Timestamptz,
35+
time_modified -> Timestamptz,
36+
time_deleted -> Nullable<Timestamptz>,
37+
38+
project_id -> Uuid,
39+
disk_id -> Uuid,
40+
volume_id -> Uuid,
41+
size_bytes -> Int8,
42+
}
43+
}
44+
2945
table! {
3046
instance (id) {
3147
id -> Uuid,

nexus/src/external_api/http_entrypoints.rs

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ use crate::ServerContext;
1212

1313
use super::{
1414
console_api, params,
15-
views::{Organization, Project, Rack, Role, Sled, User, Vpc, VpcSubnet},
15+
views::{
16+
Organization, Project, Rack, Role, Sled, Snapshot, User, Vpc, VpcSubnet,
17+
},
1618
};
1719
use crate::context::OpContext;
1820
use dropshot::endpoint;
@@ -103,6 +105,11 @@ pub fn external_api() -> NexusApiDescription {
103105
api.register(instance_disks_attach)?;
104106
api.register(instance_disks_detach)?;
105107

108+
api.register(project_snapshots_get)?;
109+
api.register(project_snapshots_post)?;
110+
api.register(project_snapshots_get_snapshot)?;
111+
api.register(project_snapshots_delete_snapshot)?;
112+
106113
api.register(project_vpcs_get)?;
107114
api.register(project_vpcs_post)?;
108115
api.register(project_vpcs_get_vpc)?;
@@ -1261,6 +1268,148 @@ async fn instance_network_interfaces_get_interface(
12611268
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
12621269
}
12631270

1271+
/*
1272+
* Snapshots
1273+
*/
1274+
1275+
/// List snapshots in a project.
1276+
#[endpoint {
1277+
method = GET,
1278+
path = "/organizations/{organization_name}/projects/{project_name}/snapshots",
1279+
tags = ["snapshots"],
1280+
}]
1281+
async fn project_snapshots_get(
1282+
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
1283+
query_params: Query<PaginatedByName>,
1284+
path_params: Path<ProjectPathParam>,
1285+
) -> Result<HttpResponseOk<ResultsPage<Snapshot>>, HttpError> {
1286+
let apictx = rqctx.context();
1287+
let nexus = &apictx.nexus;
1288+
let query = query_params.into_inner();
1289+
let path = path_params.into_inner();
1290+
let organization_name = &path.organization_name;
1291+
let project_name = &path.project_name;
1292+
let handler = async {
1293+
let opctx = OpContext::for_external_api(&rqctx).await?;
1294+
let snapshots = nexus
1295+
.project_list_snapshots(
1296+
&opctx,
1297+
organization_name,
1298+
project_name,
1299+
&data_page_params_for(&rqctx, &query)?
1300+
.map_name(|n| Name::ref_cast(n)),
1301+
)
1302+
.await?
1303+
.into_iter()
1304+
.map(|d| d.into())
1305+
.collect();
1306+
Ok(HttpResponseOk(ScanByName::results_page(&query, snapshots)?))
1307+
};
1308+
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
1309+
}
1310+
1311+
/// Create a snapshot of a disk.
1312+
#[endpoint {
1313+
method = POST,
1314+
path = "/organizations/{organization_name}/projects/{project_name}/snapshots",
1315+
tags = ["snapshots"],
1316+
}]
1317+
async fn project_snapshots_post(
1318+
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
1319+
path_params: Path<ProjectPathParam>,
1320+
new_snapshot: TypedBody<params::SnapshotCreate>,
1321+
) -> Result<HttpResponseCreated<Snapshot>, HttpError> {
1322+
let apictx = rqctx.context();
1323+
let nexus = &apictx.nexus;
1324+
let path = path_params.into_inner();
1325+
let organization_name = &path.organization_name;
1326+
let project_name = &path.project_name;
1327+
let new_snapshot_params = &new_snapshot.into_inner();
1328+
let handler = async {
1329+
let opctx = OpContext::for_external_api(&rqctx).await?;
1330+
let snapshot = nexus
1331+
.project_create_snapshot(
1332+
&opctx,
1333+
&organization_name,
1334+
&project_name,
1335+
&new_snapshot_params,
1336+
)
1337+
.await?;
1338+
Ok(HttpResponseCreated(snapshot.into()))
1339+
};
1340+
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
1341+
}
1342+
1343+
/// Path parameters for Snapshot requests
1344+
#[derive(Deserialize, JsonSchema)]
1345+
struct SnapshotPathParam {
1346+
organization_name: Name,
1347+
project_name: Name,
1348+
snapshot_name: Name,
1349+
}
1350+
1351+
/// Get a snapshot in a project.
1352+
#[endpoint {
1353+
method = GET,
1354+
path = "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}",
1355+
tags = ["snapshots"],
1356+
}]
1357+
async fn project_snapshots_get_snapshot(
1358+
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
1359+
path_params: Path<SnapshotPathParam>,
1360+
) -> Result<HttpResponseOk<Snapshot>, HttpError> {
1361+
let apictx = rqctx.context();
1362+
let nexus = &apictx.nexus;
1363+
let path = path_params.into_inner();
1364+
let organization_name = &path.organization_name;
1365+
let project_name = &path.project_name;
1366+
let snapshot_name = &path.snapshot_name;
1367+
let handler = async {
1368+
let opctx = OpContext::for_external_api(&rqctx).await?;
1369+
let snapshot = nexus
1370+
.snapshot_fetch(
1371+
&opctx,
1372+
&organization_name,
1373+
&project_name,
1374+
&snapshot_name,
1375+
)
1376+
.await?;
1377+
Ok(HttpResponseOk(snapshot.into()))
1378+
};
1379+
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
1380+
}
1381+
1382+
/// Delete a snapshot from a project.
1383+
#[endpoint {
1384+
method = DELETE,
1385+
path = "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}",
1386+
tags = ["snapshots"],
1387+
}]
1388+
async fn project_snapshots_delete_snapshot(
1389+
rqctx: Arc<RequestContext<Arc<ServerContext>>>,
1390+
path_params: Path<SnapshotPathParam>,
1391+
) -> Result<HttpResponseDeleted, HttpError> {
1392+
let apictx = rqctx.context();
1393+
let nexus = &apictx.nexus;
1394+
let path = path_params.into_inner();
1395+
let organization_name = &path.organization_name;
1396+
let project_name = &path.project_name;
1397+
let snapshot_name = &path.snapshot_name;
1398+
let handler = async {
1399+
let opctx = OpContext::for_external_api(&rqctx).await?;
1400+
nexus
1401+
.project_delete_snapshot(
1402+
&opctx,
1403+
&organization_name,
1404+
&project_name,
1405+
&snapshot_name,
1406+
)
1407+
.await?;
1408+
Ok(HttpResponseDeleted())
1409+
};
1410+
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
1411+
}
1412+
12641413
/*
12651414
* VPCs
12661415
*/

nexus/src/external_api/params.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,21 @@ pub struct NetworkInterfaceIdentifier {
291291
pub interface_name: Name,
292292
}
293293

294+
/*
295+
* SNAPSHOTS
296+
*/
297+
298+
/// Create-time parameters for a [`Snapshot`](omicron_common::api::external::Snapshot)
299+
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
300+
pub struct SnapshotCreate {
301+
/// common identifying metadata
302+
#[serde(flatten)]
303+
pub identity: IdentityMetadataCreateParams,
304+
305+
/// The name of the disk to be snapshotted
306+
pub disk: Name,
307+
}
308+
294309
/*
295310
* BUILT-IN USERS
296311
*

nexus/src/external_api/tag-config.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@
8080
"url": "http://oxide.computer/docs/#xxx"
8181
}
8282
},
83+
"snapshots": {
84+
"description": "Snapshots of Virtual Disks at a particular point in time.",
85+
"external_docs": {
86+
"url": "http://oxide.computer/docs/#xxx"
87+
}
88+
},
8389
"subnets": {
8490
"description": "This tag should be moved into a generic network tag",
8591
"external_docs": {
@@ -105,4 +111,4 @@
105111
}
106112
}
107113
}
108-
}
114+
}

nexus/src/external_api/views.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use crate::db::identity::{Asset, Resource};
1111
use crate::db::model;
1212
use api_identity::ObjectIdentity;
1313
use omicron_common::api::external::{
14-
IdentityMetadata, Ipv4Net, Ipv6Net, Name, ObjectIdentity, RoleName,
14+
ByteCount, IdentityMetadata, Ipv4Net, Ipv6Net, Name, ObjectIdentity,
15+
RoleName,
1516
};
1617
use schemars::JsonSchema;
1718
use serde::{Deserialize, Serialize};
@@ -64,6 +65,21 @@ impl Into<Project> for model::Project {
6465
}
6566
}
6667

68+
/*
69+
* SNAPSHOTS
70+
*/
71+
72+
/// Client view of a Snapshot
73+
#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)]
74+
pub struct Snapshot {
75+
#[serde(flatten)]
76+
pub identity: IdentityMetadata,
77+
78+
pub project_id: Uuid,
79+
pub disk_id: Uuid,
80+
pub size: ByteCount,
81+
}
82+
6783
/*
6884
* VPCs
6985
*/

0 commit comments

Comments
 (0)