Skip to content

Commit df2e550

Browse files
author
Justin Terry
committed
Implements the VM SCSI Controller
This change implements the SCSI controller operations for the new V2 shim. Signed-off-by: Justin Terry <terryjustin@microsoft.com>
1 parent 372f3f6 commit df2e550

12 files changed

Lines changed: 2166 additions & 37 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
//go:build windows
2+
3+
package scsi
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"sync"
9+
10+
"github.com/Microsoft/hcsshim/internal/controller/device/scsi/disk"
11+
"github.com/Microsoft/hcsshim/internal/controller/device/scsi/mount"
12+
13+
"github.com/google/uuid"
14+
)
15+
16+
type VmSCSIOps interface {
17+
disk.VmSCSIAdder
18+
disk.VmSCSIRemover
19+
}
20+
21+
type LinuxGuestSCSIOps interface {
22+
mount.LinuxGuestSCSIMounter
23+
mount.LinuxGuestSCSIUnmounter
24+
disk.LinuxGuestSCSIEjector
25+
}
26+
27+
type WindowsGuestSCSIOps interface {
28+
mount.WindowsGuestSCSIMounter
29+
mount.WindowsGuestSCSIUnmounter
30+
}
31+
32+
// numLUNsPerController is the maximum number of LUNs per controller, fixed by Hyper-V.
33+
const numLUNsPerController = 64
34+
35+
// The controller manages all SCSI attached devices and guest mounted
36+
// directories.
37+
//
38+
// It is required that all callers:
39+
//
40+
// 1. Obtain a reservation using Reserve().
41+
//
42+
// 2. Use the reservation to MapToGuest() to ensure resource availability.
43+
//
44+
// 3. Call UnmapFromGuest() to release the reservation and all resources.
45+
//
46+
// If MapToGuest() fails, the caller must call UnmapFromGuest() to release the
47+
// reservation and all resources.
48+
//
49+
// If UnmapFromGuest() fails, the caller must call UnmapFromGuest() again until
50+
// it succeeds to release the reservation and all resources.
51+
type Controller struct {
52+
vm VmSCSIOps
53+
lGuest LinuxGuestSCSIOps
54+
wGuest WindowsGuestSCSIOps
55+
56+
mu sync.Mutex
57+
58+
// Every call to Reserve gets a unique reservation ID which holds pointers
59+
// to its controllerSlot for the disk and its partition for the mount.
60+
reservations map[uuid.UUID]*reservation
61+
62+
// For fast lookup we keep a hostPath to controllerSlot mapping for all
63+
// allocated disks.
64+
disksByPath map[string]int
65+
66+
// Tracks all allocated and unallocated available slots on the SCSI
67+
// controllers.
68+
//
69+
// NumControllers == len(controllerSlots) / numLUNsPerController
70+
// ControllerID == index / numLUNsPerController
71+
// LunID == index % numLUNsPerController
72+
controllerSlots []*disk.Disk
73+
}
74+
75+
func New(numControllers int, vm VmSCSIOps, lGuest LinuxGuestSCSIOps, wGuest WindowsGuestSCSIOps) *Controller {
76+
return &Controller{
77+
vm: vm,
78+
lGuest: lGuest,
79+
wGuest: wGuest,
80+
mu: sync.Mutex{},
81+
reservations: make(map[uuid.UUID]*reservation),
82+
disksByPath: make(map[string]int),
83+
controllerSlots: make([]*disk.Disk, numControllers*numLUNsPerController),
84+
}
85+
}
86+
87+
// ReserveForRootfs reserves a specific controller and lun location for the
88+
// rootfs. This is required to ensure the rootfs is always at a known location
89+
// and that location is not used for any other disk. This should only be called
90+
// once per controller and lun location, and must be called before any calls to
91+
// Reserve() to ensure the rootfs reservation is not evicted by a dynamic
92+
// reservation.
93+
func (c *Controller) ReserveForRootfs(ctx context.Context, controller, lun uint) error {
94+
c.mu.Lock()
95+
defer c.mu.Unlock()
96+
97+
slot := int(controller*numLUNsPerController + lun)
98+
if slot >= len(c.controllerSlots) {
99+
return fmt.Errorf("invalid controller %d or lun %d", controller, lun)
100+
}
101+
if c.controllerSlots[slot] != nil {
102+
return fmt.Errorf("slot for controller %d and lun %d is already reserved", controller, lun)
103+
}
104+
c.controllerSlots[slot] = disk.NewReserved(controller, lun, disk.DiskConfig{})
105+
return nil
106+
}
107+
108+
// Reserves a referenced counted mapping entry for a SCSI attachment based on
109+
// the SCSI disk path, and partition number.
110+
//
111+
// If an error is returned from this function, it is guaranteed that no
112+
// reservation mapping was made and no UnmapFromGuest() call is necessary to
113+
// clean up.
114+
func (c *Controller) Reserve(ctx context.Context, diskConfig disk.DiskConfig, mountConfig mount.MountConfig) (uuid.UUID, error) {
115+
c.mu.Lock()
116+
defer c.mu.Unlock()
117+
118+
// Generate a new reservation id.
119+
id := uuid.New()
120+
if _, ok := c.reservations[id]; ok {
121+
return uuid.Nil, fmt.Errorf("reservation ID collision")
122+
}
123+
r := &reservation{
124+
controllerSlot: -1,
125+
partition: mountConfig.Partition,
126+
}
127+
128+
// Determine if this hostPath already had a disk known.
129+
if slot, ok := c.disksByPath[diskConfig.HostPath]; ok {
130+
r.controllerSlot = slot // Update our reservation where the disk is.
131+
d := c.controllerSlots[slot]
132+
133+
// Verify the caller config is the same.
134+
if !d.Config().Equals(diskConfig) {
135+
return uuid.Nil, fmt.Errorf("cannot reserve ref on disk with different config")
136+
}
137+
138+
// We at least have a disk, now determine if we have a mount for this
139+
// partition.
140+
if _, err := d.ReservePartition(ctx, mountConfig); err != nil {
141+
return uuid.Nil, fmt.Errorf("reserve partition %d: %w", mountConfig.Partition, err)
142+
}
143+
} else {
144+
// No hostPath was found. Find a slot for the disk.
145+
nextSlot := -1
146+
for i, d := range c.controllerSlots {
147+
if d == nil {
148+
nextSlot = i
149+
break
150+
}
151+
}
152+
if nextSlot == -1 {
153+
return uuid.Nil, fmt.Errorf("no available slots")
154+
}
155+
156+
// Create the Disk and Partition Mount in the reserved states.
157+
controller := uint(nextSlot / numLUNsPerController)
158+
lun := uint(nextSlot % numLUNsPerController)
159+
d := disk.NewReserved(controller, lun, diskConfig)
160+
if _, err := d.ReservePartition(ctx, mountConfig); err != nil {
161+
return uuid.Nil, fmt.Errorf("reserve partition %d: %w", mountConfig.Partition, err)
162+
}
163+
c.controllerSlots[controller*numLUNsPerController+lun] = d
164+
c.disksByPath[diskConfig.HostPath] = nextSlot
165+
r.controllerSlot = nextSlot
166+
}
167+
168+
// Ensure our reservation is saved for all future operations.
169+
c.reservations[id] = r
170+
return id, nil
171+
}
172+
173+
func (c *Controller) MapToGuest(ctx context.Context, reservation uuid.UUID) (string, error) {
174+
c.mu.Lock()
175+
defer c.mu.Unlock()
176+
177+
if r, ok := c.reservations[reservation]; ok {
178+
d := c.controllerSlots[r.controllerSlot]
179+
if err := d.AttachToVm(ctx, c.vm); err != nil {
180+
return "", fmt.Errorf("attach disk to vm: %w", err)
181+
}
182+
guestPath, err := d.MountPartitionToGuest(ctx, r.partition, c.lGuest, c.wGuest)
183+
if err != nil {
184+
return "", fmt.Errorf("mount partition %d to guest: %w", r.partition, err)
185+
}
186+
return guestPath, nil
187+
}
188+
return "", fmt.Errorf("reservation %s not found", reservation)
189+
}
190+
191+
func (c *Controller) UnmapFromGuest(ctx context.Context, reservation uuid.UUID) error {
192+
c.mu.Lock()
193+
defer c.mu.Unlock()
194+
195+
if r, ok := c.reservations[reservation]; ok {
196+
d := c.controllerSlots[r.controllerSlot]
197+
// Ref counted unmount.
198+
if err := d.UnmountPartitionFromGuest(ctx, r.partition, c.lGuest, c.wGuest); err != nil {
199+
return fmt.Errorf("unmount partition %d from guest: %w", r.partition, err)
200+
}
201+
if err := d.DetachFromVm(ctx, c.vm, c.lGuest); err != nil {
202+
return fmt.Errorf("detach disk from vm: %w", err)
203+
}
204+
if d.State() == disk.DiskStateDetached {
205+
// If we have no more mounts on this disk, remove the disk from the
206+
// known disks and free the slot.
207+
delete(c.disksByPath, d.HostPath())
208+
c.controllerSlots[r.controllerSlot] = nil
209+
}
210+
delete(c.reservations, reservation)
211+
return nil
212+
}
213+
return fmt.Errorf("reservation %s not found", reservation)
214+
}
215+
216+
type reservation struct {
217+
// This is the index into controllerSlots that holds this disk.
218+
controllerSlot int
219+
// This is the index into the disk mounts for this partition.
220+
partition uint64
221+
}

0 commit comments

Comments
 (0)