Skip to content

Commit

Permalink
MGMT-18464: MGMT-18452: Add validations to make iSCSI disk eligible (#…
Browse files Browse the repository at this point in the history
…819)

* MGMT-18464: MGMT-18452: Add validations to make iSCSI disk eligible

This PR adds 2 checks before making iSSI disks be be eligible as
installation disk.

MGMT-18464: Check the state of the disk, we need to ensure that it is
"running" otherwise the installtion of RHCOS will fail on the disk.

MGMT-18464: Check if the discovery as been booted using iBFT, this check
ensure that the iSCSI disk is actually a disk on which we will be able
to boot from.

* add more unit tests
  • Loading branch information
adriengentil authored Nov 13, 2024
1 parent d599caa commit a8a3349
Show file tree
Hide file tree
Showing 2 changed files with 294 additions and 51 deletions.
119 changes: 106 additions & 13 deletions src/inventory/disks.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
const (
applianceAgentPrefix = "agent"
byIdLocation = "/dev/disk/by-id"
ibftBasePath = "/sys/firmware/ibft"
wwnPrefix = "wwn-"
)

Expand Down Expand Up @@ -341,6 +342,11 @@ func (d *disks) checkEligibility(disk *ghw.Disk) (notEligibleReasons []string, i
notEligibleReasons = append(notEligibleReasons, "Disk is an LVM logical volume")
}

if isISCSIDisk(disk) {
notEligibleReasons = append(notEligibleReasons, d.checkEligibilityISCSIState(disk)...)
notEligibleReasons = append(notEligibleReasons, d.checkEligibilityISCSIinIBFT(disk)...)
}

// Don't check partitions if this is an appliance disk, as those disks should be marked as eligible for installation.
for _, partition := range disk.Partitions {
if strings.HasPrefix(partition.Label, applianceAgentPrefix) {
Expand Down Expand Up @@ -530,24 +536,14 @@ func (d *disks) getBusPath(disks []*block.Disk, index int, busPath string) strin

// getISCSIHostIPAddress retuns the IP address in use to connect on the iSCSI volume
func (d *disks) getISCSIHostIPAddress(diskName string) string {
// resolve /sys/block/sda -> /sys/devices/platform/host2/session1/target2:0:0/2:0:0:1/block/sda
sysBlockPath := filepath.Join("/sys", "block", diskName)
sysDevicesPath, err := d.dependencies.EvalSymlinks(sysBlockPath)
iSCSIHost, err := d.getISCSIHost(diskName)
if err != nil {
logrus.WithError(err).Errorf("Failed to evaluate symlink for iSCSI device")
return ""
}

// extract iSCSI host
const hostIndex = 4
splitPath := strings.Split(sysDevicesPath, "/")
if len(splitPath) < (hostIndex + 1) {
logrus.Errorf("Failed to resolve iSCSI host in path %s", sysDevicesPath)
logrus.WithError(err).Errorf("Failed to resolve iSCSI host")
return ""
}

// Read IP address
hostIPAdressFile := fmt.Sprintf("/sys/class/iscsi_host/%s/ipaddress", splitPath[hostIndex])
hostIPAdressFile := filepath.Join("/sys/class/iscsi_host", iSCSIHost, "ipaddress")
data, err := d.dependencies.ReadFile(hostIPAdressFile)
if err != nil {
logrus.WithError(err).Errorf("Failed to read host IP address file for iSCSI device")
Expand All @@ -561,3 +557,100 @@ func (d *disks) getISCSIHostIPAddress(diskName string) string {
func GetDisks(subprocessConfig *config.SubprocessConfig, dependencies util.IDependencies) []*models.Disk {
return newDisks(subprocessConfig, dependencies).getDisks()
}

// Check if the state of the iSCSI disk is running
func (d *disks) checkEligibilityISCSIState(disk *ghw.Disk) []string {
stateFile := filepath.Join("/sys/block", disk.Name, "device/state")
state, err := d.dependencies.ReadFile(stateFile)
if err != nil {
logrus.WithError(err).Errorf("Failed to read state of iSCSI disk")
return []string{"Failed to read state of iSCSI disk"}
}

if strings.TrimSpace(string(state)) != "running" {
return []string{"iSCSI disk is not in running state"}
}

return []string{}
}

// Check if the target in the iSCSI disk is in iBFT
func (d *disks) checkEligibilityISCSIinIBFT(disk *ghw.Disk) []string {
iSCSISession, err := d.getISCSISession(disk.Name)
if err != nil {
logrus.WithError(err).Errorf("Cannot resolve iSCSI session")
return []string{"Cannot find iSCSI session"}
}

// Read target name
targetNameFile := filepath.Join("/sys/class/iscsi_session", iSCSISession, "targetname")
data, err := d.dependencies.ReadFile(targetNameFile)
if err != nil {
logrus.WithError(err).Errorf("Failed to read iSCSI target name")
return []string{"Cannot find iSCSI target name"}
}
iSCSITarget := strings.TrimSpace(string(data))

// Try to find the target used by the iSCSI volume insode the iBFT
// The iBFT directory structure looks like this:
// # ls -l
// total 0
// drwxr-xr-x. 2 root root 0 Nov 7 10:14 acpi_header
// drwxr-xr-x. 2 root root 0 Nov 7 10:14 ethernet0
// drwxr-xr-x. 2 root root 0 Nov 7 10:14 initiator
// drwxr-xr-x. 2 root root 0 Nov 7 10:14 target0
//
// targetN directories contain "target-name" file, which will try to
// match with the target used by the iSCSI volume on the host.
files, err := d.dependencies.ReadDir(ibftBasePath)
if err != nil {
return []string{"iBFT firmware is missing"}
}

for _, file := range files {
if !file.IsDir() || !strings.Contains(file.Name(), "target") {
continue
}

ibftTargetNameFile := filepath.Join(ibftBasePath, file.Name(), "target-name")
ibftTargetData, err := d.dependencies.ReadFile(ibftTargetNameFile)
if err != nil {
logrus.WithError(err).Warn("Failed to read iSCSI target name in iBFT")
continue
}

if strings.TrimSpace(string(ibftTargetData)) == iSCSITarget {
return []string{}
}
}

return []string{"iSCSI disk is missing from iBFT"}
}

func (d *disks) getISCSIProperty(diskName string, propertyIndex int) (string, error) {
// resolve /sys/block/sda -> /sys/devices/platform/host2/session1/target2:0:0/2:0:0:1/block/sda
sysBlockPath := filepath.Join("/sys", "block", diskName)
sysDevicesPath, err := d.dependencies.EvalSymlinks(sysBlockPath)
if err != nil {
return "", err
}

// extract iSCSI property from the path
splitPath := strings.Split(sysDevicesPath, "/")
if len(splitPath) < (propertyIndex + 1) {
return "", fmt.Errorf("Index %d is above the number of components in path %s", propertyIndex, sysDevicesPath)
}

return splitPath[propertyIndex], nil
}

func (d *disks) getISCSIHost(diskName string) (string, error) {
const hostIndex = 4
return d.getISCSIProperty(diskName, hostIndex)
}

func (d *disks) getISCSISession(diskName string) (string, error) {
const sessionIndex = 5
return d.getISCSIProperty(diskName, sessionIndex)

}
226 changes: 188 additions & 38 deletions src/inventory/disks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -771,46 +771,196 @@ var _ = Describe("Disks test", func() {
Expect(ret).To(Equal(expectation))
})

It("iSCSI device", func() {
mockGetWWNCallForSuccess(dependencies, make(map[string]string))
path := "/dev/sda"
disk := createISCSIDisk("sda")
mockFetchDisks(dependencies, nil, disk)
mockGetPathFromDev(dependencies, disk.Name, "")
mockGetHctl(dependencies, disk.Name, "error")
mockGetBootable(dependencies, path, true, "")
mockGetByPath(dependencies, disk.BusPath, "")
mockNoUUID(dependencies, path)
mockReadDir(dependencies, fmt.Sprintf("/sys/block/%s/holders", disk.Name), "")
dependencies.On("ReadFile", "/sys/class/iscsi_host/host2/ipaddress").Return([]byte("1.2.3.4"), nil)
dependencies.On("EvalSymlinks", "/sys/block/sda").Return("/sys/devices/platform/host2/session1/target2:0:0/2:0:0:1/block/sda", nil)
dependencies.On("ReadFile", fmt.Sprintf("/sys/block/%s/hidden", disk.Name)).Return([]byte("0\n"), nil)
ret := GetDisks(&config.SubprocessConfig{}, dependencies)
Context("iSCSI device", func() {
var targetMock MockFileInfo
const ibftBasePath = "/sys/firmware/ibft"
BeforeEach(func() {
mockGetWWNCallForSuccess(dependencies, make(map[string]string))
path := "/dev/sda"
disk := createISCSIDisk("sda")
mockFetchDisks(dependencies, nil, disk)
mockGetPathFromDev(dependencies, disk.Name, "")
mockGetHctl(dependencies, disk.Name, "error")
mockGetBootable(dependencies, path, true, "")
mockGetByPath(dependencies, disk.BusPath, "")
mockNoUUID(dependencies, path)
mockReadDir(dependencies, fmt.Sprintf("/sys/block/%s/holders", disk.Name), "")
dependencies.On("ReadFile", fmt.Sprintf("/sys/block/%s/hidden", disk.Name)).Return([]byte("0\n"), nil)

Expect(ret).To(Equal([]*models.Disk{
{
ID: "/dev/disk/by-path/ip-192.168.130.10:3260-iscsi-iqn.2022-01.com.redhat.foo:disk0-lun-0",
ByPath: "/dev/disk/by-path/ip-192.168.130.10:3260-iscsi-iqn.2022-01.com.redhat.foo:disk0-lun-0",
DriveType: models.DriveTypeISCSI,
Hctl: "",
Model: "disk0",
Name: "sda",
Path: "/dev/sda",
Serial: "6001405961d8b6f55cf48beb0de296b2",
SizeBytes: 21474836480,
Vendor: "LIO-ORG",
Wwn: "0x6001405961d8b6f55cf48beb0de296b2",
Bootable: true,
Smart: "",
Holders: "",
InstallationEligibility: models.DiskInstallationEligibility{
Eligible: true,
},
Iscsi: &models.Iscsi{
HostIPAddress: "1.2.3.4",
// Mock for iSCSI target directory in iBFT
targetMock = MockFileInfo{}
targetMock.On("Name").Return("target0")
targetMock.On("IsDir").Return(true)
})
It("Is eligible", func() {
// block device symlink to iSCSI device
dependencies.On("EvalSymlinks", "/sys/block/sda").Return("/sys/devices/platform/host2/session1/target2:0:0/2:0:0:1/block/sda", nil)
// Report iSCSI host IP address
dependencies.On("ReadFile", "/sys/class/iscsi_host/host2/ipaddress").Return([]byte("1.2.3.4"), nil)
// iSCSI state validation
dependencies.On("ReadFile", "/sys/block/sda/device/state").Return([]byte("running"), nil)
// iBFT target validation
dependencies.On("ReadFile", "/sys/class/iscsi_session/session1/targetname").Return([]byte("iqn.2023-01.com.example:tm1"), nil)
dependencies.On("ReadFile", "/sys/firmware/ibft/target0/target-name").Return([]byte("iqn.2023-01.com.example:tm1"), nil)

ignoredFileMock := MockFileInfo{}
ignoredFileMock.On("IsDir").Return(false)

ignoredDirMock := MockFileInfo{}
ignoredDirMock.On("Name").Return("ethernet0")
ignoredDirMock.On("IsDir").Return(true)

mockReadDir(dependencies, ibftBasePath, "", &ignoredFileMock, &ignoredDirMock, &targetMock)

ret := GetDisks(&config.SubprocessConfig{}, dependencies)
Expect(ret).To(Equal([]*models.Disk{
{
ID: "/dev/disk/by-path/ip-192.168.130.10:3260-iscsi-iqn.2022-01.com.redhat.foo:disk0-lun-0",
ByPath: "/dev/disk/by-path/ip-192.168.130.10:3260-iscsi-iqn.2022-01.com.redhat.foo:disk0-lun-0",
DriveType: models.DriveTypeISCSI,
Hctl: "",
Model: "disk0",
Name: "sda",
Path: "/dev/sda",
Serial: "6001405961d8b6f55cf48beb0de296b2",
SizeBytes: 21474836480,
Vendor: "LIO-ORG",
Wwn: "0x6001405961d8b6f55cf48beb0de296b2",
Bootable: true,
Smart: "",
Holders: "",
InstallationEligibility: models.DiskInstallationEligibility{
Eligible: true,
},
Iscsi: &models.Iscsi{
HostIPAddress: "1.2.3.4",
},
},
},
}))
}))
})
It("Is not in running state", func() {
// block device symlink to iSCSI device
dependencies.On("EvalSymlinks", "/sys/block/sda").Return("/sys/devices/platform/host2/session1/target2:0:0/2:0:0:1/block/sda", nil)
// Report iSCSI host IP address
dependencies.On("ReadFile", "/sys/class/iscsi_host/host2/ipaddress").Return([]byte("1.2.3.4"), nil)
// iSCSI state validation
dependencies.On("ReadFile", "/sys/block/sda/device/state").Return([]byte("not-running"), nil)
// iBFT target validation
dependencies.On("ReadFile", "/sys/class/iscsi_session/session1/targetname").Return([]byte("iqn.2023-01.com.example:tm1"), nil)
dependencies.On("ReadFile", "/sys/firmware/ibft/target0/target-name").Return([]byte("iqn.2023-01.com.example:tm1"), nil)
mockReadDir(dependencies, ibftBasePath, "", &targetMock)

ret := GetDisks(&config.SubprocessConfig{}, dependencies)
Expect(ret[0].InstallationEligibility.Eligible).To(BeFalse())
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(HaveLen(1))
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(ContainElement("iSCSI disk is not in running state"))

})
It("Missing state file", func() {
// block device symlink to iSCSI device
dependencies.On("EvalSymlinks", "/sys/block/sda").Return("/sys/devices/platform/host2/session1/target2:0:0/2:0:0:1/block/sda", nil)
// Report iSCSI host IP address
dependencies.On("ReadFile", "/sys/class/iscsi_host/host2/ipaddress").Return([]byte("1.2.3.4"), nil)
// iSCSI state validation
dependencies.On("ReadFile", "/sys/block/sda/device/state").Return([]byte(""), fmt.Errorf("file not found"))
// iBFT target validation
dependencies.On("ReadFile", "/sys/class/iscsi_session/session1/targetname").Return([]byte("iqn.2023-01.com.example:tm1"), nil)
dependencies.On("ReadFile", "/sys/firmware/ibft/target0/target-name").Return([]byte("iqn.2023-01.com.example:tm1"), nil)
mockReadDir(dependencies, ibftBasePath, "", &targetMock)

ret := GetDisks(&config.SubprocessConfig{}, dependencies)
Expect(ret[0].InstallationEligibility.Eligible).To(BeFalse())
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(HaveLen(1))
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(ContainElement("Failed to read state of iSCSI disk"))

})
It("target is not in iBFT", func() {
// block device symlink to iSCSI device
dependencies.On("EvalSymlinks", "/sys/block/sda").Return("/sys/devices/platform/host2/session1/target2:0:0/2:0:0:1/block/sda", nil)
// Report iSCSI host IP address
dependencies.On("ReadFile", "/sys/class/iscsi_host/host2/ipaddress").Return([]byte("1.2.3.4"), nil)
// iSCSI state validation
dependencies.On("ReadFile", "/sys/block/sda/device/state").Return([]byte("running"), nil)
// iBFT target validation
dependencies.On("ReadFile", "/sys/class/iscsi_session/session1/targetname").Return([]byte("this-is-not-iqn.2023-01.com.example:tm1"), nil)
dependencies.On("ReadFile", "/sys/firmware/ibft/target0/target-name").Return([]byte("iqn.2023-01.com.example:tm1"), nil)
mockReadDir(dependencies, ibftBasePath, "", &targetMock)

ret := GetDisks(&config.SubprocessConfig{}, dependencies)
Expect(ret[0].InstallationEligibility.Eligible).To(BeFalse())
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(HaveLen(1))
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(ContainElement("iSCSI disk is missing from iBFT"))
})
It("targetname file is missing from iSCSI session", func() {
// block device symlink to iSCSI device
dependencies.On("EvalSymlinks", "/sys/block/sda").Return("/sys/devices/platform/host2/session1/target2:0:0/2:0:0:1/block/sda", nil)
// Report iSCSI host IP address
dependencies.On("ReadFile", "/sys/class/iscsi_host/host2/ipaddress").Return([]byte("1.2.3.4"), nil)
// iSCSI state validation
dependencies.On("ReadFile", "/sys/block/sda/device/state").Return([]byte("running"), nil)
// iBFT target validation
dependencies.On("ReadFile", "/sys/class/iscsi_session/session1/targetname").Return([]byte(""), fmt.Errorf("file not found"))

ret := GetDisks(&config.SubprocessConfig{}, dependencies)
Expect(ret[0].InstallationEligibility.Eligible).To(BeFalse())
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(HaveLen(1))
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(ContainElement("Cannot find iSCSI target name"))
})
It("iBFT is missing", func() {
// block device symlink to iSCSI device
dependencies.On("EvalSymlinks", "/sys/block/sda").Return("/sys/devices/platform/host2/session1/target2:0:0/2:0:0:1/block/sda", nil)
// Report iSCSI host IP address
dependencies.On("ReadFile", "/sys/class/iscsi_host/host2/ipaddress").Return([]byte("1.2.3.4"), nil)
// iSCSI state validation
dependencies.On("ReadFile", "/sys/block/sda/device/state").Return([]byte("running"), nil)
// iBFT target validation
dependencies.On("ReadFile", "/sys/class/iscsi_session/session1/targetname").Return([]byte("iqn.2023-01.com.example:tm1"), nil)
mockReadDir(dependencies, ibftBasePath, "no directory found")

ret := GetDisks(&config.SubprocessConfig{}, dependencies)
Expect(ret[0].InstallationEligibility.Eligible).To(BeFalse())
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(HaveLen(1))
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(ContainElement("iBFT firmware is missing"))
})
It("iBFT target-name is un-readable", func() {
// block device symlink to iSCSI device
dependencies.On("EvalSymlinks", "/sys/block/sda").Return("/sys/devices/platform/host2/session1/target2:0:0/2:0:0:1/block/sda", nil)
// Report iSCSI host IP address
dependencies.On("ReadFile", "/sys/class/iscsi_host/host2/ipaddress").Return([]byte("1.2.3.4"), nil)
// iSCSI state validation
dependencies.On("ReadFile", "/sys/block/sda/device/state").Return([]byte("running"), nil)
// iBFT target validation
dependencies.On("ReadFile", "/sys/class/iscsi_session/session1/targetname").Return([]byte("this-is-not-iqn.2023-01.com.example:tm1"), nil)
dependencies.On("ReadFile", "/sys/firmware/ibft/target0/target-name").Return([]byte(""), fmt.Errorf("file not found"))
mockReadDir(dependencies, ibftBasePath, "", &targetMock)

ret := GetDisks(&config.SubprocessConfig{}, dependencies)
Expect(ret[0].InstallationEligibility.Eligible).To(BeFalse())
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(HaveLen(1))
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(ContainElement("iSCSI disk is missing from iBFT"))
})
It("iSCSI block device symlink is not available", func() {
// block device symlink to iSCSI device
dependencies.On("EvalSymlinks", "/sys/block/sda").Return("", fmt.Errorf("symlink not found"))
// iSCSI state validation
dependencies.On("ReadFile", "/sys/block/sda/device/state").Return([]byte("running"), nil)

ret := GetDisks(&config.SubprocessConfig{}, dependencies)
Expect(ret[0].InstallationEligibility.Eligible).To(BeFalse())
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(HaveLen(1))
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(ContainElement("Cannot find iSCSI session"))
})
It("iSCSI block device symlink does not contain iSCSI session or host", func() {
// block device symlink to iSCSI device
dependencies.On("EvalSymlinks", "/sys/block/sda").Return("/something/invalid", nil)
// iSCSI state validation
dependencies.On("ReadFile", "/sys/block/sda/device/state").Return([]byte("running"), nil)

ret := GetDisks(&config.SubprocessConfig{}, dependencies)
Expect(ret[0].InstallationEligibility.Eligible).To(BeFalse())
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(HaveLen(1))
Expect(ret[0].InstallationEligibility.NotEligibleReasons).To(ContainElement("Cannot find iSCSI session"))
})
})

It("FC device", func() {
Expand Down

0 comments on commit a8a3349

Please sign in to comment.