From a6e523f2e7a2b50b26fcea99c88c66f7c65a2e3b Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanino Date: Tue, 11 Jul 2017 23:37:48 -0400 Subject: [PATCH] iSCSI volume plugin: iSCSI initiatorname support This PR adds iSCSI initiatorname parameter to ISCSIVolumeSource to enable automatic configuration of initiator name per volume. This would allow for more fine grained configuration, and remove the need to configure the initiator name on the host by administrator. fixes: #47311 --- pkg/api/types.go | 5 + pkg/api/validation/validation.go | 39 +++++- pkg/api/validation/validation_test.go | 121 +++++++++++++++- pkg/volume/iscsi/disk_manager.go | 2 +- pkg/volume/iscsi/iscsi.go | 7 + pkg/volume/iscsi/iscsi_util.go | 90 +++++++++++- pkg/volume/iscsi/iscsi_util_test.go | 178 ++++++++++++++++++++++++ staging/src/k8s.io/api/core/v1/types.go | 5 + 8 files changed, 440 insertions(+), 7 deletions(-) diff --git a/pkg/api/types.go b/pkg/api/types.go index 4d626b5d095..f281ee71ad2 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -709,6 +709,11 @@ type ISCSIVolumeSource struct { // The secret is used if either DiscoveryCHAPAuth or SessionCHAPAuth is true // +optional SecretRef *LocalObjectReference + // Optional: Custom initiator name per volume. + // If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + // : will be created for the connection. + // +optional + InitiatorName *string } // Represents a Fibre Channel volume. diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 85b6b82e167..6e9d11d03d8 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -69,6 +69,10 @@ var volumeModeErrorMsg string = "must be a number between 0 and 0777 (octal), bo // BannedOwners is a black list of object that are not allowed to be owners. var BannedOwners = genericvalidation.BannedOwners +var iscsiInitiatorIqnRegex = regexp.MustCompile(`iqn\.\d{4}-\d{2}\.([[:alnum:]-.]+)(:[^,;*&$|\s]+)$`) +var iscsiInitiatorEuiRegex = regexp.MustCompile(`^eui.[[:alnum:]]{16}$`) +var iscsiInitiatorNaaRegex = regexp.MustCompile(`^naa.[[:alnum:]]{32}$`) + // ValidateHasLabel requires that metav1.ObjectMeta has a Label with key and expectedValue func ValidateHasLabel(meta metav1.ObjectMeta, fldPath *field.Path, key, expectedValue string) field.ErrorList { allErrs := field.ErrorList{} @@ -358,7 +362,7 @@ func ValidateVolumes(volumes []api.Volume, fldPath *field.Path) (sets.String, fi for i, vol := range volumes { idxPath := fldPath.Index(i) namePath := idxPath.Child("name") - el := validateVolumeSource(&vol.VolumeSource, idxPath) + el := validateVolumeSource(&vol.VolumeSource, idxPath, vol.Name) if len(vol.Name) == 0 { el = append(el, field.Required(namePath, "")) } else { @@ -377,7 +381,7 @@ func ValidateVolumes(volumes []api.Volume, fldPath *field.Path) (sets.String, fi return allNames, allErrs } -func validateVolumeSource(source *api.VolumeSource, fldPath *field.Path) field.ErrorList { +func validateVolumeSource(source *api.VolumeSource, fldPath *field.Path, volName string) field.ErrorList { numVolumes := 0 allErrs := field.ErrorList{} if source.EmptyDir != nil { @@ -444,6 +448,10 @@ func validateVolumeSource(source *api.VolumeSource, fldPath *field.Path) field.E numVolumes++ allErrs = append(allErrs, validateISCSIVolumeSource(source.ISCSI, fldPath.Child("iscsi"))...) } + if source.ISCSI.InitiatorName != nil && len(volName+":"+source.ISCSI.TargetPortal) > 64 { + tooLongErr := "Total length of : must be under 64 characters if iscsi.initiatorName is specified." + allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), volName, tooLongErr)) + } } if source.Glusterfs != nil { if numVolumes > 0 { @@ -636,6 +644,16 @@ func validateISCSIVolumeSource(iscsi *api.ISCSIVolumeSource, fldPath *field.Path } if len(iscsi.IQN) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("iqn"), "")) + } else { + if !strings.HasPrefix(iscsi.IQN, "iqn") && !strings.HasPrefix(iscsi.IQN, "eui") && !strings.HasPrefix(iscsi.IQN, "naa") { + allErrs = append(allErrs, field.Invalid(fldPath.Child("iqn"), iscsi.IQN, "must be valid format")) + } else if strings.HasPrefix(iscsi.IQN, "iqn") && !iscsiInitiatorIqnRegex.MatchString(iscsi.IQN) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("iqn"), iscsi.IQN, "must be valid format")) + } else if strings.HasPrefix(iscsi.IQN, "eui") && !iscsiInitiatorEuiRegex.MatchString(iscsi.IQN) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("iqn"), iscsi.IQN, "must be valid format")) + } else if strings.HasPrefix(iscsi.IQN, "naa") && !iscsiInitiatorNaaRegex.MatchString(iscsi.IQN) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("iqn"), iscsi.IQN, "must be valid format")) + } } if iscsi.Lun < 0 || iscsi.Lun > 255 { allErrs = append(allErrs, field.Invalid(fldPath.Child("lun"), iscsi.Lun, validation.InclusiveRangeError(0, 255))) @@ -643,6 +661,19 @@ func validateISCSIVolumeSource(iscsi *api.ISCSIVolumeSource, fldPath *field.Path if (iscsi.DiscoveryCHAPAuth || iscsi.SessionCHAPAuth) && iscsi.SecretRef == nil { allErrs = append(allErrs, field.Required(fldPath.Child("secretRef"), "")) } + if iscsi.InitiatorName != nil { + initiator := *iscsi.InitiatorName + if !strings.HasPrefix(initiator, "iqn") && !strings.HasPrefix(initiator, "eui") && !strings.HasPrefix(initiator, "naa") { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initiatorname"), initiator, "must be valid format")) + } + if strings.HasPrefix(initiator, "iqn") && !iscsiInitiatorIqnRegex.MatchString(initiator) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initiatorname"), initiator, "must be valid format")) + } else if strings.HasPrefix(initiator, "eui") && !iscsiInitiatorEuiRegex.MatchString(initiator) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initiatorname"), initiator, "must be valid format")) + } else if strings.HasPrefix(initiator, "naa") && !iscsiInitiatorNaaRegex.MatchString(initiator) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initiatorname"), initiator, "must be valid format")) + } + } return allErrs } @@ -1292,6 +1323,10 @@ func ValidatePersistentVolume(pv *api.PersistentVolume) field.ErrorList { numVolumes++ allErrs = append(allErrs, validateISCSIVolumeSource(pv.Spec.ISCSI, specPath.Child("iscsi"))...) } + if pv.Spec.ISCSI.InitiatorName != nil && len(pv.ObjectMeta.Name+":"+pv.Spec.ISCSI.TargetPortal) > 64 { + tooLongErr := "Total length of : must be under 64 characters if iscsi.initiatorName is specified." + allErrs = append(allErrs, field.Invalid(metaPath.Child("name"), pv.ObjectMeta.Name, tooLongErr)) + } } if pv.Spec.Cinder != nil { if numVolumes > 0 { diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 400dc4a84e0..1a3bac444ad 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -1025,6 +1025,8 @@ func newInt32(val int) *int32 { // type on its own, but we want to also make sure that the logic works through // the one-of wrapper, so we just do it all in one place. func TestValidateVolumes(t *testing.T) { + validInitiatorName := "iqn.2015-02.example.com:init" + invalidInitiatorName := "2015-02.example.com:init" testCases := []struct { name string vol api.Volume @@ -1268,6 +1270,36 @@ func TestValidateVolumes(t *testing.T) { }, }, }, + { + name: "valid IQN: eui format", + vol: api.Volume{ + Name: "iscsi", + VolumeSource: api.VolumeSource{ + ISCSI: &api.ISCSIVolumeSource{ + TargetPortal: "127.0.0.1", + IQN: "eui.0123456789ABCDEF", + Lun: 1, + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + }, + { + name: "valid IQN: naa format", + vol: api.Volume{ + Name: "iscsi", + VolumeSource: api.VolumeSource{ + ISCSI: &api.ISCSIVolumeSource{ + TargetPortal: "127.0.0.1", + IQN: "naa.62004567BA64678D0123456789ABCDEF", + Lun: 1, + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + }, { name: "empty portal", vol: api.Volume{ @@ -1302,6 +1334,91 @@ func TestValidateVolumes(t *testing.T) { errtype: field.ErrorTypeRequired, errfield: "iscsi.iqn", }, + { + name: "invalid IQN: iqn format", + vol: api.Volume{ + Name: "iscsi", + VolumeSource: api.VolumeSource{ + ISCSI: &api.ISCSIVolumeSource{ + TargetPortal: "127.0.0.1", + IQN: "iqn.2015-02.example.com:test;ls;", + Lun: 1, + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "iscsi.iqn", + }, + { + name: "invalid IQN: eui format", + vol: api.Volume{ + Name: "iscsi", + VolumeSource: api.VolumeSource{ + ISCSI: &api.ISCSIVolumeSource{ + TargetPortal: "127.0.0.1", + IQN: "eui.0123456789ABCDEFGHIJ", + Lun: 1, + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "iscsi.iqn", + }, + { + name: "invalid IQN: naa format", + vol: api.Volume{ + Name: "iscsi", + VolumeSource: api.VolumeSource{ + ISCSI: &api.ISCSIVolumeSource{ + TargetPortal: "127.0.0.1", + IQN: "naa.62004567BA_4-78D.123456789ABCDEF", + Lun: 1, + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "iscsi.iqn", + }, + { + name: "valid initiatorName", + vol: api.Volume{ + Name: "iscsi", + VolumeSource: api.VolumeSource{ + ISCSI: &api.ISCSIVolumeSource{ + TargetPortal: "127.0.0.1", + IQN: "iqn.2015-02.example.com:test", + Lun: 1, + InitiatorName: &validInitiatorName, + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + }, + { + name: "invalid initiatorName", + vol: api.Volume{ + Name: "iscsi", + VolumeSource: api.VolumeSource{ + ISCSI: &api.ISCSIVolumeSource{ + TargetPortal: "127.0.0.1", + IQN: "iqn.2015-02.example.com:test", + Lun: 1, + InitiatorName: &invalidInitiatorName, + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "iscsi.initiatorname", + }, { name: "empty secret", vol: api.Volume{ @@ -2475,7 +2592,7 @@ func TestAlphaLocalStorageCapacityIsolation(t *testing.T) { return } for _, tc := range testCases { - if errs := validateVolumeSource(&tc, field.NewPath("spec")); len(errs) != 0 { + if errs := validateVolumeSource(&tc, field.NewPath("spec"), "tmpvol"); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -2486,7 +2603,7 @@ func TestAlphaLocalStorageCapacityIsolation(t *testing.T) { return } for _, tc := range testCases { - if errs := validateVolumeSource(&tc, field.NewPath("spec")); len(errs) == 0 { + if errs := validateVolumeSource(&tc, field.NewPath("spec"), "tmpvol"); len(errs) == 0 { t.Errorf("expected failure: %v", errs) } } diff --git a/pkg/volume/iscsi/disk_manager.go b/pkg/volume/iscsi/disk_manager.go index 2c470b9b1ba..3a89c05b0f9 100644 --- a/pkg/volume/iscsi/disk_manager.go +++ b/pkg/volume/iscsi/disk_manager.go @@ -35,7 +35,6 @@ type diskManager interface { // utility to mount a disk based filesystem func diskSetUp(manager diskManager, b iscsiDiskMounter, volPath string, mounter mount.Interface, fsGroup *int64) error { - globalPDPath := manager.MakeGlobalPDName(*b.iscsiDisk) // TODO: handle failed mounts here. notMnt, err := mounter.IsLikelyNotMountPoint(volPath) @@ -60,6 +59,7 @@ func diskSetUp(manager diskManager, b iscsiDiskMounter, volPath string, mounter if b.readOnly { options = append(options, "ro") } + globalPDPath := manager.MakeGlobalPDName(*b.iscsiDisk) mountOptions := volume.JoinMountOptions(b.mountOptions, options) err = mounter.Mount(globalPDPath, volPath, "", mountOptions) if err != nil { diff --git a/pkg/volume/iscsi/iscsi.go b/pkg/volume/iscsi/iscsi.go index 3e47cc02a98..25f426aa5e1 100644 --- a/pkg/volume/iscsi/iscsi.go +++ b/pkg/volume/iscsi/iscsi.go @@ -132,6 +132,11 @@ func (plugin *iscsiPlugin) newMounterInternal(spec *volume.Spec, podUID types.UI } iface := iscsi.ISCSIInterface + var initiatorName string + if iscsi.InitiatorName != nil { + initiatorName = *iscsi.InitiatorName + } + return &iscsiDiskMounter{ iscsiDisk: &iscsiDisk{ podUID: podUID, @@ -143,6 +148,7 @@ func (plugin *iscsiPlugin) newMounterInternal(spec *volume.Spec, podUID types.UI chap_discovery: iscsi.DiscoveryCHAPAuth, chap_session: iscsi.SessionCHAPAuth, secret: secret, + InitiatorName: initiatorName, manager: manager, plugin: plugin}, fsType: iscsi.FSType, @@ -198,6 +204,7 @@ type iscsiDisk struct { chap_discovery bool chap_session bool secret map[string]string + InitiatorName string plugin *iscsiPlugin // Utility interface that provides API calls to the provider to attach/detach disks. manager diskManager diff --git a/pkg/volume/iscsi/iscsi_util.go b/pkg/volume/iscsi/iscsi_util.go index 9b5d3cb7a59..21e6cdebde3 100755 --- a/pkg/volume/iscsi/iscsi_util.go +++ b/pkg/volume/iscsi/iscsi_util.go @@ -205,6 +205,20 @@ func (util *ISCSIUtil) AttachDisk(b iscsiDiskMounter) error { iscsiTransport = extractTransportname(string(out)) bkpPortal := b.Portals + + // create new iface and copy parameters from pre-configured iface to the created iface + if b.InitiatorName != "" { + // new iface name is : + newIface := bkpPortal[0] + ":" + b.volName + err = cloneIface(b, newIface) + if err != nil { + glog.Errorf("iscsi: failed to clone iface: %s error: %v", b.Iface, err) + return err + } + // update iface name + b.Iface = newIface + } + for _, tp := range bkpPortal { // Rescan sessions to discover newly mapped LUNs. Do not specify the interface when rescanning // to avoid establishing additional sessions to the same target. @@ -268,6 +282,8 @@ func (util *ISCSIUtil) AttachDisk(b iscsiDiskMounter) error { } if len(devicePaths) == 0 { + // delete cloned iface + b.plugin.execCommand("iscsiadm", []string{"-m", "iface", "-I", b.Iface, "-o", "delete"}) glog.Errorf("iscsi: failed to get any path for iscsi disk, last err seen:\n%v", lastErr) return fmt.Errorf("failed to get any path for iscsi disk, last err seen:\n%v", lastErr) } @@ -331,12 +347,13 @@ func (util *ISCSIUtil) DetachDisk(c iscsiDiskUnmounter, mntPath string) error { refCount, err := getDevicePrefixRefCount(c.mounter, prefix) if err == nil && refCount == 0 { var bkpPortal []string - var iqn, iface string + var iqn, iface, initiatorName string found := true // load iscsi disk config from json file if err := util.loadISCSI(c.iscsiDisk, mntPath); err == nil { bkpPortal, iqn, iface = c.iscsiDisk.Portals, c.iscsiDisk.Iqn, c.iscsiDisk.Iface + initiatorName = c.iscsiDisk.InitiatorName } else { // If the iscsi disk config is not found, fall back to the original behavior. // This portal/iqn/iface is no longer referenced, log out. @@ -351,7 +368,12 @@ func (util *ISCSIUtil) DetachDisk(c iscsiDiskUnmounter, mntPath string) error { // Logout may fail as no session may exist for the portal/IQN on the specified interface. iface, found = extractIface(mntPath) } - for _, portal := range removeDuplicate(bkpPortal) { + portals := removeDuplicate(bkpPortal) + if len(portals) == 0 { + return fmt.Errorf("iscsi detach disk: failed to detach iscsi disk. Couldn't get connected portals from configurations.") + } + + for _, portal := range portals { logout := []string{"-m", "node", "-p", portal, "-T", iqn, "--logout"} delete := []string{"-m", "node", "-p", portal, "-T", iqn, "-o", "delete"} if found { @@ -370,6 +392,16 @@ func (util *ISCSIUtil) DetachDisk(c iscsiDiskUnmounter, mntPath string) error { glog.Errorf("iscsi: failed to delete node record Error: %s", string(out)) } } + // Delete the iface after all sessions have logged out + // If the iface is not created via iscsi plugin, skip to delete + if initiatorName != "" && found && iface == (portals[0]+":"+c.volName) { + delete := []string{"-m", "iface", "-I", iface, "-o", "delete"} + out, err := c.plugin.execCommand("iscsiadm", delete) + if err != nil { + glog.Errorf("iscsi: failed to delete iface Error: %s", string(out)) + } + } + } } return nil @@ -448,3 +480,57 @@ func removeDuplicate(s []string) []string { s = s[:len(m)] return s } + +func parseIscsiadmShow(output string) (map[string]string, error) { + params := make(map[string]string) + slice := strings.Split(output, "\n") + for _, line := range slice { + if !strings.HasPrefix(line, "iface.") || strings.Contains(line, "") { + continue + } + iface := strings.Fields(line) + if len(iface) != 3 || iface[1] != "=" { + return nil, fmt.Errorf("Error: invalid iface setting: %v", iface) + } + // iscsi_ifacename is immutable once the iface is created + if iface[0] == "iface.iscsi_ifacename" { + continue + } + params[iface[0]] = iface[2] + } + return params, nil +} + +func cloneIface(b iscsiDiskMounter, newIface string) error { + var lastErr error + // get pre-configured iface records + out, err := b.plugin.execCommand("iscsiadm", []string{"-m", "iface", "-I", b.Iface, "-o", "show"}) + if err != nil { + lastErr = fmt.Errorf("iscsi: failed to show iface records: %s (%v)", string(out), err) + return lastErr + } + // parse obtained records + params, err := parseIscsiadmShow(string(out)) + if err != nil { + lastErr = fmt.Errorf("iscsi: failed to parse iface records: %s (%v)", string(out), err) + return lastErr + } + // update initiatorname + params["iface.initiatorname"] = b.InitiatorName + // create new iface + out, err = b.plugin.execCommand("iscsiadm", []string{"-m", "iface", "-I", newIface, "-o", "new"}) + if err != nil { + lastErr = fmt.Errorf("iscsi: failed to create new iface: %s (%v)", string(out), err) + return lastErr + } + // update new iface records + for key, val := range params { + _, err = b.plugin.execCommand("iscsiadm", []string{"-m", "iface", "-I", newIface, "-o", "update", "-n", key, "-v", val}) + if err != nil { + b.plugin.execCommand("iscsiadm", []string{"-m", "iface", "-I", newIface, "-o", "delete"}) + lastErr = fmt.Errorf("iscsi: failed to update iface records: %s (%v). iface(%s) will be used", string(out), err, b.Iface) + break + } + } + return lastErr +} diff --git a/pkg/volume/iscsi/iscsi_util_test.go b/pkg/volume/iscsi/iscsi_util_test.go index 16f79a0664b..cab0baf39bc 100755 --- a/pkg/volume/iscsi/iscsi_util_test.go +++ b/pkg/volume/iscsi/iscsi_util_test.go @@ -17,12 +17,16 @@ limitations under the License. package iscsi import ( + "errors" "os" "path/filepath" "reflect" "testing" "k8s.io/kubernetes/pkg/util/mount" + "k8s.io/kubernetes/pkg/volume" + "k8s.io/utils/exec" + fakeexec "k8s.io/utils/exec/testing" ) func TestGetDevicePrefixRefCount(t *testing.T) { @@ -183,3 +187,177 @@ func TestWaitForPathToExist(t *testing.T) { t.Errorf("waitForPathToExist: wrong code path called for %s", devicePath[1]) } } + +func TestParseIscsiadmShow(t *testing.T) { + fakeIscsiadmOutput1 := "# BEGIN RECORD 2.0-873\n" + + "iface.iscsi_ifacename = default\n" + + "iface.transport_name = tcp\n" + + "iface.initiatorname = \n" + + "iface.mtu = 0\n" + + "# END RECORD" + + fakeIscsiadmOutput2 := "# BEGIN RECORD 2.0-873\n" + + "iface.iscsi_ifacename = default\n" + + "iface.transport_name = cxgb4i\n" + + "iface.initiatorname = \n" + + "iface.mtu = 0\n" + + "# END RECORD" + + fakeIscsiadmOutput3 := "# BEGIN RECORD 2.0-873\n" + + "iface.iscsi_ifacename = custom\n" + + "iface.transport_name = \n" + + "iface.initiatorname = \n" + + "iface.mtu = 0\n" + + "# END RECORD" + + fakeIscsiadmOutput4 := "iface.iscsi_ifacename=error" + fakeIscsiadmOutput5 := "iface.iscsi_ifacename + error" + + expectedIscsiadmOutput1 := map[string]string{ + "iface.transport_name": "tcp", + "iface.mtu": "0"} + + expectedIscsiadmOutput2 := map[string]string{ + "iface.transport_name": "cxgb4i", + "iface.mtu": "0"} + + expectedIscsiadmOutput3 := map[string]string{ + "iface.mtu": "0"} + + params, _ := parseIscsiadmShow(fakeIscsiadmOutput1) + if !reflect.DeepEqual(params, expectedIscsiadmOutput1) { + t.Errorf("parseIscsiadmShow: Fail to parse iface record: %s", params) + } + params, _ = parseIscsiadmShow(fakeIscsiadmOutput2) + if !reflect.DeepEqual(params, expectedIscsiadmOutput2) { + t.Errorf("parseIscsiadmShow: Fail to parse iface record: %s", params) + } + params, _ = parseIscsiadmShow(fakeIscsiadmOutput3) + if !reflect.DeepEqual(params, expectedIscsiadmOutput3) { + t.Errorf("parseIscsiadmShow: Fail to parse iface record: %s", params) + } + _, err := parseIscsiadmShow(fakeIscsiadmOutput4) + if err == nil { + t.Errorf("parseIscsiadmShow: Fail to handle invalid record: iface %s", fakeIscsiadmOutput4) + } + _, err = parseIscsiadmShow(fakeIscsiadmOutput5) + if err == nil { + t.Errorf("parseIscsiadmShow: Fail to handle invalid record: iface %s", fakeIscsiadmOutput5) + } +} + +func TestClonedIface(t *testing.T) { + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // iscsiadm -m iface -I -o show + func() ([]byte, error) { + return []byte("iface.ipaddress = \niface.transport_name = tcp\niface.initiatorname = \n"), nil + }, + // iscsiadm -m iface -I -o new + func() ([]byte, error) { return []byte("New interface 192.168.1.10:pv0001 added"), nil }, + // iscsiadm -m iface -I -o update -n -v + func() ([]byte, error) { return []byte(""), nil }, + func() ([]byte, error) { return []byte(""), nil }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + plugins := []volume.VolumePlugin{ + &iscsiPlugin{ + host: nil, + exe: &fexec, + }, + } + plugin := plugins[0] + fakeMounter := iscsiDiskMounter{ + iscsiDisk: &iscsiDisk{ + plugin: plugin.(*iscsiPlugin)}, + } + newIface := "192.168.1.10:pv0001" + cloneIface(fakeMounter, newIface) + if fcmd.CombinedOutputCalls != 4 { + t.Errorf("expected 4 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + +} + +func TestClonedIfaceShowError(t *testing.T) { + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // iscsiadm -m iface -I -o show, return test error + func() ([]byte, error) { return []byte(""), errors.New("test error") }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + plugins := []volume.VolumePlugin{ + &iscsiPlugin{ + host: nil, + exe: &fexec, + }, + } + plugin := plugins[0] + fakeMounter := iscsiDiskMounter{ + iscsiDisk: &iscsiDisk{ + plugin: plugin.(*iscsiPlugin)}, + } + newIface := "192.168.1.10:pv0001" + cloneIface(fakeMounter, newIface) + if fcmd.CombinedOutputCalls != 1 { + t.Errorf("expected 1 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + +} + +func TestClonedIfaceUpdateError(t *testing.T) { + fcmd := fakeexec.FakeCmd{ + CombinedOutputScript: []fakeexec.FakeCombinedOutputAction{ + // iscsiadm -m iface -I -o show + func() ([]byte, error) { + return []byte("iface.ipaddress = \niface.transport_name = tcp\niface.initiatorname = \n"), nil + }, + // iscsiadm -m iface -I -o new + func() ([]byte, error) { return []byte("New interface 192.168.1.10:pv0001 added"), nil }, + // iscsiadm -m iface -I -o update -n -v + func() ([]byte, error) { return []byte(""), nil }, + func() ([]byte, error) { return []byte(""), errors.New("test error") }, + // iscsiadm -m iface -I -o delete + func() ([]byte, error) { return []byte(""), nil }, + }, + } + fexec := fakeexec.FakeExec{ + CommandScript: []fakeexec.FakeCommandAction{ + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, + }, + } + plugins := []volume.VolumePlugin{ + &iscsiPlugin{ + host: nil, + exe: &fexec, + }, + } + plugin := plugins[0] + fakeMounter := iscsiDiskMounter{ + iscsiDisk: &iscsiDisk{ + plugin: plugin.(*iscsiPlugin)}, + } + newIface := "192.168.1.10:pv0001" + cloneIface(fakeMounter, newIface) + if fcmd.CombinedOutputCalls != 5 { + t.Errorf("expected 5 CombinedOutput() calls, got %d", fcmd.CombinedOutputCalls) + } + +} diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index 2444bf0a46d..26ce08ad7bf 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -1095,6 +1095,11 @@ type ISCSIVolumeSource struct { // CHAP secret for iSCSI target and initiator authentication // +optional SecretRef *LocalObjectReference `json:"secretRef,omitempty" protobuf:"bytes,10,opt,name=secretRef"` + // Custom iSCSI initiator name. + // If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + // : will be created for the connection. + // +optional + InitiatorName *string `json:"initiatorName,omitempty" protobuf:"bytes,12,opt,name=initiatorName"` } // Represents a Fibre Channel volume.