Skip to content

Commit 8a414e9

Browse files
authored
client: re-export instance spec types for use in sled-agent (#899)
Using generated instance spec types in sled-agent's API has some substantial downsides: 1. Some component types (e.g. storage backends) have fields that we'd like to redact when logging their contents using their `Debug` impls. The native versions of these types manually impl `Debug` for these types, but their generated counterparts always derive Debug (and AFAIK there's no way to ask Progenitor not to add this derive). 2. The Dropshot-generated OpenAPI doc for sled-agent's API winds up having very gnarly type descriptions that include fully-escaped versions of the Progenitor-generated doc comments from propolis-client. To get around these problems: - Add a propolis-client submodule that re-exports all instance spec-related types from `propolis_api_types`. - Add `replace` directives to the generated client that replace the generated versions of `InstanceSpecV0`, `VersionedInstanceSpec`, and `ReplacementComponent` with their re-exported equivalents. This way, sled-agent can use the re-exported spec types internally and in its API. This reuses their manual `impl`s when they exist (solving problem 1) and avoids presenting generated JSON schema documentation when generating the sled-agent API document (solving problem 2). Remove the `JsonSchema` implementations for generated spec types, since they're no longer needed.
1 parent 42ab364 commit 8a414e9

File tree

16 files changed

+130
-165
lines changed

16 files changed

+130
-165
lines changed

bin/propolis-cli/src/main.rs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@ use anyhow::{anyhow, Context};
1616
use clap::{Args, Parser, Subcommand};
1717
use futures::{future, SinkExt};
1818
use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag};
19-
use propolis_client::support::nvme_serial_from_str;
20-
use propolis_client::types::{
19+
use propolis_client::instance_spec::{
2120
BlobStorageBackend, Board, Chipset, ComponentV0, CrucibleStorageBackend,
22-
GuestHypervisorInterface, HyperVFeatureFlag, I440Fx, InstanceEnsureRequest,
23-
InstanceInitializationMethod, InstanceMetadata, InstanceSpecGetResponse,
24-
InstanceSpecV0, NvmeDisk, QemuPvpanic, ReplacementComponent, SerialPort,
21+
GuestHypervisorInterface, HyperVFeatureFlag, I440Fx, InstanceSpecV0,
22+
NvmeDisk, PciPath, QemuPvpanic, ReplacementComponent, SerialPort,
2523
SerialPortNumber, SpecKey, VirtioDisk,
2624
};
27-
use propolis_client::PciPath;
25+
use propolis_client::support::nvme_serial_from_str;
26+
use propolis_client::types::{
27+
InstanceEnsureRequest, InstanceInitializationMethod, InstanceMetadata,
28+
InstanceSpecGetResponse,
29+
};
2830
use propolis_config_toml::spec::SpecConfig;
2931
use serde::{Deserialize, Serialize};
3032
use slog::{o, Drain, Level, Logger};
@@ -200,10 +202,17 @@ fn add_component_to_spec(
200202
id: SpecKey,
201203
component: ComponentV0,
202204
) -> anyhow::Result<()> {
203-
if spec.components.insert(id.to_string(), component).is_some() {
204-
anyhow::bail!("duplicate component ID {id:?}");
205+
use std::collections::btree_map::Entry;
206+
match spec.components.entry(id) {
207+
Entry::Vacant(vacant_entry) => {
208+
vacant_entry.insert(component);
209+
Ok(())
210+
}
211+
Entry::Occupied(occupied_entry) => Err(anyhow::anyhow!(
212+
"duplicate component ID {:?}",
213+
occupied_entry.key()
214+
)),
205215
}
206-
Ok(())
207216
}
208217

209218
/// A legacy Propolis API disk request, preserved here for compatibility with
@@ -298,11 +307,13 @@ impl VmConfig {
298307
cpus: self.vcpus,
299308
memory_mb: self.memory,
300309
guest_hv_interface: if self.hyperv {
301-
Some(GuestHypervisorInterface::HyperV {
302-
features: vec![HyperVFeatureFlag::ReferenceTsc],
303-
})
310+
GuestHypervisorInterface::HyperV {
311+
features: [HyperVFeatureFlag::ReferenceTsc]
312+
.into_iter()
313+
.collect(),
314+
}
304315
} else {
305-
None
316+
Default::default()
306317
},
307318
},
308319
components: Default::default(),

crates/propolis-api-types/src/instance_spec/components/board.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ pub enum Chipset {
3838
I440Fx(I440Fx),
3939
}
4040

41+
impl Default for Chipset {
42+
fn default() -> Self {
43+
Self::I440Fx(I440Fx { enable_pcie: false })
44+
}
45+
}
46+
4147
/// A set of CPUID values to expose to a guest.
4248
#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)]
4349
#[serde(deny_unknown_fields)]

crates/propolis-api-types/src/instance_spec/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,16 @@ impl std::fmt::Display for SpecKey {
217217
impl std::str::FromStr for SpecKey {
218218
type Err = core::convert::Infallible;
219219
fn from_str(s: &str) -> Result<Self, Self::Err> {
220-
Ok(match Uuid::parse_str(s) {
220+
Ok(s.into())
221+
}
222+
}
223+
224+
impl From<&str> for SpecKey {
225+
fn from(s: &str) -> Self {
226+
match Uuid::parse_str(s) {
221227
Ok(uuid) => Self::Uuid(uuid),
222228
Err(_) => Self::Name(s.to_owned()),
223-
})
229+
}
224230
}
225231
}
226232

crates/propolis-config-toml/src/spec.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ use std::{
1010
};
1111

1212
use propolis_client::{
13-
support::nvme_serial_from_str,
14-
types::{
13+
instance_spec::{
1514
ComponentV0, DlpiNetworkBackend, FileStorageBackend,
16-
MigrationFailureInjector, NvmeDisk, P9fs, PciPciBridge, SoftNpuP9,
17-
SoftNpuPciPort, SoftNpuPort, SpecKey, VirtioDisk, VirtioNetworkBackend,
18-
VirtioNic,
15+
MigrationFailureInjector, NvmeDisk, P9fs, PciPath, PciPciBridge,
16+
SoftNpuP9, SoftNpuPciPort, SoftNpuPort, SpecKey, VirtioDisk,
17+
VirtioNetworkBackend, VirtioNic,
1918
},
20-
PciPath,
19+
support::nvme_serial_from_str,
2120
};
2221
use thiserror::Error;
2322

lib/propolis-client/src/lib.rs

Lines changed: 28 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,28 @@
44

55
//! A client for the Propolis hypervisor frontend's server API.
66
7-
// Re-export types from propolis_api_types where callers may want to use
8-
// constructors or From impls.
9-
pub use propolis_api_types::instance_spec::{PciPath, SpecKey};
7+
/// Re-exports of types related to instance specs.
8+
///
9+
/// These types are re-exported for the convenience of components like
10+
/// sled-agent that may wish to expose instance specs in their own APIs.
11+
/// Defining the sled-agent API in terms of these "native" types allows
12+
/// sled-agent to reuse their trait implementations (and in particular use
13+
/// "manual" impls of things that Progenitor would otherwise derive).
14+
///
15+
/// In the generated client, the native "top-level" instance spec and component
16+
/// types ([`VersionedInstanceSpec`], [`InstanceSpecV0`], and
17+
/// [`ReplacementComponent`]) replace their generated counterparts. This
18+
/// obviates the need to maintain `From` impls to convert between native and
19+
/// generated types.
20+
pub mod instance_spec {
21+
pub use propolis_api_types::instance_spec::{
22+
components::{backends::*, board::*, devices::*},
23+
v0::*,
24+
*,
25+
};
26+
27+
pub use propolis_api_types::ReplacementComponent;
28+
}
1029

1130
// Re-export Crucible client types that appear in their serialized forms in
1231
// instance specs. This allows clients to ensure they serialize/deserialize
@@ -19,106 +38,21 @@ progenitor::generate_api!(
1938
interface = Builder,
2039
tags = Separate,
2140
replace = {
22-
PciPath = crate::PciPath,
41+
PciPath = crate::instance_spec::PciPath,
42+
ReplacementComponent = crate::instance_spec::ReplacementComponent,
43+
InstanceSpecV0 = crate::instance_spec::InstanceSpecV0,
44+
VersionedInstanceSpec = crate::instance_spec::VersionedInstanceSpec,
2345
},
2446
// Automatically derive JsonSchema for instance spec-related types so that
2547
// they can be reused in sled-agent's API. This can't be done with a
2648
// `derives = [schemars::JsonSchema]` directive because the `SpecKey` type
2749
// needs to implement that trait manually (see below).
2850
patch = {
29-
BlobStorageBackend = { derives = [ schemars::JsonSchema ]},
30-
Board = { derives = [ schemars::JsonSchema ]},
31-
BootOrderEntry = { derives = [ schemars::JsonSchema ]},
32-
BootSettings = { derives = [ schemars::JsonSchema, Default] },
33-
ComponentV0 = { derives = [ schemars::JsonSchema ]},
34-
Chipset = { derives = [ schemars::JsonSchema ]},
35-
CrucibleStorageBackend = { derives = [ schemars::JsonSchema ]},
36-
Cpuid = { derives = [ schemars::JsonSchema ]},
37-
CpuidEntry = { derives = [ schemars::JsonSchema, PartialEq, Eq, Copy ]},
38-
CpuidVendor = { derives = [ schemars::JsonSchema ]},
39-
DlpiNetworkBackend = { derives = [ schemars::JsonSchema ]},
40-
FileStorageBackend = { derives = [ schemars::JsonSchema ]},
41-
GuestHypervisorInterface = { derives = [ schemars::JsonSchema ]},
42-
I440Fx = { derives = [ schemars::JsonSchema ]},
43-
HyperVFeatureFlag = { derives = [ schemars::JsonSchema ]},
44-
MigrationFailureInjector = { derives = [ schemars::JsonSchema ]},
45-
NvmeDisk = { derives = [ schemars::JsonSchema ]},
51+
BootSettings = { derives = [ Default ] },
52+
CpuidEntry = { derives = [ PartialEq, Eq, Copy ] },
4653
InstanceMetadata = { derives = [ PartialEq ] },
47-
InstanceSpecV0 = { derives = [ schemars::JsonSchema ]},
48-
PciPciBridge = { derives = [ schemars::JsonSchema ]},
49-
P9fs = { derives = [ schemars::JsonSchema ]},
50-
QemuPvpanic = { derives = [ schemars::JsonSchema ]},
51-
SerialPort = { derives = [ schemars::JsonSchema ]},
52-
SerialPortNumber = { derives = [ schemars::JsonSchema ]},
53-
SoftNpuP9 = { derives = [ schemars::JsonSchema ]},
54-
SoftNpuPort = { derives = [ schemars::JsonSchema ]},
55-
SoftNpuPciPort = { derives = [ schemars::JsonSchema ]},
5654
SpecKey = { derives = [ PartialEq, Eq, Ord, PartialOrd, Hash ] },
57-
VirtioDisk = { derives = [ schemars::JsonSchema ]},
58-
VirtioNic = { derives = [ schemars::JsonSchema ]},
59-
VirtioNetworkBackend = { derives = [ schemars::JsonSchema ]},
6055
}
6156
);
6257

63-
// Supply the same JsonSchema implementation for the generated SpecKey type that
64-
// the native type has. This allows sled-agent (or another consumer) to reuse
65-
// the generated type in its own API to produce an API document that generates
66-
// the correct type for sled-agent's (or the other consumer's) clients.
67-
impl schemars::JsonSchema for types::SpecKey {
68-
fn schema_name() -> String {
69-
"SpecKey".to_owned()
70-
}
71-
72-
fn json_schema(
73-
generator: &mut schemars::gen::SchemaGenerator,
74-
) -> schemars::schema::Schema {
75-
use schemars::schema::*;
76-
fn label_schema(label: &str, schema: Schema) -> Schema {
77-
SchemaObject {
78-
metadata: Some(
79-
Metadata {
80-
title: Some(label.to_string()),
81-
..Default::default()
82-
}
83-
.into(),
84-
),
85-
subschemas: Some(
86-
SubschemaValidation {
87-
all_of: Some(vec![schema]),
88-
..Default::default()
89-
}
90-
.into(),
91-
),
92-
..Default::default()
93-
}
94-
.into()
95-
}
96-
97-
SchemaObject {
98-
metadata: Some(
99-
Metadata {
100-
description: Some(
101-
"A key identifying a component in an instance spec."
102-
.to_string(),
103-
),
104-
..Default::default()
105-
}
106-
.into(),
107-
),
108-
subschemas: Some(Box::new(SubschemaValidation {
109-
one_of: Some(vec![
110-
label_schema(
111-
"uuid",
112-
generator.subschema_for::<uuid::Uuid>(),
113-
),
114-
label_schema("name", generator.subschema_for::<String>()),
115-
]),
116-
..Default::default()
117-
})),
118-
..Default::default()
119-
}
120-
.into()
121-
}
122-
}
123-
12458
pub mod support;

phd-tests/framework/src/disk/crucible.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::{
1313

1414
use anyhow::Context;
1515
use propolis_client::{
16-
types::{ComponentV0, CrucibleStorageBackend},
16+
instance_spec::{ComponentV0, CrucibleStorageBackend},
1717
CrucibleOpts, VolumeConstructionRequest,
1818
};
1919
use rand::{rngs::StdRng, RngCore, SeedableRng};

phd-tests/framework/src/disk/file.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! Abstractions for disks with a raw file backend.
66
77
use camino::{Utf8Path, Utf8PathBuf};
8-
use propolis_client::types::{ComponentV0, FileStorageBackend};
8+
use propolis_client::instance_spec::{ComponentV0, FileStorageBackend};
99
use tracing::{debug, error, warn};
1010
use uuid::Uuid;
1111

phd-tests/framework/src/disk/in_memory.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
//! Abstractions for disks with an in-memory backend.
66
7-
use propolis_client::types::{BlobStorageBackend, ComponentV0};
7+
use propolis_client::instance_spec::{BlobStorageBackend, ComponentV0};
88

99
use super::DiskConfig;
1010
use crate::disk::DeviceName;

phd-tests/framework/src/disk/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::sync::Arc;
1313
use anyhow::Context;
1414
use camino::{Utf8Path, Utf8PathBuf};
1515
use in_memory::InMemoryDisk;
16-
use propolis_client::types::ComponentV0;
16+
use propolis_client::instance_spec::ComponentV0;
1717
use thiserror::Error;
1818

1919
use crate::{

phd-tests/framework/src/test_vm/config.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ use std::sync::Arc;
77
use anyhow::Context;
88
use cpuid_utils::CpuidIdent;
99
use propolis_client::{
10-
support::nvme_serial_from_str,
11-
types::{
10+
instance_spec::{
1211
Board, BootOrderEntry, BootSettings, Chipset, ComponentV0, Cpuid,
13-
CpuidEntry, CpuidVendor, GuestHypervisorInterface, InstanceMetadata,
14-
InstanceSpecV0, MigrationFailureInjector, NvmeDisk, SerialPort,
12+
CpuidEntry, CpuidVendor, GuestHypervisorInterface, InstanceSpecV0,
13+
MigrationFailureInjector, NvmeDisk, PciPath, SerialPort,
1514
SerialPortNumber, SpecKey, VirtioDisk,
1615
},
17-
PciPath,
16+
support::nvme_serial_from_str,
17+
types::InstanceMetadata,
1818
};
1919
use uuid::Uuid;
2020

@@ -299,7 +299,10 @@ impl<'dr> VmConfig<'dr> {
299299
cpuid_utils::CpuidVendor::Intel => CpuidVendor::Intel,
300300
},
301301
}),
302-
guest_hv_interface: guest_hv_interface.clone(),
302+
guest_hv_interface: guest_hv_interface
303+
.as_ref()
304+
.cloned()
305+
.unwrap_or_default(),
303306
},
304307
components: Default::default(),
305308
};
@@ -338,24 +341,25 @@ impl<'dr> VmConfig<'dr> {
338341
}),
339342
};
340343

341-
let _old =
342-
spec.components.insert(device_name.into_string(), device_spec);
344+
let _old = spec
345+
.components
346+
.insert(device_name.into_string().into(), device_spec);
343347
assert!(_old.is_none());
344348
let _old = spec
345349
.components
346-
.insert(backend_name.into_string(), backend_spec);
350+
.insert(backend_name.into_string().into(), backend_spec);
347351
assert!(_old.is_none());
348352
}
349353

350354
let _old = spec.components.insert(
351-
"com1".to_string(),
355+
"com1".into(),
352356
ComponentV0::SerialPort(SerialPort { num: SerialPortNumber::Com1 }),
353357
);
354358
assert!(_old.is_none());
355359

356360
if let Some(boot_order) = boot_order.as_ref() {
357361
let _old = spec.components.insert(
358-
"boot-settings".to_string(),
362+
"boot-settings".into(),
359363
ComponentV0::BootSettings(BootSettings {
360364
order: boot_order
361365
.iter()
@@ -370,7 +374,7 @@ impl<'dr> VmConfig<'dr> {
370374

371375
if let Some(mig) = migration_failure.as_ref() {
372376
let _old = spec.components.insert(
373-
"migration-failure".to_string(),
377+
"migration-failure".into(),
374378
ComponentV0::MigrationFailureInjector(mig.clone()),
375379
);
376380
assert!(_old.is_none());

0 commit comments

Comments
 (0)