From fe3fe44a14f7b7ebd434269f6ec4badb6c0a0c97 Mon Sep 17 00:00:00 2001 From: Yamato Tanaka Date: Wed, 18 Sep 2024 18:37:28 +0900 Subject: [PATCH] Add Octavia Availability Zone resource types Add the following two new resource types to create Octavia Availability Zone resouce and Availability Zone Profile resources: - OS::Octavia::AvailabilityZone - OS::Octavia::AvailabilityZoneProfile Story: 2011225 Task: 51041 Change-Id: Ieff5a82ed22cd34f51ff222a45c7a46ccfd3a5a3 --- heat/engine/clients/os/octavia.py | 14 ++ .../openstack/octavia/availability_zone.py | 129 ++++++++++++++++++ .../octavia/availability_zone_profile.py | 97 +++++++++++++ .../openstack/octavia/loadbalancer.py | 3 + heat/policies/resource_types.py | 8 ++ .../openstack/octavia/inline_templates.py | 26 +++- .../octavia/test_availability_zone.py | 111 +++++++++++++++ .../octavia/test_availability_zone_profile.py | 114 ++++++++++++++++ ...bilityzone-resources-f07af0b016f259ed.yaml | 8 ++ setup.cfg | 2 + 10 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 heat/engine/resources/openstack/octavia/availability_zone.py create mode 100644 heat/engine/resources/openstack/octavia/availability_zone_profile.py create mode 100644 heat/tests/openstack/octavia/test_availability_zone.py create mode 100644 heat/tests/openstack/octavia/test_availability_zone_profile.py create mode 100644 releasenotes/notes/add-octavia-availabilityzone-resources-f07af0b016f259ed.yaml diff --git a/heat/engine/clients/os/octavia.py b/heat/engine/clients/os/octavia.py index d9ec6b8c6d..b0a58c8fb9 100644 --- a/heat/engine/clients/os/octavia.py +++ b/heat/engine/clients/os/octavia.py @@ -89,6 +89,12 @@ class OctaviaClientPlugin(client_plugin.ClientPlugin): value=value, attr=DEFAULT_FIND_ATTR) return flavorprofile['id'] + def get_availabilityzoneprofile(self, value): + availability_zone_profile = self.client().find( + path=constants.BASE_AVAILABILITYZONEPROFILE_URL, + value=value, attr=DEFAULT_FIND_ATTR) + return availability_zone_profile['id'] + class OctaviaConstraint(constraints.BaseCustomConstraint): @@ -124,3 +130,11 @@ class FlavorConstraint(OctaviaConstraint): class FlavorProfileConstraint(OctaviaConstraint): base_url = constants.BASE_FLAVORPROFILE_URL + + +class AvailabilityZoneConstraint(OctaviaConstraint): + base_url = constants.BASE_AVAILABILITYZONE_URL + + +class AvailabilityZoneProfileConstraint(OctaviaConstraint): + base_url = constants.BASE_AVAILABILITYZONEPROFILE_URL diff --git a/heat/engine/resources/openstack/octavia/availability_zone.py b/heat/engine/resources/openstack/octavia/availability_zone.py new file mode 100644 index 0000000000..f242816b1c --- /dev/null +++ b/heat/engine/resources/openstack/octavia/availability_zone.py @@ -0,0 +1,129 @@ +# +# 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 heat.common.i18n import _ +from heat.engine import attributes +from heat.engine import constraints +from heat.engine import properties +from heat.engine import resource +from heat.engine import support +from heat.engine import translation + + +class AvailabilityZone(resource.Resource): + """A resource for creating octavia Availability Zones. + + This resource creates and manages octavia Availability Zones, + which allows to tune Load Balancers' capabilities. + """ + + default_client_name = 'octavia' + + support_status = support.SupportStatus(version='24.0.0') + + PROPERTIES = ( + DESCRIPTION, ENABLED, NAME, AVAILABILITY_ZONE_PROFILE + ) = ( + 'description', 'enabled', 'name', 'availability_zone_profile' + ) + + ATTRIBUTES = ( + AVAILABILITY_ZONE_PROFILE_ID_ATTR, + ) = ( + 'availability_zone_profile_id', + ) + + properties_schema = { + DESCRIPTION: properties.Schema( + properties.Schema.STRING, + _('Description of this Availability Zone.'), + update_allowed=True, + default='' + ), + ENABLED: properties.Schema( + properties.Schema.BOOLEAN, + _('If the resource if available for use.'), + update_allowed=True, + default=True, + ), + NAME: properties.Schema( + properties.Schema.STRING, + _('Name of this Availability Zone.'), + update_allowed=True + ), + AVAILABILITY_ZONE_PROFILE: properties.Schema( + properties.Schema.STRING, + _('The ID or the name of the Availability Zone Profile.'), + required=True, + constraints=[ + constraints.CustomConstraint('octavia.availabilityzoneprofile') + ] + ), + } + + attributes_schema = { + AVAILABILITY_ZONE_PROFILE_ID_ATTR: attributes.Schema( + _('The ID of the availability zone profile.'), + type=attributes.Schema.STRING, + ) + } + + def translation_rules(self, props): + return [ + translation.TranslationRule( + props, + translation.TranslationRule.RESOLVE, + [self.AVAILABILITY_ZONE_PROFILE], + client_plugin=self.client_plugin(), + finder='get_availabilityzoneprofile' + ) + ] + + def _prepare_args(self, properties): + props = dict((k, v) for k, v in properties.items() + if v is not None) + if self.NAME not in props: + props[self.NAME] = self.physical_resource_name() + props['availability_zone_profile_id'] = props.pop( + self.AVAILABILITY_ZONE_PROFILE + ) + return props + + def handle_create(self): + props = self._prepare_args(self.properties) + + availability_zone = self.client().availabilityzone_create( + json={'availability_zone': props})['availability_zone'] + self.resource_id_set(availability_zone.get('name')) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if prop_diff: + if self.NAME in prop_diff and prop_diff[self.NAME] is None: + prop_diff[self.NAME] = self.physical_resource_name() + self.client().availabilityzone_set( + self.resource_id, + json={'availability_zone': prop_diff}) + + def handle_delete(self): + with self.client_plugin().ignore_not_found: + self.client().availabilityzone_delete(self.resource_id) + return True + + def _show_resource(self): + return self.client().availabilityzone_show(self.resource_id) + + +def resource_mapping(): + return { + 'OS::Octavia::AvailabilityZone': AvailabilityZone + } diff --git a/heat/engine/resources/openstack/octavia/availability_zone_profile.py b/heat/engine/resources/openstack/octavia/availability_zone_profile.py new file mode 100644 index 0000000000..a94880612c --- /dev/null +++ b/heat/engine/resources/openstack/octavia/availability_zone_profile.py @@ -0,0 +1,97 @@ +# +# 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 heat.common.i18n import _ +from heat.engine import properties +from heat.engine import resource +from heat.engine import support + + +class AvailabilityZoneProfile(resource.Resource): + """A resource for creating octavia Availability Zone Profiles. + + This resource creates and manages octavia Availability Zone Profiles, + which allows to tune Load Balancers' capabilities. + """ + + default_client_name = 'octavia' + + support_status = support.SupportStatus(version='24.0.0') + + PROPERTIES = ( + NAME, AVAILABILITY_ZONE_DATA, PROVIDER_NAME + ) = ( + 'name', 'availability_zone_data', 'provider_name' + ) + + ATTRIBUTES = ( + AVAILABILITY_ZONE_PROFILE_ID_ATTR, + ) = ( + 'availability_zone_profile_id', + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('Name of this Availability Zone Profile.'), + update_allowed=True + ), + AVAILABILITY_ZONE_DATA: properties.Schema( + properties.Schema.STRING, + _('JSON string containing the availability zone metadata.'), + update_allowed=True, + required=True + ), + PROVIDER_NAME: properties.Schema( + properties.Schema.STRING, + _('Provider name of this Availability Zone.'), + update_allowed=True, + ), + } + + def _prepare_args(self, properties): + props = dict((k, v) for k, v in properties.items() + if v is not None) + if self.NAME not in props: + props[self.NAME] = self.physical_resource_name() + return props + + def handle_create(self): + props = self._prepare_args(self.properties) + + availabilityzoneprofile = self.client().availabilityzoneprofile_create( + json={'availability_zone_profile': props} + )['availability_zone_profile'] + self.resource_id_set(availabilityzoneprofile['id']) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if prop_diff: + if self.NAME in prop_diff and prop_diff[self.NAME] is None: + prop_diff[self.NAME] = self.physical_resource_name() + self.client().availabilityzoneprofile_set( + self.resource_id, + json={'availability_zone_profile': prop_diff}) + + def handle_delete(self): + with self.client_plugin().ignore_not_found: + self.client().availabilityzoneprofile_delete(self.resource_id) + return True + + def _show_resource(self): + return self.client().availabilityzoneprofile_show(self.resource_id) + + +def resource_mapping(): + return { + 'OS::Octavia::AvailabilityZoneProfile': AvailabilityZoneProfile + } diff --git a/heat/engine/resources/openstack/octavia/loadbalancer.py b/heat/engine/resources/openstack/octavia/loadbalancer.py index 0e866168f1..27c3368e47 100644 --- a/heat/engine/resources/openstack/octavia/loadbalancer.py +++ b/heat/engine/resources/openstack/octavia/loadbalancer.py @@ -101,6 +101,9 @@ class LoadBalancer(octavia_base.OctaviaBase): properties.Schema.STRING, _('The availability zone of the Load Balancer.'), support_status=support.SupportStatus(version='17.0.0'), + constraints=[ + constraints.CustomConstraint('octavia.availabilityzone') + ] ) } diff --git a/heat/policies/resource_types.py b/heat/policies/resource_types.py index 4a7bc4c6e1..b94abeffa6 100644 --- a/heat/policies/resource_types.py +++ b/heat/policies/resource_types.py @@ -108,6 +108,14 @@ resource_types_policies = [ policy.RuleDefault( name=POLICY_ROOT % 'OS::Octavia::FlavorProfile', check_str=base.RULE_PROJECT_ADMIN, + scope_types=['project']), + policy.RuleDefault( + name=POLICY_ROOT % 'OS::Octavia::AvailabilityZone', + check_str=base.RULE_PROJECT_ADMIN, + scope_types=['project']), + policy.RuleDefault( + name=POLICY_ROOT % 'OS::Octavia::AvailabilityZoneProfile', + check_str=base.RULE_PROJECT_ADMIN, scope_types=['project']) ] diff --git a/heat/tests/openstack/octavia/inline_templates.py b/heat/tests/openstack/octavia/inline_templates.py index 0b41946bf3..a909476fd0 100644 --- a/heat/tests/openstack/octavia/inline_templates.py +++ b/heat/tests/openstack/octavia/inline_templates.py @@ -151,7 +151,6 @@ resources: {"flavor_data_key": "flavor_data_value"} ''' - FLAVOR_TEMPLATE = ''' heat_template_version: 2016-10-14 description: Template to test Flavor Octavia resource @@ -164,3 +163,28 @@ resources: description: test_description enabled: True ''' + +AVAILABILITY_ZONE_PROFILE_TEMPLATE = ''' +heat_template_version: 2016-10-14 +description: Template to test AvailabilityZone Octavia resource +resources: + availability_zone_profile: + type: OS::Octavia::AvailabilityZoneProfile + properties: + name: test_availability_zone_profile + availability_zone_data: '{"compute_zone": "az-central"}' + provider_name: amphora +''' + +AVAILABILITY_ZONE_TEMPLATE = ''' +heat_template_version: 2016-10-14 +description: Template to test AvailabilityZone Octavia resource +resources: + availability_zone: + type: OS::Octavia::AvailabilityZone + properties: + name: test_availability_zone + description: my availability zone + enabled: True + availability_zone_profile: az_profile_id_1234 +''' diff --git a/heat/tests/openstack/octavia/test_availability_zone.py b/heat/tests/openstack/octavia/test_availability_zone.py new file mode 100644 index 0000000000..4f771696c3 --- /dev/null +++ b/heat/tests/openstack/octavia/test_availability_zone.py @@ -0,0 +1,111 @@ +# +# 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 heat.common import template_format +from heat.engine.resources.openstack.octavia import availability_zone +from heat.tests import common +from heat.tests.openstack.octavia import inline_templates +from heat.tests import utils + + +class AvailabilityZoneTest(common.HeatTestCase): + + def test_resource_mapping(self): + mapping = availability_zone.resource_mapping() + self.assertEqual( + availability_zone.AvailabilityZone, + mapping['OS::Octavia::AvailabilityZone'], + ) + + def _create_stack(self, tmpl=inline_templates.AVAILABILITY_ZONE_TEMPLATE): + self.t = template_format.parse(tmpl) + self.stack = utils.parse_stack(self.t) + self.az = self.stack['availability_zone'] + + self.octavia_client = mock.MagicMock() + self.az.client = mock.MagicMock() + self.az.client.return_value = self.octavia_client + + self.az.client_plugin().client = mock.MagicMock( + return_value=self.octavia_client) + + self.az.resource_id_set('1234') + self.patchobject( + self.az, 'physical_resource_name', return_value='resource_name' + ) + + def test_create(self): + self._create_stack() + expected = { + 'availability_zone': { + 'description': 'my availability zone', + 'enabled': True, + 'name': 'test_availability_zone', + 'availability_zone_profile_id': 'az_profile_id_1234', + } + } + + self.az.handle_create() + + self.octavia_client.availabilityzone_create.assert_called_with( + json=expected + ) + + def test_show_resource(self): + self._create_stack() + self.octavia_client.availabilityzone_show.return_value = { + 'id': 'az_id_1234' + } + self.assertEqual({'id': 'az_id_1234'}, self.az._show_resource()) + + self.octavia_client.availabilityzone_show.assert_called_with('1234') + + def test_update(self): + self._create_stack() + prop_diff = { + 'name': 'test_name2', + 'description': 'test_description2', + 'enabled': False, + } + + self.az.handle_update(None, None, prop_diff) + + self.octavia_client.availabilityzone_set.assert_called_once_with( + '1234', json={'availability_zone': prop_diff} + ) + + self.octavia_client.availabilityzone_set.reset_mock() + + # Updating an availability zone with None as name should use + # physical_resource_name() as new name + prop_diff = { + 'name': None, + 'description': 'test_description3', + 'enabled': True, + } + + self.az.handle_update(None, None, prop_diff) + + self.assertEqual(prop_diff['name'], 'resource_name') + self.octavia_client.availabilityzone_set.assert_called_once_with( + '1234', json={'availability_zone': prop_diff} + ) + + def test_delete(self): + self._create_stack() + + self.az.handle_delete() + + self.octavia_client.availabilityzone_delete.assert_called_with('1234') diff --git a/heat/tests/openstack/octavia/test_availability_zone_profile.py b/heat/tests/openstack/octavia/test_availability_zone_profile.py new file mode 100644 index 0000000000..44bff90883 --- /dev/null +++ b/heat/tests/openstack/octavia/test_availability_zone_profile.py @@ -0,0 +1,114 @@ +# +# 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 heat.common import template_format +from heat.engine.resources.openstack.octavia import availability_zone_profile +from heat.tests import common +from heat.tests.openstack.octavia import inline_templates +from heat.tests import utils + + +class AvailabilityZoneProfileTest(common.HeatTestCase): + + def test_resource_mapping(self): + mapping = availability_zone_profile.resource_mapping() + self.assertEqual( + availability_zone_profile.AvailabilityZoneProfile, + mapping['OS::Octavia::AvailabilityZoneProfile'], + ) + + def _create_stack( + self, tmpl=inline_templates.AVAILABILITY_ZONE_PROFILE_TEMPLATE + ): + self.t = template_format.parse(tmpl) + self.stack = utils.parse_stack(self.t) + self.az = self.stack['availability_zone_profile'] + + self.octavia_client = mock.MagicMock() + self.az.client = mock.MagicMock() + self.az.client.return_value = self.octavia_client + + self.az.resource_id_set('1234') + self.patchobject( + self.az, 'physical_resource_name', return_value='resource_name' + ) + + def test_create(self): + self._create_stack() + expected = { + 'availability_zone_profile': { + 'name': 'test_availability_zone_profile', + 'availability_zone_data': '{"compute_zone": "az-central"}', + 'provider_name': 'amphora', + } + } + + self.az.handle_create() + + self.octavia_client.availabilityzoneprofile_create.assert_called_with( + json=expected + ) + + def test_show_resource(self): + self._create_stack() + self.octavia_client.availabilityzoneprofile_show.return_value = { + 'id': 'azp_id_1234' + } + self.assertEqual({'id': 'azp_id_1234'}, self.az._show_resource()) + + self.octavia_client.availabilityzoneprofile_show.assert_called_with( + '1234' + ) + + def test_update(self): + self._create_stack() + prop_diff = { + 'name': 'test_availability_zone_profile2', + 'availability_zone_data': '{"compute_zone": "az-edge2"}', + 'provider_name': 'amphora2', + } + + self.az.handle_update(None, None, prop_diff) + + octavia_client = self.octavia_client + octavia_client.availabilityzoneprofile_set.assert_called_once_with( + '1234', json={'availability_zone_profile': prop_diff} + ) + + octavia_client.availabilityzoneprofile_set.reset_mock() + + # Updating an availability zone with None as name should use + # physical_resource_name() as new name + prop_diff = { + 'name': None, + 'availability_zone_data': '{"compute_zone": "az-edge3"}', + 'provider_name': 'amphora3', + } + + self.az.handle_update(None, None, prop_diff) + + self.assertEqual(prop_diff['name'], 'resource_name') + octavia_client.availabilityzoneprofile_set.assert_called_once_with( + '1234', json={'availability_zone_profile': prop_diff} + ) + + def test_delete(self): + self._create_stack() + + self.az.handle_delete() + + self.octavia_client.availabilityzoneprofile_delete.assert_called_with( + '1234' + ) diff --git a/releasenotes/notes/add-octavia-availabilityzone-resources-f07af0b016f259ed.yaml b/releasenotes/notes/add-octavia-availabilityzone-resources-f07af0b016f259ed.yaml new file mode 100644 index 0000000000..4523791218 --- /dev/null +++ b/releasenotes/notes/add-octavia-availabilityzone-resources-f07af0b016f259ed.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The new resources, ``OS::Octavia::AvailabilityZoneProfile`` and + ``OS::Octavia::AvailabilityZone``, are added. These resource types allow an + operator to create Octavia availabilityzone profile and availabilityzone. + A created ``OS::Octavia::AvailabilityZone`` resource can be referred by the + ``availability_zone`` property of ``OS::Octavia::LoadBalancer`` resources. diff --git a/setup.cfg b/setup.cfg index 54587e9bbf..9b7626d235 100644 --- a/setup.cfg +++ b/setup.cfg @@ -160,6 +160,8 @@ heat.constraints = octavia.pool = heat.engine.clients.os.octavia:PoolConstraint octavia.flavor = heat.engine.clients.os.octavia:FlavorConstraint octavia.flavorprofile = heat.engine.clients.os.octavia:FlavorProfileConstraint + octavia.availabilityzone = heat.engine.clients.os.octavia:AvailabilityZoneConstraint + octavia.availabilityzoneprofile = heat.engine.clients.os.octavia:AvailabilityZoneProfileConstraint trove.flavor = heat.engine.clients.os.trove:FlavorConstraint zaqar.queue = heat.engine.clients.os.zaqar:QueueConstraint #ironic