Skip to content

Commit 32390aa

Browse files
authored
[crucible-pantry] migrate to API trait (#1767)
Part of oxidecomputer/omicron#8922.
1 parent dfc6e86 commit 32390aa

File tree

11 files changed

+543
-422
lines changed

11 files changed

+543
-422
lines changed

Cargo.lock

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ members = [
1717
"nbd_server",
1818
"package",
1919
"pantry",
20+
"pantry-api",
2021
"pantry-client",
22+
"pantry-types",
2123
"protocol",
2224
"repair-client",
2325
"smf",
@@ -137,7 +139,9 @@ crucible-control-client = { path = "./control-client" }
137139
# cleanup issues in the integration tests!
138140
crucible-downstairs = { path = "./downstairs" }
139141
crucible-pantry = { path = "./pantry" }
142+
crucible-pantry-api = { path = "./pantry-api" }
140143
crucible-pantry-client = { path = "./pantry-client" }
144+
crucible-pantry-types = { path = "./pantry-types" }
141145
crucible-protocol = { path = "./protocol" }
142146
crucible-smf = { path = "./smf" }
143147
dsc-client = { path = "./dsc-client" }

pantry-api/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "crucible-pantry-api"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MPL-2.0"
6+
7+
[dependencies]
8+
crucible-client-types.workspace = true
9+
crucible-pantry-types.workspace = true
10+
crucible-workspace-hack.workspace = true
11+
dropshot.workspace = true
12+
schemars.workspace = true
13+
serde.workspace = true

pantry-api/src/lib.rs

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
// Copyright 2025 Oxide Computer Company
2+
3+
use crucible_client_types::{ReplaceResult, VolumeConstructionRequest};
4+
use crucible_pantry_types::*;
5+
use dropshot::{
6+
HttpError, HttpResponseDeleted, HttpResponseOk,
7+
HttpResponseUpdatedNoContent, Path, RequestContext, TypedBody,
8+
};
9+
use schemars::JsonSchema;
10+
use serde::{Deserialize, Serialize};
11+
12+
#[dropshot::api_description]
13+
pub trait CruciblePantryApi {
14+
type Context;
15+
16+
/// Get the Pantry's status
17+
#[endpoint {
18+
method = GET,
19+
path = "/crucible/pantry/0",
20+
}]
21+
async fn pantry_status(
22+
rqctx: RequestContext<Self::Context>,
23+
) -> Result<HttpResponseOk<PantryStatus>, HttpError>;
24+
25+
/// Get a current Volume's status
26+
#[endpoint {
27+
method = GET,
28+
path = "/crucible/pantry/0/volume/{id}",
29+
}]
30+
async fn volume_status(
31+
rqctx: RequestContext<Self::Context>,
32+
path: Path<VolumePath>,
33+
) -> Result<HttpResponseOk<VolumeStatus>, HttpError>;
34+
35+
/// Construct a volume from a VolumeConstructionRequest, storing the result in
36+
/// the Pantry.
37+
#[endpoint {
38+
method = POST,
39+
path = "/crucible/pantry/0/volume/{id}",
40+
}]
41+
async fn attach(
42+
rqctx: RequestContext<Self::Context>,
43+
path: Path<VolumePath>,
44+
body: TypedBody<AttachRequest>,
45+
) -> Result<HttpResponseOk<AttachResult>, HttpError>;
46+
47+
/// Construct a volume from a VolumeConstructionRequest, storing the result in
48+
/// the Pantry. Activate in a separate job so as not to block the request.
49+
#[endpoint {
50+
method = POST,
51+
path = "/crucible/pantry/0/volume/{id}/background",
52+
}]
53+
async fn attach_activate_background(
54+
rqctx: RequestContext<Self::Context>,
55+
path: Path<VolumePath>,
56+
body: TypedBody<AttachBackgroundRequest>,
57+
) -> Result<HttpResponseUpdatedNoContent, HttpError>;
58+
59+
/// Call a volume's target_replace function
60+
#[endpoint {
61+
method = POST,
62+
path = "/crucible/pantry/0/volume/{id}/replace",
63+
}]
64+
async fn replace(
65+
rqctx: RequestContext<Self::Context>,
66+
path: Path<VolumePath>,
67+
body: TypedBody<ReplaceRequest>,
68+
) -> Result<HttpResponseOk<ReplaceResult>, HttpError>;
69+
70+
/// Poll to see if a Pantry background job is done
71+
#[endpoint {
72+
method = GET,
73+
path = "/crucible/pantry/0/job/{id}/is-finished",
74+
}]
75+
async fn is_job_finished(
76+
rqctx: RequestContext<Self::Context>,
77+
path: Path<JobPath>,
78+
) -> Result<HttpResponseOk<JobPollResponse>, HttpError>;
79+
80+
/// Block on returning a Pantry background job result, then return 200 OK if the
81+
/// job executed OK, 500 otherwise.
82+
#[endpoint {
83+
method = GET,
84+
path = "/crucible/pantry/0/job/{id}/ok",
85+
}]
86+
async fn job_result_ok(
87+
rqctx: RequestContext<Self::Context>,
88+
path: Path<JobPath>,
89+
) -> Result<HttpResponseOk<JobResultOkResponse>, HttpError>;
90+
91+
/// Import data from a URL into a volume
92+
#[endpoint {
93+
method = POST,
94+
path = "/crucible/pantry/0/volume/{id}/import-from-url",
95+
}]
96+
async fn import_from_url(
97+
rqctx: RequestContext<Self::Context>,
98+
path: Path<VolumePath>,
99+
body: TypedBody<ImportFromUrlRequest>,
100+
) -> Result<HttpResponseOk<ImportFromUrlResponse>, HttpError>;
101+
102+
/// Take a snapshot of a volume
103+
#[endpoint {
104+
method = POST,
105+
path = "/crucible/pantry/0/volume/{id}/snapshot",
106+
}]
107+
async fn snapshot(
108+
rqctx: RequestContext<Self::Context>,
109+
path: Path<VolumePath>,
110+
body: TypedBody<SnapshotRequest>,
111+
) -> Result<HttpResponseUpdatedNoContent, HttpError>;
112+
113+
/// Bulk write data into a volume at a specified offset
114+
#[endpoint {
115+
method = POST,
116+
path = "/crucible/pantry/0/volume/{id}/bulk-write",
117+
}]
118+
async fn bulk_write(
119+
rqctx: RequestContext<Self::Context>,
120+
path: Path<VolumePath>,
121+
body: TypedBody<BulkWriteRequest>,
122+
) -> Result<HttpResponseUpdatedNoContent, HttpError>;
123+
124+
/// Bulk read data from a volume at a specified offset
125+
#[endpoint {
126+
method = POST,
127+
path = "/crucible/pantry/0/volume/{id}/bulk-read",
128+
}]
129+
async fn bulk_read(
130+
rqctx: RequestContext<Self::Context>,
131+
path: Path<VolumePath>,
132+
body: TypedBody<BulkReadRequest>,
133+
) -> Result<HttpResponseOk<BulkReadResponse>, HttpError>;
134+
135+
/// Scrub the volume (copy blocks from read-only parent to subvolumes)
136+
#[endpoint {
137+
method = POST,
138+
path = "/crucible/pantry/0/volume/{id}/scrub",
139+
}]
140+
async fn scrub(
141+
rqctx: RequestContext<Self::Context>,
142+
path: Path<VolumePath>,
143+
) -> Result<HttpResponseOk<ScrubResponse>, HttpError>;
144+
145+
/// Validate the digest of a whole volume
146+
#[endpoint {
147+
method = POST,
148+
path = "/crucible/pantry/0/volume/{id}/validate",
149+
}]
150+
async fn validate(
151+
rqctx: RequestContext<Self::Context>,
152+
path: Path<VolumePath>,
153+
body: TypedBody<ValidateRequest>,
154+
) -> Result<HttpResponseOk<ValidateResponse>, HttpError>;
155+
156+
/// Deactivate a volume, removing it from the Pantry
157+
#[endpoint {
158+
method = DELETE,
159+
path = "/crucible/pantry/0/volume/{id}",
160+
}]
161+
async fn detach(
162+
rqctx: RequestContext<Self::Context>,
163+
path: Path<VolumePath>,
164+
) -> Result<HttpResponseDeleted, HttpError>;
165+
}
166+
167+
#[derive(Deserialize, JsonSchema)]
168+
pub struct VolumePath {
169+
pub id: String,
170+
}
171+
172+
#[derive(Deserialize, JsonSchema)]
173+
pub struct AttachRequest {
174+
pub volume_construction_request: VolumeConstructionRequest,
175+
}
176+
177+
#[derive(Serialize, JsonSchema)]
178+
pub struct AttachResult {
179+
pub id: String,
180+
}
181+
182+
#[derive(Deserialize, JsonSchema)]
183+
pub struct AttachBackgroundRequest {
184+
pub volume_construction_request: VolumeConstructionRequest,
185+
pub job_id: String,
186+
}
187+
188+
#[derive(Deserialize, JsonSchema)]
189+
pub struct ReplaceRequest {
190+
pub volume_construction_request: VolumeConstructionRequest,
191+
}
192+
193+
#[derive(Deserialize, JsonSchema)]
194+
pub struct JobPath {
195+
pub id: String,
196+
}
197+
198+
#[derive(Serialize, JsonSchema)]
199+
pub struct JobPollResponse {
200+
pub job_is_finished: bool,
201+
}
202+
203+
#[derive(Serialize, JsonSchema)]
204+
pub struct JobResultOkResponse {
205+
pub job_result_ok: bool,
206+
}
207+
208+
#[derive(Deserialize, JsonSchema)]
209+
pub struct ImportFromUrlRequest {
210+
pub url: String,
211+
pub expected_digest: Option<ExpectedDigest>,
212+
}
213+
214+
#[derive(Serialize, JsonSchema)]
215+
pub struct ImportFromUrlResponse {
216+
pub job_id: String,
217+
}
218+
219+
#[derive(Deserialize, JsonSchema)]
220+
pub struct SnapshotRequest {
221+
pub snapshot_id: String,
222+
}
223+
224+
#[derive(Deserialize, JsonSchema)]
225+
pub struct BulkWriteRequest {
226+
pub offset: u64,
227+
pub base64_encoded_data: String,
228+
}
229+
230+
#[derive(Deserialize, JsonSchema)]
231+
pub struct BulkReadRequest {
232+
pub offset: u64,
233+
pub size: usize,
234+
}
235+
236+
#[derive(Serialize, JsonSchema)]
237+
pub struct BulkReadResponse {
238+
pub base64_encoded_data: String,
239+
}
240+
241+
#[derive(Serialize, JsonSchema)]
242+
pub struct ScrubResponse {
243+
pub job_id: String,
244+
}
245+
246+
#[derive(Deserialize, JsonSchema)]
247+
pub struct ValidateRequest {
248+
pub expected_digest: ExpectedDigest,
249+
250+
// Size to validate in bytes, starting from offset 0. If not specified, the
251+
// total volume size is used.
252+
pub size_to_validate: Option<u64>,
253+
}
254+
255+
#[derive(Serialize, JsonSchema)]
256+
pub struct ValidateResponse {
257+
pub job_id: String,
258+
}

pantry-types/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "crucible-pantry-types"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MPL-2.0"
6+
7+
[dependencies]
8+
crucible-workspace-hack.workspace = true
9+
schemars.workspace = true
10+
serde.workspace = true

pantry-types/src/lib.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2025 Oxide Computer Company
2+
3+
use schemars::JsonSchema;
4+
use serde::{Deserialize, Serialize};
5+
6+
#[derive(Serialize, JsonSchema)]
7+
pub struct PantryStatus {
8+
/// Which volumes does this Pantry know about? Note this may include volumes
9+
/// that are no longer active, and haven't been garbage collected yet.
10+
pub volumes: Vec<String>,
11+
12+
/// How many job handles?
13+
pub num_job_handles: usize,
14+
}
15+
16+
#[derive(Serialize, JsonSchema)]
17+
pub struct VolumeStatus {
18+
/// Is the Volume currently active?
19+
pub active: bool,
20+
21+
/// Has the Pantry ever seen this Volume active?
22+
pub seen_active: bool,
23+
24+
/// How many job handles are there for this Volume?
25+
pub num_job_handles: usize,
26+
}
27+
28+
#[derive(Debug, Deserialize, JsonSchema)]
29+
#[serde(rename_all = "snake_case")]
30+
pub enum ExpectedDigest {
31+
Sha256(String),
32+
}

pantry/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ bytes.workspace = true
1010
base64.workspace = true
1111
chrono.workspace = true
1212
clap.workspace = true
13+
crucible-pantry-api.workspace = true
14+
crucible-pantry-types.workspace = true
1315
dropshot.workspace = true
1416
futures.workspace = true
1517
http.workspace = true

0 commit comments

Comments
 (0)