Skip to content

Commit 20ecf50

Browse files
authored
[shimV2] add plan9 device controller (#2641)
* [shimV2] add plan9 device controller This change adds the plan9 device controller which can add/remove plan9 shares from a VM. The guest side operations are part of mount controller responsibility. Signed-off-by: Harsh Rawat <harshrawat@microsoft.com>
1 parent 799fe74 commit 20ecf50

File tree

20 files changed

+2724
-34
lines changed

20 files changed

+2724
-34
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
//go:build windows && lcow
2+
3+
package plan9
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"strconv"
9+
"sync"
10+
11+
"github.com/Microsoft/go-winio/pkg/guid"
12+
"github.com/Microsoft/hcsshim/internal/controller/device/plan9/mount"
13+
"github.com/Microsoft/hcsshim/internal/controller/device/plan9/share"
14+
"github.com/Microsoft/hcsshim/internal/log"
15+
"github.com/Microsoft/hcsshim/internal/logfields"
16+
"github.com/sirupsen/logrus"
17+
)
18+
19+
// Controller manages the full Plan9 share lifecycle — name allocation, VM
20+
// attachment, guest mounting, and teardown. All operations are serialized
21+
// by a single mutex.
22+
// It is required that all callers:
23+
//
24+
// 1. Obtain a reservation using Reserve().
25+
//
26+
// 2. Use the reservation in MapToGuest() to mount the share into the guest.
27+
//
28+
// 3. Call UnmapFromGuest() to release the reservation and all resources.
29+
//
30+
// If MapToGuest() fails, the caller must call UnmapFromGuest() to release the
31+
// reservation and all resources.
32+
//
33+
// If UnmapFromGuest() fails, the caller must call UnmapFromGuest() again until
34+
// it succeeds to release the reservation and all resources.
35+
type Controller struct {
36+
// mu serializes all public operations on the Controller.
37+
mu sync.Mutex
38+
39+
// vmPlan9 is the host-side interface for adding and removing Plan9 shares.
40+
// Immutable after construction.
41+
vmPlan9 vmPlan9
42+
43+
// guest is the guest-side interface for LCOW Plan9 operations.
44+
// Immutable after construction.
45+
guest guestPlan9
46+
47+
// noWritableFileShares disallows adding writable Plan9 shares.
48+
// Immutable after construction.
49+
noWritableFileShares bool
50+
51+
// reservations maps a reservation ID to its share host path.
52+
// Guarded by mu.
53+
reservations map[guid.GUID]*reservation
54+
55+
// sharesByHostPath maps a host path to its share for fast deduplication
56+
// of share additions. Guarded by mu.
57+
sharesByHostPath map[string]*share.Share
58+
59+
// nameCounter is the monotonically increasing index used to generate
60+
// unique share names. Guarded by mu.
61+
nameCounter uint64
62+
}
63+
64+
// New creates a new [Controller] for managing the plan9 shares on a VM.
65+
func New(vm vmPlan9, guest guestPlan9, noWritableFileShares bool) *Controller {
66+
return &Controller{
67+
vmPlan9: vm,
68+
guest: guest,
69+
noWritableFileShares: noWritableFileShares,
70+
reservations: make(map[guid.GUID]*reservation),
71+
sharesByHostPath: make(map[string]*share.Share),
72+
}
73+
}
74+
75+
// Reserve reserves a reference-counted mapping entry for a Plan9 share based on
76+
// the share host path.
77+
//
78+
// If an error is returned from this function, it is guaranteed that no
79+
// reservation mapping was made and no UnmapFromGuest() call is necessary to
80+
// clean up.
81+
func (c *Controller) Reserve(ctx context.Context, shareConfig share.Config, mountConfig mount.Config) (guid.GUID, error) {
82+
c.mu.Lock()
83+
defer c.mu.Unlock()
84+
85+
// Validate write-share policy before touching shared state.
86+
if !shareConfig.ReadOnly && c.noWritableFileShares {
87+
return guid.GUID{}, fmt.Errorf("adding writable Plan9 shares is denied")
88+
}
89+
90+
ctx, _ = log.WithContext(ctx, logrus.WithField(logfields.HostPath, shareConfig.HostPath))
91+
log.G(ctx).Debug("reserving Plan9 share")
92+
93+
// Generate a unique reservation ID.
94+
id, err := guid.NewV4()
95+
if err != nil {
96+
return guid.GUID{}, fmt.Errorf("generate reservation ID: %w", err)
97+
}
98+
99+
// Check if the generated reservation ID already exists, which is extremely unlikely,
100+
// but we want to be certain before proceeding with share creation.
101+
if _, ok := c.reservations[id]; ok {
102+
return guid.GUID{}, fmt.Errorf("reservation ID already exists: %s", id)
103+
}
104+
105+
// Create the reservation entry.
106+
res := &reservation{
107+
hostPath: shareConfig.HostPath,
108+
}
109+
110+
// Check whether this host path already has an allocated share.
111+
existingShare, ok := c.sharesByHostPath[shareConfig.HostPath]
112+
113+
// We have an existing share for this host path — reserve a mount on it for this caller.
114+
if ok {
115+
// Verify the caller is requesting the same share configuration.
116+
if !existingShare.Config().Equals(shareConfig) {
117+
return guid.GUID{}, fmt.Errorf("cannot reserve ref on share with different config")
118+
}
119+
120+
// Set the share name.
121+
res.name = existingShare.Name()
122+
123+
// We have a share, now reserve a mount on it.
124+
if _, err = existingShare.ReserveMount(ctx, mountConfig); err != nil {
125+
return guid.GUID{}, fmt.Errorf("reserve mount on share %s: %w", existingShare.Name(), err)
126+
}
127+
}
128+
129+
// If we don't have an existing share, we need to create one and reserve a mount on it.
130+
if !ok {
131+
// No existing share for this path — allocate a new one.
132+
name := strconv.FormatUint(c.nameCounter, 10)
133+
c.nameCounter++
134+
135+
// Create the Share and Mount in the reserved states.
136+
newShare := share.NewReserved(name, shareConfig)
137+
if _, err = newShare.ReserveMount(ctx, mountConfig); err != nil {
138+
return guid.GUID{}, fmt.Errorf("reserve mount on share %s: %w", name, err)
139+
}
140+
141+
c.sharesByHostPath[shareConfig.HostPath] = newShare
142+
res.name = newShare.Name()
143+
}
144+
145+
// Ensure our reservation is saved for all future operations.
146+
c.reservations[id] = res
147+
log.G(ctx).WithField("reservation", id).Debug("Plan9 share reserved")
148+
149+
// Return the reserved guest path in addition to the reservation ID for caller convenience.
150+
return id, nil
151+
}
152+
153+
// MapToGuest adds the reserved share to the VM and mounts it inside the guest,
154+
// returning the guest path. It is idempotent for a reservation that is already
155+
// fully mapped.
156+
func (c *Controller) MapToGuest(ctx context.Context, id guid.GUID) (string, error) {
157+
c.mu.Lock()
158+
defer c.mu.Unlock()
159+
160+
// Check if the reservation exists.
161+
res, ok := c.reservations[id]
162+
if !ok {
163+
return "", fmt.Errorf("reservation %s not found", id)
164+
}
165+
166+
// Validate if the host path has an associated share.
167+
// This should be reserved by the Reserve() call.
168+
existingShare, ok := c.sharesByHostPath[res.hostPath]
169+
if !ok {
170+
return "", fmt.Errorf("share for host path %s not found", res.hostPath)
171+
}
172+
173+
log.G(ctx).WithField(logfields.HostPath, existingShare.HostPath()).Debug("mapping Plan9 share to guest")
174+
175+
// Add the share to the VM (idempotent if already added).
176+
if err := existingShare.AddToVM(ctx, c.vmPlan9); err != nil {
177+
return "", fmt.Errorf("add share to VM: %w", err)
178+
}
179+
180+
// Mount the share inside the guest.
181+
guestPath, err := existingShare.MountToGuest(ctx, c.guest)
182+
if err != nil {
183+
return "", fmt.Errorf("mount share to guest: %w", err)
184+
}
185+
186+
log.G(ctx).WithField(logfields.UVMPath, guestPath).Debug("Plan9 share mapped to guest")
187+
return guestPath, nil
188+
}
189+
190+
// UnmapFromGuest unmounts the share from the guest and, when all reservations
191+
// for the share are released, removes the share from the VM. A failed call is
192+
// retryable with the same reservation ID.
193+
func (c *Controller) UnmapFromGuest(ctx context.Context, id guid.GUID) error {
194+
c.mu.Lock()
195+
defer c.mu.Unlock()
196+
197+
ctx, _ = log.WithContext(ctx, logrus.WithField("res", id.String()))
198+
199+
// Validate that the reservation exists before proceeding with teardown.
200+
res, ok := c.reservations[id]
201+
if !ok {
202+
return fmt.Errorf("reservation %s not found", id)
203+
}
204+
205+
// Validate that the share exists before proceeding with teardown.
206+
// This should be reserved by the Reserve() call.
207+
existingShare, ok := c.sharesByHostPath[res.hostPath]
208+
if !ok {
209+
return fmt.Errorf("share for host path %s not found", res.hostPath)
210+
}
211+
212+
log.G(ctx).WithField(logfields.HostPath, existingShare.HostPath()).Debug("unmapping Plan9 share from guest")
213+
214+
// Unmount the share from the guest (ref-counted; only issues the guest
215+
// call when this is the last res on the share).
216+
if err := existingShare.UnmountFromGuest(ctx, c.guest); err != nil {
217+
return fmt.Errorf("unmount share from guest: %w", err)
218+
}
219+
220+
// Remove the share from the VM when no mounts remain active.
221+
if err := existingShare.RemoveFromVM(ctx, c.vmPlan9); err != nil {
222+
return fmt.Errorf("remove share from VM: %w", err)
223+
}
224+
225+
// If the share is now fully removed, free its entry for reuse.
226+
// If it's used in other reservations, it will remain until the last one is released.
227+
if existingShare.State() == share.StateRemoved {
228+
delete(c.sharesByHostPath, existingShare.HostPath())
229+
log.G(ctx).Debug("Plan9 share freed")
230+
}
231+
232+
// Remove the res last so it remains available for retries if
233+
// any earlier step above fails.
234+
delete(c.reservations, id)
235+
log.G(ctx).Debug("Plan9 share unmapped from guest")
236+
return nil
237+
}

0 commit comments

Comments
 (0)