|
| 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 | +} |
0 commit comments