Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mgr/dashboard: introduce grafana frontend e2e testing #45811

Merged
merged 2 commits into from Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/pybind/mgr/dashboard/ci/cephadm/bootstrap-cluster.sh
Expand Up @@ -11,10 +11,13 @@ mkdir -p /etc/ceph
mon_ip=$(ifconfig eth0 | grep 'inet ' | awk '{ print $2}')

bootstrap_extra_options='--allow-fqdn-hostname --dashboard-password-noupdate'
bootstrap_extra_options_not_expanded='--skip-monitoring-stack'
{% if expanded_cluster is not defined %}
bootstrap_extra_options+=" ${bootstrap_extra_options_not_expanded}"
{% endif %}

# commenting the below lines. Uncomment it when any extra options are
# needed for the bootstrap.
# bootstrap_extra_options_not_expanded=''
# {% if expanded_cluster is not defined %}
# bootstrap_extra_options+=" ${bootstrap_extra_options_not_expanded}"
# {% endif %}
Comment on lines +15 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra_options are just commented for now since we are not passing any extra options. It might be useful for later uses if we decides to pass some extra options so I thought I'll leave it here. Also, Avan pointed out to comment it out too. Maybe its useful in local too if we want to start a cluster and we don't want monitoring stacks, we can just add that skip option here.


cephadm bootstrap --mon-ip $mon_ip --initial-dashboard-password {{ admin_password }} --shared_ceph_folder /mnt/{{ ceph_dev_folder }} ${bootstrap_extra_options}

Expand Down
14 changes: 11 additions & 3 deletions src/pybind/mgr/dashboard/ci/cephadm/run-cephadm-e2e-tests.sh
Expand Up @@ -24,7 +24,7 @@ export CYPRESS_BASE_URL CYPRESS_LOGIN_USER CYPRESS_LOGIN_PWD
cypress_run () {
local specs="$1"
local timeout="$2"
local override_config="ignoreTestFiles=*.po.ts,retries=0,testFiles=${specs}"
local override_config="ignoreTestFiles=*.po.ts,retries=0,testFiles=${specs},chromeWebSecurity=false"
if [[ -n "$timeout" ]]; then
override_config="${override_config},defaultCommandTimeout=${timeout}"
fi
Expand All @@ -38,5 +38,13 @@ cypress_run () {

cd ${CEPH_DEV_FOLDER}/src/pybind/mgr/dashboard/frontend

cypress_run "orchestrator/workflow/*.feature"
cypress_run "orchestrator/workflow/*-spec.ts"
# check if the prometheus daemon is running
# before starting the e2e tests

PROMETHEUS_RUNNING_COUNT=$(kcli ssh -u root ceph-node-00 'cephadm shell "ceph orch ls --service_name=prometheus --format=json"' | jq -r '.[] | .status.running')
while [[ $PROMETHEUS_RUNNING_COUNT -lt 1 ]]; do
PROMETHEUS_RUNNING_COUNT=$(kcli ssh -u root ceph-node-00 'cephadm shell "ceph orch ls --service_name=prometheus --format=json"' | jq -r '.[] | .status.running')
done

cypress_run ["orchestrator/workflow/*.feature, orchestrator/workflow/*-spec.ts"]
cypress_run "orchestrator/grafana/*.feature"
7 changes: 6 additions & 1 deletion src/pybind/mgr/dashboard/controllers/prometheus.py
Expand Up @@ -51,7 +51,12 @@ def _proxy(self, base_url, method, path, api_name, params=None, payload=None, ve
"Could not reach {}'s API on {}".format(api_name, base_url),
http_status_code=404,
component='prometheus')
content = json.loads(response.content)
try:
content = json.loads(response.content, strict=False)
except json.JSONDecodeError as e:
raise DashboardException(
"Error parsing Prometheus Alertmanager response: {}".format(e.msg),
component='prometheus')
if content['status'] == 'success':
if 'data' in content:
return content['data']
Expand Down
Expand Up @@ -33,21 +33,25 @@ export class ManagerModulesPageHelper extends PageHelper {

// Clears the editable fields
for (const input of inputs) {
const id = `#${input.id}`;
cy.get(id).clear();
if (input.oldValue) {
cy.get(id).type(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) {
cy.get('.datatable-body')
.eq(1)
.should('contain', input.id)
.and('not.contain', input.newValue);
if (input.oldValue) {
cy.get('.datatable-body')
.eq(1)
.should('contain', input.id)
.and('not.contain', input.newValue);
}
}
}
}
Expand Up @@ -155,3 +155,34 @@ And('I should see row {string} does not have {string}', (row: string, options: s
}
}
});

And('I go to the {string} tab', (names: string) => {
for (const name of names.split(', ')) {
cy.contains('.nav.nav-tabs li', name).click();
}
});

And('select {string} {string}', (selectionName: string, option: string) => {
cy.get(`select[name=${selectionName}]`).select(option);
cy.get(`select[name=${selectionName}] option:checked`).contains(option);
});

When('I expand the row {string}', (row: string) => {
cy.contains('.datatable-body-row', row).first().find('.tc_expand-collapse').click();
});

And('I should see row {string} have {string} on this tab', (row: string, options: string) => {
if (options) {
cy.get('cd-table').should('exist');
cy.get('datatable-scroller, .empty-row');
cy.get('.datatable-row-detail').within(() => {
cy.get('cd-table .search input').first().clear().type(row);
for (const option of options.split(',')) {
cy.contains(
`datatable-body-row datatable-body-cell .datatable-body-cell-label span`,
option
).should('exist');
}
});
}
});
@@ -0,0 +1,86 @@
import { e2e } from '@grafana/e2e';
import { Then, When } from 'cypress-cucumber-preprocessor/steps';
import 'cypress-iframe';

function getIframe() {
cy.frameLoaded('#iframe');
return cy.iframe();
}

Then('I should see the grafana panel {string}', (panels: string) => {
getIframe().within(() => {
for (const panel of panels.split(', ')) {
cy.get('.grafana-app')
.wait(100)
.within(() => {
e2e.components.Panels.Panel.title(panel).should('be.visible');
});
}
});
});

When('I view the grafana panel {string}', (panels: string) => {
getIframe().within(() => {
for (const panel of panels.split(', ')) {
cy.get('.grafana-app')
.wait(100)
.within(() => {
e2e.components.Panels.Panel.title(panel).should('be.visible').click();
e2e.components.Panels.Panel.headerItems('View').should('be.visible').click();
});
}
});
});

Then('I should not see {string} in the panel {string}', (value: string, panels: string) => {
getIframe().within(() => {
for (const panel of panels.split(', ')) {
cy.get('.grafana-app')
.wait(100)
.within(() => {
cy.get(`[aria-label="${panel} panel"]`)
.should('be.visible')
.within(() => {
cy.get('span').first().should('not.have.text', value);
});
});
}
});
});

Then(
'I should see the legends {string} in the graph {string}',
(legends: string, panels: string) => {
getIframe().within(() => {
for (const panel of panels.split(', ')) {
cy.get('.grafana-app')
.wait(100)
.within(() => {
cy.get(`[aria-label="${panel} panel"]`)
.should('be.visible')
.within(() => {
for (const legend of legends.split(', ')) {
cy.get('a').contains(legend);
}
});
});
}
});
}
);

Then('I should not see No Data in the graph {string}', (panels: string) => {
getIframe().within(() => {
for (const panel of panels.split(', ')) {
cy.get('.grafana-app')
.wait(100)
.within(() => {
cy.get(`[aria-label="${panel} panel"]`)
.should('be.visible')
.within(() => {
cy.get('div.datapoints-warning').should('not.exist');
});
});
}
});
});
Expand Up @@ -36,6 +36,9 @@ export class UrlsCollection extends PageHelper {
'mgr-modules': { url: '#/mgr-modules', id: 'cd-mgr-module-list' },

// Logs
logs: { url: '#/logs', id: 'cd-logs' }
logs: { url: '#/logs', id: 'cd-logs' },

// RGW Daemons
'rgw daemons': { url: '#/rgw/daemon', id: 'cd-rgw-daemon-list' }
};
}
@@ -0,0 +1,63 @@
Feature: Grafana panels

Go to some of the grafana performance section and check if
panels are populated without any issues

Background: Log in
Given I am logged in

Scenario Outline: Hosts Overall Performance
Given I am on the "hosts" page
When I go to the "Overall Performance" tab
Then I should see the grafana panel "<panel>"
When I view the grafana panel "<panel>"
Then I should not see "No Data" in the panel "<panel>"

Examples:
| panel |
| OSD Hosts |
| AVG CPU Busy |
| AVG RAM Utilization |
| Physical IOPS |
| AVG Disk Utilization |
| Network Load |
| CPU Busy - Top 10 Hosts |
| Network Load - Top 10 Hosts |

Scenario Outline: RGW Daemon Overall Performance
Given I am on the "rgw daemons" page
When I go to the "Overall Performance" tab
Then I should see the grafana panel "<panel>"
When I view the grafana panel "<panel>"
Then I should not see No Data in the graph "<panel>"
And I should see the legends "<legends>" in the graph "<panel>"

Examples:
| panel | legends |
| Total Requests/sec by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
| GET Latencies by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
| Bandwidth by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
| PUT Latencies by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
| Average GET/PUT Latencies | GET AVG, PUT AVG |
| Bandwidth Consumed by Type | GETs, PUTs |

Scenario Outline: RGW per Daemon Performance
Given I am on the "rgw daemons" page
When I expand the row "<name>"
And I go to the "Performance Details" tab
Then I should see the grafana panel "<panel>"
When I view the grafana panel "<panel>"
Then I should not see No Data in the graph "<panel>"
And I should see the legends "<name>" in the graph "<panel>"

Examples:
| name | panel |
| foo.ceph-node-00 | Bandwidth by HTTP Operation |
| foo.ceph-node-00 | HTTP Request Breakdown |
| foo.ceph-node-00 | Workload Breakdown |
| foo.ceph-node-01 | Bandwidth by HTTP Operation |
| foo.ceph-node-01 | HTTP Request Breakdown |
| foo.ceph-node-01 | Workload Breakdown |
| foo.ceph-node-02 | Bandwidth by HTTP Operation |
| foo.ceph-node-02 | HTTP Request Breakdown |
| foo.ceph-node-02 | Workload Breakdown |
@@ -1,4 +1,5 @@
/* tslint:disable*/
import { Input, ManagerModulesPageHelper } from '../../cluster/mgr-modules.po';
import { CreateClusterWizardHelper } from '../../cluster/create-cluster.po';
import { HostsPageHelper } from '../../cluster/hosts.po';
import { ServicesPageHelper } from '../../cluster/services.po';
Expand All @@ -8,6 +9,7 @@ describe('when cluster creation is completed', () => {
const createCluster = new CreateClusterWizardHelper();
const services = new ServicesPageHelper();
const hosts = new HostsPageHelper();
const mgrmodules = new ManagerModulesPageHelper();

const hostnames = ['ceph-node-00', 'ceph-node-01', 'ceph-node-02', 'ceph-node-03'];

Expand All @@ -30,6 +32,35 @@ describe('when cluster creation is completed', () => {
hosts.navigateTo();
});

it('should check if monitoring stacks are running on the root host', () => {
const monitoringStack = ['alertmanager', 'grafana', 'node-exporter', 'prometheus'];
hosts.clickTab('cd-host-details', 'ceph-node-00', 'Daemons');
for (const daemon of monitoringStack) {
cy.get('cd-host-details').within(() => {
services.checkServiceStatus(daemon);
});
}
});

// avoid creating node-exporter on the newly added host
// to favour the host draining process
it('should reduce the count for node-exporter', () => {
services.editService('node-exporter', '3');
});

// grafana ip address is set to the fqdn by default.
// kcli is not working with that, so setting the IP manually.
it('should change ip address of grafana', { retries: 2 }, () => {
const dashboardArr: Input[] = [
{
id: 'GRAFANA_API_URL',
newValue: 'https://192.168.100.100:3000',
Copy link
Member Author

@nizamial09 nizamial09 Apr 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@epuertat @aaSharma14 recently some changes went into cephadm which basically means fetching the fqdn for some config urls. And our kcli configuration causes the fqdn to be something like https://ceph-node-00.ceph-dashboard:3000 and this doesn't work. So for now I had to manually change the grafana-api-url.

oldValue: ''
}
];
mgrmodules.editMgrModule('dashboard', dashboardArr);
});

it('should add one more host', () => {
hosts.navigateTo('add');
hosts.add(hostnames[3]);
Expand Down
Expand Up @@ -31,7 +31,7 @@ describe('Host Page', () => {
}
});

it('should force maintenance and exit', { retries: 2 }, () => {
it('should force maintenance and exit', () => {
hosts.maintenance(hostnames[3], true, true);
});

Expand Down
7 changes: 7 additions & 0 deletions src/pybind/mgr/dashboard/frontend/cypress/plugins/index.js
Expand Up @@ -14,6 +14,13 @@ module.exports = (on, _config) => {
return launchOptions;
}
});

on('task', {
log({ message, optional }) {
optional ? console.log(message, optional) : console.log(message);
return null;
Comment on lines +19 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the grafana-e2e commands like e2e.components.Panels.Panel.title... were failing because it couldn't find a command called log. So I had to manually add it on our code. Then it started passing.

},
});
};

require('@applitools/eyes-cypress')(module);
Expand Up @@ -2,6 +2,7 @@ declare global {
namespace Cypress {
interface Chainable<Subject> {
login(): void;
logToConsole(message: string, optional?: any): void;
text(): Chainable<string>;
}
}
Expand Down Expand Up @@ -49,6 +50,10 @@ Cypress.Commands.add('login', () => {
});

// @ts-ignore
Cypress.Commands.add('text', { prevSubject: true }, (subject) => {
Cypress.Commands.add('text', { prevSubject: true }, (subject: any) => {
return subject.text();
});

Cypress.Commands.add('logToConsole', (message: string, optional?: any) => {
cy.task('log', { message: `(${new Date().toISOString()}) ${message}`, optional });
});