|
| 1 | +//go:build windows |
| 2 | + |
| 3 | +// Package scsi manages the full lifecycle of SCSI disk mappings on a |
| 4 | +// Hyper-V VM, from host-side slot allocation through guest-side mounting. |
| 5 | +// |
| 6 | +// # Architecture |
| 7 | +// |
| 8 | +// [Manager] is the primary entry point, exposing two methods: |
| 9 | +// |
| 10 | +// - [Manager.MapToGuest]: allocates a SCSI slot (if needed), attaches the |
| 11 | +// disk to the VM's SCSI bus, and mounts the specified partition inside the |
| 12 | +// guest. The caller supplies a stable mappingID that identifies the mapping |
| 13 | +// across retries. |
| 14 | +// - [Manager.UnmapFromGuest]: unmounts the partition from the guest, and |
| 15 | +// when all mappings for an attachment are released, unplugs the SCSI |
| 16 | +// device and detaches the disk from the VM. |
| 17 | +// |
| 18 | +// All operations are serialized by a single mutex on the [Manager]. Guest |
| 19 | +// paths are always auto-generated; callers cannot supply their own. |
| 20 | +// |
| 21 | +// # Layered State Model |
| 22 | +// |
| 23 | +// The state is tracked at two layers: |
| 24 | +// |
| 25 | +// - [attachment]: represents a disk on the VM's SCSI bus (one per [VMSlot]). |
| 26 | +// States: attachPending → attachAttached → attachDetaching → attachUnplugged → attachDetached. |
| 27 | +// - [mount]: represents a partition mounted inside the guest (keyed by |
| 28 | +// partition index within an attachment). |
| 29 | +// States: mountPending → mountMounted → mountUnmounted. |
| 30 | +// |
| 31 | +// A third structure, [mapping], links a caller-supplied mappingID to an |
| 32 | +// [attachment] and partition index. It carries no lifecycle state of its own; |
| 33 | +// the [attachment] and [mount] state machines drive all transitions. |
| 34 | +// |
| 35 | +// # Retry / Idempotency |
| 36 | +// |
| 37 | +// Both [Manager.MapToGuest] and [Manager.UnmapFromGuest] are designed to be |
| 38 | +// retriable. On failure, the [attachment] and [mount] states remain at their |
| 39 | +// pre-operation position (no poisoning). A subsequent call with the same |
| 40 | +// mappingID resumes from where the previous attempt stopped. |
| 41 | +// |
| 42 | +// Calling [Manager.MapToGuest] with the same mappingID after a successful call |
| 43 | +// is a no-op that returns the existing guest path. |
| 44 | +// |
| 45 | +// # Attachment Lifecycle |
| 46 | +// |
| 47 | +// ┌──────────────────┐ |
| 48 | +// │ attachPending │ ← stays here on attach failure (retriable) |
| 49 | +// └────────┬─────────┘ |
| 50 | +// │ disk added to VM SCSI bus |
| 51 | +// ▼ |
| 52 | +// ┌──────────────────┐ |
| 53 | +// │ attachAttached │ |
| 54 | +// └────────┬─────────┘ |
| 55 | +// (mounts driven here) |
| 56 | +// │ all partitions released; |
| 57 | +// │ detach initiated |
| 58 | +// ▼ |
| 59 | +// ┌──────────────────┐ |
| 60 | +// │ attachDetaching │ ← stays here on unplug failure (retriable) |
| 61 | +// └────────┬─────────┘ |
| 62 | +// │ SCSI device unplugged from guest |
| 63 | +// ▼ |
| 64 | +// ┌──────────────────┐ |
| 65 | +// │ attachUnplugged │ |
| 66 | +// └────────┬─────────┘ |
| 67 | +// │ disk removed from VM SCSI bus |
| 68 | +// ▼ |
| 69 | +// ┌──────────────────┐ |
| 70 | +// │ attachDetached │ |
| 71 | +// └──────────────────┘ |
| 72 | +// (entry removed from map) |
| 73 | +// |
| 74 | +// ┌──────────────────┐ |
| 75 | +// │ attachReserved │ ← no transitions; pre-reserved at construction |
| 76 | +// └──────────────────┘ |
| 77 | +// |
| 78 | +// # Mount Lifecycle |
| 79 | +// |
| 80 | +// ┌──────────────────┐ |
| 81 | +// │ mountPending │ ← stays here on mount failure (retriable) |
| 82 | +// └────────┬─────────┘ |
| 83 | +// │ guest mount succeeds |
| 84 | +// ▼ |
| 85 | +// ┌──────────────────┐ |
| 86 | +// │ mountMounted │ |
| 87 | +// └────────┬─────────┘ |
| 88 | +// │ refCount → 0; |
| 89 | +// │ guest unmount |
| 90 | +// ▼ |
| 91 | +// ┌──────────────────┐ |
| 92 | +// │ mountUnmounted │ |
| 93 | +// └──────────────────┘ |
| 94 | +// (partition entry removed from attachment) |
| 95 | +// |
| 96 | +// # Reference Counting |
| 97 | +// |
| 98 | +// Multiple mappingIDs may target the same disk and partition. [Manager.MapToGuest] |
| 99 | +// detects duplicates and increments a reference count on the [mount] instead of |
| 100 | +// issuing duplicate guest operations; the guest path is shared. |
| 101 | +// |
| 102 | +// [Manager.UnmapFromGuest] decrements the count and only unmounts when it reaches |
| 103 | +// zero. |
| 104 | +// |
| 105 | +// # Platform Variants |
| 106 | +// |
| 107 | +// Guest-side mount, unmount, and unplug steps differ between LCOW and WCOW |
| 108 | +// guests and are selected via build tags (default for the LCOW shim; |
| 109 | +// "wcow" tag for the WCOW shim): |
| 110 | +// |
| 111 | +// - LCOW: mounts via AddLCOWMappedVirtualDisk, unmounts via |
| 112 | +// RemoveLCOWMappedVirtualDisk, and unplugs via RemoveSCSIDevice. |
| 113 | +// - WCOW: mounts via AddWCOWMappedVirtualDisk (or |
| 114 | +// AddWCOWMappedVirtualDiskForContainerScratch for scratch disks), |
| 115 | +// unmounts via RemoveWCOWMappedVirtualDisk; unplug is a no-op because |
| 116 | +// Windows handles SCSI hot-unplug automatically when the host removes |
| 117 | +// the disk from the VM. |
| 118 | +// |
| 119 | +// # Usage |
| 120 | +// |
| 121 | +// mgr := scsi.New(vmID, vmScsi, linuxGuestScsi, windowsGuestScsi, numControllers, reservedSlots) |
| 122 | +// |
| 123 | +// diskConfig := scsi.DiskConfig{HostPath: "/path/to/disk.vhdx", Type: scsi.DiskTypeVirtualDisk} |
| 124 | +// mountConfig := scsi.MountConfig{ReadOnly: true} |
| 125 | +// |
| 126 | +// // Map the disk to the guest (allocate slot + attach + mount): |
| 127 | +// guestPath, err := mgr.MapToGuest(ctx, "container-abc/layer-0", diskConfig, mountConfig) |
| 128 | +// if err != nil { |
| 129 | +// // Retry with the same mappingID to resume: |
| 130 | +// guestPath, err = mgr.MapToGuest(ctx, "container-abc/layer-0", diskConfig, mountConfig) |
| 131 | +// } |
| 132 | +// |
| 133 | +// // Unmap (unmount + unplug + detach when last mapping): |
| 134 | +// if err := mgr.UnmapFromGuest(ctx, "container-abc/layer-0"); err != nil { |
| 135 | +// // Retry: |
| 136 | +// _ = mgr.UnmapFromGuest(ctx, "container-abc/layer-0") |
| 137 | +// } |
| 138 | +package scsi |
0 commit comments