Merge "Add release note for new role client methods"
diff --git a/releasenotes/notes/merge-tempest-horizon-plugin-39d555339ab8c7ce.yaml b/releasenotes/notes/merge-tempest-horizon-plugin-39d555339ab8c7ce.yaml
new file mode 100644
index 0000000..ff406fb
--- /dev/null
+++ b/releasenotes/notes/merge-tempest-horizon-plugin-39d555339ab8c7ce.yaml
@@ -0,0 +1,6 @@
+---
+prelude: >
+ The integrated horizon dashboard test is now moved
+ from tempest-horizon plugin into Tempest. You do not need
+ to install tempest-horizon to run the horizon test which
+ can be run using Tempest itself.
diff --git a/releasenotes/notes/random-bytes-size-limit-ee94a8c6534fe916.yaml b/releasenotes/notes/random-bytes-size-limit-ee94a8c6534fe916.yaml
new file mode 100644
index 0000000..42322e4
--- /dev/null
+++ b/releasenotes/notes/random-bytes-size-limit-ee94a8c6534fe916.yaml
@@ -0,0 +1,9 @@
+---
+upgrade:
+ - |
+ The ``tempest.lib.common.utils.data_utils.random_bytes()`` helper
+ function will no longer allow a ``size`` of more than 1MiB. Tests
+ generally do not need to generate and use large payloads for
+ feature verification and it is easy to lose track of and duplicate
+ large buffers. The sum total of such errors can become problematic
+ in paralllelized and constrained CI environments.
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index 9e25901..d1f6f98 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -158,7 +158,7 @@
self.client.stage_image_file(
image['id'],
- six.BytesIO(data_utils.random_bytes(10485760)))
+ six.BytesIO(data_utils.random_bytes()))
# Check image status is 'uploading'
body = self.client.show_image(image['id'])
self.assertEqual(image['id'], body['id'])
diff --git a/tempest/common/utils/__init__.py b/tempest/common/utils/__init__.py
index 914acf7..38881ee 100644
--- a/tempest/common/utils/__init__.py
+++ b/tempest/common/utils/__init__.py
@@ -59,6 +59,7 @@
# So we should set this True here.
'identity': True,
'object_storage': CONF.service_available.swift,
+ 'dashboard': CONF.service_available.horizon,
}
return service_list
diff --git a/tempest/config.py b/tempest/config.py
index 7c4539c..956b593 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -845,6 +845,18 @@
'This value will be increased in case of conflict.')
]
+dashboard_group = cfg.OptGroup(name="dashboard",
+ title="Dashboard options")
+
+DashboardGroup = [
+ cfg.StrOpt('dashboard_url',
+ default='http://localhost/',
+ help="Where the dashboard can be found"),
+ cfg.BoolOpt('disable_ssl_certificate_validation',
+ default=False,
+ help="Set to True if using self-signed SSL certificates."),
+]
+
validation_group = cfg.OptGroup(name='validation',
title='SSH Validation options')
@@ -1190,6 +1202,9 @@
cfg.BoolOpt('nova',
default=True,
help="Whether or not nova is expected to be available"),
+ cfg.BoolOpt('horizon',
+ default=True,
+ help="Whether or not horizon is expected to be available"),
]
debug_group = cfg.OptGroup(name="debug",
@@ -1253,6 +1268,7 @@
(image_feature_group, ImageFeaturesGroup),
(network_group, NetworkGroup),
(network_feature_group, NetworkFeaturesGroup),
+ (dashboard_group, DashboardGroup),
(validation_group, ValidationGroup),
(volume_group, VolumeGroup),
(volume_feature_group, VolumeFeaturesGroup),
@@ -1320,6 +1336,7 @@
self.image_feature_enabled = _CONF['image-feature-enabled']
self.network = _CONF.network
self.network_feature_enabled = _CONF['network-feature-enabled']
+ self.dashboard = _CONF.dashboard
self.validation = _CONF.validation
self.volume = _CONF.volume
self.volume_feature_enabled = _CONF['volume-feature-enabled']
diff --git a/tempest/lib/common/dynamic_creds.py b/tempest/lib/common/dynamic_creds.py
index 038528f..220d96c 100644
--- a/tempest/lib/common/dynamic_creds.py
+++ b/tempest/lib/common/dynamic_creds.py
@@ -144,9 +144,9 @@
# need a different token scope for them.
if self.default_admin_creds.system:
scope = 'system'
- elif (self.default_admin_creds.domain_id or
- self.default_admin_creds.domain_name or
- self.identity_admin_domain_scope):
+ elif (self.identity_admin_domain_scope and
+ (self.default_admin_creds.domain_id or
+ self.default_admin_creds.domain_name)):
scope = 'domain'
else:
scope = 'project'
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index b47b511..a987e03 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -104,16 +104,18 @@
'location', 'proxy-authenticate',
'retry-after', 'server',
'vary', 'www-authenticate'))
- dscv = disable_ssl_certificate_validation
+ self.dscv = disable_ssl_certificate_validation
if proxy_url:
self.http_obj = http.ClosingProxyHttp(
proxy_url,
- disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
+ disable_ssl_certificate_validation=self.dscv,
+ ca_certs=ca_certs,
timeout=http_timeout, follow_redirects=follow_redirects)
else:
self.http_obj = http.ClosingHttp(
- disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
+ disable_ssl_certificate_validation=self.dscv,
+ ca_certs=ca_certs,
timeout=http_timeout, follow_redirects=follow_redirects)
def get_headers(self, accept_type=None, send_type=None):
diff --git a/tempest/lib/common/utils/data_utils.py b/tempest/lib/common/utils/data_utils.py
index 44b55eb..b6671b5 100644
--- a/tempest/lib/common/utils/data_utils.py
+++ b/tempest/lib/common/utils/data_utils.py
@@ -169,6 +169,8 @@
:return: size randomly bytes
:rtype: string
"""
+ if size > 1 << 20:
+ raise RuntimeError('Size should be less than 1MiB')
return b''.join([six.int2byte(random.randint(0, 255))
for i in range(size)])
diff --git a/tempest/lib/services/object_storage/object_client.py b/tempest/lib/services/object_storage/object_client.py
index 383aff6..1d38153 100644
--- a/tempest/lib/services/object_storage/object_client.py
+++ b/tempest/lib/services/object_storage/object_client.py
@@ -12,6 +12,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
+import ssl
from six.moves import http_client as httplib
from six.moves.urllib import parse as urlparse
@@ -118,7 +119,7 @@
path = str(parsed.path) + "/"
path += "%s/%s" % (str(container), str(object_name))
- conn = _create_connection(parsed)
+ conn = self._create_connection(parsed)
# Send the PUT request and the headers including the "Expect" header
conn.putrequest('PUT', path)
@@ -151,15 +152,20 @@
return resp.status, resp.reason
+ def _create_connection(self, parsed_url):
+ """Helper function to create connection with httplib
-def _create_connection(parsed_url):
- """Helper function to create connection with httplib
+ :param parsed_url: parsed url of the remote location
+ """
+ context = None
+ # If CONF.identity.disable_ssl_certificate_validation is true,
+ # do not check ssl certification.
+ if self.dscv:
+ context = ssl._create_unverified_context()
+ if parsed_url.scheme == 'https':
+ conn = httplib.HTTPSConnection(parsed_url.netloc,
+ context=context)
+ else:
+ conn = httplib.HTTPConnection(parsed_url.netloc)
- :param parsed_url: parsed url of the remote location
- """
- if parsed_url.scheme == 'https':
- conn = httplib.HTTPSConnection(parsed_url.netloc)
- else:
- conn = httplib.HTTPConnection(parsed_url.netloc)
-
- return conn
+ return conn
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index ce13166..acc563a 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -143,10 +143,20 @@
# resp part which is not used in scenario tests
def create_port(self, network_id, client=None, **kwargs):
- """Creates port for the respective network_id"""
+ """Creates port for the respective network_id
+
+ :param network_id: the id of the network
+ :param client: the client to use, defaults to self.ports_client
+ :param kwargs: additional arguments such as:
+ - namestart - a string to generate a name for the port from
+ - default is self.__class__.__name__
+ - 'binding:vnic_type' - defaults to CONF.network.port_vnic_type
+ - 'binding:profile' - defaults to CONF.network.port_profile
+ """
if not client:
client = self.ports_client
- name = kwargs.pop('namestart', self.__class__.__name__)
+ name = data_utils.rand_name(
+ kwargs.pop('namestart', self.__class__.__name__))
if CONF.network.port_vnic_type and 'binding:vnic_type' not in kwargs:
kwargs['binding:vnic_type'] = CONF.network.port_vnic_type
if CONF.network.port_profile and 'binding:profile' not in kwargs:
@@ -216,6 +226,9 @@
the port.
example: port_profile = "capabilities:[switchdev]"
Defaults to ``CONF.network.port_profile``.
+ * *create_port_body* (``dict``) --
+ This attribute is a dictionary of additional arguments to be
+ passed to create_port method.
"""
# NOTE(jlanoux): As a first step, ssh checks in the scenario
@@ -241,7 +254,7 @@
# every network
if vnic_type or profile:
ports = []
- create_port_body = {}
+ create_port_body = kwargs.pop('create_port_body', {})
if vnic_type:
create_port_body['binding:vnic_type'] = vnic_type
@@ -402,19 +415,17 @@
self.assertEqual(backup_id, restore['backup_id'])
return restore
- def rebuild_server(self, server_id, image=None,
- preserve_ephemeral=False, wait=True,
- rebuild_kwargs=None):
+ def rebuild_server(self, server_id, image=None, preserve_ephemeral=False,
+ wait=True, **kwargs):
if image is None:
image = CONF.compute.image_ref
- rebuild_kwargs = rebuild_kwargs or {}
LOG.debug("Rebuilding server (id: %s, image: %s, preserve eph: %s)",
server_id, image, preserve_ephemeral)
self.servers_client.rebuild_server(
server_id=server_id,
image_ref=image,
preserve_ephemeral=preserve_ephemeral,
- **rebuild_kwargs)
+ **kwargs)
if wait:
waiters.wait_for_server_status(self.servers_client,
server_id, 'ACTIVE')
diff --git a/tempest/scenario/test_dashboard_basic_ops.py b/tempest/scenario/test_dashboard_basic_ops.py
new file mode 100644
index 0000000..b1098fa
--- /dev/null
+++ b/tempest/scenario/test_dashboard_basic_ops.py
@@ -0,0 +1,141 @@
+# 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.
+
+import html.parser
+import ssl
+from urllib import parse
+from urllib import request
+
+from tempest.common import utils
+from tempest import config
+from tempest.lib import decorators
+from tempest import test
+
+CONF = config.CONF
+
+
+class HorizonHTMLParser(html.parser.HTMLParser):
+ csrf_token = None
+ region = None
+ login = None
+
+ def _find_name(self, attrs, name):
+ for attrpair in attrs:
+ if attrpair[0] == 'name' and attrpair[1] == name:
+ return True
+ return False
+
+ def _find_value(self, attrs):
+ for attrpair in attrs:
+ if attrpair[0] == 'value':
+ return attrpair[1]
+ return None
+
+ def _find_attr_value(self, attrs, attr_name):
+ for attrpair in attrs:
+ if attrpair[0] == attr_name:
+ return attrpair[1]
+ return None
+
+ def handle_starttag(self, tag, attrs):
+ if tag == 'input':
+ if self._find_name(attrs, 'csrfmiddlewaretoken'):
+ self.csrf_token = self._find_value(attrs)
+ if self._find_name(attrs, 'region'):
+ self.region = self._find_value(attrs)
+ if tag == 'form':
+ self.login = self._find_attr_value(attrs, 'action')
+
+
+class TestDashboardBasicOps(test.BaseTestCase):
+
+ """The test suite for dashboard basic operations
+
+ This is a basic scenario test:
+ * checks that the login page is available
+ * logs in as a regular user
+ * checks that the user home page loads without error
+ """
+ opener = None
+
+ credentials = ['primary']
+
+ @classmethod
+ def skip_checks(cls):
+ super(TestDashboardBasicOps, cls).skip_checks()
+ if not CONF.service_available.horizon:
+ raise cls.skipException("Horizon support is required")
+
+ @classmethod
+ def setup_credentials(cls):
+ cls.set_network_resources()
+ super(TestDashboardBasicOps, cls).setup_credentials()
+
+ def check_login_page(self):
+ response = self._get_opener().open(CONF.dashboard.dashboard_url).read()
+ self.assertIn("id_username", response.decode("utf-8"))
+
+ def user_login(self, username, password):
+ response = self._get_opener().open(CONF.dashboard.dashboard_url).read()
+
+ # Grab the CSRF token and default region
+ parser = HorizonHTMLParser()
+ parser.feed(response.decode("utf-8"))
+
+ # construct login url for dashboard, discovery accommodates non-/ web
+ # root for dashboard
+ login_url = parse.urljoin(CONF.dashboard.dashboard_url, parser.login)
+
+ # Prepare login form request
+ req = request.Request(login_url)
+ req.add_header('Content-type', 'application/x-www-form-urlencoded')
+ req.add_header('Referer', CONF.dashboard.dashboard_url)
+
+ # Pass the default domain name regardless of the auth version in order
+ # to test the scenario of when horizon is running with keystone v3
+ params = {'username': username,
+ 'password': password,
+ 'region': parser.region,
+ 'domain': CONF.auth.default_credentials_domain_name,
+ 'csrfmiddlewaretoken': parser.csrf_token}
+ self._get_opener().open(req, parse.urlencode(params).encode())
+
+ def check_home_page(self):
+ response = self._get_opener().open(CONF.dashboard.dashboard_url).read()
+ self.assertIn('Overview', response.decode("utf-8"))
+
+ def _get_opener(self):
+ if not self.opener:
+ if (CONF.dashboard.disable_ssl_certificate_validation and
+ self._ssl_default_context_supported()):
+ ctx = ssl.create_default_context()
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+ self.opener = request.build_opener(
+ request.HTTPSHandler(context=ctx),
+ request.HTTPCookieProcessor())
+ else:
+ self.opener = request.build_opener(
+ request.HTTPCookieProcessor())
+ return self.opener
+
+ def _ssl_default_context_supported(self):
+ return (hasattr(ssl, 'create_default_context'))
+
+ @decorators.attr(type='smoke')
+ @decorators.idempotent_id('4f8851b1-0e69-482b-b63b-84c6e76f6c80')
+ @utils.services('dashboard')
+ def test_basic_scenario(self):
+ creds = self.os_primary.credentials
+ self.check_login_page()
+ self.user_login(creds.username, creds.password)
+ self.check_home_page()
diff --git a/tempest/scenario/test_encrypted_cinder_volumes.py b/tempest/scenario/test_encrypted_cinder_volumes.py
index fc93a5e..6ee9f28 100644
--- a/tempest/scenario/test_encrypted_cinder_volumes.py
+++ b/tempest/scenario/test_encrypted_cinder_volumes.py
@@ -30,8 +30,7 @@
For both LUKS and cryptsetup encryption types, this test performs
the following:
- * Creates an image in Glance
- * Boots an instance from the image
+ * Boots an instance from an image (CONF.compute.image_ref)
* Creates an encryption type (as admin)
* Creates a volume of that encryption type (as a regular user)
* Attaches and detaches the encrypted volume to the instance
@@ -44,10 +43,9 @@
raise cls.skipException('Encrypted volume attach is not supported')
def launch_instance(self):
- image = self.image_create()
keypair = self.create_keypair()
- return self.create_server(image_id=image, key_name=keypair['name'])
+ return self.create_server(key_name=keypair['name'])
def attach_detach_volume(self, server, volume):
attached_volume = self.nova_volume_attach(server, volume)
diff --git a/tempest/scenario/test_server_basic_ops.py b/tempest/scenario/test_server_basic_ops.py
index 02bc692..60242d5 100644
--- a/tempest/scenario/test_server_basic_ops.py
+++ b/tempest/scenario/test_server_basic_ops.py
@@ -67,7 +67,10 @@
def verify_metadata(self):
if self.run_ssh and CONF.compute_feature_enabled.metadata_service:
# Verify metadata service
- md_url = 'http://169.254.169.254/latest/meta-data/public-ipv4'
+ if CONF.network.public_network_id:
+ md_url = 'http://169.254.169.254/latest/meta-data/public-ipv4'
+ else:
+ md_url = 'http://169.254.169.254/latest/meta-data/local-ipv4'
def exec_cmd_and_verify_output():
cmd = 'curl ' + md_url
diff --git a/tempest/tests/lib/services/object_storage/test_object_client.py b/tempest/tests/lib/services/object_storage/test_object_client.py
index c646d61..d6df243 100644
--- a/tempest/tests/lib/services/object_storage/test_object_client.py
+++ b/tempest/tests/lib/services/object_storage/test_object_client.py
@@ -31,15 +31,18 @@
self.object_client = object_client.ObjectClient(self.fake_auth,
'swift', 'region1')
- @mock.patch.object(object_client, '_create_connection')
+ @mock.patch('tempest.lib.services.object_storage.object_client.'
+ 'ObjectClient._create_connection')
def test_create_object_continue_no_data(self, mock_poc):
self._validate_create_object_continue(None, mock_poc)
- @mock.patch.object(object_client, '_create_connection')
+ @mock.patch('tempest.lib.services.object_storage.object_client.'
+ 'ObjectClient._create_connection')
def test_create_object_continue_with_data(self, mock_poc):
self._validate_create_object_continue('hello', mock_poc)
- @mock.patch.object(object_client, '_create_connection')
+ @mock.patch('tempest.lib.services.object_storage.object_client.'
+ 'ObjectClient._create_connection')
def test_create_continue_with_no_continue_received(self, mock_poc):
self._validate_create_object_continue('hello', mock_poc,
initial_status=201)
diff --git a/zuul.d/integrated-gate.yaml b/zuul.d/integrated-gate.yaml
index 4c1ee5a..27bbf64 100644
--- a/zuul.d/integrated-gate.yaml
+++ b/zuul.d/integrated-gate.yaml
@@ -69,6 +69,8 @@
Former names for this job where:
* legacy-tempest-dsvm-py35
* gate-tempest-dsvm-py35
+ required-projects:
+ - openstack/horizon
vars:
tox_envlist: full
devstack_localrc:
@@ -89,6 +91,8 @@
network-feature-enabled:
qos_placement_physnet: public
devstack_services:
+ # Enbale horizon so that we can run horizon test.
+ horizon: true
s-account: false
s-container: false
s-object: false