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.