Generate a Client method for Dropshot websocket channels (#183)
Generated methods return `ResponseValue<reqwest::Upgrade`, which may be passed to a websocket protocol implementation such as `tokio_tungstenite::WebSocketStream::from_raw_stream(rv.into_inner(), ...)` for the purpose of implementing against the raw websocket connection, but may later be extended as a generic to allow higher-level channel message definitions. Per the changelog, consumers will need to depend on reqwest 0.11.12 or newer for HTTP Upgrade support, as well as base64 and rand if any endpoints are websocket channels: ``` [dependencies] reqwest = { version = "0.11.12" features = ["json", "stream"] } base64 = "0.13" rand = "0.8" ``` Co-authored-by: lif <>
This commit is contained in:
parent
fd1ae2b80f
commit
4e2dcc5365
|
@ -24,6 +24,7 @@ https://github.com/oxidecomputer/progenitor/compare/v0.1.1\...v0.2.0[Full list o
|
|||
* Derive `Debug` for `Client` and builders for the various operations (#145)
|
||||
* Builders for `struct` types (#171)
|
||||
* Add a prelude that include the `Client` and any extension traits (#176)
|
||||
* Add support for upgrading connections, which requires a version bump to reqwest. (#183)
|
||||
|
||||
== 0.1.1 (released 2022-05-13)
|
||||
|
||||
|
|
|
@ -352,9 +352,11 @@ dependencies = [
|
|||
name = "example-build"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
"progenitor",
|
||||
"progenitor-client",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -986,6 +988,12 @@ version = "0.3.24"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
|
@ -1024,6 +1032,7 @@ name = "progenitor"
|
|||
version = "0.2.1-dev"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"chrono",
|
||||
"clap",
|
||||
"futures",
|
||||
|
@ -1032,6 +1041,7 @@ dependencies = [
|
|||
"progenitor-client",
|
||||
"progenitor-impl",
|
||||
"progenitor-macro",
|
||||
"rand",
|
||||
"regress",
|
||||
"reqwest",
|
||||
"schemars",
|
||||
|
@ -1101,6 +1111,36 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.10"
|
||||
|
|
11
README.md
11
README.md
|
@ -54,6 +54,13 @@ Similarly if there is a `format` field set to `uuid`:
|
|||
+uuid = { version = "1.0.0", features = ["serde", "v4"] }
|
||||
```
|
||||
|
||||
And if there are any websocket channel endpoints:
|
||||
```diff
|
||||
[dependencies]
|
||||
+base64 = "0.13"
|
||||
+rand = "0.8"
|
||||
```
|
||||
|
||||
The macro has some additional fancy options to control the generated code:
|
||||
|
||||
```rust
|
||||
|
@ -116,7 +123,7 @@ You'll need to add add the following to `Cargo.toml`:
|
|||
+serde_json = "1.0"
|
||||
```
|
||||
|
||||
(`chrono` and `uuid` as above)
|
||||
(`chrono`, `uuid`, `base64`, and `rand` as above)
|
||||
|
||||
Note that `progenitor` is used by `build.rs`, but the generated code required
|
||||
`progenitor-client`.
|
||||
|
@ -290,4 +297,4 @@ let result = client
|
|||
```
|
||||
|
||||
Consumers do not need to specify parameters and struct properties that are not
|
||||
required or for which the API specifies defaults. Neat!
|
||||
required or for which the API specifies defaults. Neat!
|
||||
|
|
|
@ -7,7 +7,9 @@ edition = "2021"
|
|||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
progenitor-client = { path = "../progenitor-client" }
|
||||
reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||
reqwest = { version = "0.11.12", features = ["json", "stream"] }
|
||||
base64 = "0.13"
|
||||
rand = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "1.0", features = ["serde", "v4"] }
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ edition = "2021"
|
|||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
progenitor = { path = "../progenitor" }
|
||||
reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||
reqwest = { version = "0.11.12", features = ["json", "stream"] }
|
||||
schemars = { version = "0.8.10", features = ["uuid1"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "1.0", features = ["serde", "v4"] }
|
||||
|
|
|
@ -10,7 +10,7 @@ description = "An OpenAPI client generator - client support"
|
|||
bytes = "1.2.1"
|
||||
futures-core = "0.3.24"
|
||||
percent-encoding = "2.2"
|
||||
reqwest = { version = "0.11", default-features = false, features = ["json", "stream"] }
|
||||
reqwest = { version = "0.11.12", default-features = false, features = ["json", "stream"] }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.7.1"
|
||||
|
|
|
@ -76,6 +76,30 @@ impl<T: DeserializeOwned> ResponseValue<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl ResponseValue<reqwest::Upgraded> {
|
||||
#[doc(hidden)]
|
||||
pub async fn upgrade<E: std::fmt::Debug>(
|
||||
response: reqwest::Response,
|
||||
) -> Result<Self, Error<E>> {
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
if status == reqwest::StatusCode::SWITCHING_PROTOCOLS {
|
||||
let inner = response
|
||||
.upgrade()
|
||||
.await
|
||||
.map_err(Error::InvalidResponsePayload)?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
status,
|
||||
headers,
|
||||
})
|
||||
} else {
|
||||
Err(Error::UnexpectedResponse(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseValue<ByteStream> {
|
||||
#[doc(hidden)]
|
||||
pub fn stream(response: reqwest::Response) -> Self {
|
||||
|
|
|
@ -26,6 +26,8 @@ pub enum Error {
|
|||
UnexpectedFormat(String),
|
||||
#[error("invalid operation path {0}")]
|
||||
InvalidPath(String),
|
||||
#[error("invalid dropshot extension use: {0}")]
|
||||
InvalidExtension(String),
|
||||
#[error("internal error {0}")]
|
||||
InternalError(String),
|
||||
}
|
||||
|
@ -36,6 +38,7 @@ pub struct Generator {
|
|||
type_space: TypeSpace,
|
||||
settings: GenerationSettings,
|
||||
uses_futures: bool,
|
||||
uses_websockets: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
|
@ -116,6 +119,7 @@ impl Default for Generator {
|
|||
),
|
||||
settings: Default::default(),
|
||||
uses_futures: Default::default(),
|
||||
uses_websockets: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +137,7 @@ impl Generator {
|
|||
type_space: TypeSpace::new(&type_settings),
|
||||
settings: settings.clone(),
|
||||
uses_futures: false,
|
||||
uses_websockets: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -426,7 +431,7 @@ impl Generator {
|
|||
"bytes = \"1.1\"",
|
||||
"futures-core = \"0.3\"",
|
||||
"percent-encoding = \"2.1\"",
|
||||
"reqwest = { version = \"0.11\", features = [\"json\", \"stream\"] }",
|
||||
"reqwest = { version = \"0.11.12\", features = [\"json\", \"stream\"] }",
|
||||
"serde = { version = \"1.0\", features = [\"derive\"] }",
|
||||
"serde_urlencoded = \"0.7\"",
|
||||
];
|
||||
|
@ -444,6 +449,10 @@ impl Generator {
|
|||
if self.uses_futures {
|
||||
deps.push("futures = \"0.3\"")
|
||||
}
|
||||
if self.uses_websockets {
|
||||
deps.push("base64 = \"0.13\"");
|
||||
deps.push("rand = \"0.8\"");
|
||||
}
|
||||
if self.type_space.uses_serde_json() {
|
||||
deps.push("serde_json = \"1.0\"")
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ pub(crate) struct OperationMethod {
|
|||
params: Vec<OperationParameter>,
|
||||
responses: Vec<OperationResponse>,
|
||||
dropshot_paginated: Option<DropshotPagination>,
|
||||
dropshot_websocket: bool,
|
||||
}
|
||||
|
||||
enum HttpMethod {
|
||||
|
@ -189,6 +190,7 @@ impl OperationResponseStatus {
|
|||
matches!(
|
||||
self,
|
||||
OperationResponseStatus::Default
|
||||
| OperationResponseStatus::Code(101)
|
||||
| OperationResponseStatus::Code(200..=299)
|
||||
| OperationResponseStatus::Range(2)
|
||||
)
|
||||
|
@ -225,6 +227,7 @@ enum OperationResponseType {
|
|||
Type(TypeId),
|
||||
None,
|
||||
Raw,
|
||||
Upgrade,
|
||||
}
|
||||
|
||||
impl Generator {
|
||||
|
@ -338,6 +341,12 @@ impl Generator {
|
|||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let dropshot_websocket =
|
||||
operation.extensions.get("x-dropshot-websocket").is_some();
|
||||
if dropshot_websocket {
|
||||
self.uses_websockets = true;
|
||||
}
|
||||
|
||||
if let Some(body_param) = self.get_body_param(operation, components)? {
|
||||
params.push(body_param);
|
||||
}
|
||||
|
@ -378,9 +387,10 @@ impl Generator {
|
|||
let (status_code, response) = v?;
|
||||
|
||||
// We categorize responses as "typed" based on the
|
||||
// "application/json" content type, "raw" if there's any other
|
||||
// response content type (we don't investigate further), or
|
||||
// "none" if there is no content.
|
||||
// "application/json" content type, "upgrade" if it's a
|
||||
// websocket channel without a meaningful content-type,
|
||||
// "raw" if there's any other response content type (we don't
|
||||
// investigate further), or "none" if there is no content.
|
||||
// TODO if there are multiple response content types we could
|
||||
// treat those like different response types and create an
|
||||
// enum; the generated client method would check for the
|
||||
|
@ -407,6 +417,8 @@ impl Generator {
|
|||
};
|
||||
|
||||
OperationResponseType::Type(typ)
|
||||
} else if dropshot_websocket {
|
||||
OperationResponseType::Upgrade
|
||||
} else if response.content.first().is_some() {
|
||||
OperationResponseType::Raw
|
||||
} else {
|
||||
|
@ -449,9 +461,25 @@ impl Generator {
|
|||
});
|
||||
}
|
||||
|
||||
// Must accept HTTP 101 Switching Protocols
|
||||
if dropshot_websocket {
|
||||
responses.push(OperationResponse {
|
||||
status_code: OperationResponseStatus::Code(101),
|
||||
typ: OperationResponseType::Upgrade,
|
||||
description: None,
|
||||
})
|
||||
}
|
||||
|
||||
let dropshot_paginated =
|
||||
self.dropshot_pagination_data(operation, ¶ms, &responses);
|
||||
|
||||
if dropshot_websocket && dropshot_paginated.is_some() {
|
||||
return Err(Error::InvalidExtension(format!(
|
||||
"conflicting extensions in {:?}",
|
||||
operation_id
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(OperationMethod {
|
||||
operation_id: sanitize(operation_id, Case::Snake),
|
||||
tags: operation.tags.clone(),
|
||||
|
@ -465,6 +493,7 @@ impl Generator {
|
|||
params,
|
||||
responses,
|
||||
dropshot_paginated,
|
||||
dropshot_websocket,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -705,6 +734,20 @@ impl Generator {
|
|||
(query_build, query_use)
|
||||
};
|
||||
|
||||
let websock_hdrs = if method.dropshot_websocket {
|
||||
quote! {
|
||||
.header(reqwest::header::CONNECTION, "Upgrade")
|
||||
.header(reqwest::header::UPGRADE, "websocket")
|
||||
.header(reqwest::header::SEC_WEBSOCKET_VERSION, "13")
|
||||
.header(
|
||||
reqwest::header::SEC_WEBSOCKET_KEY,
|
||||
base64::encode(rand::random::<[u8; 16]>()),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
// Generate the path rename map; then use it to generate code for
|
||||
// assigning the path parameters to the `url` variable.
|
||||
let url_renames = method
|
||||
|
@ -791,6 +834,11 @@ impl Generator {
|
|||
Ok(ResponseValue::stream(response))
|
||||
}
|
||||
}
|
||||
OperationResponseType::Upgrade => {
|
||||
quote! {
|
||||
ResponseValue::upgrade(response).await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
quote! { #pat => { #decode } }
|
||||
|
@ -842,6 +890,13 @@ impl Generator {
|
|||
))
|
||||
}
|
||||
}
|
||||
OperationResponseType::Upgrade => {
|
||||
if response.status_code == OperationResponseStatus::Default {
|
||||
return quote! { } // catch-all handled below
|
||||
} else {
|
||||
todo!("non-default error response handling for upgrade requests is not yet implemented");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
quote! { #pat => { #decode } }
|
||||
|
@ -879,6 +934,7 @@ impl Generator {
|
|||
. #method_func (url)
|
||||
#(#body_func)*
|
||||
#query_use
|
||||
#websock_hdrs
|
||||
.build()?;
|
||||
#pre_hook
|
||||
let result = #client.client
|
||||
|
@ -988,6 +1044,9 @@ impl Generator {
|
|||
OperationResponseType::Raw => {
|
||||
quote! { ByteStream }
|
||||
}
|
||||
OperationResponseType::Upgrade => {
|
||||
quote! { reqwest::Upgraded }
|
||||
}
|
||||
})
|
||||
// TODO should this be a bytestream?
|
||||
.unwrap_or_else(|| quote! { () });
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,535 @@
|
|||
#[allow(unused_imports)]
|
||||
use progenitor_client::{encode_path, RequestBuilderExt};
|
||||
pub use progenitor_client::{ByteStream, Error, ResponseValue};
|
||||
pub mod types {
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[allow(unused_imports)]
|
||||
use std::convert::TryFrom;
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct CrucibleOpts {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cert_pem: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub control: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub flush_timeout: Option<u32>,
|
||||
pub id: uuid::Uuid,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub key: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub key_pem: Option<String>,
|
||||
pub lossy: bool,
|
||||
pub read_only: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub root_cert_pem: Option<String>,
|
||||
pub target: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct DiskAttachment {
|
||||
pub disk_id: uuid::Uuid,
|
||||
pub generation_id: u64,
|
||||
pub state: DiskAttachmentState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum DiskAttachmentState {
|
||||
Detached,
|
||||
Destroyed,
|
||||
Faulted,
|
||||
Attached(uuid::Uuid),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct DiskRequest {
|
||||
pub device: String,
|
||||
pub gen: u64,
|
||||
pub name: String,
|
||||
pub read_only: bool,
|
||||
pub slot: Slot,
|
||||
pub volume_construction_request: VolumeConstructionRequest,
|
||||
}
|
||||
|
||||
///Error information from a response.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Error {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error_code: Option<String>,
|
||||
pub message: String,
|
||||
pub request_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Instance {
|
||||
pub disks: Vec<DiskAttachment>,
|
||||
pub nics: Vec<NetworkInterface>,
|
||||
pub properties: InstanceProperties,
|
||||
pub state: InstanceState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct InstanceEnsureRequest {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cloud_init_bytes: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub disks: Vec<DiskRequest>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub migrate: Option<InstanceMigrateInitiateRequest>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub nics: Vec<NetworkInterfaceRequest>,
|
||||
pub properties: InstanceProperties,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct InstanceEnsureResponse {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub migrate: Option<InstanceMigrateInitiateResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct InstanceGetResponse {
|
||||
pub instance: Instance,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct InstanceMigrateInitiateRequest {
|
||||
pub migration_id: uuid::Uuid,
|
||||
pub src_addr: String,
|
||||
pub src_uuid: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct InstanceMigrateInitiateResponse {
|
||||
pub migration_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct InstanceMigrateStatusRequest {
|
||||
pub migration_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct InstanceMigrateStatusResponse {
|
||||
pub state: MigrationState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct InstanceProperties {
|
||||
///ID of the bootrom used to initialize this Instance.
|
||||
pub bootrom_id: uuid::Uuid,
|
||||
///Free-form text description of an Instance.
|
||||
pub description: String,
|
||||
///Unique identifier for this Instance.
|
||||
pub id: uuid::Uuid,
|
||||
///ID of the image used to initialize this Instance.
|
||||
pub image_id: uuid::Uuid,
|
||||
///Size of memory allocated to the Instance, in MiB.
|
||||
pub memory: u64,
|
||||
///Human-readable name of the Instance.
|
||||
pub name: String,
|
||||
///Number of vCPUs to be allocated to the Instance.
|
||||
pub vcpus: u8,
|
||||
}
|
||||
|
||||
///Current state of an Instance.
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub enum InstanceState {
|
||||
Creating,
|
||||
Starting,
|
||||
Running,
|
||||
Stopping,
|
||||
Stopped,
|
||||
Rebooting,
|
||||
Migrating,
|
||||
Repairing,
|
||||
Failed,
|
||||
Destroyed,
|
||||
}
|
||||
|
||||
impl ToString for InstanceState {
|
||||
fn to_string(&self) -> String {
|
||||
match *self {
|
||||
Self::Creating => "Creating".to_string(),
|
||||
Self::Starting => "Starting".to_string(),
|
||||
Self::Running => "Running".to_string(),
|
||||
Self::Stopping => "Stopping".to_string(),
|
||||
Self::Stopped => "Stopped".to_string(),
|
||||
Self::Rebooting => "Rebooting".to_string(),
|
||||
Self::Migrating => "Migrating".to_string(),
|
||||
Self::Repairing => "Repairing".to_string(),
|
||||
Self::Failed => "Failed".to_string(),
|
||||
Self::Destroyed => "Destroyed".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for InstanceState {
|
||||
type Err = &'static str;
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"Creating" => Ok(Self::Creating),
|
||||
"Starting" => Ok(Self::Starting),
|
||||
"Running" => Ok(Self::Running),
|
||||
"Stopping" => Ok(Self::Stopping),
|
||||
"Stopped" => Ok(Self::Stopped),
|
||||
"Rebooting" => Ok(Self::Rebooting),
|
||||
"Migrating" => Ok(Self::Migrating),
|
||||
"Repairing" => Ok(Self::Repairing),
|
||||
"Failed" => Ok(Self::Failed),
|
||||
"Destroyed" => Ok(Self::Destroyed),
|
||||
_ => Err("invalid value"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct InstanceStateMonitorRequest {
|
||||
pub gen: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct InstanceStateMonitorResponse {
|
||||
pub gen: u64,
|
||||
pub state: InstanceState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub enum InstanceStateRequested {
|
||||
Run,
|
||||
Stop,
|
||||
Reboot,
|
||||
MigrateStart,
|
||||
}
|
||||
|
||||
impl ToString for InstanceStateRequested {
|
||||
fn to_string(&self) -> String {
|
||||
match *self {
|
||||
Self::Run => "Run".to_string(),
|
||||
Self::Stop => "Stop".to_string(),
|
||||
Self::Reboot => "Reboot".to_string(),
|
||||
Self::MigrateStart => "MigrateStart".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for InstanceStateRequested {
|
||||
type Err = &'static str;
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"Run" => Ok(Self::Run),
|
||||
"Stop" => Ok(Self::Stop),
|
||||
"Reboot" => Ok(Self::Reboot),
|
||||
"MigrateStart" => Ok(Self::MigrateStart),
|
||||
_ => Err("invalid value"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub enum MigrationState {
|
||||
Sync,
|
||||
RamPush,
|
||||
Pause,
|
||||
RamPushDirty,
|
||||
Device,
|
||||
Arch,
|
||||
Resume,
|
||||
RamPull,
|
||||
Finish,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl ToString for MigrationState {
|
||||
fn to_string(&self) -> String {
|
||||
match *self {
|
||||
Self::Sync => "Sync".to_string(),
|
||||
Self::RamPush => "RamPush".to_string(),
|
||||
Self::Pause => "Pause".to_string(),
|
||||
Self::RamPushDirty => "RamPushDirty".to_string(),
|
||||
Self::Device => "Device".to_string(),
|
||||
Self::Arch => "Arch".to_string(),
|
||||
Self::Resume => "Resume".to_string(),
|
||||
Self::RamPull => "RamPull".to_string(),
|
||||
Self::Finish => "Finish".to_string(),
|
||||
Self::Error => "Error".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for MigrationState {
|
||||
type Err = &'static str;
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"Sync" => Ok(Self::Sync),
|
||||
"RamPush" => Ok(Self::RamPush),
|
||||
"Pause" => Ok(Self::Pause),
|
||||
"RamPushDirty" => Ok(Self::RamPushDirty),
|
||||
"Device" => Ok(Self::Device),
|
||||
"Arch" => Ok(Self::Arch),
|
||||
"Resume" => Ok(Self::Resume),
|
||||
"RamPull" => Ok(Self::RamPull),
|
||||
"Finish" => Ok(Self::Finish),
|
||||
"Error" => Ok(Self::Error),
|
||||
_ => Err("invalid value"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct NetworkInterface {
|
||||
pub attachment: NetworkInterfaceAttachmentState,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum NetworkInterfaceAttachmentState {
|
||||
Detached,
|
||||
Faulted,
|
||||
Attached(Slot),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct NetworkInterfaceRequest {
|
||||
pub name: String,
|
||||
pub slot: Slot,
|
||||
}
|
||||
|
||||
///A stable index which is translated by Propolis into a PCI BDF, visible
|
||||
/// to the guest.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Slot(pub u8);
|
||||
impl std::ops::Deref for Slot {
|
||||
type Target = u8;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum VolumeConstructionRequest {
|
||||
#[serde(rename = "volume")]
|
||||
Volume {
|
||||
block_size: u64,
|
||||
id: uuid::Uuid,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
read_only_parent: Option<Box<VolumeConstructionRequest>>,
|
||||
sub_volumes: Vec<VolumeConstructionRequest>,
|
||||
},
|
||||
#[serde(rename = "url")]
|
||||
Url {
|
||||
block_size: u64,
|
||||
id: uuid::Uuid,
|
||||
url: String,
|
||||
},
|
||||
#[serde(rename = "region")]
|
||||
Region {
|
||||
block_size: u64,
|
||||
gen: u64,
|
||||
opts: CrucibleOpts,
|
||||
},
|
||||
#[serde(rename = "file")]
|
||||
File {
|
||||
block_size: u64,
|
||||
id: uuid::Uuid,
|
||||
path: String,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Client {
|
||||
pub(crate) baseurl: String,
|
||||
pub(crate) client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(baseurl: &str) -> Self {
|
||||
let dur = std::time::Duration::from_secs(15);
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.connect_timeout(dur)
|
||||
.timeout(dur)
|
||||
.build()
|
||||
.unwrap();
|
||||
Self::new_with_client(baseurl, client)
|
||||
}
|
||||
|
||||
pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self {
|
||||
Self {
|
||||
baseurl: baseurl.to_string(),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn baseurl(&self) -> &String {
|
||||
&self.baseurl
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &reqwest::Client {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
///Sends a `GET` request to `/instance`
|
||||
pub async fn instance_get<'a>(
|
||||
&'a self,
|
||||
) -> Result<ResponseValue<types::InstanceGetResponse>, Error<types::Error>> {
|
||||
let url = format!("{}/instance", self.baseurl,);
|
||||
let request = self.client.get(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
400u16..=499u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
500u16..=599u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
///Sends a `PUT` request to `/instance`
|
||||
pub async fn instance_ensure<'a>(
|
||||
&'a self,
|
||||
body: &'a types::InstanceEnsureRequest,
|
||||
) -> Result<ResponseValue<types::InstanceEnsureResponse>, Error<types::Error>> {
|
||||
let url = format!("{}/instance", self.baseurl,);
|
||||
let request = self.client.put(url).json(&body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => ResponseValue::from_response(response).await,
|
||||
400u16..=499u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
500u16..=599u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
///Issue a snapshot request to a crucible backend
|
||||
///
|
||||
///Sends a `POST` request to `/instance/disk/{id}/snapshot/{snapshot_id}`
|
||||
pub async fn instance_issue_crucible_snapshot_request<'a>(
|
||||
&'a self,
|
||||
id: &'a uuid::Uuid,
|
||||
snapshot_id: &'a uuid::Uuid,
|
||||
) -> Result<ResponseValue<()>, Error<types::Error>> {
|
||||
let url = format!(
|
||||
"{}/instance/disk/{}/snapshot/{}",
|
||||
self.baseurl,
|
||||
encode_path(&id.to_string()),
|
||||
encode_path(&snapshot_id.to_string()),
|
||||
);
|
||||
let request = self.client.post(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
400u16..=499u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
500u16..=599u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
///Sends a `GET` request to `/instance/migrate/status`
|
||||
pub async fn instance_migrate_status<'a>(
|
||||
&'a self,
|
||||
body: &'a types::InstanceMigrateStatusRequest,
|
||||
) -> Result<ResponseValue<types::InstanceMigrateStatusResponse>, Error<types::Error>> {
|
||||
let url = format!("{}/instance/migrate/status", self.baseurl,);
|
||||
let request = self.client.get(url).json(&body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
400u16..=499u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
500u16..=599u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
///Sends a `GET` request to `/instance/serial`
|
||||
pub async fn instance_serial<'a>(
|
||||
&'a self,
|
||||
) -> Result<ResponseValue<reqwest::Upgraded>, Error<reqwest::Upgraded>> {
|
||||
let url = format!("{}/instance/serial", self.baseurl,);
|
||||
let request = self
|
||||
.client
|
||||
.get(url)
|
||||
.header(reqwest::header::CONNECTION, "Upgrade")
|
||||
.header(reqwest::header::UPGRADE, "websocket")
|
||||
.header(reqwest::header::SEC_WEBSOCKET_VERSION, "13")
|
||||
.header(
|
||||
reqwest::header::SEC_WEBSOCKET_KEY,
|
||||
base64::encode(rand::random::<[u8; 16]>()),
|
||||
)
|
||||
.build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
101u16 => ResponseValue::upgrade(response).await,
|
||||
200..=299 => ResponseValue::upgrade(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
///Sends a `PUT` request to `/instance/state`
|
||||
pub async fn instance_state_put<'a>(
|
||||
&'a self,
|
||||
body: types::InstanceStateRequested,
|
||||
) -> Result<ResponseValue<()>, Error<types::Error>> {
|
||||
let url = format!("{}/instance/state", self.baseurl,);
|
||||
let request = self.client.put(url).json(&body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
204u16 => Ok(ResponseValue::empty(response)),
|
||||
400u16..=499u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
500u16..=599u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
///Sends a `GET` request to `/instance/state-monitor`
|
||||
pub async fn instance_state_monitor<'a>(
|
||||
&'a self,
|
||||
body: &'a types::InstanceStateMonitorRequest,
|
||||
) -> Result<ResponseValue<types::InstanceStateMonitorResponse>, Error<types::Error>> {
|
||||
let url = format!("{}/instance/state-monitor", self.baseurl,);
|
||||
let request = self.client.get(url).json(&body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
400u16..=499u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
500u16..=599u16 => Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response).await?,
|
||||
)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::Client;
|
||||
}
|
|
@ -60,6 +60,11 @@ fn test_nexus() {
|
|||
verify_apis("nexus");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_propolis_server() {
|
||||
verify_apis("propolis-server");
|
||||
}
|
||||
|
||||
// TODO this file is full of inconsistencies and incorrectly specified types.
|
||||
// It's an interesting test to consider whether we try to do our best to
|
||||
// interpret the intent or just fail.
|
||||
|
|
|
@ -17,10 +17,12 @@ serde_json = "1.0"
|
|||
clap = { version = "3.2.22", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
base64 = "0.13"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
futures = "0.3.21"
|
||||
futures = "0.3.24"
|
||||
percent-encoding = "2.2"
|
||||
rand = "0.8"
|
||||
regress = "0.4.1"
|
||||
reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||
reqwest = { version = "0.11.12", features = ["json", "stream"] }
|
||||
schemars = { version = "0.8.10", features = ["uuid1"] }
|
||||
uuid = { version = "1.0", features = ["serde", "v4"] }
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2022 Oxide Computer Company
|
||||
|
||||
// ensure that the websocket channel used for serial console compiles.
|
||||
mod propolis_client {
|
||||
progenitor::generate_api!(
|
||||
spec = "../sample_openapi/propolis-server.json",
|
||||
interface = Builder,
|
||||
tags = Merged,
|
||||
);
|
||||
}
|
||||
|
||||
use propolis_client::Client;
|
||||
|
||||
pub fn _ignore() {
|
||||
let _ = async {
|
||||
let _upgraded: reqwest::Upgraded = Client::new("")
|
||||
.instance_serial()
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
};
|
||||
}
|
|
@ -0,0 +1,850 @@
|
|||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "Oxide Propolis Server API",
|
||||
"description": "API for interacting with the Propolis hypervisor frontend.",
|
||||
"contact": {
|
||||
"url": "https://oxide.computer",
|
||||
"email": "api@oxide.computer"
|
||||
},
|
||||
"version": "0.0.1"
|
||||
},
|
||||
"paths": {
|
||||
"/instance": {
|
||||
"get": {
|
||||
"operationId": "instance_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InstanceGetResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"4XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"5XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"operationId": "instance_ensure",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InstanceEnsureRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "successful creation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InstanceEnsureResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"4XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"5XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/instance/disk/{id}/snapshot/{snapshot_id}": {
|
||||
"post": {
|
||||
"summary": "Issue a snapshot request to a crucible backend",
|
||||
"operationId": "instance_issue_crucible_snapshot_request",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"style": "simple"
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "snapshot_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"style": "simple"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Null",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
null
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"4XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"5XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/instance/migrate/status": {
|
||||
"get": {
|
||||
"operationId": "instance_migrate_status",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InstanceMigrateStatusRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InstanceMigrateStatusResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"4XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"5XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/instance/serial": {
|
||||
"get": {
|
||||
"operationId": "instance_serial",
|
||||
"responses": {
|
||||
"default": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-dropshot-websocket": {}
|
||||
}
|
||||
},
|
||||
"/instance/state": {
|
||||
"put": {
|
||||
"operationId": "instance_state_put",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InstanceStateRequested"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "resource updated"
|
||||
},
|
||||
"4XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"5XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/instance/state-monitor": {
|
||||
"get": {
|
||||
"operationId": "instance_state_monitor",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InstanceStateMonitorRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InstanceStateMonitorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"4XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
},
|
||||
"5XX": {
|
||||
"$ref": "#/components/responses/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"responses": {
|
||||
"Error": {
|
||||
"description": "Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"CrucibleOpts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cert_pem": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"control": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"flush_timeout": {
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"key": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"key_pem": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"lossy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"read_only": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"root_cert_pem": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"lossy",
|
||||
"read_only",
|
||||
"target"
|
||||
]
|
||||
},
|
||||
"DiskAttachment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"disk_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"generation_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/DiskAttachmentState"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"disk_id",
|
||||
"generation_id",
|
||||
"state"
|
||||
]
|
||||
},
|
||||
"DiskAttachmentState": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Detached",
|
||||
"Destroyed",
|
||||
"Faulted"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Attached": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Attached"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"DiskRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"device": {
|
||||
"type": "string"
|
||||
},
|
||||
"gen": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"read_only": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"slot": {
|
||||
"$ref": "#/components/schemas/Slot"
|
||||
},
|
||||
"volume_construction_request": {
|
||||
"$ref": "#/components/schemas/VolumeConstructionRequest"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"device",
|
||||
"gen",
|
||||
"name",
|
||||
"read_only",
|
||||
"slot",
|
||||
"volume_construction_request"
|
||||
]
|
||||
},
|
||||
"Error": {
|
||||
"description": "Error information from a response.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error_code": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"request_id"
|
||||
]
|
||||
},
|
||||
"Instance": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"disks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DiskAttachment"
|
||||
}
|
||||
},
|
||||
"nics": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NetworkInterface"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/components/schemas/InstanceProperties"
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/InstanceState"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"disks",
|
||||
"nics",
|
||||
"properties",
|
||||
"state"
|
||||
]
|
||||
},
|
||||
"InstanceEnsureRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cloud_init_bytes": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"disks": {
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DiskRequest"
|
||||
}
|
||||
},
|
||||
"migrate": {
|
||||
"nullable": true,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/InstanceMigrateInitiateRequest"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nics": {
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NetworkInterfaceRequest"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/components/schemas/InstanceProperties"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"properties"
|
||||
]
|
||||
},
|
||||
"InstanceEnsureResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"migrate": {
|
||||
"nullable": true,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/InstanceMigrateInitiateResponse"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"InstanceGetResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"instance": {
|
||||
"$ref": "#/components/schemas/Instance"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"instance"
|
||||
]
|
||||
},
|
||||
"InstanceMigrateInitiateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"migration_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"src_addr": {
|
||||
"type": "string"
|
||||
},
|
||||
"src_uuid": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"migration_id",
|
||||
"src_addr",
|
||||
"src_uuid"
|
||||
]
|
||||
},
|
||||
"InstanceMigrateInitiateResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"migration_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"migration_id"
|
||||
]
|
||||
},
|
||||
"InstanceMigrateStatusRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"migration_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"migration_id"
|
||||
]
|
||||
},
|
||||
"InstanceMigrateStatusResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/MigrationState"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"state"
|
||||
]
|
||||
},
|
||||
"InstanceProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bootrom_id": {
|
||||
"description": "ID of the bootrom used to initialize this Instance.",
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"description": {
|
||||
"description": "Free-form text description of an Instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique identifier for this Instance.",
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"image_id": {
|
||||
"description": "ID of the image used to initialize this Instance.",
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"memory": {
|
||||
"description": "Size of memory allocated to the Instance, in MiB.",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"name": {
|
||||
"description": "Human-readable name of the Instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"vcpus": {
|
||||
"description": "Number of vCPUs to be allocated to the Instance.",
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bootrom_id",
|
||||
"description",
|
||||
"id",
|
||||
"image_id",
|
||||
"memory",
|
||||
"name",
|
||||
"vcpus"
|
||||
]
|
||||
},
|
||||
"InstanceState": {
|
||||
"description": "Current state of an Instance.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Creating",
|
||||
"Starting",
|
||||
"Running",
|
||||
"Stopping",
|
||||
"Stopped",
|
||||
"Rebooting",
|
||||
"Migrating",
|
||||
"Repairing",
|
||||
"Failed",
|
||||
"Destroyed"
|
||||
]
|
||||
},
|
||||
"InstanceStateMonitorRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"gen": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"gen"
|
||||
]
|
||||
},
|
||||
"InstanceStateMonitorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"gen": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/InstanceState"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"gen",
|
||||
"state"
|
||||
]
|
||||
},
|
||||
"InstanceStateRequested": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Run",
|
||||
"Stop",
|
||||
"Reboot",
|
||||
"MigrateStart"
|
||||
]
|
||||
},
|
||||
"MigrationState": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Sync",
|
||||
"RamPush",
|
||||
"Pause",
|
||||
"RamPushDirty",
|
||||
"Device",
|
||||
"Arch",
|
||||
"Resume",
|
||||
"RamPull",
|
||||
"Finish",
|
||||
"Error"
|
||||
]
|
||||
},
|
||||
"NetworkInterface": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attachment": {
|
||||
"$ref": "#/components/schemas/NetworkInterfaceAttachmentState"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"attachment",
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"NetworkInterfaceAttachmentState": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Detached",
|
||||
"Faulted"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Attached": {
|
||||
"$ref": "#/components/schemas/Slot"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Attached"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkInterfaceRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"slot": {
|
||||
"$ref": "#/components/schemas/Slot"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"slot"
|
||||
]
|
||||
},
|
||||
"Slot": {
|
||||
"description": "A stable index which is translated by Propolis into a PCI BDF, visible to the guest.",
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0
|
||||
},
|
||||
"VolumeConstructionRequest": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"block_size": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"read_only_parent": {
|
||||
"nullable": true,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/VolumeConstructionRequest"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sub_volumes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/VolumeConstructionRequest"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"volume"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"block_size",
|
||||
"id",
|
||||
"sub_volumes",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"block_size": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"block_size",
|
||||
"id",
|
||||
"type",
|
||||
"url"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"block_size": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"gen": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"opts": {
|
||||
"$ref": "#/components/schemas/CrucibleOpts"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"region"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"block_size",
|
||||
"gen",
|
||||
"opts",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"block_size": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"file"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"block_size",
|
||||
"id",
|
||||
"path",
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue