Merge pull request #302 from coreycb/add-tempest-support-corey

Add tempest support
This commit is contained in:
David Ames
2020-06-10 08:33:18 -07:00
committed by GitHub
13 changed files with 683 additions and 3 deletions

View File

@@ -28,11 +28,11 @@ def basic_setup():
tests.
"""
current_release = openstack_utils.get_os_release()
xenial_pike = openstack_utils.get_os_release('xenial_pike')
xenial_ocata = openstack_utils.get_os_release('xenial_ocata')
if current_release < xenial_pike:
if current_release < xenial_ocata:
logging.info(
'Skipping ceilometer-upgrade as it is not supported before Pike')
'Skipping ceilometer-upgrade as it is not supported before ocata')
return
logging.debug('Checking ceilometer-upgrade')

View File

@@ -18,6 +18,7 @@ import logging
import zaza.openstack.utilities.openstack as openstack_utils
CIRROS_IMAGE_NAME = "cirros"
CIRROS_ALT_IMAGE_NAME = "cirros_alt"
LTS_RELEASE = "bionic"
LTS_IMAGE_NAME = "bionic"
@@ -77,6 +78,18 @@ def add_cirros_image(glance_client=None, image_name=None):
image_name=image_name)
def add_cirros_alt_image(glance_client=None, image_name=None):
"""Add alt cirros image to the current deployment.
:param glance: Authenticated glanceclient
:type glance: glanceclient.Client
:param image_name: Label for the image in glance
:type image_name: str
"""
image_name = image_name or CIRROS_ALT_IMAGE_NAME
add_cirros_image(glance_client, image_name)
def add_lts_image(glance_client=None, image_name=None, release=None):
"""Add an Ubuntu LTS image to the current deployment.

View File

@@ -26,6 +26,8 @@ DEMO_ADMIN_USER_PASSWORD = 'password'
DEMO_USER = 'demo'
DEMO_PASSWORD = 'password'
TEMPEST_ROLES = ['member', 'ResellerAdmin']
class BaseKeystoneTest(test_utils.OpenStackBaseTest):
"""Base for Keystone charm tests."""

View File

@@ -14,6 +14,8 @@
"""Code for setting up keystone."""
import keystoneauth1
import zaza.openstack.utilities.openstack as openstack_utils
from zaza.openstack.charm_tests.keystone import (
BaseKeystoneTest,
@@ -24,6 +26,7 @@ from zaza.openstack.charm_tests.keystone import (
DEMO_ADMIN_USER_PASSWORD,
DEMO_USER,
DEMO_PASSWORD,
TEMPEST_ROLES,
)
@@ -115,3 +118,30 @@ def add_demo_user():
else:
# create only V3 user
_v3()
def _add_additional_roles(roles):
"""Add additional roles to this deployment.
:param ctxt: roles
:type ctxt: list
:returns: None
:rtype: None
"""
keystone_session = openstack_utils.get_overcloud_keystone_session()
keystone_client = openstack_utils.get_keystone_session_client(
keystone_session)
for role_name in roles:
try:
keystone_client.roles.create(role_name)
except keystoneauth1.exceptions.http.Conflict:
pass
def add_tempest_roles():
"""Add tempest roles to this deployment.
:returns: None
:rtype: None
"""
_add_additional_roles(TEMPEST_ROLES)

View File

@@ -35,5 +35,15 @@ FLAVORS = {
'ram': 8192,
'disk': 40,
'vcpus': 4},
'm1.tempest': {
'flavorid': 6,
'ram': 256,
'disk': 1,
'vcpus': 1},
'm2.tempest': {
'flavorid': 7,
'ram': 512,
'disk': 1,
'vcpus': 1},
}
KEYPAIR_NAME = 'zaza'

View File

@@ -0,0 +1,15 @@
# Copyright 2020 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.
"""Collection of code for setting up and running tempest."""

View File

@@ -0,0 +1,291 @@
# Copyright 2020 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.
"""Code for configuring and initializing tempest."""
import urllib.parse
import os
import subprocess
import zaza.utilities.deployment_env as deployment_env
import zaza.openstack.utilities.juju as juju_utils
import zaza.openstack.utilities.openstack as openstack_utils
import zaza.openstack.charm_tests.glance.setup as glance_setup
import zaza.openstack.charm_tests.tempest.templates.tempest_v2 as tempest_v2
import zaza.openstack.charm_tests.tempest.templates.tempest_v3 as tempest_v3
import zaza.openstack.charm_tests.tempest.templates.accounts as accounts
SETUP_ENV_VARS = [
'TEST_GATEWAY',
'TEST_CIDR_EXT',
'TEST_FIP_RANGE',
'TEST_NAMESERVER',
'TEST_CIDR_PRIV',
'TEST_SWIFT_IP',
]
TEMPEST_FLAVOR_NAME = 'm1.tempest'
TEMPEST_ALT_FLAVOR_NAME = 'm2.tempest'
def add_application_ips(ctxt):
"""Add application access IPs to context.
:param ctxt: Context dictionary
:type ctxt: dict
:returns: None
:rtype: None
"""
ctxt['keystone'] = juju_utils.get_application_ip('keystone')
ctxt['dashboard'] = juju_utils.get_application_ip('openstack-dashboard')
ctxt['ncc'] = juju_utils.get_application_ip('nova-cloud-controller')
def add_nova_config(ctxt, keystone_session):
"""Add nova config to context.
:param ctxt: Context dictionary
:type ctxt: dict
:param keystone_session: keystoneauth1.session.Session object
:type: keystoneauth1.session.Session
:returns: None
:rtype: None
"""
nova_client = openstack_utils.get_nova_session_client(
keystone_session)
for flavor in nova_client.flavors.list():
if flavor.name == TEMPEST_FLAVOR_NAME:
ctxt['flavor_ref'] = flavor.id
if flavor.name == TEMPEST_ALT_FLAVOR_NAME:
ctxt['flavor_ref_alt'] = flavor.id
def add_neutron_config(ctxt, keystone_session):
"""Add neutron config to context.
:param ctxt: Context dictionary
:type ctxt: dict
:param keystone_session: keystoneauth1.session.Session object
:type: keystoneauth1.session.Session
:returns: None
:rtype: None
"""
current_release = openstack_utils.get_os_release()
focal_ussuri = openstack_utils.get_os_release('focal_ussuri')
neutron_client = openstack_utils.get_neutron_session_client(
keystone_session)
net = neutron_client.find_resource("network", "ext_net")
ctxt['ext_net'] = net['id']
router = neutron_client.find_resource("router", "provider-router")
ctxt['provider_router_id'] = router['id']
# For focal+ with OVN, we use the same settings as upstream gate.
# This is because the l3_agent_scheduler extension is only
# applicable for OVN when conventional layer-3 agent enabled:
# https://docs.openstack.org/networking-ovn/2.0.1/features.html
# This enables test_list_show_extensions to run successfully.
if current_release >= focal_ussuri:
extensions = ('address-scope,agent,allowed-address-pairs,'
'auto-allocated-topology,availability_zone,'
'binding,default-subnetpools,external-net,'
'extra_dhcp_opt,multi-provider,net-mtu,'
'network_availability_zone,network-ip-availability,'
'port-security,provider,quotas,rbac-address-scope,'
'rbac-policies,standard-attr-revisions,security-group,'
'standard-attr-description,subnet_allocation,'
'standard-attr-tag,standard-attr-timestamp,trunk,'
'quota_details,router,extraroute,ext-gw-mode,'
'fip-port-details,pagination,sorting,project-id,'
'dns-integration,qos')
ctxt['neutron_api_extensions'] = extensions
else:
ctxt['neutron_api_extensions'] = 'all'
def add_glance_config(ctxt, keystone_session):
"""Add glance config to context.
:param ctxt: Context dictionary
:type ctxt: dict
:param keystone_session: keystoneauth1.session.Session object
:type: keystoneauth1.session.Session
:returns: None
:rtype: None
"""
glance_client = openstack_utils.get_glance_session_client(
keystone_session)
image = openstack_utils.get_images_by_name(
glance_client, glance_setup.CIRROS_IMAGE_NAME)
image_alt = openstack_utils.get_images_by_name(
glance_client, glance_setup.CIRROS_ALT_IMAGE_NAME)
if image:
ctxt['image_id'] = image[0].id
if image_alt:
ctxt['image_alt_id'] = image_alt[0].id
def add_cinder_config(ctxt, keystone_session):
"""Add cinder config to context.
:param ctxt: Context dictionary
:type ctxt: dict
:param keystone_session: keystoneauth1.session.Session object
:type: keystoneauth1.session.Session
:returns: None
:rtype: None
"""
volume_types = ['volumev2', 'volumev3']
keystone_client = openstack_utils.get_keystone_session_client(
keystone_session)
for volume_type in volume_types:
service = keystone_client.services.list(type=volume_type)
if service:
ctxt['catalog_type'] = volume_type
break
def add_keystone_config(ctxt, keystone_session):
"""Add keystone config to context.
:param ctxt: Context dictionary
:type ctxt: dict
:param keystone_session: keystoneauth1.session.Session object
:type: keystoneauth1.session.Session
:returns: None
:rtype: None
"""
keystone_client = openstack_utils.get_keystone_session_client(
keystone_session)
domain = keystone_client.domains.find(name="admin_domain")
ctxt['default_domain_id'] = domain.id
def add_environment_var_config(ctxt):
"""Add environment variable config to context.
:param ctxt: Context dictionary
:type ctxt: dict
:returns: None
:rtype: None
"""
deploy_env = deployment_env.get_deployment_context()
for var in SETUP_ENV_VARS:
value = deploy_env.get(var)
if value:
ctxt[var.lower()] = value
else:
raise ValueError(
("Environment variables {} must all be set to run this"
" test").format(', '.join(SETUP_ENV_VARS)))
def add_auth_config(ctxt):
"""Add authorization config to context.
:param ctxt: Context dictionary
:type ctxt: dict
:returns: None
:rtype: None
"""
overcloud_auth = openstack_utils.get_overcloud_auth()
ctxt['proto'] = urllib.parse.urlparse(overcloud_auth['OS_AUTH_URL']).scheme
ctxt['admin_username'] = overcloud_auth['OS_USERNAME']
ctxt['admin_password'] = overcloud_auth['OS_PASSWORD']
if overcloud_auth['API_VERSION'] == 3:
ctxt['admin_project_name'] = overcloud_auth['OS_PROJECT_NAME']
ctxt['admin_domain_name'] = overcloud_auth['OS_DOMAIN_NAME']
ctxt['default_credentials_domain_name'] = (
overcloud_auth['OS_PROJECT_DOMAIN_NAME'])
def get_tempest_context():
"""Generate the tempest config context.
:returns: Context dictionary
:rtype: dict
"""
keystone_session = openstack_utils.get_overcloud_keystone_session()
ctxt = {}
add_application_ips(ctxt)
add_nova_config(ctxt, keystone_session)
add_neutron_config(ctxt, keystone_session)
add_glance_config(ctxt, keystone_session)
add_cinder_config(ctxt, keystone_session)
add_keystone_config(ctxt, keystone_session)
add_environment_var_config(ctxt)
add_auth_config(ctxt)
return ctxt
def render_tempest_config(target_file, ctxt, template):
"""Render tempest config for specified config file and template.
:param target_file: Name of file to render config to
:type target_file: str
:param ctxt: Context dictionary
:type ctxt: dict
:param template: Template module
:type template: module
:returns: None
:rtype: None
"""
# TODO: switch to jinja2 and generate config based on available services
with open(target_file, 'w') as f:
f.write(template.file_contents.format(**ctxt))
def setup_tempest(tempest_template, accounts_template):
"""Initialize tempest and render tempest config.
:param tempest_template: tempest.conf template
:type tempest_template: module
:param accounts_template: accounts.yaml template
:type accounts_template: module
:returns: None
:rtype: None
"""
if os.path.isdir('tempest-workspace'):
try:
subprocess.check_call(['tempest', 'workspace', 'remove', '--rmdir',
'--name', 'tempest-workspace'])
except subprocess.CalledProcessError:
pass
try:
subprocess.check_call(['tempest', 'init', 'tempest-workspace'])
except subprocess.CalledProcessError:
pass
render_tempest_config(
'tempest-workspace/etc/tempest.conf',
get_tempest_context(),
tempest_template)
render_tempest_config(
'tempest-workspace/etc/accounts.yaml',
get_tempest_context(),
accounts_template)
def render_tempest_config_keystone_v2():
"""Render tempest config for Keystone V2 API.
:returns: None
:rtype: None
"""
setup_tempest(tempest_v2, accounts)
def render_tempest_config_keystone_v3():
"""Render tempest config for Keystone V3 API.
:returns: None
:rtype: None
"""
setup_tempest(tempest_v3, accounts)

View File

@@ -0,0 +1,15 @@
# Copyright 2020 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.
"""Collection of templates for tempest."""

View File

@@ -0,0 +1,9 @@
# flake8: noqa
file_contents = """
- username: 'demo'
tenant_name: 'demo'
password: 'pass'
- username: 'alt_demo'
tenant_name: 'alt_demo'
password: 'secret'
"""

View File

@@ -0,0 +1,96 @@
# flake8: noqa
file_contents = """
[DEFAULT]
debug = false
use_stderr = false
log_file = tempest.log
[auth]
test_accounts_file = accounts.yaml
default_credentials_domain_name = Default
admin_username = {admin_username}
admin_project_name = admin
admin_password = {admin_password}
admin_domain_name = Default
[compute]
image_ref = {image_id}
image_ref_alt = {image_alt_id}
flavor_ref = {flavor_ref}
flavor_ref_alt = {flavor_ref_alt}
region = RegionOne
min_compute_nodes = 3
# TODO: review this as its release specific
# min_microversion = 2.2
# max_microversion = latest
[compute-feature-enabled]
console_output = true
resize = true
live_migration = true
block_migration_for_live_migration = true
attach_encrypted_volume = false
[identity]
uri = {proto}://{keystone}:5000/v2.0
auth_version = v2
admin_role = Admin
region = RegionOne
disable_ssl_certificate_validation = true
[identity-feature-enabled]
api_v2 = true
api_v3 = false
[image]
http_image = http://{test_swift_ip}:80/swift/v1/images/cirros-0.3.4-x86_64-uec.tar.gz
[network]
project_network_cidr = {test_cidr_priv}
public_network_id = {ext_net}
dns_servers = {test_nameserver}
project_networks_reachable = false
[network-feature-enabled]
ipv6 = false
[orchestration]
stack_owner_role = Admin
instance_type = m1.small
keypair_name = testkey
[oslo_concurrency]
lock_path = /tmp
[scenario]
img_dir = /home/ubuntu/images
img_file = cirros-0.3.4-x86_64-disk.img
img_container_format = bare
img_disk_format = qcow2
[validation]
run_validation = true
image_ssh_user = cirros
[service_available]
ceilometer = true
cinder = true
glance = true
heat = true
horizon = true
ironic = false
neutron = true
nova = true
sahara = false
swift = true
trove = false
zaqar = false
[volume]
backend_names = cinder-ceph
storage_protocol = ceph
catalog_type = {catalog_type}
[volume-feature-enabled]
backup = false"""

View File

@@ -0,0 +1,100 @@
# flake8: noqa
file_contents = """
[DEFAULT]
debug = false
use_stderr = false
log_file = tempest.log
[auth]
test_accounts_file = accounts.yaml
default_credentials_domain_name = {default_credentials_domain_name}
admin_username = {admin_username}
admin_project_name = {admin_project_name}
admin_password = {admin_password}
admin_domain_name = {admin_domain_name}
[compute]
image_ref = {image_id}
image_ref_alt = {image_alt_id}
flavor_ref = {flavor_ref}
flavor_ref_alt = {flavor_ref_alt}
min_compute_nodes = 3
# TODO: review this as its release specific
# min_microversion = 2.2
# max_microversion = latest
[compute-feature-enabled]
console_output = true
resize = true
live_migration = true
block_migration_for_live_migration = true
attach_encrypted_volume = false
[identity]
uri = {proto}://{keystone}:5000/v2.0
uri_v3 = {proto}://{keystone}:5000/v3
auth_version = v3
admin_role = Admin
region = RegionOne
default_domain_id = {default_domain_id}
admin_domain_scope = true
disable_ssl_certificate_validation = true
[identity-feature-enabled]
api_v2 = false
api_v3 = true
[image]
http_image = http://{test_swift_ip}:80/swift/v1/images/cirros-0.3.4-x86_64-uec.tar.gz
[network]
project_network_cidr = {test_cidr_priv}
public_network_id = {ext_net}
dns_servers = {test_nameserver}
project_networks_reachable = false
floating_network_name = {ext_net}
[network-feature-enabled]
ipv6 = false
api_extensions = {neutron_api_extensions}
[orchestration]
stack_owner_role = Admin
instance_type = m1.small
keypair_name = testkey
[oslo_concurrency]
lock_path = /tmp
[scenario]
img_dir = /home/ubuntu/images
img_file = cirros-0.3.4-x86_64-disk.img
img_container_format = bare
img_disk_format = qcow2
[validation]
run_validation = true
image_ssh_user = cirros
[service_available]
ceilometer = true
cinder = true
glance = true
heat = true
horizon = true
ironic = false
neutron = true
nova = true
sahara = false
swift = true
trove = false
zaqar = false
[volume]
backend_names = cinder-ceph
storage_protocol = ceph
catalog_type = {catalog_type}
[volume-feature-enabled]
backup = false"""

View File

@@ -0,0 +1,78 @@
# Copyright 2020 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.
"""Code for running tempest tests."""
import os
import subprocess
import zaza
import zaza.charm_lifecycle.utils
import zaza.charm_lifecycle.test
import tempfile
class TempestTest():
"""Tempest test class."""
test_runner = zaza.charm_lifecycle.test.DIRECT
def run(self):
"""Run tempest tests as specified in tests/tests.yaml.
Test keys are parsed from ['tests_options']['tempest']['model'], where
valid test keys are: smoke (bool), whitelist (list of tests), blacklist
(list of tests), and regex (list of regex's).
:returns: Status of tempest run
:rtype: bool
"""
charm_config = zaza.charm_lifecycle.utils.get_charm_config()
tempest_options = ['tempest', 'run', '--workspace',
'tempest-workspace', '--config',
'tempest-workspace/etc/tempest.conf']
for model_alias in zaza.model.get_juju_model_aliases().keys():
tempest_test_key = model_alias
if model_alias == zaza.charm_lifecycle.utils.DEFAULT_MODEL_ALIAS:
tempest_test_key = 'default'
config = charm_config['tests_options']['tempest'][tempest_test_key]
if config.get('smoke'):
tempest_options.extend(['--smoke'])
if config.get('regex'):
tempest_options.extend(
['--regex',
' '.join([reg for reg in config.get('regex')])])
if config.get('black-regex'):
tempest_options.extend(
['--black-regex',
' '.join([reg for reg in config.get('black-regex')])])
with tempfile.TemporaryDirectory() as tmpdirname:
if config.get('whitelist'):
white_file = os.path.join(tmpdirname, 'white.cfg')
with open(white_file, 'w') as f:
f.write('\n'.join(config.get('whitelist')))
f.write('\n')
tempest_options.extend(['--whitelist-file', white_file])
if config.get('blacklist'):
black_file = os.path.join(tmpdirname, 'black.cfg')
with open(black_file, 'w') as f:
f.write('\n'.join(config.get('blacklist')))
f.write('\n')
tempest_options.extend(['--blacklist-file', black_file])
print(tempest_options)
try:
subprocess.check_call(tempest_options)
except subprocess.CalledProcessError:
return False
return True

View File

@@ -49,6 +49,27 @@ def get_application_status(application=None, unit=None, model_name=None):
return status
def get_application_ip(application):
"""Get the application's IP address.
:param application: Application name
:type application: str
:returns: Application's IP address
:rtype: str
"""
try:
app_config = model.get_application_config(application)
except KeyError:
return ''
vip = app_config.get("vip").get("value")
if vip:
ip = vip
else:
unit = model.get_units(application)[0]
ip = unit.public_address
return ip
def get_cloud_configs(cloud=None):
"""Get cloud configuration from local clouds.yaml.