Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MGMT-19670, MGMT-19484: Enable multipath + iSCSI as installation disk #7192

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 33 additions & 39 deletions internal/hardware/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ import (
)

const (
tooSmallDiskTemplate = "Disk is too small (disk only has %s, but %s are required)"
wrongDriveTypeTemplate = "Drive type is %s, it must be one of %s."
wrongMultipathTypeTemplate = "Multipath device has path of type %s, it must be %s"
iSCSIWithMultipathHolder = "iSCSI disk with a multipath holder is not eligible"
wrongISCSINetworkTemplate = "iSCSI host IP %s is the same as host IP, they must be different"
tooSmallDiskTemplate = "Disk is too small (disk only has %s, but %s are required)"
wrongDriveTypeTemplate = "Drive type is %s, it must be one of %s."
wrongMultipathTypeTemplate = "Multipath device has path of unsupported type. It must be %s"
mixedTypesInMultipath = "Multipath device has paths of different types, but they must all be the same type"
wrongISCSINetworkTemplate = "iSCSI host IP %s is the same as host IP, they must be different"
ErrsInIscsiDisableMultipathInstallation = "Installation on multipath device is not possible due to errors on at least one iSCSI disk"
adriengentil marked this conversation as resolved.
Show resolved Hide resolved
)

//go:generate mockgen -source=validator.go -package=hardware -destination=mock_validator.go
Expand All @@ -53,7 +54,7 @@ type Validator interface {
func NewValidator(log logrus.FieldLogger, cfg ValidatorCfg, operatorsAPI operators.API, reg registry.ProviderRegistry) Validator {
diskEligibilityMatchers := []*regexp.Regexp{
compileDiskReasonTemplate(tooSmallDiskTemplate, ".*", ".*"),
compileDiskReasonTemplate(wrongDriveTypeTemplate, ".*", ".*"),
compileDiskReasonTemplate(wrongDriveTypeTemplate, ".*"),
compileDiskReasonTemplate(wrongMultipathTypeTemplate, ".*", ".*"),
}

Expand Down Expand Up @@ -147,25 +148,39 @@ func (v *validator) DiskIsEligible(ctx context.Context, disk *models.Disk, infra
fmt.Sprintf(wrongDriveTypeTemplate, disk.DriveType, strings.Join(v.getValidDeviceStorageTypes(hostArchitecture, clusterVersion), ", ")))
}

// We only allow multipath if all paths are FC
if disk.DriveType == models.DriveTypeMultipath {
linoyaslan marked this conversation as resolved.
Show resolved Hide resolved
for _, inventoryDisk := range inventory.Disks {
if lo.Contains(strings.Split(inventoryDisk.Holders, ","), disk.Name) {
if inventoryDisk.DriveType != models.DriveTypeFC {
notEligibleReasons = append(notEligibleReasons,
fmt.Sprintf(wrongMultipathTypeTemplate, inventoryDisk.DriveType, string(models.DriveTypeFC)))
break
fcDisks := hostutil.GetDisksOfHolderByType(inventory.Disks, disk, models.DriveTypeFC)
iSCSIDisks := hostutil.GetDisksOfHolderByType(inventory.Disks, disk, models.DriveTypeISCSI)
// We only allow multipath if all paths are FC/ iSCSI
if len(fcDisks) == 0 && len(iSCSIDisks) == 0 {
notEligibleReasons = append(notEligibleReasons,
fmt.Sprintf(wrongMultipathTypeTemplate, strings.Join([]string{string(models.DriveTypeFC), string(models.DriveTypeISCSI)}, ", ")))
// Return since further checks are unnecessary
return notEligibleReasons, nil
}
// We only allow multipath if all paths are of the same type
if len(fcDisks) > 0 && len(fcDisks) != len(hostutil.GetAllDisksOfHolder(inventory.Disks, disk)) {
notEligibleReasons = append(notEligibleReasons, mixedTypesInMultipath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
notEligibleReasons = append(notEligibleReasons, mixedTypesInMultipath)
return append(notEligibleReasons, mixedTypesInMultipath), nil

I think we can return here and below as well?

} else if len(iSCSIDisks) > 0 && len(iSCSIDisks) != len(hostutil.GetAllDisksOfHolder(inventory.Disks, disk)) {
notEligibleReasons = append(notEligibleReasons, mixedTypesInMultipath)
}
Comment on lines +164 to +166
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if len(iSCSIDisks) > 0 && len(iSCSIDisks) != len(hostutil.GetAllDisksOfHolder(inventory.Disks, disk)) {
notEligibleReasons = append(notEligibleReasons, mixedTypesInMultipath)
}
if len(iSCSIDisks) > 0 && len(iSCSIDisks) != len(hostutil.GetAllDisksOfHolder(inventory.Disks, disk)) {
return append(notEligibleReasons, mixedTypesInMultipath), nil
}

for _, iSCSIDisk := range iSCSIDisks {
// If errors are detected on iSCSI disks, multipath is not allowed
if !lo.Contains(notEligibleReasons, ErrsInIscsiDisableMultipathInstallation) {
if iSCSIDisk.DriveType == models.DriveTypeISCSI {
// check if iSCSI boot drive is valid
isiSCSIValid := v.IsValidStorageDeviceType(iSCSIDisk, hostArchitecture, clusterVersion)
// Check if network is configured properly to install on iSCSI boot drive
err = isISCSINetworkingValid(iSCSIDisk, inventory)
if err != nil || !isiSCSIValid {
notEligibleReasons = append(notEligibleReasons, ErrsInIscsiDisableMultipathInstallation)
}
}
}
}
}

if disk.DriveType == models.DriveTypeISCSI {
err := areISCSIHoldersValid(disk, inventory)
if err != nil {
notEligibleReasons = append(notEligibleReasons, err.Error())
}

// Check if network is configured properly to install on iSCSI boot drive
err = isISCSINetworkingValid(disk, inventory)
if err != nil {
Expand All @@ -176,27 +191,6 @@ func (v *validator) DiskIsEligible(ctx context.Context, disk *models.Disk, infra
return notEligibleReasons, nil
}

// Validate holders of iSCSI disk. We do not allow iSCSI disk with multipath holder.
func areISCSIHoldersValid(disk *models.Disk, inventory *models.Inventory) error {
multipathDiskNamesMap := make(map[string]struct{})
for _, inventoryDisk := range inventory.Disks {
if inventoryDisk.DriveType == models.DriveTypeMultipath {
multipathDiskNamesMap[inventoryDisk.Name] = struct{}{}
}
}

// Check if the iSCSI disk has any holders that are multipath disks
holders := strings.Split(disk.Holders, ",")
for _, holder := range holders {
if _, exists := multipathDiskNamesMap[holder]; exists {
return fmt.Errorf(iSCSIWithMultipathHolder)
}
}

return nil

}

// isISCSINetworkingValid checks if the iSCSI disk is not connected through the
// default network interface. The default network interface is the interface
// which is used by the default gateway.
Expand Down
126 changes: 97 additions & 29 deletions internal/hardware/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,17 @@ var _ = Describe("Disk eligibility", func() {
notEligibleReasons, err = hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, nil, &host, inventory)
Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons).To(BeEmpty())

By("Check that iSCSI is eligible when its holder is Multipath.", func() {
testDisk.Holders = "dm-0"
allDisks := []*models.Disk{&testDisk, {Name: "iscsi0", DriveType: models.DriveTypeISCSI, Holders: "dm-0"}, {Name: "dm-0", DriveType: models.DriveTypeMultipath}}
inventory.Disks = allDisks

notEligibleReasons, err := hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, &cluster, &host, inventory)

Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons).To(BeEmpty())
})
})

It("Check if RAID is eligible", func() {
Expand Down Expand Up @@ -278,53 +289,110 @@ var _ = Describe("Disk eligibility", func() {
Expect(notEligibleReasons).To(BeEmpty())
})

It("Check that iSCSI multipath is not eligible", func() {
It("Check that mixed types under multipath isn't eligible", func() {
testDisk.Name = "dm-0"
testDisk.DriveType = models.DriveTypeMultipath
allDisks := []*models.Disk{&testDisk, {Name: "sda", DriveType: models.DriveTypeISCSI, Holders: "dm-0"}, {Name: "sdb", DriveType: models.DriveTypeISCSI, Holders: "dm-0"}}
inventory.Disks = allDisks
operatorsMock.EXPECT().GetRequirementsBreakdownForHostInCluster(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.OperatorHostRequirements{}, nil).AnyTimes()

By("Most of iSCSI, few of FC")
allDisks := []*models.Disk{&testDisk, {Name: "sda", DriveType: models.DriveTypeFC, Holders: "dm-0"}, {Name: "sdb", DriveType: models.DriveTypeISCSI, Holders: "dm-0"}, {Name: "sdc", DriveType: models.DriveTypeISCSI, Holders: "dm-0"}}
inventory.Disks = allDisks
notEligibleReasons, err := hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, &cluster, &host, inventory)

Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons).ToNot(BeEmpty())

By("Check infra env iSCSI multipath is not eligible")
notEligibleReasons, err = hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, nil, &host, inventory)
Expect(notEligibleReasons).To(ContainElement(mixedTypesInMultipath))

By("Most of FC, few of iSCSI")
allDisks = []*models.Disk{&testDisk, {Name: "sda", DriveType: models.DriveTypeFC, Holders: "dm-0"}, {Name: "sdb", DriveType: models.DriveTypeFC, Holders: "dm-0"}, {Name: "sdc", DriveType: models.DriveTypeISCSI, Holders: "dm-0"}}
inventory.Disks = allDisks
notEligibleReasons, err = hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, &cluster, &host, inventory)
Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons).ToNot(BeEmpty())
Expect(notEligibleReasons).To(ContainElement(mixedTypesInMultipath))
})

It("Check that iSCSI is not eligible when its holder is Multipath.", func() {
testDisk.Name = "sdb"
testDisk.Holders = "dm-0"
cluster.OpenshiftVersion = "4.15.0"
testDisk.DriveType = models.DriveTypeISCSI
allDisks := []*models.Disk{&testDisk, {Name: "sdc", DriveType: models.DriveTypeISCSI, Holders: "dm-0"}, {Name: "dm-0", DriveType: models.DriveTypeMultipath}}
It("Check that Multipath iSCSI is eligible", func() {
testDisk.Name = "dm-0"
testDisk.DriveType = models.DriveTypeMultipath
allDisks := []*models.Disk{&testDisk, {Name: "sda", DriveType: models.DriveTypeISCSI, Holders: "dm-0"}, {Name: "sdb", DriveType: models.DriveTypeISCSI, Holders: "dm-0"}}
inventory.Disks = allDisks
cluster.OpenshiftVersion = "4.15.0"
hostInventory, _ := common.UnmarshalInventory(host.Inventory)

// Add a default IPv6 route
inventory.Routes = append(hostInventory.Routes, &models.Route{
Family: int32(common.IPv6),
Interface: "eth0",
Gateway: "fe80:db8::1",
Destination: "::",
Metric: 600,
})
inventory.Interfaces = hostInventory.Interfaces

operatorsMock.EXPECT().GetRequirementsBreakdownForHostInCluster(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*models.OperatorHostRequirements{}, nil).AnyTimes()

By("Check Multipath iSCSI is not eligible when host IPv4 address isn't set")
notEligibleReasons, err := hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, &cluster, &host, inventory)
Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons).To(ContainElement(ErrsInIscsiDisableMultipathInstallation))

By("Check Multipath iSCSI is eligible when host IPv4 address is not part of default network interface")
for _, disk := range allDisks {
disk.Iscsi = &models.Iscsi{HostIPAddress: "4.5.6.7"}
}
notEligibleReasons, err = hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, &cluster, &host, inventory)
Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons[0]).To(Equal(iSCSIWithMultipathHolder))
Expect(notEligibleReasons).ToNot(BeEmpty())
})
It("Check that iSCSI is not eligible on older version when its holder is Multipath.", func() {
testDisk.Name = "sdb"
testDisk.Holders = "dm-0"
Expect(notEligibleReasons).To(BeEmpty(), fmt.Sprintf("Debug info: inventory: %s", host.Inventory))

By("Check Multipath iSCSI is not eligible when host IPv4 address is part of default network interface")
for _, disk := range allDisks {
disk.Iscsi = &models.Iscsi{HostIPAddress: "1.2.3.4"}
}
notEligibleReasons, err = hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, &cluster, &host, inventory)
Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons).To(ContainElement(ErrsInIscsiDisableMultipathInstallation))

By("Check Multipath iSCSI is eligible when host IPv6 address is not part of default network interface")
for _, disk := range allDisks {
disk.Iscsi = &models.Iscsi{HostIPAddress: "1002:db8::10"}
}
notEligibleReasons, err = hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, &cluster, &host, inventory)
Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons).To(BeEmpty())

By("Check Multipath iSCSI is not eligible when host IPv6 address is part of default network interface")
for _, disk := range allDisks {
disk.Iscsi = &models.Iscsi{HostIPAddress: "1001:db8::10"}
}
notEligibleReasons, err = hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, &cluster, &host, inventory)
Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons).To(ContainElement(ErrsInIscsiDisableMultipathInstallation))

By("Check Multipath iSCSI on older version is not eligible")
for _, disk := range allDisks {
disk.Iscsi = &models.Iscsi{HostIPAddress: "4.5.6.7"}
}
cluster.OpenshiftVersion = "4.14.1"
testDisk.DriveType = models.DriveTypeISCSI
allDisks := []*models.Disk{&testDisk, {Name: "sdc", DriveType: models.DriveTypeISCSI, Holders: "dm-0"}, {Name: "dm-0", DriveType: models.DriveTypeMultipath}}
inventory.Disks = allDisks
notEligibleReasons, err = hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, &cluster, &host, inventory)
Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons).To(ContainElement(ErrsInIscsiDisableMultipathInstallation))

notEligibleReasons, err := hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, &cluster, &host, inventory)
By("Check Multipath iSCSI is eligible on day2 cluster")
for _, disk := range allDisks {
disk.Iscsi = &models.Iscsi{HostIPAddress: "4.5.6.7"}
}
cluster.Kind = swag.String(models.ClusterKindAddHostsCluster)
cluster.OpenshiftVersion = ""
infraEnv.OpenshiftVersion = "4.16"
notEligibleReasons, err = hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, &cluster, &host, inventory)
Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons).To(BeEmpty())

By("Check infra env Multipath iSCSI is eligible")
for _, disk := range allDisks {
disk.Iscsi = &models.Iscsi{HostIPAddress: "4.5.6.7"}
}
notEligibleReasons, err = hwvalidator.DiskIsEligible(ctx, &testDisk, infraEnv, nil, &host, inventory)
Expect(err).ToNot(HaveOccurred())
Expect(notEligibleReasons[0]).To(ContainSubstring(fmt.Sprintf("Drive type is %s, it must be one of", models.DriveTypeISCSI)))
Expect(notEligibleReasons).ToNot(BeEmpty())
Expect(notEligibleReasons).To(BeEmpty())
})

It("Check if FC is not eligible on non-s390x/x86_64", func() {
testDisk.DriveType = models.DriveTypeFC
inventory.CPU.Architecture = models.ClusterCPUArchitectureArm64
Expand Down
31 changes: 25 additions & 6 deletions internal/host/hostcommands/install_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,10 @@ func constructHostInstallerArgs(cluster *common.Cluster, host *models.Host, inve
// append kargs depending on installation drive type
installationDisk := hostutil.GetDiskByInstallationPath(inventory.Disks, hostutil.GetHostInstallationPath(host))
if installationDisk != nil {
installerArgs = appendMultipathArgs(installerArgs, installationDisk)
installerArgs, err = appendMultipathArgs(installerArgs, installationDisk, inventory, hasUserConfiguredIP)
if err != nil {
return "", err
}
installerArgs, err = appendISCSIArgs(installerArgs, installationDisk, inventory, hasUserConfiguredIP)
if err != nil {
return "", err
Expand Down Expand Up @@ -380,7 +383,9 @@ func appendISCSIArgs(installerArgs []string, installationDisk *models.Disk, inve
}

// enable iSCSI on boot
installerArgs = append(installerArgs, "--append-karg", "rd.iscsi.firmware=1")
if !lo.Contains(installerArgs, "rd.iscsi.firmware=1") {
installerArgs = append(installerArgs, "--append-karg", "rd.iscsi.firmware=1")
}

if hasUserConfiguredIP {
return installerArgs, nil
Expand All @@ -401,16 +406,30 @@ func appendISCSIArgs(installerArgs []string, installationDisk *models.Disk, inve
if iSCSIHostIP.Is6() {
dhcp = "dhcp6"
}
installerArgs = append(installerArgs, "--append-karg", fmt.Sprintf("ip=%s:%s", nic.Name, dhcp))

netArgs := fmt.Sprintf("ip=%s:%s", nic.Name, dhcp)
if !lo.Contains(installerArgs, fmt.Sprintf("ip=%s:%s", nic.Name, dhcp)) {
installerArgs = append(installerArgs, "--append-karg", netArgs)
}

return installerArgs, nil
}

func appendMultipathArgs(installerArgs []string, installationDisk *models.Disk) []string {
func appendMultipathArgs(installerArgs []string, installationDisk *models.Disk, inventory *models.Inventory, hasUserConfiguredIP bool) ([]string, error) {
if installationDisk.DriveType != models.DriveTypeMultipath {
return installerArgs
return installerArgs, nil
}
return append(installerArgs, "--append-karg", "root=/dev/disk/by-label/dm-mpath-root", "--append-karg", "rw", "--append-karg", "rd.multipath=default")

installerArgs = append(installerArgs, "--append-karg", "root=/dev/disk/by-label/dm-mpath-root", "--append-karg", "rw", "--append-karg", "rd.multipath=default")
var err error
iSCSIDisks := hostutil.GetDisksOfHolderByType(inventory.Disks, installationDisk, models.DriveTypeISCSI)
for _, iscsiDisk := range iSCSIDisks {
installerArgs, err = appendISCSIArgs(installerArgs, iscsiDisk, inventory, hasUserConfiguredIP)
if err != nil {
return nil, err
}
}
return installerArgs, nil
}

func appends390xArgs(inventory *models.Inventory, installerArgs []string, log logrus.FieldLogger) ([]string, bool) {
Expand Down
Loading