summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:45:59 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:45:59 +0000
commit19fcec84d8d7d21e796c7624e521b60d28ee21ed (patch)
tree42d26aa27d1e3f7c0b8bd3fd14e7d7082f5008dc /src/pybind/mgr/dashboard/frontend/cypress/integration/cluster
parentInitial commit. (diff)
downloadceph-upstream/16.2.11+ds.tar.xz
ceph-upstream/16.2.11+ds.zip
Adding upstream version 16.2.11+ds.upstream/16.2.11+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/cypress/integration/cluster')
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.e2e-spec.ts78
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.po.ts75
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.e2e-spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.po.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.e2e-spec.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts184
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/inventory.po.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.e2e-spec.ts58
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.po.ts70
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.e2e-spec.ts78
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.po.ts57
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.e2e-spec.ts62
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.po.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.e2e-spec.ts57
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts84
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts204
17 files changed, 1177 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.e2e-spec.ts
new file mode 100644
index 000000000..d022d59cf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.e2e-spec.ts
@@ -0,0 +1,78 @@
+import { ConfigurationPageHelper } from './configuration.po';
+
+describe('Configuration page', () => {
+ const configuration = new ConfigurationPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ configuration.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ configuration.expectBreadcrumbText('Configuration');
+ });
+ });
+
+ describe('fields check', () => {
+ beforeEach(() => {
+ configuration.getExpandCollapseElement().click();
+ });
+
+ it('should check that details table opens (w/o tab header)', () => {
+ configuration.getStatusTables().should('be.visible');
+ configuration.getTabs().should('not.exist');
+ });
+ });
+
+ describe('edit configuration test', () => {
+ const configName = 'client_cache_size';
+
+ beforeEach(() => {
+ configuration.clearTableSearchInput();
+ configuration.getTableCount('found').as('configFound');
+ });
+
+ after(() => {
+ configuration.configClear(configName);
+ });
+
+ it('should click and edit a configuration and results should appear in the table', () => {
+ configuration.edit(
+ configName,
+ ['global', '1'],
+ ['mon', '2'],
+ ['mgr', '3'],
+ ['osd', '4'],
+ ['mds', '5'],
+ ['client', '6']
+ );
+ });
+
+ it('should verify modified filter is applied properly', () => {
+ configuration.filterTable('Modified', 'no');
+ configuration.getTableCount('found').as('unmodifiedConfigs');
+
+ // Modified filter value to yes
+ configuration.filterTable('Modified', 'yes');
+ configuration.getTableCount('found').as('modifiedConfigs');
+
+ cy.get('@configFound').then((configFound) => {
+ cy.get('@unmodifiedConfigs').then((unmodifiedConfigs) => {
+ const modifiedConfigs = Number(configFound) - Number(unmodifiedConfigs);
+ configuration.getTableCount('found').should('eq', modifiedConfigs);
+ });
+ });
+
+ // Modified filter value to no
+ configuration.filterTable('Modified', 'no');
+ cy.get('@configFound').then((configFound) => {
+ cy.get('@modifiedConfigs').then((modifiedConfigs) => {
+ const unmodifiedConfigs = Number(configFound) - Number(modifiedConfigs);
+ configuration.getTableCount('found').should('eq', unmodifiedConfigs);
+ });
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.po.ts
new file mode 100644
index 000000000..0133dc31f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.po.ts
@@ -0,0 +1,75 @@
+import { PageHelper } from '../page-helper.po';
+
+export class ConfigurationPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/configuration', id: 'cd-configuration' }
+ };
+
+ /**
+ * Clears out all the values in a config to reset before and after testing
+ * Does not work for configs with checkbox only, possible future PR
+ */
+ configClear(name: string) {
+ const valList = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']; // Editable values
+
+ this.navigateEdit(name);
+ // Waits for the data to load
+ cy.contains('.card-header', `Edit ${name}`);
+
+ for (const i of valList) {
+ cy.get(`#${i}`).clear();
+ }
+ // Clicks save button and checks that values are not present for the selected config
+ cy.get('[data-cy=submitBtn]').click();
+
+ // Enter config setting name into filter box
+ this.searchTable(name);
+
+ // Expand row
+ this.getExpandCollapseElement(name).click();
+
+ // Checks for visibility of details tab
+ this.getStatusTables().should('be.visible');
+
+ for (const i of valList) {
+ // Waits until values are not present in the details table
+ this.getStatusTables().should('not.contain.text', i + ':');
+ }
+ }
+
+ /**
+ * Clicks the designated config, then inputs the values passed into the edit function.
+ * Then checks if the edit is reflected in the config table.
+ * Takes in name of config and a list of tuples of values the user wants edited,
+ * each tuple having the desired value along with the number tehey want for that value.
+ * Ex: [global, '2'] is the global value with an input of 2
+ */
+ edit(name: string, ...values: [string, string][]) {
+ this.navigateEdit(name);
+
+ // Waits for data to load
+ cy.contains('.card-header', `Edit ${name}`);
+
+ values.forEach((valtuple) => {
+ // Finds desired value based off given list
+ cy.get(`#${valtuple[0]}`).type(valtuple[1]); // of values and inserts the given number for the value
+ });
+
+ // Clicks save button then waits until the desired config is visible, clicks it,
+ // then checks that each desired value appears with the desired number
+ cy.get('[data-cy=submitBtn]').click();
+
+ // Enter config setting name into filter box
+ this.searchTable(name);
+
+ // Checks for visibility of config in table
+ this.getExpandCollapseElement(name).should('be.visible').click();
+
+ // Clicks config
+ values.forEach((value) => {
+ // iterates through list of values and
+ // checks if the value appears in details with the correct number attatched
+ cy.contains('.table.table-striped.table-bordered', `${value[0]}\: ${value[1]}`);
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts
new file mode 100644
index 000000000..300eddbcc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts
@@ -0,0 +1,56 @@
+import { PageHelper } from '../page-helper.po';
+import { NotificationSidebarPageHelper } from '../ui/notification.po';
+import { HostsPageHelper } from './hosts.po';
+import { ServicesPageHelper } from './services.po';
+
+const pages = {
+ index: { url: '#/expand-cluster', id: 'cd-create-cluster' }
+};
+export class CreateClusterWizardHelper extends PageHelper {
+ pages = pages;
+
+ createCluster() {
+ cy.get('cd-create-cluster').should('contain.text', 'Please expand your cluster first');
+ cy.get('[name=expand-cluster]').click();
+ cy.get('cd-wizard').should('exist');
+ }
+
+ doSkip() {
+ cy.get('[name=skip-cluster-creation]').click();
+ cy.contains('cd-modal button', 'Continue').click();
+
+ cy.get('cd-dashboard').should('exist');
+ const notification = new NotificationSidebarPageHelper();
+ notification.open();
+ notification.getNotifications().should('contain', 'Cluster expansion skipped by user');
+ }
+}
+
+export class CreateClusterHostPageHelper extends HostsPageHelper {
+ pages = {
+ index: { url: '#/expand-cluster', id: 'cd-wizard' },
+ add: { url: '', id: 'cd-host-form' }
+ };
+
+ columnIndex = {
+ hostname: 1,
+ labels: 2,
+ status: 3,
+ services: 0
+ };
+}
+
+export class CreateClusterServicePageHelper extends ServicesPageHelper {
+ pages = {
+ index: { url: '#/expand-cluster', id: 'cd-wizard' },
+ create: { url: '', id: 'cd-service-form' }
+ };
+
+ columnIndex = {
+ service_name: 1,
+ placement: 2,
+ running: 0,
+ size: 0,
+ last_refresh: 0
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.e2e-spec.ts
new file mode 100644
index 000000000..0a454739f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.e2e-spec.ts
@@ -0,0 +1,37 @@
+import { CrushMapPageHelper } from './crush-map.po';
+
+describe('CRUSH map page', () => {
+ const crushmap = new CrushMapPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ crushmap.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ crushmap.expectBreadcrumbText('CRUSH map');
+ });
+ });
+
+ describe('fields check', () => {
+ it('should check that title & table appears', () => {
+ // Check that title (CRUSH map viewer) appears
+ crushmap.getPageTitle().should('equal', 'CRUSH map viewer');
+
+ // Check that title appears once OSD is clicked
+ crushmap.getCrushNode(0).click();
+
+ crushmap
+ .getLegends()
+ .invoke('text')
+ .then((legend) => {
+ crushmap.getCrushNode(0).should('have.text', legend);
+ });
+
+ // Check that table appears once OSD is clicked
+ crushmap.getDataTables().should('be.visible');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.po.ts
new file mode 100644
index 000000000..a5d2d591c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.po.ts
@@ -0,0 +1,13 @@
+import { PageHelper } from '../page-helper.po';
+
+export class CrushMapPageHelper extends PageHelper {
+ pages = { index: { url: '#/crush-map', id: 'cd-crushmap' } };
+
+ getPageTitle() {
+ return cy.get('cd-crushmap .card-header').text();
+ }
+
+ getCrushNode(idx: number) {
+ return cy.get('.node-name.type-osd').eq(idx);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.e2e-spec.ts
new file mode 100644
index 000000000..e4f9936c3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.e2e-spec.ts
@@ -0,0 +1,35 @@
+import { HostsPageHelper } from './hosts.po';
+
+describe('Hosts page', () => {
+ const hosts = new HostsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ hosts.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ hosts.expectBreadcrumbText('Hosts');
+ });
+
+ it('should show two tabs', () => {
+ hosts.getTabsCount().should('eq', 2);
+ });
+
+ it('should show hosts list tab at first', () => {
+ hosts.getTabText(0).should('eq', 'Hosts List');
+ });
+
+ it('should show overall performance as a second tab', () => {
+ hosts.getTabText(1).should('eq', 'Overall Performance');
+ });
+ });
+
+ describe('services link test', () => {
+ it('should check at least one host is present', () => {
+ hosts.check_for_host();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts
new file mode 100644
index 000000000..33fe756ff
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts
@@ -0,0 +1,184 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/hosts', id: 'cd-hosts' },
+ add: { url: '#/hosts/(modal:add)', id: 'cd-host-form' }
+};
+
+export class HostsPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ hostname: 2,
+ services: 3,
+ labels: 4,
+ status: 5
+ };
+
+ check_for_host() {
+ this.getTableCount('total').should('not.be.eq', 0);
+ }
+
+ add(hostname: string, exist?: boolean, maintenance?: boolean, labels: string[] = []) {
+ cy.get(`${this.pages.add.id}`).within(() => {
+ cy.get('#hostname').type(hostname);
+ if (maintenance) {
+ cy.get('label[for=maintenance]').click();
+ }
+ if (exist) {
+ cy.get('#hostname').should('have.class', 'ng-invalid');
+ }
+ });
+
+ if (labels.length) {
+ this.selectPredefinedLabels(labels);
+ }
+
+ cy.get('cd-submit-button').click();
+ // back to host list
+ cy.get(`${this.pages.index.id}`);
+ }
+
+ selectPredefinedLabels(labels: string[]) {
+ cy.get('a[data-testid=select-menu-edit]').click();
+ for (const label of labels) {
+ cy.get('.popover-body div.select-menu-item-content').contains(label).click();
+ }
+ }
+
+ checkExist(hostname: string, exist: boolean) {
+ this.getTableCell(this.columnIndex.hostname, hostname).should(($elements) => {
+ const hosts = $elements.map((_, el) => el.textContent).get();
+ if (exist) {
+ expect(hosts).to.include(hostname);
+ } else {
+ expect(hosts).to.not.include(hostname);
+ }
+ });
+ }
+
+ remove(hostname: string) {
+ super.delete(hostname, this.columnIndex.hostname, 'hosts');
+ }
+
+ // Add or remove labels on a host, then verify labels in the table
+ editLabels(hostname: string, labels: string[], add: boolean) {
+ this.getTableCell(this.columnIndex.hostname, hostname).click();
+ this.clickActionButton('edit');
+
+ // add or remove label badges
+ if (add) {
+ cy.get('cd-modal').find('.select-menu-edit').click();
+ for (const label of labels) {
+ cy.contains('cd-modal .badge', new RegExp(`^${label}$`)).should('not.exist');
+ cy.get('.popover-body input').type(`${label}{enter}`);
+ }
+ } else {
+ for (const label of labels) {
+ cy.contains('cd-modal .badge', new RegExp(`^${label}$`))
+ .find('.badge-remove')
+ .click();
+ }
+ }
+ cy.get('cd-modal cd-submit-button').click();
+ this.checkLabelExists(hostname, labels, add);
+ }
+
+ checkLabelExists(hostname: string, labels: string[], add: boolean) {
+ // Verify labels are added or removed from Labels column
+ // First find row with hostname, then find labels in the row
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .click()
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.labels}) .badge`)
+ .should(($ele) => {
+ const newLabels = $ele.toArray().map((v) => v.innerText);
+ for (const label of labels) {
+ if (add) {
+ expect(newLabels).to.include(label);
+ } else {
+ expect(newLabels).to.not.include(label);
+ }
+ }
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ maintenance(hostname: string, exit = false, force = false) {
+ this.clearTableSearchInput();
+ this.getTableCell(this.columnIndex.hostname, hostname).click();
+ if (force) {
+ this.clickActionButton('enter-maintenance');
+
+ cy.get('cd-modal').within(() => {
+ cy.contains('button', 'Continue').click();
+ });
+
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include('maintenance');
+ });
+ }
+ if (exit) {
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+ .then(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ if (status[0].includes('maintenance')) {
+ this.clickActionButton('exit-maintenance');
+ }
+ });
+
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.not.include('maintenance');
+ });
+ } else {
+ this.clickActionButton('enter-maintenance');
+
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include('maintenance');
+ });
+ }
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ drain(hostname: string) {
+ this.getTableCell(this.columnIndex.hostname, hostname).click();
+ this.clickActionButton('start-drain');
+ this.checkLabelExists(hostname, ['_no_schedule'], true);
+
+ // unselect it to avoid colliding with any other selection
+ // in different steps
+ this.getTableCell(this.columnIndex.hostname, hostname).click();
+
+ this.clickTab('cd-host-details', hostname, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ cy.wait(20000);
+ this.expectTableCount('total', 0);
+ });
+ }
+
+ checkServiceInstancesExist(hostname: string, instances: string[]) {
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.services}) .badge`)
+ .should(($ele) => {
+ const serviceInstances = $ele.toArray().map((v) => v.innerText);
+ for (const instance of instances) {
+ expect(serviceInstances).to.include(instance);
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/inventory.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/inventory.po.ts
new file mode 100644
index 000000000..5a9abdc03
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/inventory.po.ts
@@ -0,0 +1,22 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/inventory', id: 'cd-inventory' }
+};
+
+export class InventoryPageHelper extends PageHelper {
+ pages = pages;
+
+ identify() {
+ // Nothing we can do, just verify the form is there
+ this.getFirstTableCell().click();
+ cy.contains('cd-table-actions button', 'Identify').click();
+ cy.get('cd-modal').within(() => {
+ cy.get('#duration').select('15 minutes');
+ cy.get('#duration').select('10 minutes');
+ cy.get('cd-back-button').click();
+ });
+ cy.get('cd-modal').should('not.exist');
+ cy.get(`${this.pages.index.id}`);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.e2e-spec.ts
new file mode 100644
index 000000000..9868b89ae
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.e2e-spec.ts
@@ -0,0 +1,58 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { LogsPageHelper } from './logs.po';
+
+describe('Logs page', () => {
+ const logs = new LogsPageHelper();
+ const pools = new PoolPageHelper();
+
+ const poolname = 'e2e_logs_test_pool';
+ const today = new Date();
+ let hour = today.getHours();
+ if (hour > 12) {
+ hour = hour - 12;
+ }
+ const minute = today.getMinutes();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ beforeEach(() => {
+ logs.navigateTo();
+ });
+
+ it('should open and show breadcrumb', () => {
+ logs.expectBreadcrumbText('Logs');
+ });
+
+ it('should show two tabs', () => {
+ logs.getTabsCount().should('eq', 2);
+ });
+
+ it('should show cluster logs tab at first', () => {
+ logs.getTabText(0).should('eq', 'Cluster Logs');
+ });
+
+ it('should show audit logs as a second tab', () => {
+ logs.getTabText(1).should('eq', 'Audit Logs');
+ });
+ });
+
+ describe('audit logs respond to pool creation and deletion test', () => {
+ it('should create pool and check audit logs reacted', () => {
+ pools.navigateTo('create');
+ pools.create(poolname, 8);
+ pools.navigateTo();
+ pools.existTableCell(poolname, true);
+ logs.checkAuditForPoolFunction(poolname, 'create', hour, minute);
+ });
+
+ it('should delete pool and check audit logs reacted', () => {
+ pools.navigateTo();
+ pools.delete(poolname);
+ logs.checkAuditForPoolFunction(poolname, 'delete', hour, minute);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.po.ts
new file mode 100644
index 000000000..7efd8a652
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.po.ts
@@ -0,0 +1,70 @@
+import { PageHelper } from '../page-helper.po';
+
+export class LogsPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/logs', id: 'cd-logs' }
+ };
+
+ checkAuditForPoolFunction(poolname: string, poolfunction: string, hour: number, minute: number) {
+ this.navigateTo();
+
+ // sometimes the modal from deleting pool is still present at this point.
+ // This wait makes sure it isn't
+ cy.contains('.modal-dialog', 'Delete Pool').should('not.exist');
+
+ // go to audit logs tab
+ cy.contains('.nav-link', 'Audit Logs').click();
+
+ // Enter an earliest time so that no old messages with the same pool name show up
+ cy.get('.ngb-tp-input').its(0).clear();
+
+ if (hour < 10) {
+ cy.get('.ngb-tp-input').its(0).type('0');
+ }
+ cy.get('.ngb-tp-input').its(0).type(`${hour}`);
+
+ cy.get('.ngb-tp-input').its(1).clear();
+ if (minute < 10) {
+ cy.get('.ngb-tp-input').its(1).type('0');
+ }
+ cy.get('.ngb-tp-input').its(1).type(`${minute}`);
+
+ // Enter the pool name into the filter box
+ cy.get('input.form-control.ng-valid').first().clear().type(poolname);
+
+ cy.get('.tab-pane.active')
+ .get('.card-body')
+ .get('.message')
+ .should('contain.text', poolname)
+ .and('contain.text', `pool ${poolfunction}`);
+ }
+
+ checkAuditForConfigChange(configname: string, setting: string, hour: number, minute: number) {
+ this.navigateTo();
+
+ // go to audit logs tab
+ cy.contains('.nav-link', 'Audit Logs').click();
+
+ // Enter an earliest time so that no old messages with the same config name show up
+ cy.get('.ngb-tp-input').its(0).clear();
+ if (hour < 10) {
+ cy.get('.ngb-tp-input').its(0).type('0');
+ }
+ cy.get('.ngb-tp-input').its(0).type(`${hour}`);
+
+ cy.get('.ngb-tp-input').its(1).clear();
+ if (minute < 10) {
+ cy.get('.ngb-tp-input').its(1).type('0');
+ }
+ cy.get('.ngb-tp-input').its(1).type(`${minute}`);
+
+ // Enter the config name into the filter box
+ cy.get('input.form-control.ng-valid').first().clear().type(configname);
+
+ cy.get('.tab-pane.active')
+ .get('.card-body')
+ .get('.message')
+ .should('contain.text', configname)
+ .and('contain.text', setting);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.e2e-spec.ts
new file mode 100644
index 000000000..50656fece
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.e2e-spec.ts
@@ -0,0 +1,78 @@
+import { Input, ManagerModulesPageHelper } from './mgr-modules.po';
+
+describe('Manager modules page', () => {
+ const mgrmodules = new ManagerModulesPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ mgrmodules.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ mgrmodules.expectBreadcrumbText('Manager Modules');
+ });
+ });
+
+ describe('verifies editing functionality for manager modules', () => {
+ it('should test editing on balancer module', () => {
+ const balancerArr: Input[] = [
+ {
+ id: 'crush_compat_max_iterations',
+ newValue: '123',
+ oldValue: '25'
+ }
+ ];
+ mgrmodules.editMgrModule('balancer', balancerArr);
+ });
+
+ it('should test editing on dashboard module', () => {
+ const dashboardArr: Input[] = [
+ {
+ id: 'GRAFANA_API_PASSWORD',
+ newValue: 'rafa',
+ oldValue: ''
+ }
+ ];
+ mgrmodules.editMgrModule('dashboard', dashboardArr);
+ });
+
+ it('should test editing on devicehealth module', () => {
+ const devHealthArray: Input[] = [
+ {
+ id: 'mark_out_threshold',
+ newValue: '1987',
+ oldValue: '2419200'
+ },
+ {
+ id: 'pool_name',
+ newValue: 'sox',
+ oldValue: 'device_health_metrics'
+ },
+ {
+ id: 'retention_period',
+ newValue: '1999',
+ oldValue: '15552000'
+ },
+ {
+ id: 'scrape_frequency',
+ newValue: '2020',
+ oldValue: '86400'
+ },
+ {
+ id: 'sleep_interval',
+ newValue: '456',
+ oldValue: '600'
+ },
+ {
+ id: 'warn_threshold',
+ newValue: '567',
+ oldValue: '7257600'
+ }
+ ];
+
+ mgrmodules.editMgrModule('devicehealth', devHealthArray);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.po.ts
new file mode 100644
index 000000000..04d2eee46
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.po.ts
@@ -0,0 +1,57 @@
+import { PageHelper } from '../page-helper.po';
+
+export class Input {
+ id: string;
+ oldValue: string;
+ newValue: string;
+}
+
+export class ManagerModulesPageHelper extends PageHelper {
+ pages = { index: { url: '#/mgr-modules', id: 'cd-mgr-module-list' } };
+
+ /**
+ * Selects the Manager Module and then fills in the desired fields.
+ */
+ editMgrModule(name: string, inputs: Input[]) {
+ this.navigateEdit(name);
+
+ for (const input of inputs) {
+ // Clears fields and adds edits
+ cy.get(`#${input.id}`).clear().type(input.newValue);
+ }
+
+ cy.contains('button', 'Update').click();
+ // Checks if edits appear
+ this.getExpandCollapseElement(name).should('be.visible').click();
+
+ for (const input of inputs) {
+ cy.get('.datatable-body').last().contains(input.newValue);
+ }
+
+ // Clear mgr module of all edits made to it
+ this.navigateEdit(name);
+
+ // Clears the editable fields
+ for (const input of inputs) {
+ if (input.oldValue) {
+ const id = `#${input.id}`;
+ cy.get(id).clear();
+ if (input.oldValue) {
+ cy.get(id).type(input.oldValue);
+ }
+ }
+ }
+
+ // Checks that clearing represents in details tab of module
+ cy.contains('button', 'Update').click();
+ this.getExpandCollapseElement(name).should('be.visible').click();
+ for (const input of inputs) {
+ if (input.oldValue) {
+ cy.get('.datatable-body')
+ .eq(1)
+ .should('contain', input.id)
+ .and('not.contain', input.newValue);
+ }
+ }
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.e2e-spec.ts
new file mode 100644
index 000000000..a23d071e6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.e2e-spec.ts
@@ -0,0 +1,62 @@
+import { MonitorsPageHelper } from './monitors.po';
+
+describe('Monitors page', () => {
+ const monitors = new MonitorsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ monitors.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ monitors.expectBreadcrumbText('Monitors');
+ });
+ });
+
+ describe('fields check', () => {
+ it('should check status table is present', () => {
+ // check for table header 'Status'
+ monitors.getLegends().its(0).should('have.text', 'Status');
+
+ // check for fields in table
+ monitors
+ .getStatusTables()
+ .should('contain.text', 'Cluster ID')
+ .and('contain.text', 'monmap modified')
+ .and('contain.text', 'monmap epoch')
+ .and('contain.text', 'quorum con')
+ .and('contain.text', 'quorum mon')
+ .and('contain.text', 'required con')
+ .and('contain.text', 'required mon');
+ });
+
+ it('should check In Quorum and Not In Quorum tables are present', () => {
+ // check for there to be two tables
+ monitors.getDataTables().should('have.length', 2);
+
+ // check for table header 'In Quorum'
+ monitors.getLegends().its(1).should('have.text', 'In Quorum');
+
+ // check for table header 'Not In Quorum'
+ monitors.getLegends().its(2).should('have.text', 'Not In Quorum');
+
+ // verify correct columns on In Quorum table
+ monitors.getDataTableHeaders(0).contains('Name');
+
+ monitors.getDataTableHeaders(0).contains('Rank');
+
+ monitors.getDataTableHeaders(0).contains('Public Address');
+
+ monitors.getDataTableHeaders(0).contains('Open Sessions');
+
+ // verify correct columns on Not In Quorum table
+ monitors.getDataTableHeaders(1).contains('Name');
+
+ monitors.getDataTableHeaders(1).contains('Rank');
+
+ monitors.getDataTableHeaders(1).contains('Public Address');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.po.ts
new file mode 100644
index 000000000..4113b9928
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.po.ts
@@ -0,0 +1,7 @@
+import { PageHelper } from '../page-helper.po';
+
+export class MonitorsPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/monitor', id: 'cd-monitor' }
+ };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.e2e-spec.ts
new file mode 100644
index 000000000..e68d03bd5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.e2e-spec.ts
@@ -0,0 +1,57 @@
+import { OSDsPageHelper } from './osds.po';
+
+describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ osds.navigateTo();
+ });
+
+ describe('breadcrumb and tab tests', () => {
+ it('should open and show breadcrumb', () => {
+ osds.expectBreadcrumbText('OSDs');
+ });
+
+ it('should show two tabs', () => {
+ osds.getTabsCount().should('eq', 2);
+ osds.getTabText(0).should('eq', 'OSDs List');
+ osds.getTabText(1).should('eq', 'Overall Performance');
+ });
+ });
+
+ describe('check existence of fields on OSD page', () => {
+ it('should check that number of rows and count in footer match', () => {
+ osds.getTableCount('total').then((text) => {
+ osds.getTableRows().its('length').should('equal', text);
+ });
+ });
+
+ it('should verify that buttons exist', () => {
+ cy.contains('button', 'Create');
+ cy.contains('button', 'Cluster-wide configuration');
+ });
+
+ describe('by selecting one row in OSDs List', () => {
+ beforeEach(() => {
+ osds.getExpandCollapseElement().click();
+ });
+
+ it('should show the correct text for the tab labels', () => {
+ cy.get('#tabset-osd-details > li > a').then(($tabs) => {
+ const tabHeadings = $tabs.map((_i, e) => e.textContent).get();
+
+ expect(tabHeadings).to.eql([
+ 'Devices',
+ 'Attributes (OSD map)',
+ 'Metadata',
+ 'Device health',
+ 'Performance counter',
+ 'Performance Details'
+ ]);
+ });
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts
new file mode 100644
index 000000000..cd812f474
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts
@@ -0,0 +1,84 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/osd', id: 'cd-osd-list' },
+ create: { url: '#/osd/create', id: 'cd-osd-form' }
+};
+
+export class OSDsPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ id: 3,
+ status: 5
+ };
+
+ create(deviceType: 'hdd' | 'ssd', hostname?: string, expandCluster = false) {
+ cy.get('[aria-label="toggle advanced mode"]').click();
+ // Click Primary devices Add button
+ cy.get('cd-osd-devices-selection-groups[name="Primary"]').as('primaryGroups');
+ cy.get('@primaryGroups').find('button').click();
+
+ // Select all devices with `deviceType`
+ cy.get('cd-osd-devices-selection-modal').within(() => {
+ cy.get('.modal-footer .tc_submitButton').as('addButton').should('be.disabled');
+ this.filterTable('Type', deviceType);
+ if (hostname) {
+ this.filterTable('Hostname', hostname);
+ }
+
+ if (expandCluster) {
+ this.getTableCount('total').should('be.gte', 1);
+ }
+ cy.get('@addButton').click();
+ });
+
+ if (!expandCluster) {
+ cy.get('@primaryGroups').within(() => {
+ this.getTableCount('total').as('newOSDCount');
+ });
+
+ cy.get(`${pages.create.id} .card-footer .tc_submitButton`).click();
+ cy.get(`cd-osd-creation-preview-modal .modal-footer .tc_submitButton`).click();
+ }
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ checkStatus(id: number, status: string[]) {
+ this.searchTable(`id:${id}`);
+ this.expectTableCount('found', 1);
+ cy.get(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`).should(($ele) => {
+ const allStatus = $ele.toArray().map((v) => v.innerText);
+ for (const s of status) {
+ expect(allStatus).to.include(s);
+ }
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ ensureNoOsd(id: number) {
+ this.searchTable(`id:${id}`);
+ this.expectTableCount('found', 0);
+ this.clearTableSearchInput();
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ deleteByIDs(osdIds: number[], replace?: boolean) {
+ this.getTableRows().each(($el) => {
+ const rowOSD = Number(
+ $el.find('datatable-body-cell .datatable-body-cell-label').get(this.columnIndex.id - 1)
+ .textContent
+ );
+ if (osdIds.includes(rowOSD)) {
+ cy.wrap($el).click();
+ }
+ });
+ this.clickActionButton('delete');
+ if (replace) {
+ cy.get('cd-modal label[for="preserve"]').click();
+ }
+ cy.get('cd-modal label[for="confirmation"]').click();
+ cy.contains('cd-modal button', 'Delete').click();
+ cy.get('cd-modal').should('not.exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts
new file mode 100644
index 000000000..481d6bc9b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts
@@ -0,0 +1,204 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/services', id: 'cd-services' },
+ create: { url: '#/services/(modal:create)', id: 'cd-service-form' }
+};
+
+export class ServicesPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ service_name: 2,
+ placement: 3,
+ running: 4,
+ size: 5,
+ last_refresh: 6
+ };
+
+ serviceDetailColumnIndex = {
+ daemonName: 2,
+ status: 4
+ };
+
+ check_for_service() {
+ this.getTableCount('total').should('not.be.eq', 0);
+ }
+
+ private selectServiceType(serviceType: string) {
+ return this.selectOption('service_type', serviceType);
+ }
+
+ clickServiceTab(serviceName: string, tabName: string) {
+ this.getExpandCollapseElement(serviceName).click();
+ cy.get('cd-service-details').within(() => {
+ this.getTab(tabName).click();
+ });
+ }
+
+ addService(
+ serviceType: string,
+ exist?: boolean,
+ count = '1',
+ snmpVersion?: string,
+ snmpPrivProtocol?: boolean,
+ unmanaged = false
+ ) {
+ cy.get(`${this.pages.create.id}`).within(() => {
+ this.selectServiceType(serviceType);
+ switch (serviceType) {
+ case 'rgw':
+ cy.get('#service_id').type('foo');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(count);
+ break;
+
+ case 'ingress':
+ if (unmanaged) {
+ cy.get('label[for=unmanaged]').click();
+ }
+ this.selectOption('backend_service', 'rgw.foo');
+ cy.get('#service_id').should('have.value', 'rgw.foo');
+ cy.get('#virtual_ip').type('192.168.100.1/24');
+ cy.get('#frontend_port').type('8081');
+ cy.get('#monitor_port').type('8082');
+ break;
+
+ case 'nfs':
+ cy.get('#service_id').type('testnfs');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(count);
+ break;
+
+ case 'snmp-gateway':
+ this.selectOption('snmp_version', snmpVersion);
+ cy.get('#snmp_destination').type('192.168.0.1:8443');
+ if (snmpVersion === 'V2c') {
+ cy.get('#snmp_community').type('public');
+ } else {
+ cy.get('#engine_id').type('800C53F00000');
+ this.selectOption('auth_protocol', 'SHA');
+ if (snmpPrivProtocol) {
+ this.selectOption('privacy_protocol', 'DES');
+ cy.get('#snmp_v3_priv_password').type('testencrypt');
+ }
+
+ // Credentials
+ cy.get('#snmp_v3_auth_username').type('test');
+ cy.get('#snmp_v3_auth_password').type('testpass');
+ }
+ break;
+
+ default:
+ cy.get('#service_id').type('test');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(count);
+ break;
+ }
+ if (serviceType === 'snmp-gateway') {
+ cy.get('cd-submit-button').dblclick();
+ } else {
+ cy.get('cd-submit-button').click();
+ }
+ });
+ if (exist) {
+ cy.get('#service_id').should('have.class', 'ng-invalid');
+ } else {
+ // back to service list
+ cy.get(`${this.pages.index.id}`);
+ }
+ }
+
+ editService(name: string, daemonCount: string) {
+ this.navigateEdit(name, true, false);
+ cy.get(`${this.pages.create.id}`).within(() => {
+ cy.get('#service_type').should('be.disabled');
+ cy.get('#service_id').should('be.disabled');
+ cy.get('#count').clear().type(daemonCount);
+ cy.get('cd-submit-button').click();
+ });
+ }
+
+ checkServiceStatus(daemon: string, expectedStatus = 'running') {
+ let daemonNameIndex = this.serviceDetailColumnIndex.daemonName;
+ let statusIndex = this.serviceDetailColumnIndex.status;
+
+ // since hostname row is hidden from the hosts details table,
+ // we'll need to manually override the indexes when this check is being
+ // done for the daemons in host details page. So we'll get the url and
+ // verify if the current page is not the services index page
+ cy.url().then((url) => {
+ if (!url.includes(pages.index.url)) {
+ daemonNameIndex = 1;
+ statusIndex = 3;
+ }
+
+ cy.get('cd-service-daemon-list').within(() => {
+ this.getTableCell(daemonNameIndex, daemon, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${statusIndex}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include(expectedStatus);
+ });
+ });
+ });
+ }
+
+ expectPlacementCount(serviceName: string, expectedCount: string) {
+ this.getTableCell(this.columnIndex.service_name, serviceName)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
+ .should(($ele) => {
+ const running = $ele.text().split(';');
+ expect(running).to.include(`count:${expectedCount}`);
+ });
+ }
+
+ checkExist(serviceName: string, exist: boolean) {
+ this.getTableCell(this.columnIndex.service_name, serviceName).should(($elements) => {
+ const services = $elements.map((_, el) => el.textContent).get();
+ if (exist) {
+ expect(services).to.include(serviceName);
+ } else {
+ expect(services).to.not.include(serviceName);
+ }
+ });
+ }
+
+ isUnmanaged(serviceName: string, unmanaged: boolean) {
+ this.getTableCell(this.columnIndex.service_name, serviceName)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
+ .should(($ele) => {
+ const placement = $ele.text().split(';');
+ unmanaged
+ ? expect(placement).to.include('unmanaged')
+ : expect(placement).to.not.include('unmanaged');
+ });
+ }
+
+ deleteService(serviceName: string) {
+ const getRow = this.getTableCell.bind(this, this.columnIndex.service_name);
+ getRow(serviceName).click();
+
+ // Clicks on table Delete button
+ this.clickActionButton('delete');
+
+ // Confirms deletion
+ cy.get('cd-modal .custom-control-label').click();
+ cy.contains('cd-modal button', 'Delete').click();
+
+ // Wait for modal to close
+ cy.get('cd-modal').should('not.exist');
+ this.checkExist(serviceName, false);
+ }
+
+ daemonAction(daemon: string, action: string) {
+ cy.get('cd-service-daemon-list').within(() => {
+ this.getTableRow(daemon).click();
+ this.clickActionButton(action);
+
+ // unselect it to avoid colliding with any other selection
+ // in different steps
+ this.getTableRow(daemon).click();
+ });
+ }
+}