Skip to content

Commit f6bd58a

Browse files
controllerserver: implement CreateVolume
Signed-off-by: Mayank Shah <[email protected]>
1 parent 7fdf127 commit f6bd58a

File tree

4 files changed

+433
-5
lines changed

4 files changed

+433
-5
lines changed

pkg/nfs/controllerserver.go

Lines changed: 265 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package nfs
22

33
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
49
"github.com/container-storage-interface/spec/lib/go/csi"
510
"github.com/golang/glog"
611
"golang.org/x/net/context"
@@ -10,10 +15,95 @@ import (
1015

1116
type ControllerServer struct {
1217
Driver *nfsDriver
18+
// Working directory for the provisioner to temporarily mount nfs shares at
19+
workingMountDir string
20+
}
21+
22+
// nfsVolume is an internal representation of a volume
23+
// created by the provisioner.
24+
type nfsVolume struct {
25+
// Volume id
26+
id string
27+
// Address of the NFS server.
28+
// Matches paramServer.
29+
server string
30+
// Base directory of the NFS server to create volumes under
31+
// Matches paramBaseDir.
32+
baseDir string
33+
// Subdirectory of the NFS server to create volumes under
34+
subDir string
1335
}
1436

37+
// CSI CreateVolume parameters
38+
const (
39+
// Address of the NFS server
40+
paramServer = "server"
41+
// Base directory of the NFS server to create volumes under.
42+
// The base directory must be a direct child of the root directory.
43+
// The root directory is ommitted from the string, for example:
44+
// "base" instead of "/base"
45+
paramBaseDir = "base-dir"
46+
)
47+
48+
// CSI Volume attributes
49+
const (
50+
// Address of the NFS server
51+
attrServer = "server"
52+
// Path to the NFS share on the server
53+
attrShare = "share"
54+
)
55+
56+
// Ordering of elements in the CSI volume id.
57+
// ID is of the form {server}/{baseDir}/{subDir}.
58+
// TODO: This volume id format limits baseDir and
59+
// subDir to only be one directory deep.
60+
// Adding a new element should always go at the end
61+
// before totalIDElements
62+
const (
63+
idServer = iota
64+
idBaseDir
65+
idSubDir
66+
totalIDElements // Always last
67+
)
68+
1569
func (cs *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
16-
return nil, status.Error(codes.Unimplemented, "")
70+
glog.V(4).Infof("CreateVolume called with request %+v", req)
71+
72+
// Validate arguments
73+
name := req.GetName()
74+
if len(name) == 0 {
75+
return nil, status.Error(codes.InvalidArgument, "CreateVolume name must be provided")
76+
}
77+
78+
if err := cs.validateVolumeCapabilities(req.GetVolumeCapabilities()); err != nil {
79+
return nil, status.Error(codes.InvalidArgument, err.Error())
80+
}
81+
82+
nfsVol, err := cs.newNFSVolume(name, req.GetParameters())
83+
if err != nil {
84+
return nil, status.Error(codes.InvalidArgument, err.Error())
85+
}
86+
87+
// Mount nfs base share so we can create a subdirectory
88+
if err = cs.internalMount(ctx, nfsVol); err != nil {
89+
return nil, status.Errorf(codes.Internal, "failed to mount nfs server: %v", err.Error())
90+
}
91+
defer func() {
92+
if err = cs.internalUnmount(ctx, nfsVol); err != nil {
93+
glog.Warningf("failed to unmount nfs server: %v", err.Error())
94+
}
95+
}()
96+
97+
// Create subdirectory under base-dir
98+
// TODO: revisit permissions
99+
internalVolumePath := cs.getInternalVolumePath(nfsVol)
100+
glog.V(4).Infof("Creating subdirectory at %v", internalVolumePath)
101+
if err = os.Mkdir(internalVolumePath, 0755); err != nil {
102+
return nil, status.Errorf(codes.Internal, "failed to make subdirectory: %v", err.Error())
103+
}
104+
// Remove capacity setting when provisioner 1.4.0 is available with fix for
105+
// https://github.com/kubernetes-csi/external-provisioner/pull/271
106+
return &csi.CreateVolumeResponse{Volume: cs.nfsVolToCSI(nfsVol, req.GetCapacityRange().GetRequiredBytes())}, nil
17107
}
18108

19109
func (cs *ControllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
@@ -73,3 +163,177 @@ func (cs *ControllerServer) ListSnapshots(ctx context.Context, req *csi.ListSnap
73163
func (cs *ControllerServer) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) {
74164
return nil, status.Error(codes.Unimplemented, "")
75165
}
166+
167+
func (cs *ControllerServer) validateVolumeCapabilities(caps []*csi.VolumeCapability) error {
168+
if len(caps) == 0 {
169+
return fmt.Errorf("volume capabilities must be provided")
170+
}
171+
172+
for _, c := range caps {
173+
if err := cs.validateVolumeCapability(c); err != nil {
174+
return err
175+
}
176+
}
177+
return nil
178+
}
179+
180+
func (cs *ControllerServer) validateVolumeCapability(c *csi.VolumeCapability) error {
181+
if c == nil {
182+
return fmt.Errorf("volume capability must be provided")
183+
}
184+
185+
// Validate access mode
186+
accessMode := c.GetAccessMode()
187+
if accessMode == nil {
188+
return fmt.Errorf("volume capability access mode not set")
189+
}
190+
if !cs.Driver.cap[accessMode.Mode] {
191+
return fmt.Errorf("driver does not support access mode: %v", accessMode.Mode.String())
192+
}
193+
194+
// Validate access type
195+
accessType := c.GetAccessType()
196+
if accessType == nil {
197+
return fmt.Errorf("volume capability access type not set")
198+
}
199+
mountType := c.GetMount()
200+
if mountType == nil {
201+
return fmt.Errorf("driver only supports mount access type volume capability")
202+
}
203+
204+
if mountType.FsType != "" {
205+
return fmt.Errorf("driver does not support fstype %v", mountType.FsType)
206+
}
207+
// TODO: check if we want to whitelist/blacklist certain mount options
208+
return nil
209+
}
210+
211+
// Mount nfs server at base-dir
212+
func (cs *ControllerServer) internalMount(ctx context.Context, vol *nfsVolume) error {
213+
sharePath := filepath.Join("/" + vol.baseDir)
214+
targetPath := cs.getInternalMountPath(vol)
215+
stdVolCap := csi.VolumeCapability{
216+
AccessType: &csi.VolumeCapability_Mount{
217+
Mount: &csi.VolumeCapability_MountVolume{},
218+
},
219+
}
220+
221+
glog.V(4).Infof("internally mounting %v:%v at %v", vol.server, sharePath, targetPath)
222+
_, err := cs.Driver.ns.NodePublishVolume(ctx, &csi.NodePublishVolumeRequest{
223+
TargetPath: targetPath,
224+
VolumeContext: map[string]string{
225+
"server": vol.server,
226+
"share": sharePath,
227+
},
228+
VolumeCapability: &stdVolCap,
229+
VolumeId: vol.id,
230+
})
231+
return err
232+
}
233+
234+
// Unmount nfs server at base-dir
235+
func (cs *ControllerServer) internalUnmount(ctx context.Context, vol *nfsVolume) error {
236+
targetPath := cs.getInternalMountPath(vol)
237+
238+
// Unmount nfs server at base-dir
239+
glog.V(4).Infof("internally unmounting %v", targetPath)
240+
_, err := cs.Driver.ns.NodeUnpublishVolume(ctx, &csi.NodeUnpublishVolumeRequest{
241+
TargetPath: cs.getInternalMountPath(vol),
242+
})
243+
return err
244+
}
245+
246+
// Convert VolumeCreate parameters to an nfsVolume
247+
func (cs *ControllerServer) newNFSVolume(name string, params map[string]string) (*nfsVolume, error) {
248+
var (
249+
server string
250+
baseDir string
251+
)
252+
253+
// Validate parameters (case-insensitive).
254+
// TODO do more strict validation.
255+
for k, v := range params {
256+
switch strings.ToLower(k) {
257+
case paramServer:
258+
server = v
259+
case paramBaseDir:
260+
baseDir = v
261+
default:
262+
return nil, fmt.Errorf("invalid parameter %q", k)
263+
}
264+
}
265+
266+
// Validate required parameters
267+
if server == "" {
268+
return nil, fmt.Errorf("%v is a required parameter", paramServer)
269+
}
270+
if baseDir == "" {
271+
return nil, fmt.Errorf("%v is a required parameter", paramBaseDir)
272+
}
273+
274+
vol := &nfsVolume{
275+
server: server,
276+
baseDir: baseDir,
277+
subDir: name,
278+
}
279+
vol.id = cs.getVolumeIdFromNfsVol(vol)
280+
281+
return vol, nil
282+
}
283+
284+
// Get working directory for CreateVolume and DeleteVolume
285+
func (cs *ControllerServer) getInternalMountPath(vol *nfsVolume) string {
286+
return filepath.Join(cs.workingMountDir, vol.subDir)
287+
}
288+
289+
// Get internal path where the volume is created
290+
// The reason why the internal path is "workingDir/subDir/subDir" is because:
291+
// * the semantic is actually "workingDir/volId/subDir" and volId == subDir.
292+
// * we need a mount directory per volId because you can have multiple
293+
// CreateVolume calls in parallel and they may use the same underlying share.
294+
// Instead of refcounting how many CreateVolume calls are using the same
295+
// share, it's simpler to just do a mount per request.
296+
func (cs *ControllerServer) getInternalVolumePath(vol *nfsVolume) string {
297+
return filepath.Join(cs.getInternalMountPath(vol), vol.subDir)
298+
}
299+
300+
// Get user-visible share path for the volume
301+
func (cs *ControllerServer) getVolumeSharePath(vol *nfsVolume) string {
302+
return filepath.Join("/", vol.baseDir, vol.subDir)
303+
}
304+
305+
// Convert into nfsVolume into a csi.Volume
306+
func (cs *ControllerServer) nfsVolToCSI(vol *nfsVolume, reqBytes int64) *csi.Volume {
307+
return &csi.Volume{
308+
CapacityBytes: reqBytes,
309+
VolumeId: vol.id,
310+
VolumeContext: map[string]string{
311+
attrServer: vol.server,
312+
attrShare: cs.getVolumeSharePath(vol),
313+
},
314+
}
315+
}
316+
317+
// Given a CSI volume id, return a nfsVolume
318+
func (cs *ControllerServer) getNfsVolFromId(id string) (*nfsVolume, error) {
319+
tokens := strings.Split(id, "/")
320+
if len(tokens) != totalIDElements {
321+
return nil, fmt.Errorf("volume id %q unexpected format: got %v tokens", id, len(tokens))
322+
}
323+
324+
return &nfsVolume{
325+
id: id,
326+
server: tokens[idServer],
327+
baseDir: tokens[idBaseDir],
328+
subDir: tokens[idSubDir],
329+
}, nil
330+
}
331+
332+
// Given a nfsVolume, return a CSI volume id
333+
func (cs *ControllerServer) getVolumeIdFromNfsVol(vol *nfsVolume) string {
334+
idElements := make([]string, totalIDElements)
335+
idElements[idServer] = vol.server
336+
idElements[idBaseDir] = vol.baseDir
337+
idElements[idSubDir] = vol.subDir
338+
return strings.Join(idElements, "/")
339+
}

0 commit comments

Comments
 (0)