diff --git a/src/inventory/disks.go b/src/inventory/disks.go index 3fbe4c024..6350abcb5 100644 --- a/src/inventory/disks.go +++ b/src/inventory/disks.go @@ -20,6 +20,7 @@ import ( const ( applianceAgentPrefix = "agent" byIdLocation = "/dev/disk/by-id" + ibftBasePath = "/sys/firmware/ibft" wwnPrefix = "wwn-" ) @@ -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) { @@ -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") @@ -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) + +} diff --git a/src/inventory/disks_test.go b/src/inventory/disks_test.go index 48a7cdcac..e1038efee 100644 --- a/src/inventory/disks_test.go +++ b/src/inventory/disks_test.go @@ -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() {