diff --git a/Cargo.lock b/Cargo.lock index 55b46d998..44cd3c0e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1163,12 +1163,10 @@ dependencies = [ name = "crucible-agent-api" version = "0.1.0" dependencies = [ - "crucible-agent-types", + "crucible-agent-types-versions", "crucible-workspace-hack", "dropshot", "dropshot-api-manager-types", - "schemars 0.8.22", - "serde", ] [[package]] @@ -1190,7 +1188,7 @@ dependencies = [ name = "crucible-agent-types" version = "0.1.0" dependencies = [ - "chrono", + "crucible-agent-types-versions", "crucible-smf 0.0.0", "crucible-workspace-hack", "schemars 0.8.22", @@ -1198,6 +1196,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "crucible-agent-types-versions" +version = "0.1.0" +dependencies = [ + "chrono", + "crucible-workspace-hack", + "schemars 0.8.22", + "serde", +] + [[package]] name = "crucible-client-types" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index e058cc5e6..26dbd8852 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "agent-api", "agent-client", "agent-types", + "agent-types/versions", "crucible-client-types", "common", "control-client", @@ -145,6 +146,7 @@ crucible = { path = "./upstairs" } crucible-agent-api = { path = "./agent-api" } crucible-agent-client = { path = "./agent-client" } crucible-agent-types = { path = "./agent-types" } +crucible-agent-types-versions = { path = "./agent-types/versions" } crucible-client-types = { path = "./crucible-client-types" } crucible-common = { path = "./common" } crucible-control-client = { path = "./control-client" } diff --git a/agent-api/Cargo.toml b/agent-api/Cargo.toml index 5b8b2f798..414cf202f 100644 --- a/agent-api/Cargo.toml +++ b/agent-api/Cargo.toml @@ -5,9 +5,7 @@ license = "MPL-2.0" edition = "2024" [dependencies] -crucible-agent-types.workspace = true +crucible-agent-types-versions.workspace = true crucible-workspace-hack.workspace = true dropshot.workspace = true dropshot-api-manager-types.workspace = true -schemars.workspace = true -serde.workspace = true diff --git a/agent-api/src/lib.rs b/agent-api/src/lib.rs index 50e02b20b..44e54c322 100644 --- a/agent-api/src/lib.rs +++ b/agent-api/src/lib.rs @@ -1,18 +1,11 @@ -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company -use std::collections::BTreeMap; - -use crucible_agent_types::{ - region::{CreateRegion, Region, RegionId}, - snapshot::{RunningSnapshot, Snapshot}, -}; +use crucible_agent_types_versions::latest; use dropshot::{ HttpError, HttpResponseDeleted, HttpResponseOk, Path, RequestContext, TypedBody, }; use dropshot_api_manager_types::api_versions; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; api_versions!([ // WHEN CHANGING THE API (part 1 of 2): @@ -51,7 +44,7 @@ pub trait CrucibleAgentApi { }] async fn region_list( rqctx: RequestContext, - ) -> Result>, HttpError>; + ) -> Result>, HttpError>; #[endpoint { method = POST, @@ -59,8 +52,8 @@ pub trait CrucibleAgentApi { }] async fn region_create( rqctx: RequestContext, - body: TypedBody, - ) -> Result, HttpError>; + body: TypedBody, + ) -> Result, HttpError>; #[endpoint { method = GET, @@ -68,8 +61,8 @@ pub trait CrucibleAgentApi { }] async fn region_get( rqctx: RequestContext, - path: Path, - ) -> Result, HttpError>; + path: Path, + ) -> Result, HttpError>; #[endpoint { method = DELETE, @@ -77,7 +70,7 @@ pub trait CrucibleAgentApi { }] async fn region_delete( rqctx: RequestContext, - path: Path, + path: Path, ) -> Result; #[endpoint { @@ -86,8 +79,8 @@ pub trait CrucibleAgentApi { }] async fn region_get_snapshots( rqctx: RequestContext, - path: Path, - ) -> Result, HttpError>; + path: Path, + ) -> Result, HttpError>; #[endpoint { method = GET, @@ -95,8 +88,8 @@ pub trait CrucibleAgentApi { }] async fn region_get_snapshot( rqctx: RequestContext, - path: Path, - ) -> Result, HttpError>; + path: Path, + ) -> Result, HttpError>; #[endpoint { method = DELETE, @@ -104,7 +97,7 @@ pub trait CrucibleAgentApi { }] async fn region_delete_snapshot( rqctx: RequestContext, - path: Path, + path: Path, ) -> Result; #[endpoint { @@ -113,8 +106,8 @@ pub trait CrucibleAgentApi { }] async fn region_run_snapshot( rqctx: RequestContext, - path: Path, - ) -> Result, HttpError>; + path: Path, + ) -> Result, HttpError>; #[endpoint { method = DELETE, @@ -122,35 +115,6 @@ pub trait CrucibleAgentApi { }] async fn region_delete_running_snapshot( rc: RequestContext, - path: Path, + path: Path, ) -> Result; } - -#[derive(Deserialize, JsonSchema)] -pub struct RegionPath { - pub id: RegionId, -} - -#[derive(Serialize, JsonSchema)] -pub struct GetSnapshotResponse { - pub snapshots: Vec, - pub running_snapshots: BTreeMap, -} - -#[derive(Deserialize, JsonSchema)] -pub struct GetSnapshotPath { - pub id: RegionId, - pub name: String, -} - -#[derive(Deserialize, JsonSchema)] -pub struct DeleteSnapshotPath { - pub id: RegionId, - pub name: String, -} - -#[derive(Deserialize, JsonSchema)] -pub struct RunSnapshotPath { - pub id: RegionId, - pub name: String, -} diff --git a/agent-types/Cargo.toml b/agent-types/Cargo.toml index 98be73745..31d58191c 100644 --- a/agent-types/Cargo.toml +++ b/agent-types/Cargo.toml @@ -5,11 +5,11 @@ license = "MPL-2.0" edition = "2024" [dependencies] -chrono.workspace = true +crucible-agent-types-versions.workspace = true crucible-smf.workspace = true crucible-workspace-hack.workspace = true -serde.workspace = true schemars.workspace = true +serde.workspace = true [dev-dependencies] serde_json.workspace = true diff --git a/agent-types/src/lib.rs b/agent-types/src/lib.rs index 01b76f6e5..2469a2778 100644 --- a/agent-types/src/lib.rs +++ b/agent-types/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company pub mod region; pub mod smf; diff --git a/agent-types/src/region.rs b/agent-types/src/region.rs index f9040e5af..7bc94fdf4 100644 --- a/agent-types/src/region.rs +++ b/agent-types/src/region.rs @@ -1,204 +1,121 @@ -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company + +pub use crucible_agent_types_versions::latest::region::*; -use std::net::SocketAddr; use std::path::Path; use crucible_smf::scf_type_t::*; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; use crate::smf::SmfProperty; -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] -#[serde(rename_all = "lowercase")] -pub enum State { - Requested, - Created, - Tombstoned, - Destroyed, - Failed, -} +/// Given a root directory, return a list of SMF properties to ensure for the +/// corresponding running downstairs instance. +pub fn region_smf_properties<'a>( + region: &'a Region, + dir: &Path, +) -> Vec> { + let mut results = vec![ + SmfProperty { + name: "directory", + typ: SCF_TYPE_ASTRING, + val: dir.to_str().unwrap().to_string(), + }, + SmfProperty { + name: "port", + typ: SCF_TYPE_COUNT, + val: region.port_number.to_string(), + }, + ]; + + if region.cert_pem.is_some() { + let mut path = dir.to_path_buf(); + path.push("cert.pem"); + let path = path.into_os_string().into_string().unwrap(); + + results.push(SmfProperty { + name: "cert_pem_path", + typ: SCF_TYPE_ASTRING, + val: path, + }); + } -// If not provided, select None as the default for source. -fn source_default() -> Option { - None -} -// If not provided, select false as the default for read only. -fn read_only_default() -> bool { - false -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] -pub struct Region { - pub id: RegionId, - pub state: State, - - // Creation parameters - pub block_size: u64, - pub extent_size: u64, - pub extent_count: u32, - pub encrypted: bool, - - // Run-time parameters - pub port_number: u16, - pub cert_pem: Option, - - // TODO should skip serializing this on list regions response, but this - // causes crucible.json to not have it - // #[serde(skip_serializing)] - pub key_pem: Option, - - pub root_pem: Option, - // If this region was created as part of a clone. - #[serde(default = "source_default")] - pub source: Option, - - // If this region is read only - #[serde(default = "read_only_default")] - pub read_only: bool, -} + if region.key_pem.is_some() { + let mut path = dir.to_path_buf(); + path.push("key.pem"); + let path = path.into_os_string().into_string().unwrap(); -impl Region { - /** - * Given a root directory, return a list of SMF properties to ensure for - * the corresponding running instance. - */ - pub fn get_smf_properties(&self, dir: &Path) -> Vec> { - let mut results = vec![ - SmfProperty { - name: "directory", - typ: SCF_TYPE_ASTRING, - val: dir.to_str().unwrap().to_string(), - }, - SmfProperty { - name: "port", - typ: SCF_TYPE_COUNT, - val: self.port_number.to_string(), - }, - ]; - - if self.cert_pem.is_some() { - let mut path = dir.to_path_buf(); - path.push("cert.pem"); - let path = path.into_os_string().into_string().unwrap(); - - results.push(SmfProperty { - name: "cert_pem_path", - typ: SCF_TYPE_ASTRING, - val: path, - }); - } - - if self.key_pem.is_some() { - let mut path = dir.to_path_buf(); - path.push("key.pem"); - let path = path.into_os_string().into_string().unwrap(); - - results.push(SmfProperty { - name: "key_pem_path", - typ: SCF_TYPE_ASTRING, - val: path, - }); - } - - if self.root_pem.is_some() { - let mut path = dir.to_path_buf(); - path.push("root.pem"); - let path = path.into_os_string().into_string().unwrap(); - - results.push(SmfProperty { - name: "root_pem_path", - typ: SCF_TYPE_ASTRING, - val: path, - }); - } - - results + results.push(SmfProperty { + name: "key_pem_path", + typ: SCF_TYPE_ASTRING, + val: path, + }); } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] -pub struct CreateRegion { - pub id: RegionId, - - pub block_size: u64, - pub extent_size: u64, - pub extent_count: u32, - pub encrypted: bool, - - pub cert_pem: Option, - pub key_pem: Option, - pub root_pem: Option, - // TODO base64 encoded der too? - /// If requested, copy the extent contents from the provided IP:Port - /// - /// Regions created from a source will be started read_only - pub source: Option, -} + if region.root_pem.is_some() { + let mut path = dir.to_path_buf(); + path.push("root.pem"); + let path = path.into_os_string().into_string().unwrap(); -impl CreateRegion { - pub fn mismatch(&self, r: &Region) -> Option { - if self.block_size != r.block_size { - Some(format!( - "block size {} instead of requested {}", - self.block_size, r.block_size - )) - } else if self.extent_size != r.extent_size { - Some(format!( - "extent size {} instead of requested {}", - self.extent_size, r.extent_size - )) - } else if self.extent_count != r.extent_count { - Some(format!( - "extent count {} instead of requested {}", - self.extent_count, r.extent_count - )) - } else if self.encrypted != r.encrypted { - Some(format!( - "encrypted {} instead of requested {}", - self.encrypted, r.encrypted - )) - } else if self.cert_pem != r.cert_pem { - Some(format!( - "cert_pem {:?} instead of requested {:?}", - self.cert_pem, r.cert_pem - )) - } else if self.key_pem != r.key_pem { - Some(format!( - "key_pem {:?} instead of requested {:?}", - self.key_pem, r.key_pem - )) - } else if self.root_pem != r.root_pem { - Some(format!( - "root_pem {:?} instead of requested {:?}", - self.root_pem, r.root_pem - )) - } else if self.source != r.source { - Some(format!( - "source {:?} instead of requested {:?}", - self.source, r.source - )) - } else { - None - } + results.push(SmfProperty { + name: "root_pem_path", + typ: SCF_TYPE_ASTRING, + val: path, + }); } + + results } -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive( - Serialize, - Deserialize, - JsonSchema, - Debug, - PartialEq, - Eq, - Clone, - PartialOrd, - Ord, -)] -pub struct RegionId(pub String); +/// Check if a `CreateRegion` request mismatches an existing `Region`. Returns +/// `Some` with a description of the mismatch, or `None` if they match. +pub fn create_region_mismatch( + create: &CreateRegion, + r: &Region, +) -> Option { + if create.block_size != r.block_size { + Some(format!( + "block size {} instead of requested {}", + create.block_size, r.block_size + )) + } else if create.extent_size != r.extent_size { + Some(format!( + "extent size {} instead of requested {}", + create.extent_size, r.extent_size + )) + } else if create.extent_count != r.extent_count { + Some(format!( + "extent count {} instead of requested {}", + create.extent_count, r.extent_count + )) + } else if create.encrypted != r.encrypted { + Some(format!( + "encrypted {} instead of requested {}", + create.encrypted, r.encrypted + )) + } else if create.cert_pem != r.cert_pem { + Some(format!( + "cert_pem {:?} instead of requested {:?}", + create.cert_pem, r.cert_pem + )) + } else if create.key_pem != r.key_pem { + Some(format!( + "key_pem {:?} instead of requested {:?}", + create.key_pem, r.key_pem + )) + } else if create.root_pem != r.root_pem { + Some(format!( + "root_pem {:?} instead of requested {:?}", + create.root_pem, r.root_pem + )) + } else if create.source != r.source { + Some(format!( + "source {:?} instead of requested {:?}", + create.source, r.source + )) + } else { + None + } +} #[cfg(test)] mod test { @@ -221,10 +138,10 @@ mod test { read_only: false, }; - let s = serde_json::to_string(&r).expect("serialise"); + let s = serde_json::to_string(&r).expect("serialised"); println!("{}", s); - let recons: Region = serde_json::from_str(&s).expect("deserialise"); + let recons: Region = serde_json::from_str(&s).expect("deserialised"); assert_eq!(r, recons); } diff --git a/agent-types/src/smf.rs b/agent-types/src/smf.rs index fe6d2dcf3..130801176 100644 --- a/agent-types/src/smf.rs +++ b/agent-types/src/smf.rs @@ -1,4 +1,4 @@ -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use crucible_smf::scf_type_t; diff --git a/agent-types/src/snapshot.rs b/agent-types/src/snapshot.rs index fa6c24b5a..57ca5e136 100644 --- a/agent-types/src/snapshot.rs +++ b/agent-types/src/snapshot.rs @@ -1,124 +1,106 @@ -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company + +pub use crucible_agent_types_versions::latest::snapshot::*; use std::path::Path; -use chrono::{DateTime, Utc}; use crucible_smf::scf_type_t::*; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{ - region::{RegionId, State}, - smf::SmfProperty, -}; +use crate::region::RegionId; +use crate::smf::SmfProperty; + +pub struct CreateRunningSnapshotRequest { + pub id: RegionId, + pub name: String, + pub cert_pem: Option, + pub key_pem: Option, + pub root_pem: Option, +} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] -pub struct Snapshot { +pub struct DeleteRunningSnapshotRequest { + pub id: RegionId, pub name: String, - pub created: DateTime, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] -pub struct RunningSnapshot { +pub struct DeleteSnapshotRequest { pub id: RegionId, pub name: String, - pub port_number: u16, - pub state: State, } -impl RunningSnapshot { - /** - * Given a root directory, return a list of SMF properties to ensure for - * the corresponding running instance. - */ - pub fn get_smf_properties(&self, dir: &Path) -> Vec> { - let mut results = vec![ - SmfProperty { - name: "directory", - typ: SCF_TYPE_ASTRING, - val: dir.to_str().unwrap().to_string(), - }, - SmfProperty { - name: "port", - typ: SCF_TYPE_COUNT, - val: self.port_number.to_string(), - }, - SmfProperty { - name: "mode", +/// Given a root directory, return a list of SMF properties to ensure for the +/// corresponding running snapshot instance. +pub fn running_snapshot_smf_properties<'a>( + snapshot: &'a RunningSnapshot, + dir: &Path, +) -> Vec> { + let mut results = vec![ + SmfProperty { + name: "directory", + typ: SCF_TYPE_ASTRING, + val: dir.to_str().unwrap().to_string(), + }, + SmfProperty { + name: "port", + typ: SCF_TYPE_COUNT, + val: snapshot.port_number.to_string(), + }, + SmfProperty { + name: "mode", + typ: SCF_TYPE_ASTRING, + val: "ro".to_string(), + }, + ]; + + // Test for X509 files in snapshot - note this means that running + // snapshots will use the X509 information in the snapshot, not a new + // set. + { + let mut path = dir.to_path_buf(); + path.push("cert.pem"); + let path = path.into_os_string().into_string().unwrap(); + + if Path::new(&path).exists() { + results.push(SmfProperty { + name: "cert_pem_path", typ: SCF_TYPE_ASTRING, - val: "ro".to_string(), - }, - ]; - - // Test for X509 files in snapshot - note this means that running - // snapshots will use the X509 information in the snapshot, not a new - // set. - { - let mut path = dir.to_path_buf(); - path.push("cert.pem"); - let path = path.into_os_string().into_string().unwrap(); - - if Path::new(&path).exists() { - results.push(SmfProperty { - name: "cert_pem_path", - typ: SCF_TYPE_ASTRING, - val: path, - }); - } + val: path, + }); } + } - { - let mut path = dir.to_path_buf(); - path.push("key.pem"); - let path = path.into_os_string().into_string().unwrap(); + { + let mut path = dir.to_path_buf(); + path.push("key.pem"); + let path = path.into_os_string().into_string().unwrap(); - if Path::new(&path).exists() { - results.push(SmfProperty { - name: "key_pem_path", - typ: SCF_TYPE_ASTRING, - val: path, - }); - } + if Path::new(&path).exists() { + results.push(SmfProperty { + name: "key_pem_path", + typ: SCF_TYPE_ASTRING, + val: path, + }); } + } - { - let mut path = dir.to_path_buf(); - path.push("root.pem"); - let path = path.into_os_string().into_string().unwrap(); + { + let mut path = dir.to_path_buf(); + path.push("root.pem"); + let path = path.into_os_string().into_string().unwrap(); - if Path::new(&path).exists() { - results.push(SmfProperty { - name: "root_pem_path", - typ: SCF_TYPE_ASTRING, - val: path, - }); - } + if Path::new(&path).exists() { + results.push(SmfProperty { + name: "root_pem_path", + typ: SCF_TYPE_ASTRING, + val: path, + }); } - - results } -} -pub struct CreateRunningSnapshotRequest { - pub id: RegionId, - pub name: String, - pub cert_pem: Option, - pub key_pem: Option, - pub root_pem: Option, -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] -pub struct DeleteRunningSnapshotRequest { - pub id: RegionId, - pub name: String, -} - -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] -pub struct DeleteSnapshotRequest { - pub id: RegionId, - pub name: String, + results } diff --git a/agent-types/versions/Cargo.toml b/agent-types/versions/Cargo.toml new file mode 100644 index 000000000..03b3ef6ab --- /dev/null +++ b/agent-types/versions/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "crucible-agent-types-versions" +version = "0.1.0" +license = "MPL-2.0" +edition = "2024" + +[dependencies] +chrono.workspace = true +crucible-workspace-hack.workspace = true +schemars.workspace = true +serde.workspace = true diff --git a/agent-types/versions/src/impls/mod.rs b/agent-types/versions/src/impls/mod.rs new file mode 100644 index 000000000..70e2f0ff1 --- /dev/null +++ b/agent-types/versions/src/impls/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2026 Oxide Computer Company + +//! Functional code for the latest versions of types. + +// Currently a placeholder. Remove this comment when functional code is added in +// a submodule. diff --git a/agent-types/versions/src/initial/mod.rs b/agent-types/versions/src/initial/mod.rs new file mode 100644 index 000000000..36ff9818e --- /dev/null +++ b/agent-types/versions/src/initial/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2026 Oxide Computer Company + +//! Version `INITIAL` of the Crucible Agent API. + +pub mod region; +pub mod snapshot; diff --git a/agent-types/versions/src/initial/region.rs b/agent-types/versions/src/initial/region.rs new file mode 100644 index 000000000..56f0ca9ed --- /dev/null +++ b/agent-types/versions/src/initial/region.rs @@ -0,0 +1,95 @@ +// Copyright 2026 Oxide Computer Company + +use std::net::SocketAddr; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum State { + Requested, + Created, + Tombstoned, + Destroyed, + Failed, +} + +// If not provided, select None as the default for source. +fn source_default() -> Option { + None +} +// If not provided, select false as the default for read only. +fn read_only_default() -> bool { + false +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] +pub struct Region { + pub id: RegionId, + pub state: State, + + // Creation parameters + pub block_size: u64, + pub extent_size: u64, + pub extent_count: u32, + pub encrypted: bool, + + // Run-time parameters + pub port_number: u16, + pub cert_pem: Option, + + // TODO should skip serializing this on list regions response, but this + // causes crucible.json to not have it + // #[serde(skip_serializing)] + pub key_pem: Option, + + pub root_pem: Option, + // If this region was created as part of a clone. + #[serde(default = "source_default")] + pub source: Option, + + // If this region is read only + #[serde(default = "read_only_default")] + pub read_only: bool, +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] +pub struct CreateRegion { + pub id: RegionId, + + pub block_size: u64, + pub extent_size: u64, + pub extent_count: u32, + pub encrypted: bool, + + pub cert_pem: Option, + pub key_pem: Option, + pub root_pem: Option, + // TODO base64 encoded der too? + /// If requested, copy the extent contents from the provided IP:Port + /// + /// Regions created from a source will be started read_only + pub source: Option, +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive( + Serialize, + Deserialize, + JsonSchema, + Debug, + PartialEq, + Eq, + Clone, + PartialOrd, + Ord, +)] +pub struct RegionId(pub String); + +#[derive(Deserialize, JsonSchema)] +pub struct RegionPath { + pub id: RegionId, +} diff --git a/agent-types/versions/src/initial/snapshot.rs b/agent-types/versions/src/initial/snapshot.rs new file mode 100644 index 000000000..4bc1a65c1 --- /dev/null +++ b/agent-types/versions/src/initial/snapshot.rs @@ -0,0 +1,49 @@ +// Copyright 2026 Oxide Computer Company + +use std::collections::BTreeMap; + +use chrono::{DateTime, Utc}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::region::RegionId; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] +pub struct Snapshot { + pub name: String, + pub created: DateTime, +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] +pub struct RunningSnapshot { + pub id: RegionId, + pub name: String, + pub port_number: u16, + pub state: super::region::State, +} + +#[derive(Serialize, JsonSchema)] +pub struct GetSnapshotResponse { + pub snapshots: Vec, + pub running_snapshots: BTreeMap, +} + +#[derive(Deserialize, JsonSchema)] +pub struct GetSnapshotPath { + pub id: RegionId, + pub name: String, +} + +#[derive(Deserialize, JsonSchema)] +pub struct DeleteSnapshotPath { + pub id: RegionId, + pub name: String, +} + +#[derive(Deserialize, JsonSchema)] +pub struct RunSnapshotPath { + pub id: RegionId, + pub name: String, +} diff --git a/agent-types/versions/src/latest.rs b/agent-types/versions/src/latest.rs new file mode 100644 index 000000000..f5e13cf77 --- /dev/null +++ b/agent-types/versions/src/latest.rs @@ -0,0 +1,18 @@ +// Copyright 2026 Oxide Computer Company + +pub mod region { + pub use crate::v1::region::CreateRegion; + pub use crate::v1::region::Region; + pub use crate::v1::region::RegionId; + pub use crate::v1::region::RegionPath; + pub use crate::v1::region::State; +} + +pub mod snapshot { + pub use crate::v1::snapshot::DeleteSnapshotPath; + pub use crate::v1::snapshot::GetSnapshotPath; + pub use crate::v1::snapshot::GetSnapshotResponse; + pub use crate::v1::snapshot::RunSnapshotPath; + pub use crate::v1::snapshot::RunningSnapshot; + pub use crate::v1::snapshot::Snapshot; +} diff --git a/agent-types/versions/src/lib.rs b/agent-types/versions/src/lib.rs new file mode 100644 index 000000000..9c9c85820 --- /dev/null +++ b/agent-types/versions/src/lib.rs @@ -0,0 +1,33 @@ +// Copyright 2026 Oxide Computer Company + +//! Versioned types for the Crucible Agent API. +//! +//! # Adding a new API version +//! +//! When adding a new API version N with added or changed types: +//! +//! 1. Create /mod.rs, where is the lowercase +//! form of the new version's identifier, as defined in the API trait's +//! `api_versions!` macro. +//! +//! 2. Add to the end of this list: +//! +//! ```rust,ignore +//! #[path = "/mod.rs"] +//! pub mod vN; +//! ``` +//! +//! 3. Add your types to the new module, mirroring the module structure from +//! earlier versions. +//! +//! 4. Update `latest.rs` with new and updated types from the new version. +//! +//! For more information, see the [detailed guide] and [RFD 619]. +//! +//! [detailed guide]: https://github.com/oxidecomputer/dropshot-api-manager/blob/main/guides/new-version.md +//! [RFD 619]: https://rfd.shared.oxide.computer/rfd/619 + +mod impls; +pub mod latest; +#[path = "initial/mod.rs"] +pub mod v1; diff --git a/agent/src/datafile.rs b/agent/src/datafile.rs index d3bbb8d12..c09d88c2b 100644 --- a/agent/src/datafile.rs +++ b/agent/src/datafile.rs @@ -1,4 +1,4 @@ -// Copyright 2021 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use anyhow::{Result, anyhow, bail}; use crucible_agent_types::{region::*, snapshot::*}; @@ -187,7 +187,7 @@ impl DataFile { * Look for a region with this ID. */ if let Some(r) = inner.regions.get(&create.id) { - if let Some(mis) = create.mismatch(r) { + if let Some(mis) = create_region_mismatch(&create, r) { bail!( "requested region {} already exists, with {}", create.id.0, diff --git a/agent/src/main.rs b/agent/src/main.rs index 666a081d2..48bf4e7bf 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -1,8 +1,10 @@ -// Copyright 2021 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use anyhow::{Result, anyhow, bail}; use clap::Parser; +use crucible_agent_types::region::region_smf_properties; use crucible_agent_types::smf::SmfProperty; +use crucible_agent_types::snapshot::running_snapshot_smf_properties; use dropshot::{ConfigLogging, ConfigLoggingIfExists, ConfigLoggingLevel}; use slog::{Logger, debug, error, info, o}; use std::collections::HashSet; @@ -484,7 +486,7 @@ where dir.push(&r.id.0); let properties = { - let mut properties = r.get_smf_properties(&dir); + let mut properties = region_smf_properties(r, &dir); // Instruct downstairs process to listen on the same IP as the // agent, because there is currently only one address in the @@ -658,7 +660,8 @@ where dir.push(snapshot.name.clone()); let properties = { - let mut properties = snapshot.get_smf_properties(&dir); + let mut properties = + running_snapshot_smf_properties(snapshot, &dir); // Instruct downstairs process to listen on the same IP as the // agent, because there is currently only one address in the diff --git a/agent/src/server.rs b/agent/src/server.rs index 9984b03b0..53ef259ad 100644 --- a/agent/src/server.rs +++ b/agent/src/server.rs @@ -1,8 +1,15 @@ -// Copyright 2024 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use super::datafile::DataFile; use anyhow::{Result, anyhow}; use crucible_agent_api::*; -use crucible_agent_types::{region, snapshot}; +use crucible_agent_types::{ + region::{CreateRegion, Region, RegionPath}, + snapshot::{ + CreateRunningSnapshotRequest, DeleteRunningSnapshotRequest, + DeleteSnapshotPath, DeleteSnapshotRequest, GetSnapshotPath, + GetSnapshotResponse, RunSnapshotPath, RunningSnapshot, Snapshot, + }, +}; use dropshot::{ ClientSpecifiesVersionInHeader, HandlerTaskMode, HttpError, HttpResponseDeleted, HttpResponseOk, Path as TypedPath, RequestContext, @@ -21,14 +28,14 @@ impl CrucibleAgentApi for CrucibleAgentImpl { async fn region_list( rqctx: RequestContext, - ) -> SResult>, HttpError> { + ) -> SResult>, HttpError> { Ok(HttpResponseOk(rqctx.context().regions())) } async fn region_create( rqctx: RequestContext, - body: TypedBody, - ) -> SResult, HttpError> { + body: TypedBody, + ) -> SResult, HttpError> { let create = body.into_inner(); match rqctx.context().create_region_request(create) { @@ -43,7 +50,7 @@ impl CrucibleAgentApi for CrucibleAgentImpl { async fn region_get( rc: RequestContext, path: TypedPath, - ) -> SResult, HttpError> { + ) -> SResult, HttpError> { let p = path.into_inner(); match rc.context().get(&p.id) { @@ -123,7 +130,7 @@ impl CrucibleAgentApi for CrucibleAgentImpl { async fn region_get_snapshot( rc: RequestContext, path: TypedPath, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let p = path.into_inner(); match rc.context().get(&p.id) { @@ -172,7 +179,7 @@ impl CrucibleAgentApi for CrucibleAgentImpl { } } - let request = snapshot::DeleteSnapshotRequest { + let request = DeleteSnapshotRequest { id: p.id.clone(), name: p.name, }; @@ -186,7 +193,7 @@ impl CrucibleAgentApi for CrucibleAgentImpl { async fn region_run_snapshot( rc: RequestContext>, path: TypedPath, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let p = path.into_inner(); match rc.context().get(&p.id) { @@ -217,7 +224,7 @@ impl CrucibleAgentApi for CrucibleAgentImpl { } // TODO support running snapshots with their own X509 creds - let create = snapshot::CreateRunningSnapshotRequest { + let create = CreateRunningSnapshotRequest { id: p.id, name: p.name, cert_pem: None, @@ -250,7 +257,7 @@ impl CrucibleAgentApi for CrucibleAgentImpl { } } - let request = snapshot::DeleteRunningSnapshotRequest { + let request = DeleteRunningSnapshotRequest { id: p.id, name: p.name, };