Skip to content

Commit 17b9d55

Browse files
authored
[1/5] User data export: DB models and queries (#8471)
This set of PRs implements the endpoints and functionality proposed in RFD 563, and the set starts with adding the database parts: the models, the schema change, and the query routines. A "user data export" object records the attachment of a read-only volume to a particular Pantry for the purpose of exporting data. These objects will be created and deleted using sagas invoked by a background task, and not managed by a user or operator. As of right now the only read-only user resources that can be exported are snapshots and images. A user data export object will be created for each non-deleted snapshot and image. A "changeset" representing the work required to manage this will be queried for by the associated background task. If a snapshot or image is deleted, the associated user data export object will automatically be marked for deletion, and cleaned up by a saga. In order not to create unbounded work, a similar pattern of using a state plus "locking" with an operating saga id is used. Otherwise the background task's periodic activation could initiate an unbounded amount of sagas based on the computed changeset. During Pantry expungement, that Pantry will no longer be returned by a DNS lookup for all in-service Pantry addresses. The list of in-service addresses returned from a DNS lookup functions similarly to a rendezvous table, and is used to mark user data export objects affected by expungements for deletion so they can be recreated by the associated background task. Even if a Pantry is not expunged, a restart of the service (or a bounce of the sled) will cause all volume attachments to be lost. The user data export logic handles this by detecting when this occurs and marking any affected user data export as deleted so that another attachment can be created. This will be seen in future commits of this set.
1 parent 28bb4b0 commit 17b9d55

File tree

19 files changed

+2394
-3
lines changed

19 files changed

+2394
-3
lines changed

nexus/db-model/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ mod typed_uuid;
118118
mod unsigned;
119119
mod upstairs_repair;
120120
mod user_builtin;
121+
mod user_data_export;
121122
mod utilization;
122123
mod virtual_provisioning_collection;
123124
mod virtual_provisioning_resource;
@@ -241,6 +242,7 @@ pub use typed_uuid::DbTypedUuid;
241242
pub use typed_uuid::to_db_typed_uuid;
242243
pub use upstairs_repair::*;
243244
pub use user_builtin::*;
245+
pub use user_data_export::*;
244246
pub use utilization::*;
245247
pub use v2p_mapping::*;
246248
pub use virtual_provisioning_collection::*;

nexus/db-model/src/schema_versions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
1616
///
1717
/// This must be updated when you change the database schema. Refer to
1818
/// schema/crdb/README.adoc in the root of this repository for details.
19-
pub const SCHEMA_VERSION: Version = Version::new(156, 0, 0);
19+
pub const SCHEMA_VERSION: Version = Version::new(157, 0, 0);
2020

2121
/// List of all past database schema versions, in *reverse* order
2222
///
@@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
2828
// | leaving the first copy as an example for the next person.
2929
// v
3030
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
31+
KnownVersion::new(157, "user-data-export"),
3132
KnownVersion::new(156, "boot-partitions-inventory"),
3233
KnownVersion::new(155, "vpc-firewall-icmp"),
3334
KnownVersion::new(154, "add-pending-mgs-updates"),
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
use super::impl_enum_type;
6+
use crate::SqlU16;
7+
use crate::ipv6;
8+
use crate::typed_uuid::DbTypedUuid;
9+
use nexus_db_schema::schema::user_data_export;
10+
use omicron_uuid_kinds::UserDataExportKind;
11+
use omicron_uuid_kinds::UserDataExportUuid;
12+
use omicron_uuid_kinds::VolumeKind;
13+
use omicron_uuid_kinds::VolumeUuid;
14+
use serde::Deserialize;
15+
use serde::Serialize;
16+
use std::net::SocketAddrV6;
17+
use uuid::Uuid;
18+
19+
impl_enum_type!(
20+
UserDataExportResourceTypeEnum:
21+
22+
#[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)]
23+
pub enum UserDataExportResourceType;
24+
25+
// Enum values
26+
Snapshot => b"snapshot"
27+
Image => b"image"
28+
);
29+
30+
// FromStr impl required for use with clap (aka omdb)
31+
impl std::str::FromStr for UserDataExportResourceType {
32+
type Err = String;
33+
34+
fn from_str(s: &str) -> Result<Self, Self::Err> {
35+
match s {
36+
"snapshot" => Ok(UserDataExportResourceType::Snapshot),
37+
"image" => Ok(UserDataExportResourceType::Image),
38+
_ => Err(format!("unrecognized value {} for enum", s)),
39+
}
40+
}
41+
}
42+
43+
impl UserDataExportResourceType {
44+
pub fn to_string(&self) -> String {
45+
String::from(match self {
46+
UserDataExportResourceType::Snapshot => "snapshot",
47+
UserDataExportResourceType::Image => "image",
48+
})
49+
}
50+
}
51+
52+
impl_enum_type!(
53+
UserDataExportStateEnum:
54+
55+
#[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)]
56+
pub enum UserDataExportState;
57+
58+
// Enum values
59+
Requested => b"requested"
60+
Assigning => b"assigning"
61+
Live => b"live"
62+
Deleting => b"deleting"
63+
Deleted => b"deleted"
64+
);
65+
66+
/// Instead of working directly with the UserDataExportRecord, callers can use
67+
/// this enum instead, where the call site only cares of the record is live or
68+
/// not.
69+
pub enum UserDataExport {
70+
NotLive,
71+
72+
Live { pantry_address: SocketAddrV6, volume_id: VolumeUuid },
73+
}
74+
75+
/// A "user data export" object represents an attachment of a read-only volume
76+
/// to a Pantry for the purpose of exporting data. As of this writing only
77+
/// snapshots and images are able to be exported this way. Management of these
78+
/// objects is done automatically by a background task.
79+
///
80+
/// Note that read-only volumes should never directly be constructed (read: be
81+
/// passed to Volume::construct). Copies should be created so that the
82+
/// appropriate reference counting for the read-only volume targets can be
83+
/// maintained. The user data export object stores that copied Volume, among
84+
/// other things.
85+
///
86+
/// The record transitions through the following states:
87+
///
88+
/// ```text
89+
/// Requested <-- ---
90+
/// | |
91+
/// | | |
92+
/// v | | responsibility of user
93+
/// | | export create saga
94+
/// Assigning -- |
95+
/// |
96+
/// | |
97+
/// v ---
98+
/// ---
99+
/// Live <-- |
100+
/// | |
101+
/// | | |
102+
/// v | | responsibility of user
103+
/// | | export delete saga
104+
/// Deleting -- |
105+
/// |
106+
/// | |
107+
/// v |
108+
/// ---
109+
/// Deleted
110+
/// ```
111+
///
112+
/// which are captured in the UserDataExportState enum. Annotated on the right
113+
/// are which sagas are responsible for which state transitions. The state
114+
/// transitions themselves are performed by these sagas and all involve a query
115+
/// that:
116+
///
117+
/// - checks that the starting state (and other values as required) make sense
118+
/// - updates the state while setting a unique operating_saga_id id (and any
119+
/// other fields as appropriate)
120+
///
121+
/// As multiple background tasks will be waking up, checking to see what sagas
122+
/// need to be triggered, and requesting that these sagas run, this is meant to
123+
/// block multiple sagas from running at the same time in an effort to cut down
124+
/// on interference - most will unwind at the first step of performing this
125+
/// state transition instead of somewhere in the middle. This is not required
126+
/// for correctness as each saga node can deal with this type of interference.
127+
#[derive(Queryable, Insertable, Selectable, Clone, Debug)]
128+
#[diesel(table_name = user_data_export)]
129+
pub struct UserDataExportRecord {
130+
id: DbTypedUuid<UserDataExportKind>,
131+
132+
state: UserDataExportState,
133+
operating_saga_id: Option<Uuid>,
134+
generation: i64,
135+
136+
resource_id: Uuid,
137+
resource_type: UserDataExportResourceType,
138+
resource_deleted: bool,
139+
140+
pantry_ip: Option<ipv6::Ipv6Addr>,
141+
pantry_port: Option<SqlU16>,
142+
volume_id: Option<DbTypedUuid<VolumeKind>>,
143+
}
144+
145+
impl UserDataExportRecord {
146+
pub fn new(
147+
id: UserDataExportUuid,
148+
resource: UserDataExportResource,
149+
) -> Self {
150+
let (resource_type, resource_id) = match resource {
151+
UserDataExportResource::Snapshot { id } => {
152+
(UserDataExportResourceType::Snapshot, id)
153+
}
154+
155+
UserDataExportResource::Image { id } => {
156+
(UserDataExportResourceType::Image, id)
157+
}
158+
};
159+
160+
Self {
161+
id: id.into(),
162+
163+
state: UserDataExportState::Requested,
164+
operating_saga_id: None,
165+
generation: 0,
166+
167+
resource_type,
168+
resource_id,
169+
resource_deleted: false,
170+
171+
pantry_ip: None,
172+
pantry_port: None,
173+
volume_id: None,
174+
}
175+
}
176+
177+
pub fn id(&self) -> UserDataExportUuid {
178+
self.id.into()
179+
}
180+
181+
pub fn state(&self) -> UserDataExportState {
182+
self.state
183+
}
184+
185+
pub fn operating_saga_id(&self) -> Option<Uuid> {
186+
self.operating_saga_id
187+
}
188+
189+
pub fn generation(&self) -> i64 {
190+
self.generation
191+
}
192+
193+
pub fn resource(&self) -> UserDataExportResource {
194+
match self.resource_type {
195+
UserDataExportResourceType::Snapshot => {
196+
UserDataExportResource::Snapshot { id: self.resource_id }
197+
}
198+
199+
UserDataExportResourceType::Image => {
200+
UserDataExportResource::Image { id: self.resource_id }
201+
}
202+
}
203+
}
204+
205+
pub fn deleted(&self) -> bool {
206+
self.resource_deleted
207+
}
208+
209+
pub fn pantry_address(&self) -> Option<SocketAddrV6> {
210+
match (&self.pantry_ip, &self.pantry_port) {
211+
(Some(pantry_ip), Some(pantry_port)) => Some(SocketAddrV6::new(
212+
(*pantry_ip).into(),
213+
(*pantry_port).into(),
214+
0,
215+
0,
216+
)),
217+
218+
(_, _) => None,
219+
}
220+
}
221+
222+
pub fn volume_id(&self) -> Option<VolumeUuid> {
223+
self.volume_id.map(|i| i.into())
224+
}
225+
226+
pub fn is_live(&self) -> Result<UserDataExport, &'static str> {
227+
match self.state {
228+
UserDataExportState::Requested
229+
| UserDataExportState::Assigning
230+
| UserDataExportState::Deleting
231+
| UserDataExportState::Deleted => Ok(UserDataExport::NotLive),
232+
233+
UserDataExportState::Live => {
234+
let Some(pantry_ip) = self.pantry_ip else {
235+
return Err("pantry_ip is None!");
236+
};
237+
238+
let Some(pantry_port) = self.pantry_port else {
239+
return Err("pantry_port is None!");
240+
};
241+
242+
let Some(volume_id) = self.volume_id else {
243+
return Err("volume_id is None!");
244+
};
245+
246+
Ok(UserDataExport::Live {
247+
pantry_address: SocketAddrV6::new(
248+
pantry_ip.into(),
249+
*pantry_port,
250+
0,
251+
0,
252+
),
253+
254+
volume_id: volume_id.into(),
255+
})
256+
}
257+
}
258+
}
259+
}
260+
261+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
262+
pub enum UserDataExportResource {
263+
Snapshot { id: Uuid },
264+
265+
Image { id: Uuid },
266+
}
267+
268+
impl UserDataExportResource {
269+
pub fn type_string(&self) -> String {
270+
String::from(match self {
271+
UserDataExportResource::Snapshot { .. } => "snapshot",
272+
UserDataExportResource::Image { .. } => "image",
273+
})
274+
}
275+
276+
pub fn id(&self) -> Uuid {
277+
match self {
278+
UserDataExportResource::Snapshot { id } => *id,
279+
UserDataExportResource::Image { id } => *id,
280+
}
281+
}
282+
}

nexus/db-queries/src/db/datastore/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ mod target_release;
107107
#[cfg(test)]
108108
pub(crate) mod test_utils;
109109
mod update;
110+
mod user_data_export;
110111
mod utilization;
111112
mod v2p_mapping;
112113
mod virtual_provisioning_collection;
@@ -137,6 +138,7 @@ pub use sled::SledTransition;
137138
pub use sled::TransitionError;
138139
pub use support_bundle::SupportBundleExpungementReport;
139140
pub use switch_port::SwitchPortSettingsCombinedResult;
141+
pub use user_data_export::*;
140142
pub use virtual_provisioning_collection::StorageType;
141143
pub use vmm::VmmStateUpdateResult;
142144
pub use volume::*;

0 commit comments

Comments
 (0)