Add support for Octavia's Flavor and FlavorProfile resources

Added OS::Octavia::Flavor and OS::Octavia::FlavorProfile support.
Added flavor parameter in OS::Octavia::LoadBalancer.

Flavor and FlavorProfile allow to configure/tune Load Balancer
capabilities (enable/disable HA, etc...)

Story: 2007081
Task: 37993

Change-Id: If31a888e5867ac6941ff0d515d4b88894fb97572
This commit is contained in:
Gregory Thiemonge 2020-01-09 18:37:57 +01:00
parent 3ab2e2ab89
commit 68a8219315
13 changed files with 503 additions and 6 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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