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
This commit is contained in:
Carlos Goncalves 2020-06-25 15:20:09 +02:00 committed by Michael Johnson
parent 4a4a2344de
commit a422e5a203
20 changed files with 283 additions and 79 deletions

View File

@ -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

View File

@ -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}}

View File

@ -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
#

View File

@ -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')),

View File

@ -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,

View File

@ -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

View File

@ -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()

View File

View File

View File

@ -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

View File

@ -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

View File

@ -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
"""

View File

@ -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):

View File

View File

@ -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])

View File

@ -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)

View File

@ -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``.

View File

@ -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