FC plugin: Support WWID for volume identifier

This PR adds World Wide Identifier (WWID) parameter to
FCVolumeSource as an unique volume identifier.

fixes #48639
This commit is contained in:
mtanino 2017-07-10 19:51:24 -04:00
parent 4dc4c3c4a4
commit 03e28476c4
9 changed files with 308 additions and 53 deletions

View File

@ -715,9 +715,11 @@ type ISCSIVolumeSource struct {
// Fibre Channel volumes can only be mounted as read/write once.
// Fibre Channel volumes support ownership management and SELinux relabeling.
type FCVolumeSource struct {
// Required: FC target worldwide names (WWNs)
// Optional: FC target worldwide names (WWNs)
// +optional
TargetWWNs []string
// Required: FC target lun number
// Optional: FC target lun number
// +optional
Lun *int32
// Filesystem type to mount.
// Must be a filesystem type supported by the host operating system.
@ -729,6 +731,10 @@ type FCVolumeSource struct {
// the ReadOnly setting in VolumeMounts.
// +optional
ReadOnly bool
// Optional: FC volume World Wide Identifiers (WWIDs)
// Either WWIDs or TargetWWNs and Lun must be set, but not both simultaneously.
// +optional
WWIDs []string
}
// FlexVolume represents a generic volume resource that is

View File

@ -648,15 +648,21 @@ func validateISCSIVolumeSource(iscsi *api.ISCSIVolumeSource, fldPath *field.Path
func validateFCVolumeSource(fc *api.FCVolumeSource, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(fc.TargetWWNs) < 1 {
allErrs = append(allErrs, field.Required(fldPath.Child("targetWWNs"), ""))
if len(fc.TargetWWNs) < 1 && len(fc.WWIDs) < 1 {
allErrs = append(allErrs, field.Required(fldPath.Child("targetWWNs"), "must specify either targetWWNs or wwids, but not both"))
}
if fc.Lun == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("lun"), ""))
} else {
if *fc.Lun < 0 || *fc.Lun > 255 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("lun"), fc.Lun, validation.InclusiveRangeError(0, 255)))
if len(fc.TargetWWNs) != 0 && len(fc.WWIDs) != 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("targetWWNs"), fc.TargetWWNs, "targetWWNs and wwids can not be specified simultaneously"))
}
if len(fc.TargetWWNs) != 0 {
if fc.Lun == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("lun"), "lun is required if targetWWNs is specified"))
} else {
if *fc.Lun < 0 || *fc.Lun > 255 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("lun"), fc.Lun, validation.InclusiveRangeError(0, 255)))
}
}
}
return allErrs

View File

@ -27,6 +27,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/api"
@ -2099,7 +2100,7 @@ func TestValidateVolumes(t *testing.T) {
},
// FC
{
name: "valid FC",
name: "FC valid targetWWNs and lun",
vol: api.Volume{
Name: "fc",
VolumeSource: api.VolumeSource{
@ -2113,23 +2114,56 @@ func TestValidateVolumes(t *testing.T) {
},
},
{
name: "fc empty wwn",
name: "FC valid wwids",
vol: api.Volume{
Name: "fc",
VolumeSource: api.VolumeSource{
FC: &api.FCVolumeSource{
WWIDs: []string{"some_wwid"},
FSType: "ext4",
ReadOnly: false,
},
},
},
},
{
name: "FC empty targetWWNs and wwids",
vol: api.Volume{
Name: "fc",
VolumeSource: api.VolumeSource{
FC: &api.FCVolumeSource{
TargetWWNs: []string{},
Lun: newInt32(1),
WWIDs: []string{},
FSType: "ext4",
ReadOnly: false,
},
},
},
errtype: field.ErrorTypeRequired,
errfield: "fc.targetWWNs",
errtype: field.ErrorTypeRequired,
errfield: "fc.targetWWNs",
errdetail: "must specify either targetWWNs or wwids",
},
{
name: "fc empty lun",
name: "FC invalid: both targetWWNs and wwids simultaneously",
vol: api.Volume{
Name: "fc",
VolumeSource: api.VolumeSource{
FC: &api.FCVolumeSource{
TargetWWNs: []string{"some_wwn"},
Lun: newInt32(1),
WWIDs: []string{"some_wwid"},
FSType: "ext4",
ReadOnly: false,
},
},
},
errtype: field.ErrorTypeInvalid,
errfield: "fc.targetWWNs",
errdetail: "targetWWNs and wwids can not be specified simultaneously",
},
{
name: "FC valid targetWWNs and empty lun",
vol: api.Volume{
Name: "fc",
VolumeSource: api.VolumeSource{
@ -2141,8 +2175,26 @@ func TestValidateVolumes(t *testing.T) {
},
},
},
errtype: field.ErrorTypeRequired,
errfield: "fc.lun",
errtype: field.ErrorTypeRequired,
errfield: "fc.lun",
errdetail: "lun is required if targetWWNs is specified",
},
{
name: "FC valid targetWWNs and invalid lun",
vol: api.Volume{
Name: "fc",
VolumeSource: api.VolumeSource{
FC: &api.FCVolumeSource{
TargetWWNs: []string{"wwn"},
Lun: newInt32(256),
FSType: "ext4",
ReadOnly: false,
},
},
},
errtype: field.ErrorTypeInvalid,
errfield: "fc.lun",
errdetail: validation.InclusiveRangeError(0, 255),
},
// FlexVolume
{

View File

@ -20,6 +20,7 @@ import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/golang/glog"
@ -167,18 +168,27 @@ func volumeSpecToMounter(spec *volume.Spec, host volume.VolumeHost) (*fcDiskMoun
if err != nil {
return nil, err
}
if fc.Lun == nil {
return nil, fmt.Errorf("empty lun")
var lun string
var wwids []string
if fc.Lun != nil && len(fc.TargetWWNs) != 0 {
lun = strconv.Itoa(int(*fc.Lun))
} else if len(fc.WWIDs) != 0 {
for _, wwid := range fc.WWIDs {
wwids = append(wwids, strings.Replace(wwid, " ", "_", -1))
}
} else {
return nil, fmt.Errorf("fc: no fc disk information found. failed to make a new mounter")
}
lun := strconv.Itoa(int(*fc.Lun))
return &fcDiskMounter{
fcDisk: &fcDisk{
plugin: &fcPlugin{
host: host,
},
wwns: fc.TargetWWNs,
lun: lun,
io: &osIOHandler{},
wwns: fc.TargetWWNs,
lun: lun,
wwids: wwids,
io: &osIOHandler{},
},
fsType: fc.FSType,
readOnly: readOnly,

View File

@ -19,12 +19,13 @@ package fc
import (
"fmt"
"strconv"
"strings"
"github.com/golang/glog"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubernetes/pkg/util/mount"
"k8s.io/kubernetes/pkg/util/strings"
utilstrings "k8s.io/kubernetes/pkg/util/strings"
"k8s.io/kubernetes/pkg/volume"
"k8s.io/kubernetes/pkg/volume/util"
"k8s.io/utils/exec"
@ -62,8 +63,15 @@ func (plugin *fcPlugin) GetVolumeName(spec *volume.Spec) (string, error) {
return "", err
}
// TargetWWNs are the FibreChannel target worldwide names
return fmt.Sprintf("%v", volumeSource.TargetWWNs), nil
if len(volumeSource.TargetWWNs) != 0 {
// TargetWWNs are the FibreChannel target worldwide names
return fmt.Sprintf("%v", volumeSource.TargetWWNs), nil
} else if len(volumeSource.WWIDs) != 0 {
// WWIDs are the FibreChannel World Wide Identifiers
return fmt.Sprintf("%v", volumeSource.WWIDs), nil
}
return "", err
}
func (plugin *fcPlugin) CanSupport(spec *volume.Spec) bool {
@ -106,18 +114,25 @@ func (plugin *fcPlugin) newMounterInternal(spec *volume.Spec, podUID types.UID,
return nil, err
}
if fc.Lun == nil {
return nil, fmt.Errorf("empty lun")
var lun string
var wwids []string
if fc.Lun != nil && len(fc.TargetWWNs) != 0 {
lun = strconv.Itoa(int(*fc.Lun))
} else if len(fc.WWIDs) != 0 {
for _, wwid := range fc.WWIDs {
wwids = append(wwids, strings.Replace(wwid, " ", "_", -1))
}
} else {
return nil, fmt.Errorf("fc: no fc disk information found. failed to make a new mounter")
}
lun := strconv.Itoa(int(*fc.Lun))
return &fcDiskMounter{
fcDisk: &fcDisk{
podUID: podUID,
volName: spec.Name(),
wwns: fc.TargetWWNs,
lun: lun,
wwids: wwids,
manager: manager,
io: &osIOHandler{},
plugin: plugin},
@ -166,6 +181,7 @@ type fcDisk struct {
portal string
wwns []string
lun string
wwids []string
plugin *fcPlugin
// Utility interface that provides API calls to the provider to attach/detach disks.
manager diskManager
@ -177,7 +193,7 @@ type fcDisk struct {
func (fc *fcDisk) GetPath() string {
name := fcPluginName
// safe to use PodVolumeDir now: volume teardown occurs before pod is cleaned up
return fc.plugin.host.GetPodVolumeDir(fc.podUID, strings.EscapeQualifiedNameForDisk(name), fc.volName)
return fc.plugin.host.GetPodVolumeDir(fc.podUID, utilstrings.EscapeQualifiedNameForDisk(name), fc.volName)
}
type fcDiskMounter struct {

View File

@ -193,13 +193,39 @@ func doTestPlugin(t *testing.T, spec *volume.Spec) {
}
}
func doTestPluginNilMounter(t *testing.T, spec *volume.Spec) {
tmpDir, err := utiltesting.MkTmpdir("fc_test")
if err != nil {
t.Fatalf("error creating temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
plugMgr := volume.VolumePluginMgr{}
plugMgr.InitPlugins(ProbeVolumePlugins(), volumetest.NewFakeVolumeHost(tmpDir, nil, nil))
plug, err := plugMgr.FindPluginByName("kubernetes.io/fc")
if err != nil {
t.Errorf("Can't find the plugin by name")
}
fakeManager := NewFakeDiskManager()
defer fakeManager.Cleanup()
fakeMounter := &mount.FakeMounter{}
mounter, err := plug.(*fcPlugin).newMounterInternal(spec, types.UID("poduid"), fakeManager, fakeMounter)
if err == nil {
t.Errorf("Error failed to make a new Mounter is expected: %v", err)
}
if mounter != nil {
t.Errorf("A nil Mounter is expected: %v", err)
}
}
func TestPluginVolume(t *testing.T) {
lun := int32(0)
vol := &v1.Volume{
Name: "vol1",
VolumeSource: v1.VolumeSource{
FC: &v1.FCVolumeSource{
TargetWWNs: []string{"some_wwn"},
TargetWWNs: []string{"500a0981891b8dc5"},
FSType: "ext4",
Lun: &lun,
},
@ -217,7 +243,7 @@ func TestPluginPersistentVolume(t *testing.T) {
Spec: v1.PersistentVolumeSpec{
PersistentVolumeSource: v1.PersistentVolumeSource{
FC: &v1.FCVolumeSource{
TargetWWNs: []string{"some_wwn"},
TargetWWNs: []string{"500a0981891b8dc5"},
FSType: "ext4",
Lun: &lun,
},
@ -227,6 +253,64 @@ func TestPluginPersistentVolume(t *testing.T) {
doTestPlugin(t, volume.NewSpecFromPersistentVolume(vol, false))
}
func TestPluginVolumeWWIDs(t *testing.T) {
vol := &v1.Volume{
Name: "vol1",
VolumeSource: v1.VolumeSource{
FC: &v1.FCVolumeSource{
WWIDs: []string{"3600508b400105e210000900000490000"},
FSType: "ext4",
},
},
}
doTestPlugin(t, volume.NewSpecFromVolume(vol))
}
func TestPluginPersistentVolumeWWIDs(t *testing.T) {
vol := &v1.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "vol1",
},
Spec: v1.PersistentVolumeSpec{
PersistentVolumeSource: v1.PersistentVolumeSource{
FC: &v1.FCVolumeSource{
WWIDs: []string{"3600508b400105e21 000900000490000"},
FSType: "ext4",
},
},
},
}
doTestPlugin(t, volume.NewSpecFromPersistentVolume(vol, false))
}
func TestPluginVolumeNoDiskInfo(t *testing.T) {
vol := &v1.Volume{
Name: "vol1",
VolumeSource: v1.VolumeSource{
FC: &v1.FCVolumeSource{
FSType: "ext4",
},
},
}
doTestPluginNilMounter(t, volume.NewSpecFromVolume(vol))
}
func TestPluginPersistentVolumeNoDiskInfo(t *testing.T) {
vol := &v1.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "vol1",
},
Spec: v1.PersistentVolumeSpec{
PersistentVolumeSource: v1.PersistentVolumeSource{
FC: &v1.FCVolumeSource{
FSType: "ext4",
},
},
},
}
doTestPluginNilMounter(t, volume.NewSpecFromPersistentVolume(vol, false))
}
func TestPersistentClaimReadOnlyFlag(t *testing.T) {
tmpDir, err := utiltesting.MkTmpdir("fc_test")
if err != nil {

View File

@ -89,6 +89,40 @@ func findDisk(wwn, lun string, io ioHandler) (string, string) {
return "", ""
}
// given a wwid, find the device and associated devicemapper parent
func findDiskWWIDs(wwid string, io ioHandler) (string, string) {
// Example wwid format:
// 3600508b400105e210000900000490000
// <VENDOR NAME> <IDENTIFIER NUMBER>
// Example of symlink under by-id:
// /dev/by-id/scsi-3600508b400105e210000900000490000
// /dev/by-id/scsi-<VENDOR NAME>_<IDENTIFIER NUMBER>
// The wwid could contain white space and it will be replaced
// underscore when wwid is exposed under /dev/by-id.
fc_path := "scsi-" + wwid
dev_id := "/dev/disk/by-id/"
if dirs, err := io.ReadDir(dev_id); err == nil {
for _, f := range dirs {
name := f.Name()
if name == fc_path {
disk, err := io.EvalSymlinks(dev_id + name)
if err != nil {
glog.V(2).Infof("fc: failed to find a corresponding disk from symlink[%s], error %v", dev_id+name, err)
return "", ""
}
arr := strings.Split(disk, "/")
l := len(arr) - 1
dev := arr[l]
dm := findMultipathDeviceMapper(dev, io)
return disk, dm
}
}
}
glog.V(2).Infof("fc: failed to find a disk [%s]", dev_id+fc_path)
return "", ""
}
// Removes a scsi device based upon /dev/sdX name
func removeFromScsiSubsystem(deviceName string, io ioHandler) {
fileName := "/sys/block/" + deviceName + "/device/delete"
@ -110,27 +144,46 @@ func scsiHostRescan(io ioHandler) {
}
// make a directory like /var/lib/kubelet/plugins/kubernetes.io/pod/fc/target-lun-0
func makePDNameInternal(host volume.VolumeHost, wwns []string, lun string) string {
return path.Join(host.GetPluginDir(fcPluginName), wwns[0]+"-lun-"+lun)
func makePDNameInternal(host volume.VolumeHost, wwns []string, lun string, wwids []string) string {
if len(wwns) != 0 {
return path.Join(host.GetPluginDir(fcPluginName), wwns[0]+"-lun-"+lun)
} else {
return path.Join(host.GetPluginDir(fcPluginName), wwids[0])
}
}
type FCUtil struct{}
func (util *FCUtil) MakeGlobalPDName(fc fcDisk) string {
return makePDNameInternal(fc.plugin.host, fc.wwns, fc.lun)
return makePDNameInternal(fc.plugin.host, fc.wwns, fc.lun, fc.wwids)
}
func searchDisk(wwns []string, lun string, io ioHandler) (string, string) {
disk := ""
dm := ""
func searchDisk(b fcDiskMounter) (string, string) {
var diskIds []string
var disk string
var dm string
io := b.io
wwids := b.wwids
wwns := b.wwns
lun := b.lun
if len(wwns) != 0 {
diskIds = wwns
} else {
diskIds = wwids
}
rescaned := false
// two-phase search:
// first phase, search existing device path, if a multipath dm is found, exit loop
// otherwise, in second phase, rescan scsi bus and search again, return with any findings
for true {
for _, wwn := range wwns {
disk, dm = findDisk(wwn, lun, io)
for _, diskId := range diskIds {
if len(wwns) != 0 {
disk, dm = findDisk(diskId, lun, io)
} else {
disk, dm = findDiskWWIDs(diskId, io)
}
// if multipath device is found, break
if dm != "" {
break
@ -150,10 +203,9 @@ func searchDisk(wwns []string, lun string, io ioHandler) (string, string) {
func (util *FCUtil) AttachDisk(b fcDiskMounter) (string, error) {
devicePath := ""
wwns := b.wwns
lun := b.lun
io := b.io
disk, dm := searchDisk(wwns, lun, io)
var disk, dm string
disk, dm = searchDisk(b)
// if no disk matches input wwn and lun, exit
if disk == "" && dm == "" {
return "", fmt.Errorf("no fc disk found")

View File

@ -63,6 +63,11 @@ func (handler *fakeIOHandler) ReadDir(dirname string) ([]os.FileInfo, error) {
name: "dm-1",
}
return []os.FileInfo{f}, nil
case "/dev/disk/by-id/":
f := &fakeFileInfo{
name: "scsi-3600508b400105e210000900000490000",
}
return []os.FileInfo{f}, nil
}
return nil, nil
}
@ -79,13 +84,31 @@ func (handler *fakeIOHandler) WriteFile(filename string, data []byte, perm os.Fi
return nil
}
func TestIoHandler(t *testing.T) {
io := &fakeIOHandler{}
wwns := []string{"500a0981891b8dc5"}
lun := "0"
disk, dm := searchDisk(wwns, lun, io)
func TestSearchDisk(t *testing.T) {
fakeMounter := fcDiskMounter{
fcDisk: &fcDisk{
wwns: []string{"500a0981891b8dc5"},
lun: "0",
io: &fakeIOHandler{},
},
}
disk, dm := searchDisk(fakeMounter)
// if no disk matches input wwn and lun, exit
if disk == "" && dm == "" {
t.Errorf("no fc disk found")
}
}
func TestSearchDiskWWID(t *testing.T) {
fakeMounter := fcDiskMounter{
fcDisk: &fcDisk{
wwids: []string{"3600508b400105e210000900000490000"},
io: &fakeIOHandler{},
},
}
disk, dm := searchDisk(fakeMounter)
// if no disk matches input wwid, exit
if disk == "" && dm == "" {
t.Errorf("no fc disk found")
}
}

View File

@ -1102,10 +1102,12 @@ type ISCSIVolumeSource struct {
// Fibre Channel volumes can only be mounted as read/write once.
// Fibre Channel volumes support ownership management and SELinux relabeling.
type FCVolumeSource struct {
// Required: FC target worldwide names (WWNs)
TargetWWNs []string `json:"targetWWNs" protobuf:"bytes,1,rep,name=targetWWNs"`
// Required: FC target lun number
Lun *int32 `json:"lun" protobuf:"varint,2,opt,name=lun"`
// Optional: FC target worldwide names (WWNs)
// +optional
TargetWWNs []string `json:"targetWWNs,omitempty" protobuf:"bytes,1,rep,name=targetWWNs"`
// Optional: FC target lun number
// +optional
Lun *int32 `json:"lun,omitempty" protobuf:"varint,2,opt,name=lun"`
// Filesystem type to mount.
// Must be a filesystem type supported by the host operating system.
// Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified.
@ -1116,6 +1118,10 @@ type FCVolumeSource struct {
// the ReadOnly setting in VolumeMounts.
// +optional
ReadOnly bool `json:"readOnly,omitempty" protobuf:"varint,4,opt,name=readOnly"`
// Optional: FC volume world wide identifiers (wwids)
// Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously.
// +optional
WWIDs []string `json:"wwids,omitempty" protobuf:"bytes,5,rep,name=wwids"`
}
// AzureFile represents an Azure File Service mount on the host and bind mount to the pod.