Add SubnetPool neutron Resource

This patch adds OS::Neutron::SubnetPool resource.

Change-Id: I5624272a033c69356b7d81cf4af0095ad7ef672f
Depends-On: I3d577423bd6614e76afec8f787459fc53a072b0c
Blueprint: subnet-pools
This commit is contained in:
Rabi Mishra 2015-11-16 11:59:02 +05:30
parent 3d75cc8d0c
commit c716c8be3f
5 changed files with 557 additions and 0 deletions

26
heat/common/netutils.py Normal file
View File

@ -0,0 +1,26 @@
#
# 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 netaddr
def is_prefix_subset(orig_prefixes, new_prefixes):
"""Check whether orig_prefixes is subset of new_prefixes.
This takes valid prefix lists for orig_prefixes and new_prefixes,
returns 'True', if orig_prefixes is subset of new_prefixes.
"""
orig_set = netaddr.IPSet(orig_prefixes)
new_set = netaddr.IPSet(new_prefixes)
return orig_set.issubset(new_set)

View File

@ -0,0 +1,212 @@
#
# 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 neutronclient.neutron import v2_0 as nV20
from heat.common import exception
from heat.common.i18n import _
from heat.common import netutils
from heat.engine import constraints
from heat.engine import properties
from heat.engine.resources.openstack.neutron import neutron
from heat.engine import support
class SubnetPool(neutron.NeutronResource):
"""A resource that implements neutron subnet pool.
This resource can be used to create a subnet pool with a large block
of addresses and create subnets from it.
"""
support_status = support.SupportStatus(version='6.0.0')
required_service_extension = 'subnet_allocation'
PROPERTIES = (
NAME, PREFIXES, ADDRESS_SCOPE, DEFAULT_QUOTA,
DEFAULT_PREFIXLEN, MIN_PREFIXLEN, MAX_PREFIXLEN,
IS_DEFAULT, TENANT_ID, SHARED,
) = (
'name', 'prefixes', 'address_scope', 'default_quota',
'default_prefixlen', 'min_prefixlen', 'max_prefixlen',
'is_default', 'tenant_id', 'shared',
)
properties_schema = {
NAME: properties.Schema(
properties.Schema.STRING,
_('Name of the subnet pool.'),
update_allowed=True
),
PREFIXES: properties.Schema(
properties.Schema.LIST,
_('List of subnet prefixes to assign.'),
schema=properties.Schema(
properties.Schema.STRING,
constraints=[
constraints.CustomConstraint('net_cidr'),
],
),
constraints=[constraints.Length(min=1)],
required=True,
update_allowed=True,
),
ADDRESS_SCOPE: properties.Schema(
properties.Schema.STRING,
_('An address scope ID to assign to the subnet pool.'),
constraints=[
constraints.CustomConstraint('neutron.address_scope')
],
update_allowed=True,
),
DEFAULT_QUOTA: properties.Schema(
properties.Schema.INTEGER,
_('A per-tenant quota on the prefix space that can be allocated '
'from the subnet pool for tenant subnets.'),
constraints=[constraints.Range(min=0)],
update_allowed=True,
),
DEFAULT_PREFIXLEN: properties.Schema(
properties.Schema.INTEGER,
_('The size of the prefix to allocate when the cidr or '
'prefixlen attributes are not specified while creating '
'a subnet.'),
constraints=[constraints.Range(min=0)],
update_allowed=True,
),
MIN_PREFIXLEN: properties.Schema(
properties.Schema.INTEGER,
_('Smallest prefix size that can be allocated '
'from the subnet pool.'),
constraints=[constraints.Range(min=0)],
update_allowed=True,
),
MAX_PREFIXLEN: properties.Schema(
properties.Schema.INTEGER,
_('Maximum prefix size that can be allocated '
'from the subnet pool.'),
constraints=[constraints.Range(min=0)],
update_allowed=True,
),
IS_DEFAULT: properties.Schema(
properties.Schema.BOOLEAN,
_('Whether this is default IPv4/IPv6 subnet pool.'
'There can only be one default subnet pool for each IP family.'
'Note that the default policy setting restricts administrative '
'users to set this to True'),
default=False,
update_allowed=True,
),
TENANT_ID: properties.Schema(
properties.Schema.STRING,
_('The ID of the tenant who owns the subnet pool. Only '
'administrative users can specify a tenant ID '
'other than their own.')
),
SHARED: properties.Schema(
properties.Schema.BOOLEAN,
_('Whether the subnet pool will be shared across all tenants.'
'Note that the default policy setting restricts usage of this '
'attribute to administrative users only.'),
default=False,
),
}
def validate(self):
super(SubnetPool, self).validate()
self._validate_prefix_bounds()
def _validate_prefix_bounds(self):
min_prefixlen = self.properties[self.MIN_PREFIXLEN]
default_prefixlen = self.properties[self.DEFAULT_PREFIXLEN]
max_prefixlen = self.properties[self.MAX_PREFIXLEN]
msg_fmt = _('Illegal prefix bounds: %(key1)s=%(value1)s, '
'%(key2)s=%(value2)s.')
# min_prefixlen can not be greater than max_prefixlen
if min_prefixlen and max_prefixlen and min_prefixlen > max_prefixlen:
msg = msg_fmt % dict(key1=self.MAX_PREFIXLEN,
value1=max_prefixlen,
key2=self.MIN_PREFIXLEN,
value2=min_prefixlen)
raise exception.StackValidationFailed(message=msg)
if default_prefixlen:
# default_prefixlen can not be greater than max_prefixlen
if max_prefixlen and default_prefixlen > max_prefixlen:
msg = msg_fmt % dict(key1=self.MAX_PREFIXLEN,
value1=max_prefixlen,
key2=self.DEFAULT_PREFIXLEN,
value2=default_prefixlen)
raise exception.StackValidationFailed(message=msg)
# min_prefixlen can not be greater than default_prefixlen
if min_prefixlen and min_prefixlen > default_prefixlen:
msg = msg_fmt % dict(key1=self.MIN_PREFIXLEN,
value1=min_prefixlen,
key2=self.DEFAULT_PREFIXLEN,
value2=default_prefixlen)
raise exception.StackValidationFailed(message=msg)
def _validate_prefixes_for_update(self, prop_diff):
old_prefixes = self.properties[self.PREFIXES]
new_prefixes = prop_diff[self.PREFIXES]
# check new_prefixes is a superset of old_prefixes
if not netutils.is_prefix_subset(old_prefixes, new_prefixes):
msg = (_('Property %(key)s updated value %(new)s should '
'be superset of existing value '
'%(old)s.') % dict(key=self.PREFIXES,
new=sorted(new_prefixes),
old=sorted(old_prefixes)))
raise exception.StackValidationFailed(message=msg)
def handle_create(self):
props = self.prepare_properties(
self.properties,
self.physical_resource_name())
if self.ADDRESS_SCOPE in props and props[self.ADDRESS_SCOPE]:
props['address_scope_id'] = nV20.find_resourceid_by_name_or_id(
self.client(), 'address_scope', props.pop(self.ADDRESS_SCOPE))
subnetpool = self.client().create_subnetpool(
{'subnetpool': props})['subnetpool']
self.resource_id_set(subnetpool['id'])
def handle_delete(self):
if self.resource_id is not None:
with self.client_plugin().ignore_not_found:
self.client().delete_subnetpool(self.resource_id)
def _show_resource(self):
return self.client().show_subnetpool(self.resource_id)['subnetpool']
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
# check that new prefixes are superset of existing prefixes
if self.PREFIXES in prop_diff:
self._validate_prefixes_for_update(prop_diff)
if self.ADDRESS_SCOPE in prop_diff:
if prop_diff[self.ADDRESS_SCOPE]:
prop_diff[
'address_scope_id'] = nV20.find_resourceid_by_name_or_id(
self.client(), 'address_scope',
prop_diff.pop(self.ADDRESS_SCOPE))
else:
prop_diff[
'address_scope_id'] = prop_diff.pop(self.ADDRESS_SCOPE)
if prop_diff:
self.client().update_subnetpool(
self.resource_id, {'subnetpool': prop_diff})
def resource_mapping():
return {
'OS::Neutron::SubnetPool': SubnetPool,
}

View File

@ -265,6 +265,10 @@ class HeatTestCase(testscenarios.WithScenarios,
validate = self.patchobject(neutron.SubnetConstraint, 'validate')
validate.return_value = True
def stub_AddressScopeConstraint_validate(self):
validate = self.patchobject(neutron.AddressScopeConstraint, 'validate')
validate.return_value = True
def stub_RouterConstraint_validate(self):
validate = self.patchobject(neutron.RouterConstraint, 'validate')
validate.return_value = True

View File

@ -0,0 +1,44 @@
#
# 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.
SPOOL_TEMPLATE = '''
heat_template_version: 2015-04-30
description: Template to test subnetpool Neutron resource
resources:
sub_pool:
type: OS::Neutron::SubnetPool
properties:
name: the_sp
prefixes:
- 10.1.0.0/16
address_scope: test
default_quota: 2
default_prefixlen: 28
min_prefixlen: 8
max_prefixlen: 32
is_default: False
tenant_id: c1210485b2424d48804aad5d39c61b8f
shared: False
'''
SPOOL_MINIMAL_TEMPLATE = '''
heat_template_version: 2015-04-30
description: Template to test subnetpool Neutron resource
resources:
sub_pool:
type: OS::Neutron::SubnetPool
properties:
prefixes:
- 10.0.0.0/16
- 10.1.0.0/16
'''

View File

@ -0,0 +1,271 @@
#
# 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 neutronclient.common import exceptions as qe
from neutronclient.neutron import v2_0 as neutronV20
from neutronclient.v2_0 import client as neutronclient
import six
from heat.common import exception
from heat.common import template_format
from heat.engine.clients.os import neutron
from heat.engine.resources.openstack.neutron import subnetpool
from heat.engine import rsrc_defn
from heat.engine import scheduler
from heat.tests import common
from heat.tests.openstack.neutron import inline_templates
from heat.tests import utils
class NeutronSubnetPoolTest(common.HeatTestCase):
def setUp(self):
super(NeutronSubnetPoolTest, self).setUp()
self.patchobject(neutron.NeutronClientPlugin, 'has_extension',
return_value=True)
self.find_resource = self.patchobject(neutronV20,
'find_resourceid_by_name_or_id',
return_value='new_test')
def create_subnetpool(self, status='COMPLETE'):
self.t = template_format.parse(inline_templates.SPOOL_TEMPLATE)
self.stack = utils.parse_stack(self.t)
resource_defns = self.stack.t.resource_definitions(self.stack)
rsrc = subnetpool.SubnetPool('sub_pool', resource_defns['sub_pool'],
self.stack)
if status == 'FAILED':
self.patchobject(neutronclient.Client, 'create_subnetpool',
side_effect=qe.NeutronClientException(
status_code=500))
error = self.assertRaises(exception.ResourceFailure,
scheduler.TaskRunner(rsrc.create))
self.assertEqual(
'NeutronClientException: resources.sub_pool: '
'An unknown exception occurred.',
six.text_type(error))
else:
self.patchobject(neutronclient.Client, 'create_subnetpool',
return_value={'subnetpool': {
'id': 'fc68ea2c-b60b-4b4f-bd82-94ec81110766'
}})
scheduler.TaskRunner(rsrc.create)()
self.assertEqual((rsrc.CREATE, status), rsrc.state)
return rsrc
def test_resource_mapping(self):
self.t = template_format.parse(inline_templates.SPOOL_TEMPLATE)
self.stack = utils.parse_stack(self.t)
rsrc = self.stack['sub_pool']
mapping = subnetpool.resource_mapping()
self.assertEqual(1, len(mapping))
self.assertEqual(subnetpool.SubnetPool,
mapping['OS::Neutron::SubnetPool'])
self.assertIsInstance(rsrc, subnetpool.SubnetPool)
def test_validate_prefixlen_min_gt_max(self):
self.t = template_format.parse(inline_templates.SPOOL_TEMPLATE)
props = self.t['resources']['sub_pool']['properties']
props['min_prefixlen'] = 28
props['max_prefixlen'] = 24
self.stack = utils.parse_stack(self.t)
rsrc = self.stack['sub_pool']
errMessage = ('Illegal prefix bounds: max_prefixlen=24, '
'min_prefixlen=28.')
error = self.assertRaises(exception.StackValidationFailed,
rsrc.validate)
self.assertEqual(errMessage, six.text_type(error))
def test_validate_prefixlen_default_gt_max(self):
self.t = template_format.parse(inline_templates.SPOOL_TEMPLATE)
props = self.t['resources']['sub_pool']['properties']
props['default_prefixlen'] = 28
props['max_prefixlen'] = 24
self.stack = utils.parse_stack(self.t)
rsrc = self.stack['sub_pool']
errMessage = ('Illegal prefix bounds: max_prefixlen=24, '
'default_prefixlen=28.')
error = self.assertRaises(exception.StackValidationFailed,
rsrc.validate)
self.assertEqual(errMessage, six.text_type(error))
def test_validate_prefixlen_min_gt_default(self):
self.t = template_format.parse(inline_templates.SPOOL_TEMPLATE)
props = self.t['resources']['sub_pool']['properties']
props['min_prefixlen'] = 28
props['default_prefixlen'] = 24
self.stack = utils.parse_stack(self.t)
rsrc = self.stack['sub_pool']
errMessage = ('Illegal prefix bounds: min_prefixlen=28, '
'default_prefixlen=24.')
error = self.assertRaises(exception.StackValidationFailed,
rsrc.validate)
self.assertEqual(errMessage, six.text_type(error))
def test_validate_minimal(self):
self.t = template_format.parse(inline_templates.SPOOL_MINIMAL_TEMPLATE)
self.stack = utils.parse_stack(self.t)
rsrc = self.stack['sub_pool']
self.assertIsNone(rsrc.validate())
def test_create_subnetpool(self):
rsrc = self.create_subnetpool()
ref_id = rsrc.FnGetRefId()
self.assertEqual('fc68ea2c-b60b-4b4f-bd82-94ec81110766', ref_id)
def test_create_subnetpool_failed(self):
self.create_subnetpool('FAILED')
def test_delete_subnetpool(self):
self.patchobject(neutronclient.Client, 'delete_subnetpool')
rsrc = self.create_subnetpool()
ref_id = rsrc.FnGetRefId()
self.assertEqual('fc68ea2c-b60b-4b4f-bd82-94ec81110766', ref_id)
self.assertIsNone(scheduler.TaskRunner(rsrc.delete)())
self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state)
def test_delete_subnetpool_not_found(self):
self.patchobject(neutronclient.Client, 'delete_subnetpool',
side_effect=qe.NotFound(status_code=404))
rsrc = self.create_subnetpool()
ref_id = rsrc.FnGetRefId()
self.assertEqual('fc68ea2c-b60b-4b4f-bd82-94ec81110766', ref_id)
self.assertIsNone(scheduler.TaskRunner(rsrc.delete)())
self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state)
def test_delete_subnetpool_resource_id_none(self):
delete_pool = self.patchobject(neutronclient.Client,
'delete_subnetpool')
rsrc = self.create_subnetpool()
rsrc.resource_id = None
self.assertIsNone(scheduler.TaskRunner(rsrc.delete)())
delete_pool.assert_not_called()
def test_update_subnetpool(self):
update_subnetpool = self.patchobject(neutronclient.Client,
'update_subnetpool')
rsrc = self.create_subnetpool()
ref_id = rsrc.FnGetRefId()
self.assertEqual('fc68ea2c-b60b-4b4f-bd82-94ec81110766', ref_id)
props = {
'name': 'the_new_sp',
'prefixes': [
'10.1.0.0/16',
'10.2.0.0/16'],
'address_scope': 'new_test',
'default_quota': '16',
'default_prefixlen': '24',
'min_prefixlen': '24',
'max_prefixlen': '28',
'is_default': False,
}
update_snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(),
props)
self.assertIsNone(rsrc.handle_update(update_snippet, {}, props))
self.assertEqual(1, update_subnetpool.call_count)
def test_update_subnetpool_no_prop_diff(self):
update_subnetpool = self.patchobject(neutronclient.Client,
'update_subnetpool')
rsrc = self.create_subnetpool()
ref_id = rsrc.FnGetRefId()
self.assertEqual('fc68ea2c-b60b-4b4f-bd82-94ec81110766', ref_id)
update_snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(),
rsrc.t)
self.assertIsNone(rsrc.handle_update(update_snippet, {}, {}))
update_subnetpool.assert_not_called()
def test_update_subnetpool_validate_prefixes(self):
update_subnetpool = self.patchobject(neutronclient.Client,
'update_subnetpool')
rsrc = self.create_subnetpool()
ref_id = rsrc.FnGetRefId()
self.assertEqual('fc68ea2c-b60b-4b4f-bd82-94ec81110766', ref_id)
prefix_old = rsrc.properties['prefixes']
props = {
'name': 'the_new_sp',
'prefixes': ['10.5.0.0/16']
}
prefix_new = props['prefixes']
update_snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(),
props)
errMessage = ('Property prefixes updated value %(value1)s '
'should be superset of existing value %(value2)s.'
% dict(value1=sorted(prefix_new),
value2=sorted(prefix_old)))
error = self.assertRaises(exception.StackValidationFailed,
rsrc.handle_update,
update_snippet, {}, props)
self.assertEqual(errMessage, six.text_type(error))
update_subnetpool.assert_not_called()
props = {
'name': 'the_new_sp',
'prefixes': ['10.0.0.0/8',
'10.6.0.0/16'],
}
update_snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(),
props)
self.assertIsNone(rsrc.handle_update(update_snippet, {}, props))
update_subnetpool.assert_called_once_with(
'fc68ea2c-b60b-4b4f-bd82-94ec81110766',
{'subnetpool': props})
def test_update_subnetpool_update_address_scope(self):
update_subnetpool = self.patchobject(neutronclient.Client,
'update_subnetpool')
rsrc = self.create_subnetpool()
ref_id = rsrc.FnGetRefId()
self.assertEqual('fc68ea2c-b60b-4b4f-bd82-94ec81110766', ref_id)
props = {
'name': 'the_new_sp',
'address_scope': 'new_test',
'prefixes': ['10.0.0.0/8',
'10.6.0.0/16'],
}
update_dict = {
'name': 'the_new_sp',
'address_scope_id': 'new_test',
'prefixes': ['10.0.0.0/8',
'10.6.0.0/16'],
}
update_snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(),
props)
self.assertIsNone(rsrc.handle_update(update_snippet, {}, props))
self.assertEqual(3, self.find_resource.call_count)
update_subnetpool.assert_called_once_with(
'fc68ea2c-b60b-4b4f-bd82-94ec81110766',
{'subnetpool': update_dict})
def test_update_subnetpool_remove_address_scope(self):
update_subnetpool = self.patchobject(neutronclient.Client,
'update_subnetpool')
rsrc = self.create_subnetpool()
ref_id = rsrc.FnGetRefId()
self.assertEqual('fc68ea2c-b60b-4b4f-bd82-94ec81110766', ref_id)
props = {
'name': 'the_new_sp',
'prefixes': ['10.0.0.0/8',
'10.6.0.0/16'],
}
props_diff = {'address_scope': None}
update_snippet = rsrc_defn.ResourceDefinition(rsrc.name, rsrc.type(),
props)
self.assertIsNone(rsrc.handle_update(update_snippet, {}, props_diff))
self.assertEqual(2, self.find_resource.call_count)
update_subnetpool.assert_called_once_with(
'fc68ea2c-b60b-4b4f-bd82-94ec81110766',
{'subnetpool': props_diff})