diff --git a/heat/engine/clients/os/octavia.py b/heat/engine/clients/os/octavia.py index c865e01ca6..d9ec6b8c6d 100644 --- a/heat/engine/clients/os/octavia.py +++ b/heat/engine/clients/os/octavia.py @@ -78,6 +78,17 @@ class OctaviaClientPlugin(client_plugin.ClientPlugin): value=value, attr=DEFAULT_FIND_ATTR) return policy['id'] + def get_flavor(self, value): + flavor = self.client().find(path=constants.BASE_FLAVOR_URL, + value=value, attr=DEFAULT_FIND_ATTR) + return flavor['id'] + + def get_flavorprofile(self, value): + flavorprofile = self.client().find( + path=constants.BASE_FLAVORPROFILE_URL, + value=value, attr=DEFAULT_FIND_ATTR) + return flavorprofile['id'] + class OctaviaConstraint(constraints.BaseCustomConstraint): @@ -105,3 +116,11 @@ class PoolConstraint(OctaviaConstraint): class L7PolicyConstraint(OctaviaConstraint): base_url = constants.BASE_L7POLICY_URL + + +class FlavorConstraint(OctaviaConstraint): + base_url = constants.BASE_FLAVOR_URL + + +class FlavorProfileConstraint(OctaviaConstraint): + base_url = constants.BASE_FLAVORPROFILE_URL diff --git a/heat/engine/resources/openstack/octavia/flavor.py b/heat/engine/resources/openstack/octavia/flavor.py new file mode 100644 index 0000000000..8cf58b8e46 --- /dev/null +++ b/heat/engine/resources/openstack/octavia/flavor.py @@ -0,0 +1,132 @@ +# +# 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 Flavor(resource.Resource): + """A resource for creating octavia Flavors. + + This resource creates and manages octavia Flavors, + which allows to tune Load Balancers' capabilities. + """ + + default_client_name = 'octavia' + + support_status = support.SupportStatus(version='14.0.0') + + PROPERTIES = ( + DESCRIPTION, ENABLED, FLAVOR_PROFILE, NAME + ) = ( + 'description', 'enabled', 'flavor_profile', 'name' + ) + + ATTRIBUTES = ( + FLAVOR_PROFILE_ID_ATTR, + ) = ( + 'flavor_profile_id', + ) + + properties_schema = { + DESCRIPTION: properties.Schema( + properties.Schema.STRING, + _('Description of this Flavor.'), + 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 Flavor.'), + update_allowed=True + ), + FLAVOR_PROFILE: properties.Schema( + properties.Schema.STRING, + _('The ID or the name of the Flavor Profile.'), + required=True, + constraints=[ + constraints.CustomConstraint('octavia.flavorprofile') + ] + ), + } + + attributes_schema = { + FLAVOR_PROFILE_ID_ATTR: attributes.Schema( + _('The ID of the flavor profile.'), + type=attributes.Schema.STRING, + ) + } + + def translation_rules(self, props): + return [ + translation.TranslationRule( + props, + translation.TranslationRule.RESOLVE, + [self.FLAVOR_PROFILE], + client_plugin=self.client_plugin(), + finder='get_flavorprofile' + ) + ] + + 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['flavor_profile_id'] = props.pop(self.FLAVOR_PROFILE) + return props + + def handle_create(self): + props = self._prepare_args(self.properties) + + flavor = self.client().flavor_create( + json={'flavor': props})['flavor'] + self.resource_id_set(flavor['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().flavor_set(self.resource_id, + json={'flavor': prop_diff}) + + def handle_delete(self): + with self.client_plugin().ignore_not_found: + self.client().flavor_delete(self.resource_id) + return True + + def _resolve_attribute(self, name): + if self.resource_id is None: + return None + resource = self._show_resource() + return resource[name] + + def _show_resource(self): + return self.client().flavor_show(self.resource_id) + + +def resource_mapping(): + return { + 'OS::Octavia::Flavor': Flavor + } diff --git a/heat/engine/resources/openstack/octavia/flavor_profile.py b/heat/engine/resources/openstack/octavia/flavor_profile.py new file mode 100644 index 0000000000..ef20f6aca1 --- /dev/null +++ b/heat/engine/resources/openstack/octavia/flavor_profile.py @@ -0,0 +1,90 @@ +# +# 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 FlavorProfile(resource.Resource): + """A resource for creating octavia Flavor Profiles. + + This resource creates and manages octavia Flavor Profiles, + which allows to tune Load Balancers' capabilities. + """ + + default_client_name = 'octavia' + + support_status = support.SupportStatus(version='14.0.0') + + PROPERTIES = ( + NAME, FLAVOR_DATA, PROVIDER_NAME + ) = ( + 'name', 'flavor_data', 'provider_name' + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('Name of this Flavor Profile.'), + update_allowed=True + ), + FLAVOR_DATA: properties.Schema( + properties.Schema.STRING, + _('JSON string containing the flavor metadata.'), + update_allowed=True, + required=True + ), + PROVIDER_NAME: properties.Schema( + properties.Schema.STRING, + _('Provider name of this Flavor Profile.'), + 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) + + flavorprofile = self.client().flavorprofile_create( + json={'flavorprofile': props})['flavorprofile'] + self.resource_id_set(flavorprofile['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().flavorprofile_set( + self.resource_id, + json={'flavorprofile': prop_diff}) + + def handle_delete(self): + with self.client_plugin().ignore_not_found: + self.client().flavorprofile_delete(self.resource_id) + return True + + def _show_resource(self): + return self.client().flavorprofile_show(self.resource_id) + + +def resource_mapping(): + return { + 'OS::Octavia::FlavorProfile': FlavorProfile + } diff --git a/heat/engine/resources/openstack/octavia/loadbalancer.py b/heat/engine/resources/openstack/octavia/loadbalancer.py index a8d8fe510e..fc639a81a8 100644 --- a/heat/engine/resources/openstack/octavia/loadbalancer.py +++ b/heat/engine/resources/openstack/octavia/loadbalancer.py @@ -16,6 +16,7 @@ from heat.engine import attributes from heat.engine import constraints from heat.engine import properties from heat.engine.resources.openstack.octavia import octavia_base +from heat.engine import support from heat.engine import translation @@ -28,16 +29,17 @@ class LoadBalancer(octavia_base.OctaviaBase): PROPERTIES = ( DESCRIPTION, NAME, PROVIDER, VIP_ADDRESS, VIP_SUBNET, - ADMIN_STATE_UP, TENANT_ID + ADMIN_STATE_UP, TENANT_ID, FLAVOR ) = ( 'description', 'name', 'provider', 'vip_address', 'vip_subnet', - 'admin_state_up', 'tenant_id' + 'admin_state_up', 'tenant_id', 'flavor' ) ATTRIBUTES = ( - VIP_ADDRESS_ATTR, VIP_PORT_ATTR, VIP_SUBNET_ATTR, POOLS_ATTR + VIP_ADDRESS_ATTR, VIP_PORT_ATTR, VIP_SUBNET_ATTR, POOLS_ATTR, + FLAVOR_ID_ATTR ) = ( - 'vip_address', 'vip_port_id', 'vip_subnet_id', 'pools' + 'vip_address', 'vip_port_id', 'vip_subnet_id', 'pools', 'flavor_id' ) properties_schema = { @@ -86,6 +88,14 @@ class LoadBalancer(octavia_base.OctaviaBase): constraints=[ constraints.CustomConstraint('keystone.project') ], + ), + FLAVOR: properties.Schema( + properties.Schema.STRING, + _('The name or ID of the flavor of the Load Balancer.'), + support_status=support.SupportStatus(version='14.0.0'), + constraints=[ + constraints.CustomConstraint('octavia.flavor') + ] ) } @@ -106,6 +116,10 @@ class LoadBalancer(octavia_base.OctaviaBase): _('Pools this LoadBalancer is associated with.'), type=attributes.Schema.LIST, ), + FLAVOR_ID_ATTR: attributes.Schema( + _('The flavor ID of the LoadBalancer.'), + type=attributes.Schema.STRING, + ) } def translation_rules(self, props): @@ -118,6 +132,13 @@ class LoadBalancer(octavia_base.OctaviaBase): finder='find_resourceid_by_name_or_id', entity='subnet' ), + translation.TranslationRule( + props, + translation.TranslationRule.RESOLVE, + [self.FLAVOR], + client_plugin=self.client_plugin(), + finder='get_flavor', + ), ] def _prepare_args(self, properties): @@ -126,6 +147,8 @@ class LoadBalancer(octavia_base.OctaviaBase): if self.NAME not in props: props[self.NAME] = self.physical_resource_name() props['vip_subnet_id'] = props.pop(self.VIP_SUBNET) + if self.FLAVOR in props: + props['flavor_id'] = props.pop(self.FLAVOR) if 'tenant_id' in props: props['project_id'] = props.pop('tenant_id') return props diff --git a/heat/policies/resource_types.py b/heat/policies/resource_types.py index 27b067c289..009d2a6a39 100644 --- a/heat/policies/resource_types.py +++ b/heat/policies/resource_types.py @@ -64,6 +64,12 @@ resource_types_policies = [ check_str=base.RULE_PROJECT_ADMIN), policy.RuleDefault( name=POLICY_ROOT % 'OS::Blazar::Host', + check_str=base.RULE_PROJECT_ADMIN), + policy.RuleDefault( + name=POLICY_ROOT % 'OS::Octavia::Flavor', + check_str=base.RULE_PROJECT_ADMIN), + policy.RuleDefault( + name=POLICY_ROOT % 'OS::Octavia::FlavorProfile', check_str=base.RULE_PROJECT_ADMIN) ] diff --git a/heat/tests/openstack/octavia/inline_templates.py b/heat/tests/openstack/octavia/inline_templates.py index 29ed8f887d..a2b99748f7 100644 --- a/heat/tests/openstack/octavia/inline_templates.py +++ b/heat/tests/openstack/octavia/inline_templates.py @@ -25,6 +25,7 @@ resources: provider: octavia tenant_id: 1234 admin_state_up: True + flavor: f123 ''' LISTENER_TEMPLATE = ''' @@ -132,3 +133,30 @@ resources: value: test_value invert: False ''' + +FLAVORPROFILE_TEMPLATE = ''' +heat_template_version: 2016-10-14 +description: Template to test FlavorProfile Octavia resource +resources: + flavor_profile: + type: OS::Octavia::FlavorProfile + properties: + name: test_flavor_profile + provider_name: test_provider + flavor_data: | + {"flavor_data_key": "flavor_data_value"} +''' + + +FLAVOR_TEMPLATE = ''' +heat_template_version: 2016-10-14 +description: Template to test Flavor Octavia resource +resources: + flavor: + type: OS::Octavia::Flavor + properties: + flavor_profile: test_flavor_profile_id + name: test_name + description: test_description + enabled: True +''' diff --git a/heat/tests/openstack/octavia/test_flavor.py b/heat/tests/openstack/octavia/test_flavor.py new file mode 100644 index 0000000000..556b9d31fa --- /dev/null +++ b/heat/tests/openstack/octavia/test_flavor.py @@ -0,0 +1,95 @@ +# +# 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 mock + +from heat.common import template_format +from heat.tests import common +from heat.tests.openstack.octavia import inline_templates +from heat.tests import utils + + +class FlavorTest(common.HeatTestCase): + + def _create_stack(self, tmpl=inline_templates.FLAVOR_TEMPLATE): + self.t = template_format.parse(tmpl) + self.stack = utils.parse_stack(self.t) + self.flavor = self.stack['flavor'] + + self.octavia_client = mock.MagicMock() + self.flavor.client = mock.MagicMock() + self.flavor.client.return_value = self.octavia_client + + self.flavor.client_plugin().client = mock.MagicMock( + return_value=self.octavia_client) + self.patchobject(self.flavor, 'physical_resource_name', + return_value='resource_name') + + def test_create(self): + self._create_stack() + self.octavia_client.flavor_show.side_effect = [ + {'flavor': {'id': 'f123'}} + ] + expected = { + 'flavor': { + 'name': 'test_name', + 'description': 'test_description', + 'flavor_profile_id': 'test_flavor_profile_id', + 'enabled': True, + } + } + + self.flavor.handle_create() + + self.octavia_client.flavor_create.assert_called_with( + json=expected) + + def test_update(self): + self._create_stack() + self.flavor.resource_id_set('f123') + prop_diff = { + 'name': 'test_name2', + 'description': 'test_description2', + 'flavor_profile_id': 'test_flavor_profile_id2', + 'enabled': False, + } + + self.flavor.handle_update(None, None, prop_diff) + + self.octavia_client.flavor_set.assert_called_once_with( + 'f123', json={'flavor': prop_diff}) + + self.octavia_client.flavor_set.reset_mock() + + # Updating a flavor with None as name should use + # physical_resource_name() as new name + prop_diff = { + 'name': None, + 'description': 'test_description3', + 'flavor_profile_id': 'test_flavor_profile_id3', + 'enabled': True, + } + + self.flavor.handle_update(None, None, prop_diff) + + self.assertEqual(prop_diff['name'], 'resource_name') + self.octavia_client.flavor_set.assert_called_once_with( + 'f123', json={'flavor': prop_diff}) + + def test_delete(self): + self._create_stack() + self.flavor.resource_id_set('f123') + + self.flavor.handle_delete() + + self.octavia_client.flavor_delete.assert_called_with('f123') diff --git a/heat/tests/openstack/octavia/test_flavor_profile.py b/heat/tests/openstack/octavia/test_flavor_profile.py new file mode 100644 index 0000000000..0452e9639c --- /dev/null +++ b/heat/tests/openstack/octavia/test_flavor_profile.py @@ -0,0 +1,92 @@ +# +# 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 mock + +from heat.common import template_format +from heat.tests import common +from heat.tests.openstack.octavia import inline_templates +from heat.tests import utils + + +class FlavorProfileTest(common.HeatTestCase): + + def _create_stack(self, tmpl=inline_templates.FLAVORPROFILE_TEMPLATE): + self.t = template_format.parse(tmpl) + self.stack = utils.parse_stack(self.t) + self.flavor_profile = self.stack['flavor_profile'] + + self.octavia_client = mock.MagicMock() + self.flavor_profile.client = mock.MagicMock() + self.flavor_profile.client.return_value = self.octavia_client + + self.flavor_profile.client_plugin().client = mock.MagicMock( + return_value=self.octavia_client) + self.patchobject(self.flavor_profile, 'physical_resource_name', + return_value='resource_name') + + def test_create(self): + self._create_stack() + self.octavia_client.flavorprofile_show.side_effect = [ + {'flavorprofile': {'id': 'fp123'}} + ] + expected = { + 'flavorprofile': { + 'name': 'test_flavor_profile', + 'provider_name': 'test_provider', + 'flavor_data': '{"flavor_data_key": "flavor_data_value"}\n' + } + } + + self.flavor_profile.handle_create() + + self.octavia_client.flavorprofile_create.assert_called_with( + json=expected) + + def test_update(self): + self._create_stack() + self.flavor_profile.resource_id_set('f123') + prop_diff = { + 'name': 'test_flavor_profile2', + 'provider_name': 'test_provider2', + 'flavor_data': '{"flavor_data_key2": "flavor_data_value2"}\n' + } + + self.flavor_profile.handle_update(None, None, prop_diff) + + self.octavia_client.flavorprofile_set.assert_called_once_with( + 'f123', json={'flavorprofile': prop_diff}) + + self.octavia_client.flavorprofile_set.reset_mock() + + # Updating a flavor profile with None as name should use + # physical_resource_name() as new name + prop_diff = { + 'name': None, + 'provider_name': 'test_provider3', + 'flavor_data': '{"flavor_data_key3": "flavor_data_value3"}\n' + } + + self.flavor_profile.handle_update(None, None, prop_diff) + + self.assertEqual(prop_diff['name'], 'resource_name') + self.octavia_client.flavorprofile_set.assert_called_once_with( + 'f123', json={'flavorprofile': prop_diff}) + + def test_delete(self): + self._create_stack() + self.flavor_profile.resource_id_set('f123') + + self.flavor_profile.handle_delete() + + self.octavia_client.flavorprofile_delete.assert_called_with('f123') diff --git a/heat/tests/openstack/octavia/test_loadbalancer.py b/heat/tests/openstack/octavia/test_loadbalancer.py index ad5b9c8b0c..56e527ab08 100644 --- a/heat/tests/openstack/octavia/test_loadbalancer.py +++ b/heat/tests/openstack/octavia/test_loadbalancer.py @@ -18,6 +18,7 @@ from osc_lib import exceptions from heat.common import exception from heat.common import template_format +from heat.engine.clients.os.octavia import OctaviaClientPlugin from heat.engine.resources.openstack.octavia import loadbalancer from heat.tests import common from heat.tests.openstack.octavia import inline_templates @@ -41,6 +42,8 @@ class LoadBalancerTest(common.HeatTestCase): self.patchobject(neutronV20, 'find_resourceid_by_name_or_id', return_value='123') + self.patchobject(OctaviaClientPlugin, 'get_flavor', + return_value='f123') self.lb.client_plugin().client = mock.MagicMock( return_value=self.octavia_client) @@ -58,6 +61,7 @@ class LoadBalancerTest(common.HeatTestCase): 'provider': 'octavia', 'project_id': '1234', 'admin_state_up': True, + 'flavor_id': 'f123', } } diff --git a/lower-constraints.txt b/lower-constraints.txt index 8fdc81b18f..b29c57e726 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -118,7 +118,7 @@ python-mistralclient==3.1.0 python-monascaclient==1.12.0 python-neutronclient==6.7.0 python-novaclient==9.1.0 -python-octaviaclient==1.3.0 +python-octaviaclient==1.8.0 python-openstackclient==3.12.0 python-saharaclient==1.4.0 python-subunit==1.2.0 diff --git a/releasenotes/notes/add-octavia-flavor-flavorprofile-support-90ef922d19591c60.yaml b/releasenotes/notes/add-octavia-flavor-flavorprofile-support-90ef922d19591c60.yaml new file mode 100644 index 0000000000..534a9d60d4 --- /dev/null +++ b/releasenotes/notes/add-octavia-flavor-flavorprofile-support-90ef922d19591c60.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add support for ``OS::Octavia::Flavor`` and ``OS::Octavia::FlavorProfile`` + resources and add ``flavor`` parameter in ``OS::Octavia::LoadBalancer``, + allowing users to configure Load Balancer capabilities. diff --git a/requirements.txt b/requirements.txt index e0109c9d40..a62119c609 100644 --- a/requirements.txt +++ b/requirements.txt @@ -45,7 +45,7 @@ python-mistralclient!=3.2.0,>=3.1.0 # Apache-2.0 python-monascaclient>=1.12.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 python-novaclient>=9.1.0 # Apache-2.0 -python-octaviaclient>=1.3.0 # Apache-2.0 +python-octaviaclient>=1.8.0 # Apache-2.0 python-openstackclient>=3.12.0 # Apache-2.0 python-saharaclient>=1.4.0 # Apache-2.0 python-swiftclient>=3.2.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 8301eab12a..43473c7c21 100644 --- a/setup.cfg +++ b/setup.cfg @@ -157,6 +157,8 @@ heat.constraints = octavia.loadbalancer = heat.engine.clients.os.octavia:LoadbalancerConstraint octavia.l7policy = heat.engine.clients.os.octavia:L7PolicyConstraint octavia.pool = heat.engine.clients.os.octavia:PoolConstraint + octavia.flavor = heat.engine.clients.os.octavia:FlavorConstraint + octavia.flavorprofile = heat.engine.clients.os.octavia:FlavorProfileConstraint sahara.cluster = heat.engine.clients.os.sahara:ClusterConstraint sahara.cluster_template = heat.engine.clients.os.sahara:ClusterTemplateConstraint sahara.data_source = heat.engine.clients.os.sahara:DataSourceConstraint