diff --git a/requirements.txt b/requirements.txt index 9a069f9..79d07fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,6 +36,7 @@ python-glanceclient python-heatclient python-ironicclient python-keystoneclient +python-magnumclient python-manilaclient python-neutronclient python-novaclient diff --git a/setup.py b/setup.py index d90e716..1f14af9 100644 --- a/setup.py +++ b/setup.py @@ -53,6 +53,7 @@ install_require = [ 'python-ironicclient', 'python-glanceclient<3.0.0', 'python-keystoneclient<3.22.0', + 'python-magnumclient', 'python-manilaclient<2.0.0', 'python-novaclient<16.0.0', 'python-neutronclient<7.0.0', diff --git a/zaza/openstack/charm_tests/heat/setup.py b/zaza/openstack/charm_tests/heat/setup.py new file mode 100644 index 0000000..7eb0374 --- /dev/null +++ b/zaza/openstack/charm_tests/heat/setup.py @@ -0,0 +1,31 @@ +#!/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. + +"""Code for configuring heat.""" + +import logging +import zaza.model + + +def domain_setup(application_name='heat'): + """Run required action for a working Heat application.""" + # Action is REQUIRED to run for a functioning heat deployment + logging.info('Running domain-setup action on heat unit...') + zaza.model.block_until_wl_status_info_starts_with(application_name, + "Unit is ready") + zaza.model.run_action_on_leader(application_name, "domain-setup") + zaza.model.block_until_wl_status_info_starts_with(application_name, + "Unit is ready") diff --git a/zaza/openstack/charm_tests/heat/tests.py b/zaza/openstack/charm_tests/heat/tests.py index 24062b7..0aaba07 100644 --- a/zaza/openstack/charm_tests/heat/tests.py +++ b/zaza/openstack/charm_tests/heat/tests.py @@ -62,6 +62,8 @@ class HeatBasicDeployment(test_utils.OpenStackBaseTest): services = ['heat-api', 'heat-api-cfn', 'heat-engine'] return services + # TODO: Deprecate this function + # domain-setup action has been added as a setup configuration option def test_100_domain_setup(self): """Run required action for a working Heat unit.""" # Action is REQUIRED to run for a functioning heat deployment diff --git a/zaza/openstack/charm_tests/magnum/__init__.py b/zaza/openstack/charm_tests/magnum/__init__.py new file mode 100644 index 0000000..4e79a9f --- /dev/null +++ b/zaza/openstack/charm_tests/magnum/__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 magnum.""" diff --git a/zaza/openstack/charm_tests/magnum/setup.py b/zaza/openstack/charm_tests/magnum/setup.py new file mode 100644 index 0000000..4255f8c --- /dev/null +++ b/zaza/openstack/charm_tests/magnum/setup.py @@ -0,0 +1,64 @@ +#!/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. + +"""Code for configuring magnum.""" + +import logging +import os +import tenacity + +import zaza.model +import zaza.openstack.utilities.openstack as openstack_utils + +IMAGE_NAME = 'fedora-coreos' + + +def domain_setup(application_name='magnum'): + """Run required action for a working Magnum application.""" + # Action is REQUIRED to run for a functioning magnum deployment + logging.info('Running domain-setup action on magnum unit...') + zaza.model.block_until_wl_status_info_starts_with(application_name, + "Unit is ready") + zaza.model.run_action_on_leader(application_name, "domain-setup") + zaza.model.block_until_wl_status_info_starts_with(application_name, + "Unit is ready") + + +def add_image(image_url=None): + """Upload Magnum image. + + Upload the operating system images built for Kubernetes deployments. + Fedora CoreOS image was tested by Magnum team. + + :param image_url: URL where the image resides + :type image_url: str + """ + image_url = image_url or os.environ.get( + 'TEST_MAGNUM_QCOW2_IMAGE_URL', None) + if image_url is None: + raise ValueError("Missing image_url") + for attempt in tenacity.Retrying( + stop=tenacity.stop_after_attempt(3), + reraise=True): + with attempt: + keystone_session = openstack_utils.get_overcloud_keystone_session() + glance_client = openstack_utils.get_glance_session_client( + keystone_session) + image_properties = { + 'os_distro': IMAGE_NAME + } + openstack_utils.create_image(glance_client, image_url, IMAGE_NAME, + properties=image_properties) diff --git a/zaza/openstack/charm_tests/magnum/tests.py b/zaza/openstack/charm_tests/magnum/tests.py new file mode 100644 index 0000000..e2d77e8 --- /dev/null +++ b/zaza/openstack/charm_tests/magnum/tests.py @@ -0,0 +1,157 @@ +#!/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 magnum testing.""" + +import logging +import urllib + +import zaza.openstack.charm_tests.test_utils as test_utils +import zaza.openstack.utilities.openstack as openstack_utils +import zaza.openstack.utilities.generic as generic_utils + +from zaza.openstack.charm_tests.magnum.setup import IMAGE_NAME + +# Resource and name constants +CLUSTER_NAME = 'test-kubernetes' +TEMPLATE_NAME = 'test-kubernetes-template' +FLAVOR_NAME = 'm1.small' + + +class MagnumBasicDeployment(test_utils.OpenStackBaseTest): + """Encapsulate Magnum tests.""" + + @classmethod + def setUpClass(cls): + """Run class setup for running magnum tests.""" + super(MagnumBasicDeployment, cls).setUpClass() + cls.keystone_session = openstack_utils.get_overcloud_keystone_session() + cls.magnum_client = openstack_utils.get_magnum_session_client( + cls.keystone_session) + cls.glance_client = openstack_utils.get_glance_session_client( + cls.keystone_session) + + @property + def services(self): + """Return a list services for the selected OpenStack release. + + :returns: List of services + :rtype: [str] + """ + services = ['magnum-api', 'magnum-conductor'] + return services + + def test_410_magnum_cluster_create_delete(self): + """Create cluster, confirm nova compute resource, delete cluster.""" + # Verify new image name + images_list = list(self.glance_client.images.list()) + self.assertEqual(images_list[0].name, IMAGE_NAME, + "Magnum image not found") + + # Create magnum template + template_fields = { + 'name': TEMPLATE_NAME, + 'image_id': IMAGE_NAME, + 'external_network_id': 'ext_net', + 'dns_nameserver': '1.1.1.1', + 'master_flavor_id': FLAVOR_NAME, + 'flavor_id': FLAVOR_NAME, + 'coe': 'kubernetes' + } + + logging.info('Creating magnum cluster template...') + + template = self.magnum_client.cluster_templates.create( + **template_fields) + logging.info('Cluster template data: {}'.format(template)) + + # Create a magnum cluster from a magnum template, verify its status + logging.info('Creating magnum cluster...') + # Create the cluster + cluster_fields = { + 'name': CLUSTER_NAME, + 'cluster_template_id': template.uuid, + 'master_count': 1, + 'node_count': 1, + 'keypair': 'zaza' + } + + cluster = self.magnum_client.clusters.create(**cluster_fields) + logging.info('Cluster data: {}'.format(cluster)) + + # Confirm stack reaches COMPLETE status. + openstack_utils.resource_reaches_status( + self.magnum_client.clusters, + cluster.uuid, + expected_status="CREATE_COMPLETE", + msg="Cluster status wait", + stop_after_attempt=20, + wait_iteration_max_time=600, + wait_exponential_multiplier=2) + + # List cluster + clusters = list(self.magnum_client.clusters.list()) + logging.info('All clusters: {}'.format(clusters)) + + # Get cluster information + cluster = self.magnum_client.clusters.get(CLUSTER_NAME) + + # Check Kubernetes api address + api_address = urllib.parse.urlparse(cluster.api_address) + api_status = generic_utils.is_port_open(api_address.port, + api_address.hostname) + self.assertTrue(api_status, 'Kubernetes API is unavailable') + + # Delete cluster + logging.info('Deleting magnum cluster...') + openstack_utils.delete_resource(self.magnum_client.clusters, + CLUSTER_NAME, msg="magnum cluster") + + # Delete template + logging.info('Deleting magnum cluster template...') + openstack_utils.delete_resource(self.magnum_client.cluster_templates, + TEMPLATE_NAME, + msg="magnum cluster template") + + def test_900_magnum_restart_on_config_change(self): + """Verify the specified services are restarted when config changes.""" + logging.info('Testing restart on configuration change') + + # Expected default and alternate values + set_default = {'cert-manager-type': 'barbican'} + set_alternate = {'cert-manager-type': 'x509keypair'} + + # Config file affected by juju set config change + conf_file = '/etc/magnum/magnum.conf' + + # Make config change, check for service restarts + logging.info('Making configuration change') + self.restart_on_changed( + conf_file, + set_default, + set_alternate, + None, + None, + self.services) + + def test_910_pause_and_resume(self): + """Run services pause and resume tests.""" + logging.info('Checking pause and resume actions...') + + logging.info('Skipping pause resume test LP: #1886202...') + return + with self.pause_resume(self.services): + logging.info("Testing pause resume") diff --git a/zaza/openstack/utilities/openstack.py b/zaza/openstack/utilities/openstack.py index 7cec341..417dcce 100644 --- a/zaza/openstack/utilities/openstack.py +++ b/zaza/openstack/utilities/openstack.py @@ -50,6 +50,7 @@ from openstack import connection from aodhclient.v2 import client as aodh_client from cinderclient import client as cinderclient from heatclient import client as heatclient +from magnumclient import client as magnumclient from glanceclient import Client as GlanceClient from designateclient.client import Client as DesignateClient @@ -407,6 +408,19 @@ def get_heat_session_client(session, version=1): return heatclient.Client(session=session, version=version) +def get_magnum_session_client(session, version='1'): + """Return magnumclient authenticated by keystone session. + + :param session: Keystone session object + :type session: keystoneauth1.session.Session object + :param version: Magnum API version + :type version: string + :returns: Authenticated magnumclient + :rtype: magnumclient.Client object + """ + return magnumclient.Client(version, session=session) + + def get_cinder_session_client(session, version=3): """Return cinderclient authenticated by keystone session.