Browse Source

Merge "Support create amphora instance from volume based."

tags/5.0.0.0rc1
Zuul 5 months ago
parent
commit
b7278ceab4
24 changed files with 682 additions and 6 deletions
  1. +1
    -0
      devstack/plugin.sh
  2. +1
    -0
      devstack/settings
  3. +42
    -0
      etc/octavia.conf
  4. +1
    -0
      lower-constraints.txt
  5. +42
    -0
      octavia/common/clients.py
  6. +37
    -0
      octavia/common/config.py
  7. +10
    -0
      octavia/common/constants.py
  8. +8
    -0
      octavia/common/exceptions.py
  9. +51
    -2
      octavia/compute/drivers/nova_driver.py
  10. +39
    -0
      octavia/tests/unit/common/test_clients.py
  11. +58
    -4
      octavia/tests/unit/compute/drivers/test_nova_driver.py
  12. +0
    -0
      octavia/tests/unit/volume/__init__.py
  13. +0
    -0
      octavia/tests/unit/volume/drivers/__init__.py
  14. +99
    -0
      octavia/tests/unit/volume/drivers/test_cinder_driver.py
  15. +46
    -0
      octavia/tests/unit/volume/drivers/test_volume_noop_driver.py
  16. +0
    -0
      octavia/volume/__init__.py
  17. +0
    -0
      octavia/volume/drivers/__init__.py
  18. +123
    -0
      octavia/volume/drivers/cinder_driver.py
  19. +0
    -0
      octavia/volume/drivers/noop_driver/__init__.py
  20. +60
    -0
      octavia/volume/drivers/noop_driver/driver.py
  21. +46
    -0
      octavia/volume/volume_base.py
  22. +14
    -0
      releasenotes/notes/volume-based-amphora-9a1899634f5244b0.yaml
  23. +1
    -0
      requirements.txt
  24. +3
    -0
      setup.cfg

+ 1
- 0
devstack/plugin.sh View File

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


+ 1
- 0
devstack/settings View File

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

+ 42
- 0
etc/octavia.conf View File

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

+ 1
- 0
lower-constraints.txt View File

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

+ 42
- 0
octavia/common/clients.py View File

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

+ 37
- 0
octavia/common/config.py View File

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

+ 10
- 0
octavia/common/constants.py View File

@@ -704,3 +704,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'

+ 8
- 0
octavia/common/exceptions.py View File

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

+ 51
- 2
octavia/compute/drivers/nova_driver.py View File

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

+ 39
- 0
octavia/tests/unit/common/test_clients.py View File

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

+ 58
- 4
octavia/tests/unit/compute/drivers/test_nova_driver.py View File

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


+ 0
- 0
octavia/tests/unit/volume/__init__.py View File


+ 0
- 0
octavia/tests/unit/volume/drivers/__init__.py View File


+ 99
- 0
octavia/tests/unit/volume/drivers/test_cinder_driver.py View File

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

+ 46
- 0
octavia/tests/unit/volume/drivers/test_volume_noop_driver.py View File

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

+ 0
- 0
octavia/volume/__init__.py View File


+ 0
- 0
octavia/volume/drivers/__init__.py View File


+ 123
- 0
octavia/volume/drivers/cinder_driver.py View File

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

+ 0
- 0
octavia/volume/drivers/noop_driver/__init__.py View File


+ 60
- 0
octavia/volume/drivers/noop_driver/driver.py View File

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

+ 46
- 0
octavia/volume/volume_base.py View File

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

+ 14
- 0
releasenotes/notes/volume-based-amphora-9a1899634f5244b0.yaml View File

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

+ 1
- 0
requirements.txt View File

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

+ 3
- 0
setup.cfg View File

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

Loading…
Cancel
Save