mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 01:06:27 +00:00
Merge pull request #122090 from carlory/remove-intree-vsphere
remove the deprecated in-tree vsphere volume's code
This commit is contained in:
commit
d39f401767
@ -1,19 +0,0 @@
|
|||||||
# See the OWNERS docs at https://go.k8s.io/owners
|
|
||||||
|
|
||||||
approvers:
|
|
||||||
- saad-ali
|
|
||||||
- thockin
|
|
||||||
- divyenpatel
|
|
||||||
emeritus_approvers:
|
|
||||||
- matchstick
|
|
||||||
- abrarshivani
|
|
||||||
- BaluDontu
|
|
||||||
- SandeepPissay
|
|
||||||
- pmorie
|
|
||||||
reviewers:
|
|
||||||
- saad-ali
|
|
||||||
- justinsb
|
|
||||||
- jsafrane
|
|
||||||
- jingxu97
|
|
||||||
- msau42
|
|
||||||
- divyenpatel
|
|
@ -1,327 +0,0 @@
|
|||||||
//go:build !providerless
|
|
||||||
// +build !providerless
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2016 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 vsphere_volume
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"k8s.io/klog/v2"
|
|
||||||
"k8s.io/mount-utils"
|
|
||||||
"k8s.io/utils/keymutex"
|
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
"k8s.io/kubernetes/pkg/volume"
|
|
||||||
volumeutil "k8s.io/kubernetes/pkg/volume/util"
|
|
||||||
"k8s.io/legacy-cloud-providers/vsphere"
|
|
||||||
)
|
|
||||||
|
|
||||||
type vsphereVMDKAttacher struct {
|
|
||||||
host volume.VolumeHost
|
|
||||||
vsphereVolumes vsphere.Volumes
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ volume.Attacher = &vsphereVMDKAttacher{}
|
|
||||||
|
|
||||||
var _ volume.DeviceMounter = &vsphereVMDKAttacher{}
|
|
||||||
|
|
||||||
var _ volume.AttachableVolumePlugin = &vsphereVolumePlugin{}
|
|
||||||
|
|
||||||
var _ volume.DeviceMountableVolumePlugin = &vsphereVolumePlugin{}
|
|
||||||
|
|
||||||
// Singleton key mutex for keeping attach operations for the same host atomic
|
|
||||||
var attachdetachMutex = keymutex.NewHashed(0)
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) NewAttacher() (volume.Attacher, error) {
|
|
||||||
vsphereCloud, err := getCloudProvider(plugin.host.GetCloudProvider())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &vsphereVMDKAttacher{
|
|
||||||
host: plugin.host,
|
|
||||||
vsphereVolumes: vsphereCloud,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) NewDeviceMounter() (volume.DeviceMounter, error) {
|
|
||||||
return plugin.NewAttacher()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attaches the volume specified by the given spec to the given host.
|
|
||||||
// On success, returns the device path where the device was attached on the
|
|
||||||
// node.
|
|
||||||
// Callers are responsible for retryinging on failure.
|
|
||||||
// Callers are responsible for thread safety between concurrent attach and
|
|
||||||
// detach operations.
|
|
||||||
func (attacher *vsphereVMDKAttacher) Attach(spec *volume.Spec, nodeName types.NodeName) (string, error) {
|
|
||||||
volumeSource, _, err := getVolumeSource(spec)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
klog.V(4).Infof("vSphere: Attach disk called for node %s", nodeName)
|
|
||||||
|
|
||||||
// Keeps concurrent attach operations to same host atomic
|
|
||||||
attachdetachMutex.LockKey(string(nodeName))
|
|
||||||
defer attachdetachMutex.UnlockKey(string(nodeName))
|
|
||||||
|
|
||||||
// vsphereCloud.AttachDisk checks if disk is already attached to host and
|
|
||||||
// succeeds in that case, so no need to do that separately.
|
|
||||||
diskUUID, err := attacher.vsphereVolumes.AttachDisk(volumeSource.VolumePath, volumeSource.StoragePolicyName, nodeName)
|
|
||||||
if err != nil {
|
|
||||||
klog.Errorf("Error attaching volume %q to node %q: %+v", volumeSource.VolumePath, nodeName, err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Join(diskByIDPath, diskSCSIPrefix+diskUUID), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (attacher *vsphereVMDKAttacher) VolumesAreAttached(specs []*volume.Spec, nodeName types.NodeName) (map[*volume.Spec]bool, error) {
|
|
||||||
klog.Warningf("Attacher.VolumesAreAttached called for node %q - Please use BulkVerifyVolumes for vSphere", nodeName)
|
|
||||||
volumeNodeMap := map[types.NodeName][]*volume.Spec{
|
|
||||||
nodeName: specs,
|
|
||||||
}
|
|
||||||
nodeVolumesResult := make(map[*volume.Spec]bool)
|
|
||||||
nodesVerificationMap, err := attacher.BulkVerifyVolumes(volumeNodeMap)
|
|
||||||
if err != nil {
|
|
||||||
klog.Errorf("Attacher.VolumesAreAttached - error checking volumes for node %q with %v", nodeName, err)
|
|
||||||
return nodeVolumesResult, err
|
|
||||||
}
|
|
||||||
if result, ok := nodesVerificationMap[nodeName]; ok {
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
return nodeVolumesResult, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (attacher *vsphereVMDKAttacher) BulkVerifyVolumes(volumesByNode map[types.NodeName][]*volume.Spec) (map[types.NodeName]map[*volume.Spec]bool, error) {
|
|
||||||
volumesAttachedCheck := make(map[types.NodeName]map[*volume.Spec]bool)
|
|
||||||
volumePathsByNode := make(map[types.NodeName][]string)
|
|
||||||
volumeSpecMap := make(map[string]*volume.Spec)
|
|
||||||
|
|
||||||
for nodeName, volumeSpecs := range volumesByNode {
|
|
||||||
for _, volumeSpec := range volumeSpecs {
|
|
||||||
volumeSource, _, err := getVolumeSource(volumeSpec)
|
|
||||||
if err != nil {
|
|
||||||
klog.Errorf("Error getting volume (%q) source : %v", volumeSpec.Name(), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
volPath := volumeSource.VolumePath
|
|
||||||
volumePathsByNode[nodeName] = append(volumePathsByNode[nodeName], volPath)
|
|
||||||
nodeVolume, nodeVolumeExists := volumesAttachedCheck[nodeName]
|
|
||||||
if !nodeVolumeExists {
|
|
||||||
nodeVolume = make(map[*volume.Spec]bool)
|
|
||||||
}
|
|
||||||
nodeVolume[volumeSpec] = true
|
|
||||||
volumeSpecMap[volPath] = volumeSpec
|
|
||||||
volumesAttachedCheck[nodeName] = nodeVolume
|
|
||||||
}
|
|
||||||
}
|
|
||||||
attachedResult, err := attacher.vsphereVolumes.DisksAreAttached(volumePathsByNode)
|
|
||||||
if err != nil {
|
|
||||||
klog.Errorf("Error checking if volumes are attached to nodes: %+v. err: %v", volumePathsByNode, err)
|
|
||||||
return volumesAttachedCheck, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for nodeName, nodeVolumes := range attachedResult {
|
|
||||||
for volumePath, attached := range nodeVolumes {
|
|
||||||
if !attached {
|
|
||||||
spec := volumeSpecMap[volumePath]
|
|
||||||
setNodeVolume(volumesAttachedCheck, spec, nodeName, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return volumesAttachedCheck, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (attacher *vsphereVMDKAttacher) WaitForAttach(spec *volume.Spec, devicePath string, _ *v1.Pod, timeout time.Duration) (string, error) {
|
|
||||||
volumeSource, _, err := getVolumeSource(spec)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if devicePath == "" {
|
|
||||||
return "", fmt.Errorf("WaitForAttach failed for VMDK %q: devicePath is empty", volumeSource.VolumePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
ticker := time.NewTicker(checkSleepDuration)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
timer := time.NewTimer(timeout)
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
klog.V(5).Infof("Checking VMDK %q is attached", volumeSource.VolumePath)
|
|
||||||
path, err := verifyDevicePath(devicePath)
|
|
||||||
if err != nil {
|
|
||||||
// Log error, if any, and continue checking periodically. See issue #11321
|
|
||||||
klog.Warningf("Error verifying VMDK (%q) is attached: %v", volumeSource.VolumePath, err)
|
|
||||||
} else if path != "" {
|
|
||||||
// A device path has successfully been created for the VMDK
|
|
||||||
klog.Infof("Successfully found attached VMDK %q.", volumeSource.VolumePath)
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
case <-timer.C:
|
|
||||||
return "", fmt.Errorf("could not find attached VMDK %q. Timeout waiting for mount paths to be created", volumeSource.VolumePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDeviceMountPath returns a path where the device should
|
|
||||||
// point which should be bind mounted for individual volumes.
|
|
||||||
func (attacher *vsphereVMDKAttacher) GetDeviceMountPath(spec *volume.Spec) (string, error) {
|
|
||||||
volumeSource, _, err := getVolumeSource(spec)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return makeGlobalPDPath(attacher.host, volumeSource.VolumePath), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMountDeviceRefs finds all other references to the device referenced
|
|
||||||
// by deviceMountPath; returns a list of paths.
|
|
||||||
func (plugin *vsphereVolumePlugin) GetDeviceMountRefs(deviceMountPath string) ([]string, error) {
|
|
||||||
mounter := plugin.host.GetMounter(plugin.GetPluginName())
|
|
||||||
return mounter.GetMountRefs(deviceMountPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MountDevice mounts device to global mount point.
|
|
||||||
func (attacher *vsphereVMDKAttacher) MountDevice(spec *volume.Spec, devicePath string, deviceMountPath string, _ volume.DeviceMounterArgs) error {
|
|
||||||
klog.Infof("vsphere MountDevice mount %s to %s", devicePath, deviceMountPath)
|
|
||||||
mounter := attacher.host.GetMounter(vsphereVolumePluginName)
|
|
||||||
notMnt, err := mounter.IsLikelyNotMountPoint(deviceMountPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
dir := deviceMountPath
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
dir = filepath.Dir(deviceMountPath)
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
|
||||||
klog.Errorf("Failed to create directory at %#v. err: %s", dir, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
notMnt = true
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeSource, _, err := getVolumeSource(spec)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
options := []string{}
|
|
||||||
|
|
||||||
if notMnt {
|
|
||||||
diskMounter := volumeutil.NewSafeFormatAndMountFromHost(vsphereVolumePluginName, attacher.host)
|
|
||||||
mountOptions := volumeutil.MountOptionFromSpec(spec, options...)
|
|
||||||
err = diskMounter.FormatAndMount(devicePath, deviceMountPath, volumeSource.FSType, mountOptions)
|
|
||||||
if err != nil {
|
|
||||||
os.Remove(deviceMountPath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
klog.V(4).Infof("formatting spec %v devicePath %v deviceMountPath %v fs %v with options %+v", spec.Name(), devicePath, deviceMountPath, volumeSource.FSType, options)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type vsphereVMDKDetacher struct {
|
|
||||||
mounter mount.Interface
|
|
||||||
vsphereVolumes vsphere.Volumes
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ volume.Detacher = &vsphereVMDKDetacher{}
|
|
||||||
|
|
||||||
var _ volume.DeviceUnmounter = &vsphereVMDKDetacher{}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) NewDetacher() (volume.Detacher, error) {
|
|
||||||
vsphereCloud, err := getCloudProvider(plugin.host.GetCloudProvider())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &vsphereVMDKDetacher{
|
|
||||||
mounter: plugin.host.GetMounter(plugin.GetPluginName()),
|
|
||||||
vsphereVolumes: vsphereCloud,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) NewDeviceUnmounter() (volume.DeviceUnmounter, error) {
|
|
||||||
return plugin.NewDetacher()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detach the given device from the given node.
|
|
||||||
func (detacher *vsphereVMDKDetacher) Detach(volumeName string, nodeName types.NodeName) error {
|
|
||||||
volPath := getVolPathfromVolumeName(volumeName)
|
|
||||||
attached, newVolumePath, err := detacher.vsphereVolumes.DiskIsAttached(volPath, nodeName)
|
|
||||||
if err != nil {
|
|
||||||
// Log error and continue with detach
|
|
||||||
klog.Errorf(
|
|
||||||
"Error checking if volume (%q) is already attached to current node (%q). Will continue and try detach anyway. err=%v",
|
|
||||||
volPath, nodeName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil && !attached {
|
|
||||||
// Volume is already detached from node.
|
|
||||||
klog.Infof("detach operation was successful. volume %q is already detached from node %q.", volPath, nodeName)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
attachdetachMutex.LockKey(string(nodeName))
|
|
||||||
defer attachdetachMutex.UnlockKey(string(nodeName))
|
|
||||||
if err := detacher.vsphereVolumes.DetachDisk(newVolumePath, nodeName); err != nil {
|
|
||||||
klog.Errorf("Error detaching volume %q: %v", volPath, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (detacher *vsphereVMDKDetacher) UnmountDevice(deviceMountPath string) error {
|
|
||||||
return mount.CleanupMountPoint(deviceMountPath, detacher.mounter, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) CanAttach(spec *volume.Spec) (bool, error) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) CanDeviceMount(spec *volume.Spec) (bool, error) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setNodeVolume(
|
|
||||||
nodeVolumeMap map[types.NodeName]map[*volume.Spec]bool,
|
|
||||||
volumeSpec *volume.Spec,
|
|
||||||
nodeName types.NodeName,
|
|
||||||
check bool) {
|
|
||||||
|
|
||||||
volumeMap := nodeVolumeMap[nodeName]
|
|
||||||
if volumeMap == nil {
|
|
||||||
volumeMap = make(map[*volume.Spec]bool)
|
|
||||||
nodeVolumeMap[nodeName] = volumeMap
|
|
||||||
}
|
|
||||||
volumeMap[volumeSpec] = check
|
|
||||||
}
|
|
@ -1,335 +0,0 @@
|
|||||||
//go:build !providerless
|
|
||||||
// +build !providerless
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2016 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 vsphere_volume
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
"k8s.io/kubernetes/pkg/volume"
|
|
||||||
volumetest "k8s.io/kubernetes/pkg/volume/testing"
|
|
||||||
"k8s.io/legacy-cloud-providers/vsphere/vclib"
|
|
||||||
|
|
||||||
"k8s.io/klog/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// diskNameErr is the error when disk name is wrong.
|
|
||||||
diskNameErr = errors.New("wrong diskName")
|
|
||||||
// nodeNameErr is the error when node name is wrong.
|
|
||||||
nodeNameErr = errors.New("wrong nodeName")
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetDeviceName_Volume(t *testing.T) {
|
|
||||||
plugin := newPlugin(t)
|
|
||||||
volPath := "[local] volumes/test"
|
|
||||||
spec := createVolSpec(volPath)
|
|
||||||
|
|
||||||
deviceName, err := plugin.GetVolumeName(spec)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("GetDeviceName error: %v", err)
|
|
||||||
}
|
|
||||||
if deviceName != volPath {
|
|
||||||
t.Errorf("GetDeviceName error: expected %s, got %s", volPath, deviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetDeviceName_PersistentVolume(t *testing.T) {
|
|
||||||
plugin := newPlugin(t)
|
|
||||||
volPath := "[local] volumes/test"
|
|
||||||
spec := createPVSpec(volPath)
|
|
||||||
|
|
||||||
deviceName, err := plugin.GetVolumeName(spec)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("GetDeviceName error: %v", err)
|
|
||||||
}
|
|
||||||
if deviceName != volPath {
|
|
||||||
t.Errorf("GetDeviceName error: expected %s, got %s", volPath, deviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// One testcase for TestAttachDetach table test below
|
|
||||||
type testcase struct {
|
|
||||||
name string
|
|
||||||
// For fake vSphere:
|
|
||||||
attach attachCall
|
|
||||||
detach detachCall
|
|
||||||
diskIsAttached diskIsAttachedCall
|
|
||||||
t *testing.T
|
|
||||||
|
|
||||||
// Actual test to run
|
|
||||||
test func(test *testcase) (string, error)
|
|
||||||
// Expected return of the test
|
|
||||||
expectedDevice string
|
|
||||||
expectedError error
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAttachDetach(t *testing.T) {
|
|
||||||
uuid := "00000000000000"
|
|
||||||
diskName := "[local] volumes/test"
|
|
||||||
nodeName := types.NodeName("host")
|
|
||||||
spec := createVolSpec(diskName)
|
|
||||||
expectedDevice := filepath.FromSlash("/dev/disk/by-id/wwn-0x" + uuid)
|
|
||||||
attachError := errors.New("fake attach error")
|
|
||||||
detachError := errors.New("fake detach error")
|
|
||||||
diskCheckError := errors.New("fake DiskIsAttached error")
|
|
||||||
tests := []testcase{
|
|
||||||
// Successful Attach call
|
|
||||||
{
|
|
||||||
name: "Attach_Positive",
|
|
||||||
attach: attachCall{diskName, nodeName, uuid, nil},
|
|
||||||
test: func(testcase *testcase) (string, error) {
|
|
||||||
attacher := newAttacher(testcase)
|
|
||||||
return attacher.Attach(spec, nodeName)
|
|
||||||
},
|
|
||||||
expectedDevice: expectedDevice,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Attach call fails
|
|
||||||
{
|
|
||||||
name: "Attach_Negative",
|
|
||||||
attach: attachCall{diskName, nodeName, "", attachError},
|
|
||||||
test: func(testcase *testcase) (string, error) {
|
|
||||||
attacher := newAttacher(testcase)
|
|
||||||
return attacher.Attach(spec, nodeName)
|
|
||||||
},
|
|
||||||
expectedError: attachError,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Detach succeeds
|
|
||||||
{
|
|
||||||
name: "Detach_Positive",
|
|
||||||
diskIsAttached: diskIsAttachedCall{diskName, nodeName, true, nil},
|
|
||||||
detach: detachCall{diskName, nodeName, nil},
|
|
||||||
test: func(testcase *testcase) (string, error) {
|
|
||||||
detacher := newDetacher(testcase)
|
|
||||||
return "", detacher.Detach(diskName, nodeName)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Disk is already detached
|
|
||||||
{
|
|
||||||
name: "Detach_Positive_AlreadyDetached",
|
|
||||||
diskIsAttached: diskIsAttachedCall{diskName, nodeName, false, nil},
|
|
||||||
test: func(testcase *testcase) (string, error) {
|
|
||||||
detacher := newDetacher(testcase)
|
|
||||||
return "", detacher.Detach(diskName, nodeName)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Detach succeeds when DiskIsAttached fails
|
|
||||||
{
|
|
||||||
name: "Detach_Positive_CheckFails",
|
|
||||||
diskIsAttached: diskIsAttachedCall{diskName, nodeName, false, diskCheckError},
|
|
||||||
detach: detachCall{diskName, nodeName, nil},
|
|
||||||
test: func(testcase *testcase) (string, error) {
|
|
||||||
detacher := newDetacher(testcase)
|
|
||||||
return "", detacher.Detach(diskName, nodeName)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Detach fails
|
|
||||||
{
|
|
||||||
name: "Detach_Negative",
|
|
||||||
diskIsAttached: diskIsAttachedCall{diskName, nodeName, false, diskCheckError},
|
|
||||||
detach: detachCall{diskName, nodeName, detachError},
|
|
||||||
test: func(testcase *testcase) (string, error) {
|
|
||||||
detacher := newDetacher(testcase)
|
|
||||||
return "", detacher.Detach(diskName, nodeName)
|
|
||||||
},
|
|
||||||
expectedError: detachError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testcase := range tests {
|
|
||||||
testcase.t = t
|
|
||||||
device, err := testcase.test(&testcase)
|
|
||||||
if err != testcase.expectedError {
|
|
||||||
t.Errorf("%s failed: expected err=%q, got %q", testcase.name, testcase.expectedError.Error(), err.Error())
|
|
||||||
}
|
|
||||||
if device != testcase.expectedDevice {
|
|
||||||
t.Errorf("%s failed: expected device=%q, got %q", testcase.name, testcase.expectedDevice, device)
|
|
||||||
}
|
|
||||||
t.Logf("Test %q succeeded", testcase.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newPlugin creates a new vsphereVolumePlugin with fake cloud, NewAttacher
|
|
||||||
// and NewDetacher won't work.
|
|
||||||
func newPlugin(t *testing.T) *vsphereVolumePlugin {
|
|
||||||
host := volumetest.NewFakeVolumeHost(t, os.TempDir(), nil, nil)
|
|
||||||
plugins := ProbeVolumePlugins()
|
|
||||||
plugin := plugins[0]
|
|
||||||
plugin.Init(host)
|
|
||||||
return plugin.(*vsphereVolumePlugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAttacher(testcase *testcase) *vsphereVMDKAttacher {
|
|
||||||
return &vsphereVMDKAttacher{
|
|
||||||
host: nil,
|
|
||||||
vsphereVolumes: testcase,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDetacher(testcase *testcase) *vsphereVMDKDetacher {
|
|
||||||
return &vsphereVMDKDetacher{
|
|
||||||
vsphereVolumes: testcase,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createVolSpec(name string) *volume.Spec {
|
|
||||||
return &volume.Spec{
|
|
||||||
Volume: &v1.Volume{
|
|
||||||
VolumeSource: v1.VolumeSource{
|
|
||||||
VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{
|
|
||||||
VolumePath: name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createPVSpec(name string) *volume.Spec {
|
|
||||||
return &volume.Spec{
|
|
||||||
PersistentVolume: &v1.PersistentVolume{
|
|
||||||
Spec: v1.PersistentVolumeSpec{
|
|
||||||
PersistentVolumeSource: v1.PersistentVolumeSource{
|
|
||||||
VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{
|
|
||||||
VolumePath: name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fake vSphere implementation
|
|
||||||
|
|
||||||
type attachCall struct {
|
|
||||||
diskName string
|
|
||||||
nodeName types.NodeName
|
|
||||||
retDeviceUUID string
|
|
||||||
ret error
|
|
||||||
}
|
|
||||||
|
|
||||||
type detachCall struct {
|
|
||||||
diskName string
|
|
||||||
nodeName types.NodeName
|
|
||||||
ret error
|
|
||||||
}
|
|
||||||
|
|
||||||
type diskIsAttachedCall struct {
|
|
||||||
diskName string
|
|
||||||
nodeName types.NodeName
|
|
||||||
isAttached bool
|
|
||||||
ret error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (testcase *testcase) AttachDisk(diskName string, storagePolicyName string, nodeName types.NodeName) (string, error) {
|
|
||||||
expected := &testcase.attach
|
|
||||||
|
|
||||||
if expected.diskName == "" && expected.nodeName == "" {
|
|
||||||
// testcase.attach looks uninitialized, test did not expect to call
|
|
||||||
// AttachDisk
|
|
||||||
testcase.t.Errorf("Unexpected AttachDisk call!")
|
|
||||||
return "", errors.New("unexpected AttachDisk call")
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected.diskName != diskName {
|
|
||||||
testcase.t.Errorf("Unexpected AttachDisk call: expected diskName %s, got %s", expected.diskName, diskName)
|
|
||||||
return "", fmt.Errorf(`unexpected AttachDisk call: %w`, diskNameErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected.nodeName != nodeName {
|
|
||||||
testcase.t.Errorf("Unexpected AttachDisk call: expected nodeName %s, got %s", expected.nodeName, nodeName)
|
|
||||||
return "", fmt.Errorf(`unexpected AttachDisk call: %w`, nodeNameErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
klog.V(4).Infof("AttachDisk call: %s, %s, returning %q, %v", diskName, nodeName, expected.retDeviceUUID, expected.ret)
|
|
||||||
|
|
||||||
return expected.retDeviceUUID, expected.ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (testcase *testcase) DetachDisk(diskName string, nodeName types.NodeName) error {
|
|
||||||
expected := &testcase.detach
|
|
||||||
|
|
||||||
if expected.diskName == "" && expected.nodeName == "" {
|
|
||||||
// testcase.detach looks uninitialized, test did not expect to call
|
|
||||||
// DetachDisk
|
|
||||||
testcase.t.Errorf("Unexpected DetachDisk call!")
|
|
||||||
return errors.New("unexpected DetachDisk call")
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected.diskName != diskName {
|
|
||||||
testcase.t.Errorf("Unexpected DetachDisk call: expected diskName %s, got %s", expected.diskName, diskName)
|
|
||||||
return fmt.Errorf(`unexpected DetachDisk call: %w`, diskNameErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected.nodeName != nodeName {
|
|
||||||
testcase.t.Errorf("Unexpected DetachDisk call: expected nodeName %s, got %s", expected.nodeName, nodeName)
|
|
||||||
return fmt.Errorf(`unexpected DetachDisk call: %w`, nodeNameErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
klog.V(4).Infof("DetachDisk call: %s, %s, returning %v", diskName, nodeName, expected.ret)
|
|
||||||
|
|
||||||
return expected.ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (testcase *testcase) DiskIsAttached(diskName string, nodeName types.NodeName) (bool, string, error) {
|
|
||||||
expected := &testcase.diskIsAttached
|
|
||||||
|
|
||||||
if expected.diskName == "" && expected.nodeName == "" {
|
|
||||||
// testcase.diskIsAttached looks uninitialized, test did not expect to
|
|
||||||
// call DiskIsAttached
|
|
||||||
testcase.t.Errorf("Unexpected DiskIsAttached call!")
|
|
||||||
return false, diskName, errors.New("unexpected DiskIsAttached call")
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected.diskName != diskName {
|
|
||||||
testcase.t.Errorf("Unexpected DiskIsAttached call: expected diskName %s, got %s", expected.diskName, diskName)
|
|
||||||
return false, diskName, fmt.Errorf(`unexpected DiskIsAttached call: %w`, diskNameErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected.nodeName != nodeName {
|
|
||||||
testcase.t.Errorf("Unexpected DiskIsAttached call: expected nodeName %s, got %s", expected.nodeName, nodeName)
|
|
||||||
return false, diskName, fmt.Errorf(`unexpected DiskIsAttached call: %w`, nodeNameErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
klog.V(4).Infof("DiskIsAttached call: %s, %s, returning %v, %v", diskName, nodeName, expected.isAttached, expected.ret)
|
|
||||||
|
|
||||||
return expected.isAttached, diskName, expected.ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (testcase *testcase) DisksAreAttached(nodeVolumes map[types.NodeName][]string) (map[types.NodeName]map[string]bool, error) {
|
|
||||||
return nil, errors.New("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (testcase *testcase) CreateVolume(volumeOptions *vclib.VolumeOptions) (volumePath string, err error) {
|
|
||||||
return "", errors.New("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (testcase *testcase) DeleteVolume(vmDiskPath string) error {
|
|
||||||
return errors.New("not implemented")
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2019 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 vsphere_volume
|
|
@ -1,472 +0,0 @@
|
|||||||
//go:build !providerless
|
|
||||||
// +build !providerless
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2016 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 vsphere_volume
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"k8s.io/klog/v2"
|
|
||||||
"k8s.io/mount-utils"
|
|
||||||
utilstrings "k8s.io/utils/strings"
|
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
volumehelpers "k8s.io/cloud-provider/volume/helpers"
|
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/volume"
|
|
||||||
"k8s.io/kubernetes/pkg/volume/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is the primary entrypoint for volume plugins.
|
|
||||||
func ProbeVolumePlugins() []volume.VolumePlugin {
|
|
||||||
return []volume.VolumePlugin{&vsphereVolumePlugin{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
type vsphereVolumePlugin struct {
|
|
||||||
host volume.VolumeHost
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ volume.VolumePlugin = &vsphereVolumePlugin{}
|
|
||||||
var _ volume.PersistentVolumePlugin = &vsphereVolumePlugin{}
|
|
||||||
var _ volume.DeletableVolumePlugin = &vsphereVolumePlugin{}
|
|
||||||
var _ volume.ProvisionableVolumePlugin = &vsphereVolumePlugin{}
|
|
||||||
|
|
||||||
const (
|
|
||||||
vsphereVolumePluginName = "kubernetes.io/vsphere-volume"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getPath(uid types.UID, volName string, host volume.VolumeHost) string {
|
|
||||||
return host.GetPodVolumeDir(uid, utilstrings.EscapeQualifiedName(vsphereVolumePluginName), volName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// vSphere Volume Plugin
|
|
||||||
func (plugin *vsphereVolumePlugin) Init(host volume.VolumeHost) error {
|
|
||||||
plugin.host = host
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) GetPluginName() string {
|
|
||||||
return vsphereVolumePluginName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) IsMigratedToCSI() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) GetVolumeName(spec *volume.Spec) (string, error) {
|
|
||||||
volumeSource, _, err := getVolumeSource(spec)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return volumeSource.VolumePath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) CanSupport(spec *volume.Spec) bool {
|
|
||||||
return (spec.PersistentVolume != nil && spec.PersistentVolume.Spec.VsphereVolume != nil) ||
|
|
||||||
(spec.Volume != nil && spec.Volume.VsphereVolume != nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) RequiresRemount(spec *volume.Spec) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) SupportsMountOption() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) SupportsBulkVolumeVerification() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) SupportsSELinuxContextMount(spec *volume.Spec) (bool, error) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, _ volume.VolumeOptions) (volume.Mounter, error) {
|
|
||||||
return plugin.newMounterInternal(spec, pod.UID, &VsphereDiskUtil{}, plugin.host.GetMounter(plugin.GetPluginName()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) NewUnmounter(volName string, podUID types.UID) (volume.Unmounter, error) {
|
|
||||||
return plugin.newUnmounterInternal(volName, podUID, &VsphereDiskUtil{}, plugin.host.GetMounter(plugin.GetPluginName()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) newMounterInternal(spec *volume.Spec, podUID types.UID, manager vdManager, mounter mount.Interface) (volume.Mounter, error) {
|
|
||||||
vvol, _, err := getVolumeSource(spec)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
volPath := vvol.VolumePath
|
|
||||||
fsType := vvol.FSType
|
|
||||||
|
|
||||||
return &vsphereVolumeMounter{
|
|
||||||
vsphereVolume: &vsphereVolume{
|
|
||||||
podUID: podUID,
|
|
||||||
volName: spec.Name(),
|
|
||||||
volPath: volPath,
|
|
||||||
manager: manager,
|
|
||||||
mounter: mounter,
|
|
||||||
plugin: plugin,
|
|
||||||
MetricsProvider: volume.NewMetricsStatFS(getPath(podUID, spec.Name(), plugin.host)),
|
|
||||||
},
|
|
||||||
fsType: fsType,
|
|
||||||
diskMounter: util.NewSafeFormatAndMountFromHost(plugin.GetPluginName(), plugin.host),
|
|
||||||
mountOptions: util.MountOptionFromSpec(spec),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) newUnmounterInternal(volName string, podUID types.UID, manager vdManager, mounter mount.Interface) (volume.Unmounter, error) {
|
|
||||||
return &vsphereVolumeUnmounter{
|
|
||||||
&vsphereVolume{
|
|
||||||
podUID: podUID,
|
|
||||||
volName: volName,
|
|
||||||
manager: manager,
|
|
||||||
mounter: mounter,
|
|
||||||
plugin: plugin,
|
|
||||||
MetricsProvider: volume.NewMetricsStatFS(getPath(podUID, volName, plugin.host)),
|
|
||||||
}}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) ConstructVolumeSpec(volumeName, mountPath string) (volume.ReconstructedVolume, error) {
|
|
||||||
mounter := plugin.host.GetMounter(plugin.GetPluginName())
|
|
||||||
kvh, ok := plugin.host.(volume.KubeletVolumeHost)
|
|
||||||
if !ok {
|
|
||||||
return volume.ReconstructedVolume{}, fmt.Errorf("plugin volume host does not implement KubeletVolumeHost interface")
|
|
||||||
}
|
|
||||||
hu := kvh.GetHostUtil()
|
|
||||||
pluginMntDir := util.GetPluginMountDir(plugin.host, plugin.GetPluginName())
|
|
||||||
volumePath, err := hu.GetDeviceNameFromMount(mounter, mountPath, pluginMntDir)
|
|
||||||
if err != nil {
|
|
||||||
return volume.ReconstructedVolume{}, err
|
|
||||||
}
|
|
||||||
volumePath = strings.Replace(volumePath, "\\040", " ", -1)
|
|
||||||
klog.V(5).Infof("vSphere volume path is %q", volumePath)
|
|
||||||
vsphereVolume := &v1.Volume{
|
|
||||||
Name: volumeName,
|
|
||||||
VolumeSource: v1.VolumeSource{
|
|
||||||
VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{
|
|
||||||
VolumePath: volumePath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return volume.ReconstructedVolume{
|
|
||||||
Spec: volume.NewSpecFromVolume(vsphereVolume),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Abstract interface to disk operations.
|
|
||||||
type vdManager interface {
|
|
||||||
// Creates a volume
|
|
||||||
CreateVolume(provisioner *vsphereVolumeProvisioner, selectedNode *v1.Node, selectedZone []string) (volSpec *VolumeSpec, err error)
|
|
||||||
// Deletes a volume
|
|
||||||
DeleteVolume(deleter *vsphereVolumeDeleter) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// vspherePersistentDisk volumes are disk resources are attached to the kubelet's host machine and exposed to the pod.
|
|
||||||
type vsphereVolume struct {
|
|
||||||
volName string
|
|
||||||
podUID types.UID
|
|
||||||
// Unique identifier of the volume, used to find the disk resource in the provider.
|
|
||||||
volPath string
|
|
||||||
// Utility interface that provides API calls to the provider to attach/detach disks.
|
|
||||||
manager vdManager
|
|
||||||
// Mounter interface that provides system calls to mount the global path to the pod local path.
|
|
||||||
mounter mount.Interface
|
|
||||||
plugin *vsphereVolumePlugin
|
|
||||||
volume.MetricsProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ volume.Mounter = &vsphereVolumeMounter{}
|
|
||||||
|
|
||||||
type vsphereVolumeMounter struct {
|
|
||||||
*vsphereVolume
|
|
||||||
fsType string
|
|
||||||
diskMounter *mount.SafeFormatAndMount
|
|
||||||
mountOptions []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *vsphereVolumeMounter) GetAttributes() volume.Attributes {
|
|
||||||
return volume.Attributes{
|
|
||||||
SELinuxRelabel: true,
|
|
||||||
Managed: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUp attaches the disk and bind mounts to the volume path.
|
|
||||||
func (b *vsphereVolumeMounter) SetUp(mounterArgs volume.MounterArgs) error {
|
|
||||||
return b.SetUpAt(b.GetPath(), mounterArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUp attaches the disk and bind mounts to the volume path.
|
|
||||||
func (b *vsphereVolumeMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error {
|
|
||||||
klog.V(5).Infof("vSphere volume setup %s to %s", b.volPath, dir)
|
|
||||||
|
|
||||||
// TODO: handle failed mounts here.
|
|
||||||
notmnt, err := b.mounter.IsLikelyNotMountPoint(dir)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
klog.V(4).Infof("IsLikelyNotMountPoint failed: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !notmnt {
|
|
||||||
klog.V(4).Infof("Something is already mounted to target %s", dir)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
// On Windows, Mount will create the parent of dir and mklink (create a symbolic link) at dir later, so don't create a
|
|
||||||
// directory at dir now. Otherwise mklink will error: "Cannot create a file when that file already exists".
|
|
||||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
|
||||||
klog.Errorf("Could not create directory %s: %v", dir, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
options := []string{"bind"}
|
|
||||||
|
|
||||||
// Perform a bind mount to the full path to allow duplicate mounts of the same PD.
|
|
||||||
globalPDPath := makeGlobalPDPath(b.plugin.host, b.volPath)
|
|
||||||
mountOptions := util.JoinMountOptions(options, b.mountOptions)
|
|
||||||
err = b.mounter.MountSensitiveWithoutSystemd(globalPDPath, dir, "", mountOptions, nil)
|
|
||||||
if err != nil {
|
|
||||||
notmnt, mntErr := b.mounter.IsLikelyNotMountPoint(dir)
|
|
||||||
if mntErr != nil {
|
|
||||||
klog.Errorf("IsLikelyNotMountPoint check failed: %v", mntErr)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !notmnt {
|
|
||||||
if mntErr = b.mounter.Unmount(dir); mntErr != nil {
|
|
||||||
klog.Errorf("Failed to unmount: %v", mntErr)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
notmnt, mntErr := b.mounter.IsLikelyNotMountPoint(dir)
|
|
||||||
if mntErr != nil {
|
|
||||||
klog.Errorf("IsLikelyNotMountPoint check failed: %v", mntErr)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !notmnt {
|
|
||||||
klog.Errorf("%s is still mounted, despite call to unmount(). Will try again next sync loop.", b.GetPath())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
os.Remove(dir)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
volume.SetVolumeOwnership(b, dir, mounterArgs.FsGroup, mounterArgs.FSGroupChangePolicy, util.FSGroupCompleteHook(b.plugin, nil))
|
|
||||||
klog.V(3).Infof("vSphere volume %s mounted to %s", b.volPath, dir)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ volume.Unmounter = &vsphereVolumeUnmounter{}
|
|
||||||
|
|
||||||
type vsphereVolumeUnmounter struct {
|
|
||||||
*vsphereVolume
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmounts the bind mount, and detaches the disk only if the PD
|
|
||||||
// resource was the last reference to that disk on the kubelet.
|
|
||||||
func (v *vsphereVolumeUnmounter) TearDown() error {
|
|
||||||
return v.TearDownAt(v.GetPath())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmounts the bind mount, and detaches the disk only if the PD
|
|
||||||
// resource was the last reference to that disk on the kubelet.
|
|
||||||
func (v *vsphereVolumeUnmounter) TearDownAt(dir string) error {
|
|
||||||
return mount.CleanupMountPoint(dir, v.mounter, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeGlobalPDPath(host volume.VolumeHost, devName string) string {
|
|
||||||
return filepath.Join(host.GetPluginDir(vsphereVolumePluginName), util.MountsInGlobalPDPath, devName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (vv *vsphereVolume) GetPath() string {
|
|
||||||
name := vsphereVolumePluginName
|
|
||||||
return vv.plugin.host.GetPodVolumeDir(vv.podUID, utilstrings.EscapeQualifiedName(name), vv.volName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// vSphere Persistent Volume Plugin
|
|
||||||
func (plugin *vsphereVolumePlugin) GetAccessModes() []v1.PersistentVolumeAccessMode {
|
|
||||||
return []v1.PersistentVolumeAccessMode{
|
|
||||||
v1.ReadWriteOnce,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// vSphere Deletable Volume Plugin
|
|
||||||
type vsphereVolumeDeleter struct {
|
|
||||||
*vsphereVolume
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ volume.Deleter = &vsphereVolumeDeleter{}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) NewDeleter(logger klog.Logger, spec *volume.Spec) (volume.Deleter, error) {
|
|
||||||
return plugin.newDeleterInternal(spec, &VsphereDiskUtil{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) newDeleterInternal(spec *volume.Spec, manager vdManager) (volume.Deleter, error) {
|
|
||||||
if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.VsphereVolume == nil {
|
|
||||||
return nil, fmt.Errorf("spec.PersistentVolumeSource.VsphereVolume is nil")
|
|
||||||
}
|
|
||||||
return &vsphereVolumeDeleter{
|
|
||||||
&vsphereVolume{
|
|
||||||
volName: spec.Name(),
|
|
||||||
volPath: spec.PersistentVolume.Spec.VsphereVolume.VolumePath,
|
|
||||||
manager: manager,
|
|
||||||
plugin: plugin,
|
|
||||||
}}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *vsphereVolumeDeleter) Delete() error {
|
|
||||||
return r.manager.DeleteVolume(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// vSphere Provisionable Volume Plugin
|
|
||||||
type vsphereVolumeProvisioner struct {
|
|
||||||
*vsphereVolume
|
|
||||||
options volume.VolumeOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ volume.Provisioner = &vsphereVolumeProvisioner{}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) NewProvisioner(logger klog.Logger, options volume.VolumeOptions) (volume.Provisioner, error) {
|
|
||||||
return plugin.newProvisionerInternal(options, &VsphereDiskUtil{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) newProvisionerInternal(options volume.VolumeOptions, manager vdManager) (volume.Provisioner, error) {
|
|
||||||
return &vsphereVolumeProvisioner{
|
|
||||||
vsphereVolume: &vsphereVolume{
|
|
||||||
manager: manager,
|
|
||||||
plugin: plugin,
|
|
||||||
},
|
|
||||||
options: options,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *vsphereVolumeProvisioner) Provision(selectedNode *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (*v1.PersistentVolume, error) {
|
|
||||||
if !util.ContainsAllAccessModes(v.plugin.GetAccessModes(), v.options.PVC.Spec.AccessModes) {
|
|
||||||
return nil, fmt.Errorf("invalid AccessModes %v: only AccessModes %v are supported", v.options.PVC.Spec.AccessModes, v.plugin.GetAccessModes())
|
|
||||||
}
|
|
||||||
klog.V(1).Infof("Provision with selectedNode: %s and allowedTopologies : %s", getNodeName(selectedNode), allowedTopologies)
|
|
||||||
selectedZones, err := volumehelpers.ZonesFromAllowedTopologies(allowedTopologies)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
klog.V(4).Infof("Selected zones for volume : %s", selectedZones)
|
|
||||||
volSpec, err := v.manager.CreateVolume(v, selectedNode, selectedZones.List())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if volSpec.Fstype == "" {
|
|
||||||
volSpec.Fstype = "ext4"
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeMode := v.options.PVC.Spec.VolumeMode
|
|
||||||
if volumeMode != nil && *volumeMode == v1.PersistentVolumeBlock {
|
|
||||||
klog.V(5).Infof("vSphere block volume should not have any FSType")
|
|
||||||
volSpec.Fstype = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
pv := &v1.PersistentVolume{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: v.options.PVName,
|
|
||||||
Labels: map[string]string{},
|
|
||||||
Annotations: map[string]string{
|
|
||||||
util.VolumeDynamicallyCreatedByKey: "vsphere-volume-dynamic-provisioner",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Spec: v1.PersistentVolumeSpec{
|
|
||||||
PersistentVolumeReclaimPolicy: v.options.PersistentVolumeReclaimPolicy,
|
|
||||||
AccessModes: v.options.PVC.Spec.AccessModes,
|
|
||||||
Capacity: v1.ResourceList{
|
|
||||||
v1.ResourceName(v1.ResourceStorage): resource.MustParse(fmt.Sprintf("%dKi", volSpec.Size)),
|
|
||||||
},
|
|
||||||
VolumeMode: volumeMode,
|
|
||||||
PersistentVolumeSource: v1.PersistentVolumeSource{
|
|
||||||
VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{
|
|
||||||
VolumePath: volSpec.Path,
|
|
||||||
FSType: volSpec.Fstype,
|
|
||||||
StoragePolicyName: volSpec.StoragePolicyName,
|
|
||||||
StoragePolicyID: volSpec.StoragePolicyID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MountOptions: v.options.MountOptions,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if len(v.options.PVC.Spec.AccessModes) == 0 {
|
|
||||||
pv.Spec.AccessModes = v.plugin.GetAccessModes()
|
|
||||||
}
|
|
||||||
|
|
||||||
labels := volSpec.Labels
|
|
||||||
requirements := make([]v1.NodeSelectorRequirement, 0)
|
|
||||||
if len(labels) != 0 {
|
|
||||||
if pv.Labels == nil {
|
|
||||||
pv.Labels = make(map[string]string)
|
|
||||||
}
|
|
||||||
for k, v := range labels {
|
|
||||||
pv.Labels[k] = v
|
|
||||||
var values []string
|
|
||||||
if k == v1.LabelTopologyZone || k == v1.LabelFailureDomainBetaZone {
|
|
||||||
values, err = volumehelpers.LabelZonesToList(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to convert label string for Zone: %s to a List: %v", v, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
values = []string{v}
|
|
||||||
}
|
|
||||||
requirements = append(requirements, v1.NodeSelectorRequirement{Key: k, Operator: v1.NodeSelectorOpIn, Values: values})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(requirements) > 0 {
|
|
||||||
pv.Spec.NodeAffinity = new(v1.VolumeNodeAffinity)
|
|
||||||
pv.Spec.NodeAffinity.Required = new(v1.NodeSelector)
|
|
||||||
pv.Spec.NodeAffinity.Required.NodeSelectorTerms = make([]v1.NodeSelectorTerm, 1)
|
|
||||||
pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions = requirements
|
|
||||||
}
|
|
||||||
|
|
||||||
return pv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getVolumeSource(
|
|
||||||
spec *volume.Spec) (*v1.VsphereVirtualDiskVolumeSource, bool, error) {
|
|
||||||
if spec.Volume != nil && spec.Volume.VsphereVolume != nil {
|
|
||||||
return spec.Volume.VsphereVolume, spec.ReadOnly, nil
|
|
||||||
} else if spec.PersistentVolume != nil &&
|
|
||||||
spec.PersistentVolume.Spec.VsphereVolume != nil {
|
|
||||||
return spec.PersistentVolume.Spec.VsphereVolume, spec.ReadOnly, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false, fmt.Errorf("spec does not reference a VSphere volume type")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNodeName(node *v1.Node) string {
|
|
||||||
if node == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return node.Name
|
|
||||||
}
|
|
@ -1,169 +0,0 @@
|
|||||||
//go:build !providerless
|
|
||||||
// +build !providerless
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2018 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 vsphere_volume
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"k8s.io/klog/v2"
|
|
||||||
"k8s.io/mount-utils"
|
|
||||||
utilstrings "k8s.io/utils/strings"
|
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
"k8s.io/kubernetes/pkg/volume"
|
|
||||||
"k8s.io/kubernetes/pkg/volume/util/volumepathhandler"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ volume.BlockVolumePlugin = &vsphereVolumePlugin{}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) ConstructBlockVolumeSpec(podUID types.UID, volumeName, mapPath string) (*volume.Spec, error) {
|
|
||||||
|
|
||||||
pluginDir := plugin.host.GetPluginDir(plugin.GetPluginName())
|
|
||||||
blkUtil := volumepathhandler.NewBlockVolumePathHandler()
|
|
||||||
globalMapPathUUID, err := blkUtil.FindGlobalMapPathUUIDFromPod(pluginDir, mapPath, podUID)
|
|
||||||
if err != nil {
|
|
||||||
klog.Errorf("Failed to find GlobalMapPathUUID from Pod: %s with error: %+v", podUID, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
klog.V(5).Infof("globalMapPathUUID: %v", globalMapPathUUID)
|
|
||||||
globalMapPath := filepath.Dir(globalMapPathUUID)
|
|
||||||
if len(globalMapPath) <= 1 {
|
|
||||||
return nil, fmt.Errorf("failed to get volume plugin information from globalMapPathUUID: %v", globalMapPathUUID)
|
|
||||||
}
|
|
||||||
return getVolumeSpecFromGlobalMapPath(volumeName, globalMapPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getVolumeSpecFromGlobalMapPath(volumeName, globalMapPath string) (*volume.Spec, error) {
|
|
||||||
// Construct volume spec from globalMapPath
|
|
||||||
// globalMapPath example:
|
|
||||||
// plugins/kubernetes.io/{PluginName}/{DefaultKubeletVolumeDevicesDirName}/{volumeID}
|
|
||||||
// plugins/kubernetes.io/vsphere-volume/volumeDevices/[datastore1]\\040volumes/myDisk
|
|
||||||
volPath := filepath.Base(globalMapPath)
|
|
||||||
volPath = strings.Replace(volPath, "\\040", "", -1)
|
|
||||||
if len(volPath) <= 1 {
|
|
||||||
return nil, fmt.Errorf("failed to get volume path from global path=%s", globalMapPath)
|
|
||||||
}
|
|
||||||
block := v1.PersistentVolumeBlock
|
|
||||||
vsphereVolume := &v1.PersistentVolume{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: volumeName,
|
|
||||||
},
|
|
||||||
Spec: v1.PersistentVolumeSpec{
|
|
||||||
PersistentVolumeSource: v1.PersistentVolumeSource{
|
|
||||||
VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{
|
|
||||||
VolumePath: volPath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
VolumeMode: &block,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return volume.NewSpecFromPersistentVolume(vsphereVolume, true), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) NewBlockVolumeMapper(spec *volume.Spec, pod *v1.Pod, _ volume.VolumeOptions) (volume.BlockVolumeMapper, error) {
|
|
||||||
// If this called via GenerateUnmapDeviceFunc(), pod is nil.
|
|
||||||
// Pass empty string as dummy uid since uid isn't used in the case.
|
|
||||||
var uid types.UID
|
|
||||||
if pod != nil {
|
|
||||||
uid = pod.UID
|
|
||||||
}
|
|
||||||
return plugin.newBlockVolumeMapperInternal(spec, uid, &VsphereDiskUtil{}, plugin.host.GetMounter(plugin.GetPluginName()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) newBlockVolumeMapperInternal(spec *volume.Spec, podUID types.UID, manager vdManager, mounter mount.Interface) (volume.BlockVolumeMapper, error) {
|
|
||||||
volumeSource, _, err := getVolumeSource(spec)
|
|
||||||
if err != nil {
|
|
||||||
klog.Errorf("Failed to get Volume source from volume Spec: %+v with error: %+v", *spec, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
volPath := volumeSource.VolumePath
|
|
||||||
mapper := &vsphereBlockVolumeMapper{
|
|
||||||
vsphereVolume: &vsphereVolume{
|
|
||||||
volName: spec.Name(),
|
|
||||||
podUID: podUID,
|
|
||||||
volPath: volPath,
|
|
||||||
manager: manager,
|
|
||||||
mounter: mounter,
|
|
||||||
plugin: plugin,
|
|
||||||
MetricsProvider: volume.NewMetricsStatFS(getPath(podUID, spec.Name(), plugin.host)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
blockPath, err := mapper.GetGlobalMapPath(spec)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get device path: %v", err)
|
|
||||||
}
|
|
||||||
mapper.MetricsProvider = volume.NewMetricsBlock(filepath.Join(blockPath, string(podUID)))
|
|
||||||
|
|
||||||
return mapper, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) NewBlockVolumeUnmapper(volName string, podUID types.UID) (volume.BlockVolumeUnmapper, error) {
|
|
||||||
return plugin.newUnmapperInternal(volName, podUID, &VsphereDiskUtil{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *vsphereVolumePlugin) newUnmapperInternal(volName string, podUID types.UID, manager vdManager) (volume.BlockVolumeUnmapper, error) {
|
|
||||||
return &vsphereBlockVolumeUnmapper{
|
|
||||||
vsphereVolume: &vsphereVolume{
|
|
||||||
volName: volName,
|
|
||||||
podUID: podUID,
|
|
||||||
volPath: volName,
|
|
||||||
manager: manager,
|
|
||||||
plugin: plugin,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ volume.BlockVolumeMapper = &vsphereBlockVolumeMapper{}
|
|
||||||
|
|
||||||
type vsphereBlockVolumeMapper struct {
|
|
||||||
*vsphereVolume
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ volume.BlockVolumeUnmapper = &vsphereBlockVolumeUnmapper{}
|
|
||||||
|
|
||||||
type vsphereBlockVolumeUnmapper struct {
|
|
||||||
*vsphereVolume
|
|
||||||
volume.MetricsNil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGlobalMapPath returns global map path and error
|
|
||||||
// path: plugins/kubernetes.io/{PluginName}/volumeDevices/volumePath
|
|
||||||
func (v *vsphereVolume) GetGlobalMapPath(spec *volume.Spec) (string, error) {
|
|
||||||
volumeSource, _, err := getVolumeSource(spec)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.Join(v.plugin.host.GetVolumeDevicePluginDir(vsphereVolumePluginName), string(volumeSource.VolumePath)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *vsphereVolume) GetPodDeviceMapPath() (string, string) {
|
|
||||||
return v.plugin.host.GetPodVolumeDeviceDir(v.podUID, utilstrings.EscapeQualifiedName(vsphereVolumePluginName)), v.volName
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportsMetrics returns true for vsphereBlockVolumeMapper as it initializes the
|
|
||||||
// MetricsProvider.
|
|
||||||
func (vbvm *vsphereBlockVolumeMapper) SupportsMetrics() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
@ -1,153 +0,0 @@
|
|||||||
//go:build !providerless
|
|
||||||
// +build !providerless
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2018 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 vsphere_volume
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
utiltesting "k8s.io/client-go/util/testing"
|
|
||||||
"k8s.io/kubernetes/pkg/volume"
|
|
||||||
volumetest "k8s.io/kubernetes/pkg/volume/testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
testVolumePath = "volPath1"
|
|
||||||
testGlobalPath = "plugins/kubernetes.io/vsphere-volume/volumeDevices/volPath1"
|
|
||||||
testPodPath = "pods/poduid/volumeDevices/kubernetes.io~vsphere-volume"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetVolumeSpecFromGlobalMapPath(t *testing.T) {
|
|
||||||
// make our test path for fake GlobalMapPath
|
|
||||||
// /tmp symbolized our pluginDir
|
|
||||||
// /tmp/testGlobalPathXXXXX/plugins/kubernetes.io/vsphere-volume/volumeDevices/
|
|
||||||
tmpVDir, err := utiltesting.MkTmpdir("vsphereBlockVolume")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't make a temp dir: %s", err)
|
|
||||||
}
|
|
||||||
// deferred clean up
|
|
||||||
defer os.RemoveAll(tmpVDir)
|
|
||||||
|
|
||||||
expectedGlobalPath := filepath.Join(tmpVDir, testGlobalPath)
|
|
||||||
|
|
||||||
// Bad Path
|
|
||||||
badspec, err := getVolumeSpecFromGlobalMapPath("", "")
|
|
||||||
if badspec != nil || err == nil {
|
|
||||||
t.Errorf("Expected not to get spec from GlobalMapPath but did")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Good Path
|
|
||||||
spec, err := getVolumeSpecFromGlobalMapPath("myVolume", expectedGlobalPath)
|
|
||||||
if spec == nil || err != nil {
|
|
||||||
t.Fatalf("Failed to get spec from GlobalMapPath: %s", err)
|
|
||||||
}
|
|
||||||
if spec.PersistentVolume.Name != "myVolume" {
|
|
||||||
t.Errorf("Invalid PV name from GlobalMapPath spec: %s", spec.PersistentVolume.Name)
|
|
||||||
}
|
|
||||||
if spec.PersistentVolume.Spec.VsphereVolume.VolumePath != testVolumePath {
|
|
||||||
t.Fatalf("Invalid volumePath from GlobalMapPath spec: %s", spec.PersistentVolume.Spec.VsphereVolume.VolumePath)
|
|
||||||
}
|
|
||||||
block := v1.PersistentVolumeBlock
|
|
||||||
specMode := spec.PersistentVolume.Spec.VolumeMode
|
|
||||||
if specMode == nil {
|
|
||||||
t.Errorf("Invalid volumeMode from GlobalMapPath spec: %v expected: %v", specMode, block)
|
|
||||||
}
|
|
||||||
if *specMode != block {
|
|
||||||
t.Errorf("Invalid volumeMode from GlobalMapPath spec: %v expected: %v", *specMode, block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetPodAndPluginMapPaths(t *testing.T) {
|
|
||||||
tmpVDir, err := utiltesting.MkTmpdir("vsphereBlockVolume")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't make a temp dir: %s", err)
|
|
||||||
}
|
|
||||||
// deferred clean up
|
|
||||||
defer os.RemoveAll(tmpVDir)
|
|
||||||
|
|
||||||
expectedGlobalPath := filepath.Join(tmpVDir, testGlobalPath)
|
|
||||||
expectedPodPath := filepath.Join(tmpVDir, testPodPath)
|
|
||||||
|
|
||||||
spec := getTestVolume(true) // block volume
|
|
||||||
pluginMgr := volume.VolumePluginMgr{}
|
|
||||||
pluginMgr.InitPlugins(ProbeVolumePlugins(), nil, volumetest.NewFakeVolumeHost(t, tmpVDir, nil, nil))
|
|
||||||
plugin, err := pluginMgr.FindMapperPluginByName(vsphereVolumePluginName)
|
|
||||||
if err != nil {
|
|
||||||
os.RemoveAll(tmpVDir)
|
|
||||||
t.Fatalf("Can't find the plugin by name: %q", vsphereVolumePluginName)
|
|
||||||
}
|
|
||||||
if plugin.GetPluginName() != vsphereVolumePluginName {
|
|
||||||
t.Fatalf("Wrong name: %s", plugin.GetPluginName())
|
|
||||||
}
|
|
||||||
pod := &v1.Pod{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
UID: types.UID("poduid"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mapper, err := plugin.NewBlockVolumeMapper(spec, pod, volume.VolumeOptions{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to make a new Mounter: %v", err)
|
|
||||||
}
|
|
||||||
if mapper == nil {
|
|
||||||
t.Fatalf("Got a nil Mounter")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGlobalMapPath
|
|
||||||
globalMapPath, err := mapper.GetGlobalMapPath(spec)
|
|
||||||
if err != nil || len(globalMapPath) == 0 {
|
|
||||||
t.Fatalf("Invalid GlobalMapPath from spec: %s", spec.PersistentVolume.Spec.VsphereVolume.VolumePath)
|
|
||||||
}
|
|
||||||
if globalMapPath != expectedGlobalPath {
|
|
||||||
t.Errorf("Failed to get GlobalMapPath: %s %s", globalMapPath, expectedGlobalPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPodDeviceMapPath
|
|
||||||
devicePath, volumeName := mapper.GetPodDeviceMapPath()
|
|
||||||
if devicePath != expectedPodPath {
|
|
||||||
t.Errorf("Got unexpected pod path: %s, expected %s", devicePath, expectedPodPath)
|
|
||||||
}
|
|
||||||
if volumeName != testVolumePath {
|
|
||||||
t.Errorf("Got unexpected volNamne: %s, expected %s", volumeName, testVolumePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTestVolume(isBlock bool) *volume.Spec {
|
|
||||||
pv := &v1.PersistentVolume{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: testVolumePath,
|
|
||||||
},
|
|
||||||
Spec: v1.PersistentVolumeSpec{
|
|
||||||
PersistentVolumeSource: v1.PersistentVolumeSource{
|
|
||||||
VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{
|
|
||||||
VolumePath: testVolumePath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if isBlock {
|
|
||||||
blockMode := v1.PersistentVolumeBlock
|
|
||||||
pv.Spec.VolumeMode = &blockMode
|
|
||||||
}
|
|
||||||
return volume.NewSpecFromPersistentVolume(pv, true)
|
|
||||||
}
|
|
@ -1,259 +0,0 @@
|
|||||||
//go:build !providerless
|
|
||||||
// +build !providerless
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2016 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 vsphere_volume
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"k8s.io/mount-utils"
|
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
utiltesting "k8s.io/client-go/util/testing"
|
|
||||||
cloudprovider "k8s.io/cloud-provider"
|
|
||||||
"k8s.io/cloud-provider/fake"
|
|
||||||
"k8s.io/kubernetes/pkg/volume"
|
|
||||||
volumetest "k8s.io/kubernetes/pkg/volume/testing"
|
|
||||||
"k8s.io/legacy-cloud-providers/vsphere"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCanSupport(t *testing.T) {
|
|
||||||
tmpDir, err := utiltesting.MkTmpdir("vsphereVolumeTest")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't make a temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
plugMgr := volume.VolumePluginMgr{}
|
|
||||||
plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeKubeletVolumeHost(t, tmpDir, nil, nil))
|
|
||||||
|
|
||||||
plug, err := plugMgr.FindPluginByName("kubernetes.io/vsphere-volume")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Can't find the plugin by name")
|
|
||||||
}
|
|
||||||
if plug.GetPluginName() != "kubernetes.io/vsphere-volume" {
|
|
||||||
t.Errorf("Wrong name: %s", plug.GetPluginName())
|
|
||||||
}
|
|
||||||
|
|
||||||
if !plug.CanSupport(&volume.Spec{Volume: &v1.Volume{VolumeSource: v1.VolumeSource{VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{}}}}) {
|
|
||||||
t.Errorf("Expected true")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !plug.CanSupport(&volume.Spec{PersistentVolume: &v1.PersistentVolume{Spec: v1.PersistentVolumeSpec{PersistentVolumeSource: v1.PersistentVolumeSource{VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{}}}}}) {
|
|
||||||
t.Errorf("Expected true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakePDManager struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fake *fakePDManager) CreateVolume(v *vsphereVolumeProvisioner, selectedNode *v1.Node, selectedZone []string) (volSpec *VolumeSpec, err error) {
|
|
||||||
volSpec = &VolumeSpec{
|
|
||||||
Path: "[local] test-volume-name.vmdk",
|
|
||||||
Size: 100,
|
|
||||||
Fstype: "ext4",
|
|
||||||
StoragePolicyName: "gold",
|
|
||||||
StoragePolicyID: "1234",
|
|
||||||
}
|
|
||||||
return volSpec, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fake *fakePDManager) DeleteVolume(vd *vsphereVolumeDeleter) error {
|
|
||||||
if vd.volPath != "[local] test-volume-name.vmdk" {
|
|
||||||
return fmt.Errorf("Deleter got unexpected volume path: %s", vd.volPath)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlugin(t *testing.T) {
|
|
||||||
// Initial setup to test volume plugin
|
|
||||||
tmpDir, err := utiltesting.MkTmpdir("vsphereVolumeTest")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't make a temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
plugMgr := volume.VolumePluginMgr{}
|
|
||||||
plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeKubeletVolumeHost(t, tmpDir, nil, nil))
|
|
||||||
|
|
||||||
plug, err := plugMgr.FindPluginByName("kubernetes.io/vsphere-volume")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Can't find the plugin by name")
|
|
||||||
}
|
|
||||||
|
|
||||||
spec := &v1.Volume{
|
|
||||||
Name: "vol1",
|
|
||||||
VolumeSource: v1.VolumeSource{
|
|
||||||
VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{
|
|
||||||
VolumePath: "[local] test-volume-name.vmdk",
|
|
||||||
FSType: "ext4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Mounter
|
|
||||||
fakeManager := &fakePDManager{}
|
|
||||||
fakeMounter := mount.NewFakeMounter(nil)
|
|
||||||
mounter, err := plug.(*vsphereVolumePlugin).newMounterInternal(volume.NewSpecFromVolume(spec), types.UID("poduid"), fakeManager, fakeMounter)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to make a new Mounter: %v", err)
|
|
||||||
}
|
|
||||||
if mounter == nil {
|
|
||||||
t.Errorf("Got a nil Mounter")
|
|
||||||
}
|
|
||||||
|
|
||||||
mntPath := filepath.Join(tmpDir, "pods/poduid/volumes/kubernetes.io~vsphere-volume/vol1")
|
|
||||||
path := mounter.GetPath()
|
|
||||||
if path != mntPath {
|
|
||||||
t.Errorf("Got unexpected path: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mounter.SetUp(volume.MounterArgs{}); err != nil {
|
|
||||||
t.Errorf("Expected success, got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Unmounter
|
|
||||||
fakeManager = &fakePDManager{}
|
|
||||||
unmounter, err := plug.(*vsphereVolumePlugin).newUnmounterInternal("vol1", types.UID("poduid"), fakeManager, fakeMounter)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to make a new Unmounter: %v", err)
|
|
||||||
}
|
|
||||||
if unmounter == nil {
|
|
||||||
t.Errorf("Got a nil Unmounter")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := unmounter.TearDown(); err != nil {
|
|
||||||
t.Errorf("Expected success, got: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
t.Errorf("TearDown() failed, volume path still exists: %s", path)
|
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
t.Errorf("TearDown() failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Provisioner
|
|
||||||
options := volume.VolumeOptions{
|
|
||||||
PVC: volumetest.CreateTestPVC("100Mi", []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}),
|
|
||||||
PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete,
|
|
||||||
}
|
|
||||||
provisioner, err := plug.(*vsphereVolumePlugin).newProvisionerInternal(options, &fakePDManager{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("newProvisionerInternal() failed: %v", err)
|
|
||||||
}
|
|
||||||
persistentSpec, err := provisioner.Provision(nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Provision() failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if persistentSpec.Spec.PersistentVolumeSource.VsphereVolume.VolumePath != "[local] test-volume-name.vmdk" {
|
|
||||||
t.Errorf("Provision() returned unexpected path %s", persistentSpec.Spec.PersistentVolumeSource.VsphereVolume.VolumePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if persistentSpec.Spec.PersistentVolumeSource.VsphereVolume.StoragePolicyName != "gold" {
|
|
||||||
t.Errorf("Provision() returned unexpected storagepolicy name %s", persistentSpec.Spec.PersistentVolumeSource.VsphereVolume.StoragePolicyName)
|
|
||||||
}
|
|
||||||
|
|
||||||
cap := persistentSpec.Spec.Capacity[v1.ResourceStorage]
|
|
||||||
size := cap.Value()
|
|
||||||
if size != 100*1024 {
|
|
||||||
t.Errorf("Provision() returned unexpected volume size: %v", size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Deleter
|
|
||||||
volSpec := &volume.Spec{
|
|
||||||
PersistentVolume: persistentSpec,
|
|
||||||
}
|
|
||||||
deleter, err := plug.(*vsphereVolumePlugin).newDeleterInternal(volSpec, &fakePDManager{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("newDeleterInternal() failed: %v", err)
|
|
||||||
}
|
|
||||||
err = deleter.Delete()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Deleter() failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnsupportedCloudProvider(t *testing.T) {
|
|
||||||
// Initial setup to test volume plugin
|
|
||||||
tmpDir, err := utiltesting.MkTmpdir("vsphereVolumeTest")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't make a temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
testcases := []struct {
|
|
||||||
name string
|
|
||||||
cloudProvider cloudprovider.Interface
|
|
||||||
success bool
|
|
||||||
}{
|
|
||||||
{name: "nil cloudprovider", cloudProvider: nil},
|
|
||||||
{name: "vSphere", cloudProvider: &vsphere.VSphere{}, success: true},
|
|
||||||
{name: "fake cloudprovider", cloudProvider: &fake.Cloud{}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testcases {
|
|
||||||
t.Logf("test case: %v", tc.name)
|
|
||||||
|
|
||||||
plugMgr := volume.VolumePluginMgr{}
|
|
||||||
plugMgr.InitPlugins(ProbeVolumePlugins(), nil, /* prober */
|
|
||||||
volumetest.NewFakeKubeletVolumeHostWithCloudProvider(t, tmpDir, nil, nil, tc.cloudProvider))
|
|
||||||
|
|
||||||
plug, err := plugMgr.FindAttachablePluginByName("kubernetes.io/vsphere-volume")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Can't find the plugin by name")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = plug.NewAttacher()
|
|
||||||
if !tc.success && err == nil {
|
|
||||||
t.Errorf("expected error when creating attacher due to incorrect cloud provider, but got none")
|
|
||||||
} else if tc.success && err != nil {
|
|
||||||
t.Errorf("expected no error when creating attacher, but got error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = plug.NewDetacher()
|
|
||||||
if !tc.success && err == nil {
|
|
||||||
t.Errorf("expected error when creating detacher due to incorrect cloud provider, but got none")
|
|
||||||
} else if tc.success && err != nil {
|
|
||||||
t.Errorf("expected no error when creating detacher, but got error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnsupportedVolumeHost(t *testing.T) {
|
|
||||||
tmpDir, err := utiltesting.MkTmpdir("vsphereVolumeTest")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't make a temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
plugMgr := volume.VolumePluginMgr{}
|
|
||||||
plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil))
|
|
||||||
|
|
||||||
plug, err := plugMgr.FindPluginByName("kubernetes.io/vsphere-volume")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Can't find the plugin by name")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = plug.ConstructVolumeSpec("", "")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected failure constructing volume spec with unsupported VolumeHost")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,281 +0,0 @@
|
|||||||
//go:build !providerless
|
|
||||||
// +build !providerless
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2016 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 vsphere_volume
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
cloudprovider "k8s.io/cloud-provider"
|
|
||||||
volumehelpers "k8s.io/cloud-provider/volume/helpers"
|
|
||||||
"k8s.io/klog/v2"
|
|
||||||
"k8s.io/kubernetes/pkg/volume"
|
|
||||||
volumeutil "k8s.io/kubernetes/pkg/volume/util"
|
|
||||||
"k8s.io/legacy-cloud-providers/vsphere"
|
|
||||||
"k8s.io/legacy-cloud-providers/vsphere/vclib"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
checkSleepDuration = time.Second
|
|
||||||
diskByIDPath = "/dev/disk/by-id/"
|
|
||||||
diskSCSIPrefix = "wwn-0x"
|
|
||||||
// diskformat parameter is deprecated as of Kubernetes v1.21.0
|
|
||||||
diskformat = "diskformat"
|
|
||||||
datastore = "datastore"
|
|
||||||
StoragePolicyName = "storagepolicyname"
|
|
||||||
|
|
||||||
// hostfailurestotolerate parameter is deprecated as of Kubernetes v1.19.0
|
|
||||||
HostFailuresToTolerateCapability = "hostfailurestotolerate"
|
|
||||||
// forceprovisioning parameter is deprecated as of Kubernetes v1.19.0
|
|
||||||
ForceProvisioningCapability = "forceprovisioning"
|
|
||||||
// cachereservation parameter is deprecated as of Kubernetes v1.19.0
|
|
||||||
CacheReservationCapability = "cachereservation"
|
|
||||||
// diskstripes parameter is deprecated as of Kubernetes v1.19.0
|
|
||||||
DiskStripesCapability = "diskstripes"
|
|
||||||
// objectspacereservation parameter is deprecated as of Kubernetes v1.19.0
|
|
||||||
ObjectSpaceReservationCapability = "objectspacereservation"
|
|
||||||
// iopslimit parameter is deprecated as of Kubernetes v1.19.0
|
|
||||||
IopsLimitCapability = "iopslimit"
|
|
||||||
HostFailuresToTolerateCapabilityMin = 0
|
|
||||||
HostFailuresToTolerateCapabilityMax = 3
|
|
||||||
ForceProvisioningCapabilityMin = 0
|
|
||||||
ForceProvisioningCapabilityMax = 1
|
|
||||||
CacheReservationCapabilityMin = 0
|
|
||||||
CacheReservationCapabilityMax = 100
|
|
||||||
DiskStripesCapabilityMin = 1
|
|
||||||
DiskStripesCapabilityMax = 12
|
|
||||||
ObjectSpaceReservationCapabilityMin = 0
|
|
||||||
ObjectSpaceReservationCapabilityMax = 100
|
|
||||||
IopsLimitCapabilityMin = 0
|
|
||||||
// reduce number of characters in vsphere volume name. The reason for setting length smaller than 255 is because typically
|
|
||||||
// volume name also becomes part of mount path - /var/lib/kubelet/plugins/kubernetes.io/vsphere-volume/mounts/<name>
|
|
||||||
// and systemd has a limit of 256 chars in a unit name - https://github.com/systemd/systemd/pull/14294
|
|
||||||
// so if we subtract the kubelet path prefix from 256, we are left with 191 characters.
|
|
||||||
// Since datastore name is typically part of volumeName we are choosing a shorter length of 63
|
|
||||||
// and leaving room of certain characters being escaped etc.
|
|
||||||
// Given that volume name is typically of the form - pvc-0f13e3ad-97f8-41ab-9392-84562ef40d17.vmdk (45 chars),
|
|
||||||
// this should still leave plenty of room for clusterName inclusion.
|
|
||||||
maxVolumeLength = 63
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrProbeVolume = errors.New("error scanning attached volumes")
|
|
||||||
|
|
||||||
type VsphereDiskUtil struct{}
|
|
||||||
|
|
||||||
type VolumeSpec struct {
|
|
||||||
Path string
|
|
||||||
Size int
|
|
||||||
Fstype string
|
|
||||||
StoragePolicyID string
|
|
||||||
StoragePolicyName string
|
|
||||||
Labels map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateVolume creates a vSphere volume.
|
|
||||||
func (util *VsphereDiskUtil) CreateVolume(v *vsphereVolumeProvisioner, selectedNode *v1.Node, selectedZone []string) (volSpec *VolumeSpec, err error) {
|
|
||||||
var fstype string
|
|
||||||
cloud, err := getCloudProvider(v.plugin.host.GetCloudProvider())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
capacity := v.options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]
|
|
||||||
// vSphere works with KiB, but its minimum allocation unit is 1 MiB
|
|
||||||
volSizeMiB, err := volumehelpers.RoundUpToMiBInt(capacity)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
volSizeKiB := volSizeMiB * 1024
|
|
||||||
name := volumeutil.GenerateVolumeName(v.options.ClusterName, v.options.PVName, maxVolumeLength)
|
|
||||||
volumeOptions := &vclib.VolumeOptions{
|
|
||||||
CapacityKB: volSizeKiB,
|
|
||||||
Tags: *v.options.CloudTags,
|
|
||||||
Name: name,
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeOptions.Zone = selectedZone
|
|
||||||
volumeOptions.SelectedNode = selectedNode
|
|
||||||
// Apply Parameters (case-insensitive). We leave validation of
|
|
||||||
// the values to the cloud provider.
|
|
||||||
for parameter, value := range v.options.Parameters {
|
|
||||||
switch strings.ToLower(parameter) {
|
|
||||||
case diskformat:
|
|
||||||
volumeOptions.DiskFormat = value
|
|
||||||
case datastore:
|
|
||||||
volumeOptions.Datastore = value
|
|
||||||
case volume.VolumeParameterFSType:
|
|
||||||
fstype = value
|
|
||||||
klog.V(4).Infof("Setting fstype as %q", fstype)
|
|
||||||
case StoragePolicyName:
|
|
||||||
volumeOptions.StoragePolicyName = value
|
|
||||||
klog.V(4).Infof("Setting StoragePolicyName as %q", volumeOptions.StoragePolicyName)
|
|
||||||
case HostFailuresToTolerateCapability, ForceProvisioningCapability,
|
|
||||||
CacheReservationCapability, DiskStripesCapability,
|
|
||||||
ObjectSpaceReservationCapability, IopsLimitCapability:
|
|
||||||
capabilityData, err := validateVSANCapability(strings.ToLower(parameter), value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
volumeOptions.VSANStorageProfileData += capabilityData
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid option %q for volume plugin %s", parameter, v.plugin.GetPluginName())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if volumeOptions.VSANStorageProfileData != "" {
|
|
||||||
if volumeOptions.StoragePolicyName != "" {
|
|
||||||
return nil, fmt.Errorf("cannot specify storage policy capabilities along with storage policy name. Please specify only one")
|
|
||||||
}
|
|
||||||
volumeOptions.VSANStorageProfileData = "(" + volumeOptions.VSANStorageProfileData + ")"
|
|
||||||
}
|
|
||||||
klog.V(4).Infof("VSANStorageProfileData in vsphere volume %q", volumeOptions.VSANStorageProfileData)
|
|
||||||
// TODO: implement PVC.Selector parsing
|
|
||||||
if v.options.PVC.Spec.Selector != nil {
|
|
||||||
return nil, fmt.Errorf("claim.Spec.Selector is not supported for dynamic provisioning on vSphere")
|
|
||||||
}
|
|
||||||
|
|
||||||
vmDiskPath, err := cloud.CreateVolume(volumeOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
labels, err := cloud.GetVolumeLabels(vmDiskPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
volSpec = &VolumeSpec{
|
|
||||||
Path: vmDiskPath,
|
|
||||||
Size: volSizeKiB,
|
|
||||||
Fstype: fstype,
|
|
||||||
StoragePolicyName: volumeOptions.StoragePolicyName,
|
|
||||||
StoragePolicyID: volumeOptions.StoragePolicyID,
|
|
||||||
Labels: labels,
|
|
||||||
}
|
|
||||||
klog.V(2).Infof("Successfully created vsphere volume %s", name)
|
|
||||||
return volSpec, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteVolume deletes a vSphere volume.
|
|
||||||
func (util *VsphereDiskUtil) DeleteVolume(vd *vsphereVolumeDeleter) error {
|
|
||||||
cloud, err := getCloudProvider(vd.plugin.host.GetCloudProvider())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = cloud.DeleteVolume(vd.volPath); err != nil {
|
|
||||||
klog.V(2).Infof("Error deleting vsphere volume %s: %v", vd.volPath, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
klog.V(2).Infof("Successfully deleted vsphere volume %s", vd.volPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getVolPathfromVolumeName(deviceMountPath string) string {
|
|
||||||
// Assumption: No file or folder is named starting with '[' in datastore
|
|
||||||
volPath := deviceMountPath[strings.LastIndex(deviceMountPath, "["):]
|
|
||||||
// space between datastore and vmdk name in volumePath is encoded as '\040' when returned by GetMountRefs().
|
|
||||||
// volumePath eg: "[local] xxx.vmdk" provided to attach/mount
|
|
||||||
// replacing \040 with space to match the actual volumePath
|
|
||||||
return strings.Replace(volPath, "\\040", " ", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCloudProvider(cloud cloudprovider.Interface) (*vsphere.VSphere, error) {
|
|
||||||
if cloud == nil {
|
|
||||||
klog.Errorf("Cloud provider not initialized properly")
|
|
||||||
return nil, errors.New("cloud provider not initialized properly")
|
|
||||||
}
|
|
||||||
|
|
||||||
vs, ok := cloud.(*vsphere.VSphere)
|
|
||||||
if !ok || vs == nil {
|
|
||||||
return nil, errors.New("invalid cloud provider: expected vSphere")
|
|
||||||
}
|
|
||||||
return vs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the capability requirement for the user specified policy attributes.
|
|
||||||
func validateVSANCapability(capabilityName string, capabilityValue string) (string, error) {
|
|
||||||
var capabilityData string
|
|
||||||
capabilityIntVal, ok := verifyCapabilityValueIsInteger(capabilityValue)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("invalid value for %s. The capabilityValue: %s must be a valid integer value", capabilityName, capabilityValue)
|
|
||||||
}
|
|
||||||
switch strings.ToLower(capabilityName) {
|
|
||||||
case HostFailuresToTolerateCapability:
|
|
||||||
if capabilityIntVal >= HostFailuresToTolerateCapabilityMin && capabilityIntVal <= HostFailuresToTolerateCapabilityMax {
|
|
||||||
capabilityData = " (\"hostFailuresToTolerate\" i" + capabilityValue + ")"
|
|
||||||
} else {
|
|
||||||
return "", fmt.Errorf(`invalid value for hostFailuresToTolerate.
|
|
||||||
The default value is %d, minimum value is %d and maximum value is %d`,
|
|
||||||
1, HostFailuresToTolerateCapabilityMin, HostFailuresToTolerateCapabilityMax)
|
|
||||||
}
|
|
||||||
case ForceProvisioningCapability:
|
|
||||||
if capabilityIntVal >= ForceProvisioningCapabilityMin && capabilityIntVal <= ForceProvisioningCapabilityMax {
|
|
||||||
capabilityData = " (\"forceProvisioning\" i" + capabilityValue + ")"
|
|
||||||
} else {
|
|
||||||
return "", fmt.Errorf(`invalid value for forceProvisioning.
|
|
||||||
The value can be either %d or %d`,
|
|
||||||
ForceProvisioningCapabilityMin, ForceProvisioningCapabilityMax)
|
|
||||||
}
|
|
||||||
case CacheReservationCapability:
|
|
||||||
if capabilityIntVal >= CacheReservationCapabilityMin && capabilityIntVal <= CacheReservationCapabilityMax {
|
|
||||||
capabilityData = " (\"cacheReservation\" i" + strconv.Itoa(capabilityIntVal*10000) + ")"
|
|
||||||
} else {
|
|
||||||
return "", fmt.Errorf(`invalid value for cacheReservation.
|
|
||||||
The minimum percentage is %d and maximum percentage is %d`,
|
|
||||||
CacheReservationCapabilityMin, CacheReservationCapabilityMax)
|
|
||||||
}
|
|
||||||
case DiskStripesCapability:
|
|
||||||
if capabilityIntVal >= DiskStripesCapabilityMin && capabilityIntVal <= DiskStripesCapabilityMax {
|
|
||||||
capabilityData = " (\"stripeWidth\" i" + capabilityValue + ")"
|
|
||||||
} else {
|
|
||||||
return "", fmt.Errorf(`invalid value for diskStripes.
|
|
||||||
The minimum value is %d and maximum value is %d`,
|
|
||||||
DiskStripesCapabilityMin, DiskStripesCapabilityMax)
|
|
||||||
}
|
|
||||||
case ObjectSpaceReservationCapability:
|
|
||||||
if capabilityIntVal >= ObjectSpaceReservationCapabilityMin && capabilityIntVal <= ObjectSpaceReservationCapabilityMax {
|
|
||||||
capabilityData = " (\"proportionalCapacity\" i" + capabilityValue + ")"
|
|
||||||
} else {
|
|
||||||
return "", fmt.Errorf(`invalid value for ObjectSpaceReservation.
|
|
||||||
The minimum percentage is %d and maximum percentage is %d`,
|
|
||||||
ObjectSpaceReservationCapabilityMin, ObjectSpaceReservationCapabilityMax)
|
|
||||||
}
|
|
||||||
case IopsLimitCapability:
|
|
||||||
if capabilityIntVal >= IopsLimitCapabilityMin {
|
|
||||||
capabilityData = " (\"iopsLimit\" i" + capabilityValue + ")"
|
|
||||||
} else {
|
|
||||||
return "", fmt.Errorf(`invalid value for iopsLimit.
|
|
||||||
The value should be greater than %d`, IopsLimitCapabilityMin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return capabilityData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify if the capability value is of type integer.
|
|
||||||
func verifyCapabilityValueIsInteger(capabilityValue string) (int, bool) {
|
|
||||||
i, err := strconv.Atoi(capabilityValue)
|
|
||||||
if err != nil {
|
|
||||||
return -1, false
|
|
||||||
}
|
|
||||||
return i, true
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
//go:build !providerless && linux
|
|
||||||
// +build !providerless,linux
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2019 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 vsphere_volume
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"k8s.io/mount-utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func verifyDevicePath(path string) (string, error) {
|
|
||||||
if pathExists, err := mount.PathExists(path); err != nil {
|
|
||||||
return "", fmt.Errorf("error checking if path exists: %w", err)
|
|
||||||
} else if pathExists {
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
//go:build !providerless && !linux && !windows
|
|
||||||
// +build !providerless,!linux,!windows
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2019 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 vsphere_volume
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
func verifyDevicePath(path string) (string, error) {
|
|
||||||
return "", errors.New("unsupported")
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
//go:build !providerless && windows
|
|
||||||
// +build !providerless,windows
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2019 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 vsphere_volume
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"k8s.io/klog/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type diskInfoResult struct {
|
|
||||||
Number json.Number
|
|
||||||
SerialNumber string
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyDevicePath(path string) (string, error) {
|
|
||||||
if !strings.Contains(path, diskByIDPath) {
|
|
||||||
// If this volume has already been mounted then
|
|
||||||
// its devicePath will have already been converted to a disk number
|
|
||||||
klog.V(4).Infof("Found vSphere disk attached with disk number %v", path)
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
// NOTE: If a powershell command that would return an array (e.g.: Get-Disk) would return an array of
|
|
||||||
// one element, powershell will in fact return that object directly, and **not an array containing
|
|
||||||
// that elemenent, which means piping it to ConvertTo-Json would not result in array as expected below.
|
|
||||||
// The following syntax forces it to always be an array.
|
|
||||||
cmd := exec.Command("powershell", "/c", "Get-Disk | Select Number, SerialNumber | ConvertTo-JSON")
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
klog.Errorf("Get-Disk failed, error: %v, output: %q", err, string(output))
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []diskInfoResult
|
|
||||||
if err = json.Unmarshal(output, &results); err != nil {
|
|
||||||
klog.Errorf("Failed to unmarshal Get-Disk json, output: %q", string(output))
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
serialNumber := strings.TrimPrefix(path, diskByIDPath+diskSCSIPrefix)
|
|
||||||
for _, v := range results {
|
|
||||||
if v.SerialNumber == serialNumber {
|
|
||||||
klog.V(4).Infof("Found vSphere disk attached with serial %v", serialNumber)
|
|
||||||
return v.Number.String(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("unable to find vSphere disk with serial %v", serialNumber)
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
//go:build !providerless && windows
|
|
||||||
// +build !providerless,windows
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright 2022 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 vsphere_volume
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFormatIfNotFormatted(t *testing.T) {
|
|
||||||
// If this volume has already been mounted then
|
|
||||||
// its devicePath will have already been converted to a disk number,
|
|
||||||
// meaning that the original path is returned.
|
|
||||||
devPath, err := verifyDevicePath("foo")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "foo", devPath)
|
|
||||||
|
|
||||||
// Won't match any serial number, meaning that an error will be returned.
|
|
||||||
devPath, err = verifyDevicePath(diskByIDPath + diskSCSIPrefix + "fake-serial")
|
|
||||||
expectedErrMsg := `unable to find vSphere disk with serial fake-serial`
|
|
||||||
if err == nil || err.Error() != expectedErrMsg {
|
|
||||||
t.Errorf("expected error message `%s` but got `%v`", expectedErrMsg, err)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user