diff --git a/pip.sh b/pip.sh new file mode 100755 index 0000000..8a71ce4 --- /dev/null +++ b/pip.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +pip install pip==20.2.3 +pip "$@" diff --git a/requirements.txt b/requirements.txt index adec5bc..9a069f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.py b/setup.py index fb4377d..d90e716 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/tox.ini b/tox.ini index aea173f..890b193 100644 --- a/tox.ini +++ b/tox.ini @@ -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 diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py index 03c4879..8203d13 100644 --- a/unit_tests/__init__.py +++ b/unit_tests/__init__.py @@ -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() diff --git a/unit_tests/charm_tests/test_tempest.py b/unit_tests/charm_tests/test_tempest.py index 3c4c161..ac6962e 100644 --- a/unit_tests/charm_tests/test_tempest.py +++ b/unit_tests/charm_tests/test_tempest.py @@ -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'), diff --git a/unit_tests/utilities/test_zaza_utilities_openstack.py b/unit_tests/utilities/test_zaza_utilities_openstack.py index 0434a2b..472b35b 100644 --- a/unit_tests/utilities/test_zaza_utilities_openstack.py +++ b/unit_tests/utilities/test_zaza_utilities_openstack.py @@ -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): diff --git a/zaza/openstack/charm_tests/ceilometer_agent/__init__.py b/zaza/openstack/charm_tests/ceilometer_agent/__init__.py new file mode 100644 index 0000000..6272e26 --- /dev/null +++ b/zaza/openstack/charm_tests/ceilometer_agent/__init__.py @@ -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.""" diff --git a/zaza/openstack/charm_tests/ceilometer_agent/tests.py b/zaza/openstack/charm_tests/ceilometer_agent/tests.py new file mode 100644 index 0000000..655b1c7 --- /dev/null +++ b/zaza/openstack/charm_tests/ceilometer_agent/tests.py @@ -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 diff --git a/zaza/openstack/charm_tests/ceph/dashboard/setup.py b/zaza/openstack/charm_tests/ceph/dashboard/setup.py new file mode 100644 index 0000000..7a2488f --- /dev/null +++ b/zaza/openstack/charm_tests/ceph/dashboard/setup.py @@ -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)}) diff --git a/zaza/openstack/charm_tests/ceph/dashboard/tests.py b/zaza/openstack/charm_tests/ceph/dashboard/tests.py index e7c8863..50cdbaf 100644 --- a/zaza/openstack/charm_tests/ceph/dashboard/tests.py +++ b/zaza/openstack/charm_tests/ceph/dashboard/tests.py @@ -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') diff --git a/zaza/openstack/charm_tests/cinder_lvm/__init__.py b/zaza/openstack/charm_tests/cinder_lvm/__init__.py new file mode 100644 index 0000000..740be90 --- /dev/null +++ b/zaza/openstack/charm_tests/cinder_lvm/__init__.py @@ -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.""" diff --git a/zaza/openstack/charm_tests/cinder_lvm/tests.py b/zaza/openstack/charm_tests/cinder_lvm/tests.py new file mode 100644 index 0000000..ae9bc92 --- /dev/null +++ b/zaza/openstack/charm_tests/cinder_lvm/tests.py @@ -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() diff --git a/zaza/openstack/charm_tests/cinder_netapp/__init__.py b/zaza/openstack/charm_tests/cinder_netapp/__init__.py new file mode 100644 index 0000000..31c5165 --- /dev/null +++ b/zaza/openstack/charm_tests/cinder_netapp/__init__.py @@ -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.""" diff --git a/zaza/openstack/charm_tests/cinder_netapp/tests.py b/zaza/openstack/charm_tests/cinder_netapp/tests.py new file mode 100644 index 0000000..41456ba --- /dev/null +++ b/zaza/openstack/charm_tests/cinder_netapp/tests.py @@ -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) diff --git a/zaza/openstack/charm_tests/designate/tests.py b/zaza/openstack/charm_tests/designate/tests.py index 78a5f31..ba0cd6e 100644 --- a/zaza/openstack/charm_tests/designate/tests.py +++ b/zaza/openstack/charm_tests/designate/tests.py @@ -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 diff --git a/zaza/openstack/charm_tests/manila/tests.py b/zaza/openstack/charm_tests/manila/tests.py index 8381f2f..455feef 100644 --- a/zaza/openstack/charm_tests/manila/tests.py +++ b/zaza/openstack/charm_tests/manila/tests.py @@ -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.""" diff --git a/zaza/openstack/charm_tests/manila_ganesha/tests.py b/zaza/openstack/charm_tests/manila_ganesha/tests.py index 2e9a186..0e99871 100644 --- a/zaza/openstack/charm_tests/manila_ganesha/tests.py +++ b/zaza/openstack/charm_tests/manila_ganesha/tests.py @@ -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 diff --git a/zaza/openstack/charm_tests/mysql/tests.py b/zaza/openstack/charm_tests/mysql/tests.py index 3e47678..fbc6494 100644 --- a/zaza/openstack/charm_tests/mysql/tests.py +++ b/zaza/openstack/charm_tests/mysql/tests.py @@ -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") diff --git a/zaza/openstack/charm_tests/neutron/setup.py b/zaza/openstack/charm_tests/neutron/setup.py index a1d1dd4..174c51d 100644 --- a/zaza/openstack/charm_tests/neutron/setup.py +++ b/zaza/openstack/charm_tests/neutron/setup.py @@ -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, diff --git a/zaza/openstack/charm_tests/neutron/tests.py b/zaza/openstack/charm_tests/neutron/tests.py index 54420e8..96b45de 100644 --- a/zaza/openstack/charm_tests/neutron/tests.py +++ b/zaza/openstack/charm_tests/neutron/tests.py @@ -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. diff --git a/zaza/openstack/charm_tests/openstack_dashboard/tests.py b/zaza/openstack/charm_tests/openstack_dashboard/tests.py index 5cd341c..60ac7eb 100644 --- a/zaza/openstack/charm_tests/openstack_dashboard/tests.py +++ b/zaza/openstack/charm_tests/openstack_dashboard/tests.py @@ -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") diff --git a/zaza/openstack/charm_tests/openstack_upgrade/tests.py b/zaza/openstack/charm_tests/openstack_upgrade/tests.py index 82c7fbb..661a076 100644 --- a/zaza/openstack/charm_tests/openstack_upgrade/tests.py +++ b/zaza/openstack/charm_tests/openstack_upgrade/tests.py @@ -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) diff --git a/zaza/openstack/charm_tests/ovn/tests.py b/zaza/openstack/charm_tests/ovn/tests.py index 2478d66..43409ae 100644 --- a/zaza/openstack/charm_tests/ovn/tests.py +++ b/zaza/openstack/charm_tests/ovn/tests.py @@ -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.""" diff --git a/zaza/openstack/charm_tests/tempest/setup.py b/zaza/openstack/charm_tests/tempest/setup.py index 7ae0ba3..627290c 100644 --- a/zaza/openstack/charm_tests/tempest/setup.py +++ b/zaza/openstack/charm_tests/tempest/setup.py @@ -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() diff --git a/zaza/openstack/charm_tests/tempest/tests.py b/zaza/openstack/charm_tests/tempest/tests.py index 979caff..b7fc859 100644 --- a/zaza/openstack/charm_tests/tempest/tests.py +++ b/zaza/openstack/charm_tests/tempest/tests.py @@ -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() diff --git a/zaza/openstack/charm_tests/tempest/utils.py b/zaza/openstack/charm_tests/tempest/utils.py index 52ae126..830600e 100644 --- a/zaza/openstack/charm_tests/tempest/utils.py +++ b/zaza/openstack/charm_tests/tempest/utils.py @@ -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] diff --git a/zaza/openstack/charm_tests/test_utils.py b/zaza/openstack/charm_tests/test_utils.py index bc67933..be47dd9 100644 --- a/zaza/openstack/charm_tests/test_utils.py +++ b/zaza/openstack/charm_tests/test_utils.py @@ -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. diff --git a/zaza/openstack/charm_tests/trilio/tests.py b/zaza/openstack/charm_tests/trilio/tests.py index 6fb7692..c5902c2 100644 --- a/zaza/openstack/charm_tests/trilio/tests.py +++ b/zaza/openstack/charm_tests/trilio/tests.py @@ -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.""" diff --git a/zaza/openstack/charm_tests/vault/setup.py b/zaza/openstack/charm_tests/vault/setup.py index c792508..05e8cc5 100644 --- a/zaza/openstack/charm_tests/vault/setup.py +++ b/zaza/openstack/charm_tests/vault/setup.py @@ -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) diff --git a/zaza/openstack/charm_tests/vault/tests.py b/zaza/openstack/charm_tests/vault/tests.py index a83440f..4ba2e58 100644 --- a/zaza/openstack/charm_tests/vault/tests.py +++ b/zaza/openstack/charm_tests/vault/tests.py @@ -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.""" diff --git a/zaza/openstack/charm_tests/vault/utils.py b/zaza/openstack/charm_tests/vault/utils.py index b4b5579..c29c745 100644 --- a/zaza/openstack/charm_tests/vault/utils.py +++ b/zaza/openstack/charm_tests/vault/utils.py @@ -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) diff --git a/zaza/openstack/configure/guest.py b/zaza/openstack/configure/guest.py index c2e20de..d63938d 100644 --- a/zaza/openstack/configure/guest.py +++ b/zaza/openstack/configure/guest.py @@ -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') diff --git a/zaza/openstack/configure/network.py b/zaza/openstack/configure/network.py index ad1a07b..5d3a2f8 100755 --- a/zaza/openstack/configure/network.py +++ b/zaza/openstack/configure/network.py @@ -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): diff --git a/zaza/openstack/utilities/exceptions.py b/zaza/openstack/utilities/exceptions.py index 364ab49..7f73be4 100644 --- a/zaza/openstack/utilities/exceptions.py +++ b/zaza/openstack/utilities/exceptions.py @@ -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.""" diff --git a/zaza/openstack/utilities/openstack.py b/zaza/openstack/utilities/openstack.py index 3082068..2f19905 100644 --- a/zaza/openstack/utilities/openstack.py +++ b/zaza/openstack/utilities/openstack.py @@ -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, diff --git a/zaza/openstack/utilities/openstack_upgrade.py b/zaza/openstack/utilities/openstack_upgrade.py index c279e58..f75a05c 100755 --- a/zaza/openstack/utilities/openstack_upgrade.py +++ b/zaza/openstack/utilities/openstack_upgrade.py @@ -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) diff --git a/zaza/openstack/utilities/os_versions.py b/zaza/openstack/utilities/os_versions.py index beaedec..e67a97f 100644 --- a/zaza/openstack/utilities/os_versions.py +++ b/zaza/openstack/utilities/os_versions.py @@ -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',