diff --git a/devstack/plugin.sh b/devstack/plugin.sh index e6657f4ce6..d14b05af39 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -271,6 +271,7 @@ function octavia_configure { # Setting other required default options iniset $OCTAVIA_CONF controller_worker amphora_driver ${OCTAVIA_AMPHORA_DRIVER} 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 amp_image_tag ${OCTAVIA_AMP_IMAGE_TAG} diff --git a/devstack/settings b/devstack/settings index 18a96bb78e..c5b7b703fb 100644 --- a/devstack/settings +++ b/devstack/settings @@ -20,6 +20,7 @@ OCTAVIA_RUN_DIR=${OCTAVIA_RUN_DIR:-"/var/run/octavia"} 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_USERNAME=${OCTAVIA_ADMIN_USER:-"admin"} OCTAVIA_PASSWORD=${OCTAVIA_PASSWORD:-${ADMIN_PASSWORD}} diff --git a/etc/octavia.conf b/etc/octavia.conf index 2df686683b..a27e9166d6 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -243,6 +243,10 @@ # allowed_address_pairs_driver # # network_driver = network_noop_driver +# Volume driver options are volume_noop_driver +# volume_cinder_driver +# +# volume_driver = volume_noop_driver # # Distributor driver options are distributor_noop_driver # single_VIP_amphora @@ -421,6 +425,44 @@ # Nova supports: anti-affinity and soft-anti-affinity # anti_affinity_policy = anti-affinity +[cinder] +# The name of the cinder service in the keystone catalog +# service_name = +# Custom cinder endpoint if override is necessary +# endpoint = + +# Region in Identity service catalog to use for communication with the +# OpenStack services. +# region_name = + +# Endpoint type in Identity service catalog to use for communication with +# the OpenStack services. +# endpoint_type = publicURL + +# Availability zone to use for creating Volume +# availability_zone = + +# CA certificates file to verify cinder connections when TLS is enabled +# insecure = False +# ca_certificates_file = + +# Size of root volume in GB for Amphora Instance when use Cinder +# In some storage backends such as ScaleIO, the size of volume is multiple of 8 +# volume_size = 16 + +# Volume type to be used for Amphora Instance root disk +# If not specified, default_volume_type from cinder.conf will be used +# volume_type = + +# Interval time to wait until volume becomes available +# volume_create_retry_interval = 5 + +# Timeout to wait for volume creation success +# volume_create_timeout = 300 + +# Maximum number of retries to create volume +# volume_create_max_retries = 5 + [glance] # The name of the glance service in the keystone catalog # service_name = diff --git a/lower-constraints.txt b/lower-constraints.txt index 8625e4a2d4..0671be566c 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -172,3 +172,4 @@ WebTest==2.0.29 Werkzeug==0.14.1 wrapt==1.10.11 WSME==0.8.0 +python-cinderclient==3.3.0 diff --git a/octavia/common/clients.py b/octavia/common/clients.py index f573dcd4ec..1b1277ca32 100644 --- a/octavia/common/clients.py +++ b/octavia/common/clients.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from cinderclient import client as cinder_client from glanceclient import client as glance_client from neutronclient.neutron import client as neutron_client from novaclient import api_versions @@ -26,6 +27,7 @@ CONF = cfg.CONF GLANCE_VERSION = '2' NEUTRON_VERSION = '2.0' NOVA_VERSION = '2.15' +CINDER_VERSION = '3' class NovaAuth(object): @@ -143,3 +145,43 @@ class GlanceAuth(object): with excutils.save_and_reraise_exception(): LOG.exception("Error creating Glance client.") return cls.glance_client + + +class CinderAuth(object): + cinder_client = None + + @classmethod + def get_cinder_client(cls, region, service_name=None, endpoint=None, + endpoint_type='publicURL', insecure=False, + cacert=None): + """Create cinder client object. + + :param region: The region of the service + :param service_name: The name of the cinder service in the catalog + :param endpoint: The endpoint of the service + :param endpoint_type: The endpoint type of the service + :param insecure: Turn off certificate validation + :param cacert: CA Cert file path + :return: a Cinder Client object + :raise Exception: if the client cannot be created + """ + ksession = keystone.KeystoneSession() + if not cls.cinder_client: + kwargs = {'region_name': region, + 'session': ksession.get_session(), + 'interface': endpoint_type} + if service_name: + kwargs['service_name'] = service_name + if endpoint: + kwargs['endpoint'] = endpoint + if endpoint.startwith("https"): + kwargs['insecure'] = insecure + kwargs['cacert'] = cacert + try: + cls.cinder_client = cinder_client.Client( + CINDER_VERSION, **kwargs + ) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception("Error creating Cinder client.") + return cls.cinder_client diff --git a/octavia/common/config.py b/octavia/common/config.py index 8e14171a59..946896af34 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -410,6 +410,10 @@ controller_worker_opts = [ cfg.StrOpt('network_driver', default='network_noop_driver', help=_('Name of the network driver to use')), + cfg.StrOpt('volume_driver', + default=constants.VOLUME_NOOP_DRIVER, + choices=constants.SUPPORTED_VOLUME_DRIVERS, + help=_('Name of the volume driver to use')), cfg.StrOpt('distributor_driver', default='distributor_noop_driver', help=_('Name of the distributor driver to use')), @@ -560,6 +564,38 @@ nova_opts = [ cfg.StrOpt('availability_zone', default=None, help=_('Availability zone to use for creating Amphorae')), ] + +cinder_opts = [ + cfg.StrOpt('service_name', + help=_('The name of the cinder service in the keystone ' + 'catalog')), + cfg.StrOpt('endpoint', help=_('A new endpoint to override the endpoint ' + 'in the keystone catalog.')), + cfg.StrOpt('region_name', + help=_('Region in Identity service catalog to use for ' + 'communication with the OpenStack services.')), + cfg.StrOpt('endpoint_type', default='publicURL', + help=_('Endpoint interface in identity service to use')), + cfg.StrOpt('ca_certificates_file', + help=_('CA certificates file path')), + cfg.StrOpt('availability_zone', default=None, + help=_('Availability zone to use for creating Volume')), + cfg.BoolOpt('insecure', + default=False, + help=_('Disable certificate validation on SSL connections')), + cfg.IntOpt('volume_size', default=16, + help=_('Size of volume for Amphora instance')), + cfg.StrOpt('volume_type', default=None, + help=_('Type of volume for Amphorae volume root disk')), + cfg.IntOpt('volume_create_retry_interval', default=5, + help=_('Interval time to wait volume is created in available' + 'state')), + cfg.IntOpt('volume_create_timeout', default=300, + help=_('Timeout to wait for volume creation success')), + cfg.IntOpt('volume_create_max_retries', default=5, + help=_('Maximum number of retries to create volume')) +] + neutron_opts = [ cfg.StrOpt('service_name', help=_('The name of the neutron service in the ' @@ -685,6 +721,7 @@ cfg.CONF.register_cli_opts(core_cli_opts) cfg.CONF.register_opts(certificate_opts, group='certificates') cfg.CONF.register_cli_opts(healthmanager_opts, group='health_manager') cfg.CONF.register_opts(nova_opts, group='nova') +cfg.CONF.register_opts(cinder_opts, group='cinder') cfg.CONF.register_opts(glance_opts, group='glance') cfg.CONF.register_opts(neutron_opts, group='neutron') cfg.CONF.register_opts(quota_opts, group='quotas') diff --git a/octavia/common/constants.py b/octavia/common/constants.py index ff493c23ad..fa47bc13db 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -687,3 +687,13 @@ L4_PROTOCOL_MAP = { PROTOCOL_PROXY: PROTOCOL_TCP, PROTOCOL_UDP: PROTOCOL_UDP, } + +# Volume drivers +VOLUME_NOOP_DRIVER = 'volume_noop_driver' +SUPPORTED_VOLUME_DRIVERS = [VOLUME_NOOP_DRIVER, + 'volume_cinder_driver'] + +# Cinder volume driver constants +CINDER_STATUS_AVAILABLE = 'available' +CINDER_STATUS_ERROR = 'error' +CINDER_ACTION_CREATE_VOLUME = 'create volume' diff --git a/octavia/common/exceptions.py b/octavia/common/exceptions.py index 647bff73f6..a806171431 100644 --- a/octavia/common/exceptions.py +++ b/octavia/common/exceptions.py @@ -383,3 +383,11 @@ class ObjectInUse(APIException): class ProviderFlavorMismatchError(APIException): msg = _("Flavor '%(flav)s' is not compatible with provider '%(prov)s'") code = 400 + + +class VolumeDeleteException(OctaviaException): + message = _('Failed to delete volume instance.') + + +class VolumeGetException(OctaviaException): + message = _('Failed to retrieve volume instance.') diff --git a/octavia/compute/drivers/nova_driver.py b/octavia/compute/drivers/nova_driver.py index 5b422935b1..431cd3d8b1 100644 --- a/octavia/compute/drivers/nova_driver.py +++ b/octavia/compute/drivers/nova_driver.py @@ -18,6 +18,7 @@ import string from novaclient import exceptions as nova_exceptions from oslo_config import cfg from oslo_log import log as logging +from stevedore import driver as stevedore_driver from octavia.common import clients from octavia.common import constants @@ -88,6 +89,11 @@ class VirtualMachineManager(compute_base.ComputeBase): self.manager = self._nova_client.servers self.server_groups = self._nova_client.server_groups self.flavor_manager = self._nova_client.flavors + self.volume_driver = stevedore_driver.DriverManager( + namespace='octavia.volume.drivers', + name=CONF.controller_worker.volume_driver, + invoke_on_load=True + ).driver def build(self, name="amphora_name", amphora_flavor=None, image_id=None, image_tag=None, image_owner=None, @@ -122,6 +128,7 @@ class VirtualMachineManager(compute_base.ComputeBase): ''' + volume_id = None try: network_ids = network_ids or [] port_ids = port_ids or [] @@ -143,9 +150,25 @@ class VirtualMachineManager(compute_base.ComputeBase): [r.choice(string.ascii_uppercase + string.digits) for i in range(CONF.nova.random_amphora_name_length - 1)] )) - + block_device_mapping = {} + if CONF.controller_worker.volume_driver != \ + constants.VOLUME_NOOP_DRIVER: + # creating volume + LOG.debug('Creating volume for amphora from image %s', + image_id) + volume_id = self.volume_driver.create_volume_from_image( + image_id) + LOG.debug('Created boot volume %s for amphora', volume_id) + # If use volume based, does not require image ID anymore + image_id = None + # Boot from volume with parameters: target device name = vda, + # device id = volume_id, device type and size unspecified, + # delete-on-terminate = true (volume will be deleted by Nova + # on instance termination) + block_device_mapping = {'vda': '%s:::true' % volume_id} amphora = self.manager.create( name=name, image=image_id, flavor=amphora_flavor, + block_device_mapping=block_device_mapping, key_name=key_name, security_groups=sec_groups, nics=nics, files=config_drive_files, @@ -157,6 +180,9 @@ class VirtualMachineManager(compute_base.ComputeBase): return amphora.id except Exception as e: + if CONF.controller_worker.volume_driver != \ + constants.VOLUME_NOOP_DRIVER: + self.volume_driver.delete_volume(volume_id) LOG.exception("Nova failed to build the instance due to: %s", e) raise exceptions.ComputeBuildException(fault=e) @@ -216,6 +242,7 @@ class VirtualMachineManager(compute_base.ComputeBase): lb_network_ip = None availability_zone = None + image_id = None fault = None try: @@ -242,12 +269,34 @@ class VirtualMachineManager(compute_base.ComputeBase): 'os-interfaces extension failed.') fault = getattr(nova_response, 'fault', None) + if CONF.controller_worker.volume_driver == \ + constants.VOLUME_NOOP_DRIVER: + image_id = nova_response.image.get("id") + else: + try: + volumes = self._nova_client.volumes.get_server_volumes( + nova_response.id) + except Exception: + LOG.debug('Extracting volumes through nova ' + 'os-volumes extension failed.') + volumes = [] + if not volumes: + LOG.warning('Boot volume not found for volume backed ' + 'amphora instance %s ', nova_response.id) + else: + if len(volumes) > 1: + LOG.warning('Found more than one (%s) volumes ' + 'for amphora instance %s', + len(volumes), nova_response.id) + volume_id = volumes[0].volumeId + image_id = self.volume_driver.get_image_from_volume(volume_id) + response = models.Amphora( compute_id=nova_response.id, status=nova_response.status, lb_network_ip=lb_network_ip, cached_zone=availability_zone, - image_id=nova_response.image.get("id"), + image_id=image_id, compute_flavor=nova_response.flavor.get("id") ) return response, fault diff --git a/octavia/tests/unit/common/test_clients.py b/octavia/tests/unit/common/test_clients.py index a2de898d66..b7bd80518d 100644 --- a/octavia/tests/unit/common/test_clients.py +++ b/octavia/tests/unit/common/test_clients.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import cinderclient.v3 import glanceclient.v2 import mock import neutronclient.v2_0 @@ -135,3 +136,41 @@ class TestGlanceAuth(base.TestCase): region="test-region", service_name="glanceEndpoint1", endpoint="test-endpoint", endpoint_type='publicURL', insecure=True) self.assertIs(bc1, bc2) + + +class TestCinderAuth(base.TestCase): + + def setUp(self): + # Reset the session and client + clients.CinderAuth.cinder_client = None + keystone._SESSION = None + + super(TestCinderAuth, self).setUp() + + @mock.patch('keystoneauth1.session.Session', mock.Mock()) + def test_get_cinder_client(self): + # There should be no existing client + self.assertIsNone( + clients.CinderAuth.cinder_client + ) + + # Mock out the keystone session and get the client + keystone._SESSION = mock.MagicMock() + bc1 = clients.CinderAuth.get_cinder_client( + region=None, endpoint_type='publicURL', insecure=True) + + # Our returned client should also be the saved client + self.assertIsInstance( + clients.CinderAuth.cinder_client, + cinderclient.v3.client.Client + ) + self.assertIs( + clients.CinderAuth.cinder_client, + bc1 + ) + + # Getting the session again should return the same object + bc2 = clients.CinderAuth.get_cinder_client( + region="test-region", service_name="cinderEndpoint1", + endpoint="test-endpoint", endpoint_type='publicURL', insecure=True) + self.assertIs(bc1, bc2) diff --git a/octavia/tests/unit/compute/drivers/test_nova_driver.py b/octavia/tests/unit/compute/drivers/test_nova_driver.py index 0f331e6329..995b25ce36 100644 --- a/octavia/tests/unit/compute/drivers/test_nova_driver.py +++ b/octavia/tests/unit/compute/drivers/test_nova_driver.py @@ -96,12 +96,13 @@ class TestNovaClient(base.TestCase): conf.config(group="controller_worker", amp_boot_network_list=['1', '2']) self.conf = conf + self.fake_image_uuid = uuidutils.generate_uuid() self.amphora = models.Amphora( compute_id=uuidutils.generate_uuid(), status='ACTIVE', lb_network_ip='10.0.0.1', - image_id=uuidutils.generate_uuid(), + image_id=self.fake_image_uuid, compute_flavor=uuidutils.generate_uuid() ) @@ -148,6 +149,9 @@ class TestNovaClient(base.TestCase): self.server_group_mock.policy = self.server_group_policy self.server_group_mock.id = self.server_group_id + self.volume_mock = mock.MagicMock() + setattr(self.volume_mock, 'volumeId', '1') + self.port_id = uuidutils.generate_uuid() self.compute_id = uuidutils.generate_uuid() self.network_id = uuidutils.generate_uuid() @@ -177,7 +181,39 @@ class TestNovaClient(base.TestCase): userdata='Blah', config_drive=True, scheduler_hints=None, - availability_zone=None + availability_zone=None, + block_device_mapping={} + ) + + @mock.patch('stevedore.driver.DriverManager.driver') + def test_build_with_cinder_volume(self, mock_driver): + self.conf.config(group="controller_worker", + volume_driver='volume_cinder_driver') + self.manager.volume_driver = mock_driver + mock_driver.create_volume_from_image.return_value = 1 + amphora_id = self.manager.build(amphora_flavor=1, image_id=1, + key_name=1, + sec_groups=1, + network_ids=[1], + port_ids=[2], + user_data='Blah', + config_drive_files='Files Blah') + + self.assertEqual(self.amphora.compute_id, amphora_id) + mock_driver.create_volume_from_image.assert_called_with(1) + self.manager.manager.create.assert_called_with( + name="amphora_name", + nics=[{'net-id': 1}, {'port-id': 2}], + image=None, + flavor=1, + key_name=1, + security_groups=1, + files='Files Blah', + userdata='Blah', + config_drive=True, + scheduler_hints=None, + availability_zone=None, + block_device_mapping={'vda': '1:::true'} ) def test_build_with_availability_zone(self): @@ -205,7 +241,8 @@ class TestNovaClient(base.TestCase): userdata='Blah', config_drive=True, scheduler_hints=None, - availability_zone=FAKE_AZ + availability_zone=FAKE_AZ, + block_device_mapping={} ) def test_build_with_random_amphora_name_length(self): @@ -241,7 +278,8 @@ class TestNovaClient(base.TestCase): userdata='Blah', config_drive=True, scheduler_hints=None, - availability_zone=None + availability_zone=None, + block_device_mapping={} ) def test_bad_build(self): @@ -312,6 +350,22 @@ class TestNovaClient(base.TestCase): self.assertIsNone(amphora.lb_network_ip) self.nova_response.interface_list.called_with() + @mock.patch('stevedore.driver.DriverManager.driver') + def test_translate_amphora_use_cinder(self, mock_driver): + self.conf.config(group="controller_worker", + volume_driver='volume_cinder_driver') + volumes_manager = self.manager._nova_client.volumes + volumes_manager.get_server_volumes.return_value = [self.volume_mock] + self.manager.volume_driver = mock_driver + mock_driver.get_image_from_volume.return_value = self.fake_image_uuid + amphora, fault = self.manager._translate_amphora(self.nova_response) + self.assertEqual(self.amphora, amphora) + self.assertEqual(self.nova_response.fault, fault) + self.nova_response.interface_list.called_with() + volumes_manager.get_server_volumes.assert_called_with( + self.nova_response.id) + mock_driver.get_image_from_volume.assert_called_with('1') + def test_create_server_group(self): self.manager.server_groups.create.return_value = self.server_group_mock diff --git a/octavia/tests/unit/volume/__init__.py b/octavia/tests/unit/volume/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/tests/unit/volume/drivers/__init__.py b/octavia/tests/unit/volume/drivers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/tests/unit/volume/drivers/test_cinder_driver.py b/octavia/tests/unit/volume/drivers/test_cinder_driver.py new file mode 100644 index 0000000000..a303d63610 --- /dev/null +++ b/octavia/tests/unit/volume/drivers/test_cinder_driver.py @@ -0,0 +1,99 @@ +# 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 cinderclient import exceptions as cinder_exceptions +import mock +from oslo_config import cfg +from oslo_config import fixture as oslo_fixture +from oslo_utils import uuidutils + +from octavia.common import exceptions +import octavia.tests.unit.base as base +import octavia.volume.drivers.cinder_driver as cinder_common + + +CONF = cfg.CONF + + +class TestCinderClient(base.TestCase): + + def setUp(self): + fake_uuid1 = uuidutils.generate_uuid() + fake_uuid2 = uuidutils.generate_uuid() + fake_uuid3 = uuidutils.generate_uuid() + + conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) + self.conf = conf + + self.manager = cinder_common.VolumeManager() + self.manager.manager = mock.MagicMock() + + self.cinder_response = mock.Mock() + self.cinder_response.id = fake_uuid1 + + self.manager.manager.get.return_value.status = 'available' + self.manager.manager.create.return_value = self.cinder_response + self.image_id = fake_uuid2 + self.volume_id = fake_uuid3 + + super(TestCinderClient, self).setUp() + + def test_create_volume_from_image(self): + self.conf.config(group="controller_worker", + volume_driver='volume_cinder_driver') + self.conf.config(group="cinder", volume_create_retry_interval=0) + self.manager.create_volume_from_image(self.image_id) + self.manager.manager.create.assert_called_with( + size=16, + volume_type=None, + availability_zone=None, + imageRef=self.image_id) + + def test_create_volume_from_image_error(self): + self.conf.config(group="controller_worker", + volume_driver='volume_cinder_driver') + self.conf.config(group="cinder", volume_create_retry_interval=0) + self.manager.manager.get.return_value.status = 'error' + self.assertRaises(cinder_exceptions.ResourceInErrorState, + self.manager.create_volume_from_image, + self.image_id) + + def test_build_cinder_volume_timeout(self): + self.conf.config(group="controller_worker", + volume_driver='volume_cinder_driver') + self.conf.config(group="cinder", volume_create_timeout=0) + self.conf.config(group="cinder", volume_create_retry_interval=0) + self.manager.manager.get.return_value.status = 'build' + self.manager.create_volume_from_image.retry.sleep = mock.Mock() + self.assertRaises(cinder_exceptions.TimeoutException, + self.manager.create_volume_from_image, + self.image_id) + + def test_get_image_from_volume(self): + self.conf.config(group="controller_worker", + volume_driver='volume_cinder_driver') + self.conf.config(group="cinder", + volume_create_retry_interval=0) + self.manager.get_image_from_volume(self.volume_id) + self.manager.manager.get.assert_called_with( + self.volume_id) + + def test_get_image_from_volume_error(self): + self.conf.config(group="controller_worker", + volume_driver='volume_cinder_driver') + self.conf.config(group="cinder", + volume_create_retry_interval=0) + self.manager.manager.get.side_effect = [ + exceptions.VolumeGetException('test_exception')] + self.assertRaises(exceptions.VolumeGetException, + self.manager.get_image_from_volume, + self.volume_id) diff --git a/octavia/tests/unit/volume/drivers/test_volume_noop_driver.py b/octavia/tests/unit/volume/drivers/test_volume_noop_driver.py new file mode 100644 index 0000000000..3954a8bf7b --- /dev/null +++ b/octavia/tests/unit/volume/drivers/test_volume_noop_driver.py @@ -0,0 +1,46 @@ +# 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 + +import octavia.tests.unit.base as base +from octavia.volume.drivers.noop_driver import driver + + +CONF = cfg.CONF + + +class TestNoopVolumeDriver(base.TestCase): + FAKE_UUID_1 = uuidutils.generate_uuid() + FAKE_UUID_2 = uuidutils.generate_uuid() + + def setUp(self): + super(TestNoopVolumeDriver, self).setUp() + self.driver = driver.NoopVolumeDriver() + + self.image_id = self.FAKE_UUID_1 + self.volume_id = self.FAKE_UUID_2 + + def test_create_volume_from_image(self): + self.driver.create_volume_from_image(self.image_id) + self.assertEqual((self.image_id, 'create_volume_from_image'), + self.driver.driver.volumeconfig[( + self.image_id + )]) + + def test_get_image_from_volume(self): + self.driver.get_image_from_volume(self.volume_id) + self.assertEqual((self.volume_id, 'get_image_from_volume'), + self.driver.driver.volumeconfig[( + self.volume_id + )]) diff --git a/octavia/volume/__init__.py b/octavia/volume/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/volume/drivers/__init__.py b/octavia/volume/drivers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/volume/drivers/cinder_driver.py b/octavia/volume/drivers/cinder_driver.py new file mode 100644 index 0000000000..2b119d2193 --- /dev/null +++ b/octavia/volume/drivers/cinder_driver.py @@ -0,0 +1,123 @@ +# 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 time + +from cinderclient import exceptions as cinder_exceptions +from oslo_config import cfg +from oslo_log import log as logging +from tenacity import retry +from tenacity import stop_after_attempt + +from octavia.common import clients +from octavia.common import constants +from octavia.common import exceptions +from octavia.volume import volume_base + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + + +class VolumeManager(volume_base.VolumeBase): + '''Volume implementation of virtual machines via cinder.''' + + def __init__(self): + super(VolumeManager, self).__init__() + # Must initialize cinder api + self._cinder_client = clients.CinderAuth.get_cinder_client( + service_name=CONF.cinder.service_name, + endpoint=CONF.cinder.endpoint, + region=CONF.cinder.region_name, + endpoint_type=CONF.cinder.endpoint_type, + insecure=CONF.cinder.insecure, + cacert=CONF.cinder.ca_certificates_file + ) + self.manager = self._cinder_client.volumes + + @retry(reraise=True, + stop=stop_after_attempt(CONF.cinder.volume_create_max_retries)) + def create_volume_from_image(self, image_id): + """Create cinder volume + + :param image_id: ID of amphora image + + :return volume id + """ + volume = self.manager.create( + size=CONF.cinder.volume_size, + volume_type=CONF.cinder.volume_type, + availability_zone=CONF.cinder.availability_zone, + imageRef=image_id) + resource_status = self.manager.get(volume.id).status + + status = constants.CINDER_STATUS_AVAILABLE + start = int(time.time()) + + while resource_status != status: + time.sleep(CONF.cinder.volume_create_retry_interval) + instance_volume = self.manager.get(volume.id) + resource_status = instance_volume.status + if resource_status == constants.CINDER_STATUS_ERROR: + LOG.error('Error creating %s', instance_volume.id) + instance_volume.delete() + raise cinder_exceptions.ResourceInErrorState( + obj=volume, fault_msg='Cannot create volume') + if int(time.time()) - start >= CONF.cinder.volume_create_timeout: + LOG.error('Timed out waiting to create cinder volume %s', + instance_volume.id) + instance_volume.delete() + raise cinder_exceptions.TimeoutException( + obj=volume, action=constants.CINDER_ACTION_CREATE_VOLUME) + return volume.id + + def delete_volume(self, volume_id): + """Get glance image from volume + + :param volume_id: ID of amphora boot volume + + :return image id + """ + LOG.debug('Deleting cinder volume %s', volume_id) + try: + instance_volume = self.manager.get(volume_id) + try: + instance_volume.delete() + LOG.debug("Deleted volume %s", volume_id) + except Exception: + LOG.exception("Error deleting cinder volume %s", + volume_id) + raise exceptions.VolumeDeleteException() + except cinder_exceptions.NotFound: + LOG.warning("Volume %s not found: assuming already deleted", + volume_id) + + def get_image_from_volume(self, volume_id): + """Get glance image from volume + + :param volume_id: ID of amphora boot volume + + :return image id + """ + image_id = None + LOG.debug('Get glance image for volume %s', volume_id) + try: + instance_volume = self.manager.get(volume_id) + except cinder_exceptions.NotFound: + LOG.exception("Volume %s not found", volume_id) + raise exceptions.VolumeGetException() + if hasattr(instance_volume, 'volume_image_metadata'): + image_id = instance_volume.volume_image_metadata.get("image_id") + else: + LOG.error("Volume %s has no image metadata", volume_id) + image_id = None + return image_id diff --git a/octavia/volume/drivers/noop_driver/__init__.py b/octavia/volume/drivers/noop_driver/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/octavia/volume/drivers/noop_driver/driver.py b/octavia/volume/drivers/noop_driver/driver.py new file mode 100644 index 0000000000..5008f6d06e --- /dev/null +++ b/octavia/volume/drivers/noop_driver/driver.py @@ -0,0 +1,60 @@ +# 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 oslo_utils import uuidutils + +from octavia.volume import volume_base as driver_base + +LOG = logging.getLogger(__name__) + + +class NoopManager(object): + def __init__(self): + super(NoopManager, self).__init__() + self.volumeconfig = {} + + def create_volume_from_image(self, image_id): + LOG.debug("Volume %s no-op, image id %s", + self.__class__.__name__, image_id) + self.volumeconfig[image_id] = (image_id, 'create_volume_from_image') + volume_id = uuidutils.generate_uuid() + return volume_id + + def delete_volume(self, volume_id): + LOG.debug("Volume %s no-op, volume id %s", + self.__class__.__name__, volume_id) + self.volumeconfig[volume_id] = (volume_id, 'delete') + + def get_image_from_volume(self, volume_id): + LOG.debug("Volume %s no-op, volume id %s", + self.__class__.__name__, volume_id) + self.volumeconfig[volume_id] = (volume_id, 'get_image_from_volume') + image_id = uuidutils.generate_uuid() + return image_id + + +class NoopVolumeDriver(driver_base.VolumeBase): + def __init__(self): + super(NoopVolumeDriver, self).__init__() + self.driver = NoopManager() + + def create_volume_from_image(self, image_id): + volume_id = self.driver.create_volume_from_image(image_id) + return volume_id + + def delete_volume(self, volume_id): + self.driver.delete_volume(volume_id) + + def get_image_from_volume(self, volume_id): + image_id = self.driver.get_image_from_volume(volume_id) + return image_id diff --git a/octavia/volume/volume_base.py b/octavia/volume/volume_base.py new file mode 100644 index 0000000000..d6717b5bc6 --- /dev/null +++ b/octavia/volume/volume_base.py @@ -0,0 +1,46 @@ +# Copyright 2011-2019 OpenStack Foundation +# +# 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 + +import six + + +@six.add_metaclass(abc.ABCMeta) +class VolumeBase(object): + + @abc.abstractmethod + def create_volume_from_image(self, image_id): + """Create volume for instance + + :param image_id: ID of amphora image + + :return volume id + """ + + @abc.abstractmethod + def delete_volume(self, volume_id): + """Delete volume + + :param volume_id: ID of amphora volume + """ + + @abc.abstractmethod + def get_image_from_volume(self, volume_id): + """Get cinder volume + + :param volume_id: ID of amphora volume + + :return image id + """ diff --git a/releasenotes/notes/volume-based-amphora-9a1899634f5244b0.yaml b/releasenotes/notes/volume-based-amphora-9a1899634f5244b0.yaml new file mode 100644 index 0000000000..d5037ee342 --- /dev/null +++ b/releasenotes/notes/volume-based-amphora-9a1899634f5244b0.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Allow creation of volume based amphora. + Many deploy production use volume based instances because of more flexibility. + Octavia will create volume and attach this to the amphora. + + Have new settings: + * `volume_driver`: Whether to use volume driver (cinder) to create volume backed amphorae. + * `volume_size`: Size of root volume for Amphora Instance when using Cinder + * `volume_type` : Type of volume for Amphorae volume root disk + * `volume_create_retry_interval`: Interval time to wait volume is created in available state + * `volume_create_timeout`: Timeout When volume is not create success + * `volume_create_max_retries`: Maximum number of retries to create volume diff --git a/requirements.txt b/requirements.txt index adcb5c6f34..045ddb92cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,6 +34,7 @@ PyMySQL>=0.7.6 # MIT License python-barbicanclient>=4.5.2 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-novaclient>=9.1.0 # Apache-2.0 +python-cinderclient>=3.3.0 # Apache-2.0 pyOpenSSL>=17.1.0 # Apache-2.0 WSME>=0.8.0 # MIT Jinja2>=2.10 # BSD License (3 clause) diff --git a/setup.cfg b/setup.cfg index 13012d78a0..88dcde050c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -79,6 +79,9 @@ octavia.network.drivers = network_noop_driver = octavia.network.drivers.noop_driver.driver:NoopNetworkDriver allowed_address_pairs_driver = octavia.network.drivers.neutron.allowed_address_pairs:AllowedAddressPairsDriver containers_driver = octavia.network.drivers.neutron.containers:ContainersDriver +octavia.volume.drivers = + volume_noop_driver = octavia.volume.drivers.noop_driver.driver:NoopVolumeDriver + volume_cinder_driver = octavia.volume.drivers.cinder_driver:VolumeManager octavia.distributor.drivers = distributor_noop_driver = octavia.distributor.drivers.noop_driver.driver:NoopDistributorDriver single_VIP_amphora = octavia.distributor.drivers.single_VIP_amphora.driver:SingleVIPAmpDistributorDriver