Skip to content

Commit bbfcee7

Browse files
committed
[shimV2] adds the mount controller
Adds the `mount` package to manage the guest-side lifecycle of SCSI disks and Plan9 shares in Hyper-V utility VMs. Key features include: - A `Manager` entry point providing explicit Mount/Unmount APIs. - Reference-counted mounts to safely allow multiple callers to share the same guest path without conflicts. - An explicit, forward-moving state machine (`Pending` -> `Mounted` -> `Unmounted` or `Invalid`) for robust lifecycle tracking. - Platform-aware delegation supporting SCSI on both LCOW/WCOW, and Plan9 on LCOW. This package is designed to operate downstream of device managers, handling strictly the guest-side mount while the device manager maintains host-side lifetime ownership. Signed-off-by: Harsh Rawat <harshrawat@microsoft.com>
1 parent 87708ff commit bbfcee7

10 files changed

Lines changed: 884 additions & 24 deletions

File tree

internal/controller/mount/doc.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//go:build windows
2+
3+
// Package mount manages the lifecycle of guest-level mounts for Hyper-V
4+
// utility VMs.
5+
//
6+
// # Architecture
7+
//
8+
// The [Manager] type is the primary entry point. It exposes a set of APIs
9+
// that handles guest mounts for both SCSI disks and Plan9 shares,
10+
// with a dedicated Resolve/Mount/Unmount API for each device type.
11+
//
12+
// Internally, Manager tracks guest-level mounts for each device type,
13+
// each of which has its own explicit state machine. Mounts are reference-counted
14+
// so that multiple callers sharing the same guest path do not conflict.
15+
// Caller should resolve the guest path prior to invoking Mount API.
16+
//
17+
// # Device manager integration
18+
//
19+
// This package is designed to work downstream of the device managers:
20+
//
21+
// - After [device.scsi.AttachDiskToVM] returns a [VMSlot], pass its
22+
// Controller and LUN fields to [Manager.MountSCSI].
23+
// - After [device.plan9.AddToVM] returns a shareName, pass it to
24+
// [Manager.MountPlan9].
25+
//
26+
// The device manager owns the host-side lifetime of the device. The mount
27+
// manager owns only the guest-side mount. Callers must ensure the device
28+
// is attached before mounting, and must unmount before detaching.
29+
//
30+
// # SCSI mounts
31+
//
32+
// SCSI mounts are supported on both LCOW and WCOW guests. The Manager
33+
// delegates to platform specific guest operator based on the build
34+
// tag (linux shim vs. windows shim).
35+
//
36+
// # Plan9 mounts
37+
//
38+
// Plan9 mounts are LCOW-only (Plan9 is a Linux guest protocol).
39+
// The Manager delegates to Linux guest operator for
40+
// the actual GCS add/remove-mapped-directory requests.
41+
//
42+
// # Reference counting
43+
//
44+
// Both SCSI and Plan9 mounts are ref-counted. When two callers mount the
45+
// same resource at the same guest path with the same config, they share a
46+
// single mount entry. The guest-side unmount only happens when the last
47+
// reference is released.
48+
//
49+
// # State Machine
50+
//
51+
// Every mount (SCSI or Plan9) carries a [mountState]. States move forward
52+
// only.
53+
//
54+
// Each mount entry progresses through the states below.
55+
// The happy path runs down the left column; the error path is on the right.
56+
//
57+
// Mount operation requested
58+
// │
59+
// ▼
60+
// ┌─────────────────────┐
61+
// │ mountPending │
62+
// └──────────┬──────────┘
63+
// │
64+
// ┌───────┴────────────────────────────────┐
65+
// │ mountInGuest succeeds │ mountInGuest fails
66+
// ▼ ▼
67+
// ┌─────────────────────┐ ┌──────────────────────┐
68+
// │ mountMounted │ │ mountInvalid │
69+
// └──────────┬──────────┘ └──────────┬───────────┘
70+
// │ unmountFromGuest │
71+
// │ succeeds │
72+
// ▼ ▼
73+
// ┌─────────────────────┐ (auto-removed from map)
74+
// │ mountUnmounted │ ← terminal; entry removed from map
75+
// └─────────────────────┘
76+
//
77+
// State descriptions:
78+
//
79+
// - [mountPending]: entered when a mount operation begins.
80+
// The guest mount call has not yet completed.
81+
// - [mountMounted]: entered once mountInGuest succeeds; the guest path
82+
// is accessible inside the VM.
83+
// - [mountUnmounted]: entered once unmountFromGuest succeeds;
84+
// the guest path is no longer accessible. Terminal state.
85+
// - [mountInvalid]: entered when mountInGuest fails; concurrent waiters
86+
// receive the original error and the entry is removed from the map.
87+
package mount

internal/controller/mount/mount.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//go:build windows
2+
3+
package mount
4+
5+
import "sync"
6+
7+
// Manager tracks guest-level mounts for SCSI and Plan9 devices, and delegates
8+
// to the appropriate OS-specific guest manager for the actual GCS calls.
9+
type Manager struct {
10+
// globalMu protects all four mount maps (scsiByPath, scsiByDisk, plan9ByPath,
11+
// plan9ByShare) and serializes path allocation across concurrent callers.
12+
globalMu sync.Mutex
13+
14+
// scsiByPath is the global index of SCSI mounts, keyed by guest path.
15+
// Two callers mounting the same disk at the same path share one scsiMount
16+
// entry and jointly hold its refCount.
17+
// Access must be guarded by globalMu.
18+
scsiByPath map[string]*scsiMount
19+
20+
// scsiByDisk maps a SCSI disk (controller + LUN) to its resolved guest path.
21+
// Access must be guarded by globalMu.
22+
scsiByDisk map[scsiDisk]string
23+
24+
// plan9ByPath is the global index of Plan9 mounts, keyed by guest path.
25+
// Access must be guarded by globalMu.
26+
plan9ByPath map[string]*plan9Mount
27+
28+
// plan9ByShare maps a Plan9 share name to its resolved guest path.
29+
// Access must be guarded by globalMu.
30+
plan9ByShare map[string]string
31+
32+
// nextSCSIMountIdx generates stable unique guest paths for SCSI auto-paths.
33+
nextSCSIMountIdx int
34+
35+
// nextPlan9MountIdx generates stable unique guest paths for Plan9 auto-paths.
36+
nextPlan9MountIdx int
37+
38+
// Interfaces used for performing guest actions.
39+
linuxGuestSCSI linuxGuestSCSI
40+
windowsGuestSCSI windowsGuestSCSI
41+
linuxGuestPlan9 linuxGuestPlan9
42+
}
43+
44+
// New creates a new instance of mount.Manager which can be used
45+
// for perform SCSI and Plan9 mount operations.
46+
func New(
47+
linuxGuestSCSI linuxGuestSCSI,
48+
windowsGuestSCSI windowsGuestSCSI,
49+
linuxGuestPlan9 linuxGuestPlan9,
50+
) *Manager {
51+
return &Manager{
52+
scsiByPath: make(map[string]*scsiMount),
53+
scsiByDisk: make(map[scsiDisk]string),
54+
plan9ByPath: make(map[string]*plan9Mount),
55+
plan9ByShare: make(map[string]string),
56+
linuxGuestSCSI: linuxGuestSCSI,
57+
windowsGuestSCSI: windowsGuestSCSI,
58+
linuxGuestPlan9: linuxGuestPlan9,
59+
}
60+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//go:build windows
2+
3+
package mount
4+
5+
import (
6+
"context"
7+
"sync"
8+
9+
"github.com/Microsoft/hcsshim/internal/protocol/guestresource"
10+
)
11+
12+
// ==============================================================================
13+
// Exported data structures
14+
// ==============================================================================
15+
16+
// SCSIMountConfig describes how a SCSI disk should be mounted inside the guest.
17+
type SCSIMountConfig struct {
18+
// GuestPath is the path inside the guest where the disk will be mounted.
19+
// Must be non-empty.
20+
GuestPath string
21+
// Partition is the target partition index (1-based) on a partitioned device.
22+
// Only supported for LCOW.
23+
Partition uint64
24+
// ReadOnly mounts the disk read-only.
25+
ReadOnly bool
26+
// Encrypted encrypts the device with dm-crypt.
27+
// Only supported for LCOW.
28+
Encrypted bool
29+
// Options are mount flags or data passed to the guest mount call.
30+
// Only supported for LCOW.
31+
Options []string
32+
// EnsureFilesystem formats the mount as Filesystem if not already formatted.
33+
// Only supported for LCOW.
34+
EnsureFilesystem bool
35+
// Filesystem is the target filesystem type.
36+
// Only supported for LCOW.
37+
Filesystem string
38+
// BlockDev mounts the device as a block device.
39+
// Only supported for LCOW.
40+
BlockDev bool
41+
// FormatWithRefs formats the disk using refs.
42+
// Only supported for WCOW scratch disks.
43+
FormatWithRefs bool
44+
}
45+
46+
// Plan9MountConfig describes how a Plan9 share should be mounted inside the guest.
47+
type Plan9MountConfig struct {
48+
// GuestPath is the path inside the guest where the share will be mounted.
49+
// Must be non-empty.
50+
GuestPath string
51+
// ReadOnly mounts the share read-only.
52+
ReadOnly bool
53+
}
54+
55+
// ==============================================================================
56+
// Interfaces used by Manager to perform guest actions.
57+
// ==============================================================================
58+
59+
// linuxGuestSCSI performs LCOW SCSI guest mount/unmount operations.
60+
type linuxGuestSCSI interface {
61+
AddLCOWMappedVirtualDisk(ctx context.Context, settings guestresource.LCOWMappedVirtualDisk) error
62+
RemoveLCOWMappedVirtualDisk(ctx context.Context, settings guestresource.LCOWMappedVirtualDisk) error
63+
}
64+
65+
// windowsGuestSCSI performs WCOW SCSI guest mount/unmount operations.
66+
type windowsGuestSCSI interface {
67+
AddWCOWMappedVirtualDisk(ctx context.Context, settings guestresource.WCOWMappedVirtualDisk) error
68+
AddWCOWMappedVirtualDiskForContainerScratch(ctx context.Context, settings guestresource.WCOWMappedVirtualDisk) error
69+
RemoveWCOWMappedVirtualDisk(ctx context.Context, settings guestresource.WCOWMappedVirtualDisk) error
70+
}
71+
72+
// linuxGuestPlan9 performs Plan9 guest mount/unmount operations in an LCOW guest.
73+
type linuxGuestPlan9 interface {
74+
AddLCOWMappedDirectory(ctx context.Context, settings guestresource.LCOWMappedDirectory) error
75+
RemoveLCOWMappedDirectory(ctx context.Context, settings guestresource.LCOWMappedDirectory) error
76+
}
77+
78+
// ==============================================================================
79+
// Internal data structures
80+
// ==============================================================================
81+
82+
// refTracker holds the per-mount concurrency and lifecycle fields shared by
83+
// both scsiMount (SCSI) and plan9Mount (Plan9).
84+
type refTracker struct {
85+
// mu serializes state transitions and broadcasts completion to concurrent
86+
// waiters: a goroutine that finds a Pending entry simply locks mu and waits
87+
// for the owner to move the state to Mounted or Invalid.
88+
mu sync.Mutex
89+
90+
// state is the forward-only lifecycle position of this mount.
91+
// Access must be guarded by mu.
92+
state mountState
93+
94+
// stateErr records the error that caused a transition to mountInvalid.
95+
// Waiters that find the mount in the invalid state return this error so
96+
// every caller sees the original failure reason.
97+
stateErr error
98+
99+
// refCount is the number of active callers sharing this mount.
100+
// Access must be guarded by mu.
101+
refCount uint
102+
}
103+
104+
// scsiDisk is the composite map key for indexing a SCSI mount by controller and LUN.
105+
type scsiDisk struct {
106+
controller uint
107+
lun uint
108+
}
109+
110+
// scsiMount tracks one SCSI disk mounted in the guest.
111+
type scsiMount struct {
112+
refTracker
113+
114+
guestPath string
115+
controller uint
116+
lun uint
117+
config *SCSIMountConfig
118+
}
119+
120+
// plan9Mount tracks one Plan9 share mounted in the guest.
121+
type plan9Mount struct {
122+
refTracker
123+
124+
// guestPath is the path inside the guest where the share is mounted.
125+
guestPath string
126+
// shareName is the Plan9 share name returned by [plan9.AddToVM].
127+
shareName string
128+
// config holds the mount options used when this share was first mounted.
129+
config *Plan9MountConfig
130+
}

0 commit comments

Comments
 (0)