Add volume zone to availability zone profile

-- Add volume_zone option to zone profile for amphora driver

Closes-Bug: #2087794
Change-Id: I12e2118df5e31ec62e71b8650fcefe4dd9234751
Signed-off-by: Evgeniy Bykov <chypakabre@gmail.com>
This commit is contained in:
Evgeniy Bykov
2024-11-01 16:56:37 +03:00
committed by Tom Weininger
parent fd071d3391
commit 0640ba5837
26 changed files with 241 additions and 38 deletions

View File

@@ -3,6 +3,6 @@
{
"name": "some_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az1\"}"
"availability_zone_data": "{\"compute_zone\": \"az1\", \"volume_zone\": \"az1\"}"
}
}

View File

@@ -4,6 +4,6 @@
"id": "5712097e-0092-45dc-bff0-ab68b61ad51a",
"name": "some_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az1\"}"
"availability_zone_data": "{\"compute_zone\": \"az1\", \"volume_zone\": \"az1\"}"
}
}

View File

@@ -4,7 +4,7 @@
"id": "5712097e-0092-45dc-bff0-ab68b61ad51a",
"name": "some_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az1\"}"
"availability_zone_data": "{\"compute_zone\": \"az1\", \"volume_zone\": \"az2\"}"
}
]
}

View File

@@ -4,6 +4,6 @@
"id": "5712097e-0092-45dc-bff0-ab68b61ad51a",
"name": "some_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az1\"}"
"availability_zone_data": "{\"compute_zone\": \"az1\", \"volume_zone\": \"az1\"}"
}
}

View File

@@ -3,6 +3,6 @@
{
"name": "other_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az2\"}"
"availability_zone_data": "{\"compute_zone\": \"az2\", \"volume_zone\": \"az2\"}"
}
}

View File

@@ -4,6 +4,6 @@
"id": "5712097e-0092-45dc-bff0-ab68b61ad51a",
"name": "other_az",
"provider_name": "amphora",
"availability_zone_data": "{\"compute_zone\": \"az2\"}"
"availability_zone_data": "{\"compute_zone\": \"az2\", \"volume_zone\": \"az2\"}"
}
}

View File

@@ -3,6 +3,10 @@
{
"name": "compute_zone",
"description": "The compute availability zone."
},
{
"name": "volume_zone",
"description": "The volume availability zone."
}
]
}

View File

@@ -1770,6 +1770,7 @@ description. For example:
.. code-block:: python
{"compute_zone": "The compute availability zone to use for this loadbalancer.",
"volume_zone": "The volume availability zone to use for this loadbalancer.",
"management_network": "The management network ID for the loadbalancer.",
"valid_vip_networks": "List of network IDs that are allowed for VIP use. This overrides/replaces the list of allowed networks configured in `octavia.conf`."}

View File

@@ -39,6 +39,10 @@ SUPPORTED_AVAILABILITY_ZONE_SCHEMA = {
"type": "string",
"description": "The compute availability zone."
},
consts.VOLUME_ZONE: {
"type": "string",
"description": "The volume availability zone."
},
consts.MANAGEMENT_NETWORK: {
"type": "string",
"description": "The management network ID for the amphora."

View File

@@ -582,6 +582,18 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
# when the octavia-lib supports it.
compute_driver.validate_availability_zone(compute_zone)
volume_zone = availability_zone_dict.get(consts.VOLUME_ZONE, None)
if volume_zone:
volume_driver = stevedore_driver.DriverManager(
namespace='octavia.volume.drivers',
name=CONF.controller_worker.volume_driver,
invoke_on_load=True
).driver
# TODO(johnsom) Fix this to raise a NotFound error
# when the octavia-lib supports it.
volume_driver.validate_availability_zone(volume_zone)
check_nets = availability_zone_dict.get(
consts.VALID_VIP_NETWORKS, [])
management_net = availability_zone_dict.get(

View File

@@ -259,6 +259,8 @@ class NoopManager:
self.__class__.__name__)
return {"compute_zone": "The compute availability zone to use for "
"this loadbalancer.",
"volume_zone": "The volume availability zone to use for "
"this loadbalancer."}
def validate_availability_zone(self, availability_zone_metadata):

View File

@@ -332,6 +332,7 @@ CODE = 'code'
COMPUTE_ID = 'compute_id'
COMPUTE_OBJ = 'compute_obj'
COMPUTE_ZONE = 'compute_zone'
VOLUME_ZONE = 'volume_zone'
CONN_MAX_RETRIES = 'conn_max_retries'
CONN_RETRY_INTERVAL = 'conn_retry_interval'
CREATED_AT = 'created_at'

View File

@@ -42,7 +42,7 @@ class ComputeBase(metaclass=abc.ABCMeta):
well or a string
:param server_group_id: Optional server group id(uuid) which is used
for anti_affinity feature
:param availability_zone: Name of the compute availability zone.
:param availability_zone: Availability zone data dict
:raises ComputeBuildException: if compute failed to build amphora
:returns: UUID of amphora

View File

@@ -90,8 +90,7 @@ class VirtualMachineManager(compute_base.ComputeBase):
well or a string
:param server_group_id: Optional server group id(uuid) which is used
for anti_affinity feature
:param availability_zone: Name of the compute availability zone.
:param availability_zone: Availability zone data dict
:raises ComputeBuildException: if nova failed to build virtual machine
:returns: UUID of amphora
@@ -109,7 +108,12 @@ class VirtualMachineManager(compute_base.ComputeBase):
server_group = None if server_group_id is None else {
"group": server_group_id}
az_name = availability_zone or CONF.nova.availability_zone
if availability_zone:
az_name = availability_zone.get(constants.COMPUTE_ZONE,
CONF.nova.availability_zone)
else:
az_name = CONF.nova.availability_zone
image_id = self.image_driver.get_image_id_by_tag(
image_tag, image_owner)
@@ -127,7 +131,7 @@ class VirtualMachineManager(compute_base.ComputeBase):
LOG.debug('Creating volume for amphora from image %s',
image_id)
volume_id = self.volume_driver.create_volume_from_image(
image_id)
image_id, availability_zone)
LOG.debug('Created boot volume %s for amphora', volume_id)
# If use volume based, does not require image ID anymore
image_id = None

View File

@@ -107,13 +107,9 @@ class ComputeCreate(BaseComputeTask):
amp_image_tag = CONF.controller_worker.amp_image_tag
if availability_zone:
amp_availability_zone = availability_zone.get(
constants.COMPUTE_ZONE)
amp_network = availability_zone.get(constants.MANAGEMENT_NETWORK)
if amp_network:
network_ids = [amp_network]
else:
amp_availability_zone = None
try:
if CONF.haproxy_amphora.build_rate_limit != -1:
self.rate_limit.add_to_build_request_queue(
@@ -146,7 +142,7 @@ class ComputeCreate(BaseComputeTask):
config_drive_files=config_drive_files,
user_data=user_data,
server_group_id=server_group_id,
availability_zone=amp_availability_zone)
availability_zone=availability_zone)
LOG.info("Server created with id: %s for amphora id: %s",
compute_id, amphora_id)

View File

@@ -186,9 +186,14 @@ class TestAvailabilityZoneCapabilities(base.BaseAPITest):
provider='bogus'), status=400)
def test_noop_provider(self):
ref_capabilities = [{'description': 'The compute availability zone to '
'use for this loadbalancer.',
'name': constants.COMPUTE_ZONE}]
ref_capabilities = [
{'description': 'The compute availability zone to '
'use for this loadbalancer.',
'name': constants.COMPUTE_ZONE},
{'description': 'The volume availability zone to '
'use for this loadbalancer.',
'name': constants.VOLUME_ZONE},
]
result = self.get(
self.AVAILABILITY_ZONE_CAPABILITIES_PATH.format(
@@ -220,9 +225,14 @@ class TestAvailabilityZoneCapabilities(base.BaseAPITest):
provider='noop_driver'), status=501)
def test_authorized(self):
ref_capabilities = [{'description': 'The compute availability zone to '
'use for this loadbalancer.',
'name': constants.COMPUTE_ZONE}]
ref_capabilities = [
{'description': 'The compute availability zone to '
'use for this loadbalancer.',
'name': constants.COMPUTE_ZONE},
{'description': 'The volume availability zone to '
'use for this loadbalancer.',
'name': constants.VOLUME_ZONE},
]
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)

View File

@@ -853,6 +853,16 @@ class TestAmphoraDriver(base.TestRpc):
m_driver.validate_availability_zone.return_value = None
ref_dict = {consts.COMPUTE_ZONE: 'my_compute_zone'}
self.amp_driver.validate_availability_zone(ref_dict)
m_driver.validate_availability_zone.assert_called_once_with(
'my_compute_zone')
# Test volume zone
with mock.patch('stevedore.driver.DriverManager.driver') as m_driver:
m_driver.validate_availability_zone.return_value = None
ref_dict = {consts.VOLUME_ZONE: 'my_volume_zone'}
self.amp_driver.validate_availability_zone(ref_dict)
m_driver.validate_availability_zone.assert_called_once_with(
'my_volume_zone')
with mock.patch('octavia.common.utils.get_network_driver') as m_driver:
# Test vip networks

View File

@@ -145,7 +145,9 @@ class TestNoopProviderDriver(base.TestCase):
"to use for this load balancer."}
self.ref_availability_zone_metadata = {
"compute_zone": "The compute availability zone to use for this "
"loadbalancer."}
"loadbalancer.",
"volume_zone": "The volume availability zone to use for this "
"loadbalancer."}
def test_create_vip_port(self):
vip_dict, additional_vip_dicts = self.driver.create_vip_port(

View File

@@ -144,7 +144,7 @@ class TestNovaClient(base.TestCase):
config_drive_files='Files Blah')
self.assertEqual(self.amphora.compute_id, amphora_id)
mock_driver.create_volume_from_image.assert_called_with(1)
mock_driver.create_volume_from_image.assert_called_with(1, None)
self.manager.manager.create.assert_called_with(
name="amphora_name",
nics=[{'net-id': 1}, {'port-id': 2}],
@@ -161,7 +161,9 @@ class TestNovaClient(base.TestCase):
)
def test_build_with_availability_zone(self):
FAKE_AZ = "my_availability_zone"
FAKE_AZ_NAME = "my_availability_zone"
self.conf.config(group="nova", availability_zone=FAKE_AZ_NAME)
FAKE_AZ = {constants.COMPUTE_ZONE: FAKE_AZ_NAME}
amphora_id = self.manager.build(amphora_flavor=1, image_tag='malt',
key_name=1,
@@ -185,10 +187,50 @@ class TestNovaClient(base.TestCase):
userdata='Blah',
config_drive=True,
scheduler_hints=None,
availability_zone=FAKE_AZ,
availability_zone=FAKE_AZ_NAME,
block_device_mapping={}
)
@mock.patch('stevedore.driver.DriverManager.driver')
def test_build_with_availability_zone_and_volume(self, mock_driver):
FAKE_AZ_NAME = "my_availability_zone"
self.conf.config(group="controller_worker",
volume_driver='volume_cinder_driver')
self.conf.config(group="nova", availability_zone=FAKE_AZ_NAME)
self.conf.config(group="cinder", availability_zone=FAKE_AZ_NAME)
FAKE_AZ = {constants.COMPUTE_ZONE: FAKE_AZ_NAME,
constants.VOLUME_ZONE: FAKE_AZ_NAME}
self.manager.volume_driver = mock_driver
mock_driver.create_volume_from_image.return_value = 1
amphora_id = self.manager.build(amphora_flavor=1, image_tag='pilsner',
key_name=1,
sec_groups=1,
network_ids=[1],
port_ids=[2],
user_data='Blah',
config_drive_files='Files Blah',
availability_zone=FAKE_AZ)
self.assertEqual(self.amphora.compute_id, amphora_id)
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=FAKE_AZ_NAME,
block_device_mapping={'vda': '1:::true'}
)
mock_driver.create_volume_from_image.assert_called_with(1, FAKE_AZ)
def test_build_with_availability_zone_config(self):
FAKE_AZ = "my_availability_zone"
self.conf.config(group="nova", availability_zone=FAKE_AZ)

View File

@@ -269,7 +269,7 @@ class TestComputeTasks(base.TestCase):
'/etc/rsyslog.d/10-rsyslog.conf': 'FAKE CFG'},
user_data='user_data_conf',
server_group_id=SERVER_GRPOUP_ID,
availability_zone=compute_zone)
availability_zone=az_dict)
# Make sure it returns the expected compute_id
self.assertEqual(COMPUTE_ID, compute_id)

View File

@@ -13,10 +13,10 @@
from oslo_config import cfg
from oslo_utils import uuidutils
from octavia.common import constants
import octavia.tests.unit.base as base
from octavia.volume.drivers.noop_driver import driver
CONF = cfg.CONF
@@ -33,7 +33,16 @@ class TestNoopVolumeDriver(base.TestCase):
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.assertEqual((self.image_id, None, 'create_volume_from_image'),
self.driver.driver.volumeconfig[(
self.image_id
)])
def test_create_volume_from_image_with_availability_zone(self):
az_name = "some_az"
az_data = {constants.VOLUME_ZONE: az_name}
self.driver.create_volume_from_image(self.image_id, az_data)
self.assertEqual((self.image_id, az_data, 'create_volume_from_image'),
self.driver.driver.volumeconfig[(
self.image_id
)])

View File

@@ -16,6 +16,7 @@ from oslo_config import cfg
from oslo_config import fixture as oslo_fixture
from oslo_utils import uuidutils
from octavia.common import constants
from octavia.common import exceptions
import octavia.tests.unit.base as base
import octavia.volume.drivers.cinder_driver as cinder_common
@@ -42,6 +43,7 @@ class TestCinderClient(base.TestCase):
self.manager.manager.get.return_value.status = 'available'
self.manager.manager.create.return_value = self.cinder_response
self.manager.availability_zone_manager = mock.MagicMock()
self.image_id = fake_uuid2
self.volume_id = fake_uuid3
@@ -58,6 +60,36 @@ class TestCinderClient(base.TestCase):
availability_zone=None,
imageRef=self.image_id)
def test_create_volume_from_image_with_availability_zone(self):
self.conf.config(group="controller_worker",
volume_driver='volume_cinder_driver')
self.conf.config(group="cinder", volume_create_retry_interval=0)
self.conf.config(group="cinder", availability_zone="no_zone")
az_name = "some_zone"
az_data = {constants.VOLUME_ZONE: az_name}
self.manager.create_volume_from_image(self.image_id, az_data)
self.manager.manager.create.assert_called_with(
size=16,
volume_type=None,
availability_zone=az_name,
imageRef=self.image_id)
def test_create_volume_from_image_with_availability_zone_from_conf(self):
az_name = "some_az"
self.conf.config(group="controller_worker",
volume_driver='volume_cinder_driver')
self.conf.config(group="cinder", volume_create_retry_interval=0)
self.conf.config(group="cinder", availability_zone=az_name)
self.manager.create_volume_from_image(self.image_id)
self.manager.manager.create.assert_called_with(
size=16,
volume_type=None,
availability_zone=az_name,
imageRef=self.image_id)
def test_create_volume_from_image_error(self):
self.conf.config(group="controller_worker",
volume_driver='volume_cinder_driver')
@@ -97,3 +129,18 @@ class TestCinderClient(base.TestCase):
self.assertRaises(exceptions.VolumeGetException,
self.manager.get_image_from_volume,
self.volume_id)
def test_validate_availability_zone(self):
az_name = "some_az"
mock_az = mock.Mock()
mock_az.zoneName = az_name
self.manager.availability_zone_manager.list.return_value = [mock_az]
self.manager.validate_availability_zone(az_name)
self.manager.availability_zone_manager.list.assert_called_with(
detailed=False)
def test_validate_availability_zone_with_exception(self):
self.manager.availability_zone_manager.list.return_value = []
self.assertRaises(exceptions.InvalidSubresource,
self.manager.validate_availability_zone,
"bogus")

View File

@@ -43,20 +43,29 @@ class VolumeManager(volume_base.VolumeBase):
cacert=CONF.cinder.ca_certificates_file
)
self.manager = self._cinder_client.volumes
self.availability_zone_manager = self._cinder_client.availability_zones
@retry(reraise=True,
stop=stop_after_attempt(CONF.cinder.volume_create_max_retries))
def create_volume_from_image(self, image_id):
def create_volume_from_image(self, image_id, availability_zone=None):
"""Create cinder volume
:param image_id: ID of amphora image
:param availability_zone: Availability zone data dict
:return volume id
"""
if availability_zone:
az_name = availability_zone.get(
constants.VOLUME_ZONE, CONF.cinder.availability_zone)
else:
az_name = CONF.cinder.availability_zone
volume = self.manager.create(
size=CONF.cinder.volume_size,
volume_type=CONF.cinder.volume_type,
availability_zone=CONF.cinder.availability_zone,
availability_zone=az_name,
imageRef=image_id)
resource_status = self.manager.get(volume.id).status
@@ -121,3 +130,24 @@ class VolumeManager(volume_base.VolumeBase):
LOG.error("Volume %s has no image metadata", volume_id)
image_id = None
return image_id
def validate_availability_zone(self, availability_zone):
"""Validates that an availability zone exists in cinder.
:param availability_zone: Name of the availability zone to lookup.
:raises: NotFound
:returns: None
"""
try:
volume_zones = [
a.zoneName for a in self.availability_zone_manager.list(
detailed=False)]
if availability_zone not in volume_zones:
LOG.info('Availability zone %s was not found in cinder. %s',
availability_zone, volume_zones)
raise exceptions.InvalidSubresource(
resource='Cinder availability zone', id=availability_zone)
except Exception as e:
LOG.exception('Cinder reports a failure getting listing '
'availability zones: %s', str(e))
raise

View File

@@ -23,10 +23,12 @@ class NoopManager:
super().__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')
def create_volume_from_image(self, image_id, availability_zone=None):
LOG.debug("Volume %s no-op, image id %s, availability zone %s",
self.__class__.__name__, image_id, availability_zone)
self.volumeconfig[image_id] = (image_id,
availability_zone,
'create_volume_from_image')
volume_id = uuidutils.generate_uuid()
return volume_id
@@ -42,14 +44,21 @@ class NoopManager:
image_id = uuidutils.generate_uuid()
return image_id
def validate_availability_zone(self, availability_zone):
LOG.debug("Volume %s no-op, validate_availability_zone name %s",
self.__class__.__name__, availability_zone)
self.volumeconfig[availability_zone] = (
availability_zone, 'validate_availability_zone')
class NoopVolumeDriver(driver_base.VolumeBase):
def __init__(self):
super().__init__()
self.driver = NoopManager()
def create_volume_from_image(self, image_id):
volume_id = self.driver.create_volume_from_image(image_id)
def create_volume_from_image(self, image_id, availability_zone=None):
volume_id = self.driver.create_volume_from_image(
image_id, availability_zone)
return volume_id
def delete_volume(self, volume_id):
@@ -58,3 +67,6 @@ class NoopVolumeDriver(driver_base.VolumeBase):
def get_image_from_volume(self, volume_id):
image_id = self.driver.get_image_from_volume(volume_id)
return image_id
def validate_availability_zone(self, availability_zone):
self.driver.validate_availability_zone(availability_zone)

View File

@@ -18,10 +18,11 @@ import abc
class VolumeBase(metaclass=abc.ABCMeta):
@abc.abstractmethod
def create_volume_from_image(self, image_id):
def create_volume_from_image(self, image_id, availability_zone=None):
"""Create volume for instance
:param image_id: ID of amphora image
:param availability_zone: Availability zone data dict
:return volume id
"""
@@ -41,3 +42,13 @@ class VolumeBase(metaclass=abc.ABCMeta):
:return image id
"""
@abc.abstractmethod
def validate_availability_zone(self, availability_zone):
"""Validates that a volume availability zone exists.
:param availability_zone: Name of the availability zone to lookup.
:returns: None
:raises: NotFound
:raises: NotImplementedError
"""

View File

@@ -0,0 +1,6 @@
---
features:
- |
Added volume zone to availability zone profile for amphorae backend for
creating loadbalancer with specific volume availability zone