Files
zaza-openstack-tests/zaza/openstack/charm_tests/openstack_dashboard/tests.py
2019-11-21 13:04:14 +00:00

450 lines
17 KiB
Python

# Copyright 2018 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Encapsulate horizon (openstack-dashboard) charm testing."""
import http.client
import logging
import requests
import tenacity
import urllib.request
import yaml
import zaza.model as zaza_model
import zaza.openstack.utilities.openstack as openstack_utils
import zaza.openstack.charm_tests.test_utils as test_utils
import zaza.openstack.utilities.juju as openstack_juju
import zaza.openstack.charm_tests.policyd.tests as policyd
class AuthExceptions(Exception):
"""Exception base class for the 401 test."""
pass
class FailedAuth(AuthExceptions):
"""Failed exception for the 401 test."""
pass
def _get_dashboard_ip():
"""Get the IP of the dashboard.
:returns: The IP of the dashboard
:rtype: str
"""
unit_name = zaza_model.get_lead_unit_name('openstack-dashboard')
keystone_unit = zaza_model.get_lead_unit_name('keystone')
dashboard_relation = openstack_juju.get_relation_from_unit(
keystone_unit, unit_name, 'identity-service')
dashboard_ip = dashboard_relation['private-address']
logging.debug("dashboard_ip is: {}".format(dashboard_ip))
return dashboard_ip
# NOTE: intermittent authentication fails. Wrap in a retry loop.
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1,
min=5, max=10),
reraise=True)
def _login(dashboard_ip, domain, username, password):
"""Login to the website to get a session.
:param dashboard_ip: The IP address of the dashboard to log in to.
:type dashboard_ip: str
:param domain: the domain to login into
:type domain: str
:param username: the username to login as
:type username: str
:param password: the password to use to login
:type password: str
:returns: tuple of (client, response) where response is the page after
logging in.
:rtype: (requests.sessions.Session, requests.models.Response)
:raises: FailedAuth if the authorisation doesn't work
"""
auth_url = 'http://{}/horizon/auth/login/'.format(dashboard_ip)
# start session, get csrftoken
client = requests.session()
client.get(auth_url)
if 'csrftoken' in client.cookies:
csrftoken = client.cookies['csrftoken']
else:
raise Exception("Missing csrftoken")
# build and send post request
overcloud_auth = openstack_utils.get_overcloud_auth()
if overcloud_auth['OS_AUTH_URL'].endswith("v2.0"):
api_version = 2
else:
api_version = 3
keystone_client = openstack_utils.get_keystone_client(
overcloud_auth)
catalog = keystone_client.service_catalog.get_endpoints()
logging.info(catalog)
if api_version == 2:
region = catalog['identity'][0]['publicURL']
else:
region = [i['url']
for i in catalog['identity']
if i['interface'] == 'public'][0]
auth = {
'domain': domain,
'username': username,
'password': password,
'csrfmiddlewaretoken': csrftoken,
'next': '/horizon/',
'region': region,
}
# In the minimal test deployment /horizon/project/ is unauthorized,
# this does not occur in a full deployment and is probably due to
# services/information missing that horizon wants to display data
# for.
# Redirect to /horizon/identity/ instead.
if (openstack_utils.get_os_release()
>= openstack_utils.get_os_release('xenial_queens')):
auth['next'] = '/horizon/identity/'
if (openstack_utils.get_os_release()
>= openstack_utils.get_os_release('bionic_stein')):
auth['region'] = 'default'
if api_version == 2:
del auth['domain']
logging.info('POST data: "{}"'.format(auth))
response = client.post(auth_url, data=auth, headers={'Referer': auth_url})
# NOTE(ajkavanagh) there used to be a trusty/icehouse test in the
# amulet test, but as the zaza tests only test from trusty/mitaka
# onwards, the test has been dropped
if (openstack_utils.get_os_release()
>= openstack_utils.get_os_release('bionic_stein')):
expect = "Sign Out"
# update the in dashboard seems to require region to be default in
# this test configuration
region = 'default'
else:
expect = 'Projects - OpenStack Dashboard'
if expect not in response.text:
msg = 'FAILURE code={} text="{}"'.format(response,
response.text)
logging.info("Yeah, wen't wrong: {}".format(msg))
raise FailedAuth(msg)
logging.info("Logged into okay")
return client, response
class OpenStackDashboardTests(test_utils.OpenStackBaseTest):
"""Encapsulate openstack dashboard charm tests."""
@classmethod
def setUpClass(cls):
"""Run class setup for running openstack dashboard charm tests."""
super(OpenStackDashboardTests, cls).setUpClass()
cls.application = 'openstack-dashboard'
def test_050_local_settings_permissions_regression_check_lp1755027(self):
"""Assert regression check lp1755027.
Assert the intended file permissions on openstack-dashboard's
configuration file. Regression coverage for
https://bugs.launchpad.net/bugs/1755027.
Ported from amulet tests.
"""
file_path = '/etc/openstack-dashboard/local_settings.py'
expected_perms = '640'
unit_name = zaza_model.get_lead_unit_name('openstack-dashboard')
logging.info('Checking {} permissions...'.format(file_path))
# NOTE(beisner): This could be a new test helper, but it needs
# to be a clean backport to stable with high prio, so maybe later.
cmd = 'stat -c %a {}'.format(file_path)
output = zaza_model.run_on_unit(unit_name, cmd)
perms = output['Stdout'].strip()
assert perms == expected_perms, \
('{} perms of {} not expected ones of {}'
.format(file_path, perms, expected_perms))
def test_100_services(self):
"""Verify the expected services are running.
Ported from amulet tests.
"""
logging.info('Checking openstack-dashboard services...')
unit_name = zaza_model.get_lead_unit_name('openstack-dashboard')
openstack_services = ['apache2']
services = {}
services[unit_name] = openstack_services
for unit_name, unit_services in services.items():
zaza_model.block_until_service_status(
unit_name=unit_name,
services=unit_services,
target_status='running'
)
def test_302_router_settings(self):
"""Verify that the horizon router settings are correct.
Ported from amulet tests.
"""
# note this test is only valid after trusty-icehouse; however, all of
# the zaza tests are after trusty-icehouse
logging.info('Checking dashboard router settings...')
unit_name = zaza_model.get_lead_unit_name('openstack-dashboard')
conf = ('/usr/share/openstack-dashboard/openstack_dashboard/'
'enabled/_40_router.py')
cmd = 'cat {}'.format(conf)
output = zaza_model.run_on_unit(unit_name, cmd)
expected = {
'DISABLED': "True",
}
mismatches = self.crude_py_parse(output['Stdout'], expected)
assert not mismatches, ("mismatched keys on {} were:\n{}"
.format(conf, ", ".join(mismatches)))
def crude_py_parse(self, file_contents, expected):
"""Parse a python file looking for key = value assignements."""
mismatches = []
for line in file_contents.split('\n'):
if '=' in line:
args = line.split('=')
if len(args) <= 1:
continue
key = args[0].strip()
value = args[1].strip()
if key in expected.keys():
if expected[key] != value:
msg = "Mismatch %s != %s" % (expected[key], value)
mismatches.append(msg)
return mismatches
def test_400_connection(self):
"""Test that dashboard responds to http request.
Ported from amulet tests.
"""
logging.info('Checking dashboard http response...')
unit_name = zaza_model.get_lead_unit_name('openstack-dashboard')
keystone_unit = zaza_model.get_lead_unit_name('keystone')
dashboard_relation = openstack_juju.get_relation_from_unit(
keystone_unit, unit_name, 'identity-service')
dashboard_ip = dashboard_relation['private-address']
logging.debug("... dashboard_ip is:{}".format(dashboard_ip))
# NOTE(fnordahl) there is a eluding issue that currently makes the
# first request to the OpenStack Dashboard error out
# with 500 Internal Server Error in CI. Temporarilly
# add retry logic to unwedge the gate. This issue
# should be revisited and root caused properly when time
# allows.
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1,
min=5, max=10),
reraise=True)
def do_request():
logging.info("... trying to fetch the page")
try:
response = urllib.request.urlopen('http://{}/horizon'
.format(dashboard_ip))
logging.info("... fetched page")
except Exception as e:
logging.info("... exception raised was {}".format(str(e)))
raise
return response.read().decode('utf-8')
html = do_request()
self.assertIn('OpenStack Dashboard', html,
"Dashboard frontpage check failed")
def test_401_authenticate(self):
"""Validate that authentication succeeds for client log in."""
logging.info('Checking authentication through dashboard...')
dashboard_ip = _get_dashboard_ip()
overcloud_auth = openstack_utils.get_overcloud_auth()
password = overcloud_auth['OS_PASSWORD'],
logging.info("admin password is {}".format(password))
# try to get the url which will either pass or fail with a 403
overcloud_auth = openstack_utils.get_overcloud_auth()
domain = 'admin_domain',
username = 'admin',
password = overcloud_auth['OS_PASSWORD'],
_login(dashboard_ip, domain, username, password)
logging.info('OK')
def test_404_connection(self):
"""Verify the apache status module gets disabled when hardening apache.
Ported from amulet tests.
"""
logging.info('Checking apache mod_status gets disabled.')
unit_name = zaza_model.get_lead_unit_name('openstack-dashboard')
keystone_unit = zaza_model.get_lead_unit_name('keystone')
dashboard_relation = openstack_juju.get_relation_from_unit(
keystone_unit, unit_name, 'identity-service')
dashboard_ip = dashboard_relation['private-address']
logging.debug("... dashboard_ip is:{}".format(dashboard_ip))
logging.debug('Maybe enabling hardening for apache...')
_app_config = zaza_model.get_application_config(self.application_name)
logging.info(_app_config['harden'])
# NOTE(ajkavanagh): it seems that apache2 doesn't start quickly enough
# for the test, and so it gets reset errors; repeat until either that
# stops or there is a failure
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1,
min=5, max=10),
retry=tenacity.retry_if_exception_type(
http.client.RemoteDisconnected),
reraise=True)
def _do_request():
return urllib.request.urlopen('http://{}/server-status'
.format(dashboard_ip))
with self.config_change(
{'harden': _app_config['harden'].get('value', '')},
{'harden': 'apache'}):
try:
_do_request()
except urllib.request.HTTPError as e:
# test failed if it didn't return 404
msg = "Apache mod_status check failed."
self.assertEqual(e.code, 404, msg)
logging.info('OK')
def test_501_security_checklist_action(self):
"""Verify expected result on a default install.
Ported from amulet tests.
"""
logging.info("Testing security-checklist")
unit_name = zaza_model.get_lead_unit_name('openstack-dashboard')
action = zaza_model.run_action(unit_name, 'security-checklist')
assert action.data.get(u"status") == "failed", \
"Security check is expected to not pass by default"
def test_900_restart_on_config_change(self):
"""Verify that the specified services are restarted on config changed.
Ported from amulet tests.
"""
logging.info("Testing restart on config changed.")
# Expected default and alternate values
current_value = zaza_model.get_application_config(
self.application_name)['use-syslog']['value']
new_value = str(not bool(current_value)).title()
current_value = str(current_value).title()
# Expected default and alternate values
set_default = {'use-syslog': current_value}
set_alternate = {'use-syslog': new_value}
# Services which are expected to restart upon config change,
# and corresponding config files affected by the change
services = ['apache2', 'memcached']
conf_file = '/etc/openstack-dashboard/local_settings.py'
# Make config change, check for service restarts
logging.info('Setting use-syslog on openstack-dashboard {}'
.format(set_alternate))
self.restart_on_changed(
conf_file,
set_default,
set_alternate,
None, None,
services)
def test_910_pause_and_resume(self):
"""Run pause and resume tests.
Pause service and check services are stopped then resume and check
they are started
Ported from amulet tests.
"""
with self.pause_resume(['apache2']):
logging.info("Testing pause resume")
class OpenStackDashboardPolicydTests(policyd.BasePolicydSpecialization):
"""Test the policyd override using the dashboard."""
good = {
"identity/file1.yaml": "{'rule1': '!'}"
}
bad = {
"identity/file2.yaml": "{'rule': '!}"
}
path_infix = "keystone_policy.d"
_rule = {'identity/rule.yaml': yaml.dump({
'identity:list_domains': '!',
'identity:get_domain': '!',
'identity:update_domain': '!',
'identity:list_domains_for_user': '!',
})}
# url associated with rule above that will return HTTP 403
url = "http://{}/horizon/identity/domains"
@classmethod
def setUpClass(cls, application_name=None):
"""Run class setup for running horizon charm operation tests."""
super(OpenStackDashboardPolicydTests, cls).setUpClass(
application_name="openstack-dashboard")
cls.application_name = "openstack-dashboard"
def get_client_and_attempt_operation(self, ip):
"""Attempt to list users on the openstack-dashboard service.
This is slightly complicated in that the client is actually a web-site.
Thus, the test has to login first and then attempt the operation. This
makes the test a little more complicated.
:param ip: the IP address to get the session against.
:type ip: str
:raises: PolicydOperationFailedException if operation fails.
"""
dashboard_ip = _get_dashboard_ip()
logging.info("Dashboard is at {}".format(dashboard_ip))
overcloud_auth = openstack_utils.get_overcloud_auth()
password = overcloud_auth['OS_PASSWORD'],
logging.info("admin password is {}".format(password))
# try to get the url which will either pass or fail with a 403
overcloud_auth = openstack_utils.get_overcloud_auth()
domain = 'admin_domain',
username = 'admin',
password = overcloud_auth['OS_PASSWORD'],
client, response = _login(dashboard_ip, domain, username, password)
# now attempt to get the domains page
_url = self.url.format(dashboard_ip)
result = client.get(_url)
if result.status_code == 403:
raise policyd.PolicydOperationFailedException("Not authenticated")