10
10
//! * During sled-agent startup, before trust quorum has unlocked
11
11
12
12
use camino:: Utf8PathBuf ;
13
+ use core:: cmp;
13
14
use id_map:: IdMap ;
14
15
use id_map:: IdMappable ;
15
16
use omicron_common:: disk:: DiskIdentity ;
@@ -252,15 +253,13 @@ struct InternalDiskDetails {
252
253
}
253
254
254
255
impl IdMappable for InternalDiskDetails {
255
- type Id = ( bool , Arc < DiskIdentity > ) ;
256
+ type Id = InternalDiskDetailsId ;
256
257
257
258
fn id ( & self ) -> Self :: Id {
258
- // Using `!self.is_boot_disk` as the first part of the ID allows us to
259
- // guarantee we always iterate over the internal disks with the current
260
- // boot disk first.
261
- //
262
- // TODO-cleanup add tests for this
263
- ( !self . is_boot_disk , Arc :: clone ( & self . identity ) )
259
+ InternalDiskDetailsId {
260
+ identity : Arc :: clone ( & self . identity ) ,
261
+ is_boot_disk : self . is_boot_disk ,
262
+ }
264
263
}
265
264
}
266
265
@@ -291,6 +290,36 @@ impl From<&'_ Disk> for InternalDiskDetails {
291
290
}
292
291
}
293
292
293
+ // Special ID type for `InternalDiskDetails` that lets us guarantee we sort boot
294
+ // disks ahead of non-boot disks. There's a whole set of thorny problems here
295
+ // about what to do if we think both internal disks should have the same
296
+ // contents but they disagree; we'll at least try to have callers prefer the
297
+ // boot disk if they're performing a "check the first disk that succeeds" kind
298
+ // of operation.
299
+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
300
+ struct InternalDiskDetailsId {
301
+ is_boot_disk : bool ,
302
+ identity : Arc < DiskIdentity > ,
303
+ }
304
+
305
+ impl cmp:: Ord for InternalDiskDetailsId {
306
+ fn cmp ( & self , other : & Self ) -> cmp:: Ordering {
307
+ match self . is_boot_disk . cmp ( & other. is_boot_disk ) {
308
+ cmp:: Ordering :: Equal => { }
309
+ // `false` normally sorts before `true`, so intentionally reverse
310
+ // this so that we sort boot disks ahead of non-boot disks.
311
+ ord => return ord. reverse ( ) ,
312
+ }
313
+ self . identity . cmp ( & other. identity )
314
+ }
315
+ }
316
+
317
+ impl cmp:: PartialOrd for InternalDiskDetailsId {
318
+ fn partial_cmp ( & self , other : & Self ) -> Option < std:: cmp:: Ordering > {
319
+ Some ( self . cmp ( other) )
320
+ }
321
+ }
322
+
294
323
struct InternalDisksTask {
295
324
disks_tx : watch:: Sender < Arc < IdMap < InternalDiskDetails > > > ,
296
325
raw_disks_rx : watch:: Receiver < IdMap < RawDiskWithId > > ,
@@ -307,3 +336,53 @@ impl InternalDisksTask {
307
336
unimplemented ! ( )
308
337
}
309
338
}
339
+
340
+ #[ cfg( test) ]
341
+ mod tests {
342
+ use super :: * ;
343
+ use omicron_uuid_kinds:: ZpoolUuid ;
344
+ use proptest:: sample:: size_range;
345
+ use test_strategy:: Arbitrary ;
346
+ use test_strategy:: proptest;
347
+
348
+ #[ derive( Debug , Arbitrary ) ]
349
+ struct ArbitraryInternalDiskDetailsId {
350
+ is_boot_disk : bool ,
351
+ vendor : String ,
352
+ model : String ,
353
+ serial : String ,
354
+ }
355
+
356
+ #[ proptest]
357
+ fn boot_disks_sort_ahead_of_non_boot_disks_in_id_map (
358
+ #[ any( size_range( 2 ..4 ) . lift( ) ) ] values : Vec <
359
+ ArbitraryInternalDiskDetailsId ,
360
+ > ,
361
+ ) {
362
+ let disk_map: IdMap < _ > = values
363
+ . into_iter ( )
364
+ . map ( |value| InternalDiskDetails {
365
+ identity : Arc :: new ( DiskIdentity {
366
+ vendor : value. vendor ,
367
+ model : value. model ,
368
+ serial : value. serial ,
369
+ } ) ,
370
+ zpool_name : ZpoolName :: new_internal ( ZpoolUuid :: new_v4 ( ) ) ,
371
+ is_boot_disk : value. is_boot_disk ,
372
+ slot : None ,
373
+ raw_devfs_path : None ,
374
+ } )
375
+ . collect ( ) ;
376
+
377
+ // When iterating over the contents, we should never see a boot disk
378
+ // after a non-boot disk.
379
+ let mut saw_non_boot_disk = false ;
380
+ for disk in disk_map. iter ( ) {
381
+ if disk. is_boot_disk {
382
+ assert ! ( !saw_non_boot_disk) ;
383
+ } else {
384
+ saw_non_boot_disk = true ;
385
+ }
386
+ }
387
+ }
388
+ }
0 commit comments