6
6
7
7
use crate :: AllMupdateOverrides ;
8
8
use crate :: AllZoneManifests ;
9
+ use crate :: MupdateOverrideReadError ;
9
10
use crate :: MupdateOverrideStatus ;
10
11
use crate :: RAMDISK_IMAGE_PATH ;
11
12
use crate :: ZoneManifestStatus ;
12
13
use crate :: install_dataset_file_name;
14
+ use crate :: ramdisk_file_source;
13
15
use camino:: Utf8PathBuf ;
14
16
use illumos_utils:: running_zone:: ZoneImageFileSource ;
15
17
use nexus_sled_agent_shared:: inventory:: OmicronZoneImageSource ;
16
18
use sled_agent_config_reconciler:: InternalDisks ;
17
19
use sled_agent_config_reconciler:: InternalDisksWithBootDisk ;
20
+ use slog:: error;
18
21
use slog:: o;
22
+ use slog_error_chain:: InlineErrorChain ;
19
23
use std:: sync:: Arc ;
20
24
use std:: sync:: Mutex ;
21
25
26
+ /// A zone image source.
27
+ #[ derive( Clone , Debug ) ]
28
+ pub enum ZoneImageSource {
29
+ /// An Omicron zone.
30
+ Omicron ( OmicronZoneImageSource ) ,
31
+
32
+ /// A RAM disk-based zone.
33
+ Ramdisk ,
34
+ }
35
+
22
36
/// Resolves [`OmicronZoneImageSource`] instances into file names and search
23
37
/// paths.
24
38
///
@@ -57,11 +71,19 @@ impl ZoneImageSourceResolver {
57
71
pub fn file_source_for (
58
72
& self ,
59
73
zone_type : & str ,
60
- image_source : & OmicronZoneImageSource ,
74
+ image_source : & ZoneImageSource ,
61
75
internal_disks : InternalDisks ,
62
- ) -> ZoneImageFileSource {
63
- let inner = self . inner . lock ( ) . unwrap ( ) ;
64
- inner. file_source_for ( zone_type, image_source, internal_disks)
76
+ ) -> Result < ZoneImageFileSource , MupdateOverrideReadError > {
77
+ match image_source {
78
+ ZoneImageSource :: Ramdisk => {
79
+ // RAM disk images are always stored on the RAM disk path.
80
+ Ok ( ramdisk_file_source ( zone_type) )
81
+ }
82
+ ZoneImageSource :: Omicron ( image_source) => {
83
+ let inner = self . inner . lock ( ) . unwrap ( ) ;
84
+ inner. file_source_for ( zone_type, image_source, internal_disks)
85
+ }
86
+ }
65
87
}
66
88
}
67
89
@@ -77,7 +99,6 @@ pub struct ResolverStatus {
77
99
78
100
#[ derive( Debug ) ]
79
101
struct ResolverInner {
80
- #[ expect( unused) ]
81
102
log : slog:: Logger ,
82
103
image_directory_override : Option < Utf8PathBuf > ,
83
104
// Store all collected information for zones -- we're going to need to
@@ -112,41 +133,222 @@ impl ResolverInner {
112
133
zone_type : & str ,
113
134
image_source : & OmicronZoneImageSource ,
114
135
internal_disks : InternalDisks ,
115
- ) -> ZoneImageFileSource {
136
+ ) -> Result < ZoneImageFileSource , MupdateOverrideReadError > {
116
137
match image_source {
117
138
OmicronZoneImageSource :: InstallDataset => {
118
- // Look for the image in the ramdisk first
139
+ let file_name = install_dataset_file_name ( zone_type) ;
140
+ // Look for the image in the RAM disk first. Note that install
141
+ // dataset images are not stored on the RAM disk in production,
142
+ // just in development or test workflows.
119
143
let mut zone_image_paths =
120
144
vec ! [ Utf8PathBuf :: from( RAMDISK_IMAGE_PATH ) ] ;
145
+
121
146
// Inject an image path if requested by a test.
122
147
if let Some ( path) = & self . image_directory_override {
123
148
zone_image_paths. push ( path. clone ( ) ) ;
124
149
} ;
125
150
126
- // If the boot disk exists, look for the image in the "install"
127
- // dataset on the boot zpool.
151
+ // Any zones not part of the RAM disk are managed via the
152
+ // zone manifest.
153
+ //
154
+ // XXX: we ask for the boot zpool to be passed in here. But
155
+ // `AllZoneImages` also caches the boot zpool. How should we
156
+ // reconcile the two?
128
157
if let Some ( path) = internal_disks. boot_disk_install_dataset ( ) {
129
- zone_image_paths. push ( path) ;
158
+ match self . zone_manifests . boot_disk_result ( ) {
159
+ Ok ( result) => {
160
+ match result. data . get ( file_name. as_str ( ) ) {
161
+ Some ( result) => {
162
+ if result. is_valid ( ) {
163
+ zone_image_paths. push ( path) ;
164
+ } else {
165
+ // If the zone is not valid, we refuse to start
166
+ // it.
167
+ error ! (
168
+ self . log,
169
+ "zone {} is not valid in the zone manifest, \
170
+ not returning it as a source",
171
+ file_name;
172
+ "error" => %result. display( )
173
+ ) ;
174
+ }
175
+ }
176
+ None => {
177
+ error ! (
178
+ self . log,
179
+ "zone {} is not present in the boot disk zone manifest" ,
180
+ file_name,
181
+ ) ;
182
+ }
183
+ }
184
+ }
185
+ Err ( error) => {
186
+ error ! (
187
+ self . log,
188
+ "error parsing boot disk zone manifest, not returning \
189
+ install dataset as a source";
190
+ "error" => InlineErrorChain :: new( error) ,
191
+ ) ;
192
+ }
193
+ }
130
194
}
131
195
132
- ZoneImageFileSource {
133
- file_name : install_dataset_file_name ( zone_type ) ,
196
+ Ok ( ZoneImageFileSource {
197
+ file_name,
134
198
search_paths : zone_image_paths,
135
- }
199
+ } )
136
200
}
137
201
OmicronZoneImageSource :: Artifact { hash } => {
202
+ // TODO: implement mupdate override here.
203
+ //
138
204
// Search both artifact datasets. This iterator starts with the
139
205
// dataset for the boot disk (if it exists), and then is followed
140
206
// by all other disks.
141
207
let search_paths =
142
208
internal_disks. all_artifact_datasets ( ) . collect ( ) ;
143
- ZoneImageFileSource {
144
- // Images in the artifact store are named by just their
145
- // hash.
209
+ Ok ( ZoneImageFileSource {
146
210
file_name : hash. to_string ( ) ,
147
211
search_paths,
148
- }
212
+ } )
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ #[ cfg( test) ]
219
+ mod tests {
220
+ use super :: * ;
221
+
222
+ use crate :: test_utils:: {
223
+ BOOT_PATHS , BOOT_ZPOOL , WriteInstallDatasetContext ,
224
+ make_internal_disks_rx,
225
+ } ;
226
+
227
+ use camino_tempfile_ext:: prelude:: * ;
228
+ use dropshot:: { ConfigLogging , ConfigLoggingLevel , test_util:: LogContext } ;
229
+
230
+ /// Test source resolver behavior when the zone manifest is invalid.
231
+ #[ test]
232
+ fn file_source_zone_manifest_invalid ( ) {
233
+ let logctx = LogContext :: new (
234
+ "source_resolver_file_source_zone_manifest_invalid" ,
235
+ & ConfigLogging :: StderrTerminal { level : ConfigLoggingLevel :: Debug } ,
236
+ ) ;
237
+ let dir = Utf8TempDir :: new ( ) . unwrap ( ) ;
238
+ dir. child ( & BOOT_PATHS . install_dataset ) . create_dir_all ( ) . unwrap ( ) ;
239
+
240
+ let internal_disks_rx =
241
+ make_internal_disks_rx ( dir. path ( ) , BOOT_ZPOOL , & [ ] ) ;
242
+ let resolver = ZoneImageSourceResolver :: new (
243
+ & logctx. log ,
244
+ internal_disks_rx. current_with_boot_disk ( ) ,
245
+ ) ;
246
+
247
+ // RAM disk image sources should work as expected.
248
+ let ramdisk_source = resolver
249
+ . file_source_for (
250
+ "zone1" ,
251
+ & ZoneImageSource :: Ramdisk ,
252
+ internal_disks_rx. current ( ) ,
253
+ )
254
+ . unwrap ( ) ;
255
+ assert_eq ! ( ramdisk_source, ramdisk_file_source( "zone1" ) ) ;
256
+
257
+ let file_source = resolver
258
+ . file_source_for (
259
+ "zone1" ,
260
+ & ZoneImageSource :: Omicron (
261
+ OmicronZoneImageSource :: InstallDataset ,
262
+ ) ,
263
+ internal_disks_rx. current ( ) ,
264
+ )
265
+ . unwrap ( ) ;
266
+
267
+ // Because the zone manifest is missing, the file source should not
268
+ // return the install dataset.
269
+ assert_eq ! (
270
+ file_source,
271
+ ZoneImageFileSource {
272
+ file_name: install_dataset_file_name( "zone1" ) ,
273
+ search_paths: vec![ Utf8PathBuf :: from( RAMDISK_IMAGE_PATH ) ]
149
274
}
275
+ ) ;
276
+
277
+ logctx. cleanup_successful ( ) ;
278
+ }
279
+
280
+ /// Test source resolver behavior when the zone manifest detects errors.
281
+ #[ test]
282
+ fn file_source_with_errors ( ) {
283
+ let logctx = LogContext :: new (
284
+ "source_resolver_file_source_with_errors" ,
285
+ & ConfigLogging :: StderrTerminal { level : ConfigLoggingLevel :: Debug } ,
286
+ ) ;
287
+ let dir = Utf8TempDir :: new ( ) . unwrap ( ) ;
288
+ let mut cx = WriteInstallDatasetContext :: new_basic ( ) ;
289
+ cx. make_error_cases ( ) ;
290
+
291
+ cx. write_to ( & dir. child ( & BOOT_PATHS . install_dataset ) ) . unwrap ( ) ;
292
+
293
+ let internal_disks_rx =
294
+ make_internal_disks_rx ( dir. path ( ) , BOOT_ZPOOL , & [ ] ) ;
295
+ let resolver = ZoneImageSourceResolver :: new (
296
+ & logctx. log ,
297
+ internal_disks_rx. current_with_boot_disk ( ) ,
298
+ ) ;
299
+
300
+ // The resolver should not fail for ramdisk images.
301
+ let file_source = resolver
302
+ . file_source_for (
303
+ "fake-zone" ,
304
+ & ZoneImageSource :: Ramdisk ,
305
+ internal_disks_rx. current ( ) ,
306
+ )
307
+ . unwrap ( ) ;
308
+ assert_eq ! ( file_source, ramdisk_file_source( "fake-zone" ) ) ;
309
+
310
+ // zone1.tar.gz is valid.
311
+ let file_source = resolver
312
+ . file_source_for (
313
+ "zone1" ,
314
+ & ZoneImageSource :: Omicron (
315
+ OmicronZoneImageSource :: InstallDataset ,
316
+ ) ,
317
+ internal_disks_rx. current ( ) ,
318
+ )
319
+ . unwrap ( ) ;
320
+ assert_eq ! (
321
+ file_source,
322
+ ZoneImageFileSource {
323
+ file_name: "zone1.tar.gz" . to_string( ) ,
324
+ search_paths: vec![
325
+ Utf8PathBuf :: from( RAMDISK_IMAGE_PATH ) ,
326
+ dir. path( ) . join( & BOOT_PATHS . install_dataset)
327
+ ]
328
+ } ,
329
+ ) ;
330
+
331
+ // zone2, zone3, zone4 and zone5 aren't valid, and none of them will
332
+ // return the install dataset path.
333
+ for zone_name in [ "zone2" , "zone3" , "zone4" , "zone5" ] {
334
+ let file_source = resolver
335
+ . file_source_for (
336
+ zone_name,
337
+ & ZoneImageSource :: Omicron (
338
+ OmicronZoneImageSource :: InstallDataset ,
339
+ ) ,
340
+ internal_disks_rx. current ( ) ,
341
+ )
342
+ . unwrap ( ) ;
343
+ assert_eq ! (
344
+ file_source,
345
+ ZoneImageFileSource {
346
+ file_name: install_dataset_file_name( zone_name) ,
347
+ search_paths: vec![ Utf8PathBuf :: from( RAMDISK_IMAGE_PATH ) ]
348
+ }
349
+ ) ;
150
350
}
351
+
352
+ logctx. cleanup_successful ( ) ;
151
353
}
152
354
}
0 commit comments