324 lines
11 KiB
Go
324 lines
11 KiB
Go
/*
|
|
Copyright 2017 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package smb
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/container-storage-interface/spec/lib/go/csi"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"k8s.io/klog/v2"
|
|
)
|
|
|
|
// smbVolume is an internal representation of a volume
|
|
// created by the provisioner.
|
|
type smbVolume struct {
|
|
// Volume id
|
|
id string
|
|
// Address of the SMB server.
|
|
sourceField string
|
|
// Subdirectory of the SMB server to create volumes under
|
|
subDir string
|
|
// size of volume
|
|
size int64
|
|
}
|
|
|
|
// Ordering of elements in the CSI volume id.
|
|
// ID is of the form {server}/{subDir}.
|
|
const (
|
|
idsourceField = iota
|
|
idSubDir
|
|
totalIDElements // Always last
|
|
)
|
|
|
|
// CreateVolume only supports static provisioning, no create volume action
|
|
func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
|
|
name := req.GetName()
|
|
if len(name) == 0 {
|
|
return nil, status.Error(codes.InvalidArgument, "CreateVolume name must be provided")
|
|
}
|
|
|
|
var volCap *csi.VolumeCapability
|
|
volumeCapabilities := req.GetVolumeCapabilities()
|
|
if len(volumeCapabilities) == 0 {
|
|
return nil, status.Error(codes.InvalidArgument, "CreateVolume Volume capabilities must be provided")
|
|
}
|
|
if len(volumeCapabilities) > 0 {
|
|
volCap = req.GetVolumeCapabilities()[0]
|
|
}
|
|
|
|
reqCapacity := req.GetCapacityRange().GetRequiredBytes()
|
|
smbVol, err := d.newSMBVolume(name, reqCapacity, req.GetParameters())
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
|
|
// check if create SubDir is enable in storage class parameters
|
|
parameters := req.GetParameters()
|
|
var createSubDir string
|
|
for k, v := range parameters {
|
|
switch strings.ToLower(k) {
|
|
case createSubDirField:
|
|
createSubDir = v
|
|
}
|
|
}
|
|
|
|
secrets := req.GetSecrets()
|
|
if strings.EqualFold(createSubDir, "true") {
|
|
if len(secrets) > 0 {
|
|
// Mount smb base share so we can create a subdirectory
|
|
if err := d.internalMount(ctx, smbVol, volCap, secrets); err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to mount smb server: %v", err.Error())
|
|
}
|
|
defer func() {
|
|
if err = d.internalUnmount(ctx, smbVol); err != nil {
|
|
klog.Warningf("failed to unmount smb server: %v", err.Error())
|
|
}
|
|
}()
|
|
// Create subdirectory under base-dir
|
|
// TODO: revisit permissions
|
|
internalVolumePath := d.getInternalVolumePath(smbVol)
|
|
if err = os.Mkdir(internalVolumePath, 0777); err != nil && !os.IsExist(err) {
|
|
return nil, status.Errorf(codes.Internal, "failed to make subdirectory: %v", err.Error())
|
|
}
|
|
parameters[sourceField] = parameters[sourceField] + "/" + smbVol.subDir
|
|
} else {
|
|
klog.Warningf("CreateVolume: Volume secrets should be provided when createSubDir is true")
|
|
}
|
|
}
|
|
return &csi.CreateVolumeResponse{Volume: d.smbVolToCSI(smbVol, parameters)}, nil
|
|
}
|
|
|
|
// DeleteVolume only supports static provisioning, no delete volume action
|
|
func (d *Driver) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
|
|
volumeID := req.GetVolumeId()
|
|
if volumeID == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "volume id is empty")
|
|
}
|
|
smbVol, err := d.getSmbVolFromID(volumeID)
|
|
if err != nil {
|
|
// An invalid ID should be treated as doesn't exist
|
|
klog.Warningf("failed to get smb volume for volume id %v deletion: %v", volumeID, err)
|
|
return &csi.DeleteVolumeResponse{}, nil
|
|
}
|
|
|
|
secrets := req.GetSecrets()
|
|
if len(secrets) > 0 {
|
|
// Mount smb base share so we can delete the subdirectory
|
|
if err = d.internalMount(ctx, smbVol, nil, secrets); err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to mount smb server: %v", err.Error())
|
|
}
|
|
defer func() {
|
|
if err = d.internalUnmount(ctx, smbVol); err != nil {
|
|
klog.Warningf("failed to unmount smb server: %v", err.Error())
|
|
}
|
|
}()
|
|
|
|
// Delete subdirectory under base-dir
|
|
internalVolumePath := d.getInternalVolumePath(smbVol)
|
|
klog.V(2).Infof("Removing subdirectory at %v", internalVolumePath)
|
|
if err = os.RemoveAll(internalVolumePath); err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to delete subdirectory: %v", err.Error())
|
|
}
|
|
} else {
|
|
klog.Warningf("DeleteVolume: Volume secrets should be provided")
|
|
}
|
|
|
|
return &csi.DeleteVolumeResponse{}, nil
|
|
}
|
|
|
|
// ControllerGetVolume get volume
|
|
func (d *Driver) ControllerGetVolume(context.Context, *csi.ControllerGetVolumeRequest) (*csi.ControllerGetVolumeResponse, error) {
|
|
return nil, status.Error(codes.Unimplemented, "")
|
|
}
|
|
|
|
func (d *Driver) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) {
|
|
return nil, status.Error(codes.Unimplemented, "")
|
|
}
|
|
|
|
func (d *Driver) ControllerUnpublishVolume(ctx context.Context, req *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) {
|
|
return nil, status.Error(codes.Unimplemented, "")
|
|
}
|
|
|
|
// ControllerGetCapabilities returns the capabilities of the Controller plugin
|
|
func (d *Driver) ControllerGetCapabilities(ctx context.Context, req *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) {
|
|
return &csi.ControllerGetCapabilitiesResponse{
|
|
Capabilities: d.Cap,
|
|
}, nil
|
|
}
|
|
|
|
func (d *Driver) ValidateVolumeCapabilities(ctx context.Context, req *csi.ValidateVolumeCapabilitiesRequest) (*csi.ValidateVolumeCapabilitiesResponse, error) {
|
|
if len(req.GetVolumeId()) == 0 {
|
|
return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request")
|
|
}
|
|
if req.GetVolumeCapabilities() == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "Volume capabilities missing in request")
|
|
}
|
|
|
|
// supports all AccessModes, no need to check capabilities here
|
|
return &csi.ValidateVolumeCapabilitiesResponse{Message: ""}, nil
|
|
}
|
|
|
|
// GetCapacity returns the capacity of the total available storage pool
|
|
func (d *Driver) GetCapacity(ctx context.Context, req *csi.GetCapacityRequest) (*csi.GetCapacityResponse, error) {
|
|
return nil, status.Error(codes.Unimplemented, "")
|
|
}
|
|
|
|
// ListVolumes return all available volumes
|
|
func (d *Driver) ListVolumes(ctx context.Context, req *csi.ListVolumesRequest) (*csi.ListVolumesResponse, error) {
|
|
return nil, status.Error(codes.Unimplemented, "")
|
|
}
|
|
|
|
// ControllerExpandVolume expand volume
|
|
func (d *Driver) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) {
|
|
return nil, status.Error(codes.Unimplemented, "")
|
|
}
|
|
|
|
func (d *Driver) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) {
|
|
return nil, status.Error(codes.Unimplemented, "")
|
|
}
|
|
|
|
func (d *Driver) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) {
|
|
return nil, status.Error(codes.Unimplemented, "")
|
|
}
|
|
|
|
func (d *Driver) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) {
|
|
return nil, status.Error(codes.Unimplemented, "")
|
|
}
|
|
|
|
// Given a smbVolume, return a CSI volume id
|
|
func (d *Driver) getVolumeIDFromSmbVol(vol *smbVolume) string {
|
|
idElements := make([]string, totalIDElements)
|
|
idElements[idsourceField] = strings.Trim(vol.sourceField, "/")
|
|
idElements[idSubDir] = strings.Trim(vol.subDir, "/")
|
|
return strings.Join(idElements, "/")
|
|
}
|
|
|
|
// Get working directory for CreateVolume and DeleteVolume
|
|
func (d *Driver) getInternalMountPath(vol *smbVolume) string {
|
|
// use default if empty
|
|
if d.workingMountDir == "" {
|
|
d.workingMountDir = "/tmp"
|
|
}
|
|
return filepath.Join(d.workingMountDir, vol.subDir)
|
|
}
|
|
|
|
// Mount smb server at base-dir
|
|
func (d *Driver) internalMount(ctx context.Context, vol *smbVolume, volCap *csi.VolumeCapability, secrets map[string]string) error {
|
|
stagingPath := d.getInternalMountPath(vol)
|
|
|
|
if volCap == nil {
|
|
volCap = &csi.VolumeCapability{
|
|
AccessType: &csi.VolumeCapability_Mount{
|
|
Mount: &csi.VolumeCapability_MountVolume{},
|
|
},
|
|
}
|
|
}
|
|
|
|
klog.V(4).Infof("internally mounting %v at %v", sourceField, stagingPath)
|
|
_, err := d.NodeStageVolume(ctx, &csi.NodeStageVolumeRequest{
|
|
StagingTargetPath: stagingPath,
|
|
VolumeContext: map[string]string{
|
|
sourceField: vol.sourceField,
|
|
},
|
|
VolumeCapability: volCap,
|
|
VolumeId: vol.id,
|
|
Secrets: secrets,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// Unmount smb server at base-dir
|
|
func (d *Driver) internalUnmount(ctx context.Context, vol *smbVolume) error {
|
|
targetPath := d.getInternalMountPath(vol)
|
|
|
|
// Unmount smb server at base-dir
|
|
klog.V(4).Infof("internally unmounting %v", targetPath)
|
|
_, err := d.NodeUnstageVolume(ctx, &csi.NodeUnstageVolumeRequest{
|
|
VolumeId: vol.id,
|
|
StagingTargetPath: d.getInternalMountPath(vol),
|
|
})
|
|
return err
|
|
}
|
|
|
|
// Convert VolumeCreate parameters to an smbVolume
|
|
func (d *Driver) newSMBVolume(name string, size int64, params map[string]string) (*smbVolume, error) {
|
|
var sourceField string
|
|
|
|
// Validate parameters (case-insensitive).
|
|
for k, v := range params {
|
|
switch strings.ToLower(k) {
|
|
case paramSource:
|
|
sourceField = v
|
|
}
|
|
}
|
|
|
|
// Validate required parameters
|
|
if sourceField == "" {
|
|
return nil, fmt.Errorf("%v is a required parameter", paramSource)
|
|
}
|
|
|
|
vol := &smbVolume{
|
|
sourceField: sourceField,
|
|
subDir: name,
|
|
size: size,
|
|
}
|
|
vol.id = d.getVolumeIDFromSmbVol(vol)
|
|
|
|
return vol, nil
|
|
}
|
|
|
|
// Get internal path where the volume is created
|
|
// The reason why the internal path is "workingDir/subDir/subDir" is because:
|
|
// * the semantic is actually "workingDir/volId/subDir" and volId == subDir.
|
|
// * we need a mount directory per volId because you can have multiple
|
|
// CreateVolume calls in parallel and they may use the same underlying share.
|
|
// Instead of refcounting how many CreateVolume calls are using the same
|
|
// share, it's simpler to just do a mount per request.
|
|
func (d *Driver) getInternalVolumePath(vol *smbVolume) string {
|
|
return filepath.Join(d.getInternalMountPath(vol), vol.subDir)
|
|
}
|
|
|
|
// Convert into smbVolume into a csi.Volume
|
|
func (d *Driver) smbVolToCSI(vol *smbVolume, parameters map[string]string) *csi.Volume {
|
|
return &csi.Volume{
|
|
CapacityBytes: 0, // by setting it to zero, Provisioner will use PVC requested size as PV size
|
|
VolumeId: vol.id,
|
|
VolumeContext: parameters,
|
|
}
|
|
}
|
|
|
|
// Given a CSI volume id, return a smbVolume
|
|
func (d *Driver) getSmbVolFromID(id string) (*smbVolume, error) {
|
|
volRegex := regexp.MustCompile("^([^/]+)/([^/]+)$")
|
|
tokens := volRegex.FindStringSubmatch(id)
|
|
if tokens == nil {
|
|
return nil, fmt.Errorf("Could not split %q into server, baseDir and subDir", id)
|
|
}
|
|
return &smbVolume{
|
|
id: id,
|
|
sourceField: tokens[1],
|
|
subDir: tokens[2],
|
|
}, nil
|
|
}
|