From 3548a0a196c4bdeef61febe53693de52730ba3a3 Mon Sep 17 00:00:00 2001 From: dmaiocchi Date: Tue, 30 Jun 2020 19:23:18 +0200 Subject: [PATCH 1/4] add e2e tests --- e2e_test/.dockerignore | 3 + e2e_test/.pylintrc | 2 + e2e_test/.travis.yml | 19 ++ e2e_test/AUTHORS | 3 + e2e_test/Dockerfile | 38 +++ e2e_test/Dockerfile.alpine | 36 ++ e2e_test/Makefile | 7 + e2e_test/README.md | 72 ++++ e2e_test/hawk_test.py | 161 +++++++++ e2e_test/hawk_test_driver.py | 589 +++++++++++++++++++++++++++++++++ e2e_test/hawk_test_results.py | 62 ++++ e2e_test/hawk_test_ssh.py | 86 +++++ e2e_test/requirements-test.txt | 4 + e2e_test/requirements.txt | 11 + 14 files changed, 1093 insertions(+) create mode 100644 e2e_test/.dockerignore create mode 100644 e2e_test/.pylintrc create mode 100644 e2e_test/.travis.yml create mode 100644 e2e_test/AUTHORS create mode 100644 e2e_test/Dockerfile create mode 100644 e2e_test/Dockerfile.alpine create mode 100644 e2e_test/Makefile create mode 100644 e2e_test/README.md create mode 100644 e2e_test/hawk_test.py create mode 100644 e2e_test/hawk_test_driver.py create mode 100644 e2e_test/hawk_test_results.py create mode 100644 e2e_test/hawk_test_ssh.py create mode 100644 e2e_test/requirements-test.txt create mode 100644 e2e_test/requirements.txt diff --git a/e2e_test/.dockerignore b/e2e_test/.dockerignore new file mode 100644 index 000000000..3cc11bd29 --- /dev/null +++ b/e2e_test/.dockerignore @@ -0,0 +1,3 @@ +.git +**/__pycache__ +**/*.pyc diff --git a/e2e_test/.pylintrc b/e2e_test/.pylintrc new file mode 100644 index 000000000..de23ab31d --- /dev/null +++ b/e2e_test/.pylintrc @@ -0,0 +1,2 @@ +[MESSAGES CONTROL] +disable=missing-docstring,line-too-long,too-many-public-methods,too-many-instance-attributes,too-many-arguments,too-many-statements,too-few-public-methods diff --git a/e2e_test/.travis.yml b/e2e_test/.travis.yml new file mode 100644 index 000000000..838aefb82 --- /dev/null +++ b/e2e_test/.travis.yml @@ -0,0 +1,19 @@ +language: python + +services: + - docker + +dist: xenial + +python: + - "3.5" + - "3.6" + - "3.7" + - "3.8" + +install: + - pip install -r requirements-test.txt + +script: + - make test + - make test-docker diff --git a/e2e_test/AUTHORS b/e2e_test/AUTHORS new file mode 100644 index 000000000..ca190e275 --- /dev/null +++ b/e2e_test/AUTHORS @@ -0,0 +1,3 @@ +Alexei.Tighineanu@suse.com @atighineanu +Alvaro.Carvajal@suse.com @alvarocarvajald +Ricardo.Branco@suse.com @ricardobranco777 diff --git a/e2e_test/Dockerfile b/e2e_test/Dockerfile new file mode 100644 index 000000000..0c2d72556 --- /dev/null +++ b/e2e_test/Dockerfile @@ -0,0 +1,38 @@ +# Defines the tag for OBS and build script builds: +#!BuildTag: hawk_test +# Use the repositories defined in OBS for installing packages +#!UseOBSRepositories +FROM opensuse/tumbleweed + +RUN zypper -n install -y --no-recommends \ + MozillaFirefox \ + MozillaFirefox-branding-upstream \ + chromium \ + file \ + python3 \ + python3-paramiko \ + python3-PyVirtualDisplay \ + python3-selenium \ + shadow \ + xauth \ + xdpyinfo \ + xorg-x11-fonts \ + xorg-x11-server-Xvfb && \ + zypper -n clean -a + +COPY geckodriver /usr/local/bin/ +COPY chromedriver /usr/local/bin/ +RUN chmod +x /usr/local/bin/* + +RUN useradd -l -m -d /test test + +COPY *.py / + +ENV PYTHONPATH / +ENV PYTHONUNBUFFERED 1 +ENV DBUS_SESSION_BUS_ADDRESS /dev/null + +WORKDIR /test + +USER test +ENTRYPOINT ["/usr/bin/python3", "/hawk_test.py"] diff --git a/e2e_test/Dockerfile.alpine b/e2e_test/Dockerfile.alpine new file mode 100644 index 000000000..458c86a55 --- /dev/null +++ b/e2e_test/Dockerfile.alpine @@ -0,0 +1,36 @@ +FROM python:3.8-alpine + +COPY requirements.txt /tmp + +RUN apk --no-cache --virtual .build-deps add \ + gcc \ + libc-dev \ + libffi-dev \ + make \ + openssl-dev && \ + apk add --no-cache \ + chromium \ + chromium-chromedriver \ + firefox-esr \ + tzdata \ + xdpyinfo \ + xvfb && \ + pip install --no-cache-dir -r /tmp/requirements.txt && \ + apk del .build-deps + +RUN wget -q -O- https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz | tar zxf - -C /usr/local/bin/ + +RUN adduser -D test -h /test + +COPY *.py / +RUN python -OO -m compileall && \ + python -OO -m compileall /*.py + +ENV PYTHONPATH / +ENV PYTHONUNBUFFERED 1 +ENV DBUS_SESSION_BUS_ADDRESS /dev/null + +WORKDIR /test + +USER test +ENTRYPOINT ["/usr/local/bin/python3", "/hawk_test.py"] diff --git a/e2e_test/Makefile b/e2e_test/Makefile new file mode 100644 index 000000000..a211244c1 --- /dev/null +++ b/e2e_test/Makefile @@ -0,0 +1,7 @@ +test: + @flake8 --ignore=E501 + @pylint --ignored-modules=distutils *.py + +test-docker: + @docker build -t hawk_test -f Dockerfile.alpine . + @docker run --rm hawk_test --help diff --git a/e2e_test/README.md b/e2e_test/README.md new file mode 100644 index 000000000..07c1b1f21 --- /dev/null +++ b/e2e_test/README.md @@ -0,0 +1,72 @@ +# Hawk end to end tests. + +This Docker image runs a set of Selenium tests for testing [Hawk](https://github.com/ClusterLabs/hawk/) + +The following tests are executed by openQA during ci regularly. + +As developer you can execute them manually when you do an update on hawk. + + +# Quickstart: + +1) Create the docker image +`docker build . -t hawk_test -f Dockerfile.alpine` + + +2) You need 2 virtual-machines where hawk is running. (normally it is a cluster) + +See https://github.com/SUSE/pacemaker-deploy for deploying hawk + +``` docker run --ipc=host -xvfb hawk_test -H 10.162.32.175 -S 10.162.29.122 -t 15 -s linux``` + + +Notes: + - You may want to add `--net=host` if you have problems with DNS resolution. + + + +## Dependencies + +- OS packages: + - Xvfb (optional) + - Docker (optional) + - Firefox + - [Geckodriver](https://github.com/mozilla/geckodriver/releases) + - Chromium (optional) + - [Chromedriver](https://chromedriver.chromium.org/downloads) (optional) + - Python 3 +- Python packages: + - paramiko + - selenium + - PyVirtualDisplay + +## Options + +``` + -h, --help show this help message and exit + -b {firefox,chrome,chromium}, --browser {firefox,chrome,chromium} + Browser to use in the test + -H HOST, --host HOST Host or IP address where HAWK is running + -S SLAVE, --slave SLAVE + Host or IP address of the slave + -I VIRTUAL_IP, --virtual-ip VIRTUAL_IP + Virtual IP address in CIDR notation + -P PORT, --port PORT TCP port where HAWK is running + -p PREFIX, --prefix PREFIX + Prefix to add to Resources created during the test + -t TEST_VERSION, --test-version TEST_VERSION + Test SLES Version. Ex: 12-SP3, 12-SP4, 15, 15-SP1 + -s SECRET, --secret SECRET + root SSH Password of the HAWK node + -r RESULTS, --results RESULTS + Generate hawk_test.results file for use with openQA. + --xvfb Use Xvfb. Headless mode +``` + +## FAQ + +- Why Xvfb? + - The `-headless` in both browsers still have bugs, specially with modal dialogs. + - Having Xvfb prevents it from connecting to our X system. +- Why docker? + - The Docker image packs the necessary dependencies in such a way that fits the compatibility matrix between Python, Selenium, Firefox (and Geckodriver) & Chromium (and Chromedriver). diff --git a/e2e_test/hawk_test.py b/e2e_test/hawk_test.py new file mode 100644 index 000000000..991ac76da --- /dev/null +++ b/e2e_test/hawk_test.py @@ -0,0 +1,161 @@ +#!/usr/bin/python3 +# Copyright (C) 2019 SUSE LLC +"""HAWK GUI interface Selenium test: tests hawk GUI with Selenium using firefox or chrome""" + +import argparse +import ipaddress +import re +import shutil +import socket +import sys + +from pyvirtualdisplay import Display + +from hawk_test_driver import HawkTestDriver +from hawk_test_results import ResultSet +from hawk_test_ssh import HawkTestSSH + + +def hostname(string): + try: + socket.getaddrinfo(string, 1) + return string + except socket.gaierror: + raise argparse.ArgumentTypeError("Unknown host: %s" % string) + + +def cidr_address(string): + try: + ipaddress.ip_network(string, False) + return string + except ValueError: + raise argparse.ArgumentTypeError("Invalid CIDR address: %s" % string) + + +def port(string): + if string.isdigit() and 1 <= int(string) <= 65535: + return string + raise argparse.ArgumentTypeError("Invalid port number: %s" % string) + + +def sles_version(string): + if re.match(r"\d{2}(?:-SP\d)?$", string): + return string + raise argparse.ArgumentTypeError("Invalid SLES version: %s" % string) + + +def parse_args(): + parser = argparse.ArgumentParser(description='HAWK GUI interface Selenium test') + parser.add_argument('-b', '--browser', default='firefox', choices=['firefox', 'chrome', 'chromium'], + help='Browser to use in the test') + parser.add_argument('-H', '--host', default='localhost', type=hostname, + help='Host or IP address where HAWK is running') + parser.add_argument('-S', '--slave', type=hostname, + help='Host or IP address of the slave') + parser.add_argument('-I', '--virtual-ip', type=cidr_address, + help='Virtual IP address in CIDR notation') + parser.add_argument('-P', '--port', default='7630', type=port, + help='TCP port where HAWK is running') + parser.add_argument('-p', '--prefix', default='', + help='Prefix to add to Resources created during the test') + parser.add_argument('-t', '--test-version', required=True, type=sles_version, + help='Test SLES Version. Ex: 12-SP3, 12-SP4, 15, 15-SP1') + parser.add_argument('-s', '--secret', + help='root SSH Password of the HAWK node') + parser.add_argument('-r', '--results', + help='Generate hawk_test.results file') + parser.add_argument('--xvfb', action='store_true', + help='Use Xvfb. Headless mode') + args = parser.parse_args() + return args + + +def main(): + args = parse_args() + + if args.prefix and not args.prefix.isalpha(): + print("ERROR: Prefix must be alphanumeric", file=sys.stderr) + sys.exit(1) + + driver = "geckodriver" if args.browser == "firefox" else "chromedriver" + if shutil.which(driver) is None: + print("ERROR: Please download %s to a directory in PATH" % driver, file=sys.stderr) + sys.exit(1) + + if args.xvfb: + global DISPLAY # pylint: disable=global-statement + DISPLAY = Display() + DISPLAY.start() + + # Create driver instance + browser = HawkTestDriver(addr=args.host, port=args.port, + browser=args.browser, headless=args.xvfb, + version=args.test_version.lower()) + + # Initialize results set + results = ResultSet() + + # Establish SSH connection to verify status + ssh = HawkTestSSH(args.host, args.secret) + results.add_ssh_tests() + + # Resources to create + mycluster = args.prefix + 'Anderes' + myprimitive = args.prefix + 'cool_primitive' + myclone = args.prefix + 'cool_clone' + mygroup = args.prefix + 'cool_group' + + # Tests to perform + if args.virtual_ip: + browser.test('test_add_virtual_ip', results, args.virtual_ip) + browser.test('test_remove_virtual_ip', results) + else: + results.set_test_status('test_add_virtual_ip', 'skipped') + results.set_test_status('test_remove_virtual_ip', 'skipped') + browser.test('test_set_stonith_maintenance', results) + ssh.verify_stonith_in_maintenance(results) + browser.test('test_disable_stonith_maintenance', results) + browser.test('test_view_details_first_node', results) + browser.test('test_clear_state_first_node', results) + browser.test('test_set_first_node_maintenance', results) + ssh.verify_node_maintenance(results) + browser.test('test_disable_maintenance_first_node', results) + browser.test('test_add_new_cluster', results, mycluster) + browser.test('test_remove_cluster', results, mycluster) + browser.test('test_click_on_history', results) + browser.test('test_generate_report', results) + browser.test('test_click_on_command_log', results) + browser.test('test_click_on_status', results) + browser.test('test_add_primitive', results, myprimitive) + ssh.verify_primitive(myprimitive, args.test_version, results) + browser.test('test_remove_primitive', results, myprimitive) + ssh.verify_primitive_removed(myprimitive, results) + browser.test('test_add_clone', results, myclone) + browser.test('test_remove_clone', results, myclone) + browser.test('test_add_group', results, mygroup) + browser.test('test_remove_group', results, mygroup) + browser.test('test_click_around_edit_conf', results) + if args.slave: + browser.addr = args.slave + browser.test('test_fencing', results) + else: + results.set_test_status('test_fencing', 'skipped') + + # Save results if run with -r or --results + if args.results: + results.logresults(args.results) + + return results.get_failed_tests_total() + + +if __name__ == "__main__": + DISPLAY = None + try: + sys.exit(main()) + except KeyboardInterrupt: + if DISPLAY is not None: + DISPLAY.stop() + sys.exit(1) + finally: + if DISPLAY is not None: + DISPLAY.stop() diff --git a/e2e_test/hawk_test_driver.py b/e2e_test/hawk_test_driver.py new file mode 100644 index 000000000..8069f1373 --- /dev/null +++ b/e2e_test/hawk_test_driver.py @@ -0,0 +1,589 @@ +#!/usr/bin/python3 +# Copyright (C) 2019 SUSE LLC +"""Define Selenium driver related functions and classes to test the HAWK GUI""" + +import ipaddress +import time +from distutils.version import LooseVersion as Version + +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.by import By +from selenium.common.exceptions import TimeoutException, WebDriverException +from selenium.common.exceptions import ElementNotInteractableException +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +# Error messages +class Error: + MAINT_TOGGLE_ERR = "Could not find Switch to Maintenance toggle button for node" + PRIMITIVE_TARGET_ROLE_ERR = "Couldn't find value [Started] for primitive target-role" + STONITH_ERR = "Couldn't find stonith-sbd menu to place it in maintenance mode" + STONITH_ERR_OFF = "Could not find Disable Maintenance Mode button for stonith-sbd" + XPATH_ERR_FMT = "check_and_click_by_xpath requires a list of xpath strings. Got [%s]" + + +# XPATH constants +class Xpath: + ANYTHING_OPT_LIST = '//option[contains(@value, "anything")]' + CLICK_OK_SUBMIT = '//*[@id="modal"]/div/div/form/div[3]/input' + CLONE_CHILD = '//select[contains(@data-help-filter, ".row.resource") and contains(@name, "clone[child]")]' + CLONE_DATA_HELP_FILTER = '//a[contains(@data-help-filter, ".clone")]' + COMMIT_BTN_DANGER = '//button[contains(@class, "btn-danger") and contains(@class, "commit")]' + CONFIG_EDIT = '//a[contains(@href, "config/edit")]' + DISMISS_MODAL = '//*[@id="modal"]/div/div/div[3]/button' + DROP_DOWN_FORMAT = '//*[@id="resources"]/div[1]/div[2]/div[2]/table/tbody/tr[%d]/td[6]/div/div' + EDIT_MONITOR_TIMEOUT = '//*[@id="oplist"]/fieldset/div/div[1]/div[3]/div[2]/div/div/a[1]' + EDIT_START_TIMEOUT = '//*[@id="oplist"]/fieldset/div/div[1]/div[1]/div[2]/div/div/a[1]' + EDIT_STOP_TIMEOUT = '//*[@id="oplist"]/fieldset/div/div[1]/div[2]/div[2]/div/div/a[1]' + GENERATE_REPORT = '//*[@id="generate"]/form/div/div[2]/button' + GROUP_DATA_FILTER = '//a[contains(@data-help-filter, ".group")]' + HREF_ALERTS = '//a[contains(@href, "#alerts")]' + HREF_CONSTRAINTS = '//a[contains(@href, "#constraints")]' + HREF_DASHBOARD = '//a[contains(@href, "/dashboard")]' + HREF_DELETE_FORMAT = '//a[contains(@href, "%s") and contains(@title, "Delete")]' + HREF_FENCING = '//a[contains(@href, "#fencing")]' + HREF_NODES = '//a[contains(@href, "#nodes")]' + HREF_REPORTS = '//a[contains(@href, "/reports")]' + HREF_TAGS = '//a[contains(@href, "#tags")]' + MODAL_MONITOR_TIMEOUT = '//*[@id="modal"]/div/div/form/div[2]/fieldset/div/div[1]/div' + MODAL_STOP = '//*[@id="modal"]/div/div/form/div[2]/fieldset/div/div[2]/div/div/select/option[6]' + MODAL_TIMEOUT = '//*[@id="modal"]/div/div/form/div[2]/fieldset/div/div[1]/div/div' + NODE_DETAILS = '//*[@id="nodes"]/div[1]/div[2]/div[2]/table/tbody/tr[1]/td[5]/div/a[2]' + NODE_MAINT = '//a[contains(@href, "maintenance") and contains(@title, "Switch to maintenance")]' + NODE_READY = '//a[contains(@href, "ready") and contains(@title, "Switch to ready")]' + OCF_OPT_LIST = '//option[contains(@value, "ocf")]' + OPERATIONS = '//*[@id="nodes"]/div[1]/div[2]/div[2]/table/tbody/tr[1]/td[5]/div/div/button' + OPT_STONITH = '//option[contains(@value, "stonith-sbd")]' + RESOURCES_TYPES = '//a[contains(@href, "resources/types")]' + RSC_OK_SUBMIT = '//input[contains(@class, "submit")]' + RSC_ROWS = '//*[@id="resources"]/div[1]/div[2]/div[2]/table/tbody/tr' + STONITH_CHKBOX = '//input[contains(@type, "checkbox") and contains(@value, "stonith-sbd")]' + STONITH_MAINT_OFF = '//a[contains(@href, "stonith-sbd") and contains(@title, "Disable Maintenance Mode")]' + STONITH_MAINT_ON = '//a[contains(@href, "stonith-sbd/maintenance_on")]' + TARGET_ROLE_FORMAT = '//select[contains(@class, "form-control") and contains(@name, "%s[meta][target-role]")]' + TARGET_ROLE_STARTED = '//option[contains(@value, "tarted")]' + WIZARDS_BASIC = '//span[contains(@href, "basic")]' + + +class HawkTestDriver: + def __init__(self, addr='localhost', port='7630', browser='firefox', headless=False, version='12-SP2'): + self.addr = addr + self.port = port + self.driver = None + self.test_version = version + self.test_status = True + self.headless = headless + self.browser = browser + if browser == 'firefox': + self.timeout_scale = 2.5 + else: + self.timeout_scale = 1 + + def _connect(self): + if self.browser in ['chrome', 'chromium']: + options = webdriver.ChromeOptions() + options.add_argument('--no-sandbox') + options.add_argument('--disable-gpu') + if self.headless: + options.add_argument('--headless') + options.add_argument('--disable-dev-shm-usage') + self.driver = webdriver.Chrome(chrome_options=options) + else: + profile = webdriver.FirefoxProfile() + profile.accept_untrusted_certs = True + profile.assume_untrusted_cert_issuer = True + self.driver = webdriver.Firefox(firefox_profile=profile) + self.driver.maximize_window() + return self.driver + + def _close(self): + self.click_on('Logout') + self.driver.quit() + + @staticmethod + def set_test_status(results, testname, status): + results.set_test_status(testname, status) + + def _do_login(self): + mainlink = 'https://%s:%s' % (self.addr, self.port) + self.driver.get(mainlink) + elem = self.find_element(By.NAME, "session[username]") + if not elem: + print("ERROR: couldn't find element [session[username]]. Cannot login") + self.driver.quit() + return False + elem.send_keys("hacluster") + elem = self.find_element(By.NAME, "session[password]") + if not elem: + print("ERROR: Couldn't find element [session[password]]. Cannot login") + self.driver.quit() + return False + elem.send_keys("linux") + elem.send_keys(Keys.RETURN) + return True + + # Clicks on element identified by clicker if major version from the test is greater or + # equal than the version to check + def click_if_major_version(self, version_to_check, text): + if Version(self.test_version) >= Version(version_to_check): + self.find_element(By.XPATH, "//*[text()='%s']" % text.capitalize()).click() + + # Internal support function click_on partial link test. Sets test_status to False on failure + def click_on(self, text): + print("INFO: Main page. Click on %s" % text) + elem = self.find_element(By.PARTIAL_LINK_TEXT, text) + if not elem: + print("ERROR: Couldn't find element '%s'" % text) + self.test_status = False + return False + try: + elem.click() + except ElementNotInteractableException: + # Element is obscured. Wait and click again + time.sleep(2 * self.timeout_scale) + elem.click() + time.sleep(self.timeout_scale) + return True + + def find_element(self, bywhat, texto, tout=60): + try: + elem = WebDriverWait(self.driver, + tout).until(EC.presence_of_element_located((bywhat, texto))) + except TimeoutException: + print("INFO: %d seconds timeout while looking for element [%s] by [%s]" % + (tout, texto, bywhat)) + return False + return elem + + def verify_success(self): + elem = self.find_element(By.CLASS_NAME, 'alert-success', 60) + if not elem: + elem = self.find_element(By.PARTIAL_LINK_TEXT, 'Rename', 5) + if not elem: + return False + return True + + def fill_value(self, field, tout): + elem = self.find_element(By.NAME, field) + if not elem: + print("ERROR: couldn't find element [%s]." % field) + return + elem.clear() + elem.send_keys("%s" % tout) + + def submit_operation_params(self, errmsg): + self.check_and_click_by_xpath(errmsg, [Xpath.CLICK_OK_SUBMIT]) + + def check_edit_conf(self): + print("INFO: Check edit configuration") + self.click_if_major_version("15", 'configuration') + self.click_on('Edit Configuration') + self.check_and_click_by_xpath("Couldn't find Edit Configuration element", [Xpath.CONFIG_EDIT]) + + # Internal support function to find element(s) by xpath and click them + # Sets test_status to False on failure. + def check_and_click_by_xpath(self, errmsg, xpath_exps): + for xpath in xpath_exps: + elem = self.find_element(By.XPATH, xpath) + if not elem: + print("ERROR: Couldn't find element by xpath [%s] %s" % (xpath, errmsg)) + self.test_status = False + return + try: + elem.click() + except ElementNotInteractableException: + # Element is obscured. Wait and click again + time.sleep(2 * self.timeout_scale) + elem.click() + time.sleep(2 * self.timeout_scale) + + # Generic function to perform the tests + def test(self, testname, results, *extra): + self.test_status = True # Clear internal test status before testing + self._connect() + if self._do_login(): + time.sleep(5) + if getattr(self, testname)(*extra): + self.set_test_status(results, testname, 'passed') + else: + self.set_test_status(results, testname, 'failed') + self.driver.save_screenshot('%s.png' % testname) + self._close() + + # Set STONITH/sbd in maintenance. Assumes stonith-sbd resource is the last one listed on the + # resources table + def test_set_stonith_maintenance(self): + # wait for page to fully load + if self.find_element(By.XPATH, Xpath.RSC_ROWS): + totalrows = len(self.driver.find_elements_by_xpath(Xpath.RSC_ROWS)) + if not totalrows: + totalrows = 1 + print("TEST: test_set_stonith_maintenance: Placing stonith-sbd in maintenance") + self.check_and_click_by_xpath(Error.STONITH_ERR, [Xpath.DROP_DOWN_FORMAT % totalrows, + Xpath.STONITH_MAINT_ON, Xpath.COMMIT_BTN_DANGER]) + if self.verify_success(): + print("INFO: stonith-sbd successfully placed in maintenance mode") + return True + print("ERROR: failed to place stonith-sbd in maintenance mode") + return False + + def test_disable_stonith_maintenance(self): + print("TEST: test_disable_stonith_maintenance: Re-activating stonith-sbd") + self.check_and_click_by_xpath(Error.STONITH_ERR_OFF, [Xpath.STONITH_MAINT_OFF, Xpath.COMMIT_BTN_DANGER]) + if self.verify_success(): + print("INFO: stonith-sbd successfully reactivated") + return True + print("ERROR: failed to reactive stonith-sbd from maintenance mode") + return False + + def test_view_details_first_node(self): + print("TEST: test_view_details_first_node: Checking details of first cluster node") + self.click_on('Nodes') + self.check_and_click_by_xpath("Click on Nodes", [Xpath.HREF_NODES]) + self.check_and_click_by_xpath("Could not find first node pull down menu", [Xpath.NODE_DETAILS]) + self.check_and_click_by_xpath("Could not find button to dismiss node details popup", + [Xpath.DISMISS_MODAL]) + time.sleep(self.timeout_scale) + return self.test_status + + def test_clear_state_first_node(self): + print("TEST: test_clear_state_first_node") + self.click_on('Nodes') + self.check_and_click_by_xpath("Click on Nodes", [Xpath.HREF_NODES]) + self.check_and_click_by_xpath("Could not find pull down menu for first cluster node", + [Xpath.OPERATIONS]) + self.click_on('Clear state') + self.check_and_click_by_xpath("Could not clear the state of the first node", + [Xpath.COMMIT_BTN_DANGER]) + if self.verify_success(): + print("INFO: cleared state of first node successfully") + time.sleep(2 * self.timeout_scale) + return True + print("ERROR: failed to clear state of the first node") + return False + + def test_set_first_node_maintenance(self): + print("TEST: test_set_first_node_maintenance: switching node to maintenance") + self.click_on('Nodes') + self.check_and_click_by_xpath("Click on Nodes", [Xpath.HREF_NODES]) + self.check_and_click_by_xpath(Error.MAINT_TOGGLE_ERR, [Xpath.NODE_MAINT, Xpath.COMMIT_BTN_DANGER]) + if self.verify_success(): + print("INFO: node successfully switched to maintenance mode") + return True + print("ERROR: failed to switch node to maintenance mode") + return False + + def test_disable_maintenance_first_node(self): + print("TEST: test_disable_maintenance_first_node: switching node to ready") + self.click_on('Nodes') + self.check_and_click_by_xpath("Click on Nodes", [Xpath.HREF_NODES]) + self.check_and_click_by_xpath(Error.MAINT_TOGGLE_ERR, [Xpath.NODE_READY, Xpath.COMMIT_BTN_DANGER]) + if self.verify_success(): + print("INFO: node successfully switched to ready mode") + return True + print("ERROR: failed to switch node to ready mode") + return False + + def test_add_new_cluster(self, cluster_name): + print("TEST: test_add_new_cluster") + self.click_on('Dashboard') + self.check_and_click_by_xpath("Click on Dashboard", [Xpath.HREF_DASHBOARD]) + elem = self.find_element(By.CLASS_NAME, "btn-default") + if not elem: + print("ERROR: Couldn't find class 'btn-default'") + return False + elem.click() + elem = self.find_element(By.NAME, "cluster[name]") + if not elem: + print("ERROR: Couldn't find element [cluster[name]]. Cannot add cluster") + return False + elem.send_keys(cluster_name) + elem = self.find_element(By.NAME, "cluster[host]") + if not elem: + print("ERROR: Couldn't find element [cluster[host]]. Cannot add cluster") + return False + elem.send_keys(self.addr) + elem = self.find_element(By.NAME, "submit") + if not elem: + print("ERROR: Couldn't find submit button") + return False + elem.click() + while True: + elem = self.find_element(By.PARTIAL_LINK_TEXT, 'Dashboard') + try: + elem.click() + return True + except WebDriverException: + time.sleep(1 + self.timeout_scale) + return False + + def test_remove_cluster(self, cluster_name): + print("TEST: test_remove_cluster") + self.click_on('Dashboard') + self.check_and_click_by_xpath("Click on Dashboard", [Xpath.HREF_DASHBOARD]) + elem = self.find_element(By.PARTIAL_LINK_TEXT, cluster_name) + if not elem: + print("ERROR: Couldn't find cluster [%s]. Cannot remove" % cluster_name) + return False + elem.click() + time.sleep(2 * self.timeout_scale) + elem = self.find_element(By.CLASS_NAME, 'close') + if not elem: + print("ERROR: Cannot find cluster remove button") + return False + elem.click() + time.sleep(2 * self.timeout_scale) + elem = self.find_element(By.CLASS_NAME, 'cancel') + if not elem: + print("ERROR: No cancel button while removing cluster [%s]" % cluster_name) + else: + elem.click() + time.sleep(self.timeout_scale) + elem = self.find_element(By.CLASS_NAME, 'close') + elem.click() + time.sleep(2 * self.timeout_scale) + elem = self.find_element(By.CLASS_NAME, 'btn-danger') + if not elem: + print("ERROR: No OK button found while removing cluster [%s]" % cluster_name) + else: + elem.click() + if self.verify_success(): + print("INFO: Successfully removed cluster: [%s]" % cluster_name) + return True + print("ERROR: Could not remove cluster [%s]" % cluster_name) + return False + + def test_click_on_history(self): + print("TEST: test_click_on_history") + self.click_if_major_version("15", 'troubleshooting') + if not self.test_status: + return False + return self.click_on('History') + + def test_generate_report(self): + print("TEST: test_generate_report: click on Generate report") + self.click_if_major_version("15", 'troubleshooting') + self.click_on('History') + self.check_and_click_by_xpath("Click on History", [Xpath.HREF_REPORTS]) + if self.find_element(By.XPATH, Xpath.GENERATE_REPORT): + self.check_and_click_by_xpath("Could not find button for Generate report", + [Xpath.GENERATE_REPORT]) + # Need to wait here because there are 2 success notices being shown in the GUI: on + # clicking the Generate report button and on completing the generation. This next + # sleep() waits for the first notice to disappear before waiting for the second one + time.sleep(6) + if self.verify_success(): + print("INFO: successfully generated report") + return True + print("ERROR: failed to generate report") + return False + + def test_click_on_command_log(self): + print("TEST: test_click_on_command_log") + self.click_if_major_version("15", 'troubleshooting') + if not self.test_status: + return False + return self.click_on('Command Log') + + def test_click_on_status(self): + print("TEST: test_click_on_status") + return self.click_on('Status') + + def test_add_primitive(self, priminame): + print("TEST: test_add_primitive: Add Resources: Primitive %s" % priminame) + self.click_if_major_version("15", 'configuration') + self.click_on('Resource') + self.check_and_click_by_xpath("Click on Add Resource", [Xpath.RESOURCES_TYPES]) + self.click_on('rimitive') + # Fill the primitive + elem = self.find_element(By.NAME, 'primitive[id]') + if not elem: + print("ERROR: Couldn't find element [primitive[id]]. Cannot add primitive [%s]." % + priminame) + return False + elem.send_keys(priminame) + elem = self.find_element(By.NAME, 'primitive[clazz]') + if not elem: + print("ERROR: Couldn't find element [primitive[clazz]]. Cannot add primitive [%s]" % + priminame) + return False + elem.click() + self.check_and_click_by_xpath("Couldn't find value [ocf] for primitive class", + [Xpath.OCF_OPT_LIST]) + elem = self.find_element(By.NAME, 'primitive[type]') + if not elem: + print("ERROR: Couldn't find element [primitive[type]]. Cannot add primitive [%s]." % + priminame) + return False + elem.click() + self.check_and_click_by_xpath("Couldn't find value [anything] for primitive type", + [Xpath.ANYTHING_OPT_LIST]) + elem = self.find_element(By.NAME, 'primitive[params][binfile]') + if not elem: + print("ERROR: Couldn't find element [primitive[params][binfile]]") + return False + elem.send_keys("file") + # Set start timeout value in 35s + self.check_and_click_by_xpath("Couldn't find edit button for start operation", + [Xpath.EDIT_START_TIMEOUT, Xpath.MODAL_TIMEOUT]) + self.fill_value('op[timeout]', "35s") + self.submit_operation_params(". Couldn't Apply changes for start operation") + # Set stop timeout value in 15s and on-fail + self.check_and_click_by_xpath("Couldn't find edit button for stop operation", + [Xpath.EDIT_STOP_TIMEOUT, Xpath.MODAL_TIMEOUT]) + self.fill_value('op[timeout]', "15s") + self.check_and_click_by_xpath("Couldn't add on-fail option for stop operation", + [Xpath.MODAL_STOP]) + self.submit_operation_params(". Couldn't Apply changes for stop operation") + # Set monitor timeout value in 9s and interval in 13s + self.check_and_click_by_xpath("Couldn't find edit button for monitor operation", + [Xpath.EDIT_MONITOR_TIMEOUT, Xpath.MODAL_MONITOR_TIMEOUT]) + self.fill_value('op[timeout]', "9s") + self.fill_value('op[interval]', "13s") + self.submit_operation_params(". Couldn't Apply changes for monitor operation") + elem = self.find_element(By.NAME, 'primitive[meta][target-role]') + if not elem: + print("ERROR: Couldn't find element [primitive[meta][target-role]]. " + "Cannot add primitive [%s]." % priminame) + return False + elem.click() + self.check_and_click_by_xpath(Error.PRIMITIVE_TARGET_ROLE_ERR, [Xpath.TARGET_ROLE_STARTED]) + elem = self.find_element(By.NAME, 'submit') + if not elem: + print("ERROR: Couldn't find submit button for primitive [%s] creation." % priminame) + else: + elem.click() + status = self.verify_success() + if status: + print("INFO: Successfully added primitive [%s] of class [ocf:heartbeat:anything]" % priminame) + else: + print("ERROR: Could not create primitive [%s]" % priminame) + return status + + def remove_rsc(self, name): + print("INFO: Remove Resource: %s" % name) + self.check_edit_conf() + self.check_and_click_by_xpath("Cannot edit or remove resource [%s]" % name, + [Xpath.HREF_DELETE_FORMAT % name, Xpath.COMMIT_BTN_DANGER, Xpath.CONFIG_EDIT]) + if not self.test_status: + print("ERROR: One of the elements required to remove resource [%s] wasn't found" % name) + return False + elem = self.find_element(By.XPATH, Xpath.HREF_DELETE_FORMAT % name, 5) + if not elem: + print("INFO: Successfully removed resource [%s]" % name) + return True + print("ERROR: Failed to remove resource [%s]" % name) + return False + + def test_remove_primitive(self, name): + print("TEST: test_remove_primitive: Remove Primitive: %s" % name) + return self.remove_rsc(name) + + def test_remove_clone(self, clone): + print("TEST: test_remove_clone: Remove Clone: %s" % clone) + return self.remove_rsc(clone) + + def test_remove_group(self, group): + print("TEST: test_remove_group: Remove Group: %s" % group) + return self.remove_rsc(group) + + def test_add_clone(self, clone): + print("TEST: test_add_clone: Adding clone [%s]" % clone) + self.click_if_major_version("15", 'configuration') + self.click_on('Resource') + self.check_and_click_by_xpath("Click on Add Resource", [Xpath.RESOURCES_TYPES]) + self.check_and_click_by_xpath("on Create Clone [%s]" % clone, [Xpath.CLONE_DATA_HELP_FILTER]) + elem = self.find_element(By.NAME, 'clone[id]') + if not elem: + print("ERROR: Couldn't find element [clone[id]]. No text-field where to type clone id") + return False + elem.send_keys(clone) + self.check_and_click_by_xpath("while adding clone [%s]" % clone, + [Xpath.CLONE_CHILD, Xpath.OPT_STONITH, Xpath.TARGET_ROLE_FORMAT % 'clone', + Xpath.TARGET_ROLE_STARTED, Xpath.RSC_OK_SUBMIT]) + if self.verify_success(): + print("INFO: Successfully added clone [%s] of [stonith-sbd]" % clone) + return True + print("ERROR: Could not create clone [%s]" % clone) + return False + + def test_add_group(self, group): + print("TEST: test_add_group: Adding group [%s]" % group) + self.click_if_major_version("15", 'configuration') + self.click_on('Resource') + self.check_and_click_by_xpath("Click on Add Resource", [Xpath.RESOURCES_TYPES]) + self.check_and_click_by_xpath("while adding group [%s]" % group, [Xpath.GROUP_DATA_FILTER]) + elem = self.find_element(By.NAME, 'group[id]') + if not elem: + print("ERROR: Couldn't find text-field [group[id]] to input group id") + return False + elem.send_keys(group) + self.check_and_click_by_xpath("while adding group [%s]" % group, + [Xpath.STONITH_CHKBOX, Xpath.TARGET_ROLE_FORMAT % 'group', + Xpath.TARGET_ROLE_STARTED, Xpath.RSC_OK_SUBMIT]) + if self.verify_success(): + print("INFO: Successfully added group [%s] of [stonith-sbd]" % group) + return True + print("ERROR: Could not create group [%s]" % group) + return False + + def test_click_around_edit_conf(self): + print("TEST: test_click_around_edit_conf") + print("TEST: Will click on Constraints, Nodes, Tags, Alerts and Fencing") + self.check_edit_conf() + self.check_and_click_by_xpath("while checking around edit configuration", + [Xpath.HREF_CONSTRAINTS, Xpath.HREF_NODES, Xpath.HREF_TAGS, + Xpath.HREF_ALERTS, Xpath.HREF_FENCING]) + return self.test_status + + def test_add_virtual_ip(self, virtual_ip): + print("TEST: test_add_virtual_ip: Add virtual IP from the Wizard") + self.click_if_major_version("15", 'configuration') + broadcast = str(ipaddress.IPv4Network(virtual_ip, False).broadcast_address) + virtual_ip, netmask = virtual_ip.split('/') + self.find_element(By.LINK_TEXT, 'Wizards').click() + self.check_and_click_by_xpath('while clicking Basic', [Xpath.WIZARDS_BASIC]) + self.click_on('Virtual IP') + self.fill_value('virtual-ip.id', 'vip') + self.fill_value('virtual-ip.ip', virtual_ip) + self.fill_value('virtual-ip.cidr_netmask', netmask) + self.fill_value('virtual-ip.broadcast', broadcast) + self.find_element(By.NAME, 'submit').click() + time.sleep(3) + self.find_element(By.NAME, 'submit').click() + time.sleep(3) + # Check that we can connect to the Wizard on the virtual IP + old_addr = self.addr + self.addr = virtual_ip + self._close() + time.sleep(10) + self._connect() + try: + self._do_login() + except WebDriverException: + print("ERROR: Error while adding virtual IP") + return False + print("INFO: Successfully added virtual IP") + self.addr = old_addr + return True + + def test_remove_virtual_ip(self): + print("TEST: test_remove_virtual_ip: Remove virtual IP") + self.click_if_major_version("15", 'configuration') + self.remove_rsc("vip") + return True + + def test_fencing(self): + print("TEST: test_fencing") + self.click_on('Nodes') + self.check_and_click_by_xpath("Click on Nodes", [Xpath.OPERATIONS]) + self.click_on('Fence') + self.check_and_click_by_xpath("Could not fence first node", + [Xpath.COMMIT_BTN_DANGER]) + if self.verify_success(): + print("INFO: Master node successfully fenced") + return True + print("ERROR: Could not fence master node") + return False diff --git a/e2e_test/hawk_test_results.py b/e2e_test/hawk_test_results.py new file mode 100644 index 000000000..82cb856bc --- /dev/null +++ b/e2e_test/hawk_test_results.py @@ -0,0 +1,62 @@ +#!/usr/bin/python3 +# Copyright (C) 2019 SUSE LLC +"""Define classes and functions to handle results in HAWK GUI test""" + +import time +import json + +from hawk_test_driver import HawkTestDriver +from hawk_test_ssh import HawkTestSSH + + +class ResultSet: + def __init__(self): + self.my_tests = [] + self.start_time = time.time() + for func in dir(HawkTestDriver): + if func.startswith('test_') and callable(getattr(HawkTestDriver, func)): + self.my_tests.append(func) + self.results_set = {'tests': [], 'info': {}, 'summary': {}} + for test in self.my_tests: + auxd = {'name': test, 'test_index': 0, 'outcome': 'failed'} + self.results_set['tests'].append(auxd) + self.results_set['info']['timestamp'] = time.time() + with open('/etc/os-release') as file: + lines = file.read().splitlines() + osrel = {k: v[1:-1] for (k, v) in [line.split('=') for line in lines if '=' in line]} + self.results_set['info']['distro'] = osrel['PRETTY_NAME'] + self.results_set['info']['results_file'] = 'hawk_test.results' + self.results_set['summary']['duration'] = 0 + self.results_set['summary']['passed'] = 0 + self.results_set['summary']['num_tests'] = len(self.my_tests) + + def add_ssh_tests(self): + for func in dir(HawkTestSSH): + if func.startswith('verify_') and callable(getattr(HawkTestSSH, func)): + self.my_tests.append(func) + auxd = {'name': str(func), 'test_index': 0, 'outcome': 'failed'} + self.results_set['tests'].append(auxd) + self.results_set['summary']['num_tests'] = len(self.my_tests) + + def logresults(self, filename): + with open(filename, "w") as resfh: + resfh.write(json.dumps(self.results_set)) + + def set_test_status(self, testname, status): + if status not in ['passed', 'failed', 'skipped']: + raise ValueError('test status must be either [passed] or [failed]') + if status == 'passed' and \ + self.results_set['tests'][self.my_tests.index(testname)]['outcome'] != 'passed': + self.results_set['summary']['passed'] += 1 + elif status == 'failed' and \ + self.results_set['tests'][self.my_tests.index(testname)]['outcome'] != 'failed': + self.results_set['summary']['passed'] -= 1 + elif status == 'skipped' and \ + self.results_set['tests'][self.my_tests.index(testname)]['outcome'] != 'skipped': + self.results_set['summary']['num_tests'] -= 1 + self.results_set['tests'][self.my_tests.index(testname)]['outcome'] = status + self.results_set['summary']['duration'] = time.time() - self.start_time + self.results_set['info']['timestamp'] = time.time() + + def get_failed_tests_total(self): + return self.results_set['summary']['num_tests'] - self.results_set['summary']['passed'] diff --git a/e2e_test/hawk_test_ssh.py b/e2e_test/hawk_test_ssh.py new file mode 100644 index 000000000..262108984 --- /dev/null +++ b/e2e_test/hawk_test_ssh.py @@ -0,0 +1,86 @@ +#!/usr/bin/python3 +# Copyright (C) 2019 SUSE LLC +"""Define SSH related functions to test the HAWK GUI""" + +from distutils.version import LooseVersion as Version +import paramiko + + +class HawkTestSSH: + def __init__(self, hostname, secret=None): + self.ssh = paramiko.SSHClient() + self.ssh.load_system_host_keys() + self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy) + self.ssh.connect(hostname=hostname.lower(), username="root", password=secret) + + def check_cluster_conf_ssh(self, command, mustmatch): + _, out, err = self.ssh.exec_command(command) + out, err = map(lambda f: f.read().decode().rstrip('\n'), (out, err)) + print("INFO: ssh command [%s] got output [%s] and error [%s]" % (command, out, err)) + if err: + print("ERROR: got an error over SSH: [%s]" % err) + return False + if isinstance(mustmatch, str): + if mustmatch: + if mustmatch in out: + return True + return False + return out == mustmatch + if isinstance(mustmatch, list): + for exp in mustmatch: + if exp not in out: + return False + return True + raise ValueError("check_cluster_conf_ssh: mustmatch must be str or list") + + @staticmethod + def set_test_status(results, test, status): + results.set_test_status(test, status) + + def verify_stonith_in_maintenance(self, results): + print("TEST: verify_stonith_in_maintenance") + if self.check_cluster_conf_ssh("crm status | grep stonith-sbd", "unmanaged"): + print("INFO: stonith-sbd is unmanaged") + self.set_test_status(results, 'verify_stonith_in_maintenance', 'passed') + return True + print("ERROR: stonith-sbd is not unmanaged but should be") + self.set_test_status(results, 'verify_stonith_in_maintenance', 'failed') + return False + + def verify_node_maintenance(self, results): + print("TEST: verify_node_maintenance: check cluster node is in maintenance mode") + if self.check_cluster_conf_ssh("crm status | grep -i node", "maintenance"): + print("INFO: cluster node set successfully in maintenance mode") + self.set_test_status(results, 'verify_node_maintenance', 'passed') + return True + print("ERROR: cluster node failed to switch to maintenance mode") + self.set_test_status(results, 'verify_node_maintenance', 'failed') + return False + + def verify_primitive(self, myprimitive, version, results): + print("TEST: verify_primitive: check primitive [%s] exists" % myprimitive) + matches = ["%s anything" % myprimitive, "binfile=file", "op start timeout=35s", + "op monitor timeout=9s interval=13s", "meta target-role=Started"] + if Version(version) < Version('15'): + matches.append("op stop timeout=15s") + else: + matches.append("op stop timeout=15s on-fail=stop") + if self.check_cluster_conf_ssh("crm configure show", matches): + print("INFO: primitive [%s] correctly defined in the cluster configuration" % + myprimitive) + self.set_test_status(results, 'verify_primitive', 'passed') + return True + print("ERROR: primitive [%s] missing from cluster configuration" % myprimitive) + self.set_test_status(results, 'verify_primitive', 'failed') + return False + + def verify_primitive_removed(self, myprimitive, results): + print("TEST: verify_primitive_removed: check primitive [%s] is removed" % myprimitive) + if self.check_cluster_conf_ssh("crm resource status | grep ocf::heartbeat:anything", ''): + print("INFO: primitive successfully removed") + self.set_test_status(results, 'verify_primitive_removed', 'passed') + return True + print("ERROR: primitive [%s] still present in the cluster while checking with SSH" % + myprimitive) + self.set_test_status(results, 'verify_primitive_removed', 'failed') + return False diff --git a/e2e_test/requirements-test.txt b/e2e_test/requirements-test.txt new file mode 100644 index 000000000..4fcdb8116 --- /dev/null +++ b/e2e_test/requirements-test.txt @@ -0,0 +1,4 @@ +-r requirements.txt + +flake8 +pylint diff --git a/e2e_test/requirements.txt b/e2e_test/requirements.txt new file mode 100644 index 000000000..aa5747fb3 --- /dev/null +++ b/e2e_test/requirements.txt @@ -0,0 +1,11 @@ +bcrypt==3.1.7 +cffi==1.13.1 +cryptography==2.8 +EasyProcess==0.2.7 +paramiko==2.6.0 +pycparser==2.19 +PyNaCl==1.3.0 +PyVirtualDisplay==0.2.4 +selenium==3.141.0 +six==1.12.0 +urllib3==1.25.6 From 31188960b3ab817dd2aeffbd1bd89dd5dcccc1ec Mon Sep 17 00:00:00 2001 From: dmaiocchi Date: Tue, 30 Jun 2020 19:26:06 +0200 Subject: [PATCH 2/4] Reference testing doc to main readme --- README.md | 6 ++++++ e2e_test/README.md | 15 +++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 80ed06cb9..b8abd5789 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ http://hawk-ui.github.io - [Release](doc/release.md) - [Installation](#installation-and-deployment) - [Devel notes](#devel) + - [Testing](#testing) ## Build Dependencies @@ -89,3 +90,8 @@ of Pacemaker CLI tools as another user in order to support Pacemaker's ACL feature. It is used by Hawk when performing various management tasks. +# testing: + +In addition to unit test we provide End to end test for a Hawk validation. + +See e2e_test/README.md for full documentation diff --git a/e2e_test/README.md b/e2e_test/README.md index 07c1b1f21..64a8d2c74 100644 --- a/e2e_test/README.md +++ b/e2e_test/README.md @@ -6,25 +6,24 @@ The following tests are executed by openQA during ci regularly. As developer you can execute them manually when you do an update on hawk. +# Pre-requisites: + +* docker +* 2 Hawk vms running + (normally it is a cluster) +See https://github.com/SUSE/pacemaker-deploy for deploying hawk + # Quickstart: 1) Create the docker image `docker build . -t hawk_test -f Dockerfile.alpine` - -2) You need 2 virtual-machines where hawk is running. (normally it is a cluster) - -See https://github.com/SUSE/pacemaker-deploy for deploying hawk - ``` docker run --ipc=host -xvfb hawk_test -H 10.162.32.175 -S 10.162.29.122 -t 15 -s linux``` - Notes: - You may want to add `--net=host` if you have problems with DNS resolution. - - ## Dependencies - OS packages: From d57c397690dec4c62ec2985526f47a55f8b20d88 Mon Sep 17 00:00:00 2001 From: dmaiocchi Date: Wed, 1 Jul 2020 13:20:10 +0200 Subject: [PATCH 3/4] Download driver during docker image build --- README.md | 8 +------- e2e_test/Dockerfile | 9 ++++++--- e2e_test/Dockerfile.alpine | 36 ------------------------------------ e2e_test/README.md | 4 ++-- 4 files changed, 9 insertions(+), 48 deletions(-) delete mode 100644 e2e_test/Dockerfile.alpine diff --git a/README.md b/README.md index b8abd5789..e3858a919 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ http://hawk-ui.github.io - [Release](doc/release.md) - [Installation](#installation-and-deployment) - [Devel notes](#devel) - - [Testing](#testing) + - [Testing](e2e_test/README.md) ## Build Dependencies @@ -89,9 +89,3 @@ server instance runs as). of Pacemaker CLI tools as another user in order to support Pacemaker's ACL feature. It is used by Hawk when performing various management tasks. - -# testing: - -In addition to unit test we provide End to end test for a Hawk validation. - -See e2e_test/README.md for full documentation diff --git a/e2e_test/Dockerfile b/e2e_test/Dockerfile index 0c2d72556..dda723a23 100644 --- a/e2e_test/Dockerfile +++ b/e2e_test/Dockerfile @@ -19,9 +19,12 @@ RUN zypper -n install -y --no-recommends \ xorg-x11-fonts \ xorg-x11-server-Xvfb && \ zypper -n clean -a - -COPY geckodriver /usr/local/bin/ -COPY chromedriver /usr/local/bin/ + +RUN zypper -n install -y --no-recommends wget tar gzip +RUN zypper -n clean -a +RUN wget https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz +RUN tar xvf geckodriver-v0.26.0-linux64.tar.gz +RUN mv geckodriver /usr/local/bin RUN chmod +x /usr/local/bin/* RUN useradd -l -m -d /test test diff --git a/e2e_test/Dockerfile.alpine b/e2e_test/Dockerfile.alpine deleted file mode 100644 index 458c86a55..000000000 --- a/e2e_test/Dockerfile.alpine +++ /dev/null @@ -1,36 +0,0 @@ -FROM python:3.8-alpine - -COPY requirements.txt /tmp - -RUN apk --no-cache --virtual .build-deps add \ - gcc \ - libc-dev \ - libffi-dev \ - make \ - openssl-dev && \ - apk add --no-cache \ - chromium \ - chromium-chromedriver \ - firefox-esr \ - tzdata \ - xdpyinfo \ - xvfb && \ - pip install --no-cache-dir -r /tmp/requirements.txt && \ - apk del .build-deps - -RUN wget -q -O- https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz | tar zxf - -C /usr/local/bin/ - -RUN adduser -D test -h /test - -COPY *.py / -RUN python -OO -m compileall && \ - python -OO -m compileall /*.py - -ENV PYTHONPATH / -ENV PYTHONUNBUFFERED 1 -ENV DBUS_SESSION_BUS_ADDRESS /dev/null - -WORKDIR /test - -USER test -ENTRYPOINT ["/usr/local/bin/python3", "/hawk_test.py"] diff --git a/e2e_test/README.md b/e2e_test/README.md index 64a8d2c74..ec5f360fb 100644 --- a/e2e_test/README.md +++ b/e2e_test/README.md @@ -17,9 +17,9 @@ See https://github.com/SUSE/pacemaker-deploy for deploying hawk # Quickstart: 1) Create the docker image -`docker build . -t hawk_test -f Dockerfile.alpine` +`docker build . -t hawk_test ` -``` docker run --ipc=host -xvfb hawk_test -H 10.162.32.175 -S 10.162.29.122 -t 15 -s linux``` +``` docker run --ipc=host hawk_test -H 10.162.32.175 -S 10.162.29.122 -t 15 -s linux --xvfb ``` Notes: - You may want to add `--net=host` if you have problems with DNS resolution. From e90fd31435d1eb45c9d49414db9385bed09b2b18 Mon Sep 17 00:00:00 2001 From: dmaiocchi Date: Wed, 1 Jul 2020 13:26:22 +0200 Subject: [PATCH 4/4] update doc --- e2e_test/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e_test/README.md b/e2e_test/README.md index ec5f360fb..71d1872d5 100644 --- a/e2e_test/README.md +++ b/e2e_test/README.md @@ -19,6 +19,7 @@ See https://github.com/SUSE/pacemaker-deploy for deploying hawk 1) Create the docker image `docker build . -t hawk_test ` +2) Run the tests with: ``` docker run --ipc=host hawk_test -H 10.162.32.175 -S 10.162.29.122 -t 15 -s linux --xvfb ``` Notes: