Merge branch 'master' into hm-port

This commit is contained in:
Zhang Hua
2021-10-28 21:16:43 +08:00
38 changed files with 1684 additions and 493 deletions

4
pip.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
pip install pip==20.2.3
pip "$@"

View File

@@ -2,6 +2,7 @@
# This is necessary for Xenial builders
# BUG: https://github.com/openstack-charmers/zaza-openstack-tests/issues/530
lxml<4.6.3
pyparsing<3.0.0 # pin for aodhclient which is held for py35
aiounittest
async_generator
boto3
@@ -12,7 +13,7 @@ flake8>=2.2.4
flake8-docstrings
flake8-per-file-ignores
pydocstyle<4.0.0
coverage
coverage<6.0.0 # coverage 6.0+ drops support for py3.5/py2.7
mock>=1.2
nose>=1.3.7
pbr>=1.8.0,<1.9.0
@@ -41,7 +42,6 @@ python-novaclient
python-octaviaclient
python-swiftclient
tenacity
distro-info
paramiko
# Documentation requirements

View File

@@ -43,6 +43,7 @@ install_require = [
'PyYAML',
'tenacity',
'oslo.config<6.12.0',
'pyparsing<3.0.0', # pin for aodhclient which is held for py35
'aodhclient<1.4.0',
'gnocchiclient>=7.0.5,<8.0.0',
'pika>=1.1.0,<2.0.0',

View File

@@ -22,7 +22,7 @@ minversion = 3.2.0
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
install_command =
pip install {opts} {packages}
{toxinidir}/pip.sh install {opts} {packages}
commands = nosetests --with-coverage --cover-package=zaza.openstack {posargs} {toxinidir}/unit_tests

View File

@@ -11,8 +11,3 @@
# 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 sys
import unittest.mock as mock
sys.modules['zaza.utilities.maas'] = mock.MagicMock()

View File

@@ -15,25 +15,22 @@
import mock
import unittest
import zaza.openstack.charm_tests.tempest.setup as tempest_setup
import zaza.openstack.charm_tests.tempest.utils as tempest_utils
class TestTempestSetup(unittest.TestCase):
"""Test class to encapsulate testing Mysql test utils."""
def setUp(self):
super(TestTempestSetup, self).setUp()
class TestTempestUtils(unittest.TestCase):
"""Test class to encapsulate testing Tempest test utils."""
def test_add_environment_var_config_with_missing_variable(self):
ctxt = {}
with self.assertRaises(Exception) as context:
tempest_setup.add_environment_var_config(ctxt, ['swift'])
tempest_utils._add_environment_var_config(ctxt, ['swift'])
self.assertEqual(
('Environment variables [TEST_SWIFT_IP] must all be '
'set to run this test'),
str(context.exception))
@mock.patch.object(tempest_setup.deployment_env, 'get_deployment_context')
@mock.patch.object(tempest_utils.deployment_env, 'get_deployment_context')
def test_add_environment_var_config_with_all_variables(
self,
get_deployment_context):
@@ -45,10 +42,10 @@ class TestTempestSetup(unittest.TestCase):
'TEST_NAME_SERVER': 'test',
'TEST_CIDR_PRIV': 'test',
}
tempest_setup.add_environment_var_config(ctxt, ['neutron'])
tempest_utils._add_environment_var_config(ctxt, ['neutron'])
self.assertEqual(ctxt['test_gateway'], 'test')
@mock.patch.object(tempest_setup.deployment_env, 'get_deployment_context')
@mock.patch.object(tempest_utils.deployment_env, 'get_deployment_context')
def test_add_environment_var_config_with_some_variables(
self,
get_deployment_context):
@@ -59,7 +56,7 @@ class TestTempestSetup(unittest.TestCase):
'TEST_CIDR_PRIV': 'test',
}
with self.assertRaises(Exception) as context:
tempest_setup.add_environment_var_config(ctxt, ['neutron'])
tempest_utils._add_environment_var_config(ctxt, ['neutron'])
self.assertEqual(
('Environment variables [TEST_CIDR_EXT, TEST_FIP_RANGE] must '
'all be set to run this test'),

View File

@@ -24,6 +24,7 @@ import tenacity
import unit_tests.utils as ut_utils
from zaza.openstack.utilities import openstack as openstack_utils
from zaza.openstack.utilities import exceptions
from zaza.utilities.maas import LinkMode, MachineInterfaceMac
class TestOpenStackUtils(ut_utils.BaseTestCase):
@@ -57,11 +58,12 @@ class TestOpenStackUtils(ut_utils.BaseTestCase):
self.network = {
"network": {"id": "network_id",
"name": self.ext_net,
"tenant_id": self.project_id,
"router:external": True,
"provider:physical_network": "physnet1",
"provider:network_type": "flat"}}
"name": self.ext_net,
"router:external": True,
"shared": False,
"tenant_id": self.project_id,
"provider:physical_network": "physnet1",
"provider:network_type": "flat"}}
self.networks = {
"networks": [self.network["network"]]}
@@ -156,12 +158,12 @@ class TestOpenStackUtils(ut_utils.BaseTestCase):
self.neutronclient.create_address_scope.assert_called_once_with(
address_scope_msg)
def test_create_external_network(self):
def test_create_provider_network(self):
self.patch_object(openstack_utils, "get_net_uuid")
self.get_net_uuid.return_value = self.net_uuid
# Already exists
network = openstack_utils.create_external_network(
network = openstack_utils.create_provider_network(
self.neutronclient, self.project_id)
self.assertEqual(network, self.network["network"])
self.neutronclient.create_network.assert_not_called()
@@ -171,7 +173,7 @@ class TestOpenStackUtils(ut_utils.BaseTestCase):
"networks": []}
network_msg = copy.deepcopy(self.network)
network_msg["network"].pop("id")
network = openstack_utils.create_external_network(
network = openstack_utils.create_provider_network(
self.neutronclient, self.project_id)
self.assertEqual(network, self.network["network"])
self.neutronclient.create_network.assert_called_once_with(
@@ -1428,6 +1430,34 @@ class TestOpenStackUtils(ut_utils.BaseTestCase):
self.move.assert_called_once_with(
'tempfilename', '/tmp/default/ca1.cert')
def test_configure_charmed_openstack_on_maas(self):
self.patch_object(openstack_utils, 'get_charm_networking_data')
self.patch_object(openstack_utils.zaza.utilities.maas,
'get_macs_from_cidr')
self.patch_object(openstack_utils.zaza.utilities.maas,
'get_maas_client_from_juju_cloud_data')
self.patch_object(openstack_utils.zaza.model, 'get_cloud_data')
self.patch_object(openstack_utils, 'configure_networking_charms')
self.get_charm_networking_data.return_value = 'fakenetworkingdata'
self.get_macs_from_cidr.return_value = [
MachineInterfaceMac('id_a', 'ens6', '00:53:00:00:00:01',
'192.0.2.0/24', LinkMode.LINK_UP),
MachineInterfaceMac('id_a', 'ens7', '00:53:00:00:00:02',
'192.0.2.0/24', LinkMode.LINK_UP),
MachineInterfaceMac('id_b', 'ens6', '00:53:00:00:01:01',
'192.0.2.0/24', LinkMode.LINK_UP),
]
network_config = {'external_net_cidr': '192.0.2.0/24'}
expect = [
'00:53:00:00:00:01',
'00:53:00:00:01:01',
]
openstack_utils.configure_charmed_openstack_on_maas(
network_config)
self.configure_networking_charms.assert_called_once_with(
'fakenetworkingdata', expect, use_juju_wait=False)
class TestAsyncOpenstackUtils(ut_utils.AioTestCase):

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env python3
# Copyright 2021 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 testing ceilometer-agent."""

View File

@@ -0,0 +1,225 @@
#!/usr/bin/env python3
# Copyright 2021 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 ceilometer-agent testing."""
import logging
import time
from gnocchiclient.v1 import client as gnocchi_client
import zaza.openstack.charm_tests.glance.setup as glance_setup
import zaza.openstack.charm_tests.test_utils as test_utils
import zaza.openstack.utilities.openstack as openstack_utils
class CeilometerAgentTest(test_utils.OpenStackBaseTest):
"""Encapsulate ceilometer-agent tests."""
def tearDown(self):
"""Cleanup of VM guests."""
self.resource_cleanup()
def test_400_gnocchi_metrics(self):
"""Verify that ceilometer-agent publishes metrics to gnocchi."""
current_os_release = openstack_utils.get_os_release()
openstack_pike_or_older = (
current_os_release <=
openstack_utils.get_os_release('xenial_pike'))
if openstack_pike_or_older:
# Both the charm and Ceilometer itself had different behaviors in
# terms of which metrics were published and how fast, which would
# lead to a combinatorial explosion if we had to maintain test
# expectations for these old releases.
logging.info(
'OpenStack Pike or older, skipping')
return
# ceilometer-agent-compute reports metrics for each existing VM, so at
# least one VM is needed:
self.RESOURCE_PREFIX = 'zaza-ceilometer-agent'
self.launch_guest(
'ubuntu', instance_key=glance_setup.LTS_IMAGE_NAME)
logging.info('Instantiating gnocchi client...')
overcloud_auth = openstack_utils.get_overcloud_auth()
keystone = openstack_utils.get_keystone_client(overcloud_auth)
gnocchi_ep = keystone.service_catalog.url_for(
service_type='metric',
interface='publicURL'
)
gnocchi = gnocchi_client.Client(
session=openstack_utils.get_overcloud_keystone_session(),
adapter_options={
'endpoint_override': gnocchi_ep,
}
)
expected_metric_names = self.__get_expected_metric_names(
current_os_release)
min_timeout_seconds = 500
polling_interval_seconds = (
openstack_utils.get_application_config_option(
self.application_name, 'polling-interval'))
timeout_seconds = max(10 * polling_interval_seconds,
min_timeout_seconds)
logging.info('Giving ceilometer-agent {}s to publish all metrics to '
'gnocchi...'.format(timeout_seconds))
max_time = time.time() + timeout_seconds
while time.time() < max_time:
found_metric_names = {metric['name']
for metric in gnocchi.metric.list()}
missing_metric_names = expected_metric_names - found_metric_names
if len(missing_metric_names) == 0:
logging.info('All expected metrics found.')
break
time.sleep(polling_interval_seconds)
unexpected_found_metric_names = (
found_metric_names - expected_metric_names)
if len(unexpected_found_metric_names) > 0:
self.fail(
'Unexpected metrics '
'published: ' + ', '.join(unexpected_found_metric_names))
if len(missing_metric_names) > 0:
self.fail('These metrics should have been published but '
"weren't: " + ', '.join(missing_metric_names))
def __get_expected_metric_names(self, current_os_release):
expected_metric_names = {
'compute.instance.booting.time',
'disk.ephemeral.size',
'disk.root.size',
'image.download',
'image.serve',
'image.size',
'memory',
'vcpus',
}
all_polsters_are_enabled = (
openstack_utils.get_application_config_option(
self.application_name, 'enable-all-pollsters'))
if all_polsters_are_enabled:
expected_metric_names |= {
'disk.device.allocation',
'disk.device.capacity',
'disk.device.read.latency',
'disk.device.usage',
'disk.device.write.latency',
'memory.resident',
'memory.swap.in',
'memory.swap.out',
'network.incoming.packets.drop',
'network.incoming.packets.error',
'network.outgoing.packets.drop',
'network.outgoing.packets.error',
}
openstack_queens_or_older = (
current_os_release <=
openstack_utils.get_os_release('bionic_queens'))
openstack_rocky_or_older = (
current_os_release <=
openstack_utils.get_os_release('bionic_rocky'))
openstack_victoria_or_older = (
current_os_release <=
openstack_utils.get_os_release('groovy_victoria'))
if openstack_victoria_or_older:
expected_metric_names |= {
'cpu',
'disk.device.read.bytes',
'disk.device.read.requests',
'disk.device.write.bytes',
'disk.device.write.requests',
'memory.usage',
'network.incoming.bytes',
'network.incoming.packets',
'network.outgoing.bytes',
'network.outgoing.packets',
}
if openstack_rocky_or_older:
expected_metric_names |= {
'cpu.delta',
'cpu_util',
'disk.device.read.bytes.rate',
'disk.device.read.requests.rate',
'disk.device.write.bytes.rate',
'disk.device.write.requests.rate',
'network.incoming.bytes.rate',
'network.incoming.packets.rate',
'network.outgoing.bytes.rate',
'network.outgoing.packets.rate',
}
if all_polsters_are_enabled:
expected_metric_names |= {
'disk.allocation',
'disk.capacity',
'disk.read.bytes',
'disk.read.bytes.rate',
'disk.read.requests',
'disk.read.requests.rate',
'disk.usage',
'disk.write.bytes',
'disk.write.bytes.rate',
'disk.write.requests',
'disk.write.requests.rate',
}
if openstack_queens_or_older:
expected_metric_names |= {
'cpu_l3_cache',
'disk.allocation',
'disk.capacity',
'disk.device.allocation',
'disk.device.capacity',
'disk.device.iops',
'disk.device.latency',
'disk.device.read.latency',
'disk.device.usage',
'disk.device.write.latency',
'disk.iops',
'disk.latency',
'disk.read.bytes',
'disk.read.bytes.rate',
'disk.read.requests',
'disk.read.requests.rate',
'disk.usage',
'disk.write.bytes',
'disk.write.bytes.rate',
'disk.write.requests',
'disk.write.requests.rate',
'memory.bandwidth.local',
'memory.bandwidth.total',
'memory.resident',
'memory.swap.in',
'memory.swap.out',
'network.incoming.packets.drop',
'network.incoming.packets.error',
'network.outgoing.packets.drop',
'network.outgoing.packets.error',
'perf.cache.misses',
'perf.cache.references',
'perf.cpu.cycles',
'perf.instructions',
}
return expected_metric_names

View File

@@ -0,0 +1,51 @@
# Copyright 2021 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 setting up Ceph Dashboard."""
import logging
import zaza.model
import zaza.openstack.utilities.openstack
def check_dashboard_cert(model_name=None):
"""Wait for Dashboard to be ready.
:param model_name: Name of model to query.
:type model_name: str
"""
logging.info("Check dashbaord Waiting for cacert")
zaza.openstack.utilities.openstack.block_until_ca_exists(
'ceph-dashboard',
'CERTIFICATE',
model_name=model_name)
zaza.model.block_until_all_units_idle(model_name=model_name)
def set_grafana_url(model_name=None):
"""Set the url for the grafana api.
:param model_name: Name of model to query.
:type model_name: str
"""
try:
unit = zaza.model.get_units('grafana')[0]
except KeyError:
return
zaza.model.set_application_config(
'ceph-dashboard',
{
'grafana-api-url': "https://{}:3000".format(
unit.public_address)})

View File

@@ -15,12 +15,15 @@
"""Encapsulating `ceph-dashboard` testing."""
import collections
import os
import json
import logging
import requests
import tenacity
import uuid
import zaza
import zaza.openstack.charm_tests.test_utils as test_utils
import zaza.utilities.deployment_env as deployment_env
import zaza.openstack.utilities.openstack as openstack_utils
class CephDashboardTest(test_utils.BaseCharmTest):
@@ -34,33 +37,85 @@ class CephDashboardTest(test_utils.BaseCharmTest):
"""Run class setup for running ceph dashboard tests."""
super().setUpClass()
cls.application_name = 'ceph-dashboard'
cls.local_ca_cert = cls.collect_ca()
cls.local_ca_cert = openstack_utils.get_remote_ca_cert_file(
cls.application_name)
@classmethod
def collect_ca(cls):
"""Collect CA from ceph-dashboard unit."""
local_ca_cert = os.path.join(
deployment_env.get_tmpdir(),
os.path.basename(cls.REMOTE_CERT_FILE))
if not os.path.isfile(local_ca_cert):
units = zaza.model.get_units(cls.application_name)
zaza.model.scp_from_unit(
units[0].entity_id,
cls.REMOTE_CERT_FILE,
local_ca_cert)
return local_ca_cert
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1,
min=5, max=10),
retry=tenacity.retry_if_exception_type(
requests.exceptions.ConnectionError),
reraise=True)
def _run_request_get(self, url, verify, allow_redirects):
"""Run a GET request against `url` with tenacity retries.
:param url: url to access
:type url: str
:param verify: Path to a CA_BUNDLE file or directory with certificates
of trusted CAs or False to ignore verifying the SSL
certificate.
:type verify: Union[str, bool]
:param allow_redirects: Set to True if redirect following is allowed.
:type allow_redirects: bool
:returns: Request response
:rtype: requests.models.Response
"""
return requests.get(
url,
verify=verify,
allow_redirects=allow_redirects)
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1,
min=5, max=10),
retry=tenacity.retry_if_exception_type(
requests.exceptions.ConnectionError),
reraise=True)
def _run_request_post(self, url, verify, data, headers):
"""Run a POST request against `url` with tenacity retries.
:param url: url to access
:type url: str
:param verify: Path to a CA_BUNDLE file or directory with certificates
of trusted CAs or False to ignore verifying the SSL
certificate.
:type verify: Union[str, bool]
:param data: Data to post to url
:type data: str
:param headers: Headers to set when posting
:type headers: dict
:returns: Request response
:rtype: requests.models.Response
"""
return requests.post(
url,
data=data,
headers=headers,
verify=verify)
def get_master_dashboard_url(self):
"""Get the url of the dashboard servicing requests.
Only one unit serves requests at any one time, the other units
redirect to that unit.
:returns: URL of dashboard on unit
:rtype: Union[str, None]
"""
units = zaza.model.get_units(self.application_name)
for unit in units:
r = self._run_request_get(
'https://{}:8443'.format(unit.public_address),
verify=self.local_ca_cert,
allow_redirects=False)
if r.status_code == requests.codes.ok:
return 'https://{}:8443'.format(unit.public_address)
def test_dashboard_units(self):
"""Check dashboard units are configured correctly."""
# XXX: Switch to using CA for verification when
# https://bugs.launchpad.net/cloud-archive/+bug/1933410
# is fix released.
# verify = self.local_ca_cert
verify = False
verify = self.local_ca_cert
units = zaza.model.get_units(self.application_name)
rcs = collections.defaultdict(list)
for unit in units:
r = requests.get(
r = self._run_request_get(
'https://{}:8443'.format(unit.public_address),
verify=verify,
allow_redirects=False)
@@ -86,12 +141,70 @@ class CephDashboardTest(test_utils.BaseCharmTest):
'role': role})
return action
def get_random_username(self):
"""Generate a username to use in tests.
:returns: Username
:rtype: str
"""
return "zazauser-{}".format(uuid.uuid1())
def test_create_user(self):
"""Test create user action."""
test_user = 'marvin'
test_user = self.get_random_username()
action = self.create_user(test_user)
self.assertEqual(action.status, "completed")
self.assertTrue(action.data['results']['password'])
action = self.create_user(test_user)
# Action should fail as the user already exists
self.assertEqual(action.status, "failed")
def access_dashboard(self, dashboard_url):
"""Test logging via a dashboard url.
:param dashboard_url: Base url to use to login to
:type dashboard_url: str
"""
user = self.get_random_username()
action = self.create_user(username=user)
self.assertEqual(action.status, "completed")
password = action.data['results']['password']
path = "api/auth"
headers = {
'Content-type': 'application/json',
'Accept': 'application/vnd.ceph.api.v1.0'}
payload = {"username": user, "password": password}
verify = self.local_ca_cert
r = self._run_request_post(
"{}/{}".format(dashboard_url, path),
verify=verify,
data=json.dumps(payload),
headers=headers)
self.assertEqual(r.status_code, requests.codes.created)
def test_access_dashboard(self):
"""Test logging in to the dashboard."""
self.access_dashboard(self.get_master_dashboard_url())
def test_ceph_keys(self):
"""Check that ceph services are properly registered."""
status = zaza.model.get_status()
applications = status.applications.keys()
dashboard_keys = []
ceph_keys = []
if 'ceph-radosgw' in applications:
dashboard_keys.extend(['RGW_API_ACCESS_KEY', 'RGW_API_SECRET_KEY'])
if 'grafana' in applications:
dashboard_keys.append('GRAFANA_API_URL')
if 'prometheus' in applications:
dashboard_keys.append('PROMETHEUS_API_HOST')
ceph_keys.extend(
['config/mgr/mgr/dashboard/{}'.format(k) for k in dashboard_keys])
if 'ceph-iscsi' in applications:
ceph_keys.append('mgr/dashboard/_iscsi_config')
for key in ceph_keys:
logging.info("Checking key {} exists".format(key))
check_out = zaza.model.run_on_leader(
'ceph-dashboard',
'ceph config-key exists {}'.format(key))
self.assertEqual(check_out['Code'], '0')

View File

@@ -0,0 +1,15 @@
# Copyright 2021 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 testing cinder-lvm."""

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# Copyright 2019 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 cinder-lvm testing."""
import logging
import uuid
import zaza.model
import zaza.openstack.charm_tests.test_utils as test_utils
import zaza.openstack.utilities.openstack as openstack_utils
class CinderLVMTest(test_utils.OpenStackBaseTest):
"""Encapsulate cinder-lvm tests."""
@classmethod
def setUpClass(cls):
"""Run class setup for running tests."""
super(CinderLVMTest, cls).setUpClass(application_name='cinder-lvm')
cls.model_name = zaza.model.get_juju_model()
cls.cinder_client = openstack_utils.get_cinder_session_client(
cls.keystone_session)
cls.block_device = openstack_utils.get_application_config_option(
'cinder-lvm', 'block-device', model_name=cls.model_name)
@classmethod
def tearDown(cls):
"""Remove test resources."""
volumes = cls.cinder_client.volumes
for volume in volumes.list():
if volume.name.startswith('zaza'):
try:
volume.detach()
volumes.delete(volume)
except Exception:
pass
def test_cinder_config(self):
"""Test that configuration options match our expectations."""
logging.info('cinder-lvm')
expected_contents = {
'LVM-zaza-lvm': {
'volume_clear': ['zero'],
'volumes_dir': ['/var/lib/cinder/volumes'],
'volume_name_template': ['volume-%s'],
'volume_clear_size': ['0'],
'volume_driver': ['cinder.volume.drivers.lvm.LVMVolumeDriver'],
}}
zaza.model.run_on_leader(
'cinder',
'sudo cp /etc/cinder/cinder.conf /tmp/')
zaza.model.block_until_oslo_config_entries_match(
'cinder',
'/tmp/cinder.conf',
expected_contents,
timeout=10)
def _create_volume(self):
"""Create a volume via the LVM backend."""
test_vol_name = "zaza{}".format(uuid.uuid1().fields[0])
vol_new = self.cinder_client.volumes.create(
name=test_vol_name,
size='1')
openstack_utils.resource_reaches_status(
self.cinder_client.volumes,
vol_new.id,
wait_iteration_max_time=12000,
stop_after_attempt=5,
expected_status='available',
msg='Volume status wait')
return self.cinder_client.volumes.find(name=test_vol_name)
def test_create_volume(self):
"""Test creating a volume with basic configuration."""
test_vol = self._create_volume()
self.assertTrue(test_vol)
host = getattr(test_vol, 'os-vol-host-attr:host').split('#')[0]
self.assertIn('@LVM', host)
def test_volume_overwrite(self):
"""Test creating a volume by overwriting one on a loop device."""
with self.config_change({'overwrite': 'false',
'block-device': self.block_device},
{'overwrite': 'true',
'block-device': '/tmp/vol|2G'}):
self._create_volume()
def test_device_none(self):
"""Test creating a volume in a dummy device (set as 'none')."""
with self.config_change({'block-device': self.block_device},
{'block-device': 'none'}):
self._create_volume()
def test_remove_missing_volume(self):
"""Test creating a volume after remove missing ones in a group."""
with self.config_change({'remove-missing': 'false'},
{'remove-missing': 'true'}):
self._create_volume()
def test_remove_missing_force(self):
"""Test creating a volume by forcefully removing missing ones."""
with self.config_change({'remove-missing-force': 'false'},
{'remove-missing-force': 'true'}):
self._create_volume()

View File

@@ -0,0 +1,15 @@
# Copyright 2021 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 testing cinder-netapp."""

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python3
# Copyright 2021 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 cinder-netapp testing."""
import uuid
import zaza.model
import zaza.openstack.charm_tests.test_utils as test_utils
import zaza.openstack.utilities.openstack as openstack_utils
class CinderNetAppTest(test_utils.OpenStackBaseTest):
"""Encapsulate netapp tests."""
@classmethod
def setUpClass(cls):
"""Run class setup for running tests."""
super(CinderNetAppTest, cls).setUpClass()
cls.keystone_session = openstack_utils.get_overcloud_keystone_session()
cls.model_name = zaza.model.get_juju_model()
cls.cinder_client = openstack_utils.get_cinder_session_client(
cls.keystone_session)
def test_cinder_config(self):
"""Test that configuration options match our expectations."""
expected_contents = {
'cinder-netapp': {
'netapp_storage_family': ['ontap_cluster'],
'netapp_storage_protocol': ['iscsi'],
'volume_backend_name': ['cinder_netapp'],
'volume_driver':
['cinder.volume.drivers.netapp.common.NetAppDriver'],
}}
zaza.model.run_on_leader(
'cinder',
'sudo cp /etc/cinder/cinder.conf /tmp/')
zaza.model.block_until_oslo_config_entries_match(
'cinder',
'/tmp/cinder.conf',
expected_contents,
timeout=2)
def test_create_volume(self):
"""Test creating a volume with basic configuration."""
test_vol_name = "zaza{}".format(uuid.uuid1().fields[0])
vol_new = self.cinder_client.volumes.create(
name=test_vol_name,
size='1')
try:
openstack_utils.resource_reaches_status(
self.cinder_client.volumes,
vol_new.id,
wait_iteration_max_time=12000,
stop_after_attempt=5,
expected_status='available',
msg='Volume status wait')
test_vol = self.cinder_client.volumes.find(name=test_vol_name)
self.assertEqual(
getattr(test_vol, 'os-vol-host-attr:host').split('#')[0],
'cinder@cinder-netapp')
finally:
self.cinder_client.volumes.delete(vol_new)

View File

@@ -25,6 +25,7 @@ import designateclient.v1.servers as servers
import zaza.model
import zaza.utilities.juju as juju_utils
import zaza.openstack.charm_tests.test_utils as test_utils
import zaza.openstack.utilities.generic as generic_utils
import zaza.openstack.utilities.openstack as openstack_utils
import zaza.openstack.charm_tests.designate.utils as designate_utils
import zaza.charm_lifecycle.utils as lifecycle_utils
@@ -33,6 +34,8 @@ import zaza.charm_lifecycle.utils as lifecycle_utils
class BaseDesignateTest(test_utils.OpenStackBaseTest):
"""Base for Designate charm tests."""
DESIGNATE_CONF = '/etc/designate/designate.conf'
@classmethod
def setUpClass(cls, application_name=None, model_alias=None):
"""Run class setup for running Designate charm operation tests."""
@@ -88,6 +91,14 @@ class BaseDesignateTest(test_utils.OpenStackBaseTest):
cls.server_create = cls.designate.servers.create
cls.server_delete = cls.designate.servers.delete
@tenacity.retry(
retry=tenacity.retry_if_result(lambda ret: ret is not None),
# sleep for 2mins to allow 1min cron job to run...
wait=tenacity.wait_fixed(120),
stop=tenacity.stop_after_attempt(2))
def _retry_check_commands_on_units(self, cmds, units):
return generic_utils.check_commands_on_units(cmds, units)
class DesignateAPITests(BaseDesignateTest):
"""Tests interact with designate api."""
@@ -119,6 +130,41 @@ class DesignateAPITests(BaseDesignateTest):
self.server_delete(server_id)
return wait()
def test_300_default_soa_config_options(self):
"""Configure default SOA options."""
test_domain = "test_300_example.com."
DEFAULT_TTL = 60
alternate_config = {'default-soa-minimum': 600,
'default-ttl': DEFAULT_TTL,
'default-soa-refresh-min': 300,
'default-soa-refresh-max': 400,
'default-soa-retry': 30}
with self.config_change({}, alternate_config, "designate",
reset_to_charm_default=True):
for key, value in alternate_config.items():
expected = "\n%s = %s\n" % (key.replace('-', '_'), value)
zaza.model.block_until_file_has_contents(self.application_name,
self.DESIGNATE_CONF,
expected)
logging.debug('Creating domain %s' % test_domain)
domain = domains.Domain(name=test_domain,
email="fred@amuletexample.com")
if self.post_xenial_queens:
new_domain = self.domain_create(
name=domain.name, email=domain.email)
domain_id = new_domain['id']
else:
new_domain = self.domain_create(domain)
domain_id = new_domain.id
self.assertIsNotNone(new_domain)
self.assertEqual(new_domain['ttl'], DEFAULT_TTL)
logging.debug('Tidy up delete test record %s' % domain_id)
self._wait_on_domain_gone(domain_id)
logging.debug('Done with deletion of domain %s' % domain_id)
def test_400_server_creation(self):
"""Simple api calls to create a server."""
# Designate does not allow the last server to be deleted so ensure
@@ -257,7 +303,26 @@ class DesignateCharmTests(BaseDesignateTest):
logging.info("Testing pause resume")
class DesignateTests(DesignateAPITests, DesignateCharmTests):
class DesignateMonitoringTests(BaseDesignateTest):
"""Designate charm monitoring tests."""
def test_nrpe_configured(self):
"""Confirm that the NRPE service check files are created."""
units = zaza.model.get_units(self.application_name)
cmds = []
for check_name in self.designate_svcs:
cmds.append(
'egrep -oh /usr/local.* /etc/nagios/nrpe.d/'
'check_{}.cfg'.format(check_name)
)
ret = self._retry_check_commands_on_units(cmds, units)
if ret:
logging.info(ret)
self.assertIsNone(ret, msg=ret)
class DesignateTests(DesignateAPITests, DesignateCharmTests,
DesignateMonitoringTests):
"""Collection of all Designate test classes."""
pass

View File

@@ -23,6 +23,7 @@ from manilaclient import client as manilaclient
import zaza.model
import zaza.openstack.configure.guest as guest
import zaza.openstack.utilities.generic as generic_utils
import zaza.openstack.utilities.openstack as openstack_utils
import zaza.openstack.charm_tests.test_utils as test_utils
import zaza.openstack.charm_tests.nova.utils as nova_utils
@@ -85,6 +86,27 @@ class ManilaTests(test_utils.OpenStackBaseTest):
def _list_shares(self):
return self.manila_client.shares.list()
def test_902_nrpe_service_checks(self):
"""Confirm that the NRPE service check files are created."""
units = zaza.model.get_units('manila')
services = ['apache2', 'haproxy', 'manila-scheduler', 'manila-data']
cmds = []
for check_name in services:
cmds.append(
'egrep -oh /usr/local.* /etc/nagios/nrpe.d/'
'check_{}.cfg'.format(check_name)
)
for attempt in tenacity.Retrying(
wait=tenacity.wait_fixed(20),
stop=tenacity.stop_after_attempt(2),
reraise=True
):
with attempt:
ret = generic_utils.check_commands_on_units(cmds, units)
self.assertIsNone(ret, msg=ret)
class ManilaBaseTest(test_utils.OpenStackBaseTest):
"""Encapsulate a Manila basic functionality test."""

View File

@@ -40,15 +40,19 @@ class ManilaGaneshaTests(manila_tests.ManilaBaseTest):
def _restart_share_instance(self):
logging.info('Restarting manila-share and nfs-ganesha')
# It would be better for thie to derive the application name,
# manila-ganesha-az1, from deployed instances fo the manila-ganesha
# manila-ganesha-az1, from deployed instances of the manila-ganesha
# charm; however, that functionality isn't present yet in zaza, so
# this is hard coded to the application name used in that charm's
# test bundles.
for unit in zaza.model.get_units('manila-ganesha-az1'):
# While we really only need to run this on the machine hosting
# nfs-ganesha and manila-share, running it everywhere isn't
# harmful. Pacemaker handles restarting the services
zaza.model.run_on_unit(
unit.entity_id,
"systemctl stop manila-share nfs-ganesha")
# this is a best-guestimate arrived at by looking for applications
# with the word 'ganesha' in their names.
ganeshas = [
app for app in zaza.model.sync_deployed()
if 'ganesha' in app and 'mysql' not in app]
for ganesha in ganeshas:
for unit in zaza.model.get_units(ganesha):
# While we really only need to run this on the machine hosting
# nfs-ganesha and manila-share, running it everywhere isn't
# harmful. Pacemaker handles restarting the services
zaza.model.run_on_unit(
unit.entity_id,
"systemctl stop manila-share nfs-ganesha")
return True

View File

@@ -203,7 +203,7 @@ class MySQLCommonTests(MySQLBaseTest):
logging.info("Wait till model is idle ...")
zaza.model.block_until_all_units_idle()
# If there are any blocekd mysql routers restart them.
# If there are any blocked mysql routers restart them.
self.restart_blocked_mysql_routers()
assert not self.get_blocked_mysql_routers(), (
"Should no longer be blocked mysql-router units")

View File

@@ -44,6 +44,13 @@ OVERCLOUD_NETWORK_CONFIG = {
"subnetpool_prefix": "192.168.0.0/16",
}
OVERCLOUD_PROVIDER_VLAN_NETWORK_CONFIG = {
"provider_vlan_net_name": "provider_vlan",
"provider_vlan_subnet_name": "provider_vlan_subnet",
"provider_vlan_cidr": "10.42.33.0/24",
"provider_vlan_id": "2933",
}
# The undercloud network configuration settings are substrate specific to
# the environment where the tests are being executed. These settings may be
# overridden by environment variables. See the doc string documentation for
@@ -82,7 +89,7 @@ def basic_overcloud_network(limit_gws=None):
# Get keystone session
keystone_session = openstack_utils.get_overcloud_keystone_session()
# Get optional use_juju_wait for netw ork option
# Get optional use_juju_wait for network option
options = (lifecycle_utils
.get_charm_config(fatal=False)
.get('configure_options', {}))
@@ -110,10 +117,31 @@ def basic_overcloud_network(limit_gws=None):
' charm network configuration.'
.format(provider_type))
# Confugre the overcloud network
# Configure the overcloud network
network.setup_sdn(network_config, keystone_session=keystone_session)
def vlan_provider_overcloud_network():
"""Run setup to create a VLAN provider network."""
cli_utils.setup_logging()
# Get network configuration settings
network_config = {}
# Declared overcloud settings
network_config.update(OVERCLOUD_NETWORK_CONFIG)
# Declared provider vlan overcloud settings
network_config.update(OVERCLOUD_PROVIDER_VLAN_NETWORK_CONFIG)
# Environment specific settings
network_config.update(generic_utils.get_undercloud_env_vars())
# Get keystone session
keystone_session = openstack_utils.get_overcloud_keystone_session()
# Configure the overcloud network
network.setup_sdn_provider_vlan(network_config,
keystone_session=keystone_session)
# Configure function to get one gateway with external network
overcloud_network_one_gw = functools.partial(
basic_overcloud_network,

View File

@@ -26,6 +26,7 @@ import tenacity
from neutronclient.common import exceptions as neutronexceptions
import yaml
import zaza
import zaza.openstack.charm_tests.nova.utils as nova_utils
import zaza.openstack.charm_tests.test_utils as test_utils
@@ -248,6 +249,135 @@ class NeutronGatewayTest(NeutronPluginApiSharedTests):
return services
class NeutronGatewayShowActionsTest(test_utils.OpenStackBaseTest):
"""Test "show" actions of Neutron Gateway Charm.
actions:
* show-routers
* show-dhcp-networks
* show-loadbalancers
"""
SKIP_LBAAS_TESTS = True
@classmethod
def setUpClass(cls, application_name='neutron-gateway', model_alias=None):
"""Run class setup for running Neutron Gateway tests."""
super(NeutronGatewayShowActionsTest, cls).setUpClass(
application_name, model_alias)
# set up clients
cls.neutron_client = (
openstack_utils.get_neutron_session_client(cls.keystone_session))
# Loadbalancer tests not supported on Train and above and on
# releases Mitaka and below
current = openstack_utils.get_os_release()
bionic_train = openstack_utils.get_os_release('bionic_train')
xenial_mitaka = openstack_utils.get_os_release('xenial_mitaka')
cls.SKIP_LBAAS_TESTS = not (xenial_mitaka > current < bionic_train)
def _assert_result_match(self, action_result, resource_list,
resource_name):
"""Assert that action_result contains same data as resource_list."""
# make sure that action completed successfully
if action_result.status != 'completed':
self.fail('Juju Action failed: {}'.format(action_result.message))
# extract data from juju action
action_data = action_result.data.get('results', {}).get(resource_name)
resources_from_action = yaml.load(action_data)
# pull resource IDs from expected resource list and juju action data
expected_resource_ids = {resource['id'] for resource in resource_list}
result_resource_ids = resources_from_action.keys()
# assert that juju action returned expected resources
self.assertEqual(result_resource_ids, expected_resource_ids)
def test_show_routers(self):
"""Test that show-routers action reports correct neutron routers."""
# fetch neutron routers using neutron client
ngw_unit = zaza.model.get_units(self.application_name,
model_name=self.model_name)[0]
routers_from_client = self.neutron_client.list_routers().get(
'routers', [])
if not routers_from_client:
self.fail('At least one router must be configured for this test '
'to pass.')
# fetch neutron routers using juju-action
result = zaza.model.run_action(ngw_unit.entity_id,
'show-routers',
model_name=self.model_name)
# assert that data from neutron client match data from juju action
self._assert_result_match(result, routers_from_client, 'router-list')
def test_show_dhcp_networks(self):
"""Test that show-dhcp-networks reports correct DHCP networks."""
# fetch DHCP networks using neutron client
ngw_unit = zaza.model.get_units(self.application_name,
model_name=self.model_name)[0]
networks_from_client = self.neutron_client.list_networks().get(
'networks', [])
if not networks_from_client:
self.fail('At least one network must be configured for this test '
'to pass.')
# fetch DHCP networks using juju-action
result = zaza.model.run_action(ngw_unit.entity_id,
'show-dhcp-networks',
model_name=self.model_name)
# assert that data from neutron client match data from juju action
self._assert_result_match(result, networks_from_client,
'dhcp-networks')
def test_show_load_balancers(self):
"""Test that show-loadbalancers reports correct loadbalancers."""
if self.SKIP_LBAAS_TESTS:
self.skipTest('LBaasV2 is not supported in this version.')
loadbalancer_id = None
try:
# create LBaasV2 for the purpose of this test
lbaas_name = 'test_lbaas'
subnet_list = self.neutron_client.list_subnets(
name='private_subnet').get('subnets', [])
if not subnet_list:
raise RuntimeError('Expected subnet "private_subnet" is not '
'configured.')
subnet = subnet_list[0]
loadbalancer_data = {'loadbalancer': {'name': lbaas_name,
'vip_subnet_id': subnet['id']
}
}
loadbalancer = self.neutron_client.create_loadbalancer(
body=loadbalancer_data)
loadbalancer_id = loadbalancer['loadbalancer']['id']
# test that client and action report same data
ngw_unit = zaza.model.get_units(self.application_name,
model_name=self.model_name)[0]
lbaas_from_client = self.neutron_client.list_loadbalancers().get(
'loadbalancers', [])
result = zaza.model.run_action(ngw_unit.entity_id,
'show-load-balancers',
model_name=self.model_name)
self._assert_result_match(result, lbaas_from_client,
'load-balancers')
finally:
if loadbalancer_id:
self.neutron_client.delete_loadbalancer(loadbalancer_id)
class NeutronCreateNetworkTest(test_utils.OpenStackBaseTest):
"""Test creating a Neutron network through the API.

View File

@@ -66,7 +66,7 @@ def _login(dashboard_url, domain, username, password, cafile=None):
# start session, get csrftoken
client = requests.session()
client.get(auth_url, verify=cafile)
client.get(auth_url, verify=cafile, timeout=30)
if 'csrftoken' in client.cookies:
csrftoken = client.cookies['csrftoken']
else:
@@ -495,17 +495,17 @@ class OpenStackDashboardPolicydTests(policyd.BasePolicydSpecialization,
zaza_model.get_lead_unit_name(self.application_name))
logging.info("Dashboard is at {}".format(unit.public_address))
overcloud_auth = openstack_utils.get_overcloud_auth()
password = overcloud_auth['OS_PASSWORD'],
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'],
domain = 'admin_domain'
username = 'admin'
client, response = _login(
self.get_horizon_url(), domain, username, password)
self.get_horizon_url(), domain, username, password,
cafile=self.cacert)
# now attempt to get the domains page
_url = self.url.format(unit.public_address)
logging.info("URL is {}".format(_url))
result = client.get(_url)
if result.status_code == 403:
raise policyd.PolicydOperationFailedException("Not authenticated")

View File

@@ -19,16 +19,21 @@
import logging
import unittest
import zaza.model
import zaza.global_options
from zaza.openstack.utilities import (
cli as cli_utils,
upgrade_utils as upgrade_utils,
openstack as openstack_utils,
openstack_upgrade as openstack_upgrade,
exceptions,
generic,
)
from zaza.openstack.charm_tests.nova.tests import LTSGuestCreateTest
class OpenStackUpgradeVMLaunchBase(object):
class OpenStackUpgradeVMLaunchBase(unittest.TestCase):
"""A base class to peform a simple validation on the cloud.
This wraps an OpenStack upgrade with a VM launch before and after the
@@ -44,7 +49,6 @@ class OpenStackUpgradeVMLaunchBase(object):
@classmethod
def setUpClass(cls):
"""Run setup for OpenStack Upgrades."""
print("Running OpenStackUpgradeMixin setUpClass")
super().setUpClass()
cls.lts = LTSGuestCreateTest()
cls.lts.setUpClass()
@@ -71,7 +75,6 @@ class WaitForMySQL(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Set up class."""
print("Running OpenstackUpgradeTests setUpClass")
super().setUpClass()
cli_utils.setup_logging()
@@ -88,7 +91,6 @@ class OpenStackUpgradeTestsFocalUssuri(OpenStackUpgradeVMLaunchBase):
@classmethod
def setUpClass(cls):
"""Run setup for OpenStack Upgrades."""
print("Running OpenstackUpgradeTests setUpClass")
super().setUpClass()
cli_utils.setup_logging()
@@ -97,21 +99,24 @@ class OpenStackUpgradeTestsFocalUssuri(OpenStackUpgradeVMLaunchBase):
openstack_upgrade.run_upgrade_tests("cloud:focal-victoria")
class OpenStackUpgradeTests(OpenStackUpgradeVMLaunchBase):
class OpenStackUpgradeTestsByOption(OpenStackUpgradeVMLaunchBase):
"""A Principal Class to encapsulate OpenStack Upgrade Tests.
A generic Test class that can discover which Ubuntu version and OpenStack
version to upgrade from.
A generic Test class that uses the options in the tests.yaml to use a charm
to detect the Ubuntu and OpenStack versions and then workout what to
upgrade to.
TODO: Not used at present. Use the declarative tests directly that choose
the version to upgrade to. The functions that this class depends on need a
bit more work regarding how the determine which version to go to.
tests_options:
openstack-upgrade:
detect-using-charm: keystone
This will use the octavia application, detect the ubuntu version and then
read the config to discover the current OpenStack version.
"""
@classmethod
def setUpClass(cls):
"""Run setup for OpenStack Upgrades."""
print("Running OpenstackUpgradeTests setUpClass")
super().setUpClass()
cli_utils.setup_logging()
@@ -122,16 +127,31 @@ class OpenStackUpgradeTests(OpenStackUpgradeVMLaunchBase):
determine which ubuntu version to work from. Don't use until we can
make it better.
"""
# TODO: work out the most recent Ubuntu version; we assume this is the
# version that OpenStack is running on.
ubuntu_version = "focal"
logging.info("Getting all principle applications ...")
principle_services = upgrade_utils.get_all_principal_applications()
# get the tests_options / openstack-upgrade.detect-using-charm so that
# the ubuntu version and OpenStack version can be detected.
try:
detect_charm = (
zaza.global_options.get_options()
.openstack_upgrade.detect_using_charm)
except KeyError:
raise exceptions.InvalidTestConfig(
"Missing tests_options.openstack-upgrade.detect-using-charm "
"config.")
unit = zaza.model.get_lead_unit(detect_charm)
ubuntu_version = generic.get_series(unit)
logging.info("Current version detected from {} is {}"
.format(detect_charm, ubuntu_version))
logging.info(
"Getting OpenStack vesions from principal applications ...")
"Getting OpenStack version from %s ..." % detect_charm)
current_versions = openstack_utils.get_current_os_versions(
principle_services)
logging.info("current versions: %s" % current_versions)
[detect_charm])
if not current_versions:
raise exceptions.ApplicationNotFound(
"No version found for {}?".format(detect_charm))
logging.info("current version: %s" % current_versions[detect_charm])
# Find the lowest value openstack release across all services and make
# sure all servcies are upgraded to one release higher than the lowest
from_version = upgrade_utils.get_lowest_openstack_version(
@@ -140,7 +160,6 @@ class OpenStackUpgradeTests(OpenStackUpgradeVMLaunchBase):
to_version = upgrade_utils.determine_next_openstack_release(
from_version)[1]
logging.info("to version: %s" % to_version)
# TODO: need to determine the ubuntu base verion that is being upgraded
target_source = upgrade_utils.determine_new_source(
ubuntu_version, from_version, to_version, single_increment=True)
logging.info("target source: %s" % target_source)

View File

@@ -168,6 +168,28 @@ class ChassisCharmOperationTest(BaseCharmOperationTest):
'{}: "{}" no longer present'
.format(unit.entity_id, expected_key))
def test_wrong_bridge_config(self):
"""Confirm that ovn-chassis units block with wrong bridge config."""
stored_target_deploy_status = self.test_config.get(
'target_deploy_status', {})
new_target_deploy_status = stored_target_deploy_status.copy()
new_target_deploy_status[self.application_name] = {
'ovn-chassis': 'blocked',
}
if 'target_deploy_status' in self.test_config:
self.test_config['target_deploy_status'].update(
new_target_deploy_status)
else:
self.test_config['target_deploy_status'] = new_target_deploy_status
with self.config_change(
{'bridge-interface-mappings': ''},
{'bridge-interface-mappings': 'incorrect'}):
logging.info('Charm went into blocked state as expected, restore '
'configuration')
self.test_config[
'target_deploy_status'] = stored_target_deploy_status
class OVSOVNMigrationTest(test_utils.BaseCharmTest):
"""OVS to OVN migration tests."""

View File

@@ -14,315 +14,9 @@
"""Code for configuring and initializing tempest."""
import jinja2
import urllib.parse
import os
import subprocess
import logging
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.tempest.utils as tempest_utils
import zaza.openstack.charm_tests.glance.setup as glance_setup
SETUP_ENV_VARS = {
'neutron': ['TEST_GATEWAY', 'TEST_CIDR_EXT', 'TEST_FIP_RANGE',
'TEST_NAME_SERVER', 'TEST_CIDR_PRIV'],
'swift': ['TEST_SWIFT_IP'],
}
IGNORABLE_VARS = ['TEST_CIDR_PRIV']
TEMPEST_FLAVOR_NAME = 'm1.tempest'
TEMPEST_ALT_FLAVOR_NAME = 'm2.tempest'
TEMPEST_SVC_LIST = ['ceilometer', 'cinder', 'glance', 'heat', 'horizon',
'ironic', 'neutron', 'nova', 'octavia', 'sahara', 'swift',
'trove', 'zaqar']
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_octavia_config(ctxt):
"""Add octavia config to context.
:param ctxt: Context dictionary
:type ctxt: dict
:returns: None
:rtype: None
:raises: subprocess.CalledProcessError
"""
subprocess.check_call([
'curl',
"{}:80/swift/v1/fixtures/test_server.bin".format(
ctxt['test_swift_ip']),
'-o', "{}/test_server.bin".format(ctxt['workspace_path'])
])
subprocess.check_call([
'chmod', '+x',
"{}/test_server.bin".format(ctxt['workspace_path'])
])
def get_service_list(keystone_session):
"""Retrieve list of services from keystone.
: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)
return [s.name for s in keystone_client.services.list() if s.enabled]
def add_environment_var_config(ctxt, services):
"""Add environment variable config to context.
:param ctxt: Context dictionary
:type ctxt: dict
:returns: None
:rtype: None
"""
deploy_env = deployment_env.get_deployment_context()
missing_vars = []
for svc, env_vars in SETUP_ENV_VARS.items():
if svc in services:
for var in env_vars:
value = deploy_env.get(var)
if value:
ctxt[var.lower()] = value
else:
if var not in IGNORABLE_VARS:
missing_vars.append(var)
if missing_vars:
raise ValueError(
("Environment variables [{}] must all be set to run this"
" test").format(', '.join(missing_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(workspace_path):
"""Generate the tempest config context.
:returns: Context dictionary
:rtype: dict
"""
keystone_session = openstack_utils.get_overcloud_keystone_session()
ctxt = {}
ctxt['workspace_path'] = workspace_path
ctxt_funcs = {
'nova': add_nova_config,
'neutron': add_neutron_config,
'glance': add_glance_config,
'cinder': add_cinder_config,
'keystone': add_keystone_config}
ctxt['enabled_services'] = get_service_list(keystone_session)
if set(['cinderv2', 'cinderv3']) \
.intersection(set(ctxt['enabled_services'])):
ctxt['enabled_services'].append('cinder')
ctxt['disabled_services'] = list(
set(TEMPEST_SVC_LIST) - set(ctxt['enabled_services']))
add_application_ips(ctxt)
for svc_name, ctxt_func in ctxt_funcs.items():
if svc_name in ctxt['enabled_services']:
ctxt_func(ctxt, keystone_session)
add_environment_var_config(ctxt, ctxt['enabled_services'])
add_auth_config(ctxt)
if 'octavia' in ctxt['enabled_services']:
add_octavia_config(ctxt)
return ctxt
def render_tempest_config(target_file, ctxt, template_name):
"""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_name: Name of template file
:type template_name: str
:returns: None
:rtype: None
"""
jenv = jinja2.Environment(loader=jinja2.PackageLoader(
'zaza.openstack',
'charm_tests/tempest/templates'))
template = jenv.get_template(template_name)
with open(target_file, 'w') as f:
f.write(template.render(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
"""
workspace_name, workspace_path = tempest_utils.get_workspace()
tempest_utils.destroy_workspace(workspace_name, workspace_path)
tempest_utils.init_workspace(workspace_path)
context = get_tempest_context(workspace_path)
render_tempest_config(
os.path.join(workspace_path, 'etc/tempest.conf'),
context,
tempest_template)
render_tempest_config(
os.path.join(workspace_path, 'etc/accounts.yaml'),
context,
accounts_template)
def render_tempest_config_keystone_v2():
@@ -331,7 +25,11 @@ def render_tempest_config_keystone_v2():
:returns: None
:rtype: None
"""
setup_tempest('tempest_v2.j2', 'accounts.j2')
logging.warning(
'The render_tempest_config_keystone_v2 config step is deprecated. '
'This is now directly done by the TempestTestWithKeystoneV2 test '
'class.')
tempest_utils.render_tempest_config_keystone_v2()
def render_tempest_config_keystone_v3():
@@ -340,4 +38,8 @@ def render_tempest_config_keystone_v3():
:returns: None
:rtype: None
"""
setup_tempest('tempest_v3.j2', 'accounts.j2')
logging.warning(
'The render_tempest_config_keystone_v3 config step is deprecated. '
'This is now directly done by the TempestTestWithKeystoneV3 test '
'class.')
tempest_utils.render_tempest_config_keystone_v3()

View File

@@ -14,6 +14,7 @@
"""Code for running tempest tests."""
import logging
import os
import subprocess
@@ -24,8 +25,8 @@ import zaza.openstack.charm_tests.tempest.utils as tempest_utils
import tempfile
class TempestTest():
"""Tempest test class."""
class TempestTestBase():
"""Tempest test base class."""
test_runner = zaza.charm_lifecycle.test.DIRECT
@@ -33,8 +34,13 @@ class TempestTest():
"""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), regex (list of regex's), and keep-workspace (bool).
valid test keys are:
- smoke (bool)
- include-list (list of tests)
- exclude-list (list of tests)
- regex (list of regex's)
- exclude-regex (list of regex's)
- keep-workspace (bool)
:returns: Status of tempest run
:rtype: bool
@@ -57,23 +63,23 @@ class TempestTest():
tempest_options.extend(
['--regex',
' '.join([reg for reg in config.get('regex')])])
if config.get('black-regex'):
if config.get('exclude-regex'):
tempest_options.extend(
['--black-regex',
' '.join([reg for reg in config.get('black-regex')])])
['--exclude-regex',
' '.join([reg for reg in config.get('exclude-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')))
if config.get('include-list'):
include_file = os.path.join(tmpdirname, 'include.cfg')
with open(include_file, 'w') as f:
f.write('\n'.join(config.get('include-list')))
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')))
tempest_options.extend(['--include-list', include_file])
if config.get('exclude-list'):
exclude_file = os.path.join(tmpdirname, 'exclude.cfg')
with open(exclude_file, 'w') as f:
f.write('\n'.join(config.get('exclude-list')))
f.write('\n')
tempest_options.extend(['--blacklist-file', black_file])
tempest_options.extend(['--exclude-list', exclude_file])
print(tempest_options)
try:
subprocess.check_call(tempest_options)
@@ -84,3 +90,54 @@ class TempestTest():
if not keep_workspace or keep_workspace is not True:
tempest_utils.destroy_workspace(workspace_name, workspace_path)
return result
class TempestTestWithKeystoneV2(TempestTestBase):
"""Tempest test class to validate an OpenStack setup with Keystone V2."""
def run(self):
"""Run tempest tests as specified in tests/tests.yaml.
See TempestTestBase.run() for the available test options.
:returns: Status of tempest run
:rtype: bool
"""
tempest_utils.render_tempest_config_keystone_v2()
return super().run()
class TempestTestWithKeystoneV3(TempestTestBase):
"""Tempest test class to validate an OpenStack setup with Keystone V2."""
def run(self):
"""Run tempest tests as specified in tests/tests.yaml.
See TempestTestBase.run() for the available test options.
:returns: Status of tempest run
:rtype: bool
"""
tempest_utils.render_tempest_config_keystone_v3()
return super().run()
class TempestTest(TempestTestBase):
"""Tempest test class.
Requires running one of the render_tempest_config_keystone_v? Zaza
configuration steps before.
"""
def run(self):
"""Run tempest tests as specified in tests/tests.yaml.
See TempestTestBase.run() for the available test options.
:returns: Status of tempest run
:rtype: bool
"""
logging.warning(
'The TempestTest test class is deprecated. Please use one of the '
'TempestTestWithKeystoneV? test classes instead.')
return super().run()

View File

@@ -14,12 +14,50 @@
"""Utility code for working with tempest workspaces."""
import jinja2
import os
from pathlib import Path
import shutil
import subprocess
import urllib.parse
import zaza.model as model
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
SETUP_ENV_VARS = {
'neutron': ['TEST_GATEWAY', 'TEST_CIDR_EXT', 'TEST_FIP_RANGE',
'TEST_NAME_SERVER', 'TEST_CIDR_PRIV'],
'swift': ['TEST_SWIFT_IP'],
}
IGNORABLE_VARS = ['TEST_CIDR_PRIV']
TEMPEST_FLAVOR_NAME = 'm1.tempest'
TEMPEST_ALT_FLAVOR_NAME = 'm2.tempest'
TEMPEST_SVC_LIST = ['ceilometer', 'cinder', 'glance', 'heat', 'horizon',
'ironic', 'neutron', 'nova', 'octavia', 'sahara', 'swift',
'trove', 'zaqar']
def render_tempest_config_keystone_v2():
"""Render tempest config for Keystone V2 API.
:returns: None
:rtype: None
"""
_setup_tempest('tempest_v2.j2', 'accounts.j2')
def render_tempest_config_keystone_v3():
"""Render tempest config for Keystone V3 API.
:returns: None
:rtype: None
"""
_setup_tempest('tempest_v3.j2', 'accounts.j2')
def get_workspace():
@@ -53,7 +91,7 @@ def destroy_workspace(workspace_name, workspace_path):
shutil.rmtree(workspace_path)
def init_workspace(workspace_path):
def _init_workspace(workspace_path):
"""Initialize tempest workspace.
:param workspace_path: directory path where workspace is stored
@@ -65,3 +103,289 @@ def init_workspace(workspace_path):
subprocess.check_call(['tempest', 'init', workspace_path])
except subprocess.CalledProcessError:
pass
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
"""
workspace_name, workspace_path = get_workspace()
destroy_workspace(workspace_name, workspace_path)
_init_workspace(workspace_path)
context = _get_tempest_context(workspace_path)
_render_tempest_config(
os.path.join(workspace_path, 'etc/tempest.conf'),
context,
tempest_template)
_render_tempest_config(
os.path.join(workspace_path, 'etc/accounts.yaml'),
context,
accounts_template)
def _get_tempest_context(workspace_path):
"""Generate the tempest config context.
:returns: Context dictionary
:rtype: dict
"""
keystone_session = openstack_utils.get_overcloud_keystone_session()
ctxt = {}
ctxt['workspace_path'] = workspace_path
ctxt_funcs = {
'nova': _add_nova_config,
'neutron': _add_neutron_config,
'glance': _add_glance_config,
'cinder': _add_cinder_config,
'keystone': _add_keystone_config}
ctxt['enabled_services'] = _get_service_list(keystone_session)
if set(['cinderv2', 'cinderv3']) \
.intersection(set(ctxt['enabled_services'])):
ctxt['enabled_services'].append('cinder')
ctxt['disabled_services'] = list(
set(TEMPEST_SVC_LIST) - set(ctxt['enabled_services']))
_add_application_ips(ctxt)
for svc_name, ctxt_func in ctxt_funcs.items():
if svc_name in ctxt['enabled_services']:
ctxt_func(ctxt, keystone_session)
_add_environment_var_config(ctxt, ctxt['enabled_services'])
_add_auth_config(ctxt)
if 'octavia' in ctxt['enabled_services']:
_add_octavia_config(ctxt)
return ctxt
def _render_tempest_config(target_file, ctxt, template_name):
"""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_name: Name of template file
:type template_name: str
:returns: None
:rtype: None
"""
jenv = jinja2.Environment(loader=jinja2.PackageLoader(
'zaza.openstack',
'charm_tests/tempest/templates'))
template = jenv.get_template(template_name)
with open(target_file, 'w') as f:
f.write(template.render(ctxt))
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
"""
# The most most recent API version must be listed first.
volume_types = ['volumev3', 'volumev2']
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_octavia_config(ctxt):
"""Add octavia config to context.
:param ctxt: Context dictionary
:type ctxt: dict
:returns: None
:rtype: None
:raises: subprocess.CalledProcessError
"""
subprocess.check_call([
'curl',
"{}:80/swift/v1/fixtures/test_server.bin".format(
ctxt['test_swift_ip']),
'-o', "{}/test_server.bin".format(ctxt['workspace_path'])
])
subprocess.check_call([
'chmod', '+x',
"{}/test_server.bin".format(ctxt['workspace_path'])
])
def _add_environment_var_config(ctxt, services):
"""Add environment variable config to context.
:param ctxt: Context dictionary
:type ctxt: dict
:returns: None
:rtype: None
"""
deploy_env = deployment_env.get_deployment_context()
missing_vars = []
for svc, env_vars in SETUP_ENV_VARS.items():
if svc in services:
for var in env_vars:
value = deploy_env.get(var)
if value:
ctxt[var.lower()] = value
else:
if var not in IGNORABLE_VARS:
missing_vars.append(var)
if missing_vars:
raise ValueError(
("Environment variables [{}] must all be set to run this"
" test").format(', '.join(missing_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_service_list(keystone_session):
"""Retrieve list of services from keystone.
: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)
return [s.name for s in keystone_client.services.list() if s.enabled]

View File

@@ -635,7 +635,7 @@ class OpenStackBaseTest(BaseCharmTest):
def launch_guest(self, guest_name, userdata=None, use_boot_volume=False,
instance_key=None):
"""Launch two guests to use in tests.
"""Launch one guest to use in tests.
Note that it is up to the caller to have set the RESOURCE_PREFIX class
variable prior to calling this method.

View File

@@ -18,6 +18,7 @@
import logging
import tenacity
import unittest
import zaza.model as zaza_model
@@ -262,7 +263,7 @@ class WorkloadmgrCLIHelper(object):
retryer = tenacity.Retrying(
wait=tenacity.wait_exponential(multiplier=1, max=30),
stop=tenacity.stop_after_delay(900),
stop=tenacity.stop_after_delay(1200),
reraise=True,
)
@@ -424,6 +425,22 @@ class TrilioBaseTest(test_utils.OpenStackBaseTest):
logging.info("Initiating restore")
workloadmgrcli.oneclick_restore(snapshot_id)
def test_update_trilio_action(self):
"""Test that the action runs successfully."""
action_name = 'update-trilio'
actions = zaza_model.get_actions(
self.application_name)
if action_name not in actions:
raise unittest.SkipTest(
'Action {} not defined'.format(action_name))
generic_utils.assertActionRanOK(zaza_model.run_action(
self.lead_unit,
action_name,
action_params={},
model_name=self.model_name)
)
class TrilioGhostNFSShareTest(TrilioBaseTest):
"""Tests for Trilio charms providing the ghost-share action."""

View File

@@ -17,14 +17,12 @@
import base64
import functools
import logging
import requests
import tempfile
import zaza.charm_lifecycle.utils as lifecycle_utils
import zaza.openstack.charm_tests.vault.utils as vault_utils
import zaza.model
import zaza.openstack.utilities.cert
import zaza.openstack.utilities.openstack
import zaza.openstack.utilities.generic
import zaza.openstack.utilities.exceptions as zaza_exceptions
import zaza.utilities.juju as juju_utils
@@ -95,7 +93,7 @@ def mojo_unseal_by_unit():
def unseal_by_unit(cacert=None):
"""Unseal any units reported as sealed using mojo cacert."""
cacert = cacert or get_cacert_file()
vault_creds = vault_utils.get_credentails()
vault_creds = vault_utils.get_credentials()
for client in vault_utils.get_clients(cacert=cacert):
if client.hvac_client.is_sealed():
client.hvac_client.unseal(vault_creds['keys'][0])
@@ -126,7 +124,7 @@ async def async_mojo_unseal_by_unit():
async def async_unseal_by_unit(cacert=None):
"""Unseal any units reported as sealed using vault cacert."""
cacert = cacert or get_cacert_file()
vault_creds = vault_utils.get_credentails()
vault_creds = vault_utils.get_credentials()
for client in vault_utils.get_clients(cacert=cacert):
if client.hvac_client.is_sealed():
client.hvac_client.unseal(vault_creds['keys'][0])
@@ -137,7 +135,8 @@ async def async_unseal_by_unit(cacert=None):
unit_name, './hooks/update-status')
def auto_initialize(cacert=None, validation_application='keystone', wait=True):
def auto_initialize(cacert=None, validation_application='keystone', wait=True,
skip_on_absent=False):
"""Auto initialize vault for testing.
Generate a csr and uploading a signed certificate.
@@ -149,9 +148,16 @@ def auto_initialize(cacert=None, validation_application='keystone', wait=True):
:param validation_application: Name of application to be used as a
client for validation.
:type validation_application: str
:param skip_on_absent: Non-fatal skip initialise if vault absent.
:type validation_application: bool
:returns: None
:rtype: None
"""
if skip_on_absent:
status = zaza.model.get_status()
if 'vault' not in status.applications.keys():
logging.info('Skipping auto_initialize, vault not in model')
return
logging.info('Running auto_initialize')
basic_setup(cacert=cacert, unseal_and_authorize=True)
@@ -199,6 +205,24 @@ def auto_initialize(cacert=None, validation_application='keystone', wait=True):
pass
auto_initialize_opportunistic = functools.partial(
auto_initialize,
skip_on_absent=True)
auto_initialize_opportunistic_no_validation = functools.partial(
auto_initialize,
validation_application=None,
skip_on_absent=True)
auto_initialize_opportunistic_no_validation_no_wait = functools.partial(
auto_initialize,
validation_application=None,
wait=False,
skip_on_absent=True)
auto_initialize_no_validation = functools.partial(
auto_initialize,
validation_application=None)
@@ -222,16 +246,4 @@ def validate_ca(cacertificate, application="keystone", port=5000):
:returns: None
:rtype: None
"""
zaza.openstack.utilities.openstack.block_until_ca_exists(
application,
cacertificate.decode().strip())
vip = (zaza.model.get_application_config(application)
.get("vip").get("value"))
if vip:
ip = vip
else:
ip = zaza.model.get_app_ips(application)[0]
with tempfile.NamedTemporaryFile(mode='w') as fp:
fp.write(cacertificate.decode())
fp.flush()
requests.get('https://{}:{}'.format(ip, str(port)), verify=fp.name)
vault_utils.validate_ca(cacertificate, application, port)

View File

@@ -21,9 +21,7 @@ import json
import logging
import unittest
import uuid
import tempfile
import requests
import tenacity
from hvac.exceptions import InternalServerError
@@ -64,7 +62,7 @@ class BaseVaultTest(test_utils.OpenStackBaseTest):
cls.vip_client = vault_utils.get_vip_client()
if cls.vip_client:
cls.clients.append(cls.vip_client)
cls.vault_creds = vault_utils.get_credentails()
cls.vault_creds = vault_utils.get_credentials()
vault_utils.unseal_all(cls.clients, cls.vault_creds['keys'][0])
vault_utils.auth_all(cls.clients, cls.vault_creds['root_token'])
vault_utils.ensure_secret_backend(cls.clients[0])
@@ -180,26 +178,10 @@ class VaultTest(BaseVaultTest):
except KeyError:
# Already removed
pass
zaza.openstack.utilities.openstack.block_until_ca_exists(
'keystone',
cacert.decode().strip())
zaza.model.wait_for_application_states(
states=test_config.get('target_deploy_status', {}))
ip = zaza.model.get_app_ips(
'keystone')[0]
with tempfile.NamedTemporaryFile(mode='w') as fp:
fp.write(cacert.decode())
fp.flush()
# Avoid race condition and retry
for attempt in tenacity.Retrying(
stop=tenacity.stop_after_attempt(3),
wait=tenacity.wait_exponential(
multiplier=2, min=2, max=10)):
with attempt:
logging.info(
"Attempting to connect to https://{}:5000".format(ip))
requests.get('https://{}:5000'.format(ip), verify=fp.name)
vault_utils.validate_ca(cacert)
def test_all_clients_authenticated(self):
"""Check all vault clients are authenticated."""

View File

@@ -18,6 +18,7 @@
import base64
import hvac
import logging
import requests
import tempfile
import urllib3
@@ -27,6 +28,7 @@ import tenacity
import collections
import zaza.model
import zaza.openstack.utilities.openstack
import zaza.utilities.networking as network_utils
AUTH_FILE = "vault_tests.yaml"
@@ -70,10 +72,10 @@ class VaultFacade:
def initialize(self):
"""Initialise vault and store resulting credentials."""
if self.is_initialized:
self.vault_creds = get_credentails()
self.vault_creds = get_credentials()
else:
self.vault_creds = init_vault(self.unseal_client)
store_credentails(self.vault_creds)
store_credentials(self.vault_creds)
self.initialized = is_initialized(self.unseal_client)
def unseal(self):
@@ -294,7 +296,7 @@ def find_unit_with_creds():
return unit
def get_credentails():
def get_credentials():
"""Retrieve vault token and keys from unit.
Retrieve vault token and keys from unit. These are stored on a unit
@@ -315,7 +317,7 @@ def get_credentails():
return creds
def store_credentails(creds):
def store_credentials(creds):
"""Store the supplied credentials.
Store the supplied credentials on a vault unit. ONLY USE FOR FUNCTIONAL
@@ -334,7 +336,7 @@ def store_credentails(creds):
'~/{}'.format(AUTH_FILE))
def get_credentails_from_file(auth_file):
def get_credentials_from_file(auth_file):
"""Read the vault credentials from the auth_file.
:param auth_file: Path to file with credentials
@@ -347,7 +349,7 @@ def get_credentails_from_file(auth_file):
return vault_creds
def write_credentails(auth_file, vault_creds):
def write_credentials(auth_file, vault_creds):
"""Write the vault credentials to the auth_file.
:param auth_file: Path to file to write credentials
@@ -434,3 +436,37 @@ def run_upload_signed_csr(pem, root_ca, allowed_domains):
'root-ca': base64.b64encode(root_ca).decode(),
'allowed-domains=': allowed_domains,
'ttl': '24h'})
@tenacity.retry(
reraise=True,
wait=tenacity.wait_exponential(multiplier=2, min=2, max=10),
stop=tenacity.stop_after_attempt(3))
def validate_ca(cacertificate, application="keystone", port=5000):
"""Validate Certificate Authority against application.
:param cacertificate: PEM formatted CA certificate
:type cacertificate: str
:param application: Which application to validate against.
:type application: str
:param port: Port to validate against.
:type port: int
:returns: None
:rtype: None
"""
zaza.openstack.utilities.openstack.block_until_ca_exists(
application,
cacertificate.decode().strip())
vip = (zaza.model.get_application_config(application)
.get("vip").get("value"))
if vip:
ip = vip
else:
ip = zaza.model.get_app_ips(application)[0]
with tempfile.NamedTemporaryFile(mode='w') as fp:
fp.write(cacertificate.decode())
fp.flush()
keystone_url = 'https://{}:{}'.format(ip, str(port))
logging.info(
'Attempting to connect to {}'.format(keystone_url))
requests.get(keystone_url, verify=fp.name)

View File

@@ -125,6 +125,9 @@ def launch_instance(instance_key, use_boot_volume=False, vm_name=None,
nova_client.servers,
instance.id,
expected_status='ACTIVE',
# NOTE(lourot): in some models this may sometimes take more than 15
# minutes. See lp:1945991
wait_iteration_max_time=120,
stop_after_attempt=16)
logging.info('Checking cloud init is complete')

View File

@@ -126,19 +126,19 @@ def setup_sdn(network_config, keystone_session=None):
logging.info("Configuring overcloud network")
# Create the external network
ext_network = openstack_utils.create_external_network(
ext_network = openstack_utils.create_provider_network(
neutron_client,
project_id,
network_config["external_net_name"])
openstack_utils.create_external_subnet(
openstack_utils.create_provider_subnet(
neutron_client,
project_id,
ext_network,
network_config["external_subnet_name"],
network_config["default_gateway"],
network_config["external_net_cidr"],
network_config["start_floating_ip"],
network_config["end_floating_ip"],
network_config["external_subnet_name"])
network_config["end_floating_ip"])
provider_router = (
openstack_utils.create_provider_router(neutron_client, project_id))
openstack_utils.plug_extnet_into_router(
@@ -183,6 +183,61 @@ def setup_sdn(network_config, keystone_session=None):
openstack_utils.add_neutron_secgroup_rules(neutron_client, project_id)
def setup_sdn_provider_vlan(network_config, keystone_session=None):
"""Perform setup for Software Defined Network, specifically a provider VLAN.
:param network_config: Network configuration settings dictionary
:type network_config: dict
:param keystone_session: Keystone session object for overcloud
:type keystone_session: keystoneauth1.session.Session object
:returns: None
:rtype: None
"""
# If a session has not been provided, acquire one
if not keystone_session:
keystone_session = openstack_utils.get_overcloud_keystone_session()
# Get authenticated clients
keystone_client = openstack_utils.get_keystone_session_client(
keystone_session)
neutron_client = openstack_utils.get_neutron_session_client(
keystone_session)
admin_domain = None
if openstack_utils.get_keystone_api_version() > 2:
admin_domain = "admin_domain"
# Resolve the project name from the overcloud openrc into a project id
project_id = openstack_utils.get_project_id(
keystone_client,
"admin",
domain_name=admin_domain,
)
logging.info("Configuring VLAN provider network")
# Create the external network
provider_vlan_network = openstack_utils.create_provider_network(
neutron_client,
project_id,
net_name=network_config["provider_vlan_net_name"],
external=False,
shared=True,
network_type='vlan',
vlan_id=network_config["provider_vlan_id"])
provider_vlan_subnet = openstack_utils.create_provider_subnet(
neutron_client,
project_id,
provider_vlan_network,
network_config["provider_vlan_subnet_name"],
cidr=network_config["provider_vlan_cidr"],
dhcp=True)
openstack_utils.plug_subnet_into_router(
neutron_client,
network_config["router_name"],
provider_vlan_network,
provider_vlan_subnet)
openstack_utils.add_neutron_secgroup_rules(neutron_client, project_id)
def setup_gateway_ext_port(network_config, keystone_session=None,
limit_gws=None,
use_juju_wait=True):

View File

@@ -15,6 +15,12 @@
"""Module of exceptions that zaza may raise."""
class InvalidTestConfig(Exception):
"""Exception when the test configuration is invalid."""
pass
class MissingOSAthenticationException(Exception):
"""Exception when some data needed to authenticate is missing."""

View File

@@ -986,8 +986,11 @@ def configure_networking_charms(networking_data, macs, use_juju_wait=True):
current_data_port = get_application_config_option(
application_name,
networking_data.port_config_key)
if current_data_port == config[networking_data.port_config_key]:
logging.info('Config already set to value')
if current_data_port:
logging.info("Skip update of external network data port config."
"Config '{}' already set to value: {}".format(
networking_data.port_config_key,
current_data_port))
return
model.set_application_config(
@@ -997,7 +1000,7 @@ def configure_networking_charms(networking_data, macs, use_juju_wait=True):
# to deal with all the non ['active', 'idle', 'Unit is ready.']
# workload/agent states and msgs that our mojo specs are exposed to.
if use_juju_wait:
juju_wait.wait(wait_for_workload=True)
juju_wait.wait(wait_for_workload=True, max_wait=2700)
else:
zaza.model.wait_for_agent_status()
# TODO: shouldn't access get_charm_config() here as it relies on
@@ -1056,14 +1059,23 @@ def configure_charmed_openstack_on_maas(network_config, limit_gws=None):
:type limit_gws: Optional[int]
"""
networking_data = get_charm_networking_data(limit_gws=limit_gws)
macs = [
mim.mac
for mim in zaza.utilities.maas.get_macs_from_cidr(
macs = []
machines = set()
for mim in zaza.utilities.maas.get_macs_from_cidr(
zaza.utilities.maas.get_maas_client_from_juju_cloud_data(
zaza.model.get_cloud_data()),
network_config['external_net_cidr'],
link_mode=zaza.utilities.maas.LinkMode.LINK_UP)
]
link_mode=zaza.utilities.maas.LinkMode.LINK_UP):
if mim.machine_id in machines:
logging.warning("Machine {} has multiple unconfigured interfaces, "
"ignoring interface {} ({})."
.format(mim.machine_id, mim.ifname, mim.mac))
continue
logging.info("Machine {} selected {} ({}) for external networking."
.format(mim.machine_id, mim.ifname, mim.mac))
machines.add(mim.machine_id)
macs.append(mim.mac)
if macs:
configure_networking_charms(
networking_data, macs, use_juju_wait=False)
@@ -1127,8 +1139,10 @@ def create_project_network(neutron_client, project_id, net_name='private',
return network
def create_external_network(neutron_client, project_id, net_name='ext_net'):
"""Create the external network.
def create_provider_network(neutron_client, project_id, net_name='ext_net',
external=True, shared=False, network_type='flat',
vlan_id=None):
"""Create a provider network.
:param neutron_client: Authenticated neutronclient
:type neutron_client: neutronclient.Client object
@@ -1136,25 +1150,35 @@ def create_external_network(neutron_client, project_id, net_name='ext_net'):
:type project_id: string
:param net_name: Network name
:type net_name: string
:param shared: The network should be external
:type shared: boolean
:param shared: The network should be shared between projects
:type shared: boolean
:param net_type: Network type: GRE, VXLAN, local, VLAN
:type net_type: string
:param net_name: VLAN ID
:type net_name: string
:returns: Network object
:rtype: dict
"""
networks = neutron_client.list_networks(name=net_name)
if len(networks['networks']) == 0:
logging.info('Configuring external network')
logging.info('Creating %s %s network: %s', network_type,
'external' if external else 'provider', net_name)
network_msg = {
'name': net_name,
'router:external': True,
'router:external': external,
'shared': shared,
'tenant_id': project_id,
'provider:physical_network': 'physnet1',
'provider:network_type': 'flat',
'provider:network_type': network_type,
}
logging.info('Creating new external network definition: %s',
net_name)
if network_type == 'vlan':
network_msg['provider:segmentation_id'] = int(vlan_id)
network = neutron_client.create_network(
{'network': network_msg})['network']
logging.info('New external network created: %s', network['id'])
logging.info('Network %s created: %s', net_name, network['id'])
else:
logging.warning('Network %s already exists.', net_name)
network = networks['networks'][0]
@@ -1214,11 +1238,12 @@ def create_project_subnet(neutron_client, project_id, network, cidr, dhcp=True,
return subnet
def create_external_subnet(neutron_client, project_id, network,
def create_provider_subnet(neutron_client, project_id, network,
subnet_name='ext_net_subnet',
default_gateway=None, cidr=None,
start_floating_ip=None, end_floating_ip=None,
subnet_name='ext_net_subnet'):
"""Create the external subnet.
dhcp=False):
"""Create the provider subnet.
:param neutron_client: Authenticated neutronclient
:type neutron_client: neutronclient.Client object
@@ -1228,14 +1253,16 @@ def create_external_subnet(neutron_client, project_id, network,
:type network: dict
:param default_gateway: Deafault gateway IP address
:type default_gateway: string
:param subnet_name: Subnet name
:type subnet_name: string
:param cidr: Network CIDR
:type cidr: string
:param start_floating_ip: Start of floating IP range: IP address
:type start_floating_ip: string or None
:param end_floating_ip: End of floating IP range: IP address
:type end_floating_ip: string or None
:param subnet_name: Subnet name
:type subnet_name: string
:param dhcp: Run DHCP on this subnet
:type dhcp: boolean
:returns: Subnet object
:rtype: dict
"""
@@ -1244,7 +1271,7 @@ def create_external_subnet(neutron_client, project_id, network,
subnet_msg = {
'name': subnet_name,
'network_id': network['id'],
'enable_dhcp': False,
'enable_dhcp': dhcp,
'ip_version': 4,
'tenant_id': project_id
}
@@ -2194,8 +2221,9 @@ def find_cirros_image(arch):
:returns: URL for latest cirros image
:rtype: str
"""
http_connection_timeout = 10 # seconds
opener = get_urllib_opener()
f = opener.open(CIRROS_RELEASE_URL)
f = opener.open(CIRROS_RELEASE_URL, timeout=http_connection_timeout)
version = f.read().strip().decode()
cirros_img = 'cirros-{}-{}-disk.img'.format(version, arch)
return '{}/{}/{}'.format(CIRROS_IMAGE_URL, version, cirros_img)
@@ -2819,7 +2847,7 @@ def ssh_test(username, ip, vm_name, password=None, privkey=None, retry=True):
return_string = stdout.readlines()[0].strip()
if return_string == vm_name:
logging.info('SSH to %s(%s) succesfull' % (vm_name, ip))
logging.info('SSH to %s(%s) successful' % (vm_name, ip))
else:
logging.info('SSH to %s(%s) failed (%s != %s)' % (vm_name, ip,
return_string,

View File

@@ -132,12 +132,23 @@ def action_upgrade_apps(applications, model_name=None):
status=status,
model_name=model_name)
# NOTE(lourot): we're more likely to time out while waiting for the
# action's result if we launch an action while the model is still
# executing. Thus it's safer to wait for the model to settle between
# actions.
zaza.model.block_until_all_units_idle(model_name)
pause_units(hacluster_units, model_name=model_name)
zaza.model.block_until_all_units_idle(model_name)
pause_units(target, model_name=model_name)
zaza.model.block_until_all_units_idle(model_name)
action_unit_upgrade(target, model_name=model_name)
zaza.model.block_until_all_units_idle(model_name)
resume_units(target, model_name=model_name)
zaza.model.block_until_all_units_idle(model_name)
resume_units(hacluster_units, model_name=model_name)
done.extend(target)

View File

@@ -37,6 +37,8 @@ UBUNTU_OPENSTACK_RELEASE = OrderedDict([
('focal', 'ussuri'),
('groovy', 'victoria'),
('hirsute', 'wallaby'),
('impish', 'xena'),
('jammy', 'yoga'),
])
@@ -60,6 +62,9 @@ OPENSTACK_CODENAMES = OrderedDict([
('2019.2', 'train'),
('2020.1', 'ussuri'),
('2020.2', 'victoria'),
('2021.1', 'wallaby'),
('2021.2', 'xena'),
('2022.1', 'yoga'),
])
OPENSTACK_RELEASES_PAIRS = [
@@ -71,7 +76,10 @@ OPENSTACK_RELEASES_PAIRS = [
'bionic_stein', 'disco_stein', 'bionic_train',
'eoan_train', 'bionic_ussuri', 'focal_ussuri',
'focal_victoria', 'groovy_victoria',
'focal_wallaby', 'hirsute_wallaby']
'focal_wallaby', 'hirsute_wallaby',
'focal_xena', 'impish_xena',
'focal_yoga', 'jammy_yoga',
]
SWIFT_CODENAMES = OrderedDict([
('diablo',