diff --git a/libs/ui-lib-tests/cypress/fixtures/storage/host-inventory.ts b/libs/ui-lib-tests/cypress/fixtures/storage/host-inventory.ts index b68597a3fe..15b38170a6 100644 --- a/libs/ui-lib-tests/cypress/fixtures/storage/host-inventory.ts +++ b/libs/ui-lib-tests/cypress/fixtures/storage/host-inventory.ts @@ -238,4 +238,141 @@ const createHostInventory = (id: number, memory: number, diskSpace: number) => { }; }; -export default createHostInventory; +const disksWithHolders = [ + { + drive_type: 'RAID', + id: '/dev/md0', + installation_eligibility: { eligible: true }, + name: 'md0', + path: '/dev/md0', + size_bytes: 21455962112, + }, + { + drive_type: 'LVM', + id: '/dev/dm-0', + installation_eligibility: { eligible: true }, + name: 'dm-0', + path: '/dev/dm-0', + size_bytes: 15032385536, + }, + { + by_id: '/dev/disk/by-id/wwn-0x60000000000000000e00000000010001', + drive_type: 'Multipath', + has_uuid: true, + id: '/dev/disk/by-id/wwn-0x60000000000000000e00000000010001', + installation_eligibility: { eligible: true }, + name: 'dm-1', + path: '/dev/dm-1', + size_bytes: 10737418240, + }, + { + drive_type: 'LVM', + id: '/dev/dm-2', + installation_eligibility: { eligible: true }, + name: 'dm-2', + path: '/dev/dm-2', + size_bytes: 4294967296, + }, + { + by_path: + '/dev/disk/by-path/ip-192.168.122.41:3260-iscsi-iqn.2015-06.com.example.test:target1-lun-1', + drive_type: 'iSCSI', + has_uuid: true, + hctl: '8:0:0:1', + holders: 'dm-1', + id: '/dev/disk/by-path/ip-192.168.122.41:3260-iscsi-iqn.2015-06.com.example.test:target1-lun-1', + installation_eligibility: { eligible: true }, + model: 'VIRTUAL-DISK', + name: 'sda', + path: '/dev/sda', + serial: '60000000000000000e00000000010001', + size_bytes: 10737418240, + vendor: 'IET', + wwn: '0x60000000000000000e00000000010001', + }, + { + by_path: '/dev/disk/by-path/ip-192.168.123.49:3260', + drive_type: 'iSCSI', + has_uuid: true, + hctl: '9:0:0:1', + holders: 'dm-1', + id: '/dev/disk/by-path/ip-192.168.123.49:3260', + installation_eligibility: { eligible: true }, + model: 'VIRTUAL-DISK', + name: 'sdb', + path: '/dev/sdb', + serial: '60000000000000000e00000000010001', + size_bytes: 10737418240, + vendor: 'IET', + wwn: '0x60000000000000000e00000000010001', + }, + { + bootable: true, + by_path: '/dev/disk/by-path/pci-0000:00:05.0-ata-3', + drive_type: 'ODD', + has_uuid: true, + hctl: '4:0:0:0', + id: '/dev/disk/by-path/pci-0000:00:05.0-ata-3', + installation_eligibility: { eligible: true }, + is_installation_media: true, + model: 'QEMU_DVD-ROM', + name: 'sr0', + path: '/dev/sr0', + removable: true, + serial: 'QM00009', + size_bytes: 1135607808, + smart: 'SMART support is: Unavailable - device lacks SMART capability.\n', + vendor: 'QEMU', + }, + { + by_path: '/dev/disk/by-path/pci-0000:00:07.0', + drive_type: 'HDD', + holders: 'dm-0,dm-2', + id: '/dev/disk/by-path/pci-0000:00:07.0', + installation_eligibility: { eligible: true }, + name: 'vda', + path: '/dev/vda', + size_bytes: 10737418240, + vendor: '0x1af4', + }, + { + by_path: '/dev/disk/by-path/pci-0000:00:08.0', + drive_type: 'HDD', + holders: 'dm-0,dm-2', + id: '/dev/disk/by-path/pci-0000:00:08.0', + installation_eligibility: { eligible: true }, + name: 'vdb', + path: '/dev/vdb', + size_bytes: 10737418240, + vendor: '0x1af4', + }, + { + by_path: '/dev/disk/by-path/pci-0000:00:09.0', + drive_type: 'HDD', + holders: 'md0', + id: '/dev/disk/by-path/pci-0000:00:09.0', + installation_eligibility: { eligible: true }, + name: 'vdc', + path: '/dev/vdc', + size_bytes: 10737418240, + vendor: '0x1af4', + }, + { + by_path: '/dev/disk/by-path/pci-0000:00:0a.0', + drive_type: 'HDD', + holders: 'md0', + id: '/dev/disk/by-path/pci-0000:00:0a.0', + installation_eligibility: { eligible: true }, + name: 'vdd', + path: '/dev/vdd', + size_bytes: 10737418240, + vendor: '0x1af4', + }, +]; + +const createHostInventoryWithDiskHolders = (id: number, memory: number, diskSpace: number) => ({ + ...createHostInventory(id, memory, diskSpace), + disks: disksWithHolders, +}); + +export { createHostInventory, createHostInventoryWithDiskHolders }; diff --git a/libs/ui-lib-tests/cypress/fixtures/storage/index.ts b/libs/ui-lib-tests/cypress/fixtures/storage/index.ts index 083304428c..49a7f69840 100644 --- a/libs/ui-lib-tests/cypress/fixtures/storage/index.ts +++ b/libs/ui-lib-tests/cypress/fixtures/storage/index.ts @@ -1,5 +1,6 @@ import storageCluster from './storage-cluster'; -import storageHosts from './storage-hosts'; +import { createMultinodeFixtureMapping } from '../create-mn'; +import { storageHosts, hostsWithDiskHolders } from './storage-hosts'; const createStorageFixtureMapping = { clusters: { @@ -10,4 +11,13 @@ const createStorageFixtureMapping = { }, }; -export { createStorageFixtureMapping }; +const createDiskHoldersFixtureMapping = { + clusters: { + default: createMultinodeFixtureMapping.clusters.READY_TO_INSTALL, + }, + hosts: { + default: hostsWithDiskHolders, + }, +}; + +export { createStorageFixtureMapping, createDiskHoldersFixtureMapping }; diff --git a/libs/ui-lib-tests/cypress/fixtures/storage/storage-hosts.ts b/libs/ui-lib-tests/cypress/fixtures/storage/storage-hosts.ts index a42ffecefa..de34eae383 100644 --- a/libs/ui-lib-tests/cypress/fixtures/storage/storage-hosts.ts +++ b/libs/ui-lib-tests/cypress/fixtures/storage/storage-hosts.ts @@ -1,5 +1,5 @@ import { fakeClusterId } from '../cluster/base-cluster'; -import createHostInventory from './host-inventory'; +import { createHostInventory, createHostInventoryWithDiskHolders } from './host-inventory'; const operatorValidations = [ { @@ -118,6 +118,14 @@ const workerMemory = 7179869184; // TODO const masterDisk = 17797418240; // 17.80 GB const workerDisk = 10476748240; // 10.48 GB +const installationDiskIds = [ + '/dev/disk/by-path/pci-0000:00:08.0', // LVM + '/dev/disk/by-path/pci-0000:00:09.0', // raid + '/dev/disk/by-path/ip-192.168.123.49:3260', //multipath + '/dev/disk/by-path/pci-0000:00:05.0-ata-3', // sr0 + '/dev/disk/by-path/pci-0000:00:05.0-ata-3', // sr0 +]; + const hosts = [ { checked_in_at: '2022-08-16T15:02:43.543Z', @@ -368,4 +376,17 @@ const hosts = [ }, ]; -export default hosts; +const hostsWithDiskHolders = hosts.map((host, index) => ({ + ...host, + inventory: JSON.stringify( + createHostInventoryWithDiskHolders( + index, + host['role'] === 'master' ? masterMemory : workerMemory, + host['role'] === 'master' ? masterDisk : workerDisk, + ), + ), + installation_disk_id: installationDiskIds[index], + skip_formatting_disks: false, +})); + +export { hosts as storageHosts, hostsWithDiskHolders }; diff --git a/libs/ui-lib-tests/cypress/integration/storage/storage-step-disk-holders.cy.ts b/libs/ui-lib-tests/cypress/integration/storage/storage-step-disk-holders.cy.ts new file mode 100644 index 0000000000..46c56a2dfb --- /dev/null +++ b/libs/ui-lib-tests/cypress/integration/storage/storage-step-disk-holders.cy.ts @@ -0,0 +1,92 @@ +import { commonActions } from '../../views/common'; +import { ValidateDiskHoldersParams, hostsTableSection } from '../../views/hostsTableSection'; +import { StorageForm } from '../../views/forms/Storage/StorageForm'; + +describe(`Assisted Installer Storage Step`, () => { + let storageForm; + const disks = [ + { name: 'vda' }, + { name: 'vdb' }, + { name: 'dm-1' }, + { name: 'sda', indented: true }, + { name: 'sdb', indented: true }, + { name: 'md0' }, + { name: 'vdc', indented: true }, + { name: 'vdd', indented: true }, + { name: 'sr0' }, + ] as ValidateDiskHoldersParams; + + const warningTexts = [ + 'LVM logical volumes were found on the installation disk vdb selected for host storage-test-odf-master-1 and will be deleted during installation.', + 'The installation disk vdc selected for host storage-test-odf-master-2, is part of a software RAID that will be deleted during the installation.', + 'The installation disk sdb selected for host storage-test-odf-master-3 is managed by multipath. We strongly recommend using the multipath device dm-1 to improve reliability.', + ]; + + const setTestStartSignal = (activeSignal: string) => { + cy.setTestEnvironment({ + activeSignal, + activeScenario: 'AI_DISK_HOLDERS_CLUSTER', + }); + }; + + before(() => { + setTestStartSignal('READY_TO_INSTALL'); + }); + + describe(`Host storage`, () => { + beforeEach(() => { + setTestStartSignal('READY_TO_INSTALL'); + commonActions.visitClusterDetailsPage(); + commonActions.startAtWizardStep('Storage'); + + storageForm = new StorageForm(); + }); + + it('Should display the correct alerts', () => { + storageForm.diskLimitationAlert.title.should( + 'contain.text', + 'Warning alert:Installation disk limitations', + ); + + storageForm.diskLimitationAlert.description.find('li').then(($res) => { + expect($res).to.have.length(3); + warningTexts.forEach((text, index) => expect($res[index]).to.contain(text)); + }); + + storageForm.diskFormattingAlert.title.should( + 'contain.text', + 'Warning alert:All bootable disks, except for read-only disks, will be formatted during installation. Make sure to back up any critical data before proceeding.', + ); + }); + + it('Should display the correct warning for LVM', () => { + disks[1].warning = true; + + hostsTableSection.getHostDisksExpander(0).click(); + hostsTableSection.validateGroupingByDiskHolders(disks, warningTexts[0]); + }); + + it('Should display the correct warning for RAID', () => { + disks[1].warning = false; + disks[6].warning = true; + + hostsTableSection.getHostDisksExpander(1).click(); + hostsTableSection.validateGroupingByDiskHolders(disks, warningTexts[1]); + }); + + it('Should display the correct warning for multipath', () => { + disks[6].warning = false; + disks[4].warning = true; + + hostsTableSection.getHostDisksExpander(2).click(); + hostsTableSection.validateGroupingByDiskHolders(disks, warningTexts[2]); + }); + + it('Should display no warnings', () => { + disks[6].warning = false; + + hostsTableSection.getHostDisksExpander(2).click(); + hostsTableSection.validateGroupingByDiskHolders(disks); + }); + }); +}); diff --git a/libs/ui-lib-tests/cypress/integration/use-cases/create-cluster/with-mce-operator.cy.ts b/libs/ui-lib-tests/cypress/integration/use-cases/create-cluster/with-mce-operator.cy.ts index e7a862a883..1acaf10ca8 100644 --- a/libs/ui-lib-tests/cypress/integration/use-cases/create-cluster/with-mce-operator.cy.ts +++ b/libs/ui-lib-tests/cypress/integration/use-cases/create-cluster/with-mce-operator.cy.ts @@ -1,5 +1,5 @@ import { commonActions } from '../../../views/common'; -import OperatorsForm from '../../../views/forms/OperatorsForm'; +import OperatorsForm from '../../../views/forms/Operators/OperatorsForm'; describe(`Create cluster with mce operator enabled`, () => { const setTestStartSignal = (activeSignal: string) => { diff --git a/libs/ui-lib-tests/cypress/support/interceptors.ts b/libs/ui-lib-tests/cypress/support/interceptors.ts index 3ea02b2672..3891009198 100644 --- a/libs/ui-lib-tests/cypress/support/interceptors.ts +++ b/libs/ui-lib-tests/cypress/support/interceptors.ts @@ -67,6 +67,9 @@ const getScenarioFixtureMapping = () => { case 'AI_STORAGE_CLUSTER': fixtureMapping = fixtures.createStorageFixtureMapping; break; + case 'AI_DISK_HOLDERS_CLUSTER': + fixtureMapping = fixtures.createDiskHoldersFixtureMapping; + break; case 'AI_CREATE_STATIC_IP': fixtureMapping = fixtures.createStaticIpFixtureMapping; break; @@ -154,6 +157,11 @@ const setScenarioEnvVars = (activeScenario) => { Cypress.env('NUM_MASTERS', 3); Cypress.env('NUM_WORKERS', 2); break; + case 'AI_DISK_HOLDERS_CLUSTER': + Cypress.env('CLUSTER_NAME', 'ai-e2e-disk-holders'); + Cypress.env('NUM_MASTERS', 3); + Cypress.env('NUM_WORKERS', 2); + break; case 'AI_CREATE_STATIC_IP': Cypress.env('CLUSTER_NAME', 'ai-e2e-static-ip'); break; diff --git a/libs/ui-lib-tests/cypress/support/variables/index.ts b/libs/ui-lib-tests/cypress/support/variables/index.ts index 275b182c04..0ad71a0312 100644 --- a/libs/ui-lib-tests/cypress/support/variables/index.ts +++ b/libs/ui-lib-tests/cypress/support/variables/index.ts @@ -2,9 +2,7 @@ import './common'; import './cluster-list'; import './cluster-details'; import './host-discovery'; -import './installation'; import './misc'; import './networking'; import './review-create'; import './test-constants'; -import './storage-step'; diff --git a/libs/ui-lib-tests/cypress/support/variables/installation.ts b/libs/ui-lib-tests/cypress/support/variables/installation.ts deleted file mode 100644 index 34ef378ee1..0000000000 --- a/libs/ui-lib-tests/cypress/support/variables/installation.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Installation -Cypress.env('hostnameDataTestId', `[data-testid=host-name]`); -Cypress.env('roleDataLabel', `td[data-label="Role"][data-testid="host-role"]`); -Cypress.env('odfUsageDataLabel', `td[data-label="ODF Usage"]`); -Cypress.env('cpuCoresDataLabel', `td[data-label='CPU Cores']`); -Cypress.env('memoryDataLabel', `td[data-label='Memory']`); -Cypress.env('totalStorageDataLabel', `td[data-label='Total storage']`); -Cypress.env('diskNumberDataLabel', `td[data-label='Number of disks']`); -Cypress.env( - 'clusterDetailButtonDownloadKubeconfigId', - '#cluster-detail-button-download-kubeconfig', -); -Cypress.env( - 'clusterDetailClusterCredsTshootHintOpen', - 'cluster-detail-cluster-creds-troubleshooting-hint-open', -); -Cypress.env('clusterProgressStatusValueId', '#cluster-progress-status-value'); -Cypress.env('operatorsProgressItem', `[data-testid=operators-progress-item]`); -Cypress.env('skipFormattingDataLabel', `td[data-label='Format?']`); diff --git a/libs/ui-lib-tests/cypress/support/variables/storage-step.ts b/libs/ui-lib-tests/cypress/support/variables/storage-step.ts deleted file mode 100644 index 02de7143a1..0000000000 --- a/libs/ui-lib-tests/cypress/support/variables/storage-step.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Storage Step -Cypress.env('skipFormattingWarningTitle', 'There might be issues with the boot order'); -Cypress.env( - 'skipFormattingWarningDesc', - 'You have opted out of formatting bootable disks on some hosts. To ensure the hosts reboot into the expected installation disk, manual user intervention might be required during OpenShift installation.', -); -Cypress.env('warningIconFillColor', '#f0ab00'); diff --git a/libs/ui-lib-tests/cypress/support/variables/test-constants.ts b/libs/ui-lib-tests/cypress/support/variables/test-constants.ts index bee5916706..ea4e13d572 100644 --- a/libs/ui-lib-tests/cypress/support/variables/test-constants.ts +++ b/libs/ui-lib-tests/cypress/support/variables/test-constants.ts @@ -1,42 +1,11 @@ // timeouts -Cypress.env('DEFAULT_API_REQUEST_TIMEOUT', 20 * 1000); -Cypress.env('DEFAULT_CREATE_CLUSTER_BUTTON_SHOW_TIMEOUT', 60 * 1000); -Cypress.env('DEFAULT_SAVE_BUTTON_TIMEOUT', 30 * 1000); Cypress.env('HOST_REGISTRATION_TIMEOUT', 11 * 1000); Cypress.env('HOST_DISCOVERY_TIMEOUT', 11 * 1000); Cypress.env('HOST_READY_TIMEOUT', 11 * 1000); -Cypress.env('VALIDATE_CHANGES_TIMEOUT', 10 * 1000); Cypress.env('START_INSTALLATION_TIMEOUT', 2.5 * 60 * 1000); -Cypress.env('INSTALL_PREPARATION_TIMEOUT', 5 * 60 * 1000); Cypress.env('GENERATE_ISO_TIMEOUT', 2 * 60 * 1000); -Cypress.env('FILE_DOWNLOAD_TIMEOUT', 60 * 1000); -Cypress.env('ISO_DOWNLOAD_TIMEOUT', 60 * 60 * 1000); -Cypress.env('CLUSTER_CREATION_TIMEOUT', 9000000); -Cypress.env('CLUSTER_REGISTRATION_TIMEOUT', 20 * 60 * 1000); -Cypress.env('DAY2_HOST_INSTALLATION_TIMEOUT', 10 * 60 * 1000); Cypress.env('HOST_STATUS_INSUFFICIENT_TIMEOUT', 300000); -Cypress.env('NETWORK_LATENCY_ALERT_MESSAGE_TIMEOUT', 300000); -Cypress.env('WAIT_FOR_PROGRESS_STATUS_INSTALLED', 1800000); -Cypress.env('KUBECONFIG_DOWNLOAD_TIMEOUT', 300000); -Cypress.env('WAIT_FOR_HEADER_TIMEOUT', 120000); -Cypress.env('WAIT_FOR_CONSOLE_TIMEOUT', 2900000); Cypress.env('DNS_RESOLUTION_ALERT_MESSAGE_TIMEOUT', 900000); // Deployment Cypress.env('NUM_MASTERS', parseInt(Cypress.env('NUM_MASTERS'))); Cypress.env('NUM_WORKERS', parseInt(Cypress.env('NUM_WORKERS'))); -Cypress.env('MASTER_HOST_ROW_MAX_INDEX', Number(Cypress.env('NUM_MASTERS')) * 2 - 2); -Cypress.env( - 'WORKER_HOST_ROW_MAX_INDEX', - (Number(Cypress.env('NUM_MASTERS')) + Number(Cypress.env('NUM_WORKERS'))) * 2 - 2, -); -Cypress.env('DISCOVERY_IMAGE_GLOB_PATTERN', 'discovery_image_*.iso'); -Cypress.env('DISCOVERY_IMAGE_PATH', '/var/lib/libvirt/images/0/cluster-discovery.iso'); -Cypress.env('DEFAULT_CLUSTER_NAME', 'ocp-edge-cluster-0'); -Cypress.env('OPENSHIFT_CONF', '/etc/NetworkManager/dnsmasq.d/openshift.conf'); -Cypress.env('HYPERVISOR_IP', '192.168.123.1'); -Cypress.env('BAREMETAL_QE3', 'r640-u09.qe3.kni.lab.eng.bos.redhat.com'); -Cypress.env('HOST_ROLE_MASTER_LABEL', 'Control plane node'); -Cypress.env('HOST_ROLE_WORKER_LABEL', 'Worker'); -Cypress.env('VMWARE_ENV', false); -Cypress.env('VMWARE_SNO', false); -Cypress.env('IS_BAREMETAL', false); diff --git a/libs/ui-lib-tests/cypress/views/forms/OperatorsForm.ts b/libs/ui-lib-tests/cypress/views/forms/Operators/OperatorsForm.ts similarity index 100% rename from libs/ui-lib-tests/cypress/views/forms/OperatorsForm.ts rename to libs/ui-lib-tests/cypress/views/forms/Operators/OperatorsForm.ts diff --git a/libs/ui-lib-tests/cypress/views/forms/Storage/StorageForm.ts b/libs/ui-lib-tests/cypress/views/forms/Storage/StorageForm.ts new file mode 100644 index 0000000000..448a377eed --- /dev/null +++ b/libs/ui-lib-tests/cypress/views/forms/Storage/StorageForm.ts @@ -0,0 +1,18 @@ +import { Alert } from '../../reusableComponents/Alert'; + +export class StorageForm { + static readonly alias = `@${StorageForm.name}`; + static readonly selector = '.pf-c-wizard__main-body'; + + constructor() { + cy.findWithinOrGet(StorageForm.selector).as(StorageForm.name); + } + + get diskLimitationAlert() { + return new Alert(StorageForm.alias, '[data-testid="diskLimitationsAlert"]'); + } + + get diskFormattingAlert() { + return new Alert(StorageForm.alias, '[data-testid="alert-format-bootable-disks"]'); + } +} diff --git a/libs/ui-lib-tests/cypress/views/hostsTableSection.ts b/libs/ui-lib-tests/cypress/views/hostsTableSection.ts index b0f50e93a8..6d0056ec51 100644 --- a/libs/ui-lib-tests/cypress/views/hostsTableSection.ts +++ b/libs/ui-lib-tests/cypress/views/hostsTableSection.ts @@ -1,10 +1,12 @@ +export type ValidateDiskHoldersParams = { name: string; indented?: boolean; warning?: boolean }[]; + export const hostsTableSection = { validateHostNames: ( numMasters: number = Cypress.env('NUM_MASTERS'), numWorkers: number = Cypress.env('NUM_WORKERS'), hostNames = Cypress.env('requestedHostnames'), ) => { - cy.get(Cypress.env('hostnameDataTestId')) + cy.get('[data-testid=host-name]') .should('have.length', numMasters + numWorkers) .each((hostName, idx) => { expect(hostName).to.contain(hostNames[idx]); @@ -14,14 +16,14 @@ export const hostsTableSection = { numMasters: number = Cypress.env('NUM_MASTERS'), numWorkers: number = Cypress.env('NUM_WORKERS'), ) => { - cy.get(Cypress.env('roleDataLabel')) + cy.get('td[data-testid="host-role"]') .should('have.length', numMasters + numWorkers) .each((hostRole, idx) => { const isMaster = idx <= numMasters - 1; if (isMaster) { - expect(hostRole).to.contain(Cypress.env('HOST_ROLE_MASTER_LABEL')); + expect(hostRole).to.contain('Control plane node'); } else { - expect(hostRole).to.contain(Cypress.env('HOST_ROLE_WORKER_LABEL')); + expect(hostRole).to.contain('Worker'); } }); }, @@ -29,7 +31,7 @@ export const hostsTableSection = { numMasters: number = Cypress.env('NUM_MASTERS'), numWorkers: number = Cypress.env('NUM_WORKERS'), ) => { - cy.get(Cypress.env('cpuCoresDataLabel')) + cy.get('td[data-label="CPU Cores"]') .should('have.length', numMasters + numWorkers) .each((hostCpuCores, idx) => { const isMaster = idx <= numMasters - 1; @@ -44,7 +46,7 @@ export const hostsTableSection = { numMasters: number = Cypress.env('NUM_MASTERS'), numWorkers: number = Cypress.env('NUM_WORKERS'), ) => { - cy.get(Cypress.env('memoryDataLabel')) + cy.get('td[data-label="Memory"]') .should('have.length', numMasters + numWorkers) .each((hostMemory, idx) => { const isMaster = idx <= numMasters - 1; @@ -59,7 +61,7 @@ export const hostsTableSection = { numMasters: number = Cypress.env('NUM_MASTERS'), numWorkers: number = Cypress.env('NUM_WORKERS'), ) => { - cy.get(Cypress.env('totalStorageDataLabel')) + cy.get('td[data-label="Total storage"]') .should('have.length', numMasters + numWorkers) .each((hostDisk, idx) => { const isMaster = idx <= numMasters - 1; @@ -101,4 +103,22 @@ export const hostsTableSection = { ); }); }, + validateGroupingByDiskHolders: (disks: ValidateDiskHoldersParams, message?: string) => { + cy.get('td[data-testid="disk-name"]').then(($diskNames) => { + disks.forEach((disk, index) => { + cy.wrap($diskNames).eq(index).should('contain.text', disk.name); + + if (disk.indented) { + cy.wrap($diskNames).eq(index).find('span').should('exist'); + } else { + cy.wrap($diskNames).eq(index).find('span').should('not.exist'); + } + + if (disk.warning && message) { + cy.wrap($diskNames).eq(index).find('svg').click(); + cy.get('[data-testid="disk-limitations-popover"').should('contain.text', message); + } + }); + }); + }, }; diff --git a/libs/ui-lib-tests/cypress/views/installationPage.ts b/libs/ui-lib-tests/cypress/views/installationPage.ts deleted file mode 100644 index 4157d1a904..0000000000 --- a/libs/ui-lib-tests/cypress/views/installationPage.ts +++ /dev/null @@ -1,90 +0,0 @@ -export const installationPage = { - validateInstallConfigWarning: (msg) => { - cy.get(`.pf-m-warning:contains(${msg})`); - }, - waitForDownloadKubeconfigToBeEnabled: (timeout = 600000) => { - cy.get(Cypress.env('clusterDetailButtonDownloadKubeconfigId'), { - timeout: timeout, - }).should('be.enabled'); - }, - downloadKubeconfigAndSetKubeconfigEnv: ( - kubeconfigFile, - timeout = Cypress.env('KUBECONFIG_DOWNLOAD_TIMEOUT'), - ) => { - cy.get(Cypress.env('clusterDetailButtonDownloadKubeconfigId')).should('be.visible').click(); - cy.readFile(kubeconfigFile, { timeout: timeout }) - .should('have.length.gt', 50) - .then((kubeconfig) => { - Cypress.env('kubeconfig', kubeconfig); - }); - }, - copyAllFiles: () => { - installationPage.downloadKubeconfigAndSetKubeconfigEnv( - Cypress.env('kubeconfigFile'), - Cypress.env('KUBECONFIG_DOWNLOAD_TIMEOUT'), - ); - cy.runCopyCmd(Cypress.env('kubeconfigFile'), '~/clusterconfigs/auth/kubeconfig'); - cy.runCopyCmd(Cypress.env('kubeconfigFile'), '~/kubeconfig'); - cy.runCopyCmd( - Cypress.env('kubeconfigFile'), - `${Cypress.env( - 'BASE_REPO_DIR_REMOTE', - )}/linchpin-workspace/hooks/ansible/ocp-edge-setup/kubeconfig`, - ); - cy.setKubeAdminPassword(Cypress.env('API_BASE_URL'), Cypress.env('clusterId'), true); - cy.get('@kubeadmin-password').then((kubeadminPassword) => { - cy.writeFile(Cypress.env('kubeadminPasswordFilePath'), kubeadminPassword); - }); - cy.runCopyCmd( - Cypress.env('kubeadminPasswordFilePath'), - '~/clusterconfigs/auth/kubeadmin-password', - ); - cy.runCopyCmd(Cypress.env('kubeadminPasswordFilePath'), '~/kubeadmin-password'); - cy.runCopyCmd( - Cypress.env('kubeadminPasswordFilePath'), - `${Cypress.env( - 'BASE_REPO_DIR_REMOTE', - )}/linchpin-workspace/hooks/ansible/ocp-edge-setup/kubeadmin-password`, - ); - cy.setInstallConfig(Cypress.env('API_BASE_URL'), Cypress.env('clusterId'), true); - cy.get('@install-config').then((installConfig) => { - cy.writeFile(Cypress.env('installConfigFilePath'), installConfig); - }); - cy.runCopyCmd(Cypress.env('installConfigFilePath'), '~/install-config.yaml'); - cy.runCopyCmd( - Cypress.env('installConfigFilePath'), - `${Cypress.env( - 'BASE_REPO_DIR_REMOTE', - )}/linchpin-workspace/hooks/ansible/ocp-edge-setup/install-config.yaml`, - ); - }, - waitForConsoleTroubleShootingHintToBeVisible: ( - timeout = Cypress.env('WAIT_FOR_CONSOLE_TIMEOUT'), - ) => { - cy.newByDataTestId(Cypress.env('clusterDetailClusterCredsTshootHintOpen'), timeout) - .scrollIntoView() - .should('be.visible'); - }, - progressStatusShouldContain: ( - status = 'Installed', - timeout = Cypress.env('WAIT_FOR_PROGRESS_STATUS_INSTALLED'), - ) => { - cy.get(Cypress.env('clusterProgressStatusValueId')).scrollIntoView().should('be.visible'); - cy.get(Cypress.env('clusterProgressStatusValueId'), { - timeout: timeout, - }).should('contain.text', status); - }, - operatorsPopover: { - open: () => { - cy.get(Cypress.env('operatorsProgressItem')).click(); - }, - validateListItemContents: (msg) => { - cy.get('.pf-c-popover__body').within(() => { - cy.get('li').should('contain.text', msg); - }); - }, - close: () => { - cy.get('.pf-c-popover__content > .pf-c-button > svg').should('be.visible').click(); - }, - }, -}; diff --git a/libs/ui-lib-tests/cypress/views/reusableComponents/Alert.ts b/libs/ui-lib-tests/cypress/views/reusableComponents/Alert.ts new file mode 100644 index 0000000000..c3c53e637b --- /dev/null +++ b/libs/ui-lib-tests/cypress/views/reusableComponents/Alert.ts @@ -0,0 +1,20 @@ +export class Alert { + static readonly alias = `@${Alert.name}`; + static readonly selector = '.pf-c-alert'; + + constructor(parentAlias: string, id: string = Alert.selector) { + cy.findWithinOrGet(id, parentAlias).as(Alert.name); + } + + get body() { + return cy.get(Alert.alias); + } + + get title() { + return this.body.find('.pf-c-alert__title'); + } + + get description() { + return this.body.find('.pf-c-alert__description'); + } +} diff --git a/libs/ui-lib-tests/cypress/views/storagePage.ts b/libs/ui-lib-tests/cypress/views/storagePage.ts index 7e14a365e9..311b6ae47f 100644 --- a/libs/ui-lib-tests/cypress/views/storagePage.ts +++ b/libs/ui-lib-tests/cypress/views/storagePage.ts @@ -3,7 +3,7 @@ export const storagePage = { numMasters: number = Cypress.env('NUM_MASTERS'), numWorkers: number = Cypress.env('NUM_WORKERS'), ) => { - cy.get(Cypress.env('odfUsageDataLabel')) + cy.get('td[data-label="ODF Usage"]') .should('have.length', numMasters + numWorkers) .each((hostRole, idx) => { const isMaster = idx <= numMasters - 1; @@ -18,7 +18,7 @@ export const storagePage = { numMasters: number = Cypress.env('NUM_MASTERS'), numWorkers: number = Cypress.env('NUM_WORKERS'), ) => { - cy.get(Cypress.env('diskNumberDataLabel')) + cy.get('td[data-label="Number of disks"]') .should('have.length', numMasters + numWorkers) .each((hostDisk) => { expect(hostDisk).to.contain('3'); @@ -28,7 +28,7 @@ export const storagePage = { return cy.get(`input[id="select-formatted-${hostId}-${indexSelect}"]`); }, validateSkipFormattingDisks: (hostId: string, numDisks: number) => { - cy.get(Cypress.env('skipFormattingDataLabel')).should('have.length', numDisks); + cy.get("td[data-label='Format?']").should('have.length', numDisks); //Checking if checkboxes are checked/unchecked storagePage.getSkipFormattingCheckbox(hostId, 0).should('not.be.checked'); storagePage.getSkipFormattingCheckbox(hostId, 1).should('be.checked'); @@ -39,17 +39,20 @@ export const storagePage = { storagePage.getSkipFormattingCheckbox(hostId, 2).should('be.disabled'); }, validateSkipFormattingWarning: () => { - cy.get('.pf-c-alert__title').should('contain.text', Cypress.env('skipFormattingWarningTitle')); + cy.get('.pf-c-alert__title').should( + 'contain.text', + 'There might be issues with the boot order', + ); cy.get('.pf-c-alert__description').should( 'contain.text', - Cypress.env('skipFormattingWarningDesc'), + 'You have opted out of formatting bootable disks on some hosts. To ensure the hosts reboot into the expected installation disk, manual user intervention might be required during OpenShift installation.', ); }, validateSkipFormattingIcon: (diskId: string) => { //If a disk is skip formatting validate that warning icon is shown cy.get(`[data-testid="disk-row-${diskId}"] [data-testid="disk-name"]`).within( (/* $diskRow */) => { - cy.get('[role="img"]').should('have.attr', 'fill', Cypress.env('warningIconFillColor')); + cy.get('[role="img"]').should('have.attr', 'fill', '#f0ab00'); }, ); }, diff --git a/libs/ui-lib/lib/common/components/storage/DisksTable.tsx b/libs/ui-lib/lib/common/components/storage/DisksTable.tsx index a0abb9758c..a9376d5c42 100644 --- a/libs/ui-lib/lib/common/components/storage/DisksTable.tsx +++ b/libs/ui-lib/lib/common/components/storage/DisksTable.tsx @@ -1,5 +1,12 @@ import React from 'react'; -import { TextContent, Text, TextVariants, Popover } from '@patternfly/react-core'; +import { + TextContent, + Text, + TextVariants, + Popover, + Alert, + AlertVariant, +} from '@patternfly/react-core'; import { Table, TableHeader, @@ -55,6 +62,80 @@ const SkipFormattingDisk = () => ( ); +const getDiskLimitation = ( + diskName: Disk['name'], + hostName: Host['requestedHostname'], + holder: Disk, +) => { + if (holder.driveType) { + switch (holder.driveType) { + case 'LVM': + return `LVM logical volumes were found on the installation disk ${ + diskName as string + } selected for host ${hostName as string} and will be deleted during installation.`; + case 'RAID': + return `The installation disk ${diskName as string} selected for host ${ + hostName as string + }, is part of a software RAID that will be deleted during the installation.`; + case 'Multipath': + return `The installation disk ${diskName as string} selected for host ${ + hostName as string + } is managed by multipath. We strongly recommend using the multipath device ${ + holder.name as string + } to improve reliability.`; + } + } +}; + +const DiskName = ({ + disk, + disks, + host, + installationDiskId, +}: { + disk: Disk; + disks: Disk[]; + host: Host; + installationDiskId?: string; +}) => { + const isIndented = disk.holders?.split(',').length === 1; + let diskLimitations = null; + + if (disk.id === installationDiskId) { + const parentDisk = disks.find((d) => disk.holders?.split(',').includes(d.name as string)); + if (parentDisk) { + diskLimitations = getDiskLimitation(disk.name, host.requestedHostname, parentDisk); + } + } + + return ( + <> + {isIndented && } + {isInDiskSkipFormattingList(host, disk.id) && ( + } minWidth="20rem" maxWidth="30rem"> + + + )} + {' '} + {disk.bootable ? `${disk.name || ''} (bootable)` : disk.name} + {diskLimitations && ( + <> + {' '} + } + minWidth="20rem" + maxWidth="30rem" + data-testid="disk-limitations-popover" + > + + + + )} + + ); +}; + const DisksTable = ({ canEditDisks, host, @@ -67,21 +148,25 @@ const DisksTable = ({ const isEditable = !!canEditDisks?.(host); const diskColumnTitles = diskColumns(isEditable); - const rows: IRow[] = [...disks] - .sort((diskA, diskB) => diskA.name?.localeCompare(diskB.name || '') || 0) + const rows: IRow[] = disks + .filter((disk) => disk.driveType !== 'LVM') + .sort((a, b) => (a.name && a.name.localeCompare(b.name as string)) || 0) + .sort((a, b) => { + const aVal = (a.holders || a.name) as string; + const bVal = (b.holders || b.name) as string; + + return aVal?.localeCompare(bVal) || 0; + }) .map((disk, index) => ({ cells: [ { title: ( - <> - {isInDiskSkipFormattingList(host, disk.id) && ( - } minWidth="20rem" maxWidth="30rem"> - - - )} - {' '} - {disk.bootable ? `${disk.name || ''} (bootable)` : disk.name} - + ), props: { 'data-testid': 'disk-name' }, }, @@ -135,4 +220,4 @@ const DisksTable = ({ ); }; -export default DisksTable; +export { DisksTable, getDiskLimitation }; diff --git a/libs/ui-lib/lib/common/components/storage/StorageDetail.tsx b/libs/ui-lib/lib/common/components/storage/StorageDetail.tsx index 9ac1e4f689..3c4521402c 100644 --- a/libs/ui-lib/lib/common/components/storage/StorageDetail.tsx +++ b/libs/ui-lib/lib/common/components/storage/StorageDetail.tsx @@ -3,7 +3,7 @@ import { Grid, GridItem } from '@patternfly/react-core'; import { getInventory, Host } from '../../index'; import { OnDiskRoleType } from '../hosts/DiskRole'; import { DiskFormattingType } from '../hosts/FormatDiskCheckbox'; -import DisksTable from './DisksTable'; +import { DisksTable } from './DisksTable'; import SectionTitle from '../ui/SectionTitle'; type StorageDetailProps = { diff --git a/libs/ui-lib/lib/ocm/components/hosts/OdfDisksManualFormattingHint.tsx b/libs/ui-lib/lib/ocm/components/hosts/OdfDisksManualFormattingHint.tsx index 8daf655f61..a27dc1e2bf 100644 --- a/libs/ui-lib/lib/ocm/components/hosts/OdfDisksManualFormattingHint.tsx +++ b/libs/ui-lib/lib/ocm/components/hosts/OdfDisksManualFormattingHint.tsx @@ -1,15 +1,5 @@ import React from 'react'; -import { Alert, AlertVariant, Text, TextContent, TextVariants } from '@patternfly/react-core'; - -const Hint = () => ( - - - All non-installation disks will be used for local storage and must be formatted before the - storage operator's installation. To view and format available disks, expand each host row in - the table. - - -); +import { Alert, AlertVariant } from '@patternfly/react-core'; const OdfDisksManualFormattingHint = () => { return ( @@ -18,7 +8,9 @@ const OdfDisksManualFormattingHint = () => { isInline title="Make sure you format all non-installation disks" > - + All non-installation disks will be used for local storage and must be formatted before the + storage operator's installation. To view and format available disks, expand each host row in + the table. ); }; diff --git a/libs/ui-lib/lib/ocm/components/hosts/StorageAlerts.tsx b/libs/ui-lib/lib/ocm/components/hosts/StorageAlerts.tsx index ec7cde4f8f..3686f62d9d 100644 --- a/libs/ui-lib/lib/ocm/components/hosts/StorageAlerts.tsx +++ b/libs/ui-lib/lib/ocm/components/hosts/StorageAlerts.tsx @@ -1,13 +1,16 @@ import * as React from 'react'; -import { Stack, StackItem } from '@patternfly/react-core'; +import { Alert, AlertVariant, List, ListItem, Stack, StackItem } from '@patternfly/react-core'; import { Cluster, FormatDiskWarning, + getInventory, hasEnabledOperators, + Host, OPERATOR_NAME_ODF, } from '../../../common'; import { isAddHostsCluster, isSomeDisksSkipFormatting } from '../clusters/utils'; import OdfDisksManualFormattingHint from './OdfDisksManualFormattingHint'; +import { getDiskLimitation } from '../../../common/components/storage/DisksTable'; const StorageAlerts = ({ cluster }: { cluster: Cluster }) => { const showFormattingHint = @@ -15,8 +18,54 @@ const StorageAlerts = ({ cluster }: { cluster: Cluster }) => { !isAddHostsCluster(cluster); const someDisksAreSkipFormatting = isSomeDisksSkipFormatting(cluster); + const diskLimitations = [...(cluster.hosts as Host[])] + ?.sort( + (a, b) => + (a.requestedHostname && a.requestedHostname.localeCompare(b.requestedHostname as string)) || + 0, + ) + ?.map((host) => { + const disks = getInventory(host).disks || []; + const installationDisk = disks.find((disk) => disk.id === host.installationDiskId); + if (installationDisk) { + const holder = disks.find((d) => + installationDisk.holders?.split(',').includes(d.name as string), + ); + + if (holder) { + return getDiskLimitation(installationDisk.name, host.requestedHostname, holder); + } + } + }) + .filter(Boolean); + return ( + {!!diskLimitations.length && ( + + {diskLimitations?.length === 1 ? ( + + ) : ( + + + {diskLimitations.map((warning, index) => ( + {warning} + ))} + + + )} + + )} {showFormattingHint && (