From a422e5a203911e7d39fb7cc7bb66449131a04cbf Mon Sep 17 00:00:00 2001 From: Carlos Goncalves Date: Thu, 25 Jun 2020 15:20:09 +0200 Subject: [PATCH] Introduce an image driver interface With this image driver interface, we align our codebase with other existing driver interfaces like compute, network and volume. This interface also allows the amphora provider driver to check for existence of tagged images at API level (e.g. amphora image tag capability in Octavia flavors). Change-Id: Id808c082808fafe1a1e004957ff47eca57f97ee8 --- devstack/plugin.sh | 1 + devstack/settings | 1 + etc/octavia.conf | 5 ++ octavia/common/config.py | 4 ++ octavia/common/constants.py | 4 ++ octavia/common/exceptions.py | 4 +- octavia/compute/drivers/nova_driver.py | 42 ++--------- octavia/image/__init__.py | 0 octavia/image/drivers/__init__.py | 0 octavia/image/drivers/glance_driver.py | 69 +++++++++++++++++++ octavia/image/drivers/noop_driver/__init__.py | 0 octavia/image/drivers/noop_driver/driver.py | 43 ++++++++++++ octavia/image/image_base.py | 28 ++++++++ .../unit/compute/drivers/test_nova_driver.py | 44 +----------- octavia/tests/unit/image/__init__.py | 0 octavia/tests/unit/image/drivers/__init__.py | 0 .../unit/image/drivers/test_glance_driver.py | 65 +++++++++++++++++ .../image/drivers/test_image_noop_driver.py | 39 +++++++++++ releasenotes/notes/add-c9b9401b831efb25.yaml | 10 +++ setup.cfg | 3 + 20 files changed, 283 insertions(+), 79 deletions(-) create mode 100644 octavia/image/__init__.py create mode 100644 octavia/image/drivers/__init__.py create mode 100644 octavia/image/drivers/glance_driver.py create mode 100644 octavia/image/drivers/noop_driver/__init__.py create mode 100644 octavia/image/drivers/noop_driver/driver.py create mode 100644 octavia/image/image_base.py create mode 100644 octavia/tests/unit/image/__init__.py create mode 100644 octavia/tests/unit/image/drivers/__init__.py create mode 100644 octavia/tests/unit/image/drivers/test_glance_driver.py create mode 100644 octavia/tests/unit/image/drivers/test_image_noop_driver.py create mode 100644 releasenotes/notes/add-c9b9401b831efb25.yaml diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 435a0f367e..ae517a9f9b 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -307,6 +307,7 @@ function octavia_configure { iniset $OCTAVIA_CONF controller_worker compute_driver ${OCTAVIA_COMPUTE_DRIVER} iniset $OCTAVIA_CONF controller_worker volume_driver ${OCTAVIA_VOLUME_DRIVER} iniset $OCTAVIA_CONF controller_worker network_driver ${OCTAVIA_NETWORK_DRIVER} + iniset $OCTAVIA_CONF controller_worker image_driver ${OCTAVIA_IMAGE_DRIVER} iniset $OCTAVIA_CONF controller_worker amp_image_tag ${OCTAVIA_AMP_IMAGE_TAG} iniuncomment $OCTAVIA_CONF health_manager heartbeat_key diff --git a/devstack/settings b/devstack/settings index 8af15ea1fe..8233e44a96 100644 --- a/devstack/settings +++ b/devstack/settings @@ -21,6 +21,7 @@ OCTAVIA_AMPHORA_DRIVER=${OCTAVIA_AMPHORA_DRIVER:-"amphora_haproxy_rest_driver"} OCTAVIA_NETWORK_DRIVER=${OCTAVIA_NETWORK_DRIVER:-"allowed_address_pairs_driver"} OCTAVIA_COMPUTE_DRIVER=${OCTAVIA_COMPUTE_DRIVER:-"compute_nova_driver"} OCTAVIA_VOLUME_DRIVER=${OCTAVIA_VOLUME_DRIVER:-"volume_noop_driver"} +OCTAVIA_IMAGE_DRIVER=${OCTAVIA_IMAGE_DRIVER:-"image_glance_driver"} OCTAVIA_USERNAME=${OCTAVIA_ADMIN_USER:-"admin"} OCTAVIA_PASSWORD=${OCTAVIA_PASSWORD:-${ADMIN_PASSWORD}} diff --git a/etc/octavia.conf b/etc/octavia.conf index a9a27241eb..1eb862fbf7 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -331,6 +331,11 @@ # # volume_driver = volume_noop_driver # +# Image driver options are image_noop_driver +# image_glance_driver +# +# image_driver = image_glance_driver +# # Distributor driver options are distributor_noop_driver # single_VIP_amphora # diff --git a/octavia/common/config.py b/octavia/common/config.py index 747584a2ad..a2a05f63db 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -478,6 +478,10 @@ controller_worker_opts = [ default=constants.VOLUME_NOOP_DRIVER, choices=constants.SUPPORTED_VOLUME_DRIVERS, help=_('Name of the volume driver to use')), + cfg.StrOpt('image_driver', + default='image_glance_driver', + choices=constants.SUPPORTED_IMAGE_DRIVERS, + help=_('Name of the image driver to use')), cfg.StrOpt('distributor_driver', default='distributor_noop_driver', help=_('Name of the distributor driver to use')), diff --git a/octavia/common/constants.py b/octavia/common/constants.py index bdd05f6444..cbbf6411b9 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -811,6 +811,10 @@ L4_PROTOCOL_MAP = { PROTOCOL_UDP: PROTOCOL_UDP, } +# Image drivers +SUPPORTED_IMAGE_DRIVERS = ['image_noop_driver', + 'image_glance_driver'] + # Volume drivers VOLUME_NOOP_DRIVER = 'volume_noop_driver' SUPPORTED_VOLUME_DRIVERS = [VOLUME_NOOP_DRIVER, diff --git a/octavia/common/exceptions.py b/octavia/common/exceptions.py index 9eff1a2842..fca49bf300 100644 --- a/octavia/common/exceptions.py +++ b/octavia/common/exceptions.py @@ -232,8 +232,8 @@ class NoReadyAmphoraeException(OctaviaException): message = _('There are not any READY amphora available.') -class GlanceNoTaggedImages(OctaviaException): - message = _("No Glance images are tagged with %(tag)s tag.") +class ImageGetException(OctaviaException): + message = _('Failed to retrieve image with %(tag)s tag.') # This is an internal use exception for the taskflow work flow diff --git a/octavia/compute/drivers/nova_driver.py b/octavia/compute/drivers/nova_driver.py index f07d8d0178..dac9a28f10 100644 --- a/octavia/compute/drivers/nova_driver.py +++ b/octavia/compute/drivers/nova_driver.py @@ -31,32 +31,6 @@ LOG = logging.getLogger(__name__) CONF = cfg.CONF -def _extract_amp_image_id_by_tag(client, image_tag, image_owner): - if image_owner: - images = list(client.images.list( - filters={'tag': [image_tag], - 'owner': image_owner, - 'status': constants.GLANCE_IMAGE_ACTIVE}, - sort='created_at:desc', - limit=2)) - else: - images = list(client.images.list( - filters={'tag': [image_tag], - 'status': constants.GLANCE_IMAGE_ACTIVE}, - sort='created_at:desc', - limit=2)) - - if not images: - raise exceptions.GlanceNoTaggedImages(tag=image_tag) - image_id = images[0]['id'] - num_images = len(images) - if num_images > 1: - LOG.warning("A single Glance image should be tagged with %(tag)s tag, " - "but at least two were found. Using %(image_id)s.", - {'tag': image_tag, 'image_id': image_id}) - return image_id - - class VirtualMachineManager(compute_base.ComputeBase): '''Compute implementation of virtual machines via nova.''' @@ -69,13 +43,6 @@ class VirtualMachineManager(compute_base.ComputeBase): endpoint_type=CONF.nova.endpoint_type, insecure=CONF.nova.insecure, cacert=CONF.nova.ca_certificates_file) - self._glance_client = clients.GlanceAuth.get_glance_client( - service_name=CONF.glance.service_name, - endpoint=CONF.glance.endpoint, - region=CONF.glance.region_name, - endpoint_type=CONF.glance.endpoint_type, - insecure=CONF.glance.insecure, - cacert=CONF.glance.ca_certificates_file) self.manager = self._nova_client.servers self.server_groups = self._nova_client.server_groups self.flavor_manager = self._nova_client.flavors @@ -85,6 +52,11 @@ class VirtualMachineManager(compute_base.ComputeBase): name=CONF.controller_worker.volume_driver, invoke_on_load=True ).driver + self.image_driver = stevedore_driver.DriverManager( + namespace='octavia.image.drivers', + name=CONF.controller_worker.image_driver, + invoke_on_load=True + ).driver def build(self, name="amphora_name", amphora_flavor=None, image_tag=None, image_owner=None, key_name=None, sec_groups=None, @@ -132,8 +104,8 @@ class VirtualMachineManager(compute_base.ComputeBase): "group": server_group_id} az_name = availability_zone or CONF.nova.availability_zone - image_id = _extract_amp_image_id_by_tag( - self._glance_client, image_tag, image_owner) + image_id = self.image_driver.get_image_id_by_tag( + image_tag, image_owner) if CONF.nova.random_amphora_name_length: r = random.SystemRandom() diff --git a/octavia/image/__init__.py b/octavia/image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/image/drivers/__init__.py b/octavia/image/drivers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/image/drivers/glance_driver.py b/octavia/image/drivers/glance_driver.py new file mode 100644 index 0000000000..2b77ce30dc --- /dev/null +++ b/octavia/image/drivers/glance_driver.py @@ -0,0 +1,69 @@ +# Copyright 2020 Red Hat, Inc. All rights reserved. +# +# 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. + +from oslo_config import cfg +from oslo_log import log as logging + +from octavia.common import clients +from octavia.common import constants +from octavia.common import exceptions +from octavia.image import image_base + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + + +class ImageManager(image_base.ImageBase): + '''Image implementation of virtual machines via Glance.''' + + def __init__(self): + super().__init__() + # Must initialize glance api + self._glance_client = clients.GlanceAuth.get_glance_client( + service_name=CONF.glance.service_name, + endpoint=CONF.glance.endpoint, + region=CONF.glance.region_name, + endpoint_type=CONF.glance.endpoint_type, + insecure=CONF.glance.insecure, + cacert=CONF.glance.ca_certificates_file + ) + self.manager = self._glance_client.images + + def get_image_id_by_tag(self, image_tag, image_owner=None): + """Get image ID by image tag and owner + + :param image_tag: image tag + :param image_owner: optional image owner + :raises: ImageGetException if no images found with given tag + :return: image id + """ + filters = {'tag': [image_tag], + 'status': constants.GLANCE_IMAGE_ACTIVE} + if image_owner: + filters.update({'owner': image_owner}) + + images = list(self.manager.list( + filters=filters, sort='created_at:desc', limit=2)) + + if not images: + raise exceptions.ImageGetException(tag=image_tag) + image_id = images[0]['id'] + num_images = len(images) + if num_images > 1: + LOG.warning("A single Glance image should be tagged with %(tag)s " + "tag, but at least two were found. Using " + "%(image_id)s.", + {'tag': image_tag, 'image_id': image_id}) + return image_id diff --git a/octavia/image/drivers/noop_driver/__init__.py b/octavia/image/drivers/noop_driver/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/image/drivers/noop_driver/driver.py b/octavia/image/drivers/noop_driver/driver.py new file mode 100644 index 0000000000..10acf186ce --- /dev/null +++ b/octavia/image/drivers/noop_driver/driver.py @@ -0,0 +1,43 @@ +# Copyright 2020 Red Hat, Inc. All rights reserved. +# +# 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. + +from oslo_log import log as logging + +from octavia.image import image_base as driver_base + +LOG = logging.getLogger(__name__) + + +class NoopManager(object): + def __init__(self): + super().__init__() + self.imageconfig = {} + + def get_image_id_by_tag(self, image_tag, image_owner=None): + LOG.debug("Image %s no-op, get_image_id_by_tag image tag %s, " + "image owner %s", + self.__class__.__name__, image_tag, image_owner) + self.imageconfig[image_tag, image_owner] = ( + image_tag, image_owner, 'get_image_id_by_tag') + return 1 + + +class NoopImageDriver(driver_base.ImageBase): + def __init__(self): + super().__init__() + self.driver = NoopManager() + + def get_image_id_by_tag(self, image_tag, image_owner=None): + image_id = self.driver.get_image_id_by_tag(image_tag, image_owner) + return image_id diff --git a/octavia/image/image_base.py b/octavia/image/image_base.py new file mode 100644 index 0000000000..3bb0af0d1c --- /dev/null +++ b/octavia/image/image_base.py @@ -0,0 +1,28 @@ +# Copyright 2020 Red Hat, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc + + +class ImageBase(object, metaclass=abc.ABCMeta): + + @abc.abstractmethod + def get_image_id_by_tag(self, image_tag, image_owner=None): + """Get image ID by image tag and owner. + + :param image_tag: image tag + :param image_owner: optional image owner + :raises: ImageGetException if no images found with given tag + :return: image id + """ diff --git a/octavia/tests/unit/compute/drivers/test_nova_driver.py b/octavia/tests/unit/compute/drivers/test_nova_driver.py index 5f45d392f3..f1604ff770 100644 --- a/octavia/tests/unit/compute/drivers/test_nova_driver.py +++ b/octavia/tests/unit/compute/drivers/test_nova_driver.py @@ -18,7 +18,6 @@ from oslo_config import cfg from oslo_config import fixture as oslo_fixture from oslo_utils import uuidutils -from octavia.common import clients from octavia.common import constants from octavia.common import data_models as models from octavia.common import exceptions @@ -29,41 +28,6 @@ import octavia.tests.unit.base as base CONF = cfg.CONF -class Test_ExtractAmpImageIdByTag(base.TestCase): - - def setUp(self): - super(Test_ExtractAmpImageIdByTag, self).setUp() - client_mock = mock.patch.object(clients.GlanceAuth, - 'get_glance_client') - self.client = client_mock.start().return_value - - def test_no_images(self): - self.client.images.list.return_value = [] - self.assertRaises( - exceptions.GlanceNoTaggedImages, - nova_common._extract_amp_image_id_by_tag, self.client, - 'faketag', None) - - def test_single_image(self): - images = [ - {'id': uuidutils.generate_uuid(), 'tag': 'faketag'} - ] - self.client.images.list.return_value = images - image_id = nova_common._extract_amp_image_id_by_tag(self.client, - 'faketag', None) - self.assertIn(image_id, images[0]['id']) - - def test_multiple_images_returns_one_of_images(self): - images = [ - {'id': image_id, 'tag': 'faketag'} - for image_id in [uuidutils.generate_uuid() for i in range(10)] - ] - self.client.images.list.return_value = images - image_id = nova_common._extract_amp_image_id_by_tag(self.client, - 'faketag', None) - self.assertIn(image_id, [image['id'] for image in images]) - - class TestNovaClient(base.TestCase): def setUp(self): @@ -72,6 +36,8 @@ class TestNovaClient(base.TestCase): self.net_name = "lb-mgmt-net" conf.config(group="controller_worker", amp_boot_network_list=['1', '2']) + conf.config(group="controller_worker", + image_driver='image_noop_driver') self.conf = conf self.fake_image_uuid = uuidutils.generate_uuid() @@ -135,12 +101,6 @@ class TestNovaClient(base.TestCase): self.flavor_id = uuidutils.generate_uuid() self.availability_zone = 'my_test_az' - self.mock_image_tag = mock.patch( - 'octavia.compute.drivers.nova_driver.' - '_extract_amp_image_id_by_tag').start() - self.mock_image_tag.return_value = 1 - self.addCleanup(self.mock_image_tag.stop) - super().setUp() def test_build(self): diff --git a/octavia/tests/unit/image/__init__.py b/octavia/tests/unit/image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/tests/unit/image/drivers/__init__.py b/octavia/tests/unit/image/drivers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/tests/unit/image/drivers/test_glance_driver.py b/octavia/tests/unit/image/drivers/test_glance_driver.py new file mode 100644 index 0000000000..61defe2238 --- /dev/null +++ b/octavia/tests/unit/image/drivers/test_glance_driver.py @@ -0,0 +1,65 @@ +# Copyright 2020 Red Hat, Inc. All rights reserved. +# +# 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. + +from unittest import mock + +from oslo_utils import uuidutils + +from octavia.common import exceptions +import octavia.image.drivers.glance_driver as glance_common +import octavia.tests.unit.base as base + + +class TestGlanceClient(base.TestCase): + + def setUp(self): + self.manager = glance_common.ImageManager() + self.manager.manager = mock.MagicMock() + + super(TestGlanceClient, self).setUp() + + def test_no_images(self): + self.manager.manager.list.return_value = [] + self.assertRaises( + exceptions.ImageGetException, + self.manager.get_image_id_by_tag, 'faketag') + + def test_single_image(self): + images = [ + {'id': uuidutils.generate_uuid(), 'tag': 'faketag'} + ] + self.manager.manager.list.return_value = images + image_id = self.manager.get_image_id_by_tag('faketag', None) + self.assertEqual(image_id, images[0]['id']) + + def test_single_image_owner(self): + owner = uuidutils.generate_uuid() + images = [ + {'id': uuidutils.generate_uuid(), + 'tag': 'faketag', + 'owner': owner} + ] + self.manager.manager.list.return_value = images + image_id = self.manager.get_image_id_by_tag('faketag', owner) + self.assertEqual(image_id, images[0]['id']) + self.assertEqual(owner, images[0]['owner']) + + def test_multiple_images_returns_one_of_images(self): + images = [ + {'id': image_id, 'tag': 'faketag'} + for image_id in [uuidutils.generate_uuid() for i in range(10)] + ] + self.manager.manager.list.return_value = images + image_id = self.manager.get_image_id_by_tag('faketag', None) + self.assertIn(image_id, [image['id'] for image in images]) diff --git a/octavia/tests/unit/image/drivers/test_image_noop_driver.py b/octavia/tests/unit/image/drivers/test_image_noop_driver.py new file mode 100644 index 0000000000..470758ee21 --- /dev/null +++ b/octavia/tests/unit/image/drivers/test_image_noop_driver.py @@ -0,0 +1,39 @@ +# Copyright 2020 Red Hat, Inc. All rights reserved. +# +# 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. + +from oslo_config import cfg +from oslo_utils import uuidutils + +from octavia.image.drivers.noop_driver import driver +import octavia.tests.unit.base as base + + +CONF = cfg.CONF + + +class TestNoopImageDriver(base.TestCase): + + def setUp(self): + super(TestNoopImageDriver, self).setUp() + self.driver = driver.NoopImageDriver() + + def test_get_image_id_by_tag(self): + image_tag = 'amphora' + image_owner = uuidutils.generate_uuid() + image_id = self.driver.get_image_id_by_tag(image_tag, image_owner) + self.assertEqual((image_tag, image_owner, 'get_image_id_by_tag'), + self.driver.driver.imageconfig[( + image_tag, image_owner + )]) + self.assertEqual(1, image_id) diff --git a/releasenotes/notes/add-c9b9401b831efb25.yaml b/releasenotes/notes/add-c9b9401b831efb25.yaml new file mode 100644 index 0000000000..dfb2813cca --- /dev/null +++ b/releasenotes/notes/add-c9b9401b831efb25.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Introduced an image driver interface. Supported drivers are noop and + Glance. +upgrade: + - | + When the amphora provider driver is enabled, operators need to set option + ``[controller_worker]/image_driver``. The default image driver is + ``image_glance_driver``. For testing could be used ``image_noop_driver``. diff --git a/setup.cfg b/setup.cfg index bb82ecd2ee..21beb41696 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,6 +82,9 @@ octavia.network.drivers = octavia.volume.drivers = volume_noop_driver = octavia.volume.drivers.noop_driver.driver:NoopVolumeDriver volume_cinder_driver = octavia.volume.drivers.cinder_driver:VolumeManager +octavia.image.drivers = + image_noop_driver = octavia.image.drivers.noop_driver.driver:NoopImageDriver + image_glance_driver = octavia.image.drivers.glance_driver:ImageManager octavia.distributor.drivers = distributor_noop_driver = octavia.distributor.drivers.noop_driver.driver:NoopDistributorDriver single_VIP_amphora = octavia.distributor.drivers.single_VIP_amphora.driver:SingleVIPAmpDistributorDriver