Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ members = [
"nbd_server",
"package",
"pantry",
"pantry-api",
"pantry-client",
"pantry-types",
"protocol",
"repair-client",
"smf",
Expand Down Expand Up @@ -137,7 +139,9 @@ crucible-control-client = { path = "./control-client" }
# cleanup issues in the integration tests!
crucible-downstairs = { path = "./downstairs" }
crucible-pantry = { path = "./pantry" }
crucible-pantry-api = { path = "./pantry-api" }
crucible-pantry-client = { path = "./pantry-client" }
crucible-pantry-types = { path = "./pantry-types" }
crucible-protocol = { path = "./protocol" }
crucible-smf = { path = "./smf" }
dsc-client = { path = "./dsc-client" }
Expand Down
13 changes: 13 additions & 0 deletions pantry-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "crucible-pantry-api"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"

[dependencies]
crucible-client-types.workspace = true
crucible-pantry-types.workspace = true
crucible-workspace-hack.workspace = true
dropshot.workspace = true
schemars.workspace = true
serde.workspace = true
258 changes: 258 additions & 0 deletions pantry-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
// Copyright 2025 Oxide Computer Company

use crucible_client_types::{ReplaceResult, VolumeConstructionRequest};
use crucible_pantry_types::*;
use dropshot::{
HttpError, HttpResponseDeleted, HttpResponseOk,
HttpResponseUpdatedNoContent, Path, RequestContext, TypedBody,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[dropshot::api_description]
pub trait CruciblePantryApi {
type Context;

/// Get the Pantry's status
#[endpoint {
method = GET,
path = "/crucible/pantry/0",
}]
async fn pantry_status(
rqctx: RequestContext<Self::Context>,
) -> Result<HttpResponseOk<PantryStatus>, HttpError>;

/// Get a current Volume's status
#[endpoint {
method = GET,
path = "/crucible/pantry/0/volume/{id}",
}]
async fn volume_status(
rqctx: RequestContext<Self::Context>,
path: Path<VolumePath>,
) -> Result<HttpResponseOk<VolumeStatus>, HttpError>;

/// Construct a volume from a VolumeConstructionRequest, storing the result in
/// the Pantry.
#[endpoint {
method = POST,
path = "/crucible/pantry/0/volume/{id}",
}]
async fn attach(
rqctx: RequestContext<Self::Context>,
path: Path<VolumePath>,
body: TypedBody<AttachRequest>,
) -> Result<HttpResponseOk<AttachResult>, HttpError>;

/// Construct a volume from a VolumeConstructionRequest, storing the result in
/// the Pantry. Activate in a separate job so as not to block the request.
#[endpoint {
method = POST,
path = "/crucible/pantry/0/volume/{id}/background",
}]
async fn attach_activate_background(
rqctx: RequestContext<Self::Context>,
path: Path<VolumePath>,
body: TypedBody<AttachBackgroundRequest>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

/// Call a volume's target_replace function
#[endpoint {
method = POST,
path = "/crucible/pantry/0/volume/{id}/replace",
}]
async fn replace(
rqctx: RequestContext<Self::Context>,
path: Path<VolumePath>,
body: TypedBody<ReplaceRequest>,
) -> Result<HttpResponseOk<ReplaceResult>, HttpError>;

/// Poll to see if a Pantry background job is done
#[endpoint {
method = GET,
path = "/crucible/pantry/0/job/{id}/is-finished",
}]
async fn is_job_finished(
rqctx: RequestContext<Self::Context>,
path: Path<JobPath>,
) -> Result<HttpResponseOk<JobPollResponse>, HttpError>;

/// Block on returning a Pantry background job result, then return 200 OK if the
/// job executed OK, 500 otherwise.
#[endpoint {
method = GET,
path = "/crucible/pantry/0/job/{id}/ok",
}]
async fn job_result_ok(
rqctx: RequestContext<Self::Context>,
path: Path<JobPath>,
) -> Result<HttpResponseOk<JobResultOkResponse>, HttpError>;

/// Import data from a URL into a volume
#[endpoint {
method = POST,
path = "/crucible/pantry/0/volume/{id}/import-from-url",
}]
async fn import_from_url(
rqctx: RequestContext<Self::Context>,
path: Path<VolumePath>,
body: TypedBody<ImportFromUrlRequest>,
) -> Result<HttpResponseOk<ImportFromUrlResponse>, HttpError>;

/// Take a snapshot of a volume
#[endpoint {
method = POST,
path = "/crucible/pantry/0/volume/{id}/snapshot",
}]
async fn snapshot(
rqctx: RequestContext<Self::Context>,
path: Path<VolumePath>,
body: TypedBody<SnapshotRequest>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

/// Bulk write data into a volume at a specified offset
#[endpoint {
method = POST,
path = "/crucible/pantry/0/volume/{id}/bulk-write",
}]
async fn bulk_write(
rqctx: RequestContext<Self::Context>,
path: Path<VolumePath>,
body: TypedBody<BulkWriteRequest>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

/// Bulk read data from a volume at a specified offset
#[endpoint {
method = POST,
path = "/crucible/pantry/0/volume/{id}/bulk-read",
}]
async fn bulk_read(
rqctx: RequestContext<Self::Context>,
path: Path<VolumePath>,
body: TypedBody<BulkReadRequest>,
) -> Result<HttpResponseOk<BulkReadResponse>, HttpError>;

/// Scrub the volume (copy blocks from read-only parent to subvolumes)
#[endpoint {
method = POST,
path = "/crucible/pantry/0/volume/{id}/scrub",
}]
async fn scrub(
rqctx: RequestContext<Self::Context>,
path: Path<VolumePath>,
) -> Result<HttpResponseOk<ScrubResponse>, HttpError>;

/// Validate the digest of a whole volume
#[endpoint {
method = POST,
path = "/crucible/pantry/0/volume/{id}/validate",
}]
async fn validate(
rqctx: RequestContext<Self::Context>,
path: Path<VolumePath>,
body: TypedBody<ValidateRequest>,
) -> Result<HttpResponseOk<ValidateResponse>, HttpError>;

/// Deactivate a volume, removing it from the Pantry
#[endpoint {
method = DELETE,
path = "/crucible/pantry/0/volume/{id}",
}]
async fn detach(
rqctx: RequestContext<Self::Context>,
path: Path<VolumePath>,
) -> Result<HttpResponseDeleted, HttpError>;
}

#[derive(Deserialize, JsonSchema)]
pub struct VolumePath {
pub id: String,
}

#[derive(Deserialize, JsonSchema)]
pub struct AttachRequest {
pub volume_construction_request: VolumeConstructionRequest,
}

#[derive(Serialize, JsonSchema)]
pub struct AttachResult {
pub id: String,
}

#[derive(Deserialize, JsonSchema)]
pub struct AttachBackgroundRequest {
pub volume_construction_request: VolumeConstructionRequest,
pub job_id: String,
}

#[derive(Deserialize, JsonSchema)]
pub struct ReplaceRequest {
pub volume_construction_request: VolumeConstructionRequest,
}

#[derive(Deserialize, JsonSchema)]
pub struct JobPath {
pub id: String,
}

#[derive(Serialize, JsonSchema)]
pub struct JobPollResponse {
pub job_is_finished: bool,
}

#[derive(Serialize, JsonSchema)]
pub struct JobResultOkResponse {
pub job_result_ok: bool,
}

#[derive(Deserialize, JsonSchema)]
pub struct ImportFromUrlRequest {
pub url: String,
pub expected_digest: Option<ExpectedDigest>,
}

#[derive(Serialize, JsonSchema)]
pub struct ImportFromUrlResponse {
pub job_id: String,
}

#[derive(Deserialize, JsonSchema)]
pub struct SnapshotRequest {
pub snapshot_id: String,
}

#[derive(Deserialize, JsonSchema)]
pub struct BulkWriteRequest {
pub offset: u64,
pub base64_encoded_data: String,
}

#[derive(Deserialize, JsonSchema)]
pub struct BulkReadRequest {
pub offset: u64,
pub size: usize,
}

#[derive(Serialize, JsonSchema)]
pub struct BulkReadResponse {
pub base64_encoded_data: String,
}

#[derive(Serialize, JsonSchema)]
pub struct ScrubResponse {
pub job_id: String,
}

#[derive(Deserialize, JsonSchema)]
pub struct ValidateRequest {
pub expected_digest: ExpectedDigest,

// Size to validate in bytes, starting from offset 0. If not specified, the
// total volume size is used.
pub size_to_validate: Option<u64>,
}

#[derive(Serialize, JsonSchema)]
pub struct ValidateResponse {
pub job_id: String,
}
10 changes: 10 additions & 0 deletions pantry-types/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "crucible-pantry-types"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"

[dependencies]
crucible-workspace-hack.workspace = true
schemars.workspace = true
serde.workspace = true
32 changes: 32 additions & 0 deletions pantry-types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2025 Oxide Computer Company

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, JsonSchema)]
pub struct PantryStatus {
/// Which volumes does this Pantry know about? Note this may include volumes
/// that are no longer active, and haven't been garbage collected yet.
pub volumes: Vec<String>,

/// How many job handles?
pub num_job_handles: usize,
}

#[derive(Serialize, JsonSchema)]
pub struct VolumeStatus {
/// Is the Volume currently active?
pub active: bool,

/// Has the Pantry ever seen this Volume active?
pub seen_active: bool,

/// How many job handles are there for this Volume?
pub num_job_handles: usize,
}

#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExpectedDigest {
Sha256(String),
}
2 changes: 2 additions & 0 deletions pantry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ bytes.workspace = true
base64.workspace = true
chrono.workspace = true
clap.workspace = true
crucible-pantry-api.workspace = true
crucible-pantry-types.workspace = true
dropshot.workspace = true
futures.workspace = true
http.workspace = true
Expand Down
Loading