From 69a6f4ef467ecbe4d68d90a3f61da905bf4c1e98 Mon Sep 17 00:00:00 2001 From: Linoy Hadad Date: Sun, 19 Jan 2025 16:15:02 +0200 Subject: [PATCH] Expand iSCSI validations to include multipath This commit expand the 2 iSCSI validations, one during the discovery phase, which checks that the iSCSI disk is not connected through the default network interface (the one used by the default gateway), and another during the networking stage, ensuring that the disk does not belong to the machine's network. --- internal/hardware/validator.go | 35 +++++- internal/hardware/validator_test.go | 83 +++++++++++++- internal/host/validations_test.go | 170 +++++++++++++++++++++------- internal/host/validator.go | 41 +++++-- 4 files changed, 275 insertions(+), 54 deletions(-) diff --git a/internal/hardware/validator.go b/internal/hardware/validator.go index e24c0086928..d74c4c2459c 100644 --- a/internal/hardware/validator.go +++ b/internal/hardware/validator.go @@ -27,10 +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" - 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 type %s, 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" ) //go:generate mockgen -source=validator.go -package=hardware -destination=mock_validator.go @@ -147,6 +149,7 @@ func (v *validator) DiskIsEligible(ctx context.Context, disk *models.Disk, infra } if disk.DriveType == models.DriveTypeMultipath { + fc, iscsi := false, false for _, inventoryDisk := range inventory.Disks { if strings.Contains(inventoryDisk.Holders, disk.Name) { // We only allow multipath if all paths are FC/ iSCSI @@ -155,6 +158,30 @@ func (v *validator) DiskIsEligible(ctx context.Context, disk *models.Disk, infra fmt.Sprintf(wrongMultipathTypeTemplate, inventoryDisk.DriveType, strings.Join([]string{string(models.DriveTypeFC), string(models.DriveTypeISCSI)}, ", "))) break } + // We only allow multipath if all paths are of the same type + if !iscsi && inventoryDisk.DriveType == models.DriveTypeISCSI { + iscsi = true + } else if !fc && inventoryDisk.DriveType == models.DriveTypeFC { + fc = true + } + if iscsi && fc { + notEligibleReasons = append(notEligibleReasons, mixedTypesInMultipath) + break + } + // If errors are detected on iSCSI disks, multipath is not allowed + if !lo.Contains(notEligibleReasons, ErrsInIscsiDisableMultipathInstallation) { + if inventoryDisk.DriveType == models.DriveTypeISCSI { + // check if iSCSI boot drive is valid + if !v.IsValidStorageDeviceType(inventoryDisk, hostArchitecture, clusterVersion) { + notEligibleReasons = append(notEligibleReasons, ErrsInIscsiDisableMultipathInstallation) + } + // Check if network is configured properly to install on iSCSI boot drive + err = isISCSINetworkingValid(inventoryDisk, inventory) + if err != nil { + notEligibleReasons = append(notEligibleReasons, ErrsInIscsiDisableMultipathInstallation) + } + } + } } } } diff --git a/internal/hardware/validator_test.go b/internal/hardware/validator_test.go index abfa5076a33..8603398cdf7 100644 --- a/internal/hardware/validator_test.go +++ b/internal/hardware/validator_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "math" "os" "testing" @@ -288,20 +289,100 @@ var _ = Describe("Disk eligibility", func() { Expect(notEligibleReasons).To(BeEmpty()) }) + 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.DriveTypeFC, Holders: "dm-0"}, {Name: "sdb", 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).To(ContainElement(mixedTypesInMultipath)) + }) + 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).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" + 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 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).To(BeEmpty()) }) diff --git a/internal/host/validations_test.go b/internal/host/validations_test.go index 5a8cb80e4b0..b93c39db045 100644 --- a/internal/host/validations_test.go +++ b/internal/host/validations_test.go @@ -2347,6 +2347,14 @@ var _ = Describe("Validations test", func() { cluster common.Cluster host models.Host ) + + standaloneiSCSIDisks := func(iscsi *models.Iscsi) []*models.Disk { + return []*models.Disk{{ID: "install-disk", DriveType: models.DriveTypeISCSI, Iscsi: iscsi}} + } + multipathiSCSIDisks := func(iscsi *models.Iscsi) []*models.Disk { + return []*models.Disk{{ID: "install-disk", Name: "dm-0", DriveType: models.DriveTypeMultipath}, {Name: "sda", DriveType: models.DriveTypeISCSI, Holders: "dm-0", Iscsi: iscsi}, {Name: "sdb", DriveType: models.DriveTypeISCSI, Holders: "dm-0", Iscsi: iscsi}} + } + BeforeEach(func() { cluster = hostutil.GenerateTestCluster(clusterID) hostId, infraEnvId := strfmt.UUID(uuid.New().String()), strfmt.UUID(uuid.New().String()) @@ -2379,7 +2387,7 @@ var _ = Describe("Validations test", func() { _, _, ok := getValidationResult(refreshedHost.ValidationsInfo, NoIscsiNicBelongsToMachineCidr) Expect(ok).To(BeFalse()) }) - It("Not iSCSI drive", func() { + It("Not iSCSI/ Multipath iSCSI drives", func() { var inventory models.Inventory err := json.Unmarshal([]byte(host.Inventory), &inventory) Expect(err).ShouldNot(HaveOccurred()) @@ -2401,20 +2409,13 @@ var _ = Describe("Validations test", func() { _, _, ok := getValidationResult(refreshedHost.ValidationsInfo, NoIscsiNicBelongsToMachineCidr) Expect(ok).To(BeFalse()) }) - It("iSCSI drive - no network interface set with host IP", func() { + DescribeTable("iSCSI / Multipath iSCSI drive - no network interface set with host IP", func(disks []*models.Disk) { var inventory models.Inventory err := json.Unmarshal([]byte(host.Inventory), &inventory) Expect(err).ShouldNot(HaveOccurred()) host.InstallationDiskID = "install-disk" - inventory.Disks = []*models.Disk{ - { - ID: "install-disk", - DriveType: models.DriveTypeISCSI, - Iscsi: &models.Iscsi{ - HostIPAddress: "99.99.99.99", - }}, - } + inventory.Disks = disks inventoryByte, _ := json.Marshal(inventory) host.Inventory = string(inventoryByte) @@ -2429,22 +2430,19 @@ var _ = Describe("Validations test", func() { Expect(ok).To(BeTrue()) Expect(status).To(Equal(ValidationError)) Expect(message).To(Equal("Cannot find network interface associated to iSCSI host IP address")) - }) + }, + Entry("Standalone iSCSI", standaloneiSCSIDisks(&models.Iscsi{HostIPAddress: "99.99.99.99"})), + Entry("Multipath iSCSI", multipathiSCSIDisks(&models.Iscsi{HostIPAddress: "99.99.99.99"})), + ) DescribeTable( - "iSCSI drive - malformed iSCSI properties", - func(iSCSI *models.Iscsi, expectedMessage string) { + "iSCSI / Multipath iSCSI drive - malformed iSCSI properties", + func(disks []*models.Disk, expectedMessage string) { var inventory models.Inventory err := json.Unmarshal([]byte(host.Inventory), &inventory) Expect(err).ShouldNot(HaveOccurred()) host.InstallationDiskID = "install-disk" - inventory.Disks = []*models.Disk{ - { - ID: "install-disk", - DriveType: models.DriveTypeISCSI, - Iscsi: iSCSI, - }, - } + inventory.Disks = disks inventoryByte, _ := json.Marshal(inventory) host.Inventory = string(inventoryByte) @@ -2460,27 +2458,22 @@ var _ = Describe("Validations test", func() { Expect(status).To(Equal(ValidationError)) Expect(message).To(Equal(expectedMessage)) }, - Entry("iSCSI is nil", nil, "iSCSI installation disk is missing host IP address"), - Entry("Host IP address is empty", &models.Iscsi{}, "Cannot find network interface associated to iSCSI host IP address"), - Entry("Host IP address is invalid", &models.Iscsi{HostIPAddress: "invalid"}, "Cannot find network interface associated to iSCSI host IP address"), + Entry("Standalone iSCSI - iSCSI is nil", standaloneiSCSIDisks(nil), "iSCSI disk is missing host IP address"), + Entry("Multipath iSCSI - iSCSI is nil", multipathiSCSIDisks(nil), "iSCSI disk is missing host IP address"), + Entry("Standalone iSCSI - Host IP address is empty", standaloneiSCSIDisks(&models.Iscsi{}), "Cannot find network interface associated to iSCSI host IP address"), + Entry("Multipath iSCSI - Host IP address is empty", multipathiSCSIDisks(&models.Iscsi{}), "Cannot find network interface associated to iSCSI host IP address"), + Entry("Standalone iSCSI - Host IP address is invalid", standaloneiSCSIDisks(&models.Iscsi{HostIPAddress: "invalid"}), "Cannot find network interface associated to iSCSI host IP address"), + Entry("Multipath iSCSI - Host IP address is invalid", multipathiSCSIDisks(&models.Iscsi{HostIPAddress: "invalid"}), "Cannot find network interface associated to iSCSI host IP address"), ) DescribeTable( - "iSCSI drive - machine networks", - func(iSCSIHostIPAdress string, machineNetworks []*models.MachineNetwork, interfaces []*models.Interface, expectedStatus ValidationStatus, expectedMessage string) { + "iSCSI / Multipath iSCSI drive - machine networks", + func(disks []*models.Disk, machineNetworks []*models.MachineNetwork, interfaces []*models.Interface, expectedStatus ValidationStatus, expectedMessage string) { var inventory models.Inventory err := json.Unmarshal([]byte(host.Inventory), &inventory) Expect(err).ShouldNot(HaveOccurred()) host.InstallationDiskID = "install-disk" - inventory.Disks = []*models.Disk{ - { - ID: "install-disk", - DriveType: models.DriveTypeISCSI, - Iscsi: &models.Iscsi{ - HostIPAddress: iSCSIHostIPAdress, - }, - }, - } + inventory.Disks = disks inventory.Interfaces = interfaces inventoryByte, _ := json.Marshal(inventory) @@ -2497,9 +2490,11 @@ var _ = Describe("Validations test", func() { Expect(message).To(Equal(expectedMessage)) Expect(status).To(Equal(expectedStatus)) }, - Entry("Machine networks is nil", "192.168.1.1", nil, nil, ValidationPending, "Missing inventory or machine network CIDR"), - Entry("Machine networks is empty", "192.168.1.1", []*models.MachineNetwork{}, nil, ValidationPending, "Missing inventory or machine network CIDR"), - Entry("Machine networks IPv4 overlaps with host IPv4 address", "192.168.1.1", + Entry("Standalone iSCSI - Machine networks is nil", standaloneiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), nil, nil, ValidationPending, "Missing inventory or machine network CIDR"), + Entry("Multipath iSCSI - Machine networks is nil", multipathiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), nil, nil, ValidationPending, "Missing inventory or machine network CIDR"), + Entry("Standalone iSCSI - Machine networks is empty", standaloneiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), []*models.MachineNetwork{}, nil, ValidationPending, "Missing inventory or machine network CIDR"), + Entry("Multipath iSCSI - Machine networks is empty", multipathiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), []*models.MachineNetwork{}, nil, ValidationPending, "Missing inventory or machine network CIDR"), + Entry("Standalone iSCSI - Machine networks IPv4 overlaps with host IPv4 address", standaloneiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), []*models.MachineNetwork{ { Cidr: "192.168.1.0/24", @@ -2512,7 +2507,20 @@ var _ = Describe("Validations test", func() { }, }, ValidationFailure, "Network interface connected to iSCSI disk cannot belong to machine network CIDRs"), - Entry("Machine networks IPv6 overlaps with host IPv6 address", "2001:0db8:85a3::8a2e:0370:7334", + Entry("Multipath iSCSI - Machine networks IPv4 overlaps with host IPv4 address", multipathiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), + []*models.MachineNetwork{ + { + Cidr: "192.168.1.0/24", + ClusterID: clusterID, + }, + }, + []*models.Interface{ + { + IPV4Addresses: []string{"192.168.1.1/24"}, + }, + }, + ValidationFailure, fmt.Sprintf("%s: Network interface connected to iSCSI disk (%s) cannot belong to machine network CIDRs", hardware.ErrsInIscsiDisableMultipathInstallation, "sda, sdb")), + Entry("Standalone iSCSI - Machine networks IPv6 overlaps with host IPv6 address", standaloneiSCSIDisks(&models.Iscsi{HostIPAddress: "2001:0db8:85a3::8a2e:0370:7334"}), []*models.MachineNetwork{ { Cidr: "2001:0db8:85a3::/48", @@ -2525,7 +2533,20 @@ var _ = Describe("Validations test", func() { }, }, ValidationFailure, "Network interface connected to iSCSI disk cannot belong to machine network CIDRs"), - Entry("Machine networks IPv6 uses the same interface as host IPv4 address", "192.168.1.1", + Entry("Multipath iSCSI - Machine networks IPv6 overlaps with host IPv6 address", multipathiSCSIDisks(&models.Iscsi{HostIPAddress: "2001:0db8:85a3::8a2e:0370:7334"}), + []*models.MachineNetwork{ + { + Cidr: "2001:0db8:85a3::/48", + ClusterID: clusterID, + }, + }, + []*models.Interface{ + { + IPV6Addresses: []string{"2001:0db8:85a3::8a2e:0370:7334/48"}, + }, + }, + ValidationFailure, fmt.Sprintf("%s: Network interface connected to iSCSI disk (%s) cannot belong to machine network CIDRs", hardware.ErrsInIscsiDisableMultipathInstallation, "sda, sdb")), + Entry("Standalone iSCSI - Machine networks IPv6 uses the same interface as host IPv4 address", standaloneiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), []*models.MachineNetwork{ { Cidr: "2001:0db8:85a3::/48", @@ -2539,7 +2560,21 @@ var _ = Describe("Validations test", func() { }, }, ValidationFailure, "Network interface connected to iSCSI disk cannot belong to machine network CIDRs"), - Entry("Machine networks IPv4 uses the same interface as host IPv6 address", "2001:0db8:85a3::8a2e:0370:7334", + Entry("Multipath iSCSI - Machine networks IPv6 uses the same interface as host IPv4 address", multipathiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), + []*models.MachineNetwork{ + { + Cidr: "2001:0db8:85a3::/48", + ClusterID: clusterID, + }, + }, + []*models.Interface{ + { + IPV4Addresses: []string{"192.168.1.1/24"}, + IPV6Addresses: []string{"2001:0db8:85a3::8a2e:0370:7334/48"}, + }, + }, + ValidationFailure, fmt.Sprintf("%s: Network interface connected to iSCSI disk (%s) cannot belong to machine network CIDRs", hardware.ErrsInIscsiDisableMultipathInstallation, "sda, sdb")), + Entry("Standalone iSCSI - Machine networks IPv4 uses the same interface as host IPv6 address", standaloneiSCSIDisks(&models.Iscsi{HostIPAddress: "2001:0db8:85a3::8a2e:0370:7334"}), []*models.MachineNetwork{ { Cidr: "192.168.1.1/24", @@ -2553,12 +2588,62 @@ var _ = Describe("Validations test", func() { }, }, ValidationFailure, "Network interface connected to iSCSI disk cannot belong to machine network CIDRs"), - Entry("Machine networks IPv4 and host IPv4 address use different interfaces", "192.168.1.1", + Entry("Multipath iSCSI - Machine networks IPv4 uses the same interface as host IPv6 address", multipathiSCSIDisks(&models.Iscsi{HostIPAddress: "2001:0db8:85a3::8a2e:0370:7334"}), + []*models.MachineNetwork{ + { + Cidr: "192.168.1.1/24", + ClusterID: clusterID, + }, + }, + []*models.Interface{ + { + IPV4Addresses: []string{"192.168.1.1/24"}, + IPV6Addresses: []string{"2001:0db8:85a3::8a2e:0370:7334/48"}, + }, + }, + ValidationFailure, fmt.Sprintf("%s: Network interface connected to iSCSI disk (%s) cannot belong to machine network CIDRs", hardware.ErrsInIscsiDisableMultipathInstallation, "sda, sdb")), + Entry("Standalone iSCSI - Machine networks IPv4 and host IPv4 address use different interfaces", standaloneiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), + []*models.MachineNetwork{ + { + Cidr: "192.168.2.1/24", + ClusterID: clusterID, + }, + }, + []*models.Interface{ + { + IPV4Addresses: []string{"192.168.1.1/24"}, + }, + { + IPV4Addresses: []string{"192.168.2.1/24"}, + }, + }, + ValidationSuccess, "Network interface connected to iSCSI disk does not belong to machine network CIDRs"), + Entry("Multipath iSCSI - Machine networks IPv4 and host IPv4 address use different interfaces", multipathiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), + []*models.MachineNetwork{ + { + Cidr: "192.168.2.1/24", + ClusterID: clusterID, + }, + }, + []*models.Interface{ + { + IPV4Addresses: []string{"192.168.1.1/24"}, + }, + { + IPV4Addresses: []string{"192.168.2.1/24"}, + }, + }, + ValidationSuccess, "Network interface connected to iSCSI disk does not belong to machine network CIDRs"), + Entry("Standalone iSCSI - Machine networks IPv4/IPv6 and host IPv4 address use different interfaces", standaloneiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), []*models.MachineNetwork{ { Cidr: "192.168.2.1/24", ClusterID: clusterID, }, + { + Cidr: "2001:0db8:85a3::/48", + ClusterID: clusterID, + }, }, []*models.Interface{ { @@ -2566,10 +2651,11 @@ var _ = Describe("Validations test", func() { }, { IPV4Addresses: []string{"192.168.2.1/24"}, + IPV6Addresses: []string{"2001:0db8:85a3::8a2e:0370:7334/48"}, }, }, ValidationSuccess, "Network interface connected to iSCSI disk does not belong to machine network CIDRs"), - Entry("Machine networks IPv4/IPv6 and host IPv4 address use different interfaces", "192.168.1.1", + Entry("Multipath iSCSI - Machine networks IPv4/IPv6 and host IPv4 address use different interfaces", multipathiSCSIDisks(&models.Iscsi{HostIPAddress: "192.168.1.1"}), []*models.MachineNetwork{ { Cidr: "192.168.2.1/24", diff --git a/internal/host/validator.go b/internal/host/validator.go index 63c7eee0767..64d3b8d48d1 100644 --- a/internal/host/validator.go +++ b/internal/host/validator.go @@ -1929,19 +1929,26 @@ func (v *validator) noIscsiNicBelongsToMachineCidr(c *validationContext) (Valida return ValidationSuccessSuppressOutput, "" } - if installationDisk.DriveType != models.DriveTypeISCSI { - // Validation is not relevant in this case - return ValidationSuccessSuppressOutput, "" - } - if c.inventory == nil || !network.IsMachineCidrAvailable(c.cluster) { return ValidationPending, "Missing inventory or machine network CIDR" } + if installationDisk.DriveType == models.DriveTypeISCSI { + return v.standaloneiSCSI(c, installationDisk) + } else if installationDisk.DriveType == models.DriveTypeMultipath { + return v.multipathiSCSI(c, installationDisk) + } else { + // Validation is not relevant in this case + return ValidationSuccessSuppressOutput, "" + } +} + +// standaloneiSCSI - Related to the noIscsiNicBelongsToMachineCidr validation. This is executed when the installation disk is an iSCSI disk. +func (v *validator) standaloneiSCSI(c *validationContext, installationDisk *models.Disk) (ValidationStatus, string) { if installationDisk.Iscsi == nil { // If this is nil, the disk shouldn't have passed the eligilibily test in the first place - v.log.Warn("iSCSI installation disk is missing host IP address") - return ValidationError, "iSCSI installation disk is missing host IP address" + v.log.Warn("iSCSI disk is missing host IP address") + return ValidationError, "iSCSI disk is missing host IP address" } nic, err := network.FindInterfaceByIPString(installationDisk.Iscsi.HostIPAddress, c.inventory.Interfaces) @@ -1957,3 +1964,23 @@ func (v *validator) noIscsiNicBelongsToMachineCidr(c *validationContext) (Valida return ValidationSuccess, "Network interface connected to iSCSI disk does not belong to machine network CIDRs" } + +// multipathiSCSI - Related to the noIscsiNicBelongsToMachineCidr validation. This is executed when the installation disk is a multipath disk serving as a holder for iSCSI drives. +func (v *validator) multipathiSCSI(c *validationContext, installationDisk *models.Disk) (ValidationStatus, string) { + var status ValidationStatus + msg := "" + var iSCSIDisksWithErrors []string + iSCSIDisks := hostutil.GetDisksOfHolder(c.inventory.Disks, installationDisk, models.DriveTypeISCSI) + for _, iscsiDisk := range iSCSIDisks { + status, msg = v.standaloneiSCSI(c, iscsiDisk) + if status == ValidationError { + return ValidationError, msg + } else if status == ValidationFailure { + iSCSIDisksWithErrors = append(iSCSIDisksWithErrors, iscsiDisk.Name) + } + } + if len(iSCSIDisksWithErrors) > 0 { + return ValidationFailure, fmt.Sprintf("%s: Network interface connected to iSCSI disk (%s) cannot belong to machine network CIDRs", hardware.ErrsInIscsiDisableMultipathInstallation, strings.Join(iSCSIDisksWithErrors, ", ")) + } + return ValidationSuccess, msg +}