Enable CRUD for Subnet Service Types

This patch enables basic CRUD operations to support
Subnet service-types.

Partially-implements: blueprint service-subnets

Co-Authored-By: John Davidge <john.davidge@rackspace.com>

Change-Id: I0a1724ad00f0a3e675bb700cdd291f55f898c6f3
This commit is contained in:
Brian Haley 2016-06-23 17:48:13 -04:00 committed by Kevin Benton
parent 87517709f2
commit eead641242
10 changed files with 389 additions and 4 deletions

View File

@ -17,9 +17,11 @@
"create_subnet": "rule:admin_or_network_owner",
"create_subnet:segment_id": "rule:admin_only",
"create_subnet:service_types": "rule:admin_only",
"get_subnet": "rule:admin_or_owner or rule:shared",
"get_subnet:segment_id": "rule:admin_only",
"update_subnet": "rule:admin_or_network_owner",
"update_subnet:service_types": "rule:admin_only",
"delete_subnet": "rule:admin_or_network_owner",
"create_subnetpool": "",

View File

@ -35,6 +35,7 @@ from neutron.common import utils as common_utils
from neutron.db import db_base_plugin_common
from neutron.db import models_v2
from neutron.db import segments_db
from neutron.db import subnet_service_type_db_models as service_type_db
from neutron.extensions import portbindings
from neutron.extensions import segment
from neutron.ipam import utils as ipam_utils
@ -177,6 +178,20 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
del s['allocation_pools']
return result_pools
def _update_subnet_service_types(self, context, subnet_id, s):
old_types = context.session.query(
service_type_db.SubnetServiceType).filter_by(
subnet_id=subnet_id)
for service_type in old_types:
context.session.delete(service_type)
updated_types = s.pop('service_types')
for service_type in updated_types:
new_type = service_type_db.SubnetServiceType(
subnet_id=subnet_id,
service_type=service_type)
context.session.add(new_type)
return updated_types
def update_db_subnet(self, context, subnet_id, s, oldpools):
changes = {}
if "dns_nameservers" in s:
@ -191,6 +206,10 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
changes['allocation_pools'] = (
self._update_subnet_allocation_pools(context, subnet_id, s))
if "service_types" in s:
changes['service_types'] = (
self._update_subnet_service_types(context, subnet_id, s))
subnet = self._get_subnet(context, subnet_id)
subnet.update(s)
return subnet, changes
@ -472,6 +491,8 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
subnet_args['subnetpool_id'],
subnet_args['ip_version'])
service_types = subnet_args.pop('service_types', [])
subnet = models_v2.Subnet(**subnet_args)
segment_id = subnet_args.get('segment_id')
try:
@ -499,6 +520,13 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
nexthop=rt['nexthop'])
context.session.add(route)
if validators.is_attr_set(service_types):
for service_type in service_types:
service_type_entry = service_type_db.SubnetServiceType(
subnet_id=subnet.id,
service_type=service_type)
context.session.add(service_type_entry)
self.save_allocation_pools(context, subnet,
subnet_request.allocation_pools)
@ -598,6 +626,8 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
detail, subnet, subnetpool_id)
if validators.is_attr_set(subnet.get(segment.SEGMENT_ID)):
args['segment_id'] = subnet[segment.SEGMENT_ID]
if validators.is_attr_set(subnet.get('service_types')):
args['service_types'] = subnet['service_types']
return args
def update_port(self, context, old_port_db, old_port, new_port):

View File

@ -1 +1 @@
030a959ceafa
a5648cfeeadf

View File

@ -0,0 +1,39 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company, LP
#
# 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.
#
"""Add support for Subnet Service Types
Revision ID: a5648cfeeadf
Revises: 030a959ceafa
Create Date: 2016-03-15 18:00:00.190173
"""
# revision identifiers, used by Alembic.
revision = 'a5648cfeeadf'
down_revision = '030a959ceafa'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table('subnet_service_types',
sa.Column('subnet_id', sa.String(length=36)),
sa.Column('service_type', sa.String(length=255)),
sa.ForeignKeyConstraint(['subnet_id'], ['subnets.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('subnet_id', 'service_type')
)

View File

@ -0,0 +1,55 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company, LP
# All Rights Reserved.
#
# 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 sqlalchemy as sa
from sqlalchemy import orm
from neutron.api.v2 import attributes
from neutron.db import common_db_mixin
from neutron.db import model_base
from neutron.db import models_v2
class SubnetServiceType(model_base.BASEV2):
"""Subnet Service Types table"""
__tablename__ = "subnet_service_types"
subnet_id = sa.Column(sa.String(36),
sa.ForeignKey('subnets.id', ondelete="CASCADE"))
# Service types must be valid device owners, therefore share max length
service_type = sa.Column(sa.String(
length=attributes.DEVICE_OWNER_MAX_LEN))
subnet = orm.relationship(models_v2.Subnet,
backref=orm.backref('service_types',
lazy='joined',
cascade='all, delete-orphan',
uselist=True))
__table_args__ = (
sa.PrimaryKeyConstraint('subnet_id', 'service_type'),
model_base.BASEV2.__table_args__
)
class SubnetServiceTypeMixin(object):
"""Mixin class to extend subnet with service type attribute"""
def _extend_subnet_service_types(self, subnet_res, subnet_db):
subnet_res['service_types'] = [service_type['service_type'] for
service_type in
subnet_db.service_types]
common_db_mixin.CommonDbMixin.register_dict_extend_funcs(
attributes.SUBNETS, [_extend_subnet_service_types])

View File

@ -0,0 +1,87 @@
# 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 neutron_lib.api import validators
from neutron_lib import constants
from neutron_lib import exceptions
import webob.exc
from neutron._i18n import _
from neutron.api import extensions
from neutron.api.v2 import attributes
# List for service plugins to register their own prefixes
valid_prefixes = []
class InvalidSubnetServiceType(exceptions.InvalidInput):
message = _("Subnet service type %(service_type)s does not correspond "
"to a valid device owner.")
def _validate_subnet_service_types(service_types, valid_values=None):
if service_types:
if not isinstance(service_types, list):
raise webob.exc.HTTPBadRequest(
_("Subnet service types must be a list."))
prefixes = valid_prefixes
# Include standard prefixes
prefixes += list(constants.DEVICE_OWNER_PREFIXES)
prefixes += constants.DEVICE_OWNER_COMPUTE_PREFIX
for service_type in service_types:
if not service_type.startswith(tuple(prefixes)):
raise InvalidSubnetServiceType(service_type=service_type)
validators.add_validator('type:validate_subnet_service_types',
_validate_subnet_service_types)
EXTENDED_ATTRIBUTES_2_0 = {
attributes.SUBNETS: {
'service_types': {'allow_post': True,
'allow_put': True,
'default': constants.ATTR_NOT_SPECIFIED,
'validate': {'type:validate_subnet_service_types':
None},
'is_visible': True, },
},
}
class Subnet_service_types(extensions.ExtensionDescriptor):
"""Extension class supporting subnet service types."""
@classmethod
def get_name(cls):
return "Subnet service types"
@classmethod
def get_alias(cls):
return "subnet-service-types"
@classmethod
def get_description(cls):
return "Provides ability to set the subnet service_types field"
@classmethod
def get_updated(cls):
return "2016-03-15T18:00:00-00:00"
def get_extended_resources(self, version):
if version == "2.0":
return EXTENDED_ATTRIBUTES_2_0
else:
return {}

View File

@ -62,6 +62,7 @@ from neutron.db.quota import driver # noqa
from neutron.db import securitygroups_db
from neutron.db import securitygroups_rpc_base as sg_db_rpc
from neutron.db import segments_db
from neutron.db import subnet_service_type_db_models as service_type_db
from neutron.db import vlantransparent_db
from neutron.extensions import allowedaddresspairs as addr_pair
from neutron.extensions import availability_zone as az_ext
@ -103,7 +104,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
addr_pair_db.AllowedAddressPairsMixin,
vlantransparent_db.Vlantransparent_db_mixin,
extradhcpopt_db.ExtraDhcpOptMixin,
address_scope_db.AddressScopeDbMixin):
address_scope_db.AddressScopeDbMixin,
service_type_db.SubnetServiceTypeMixin):
"""Implement the Neutron L2 abstractions using modules.
@ -131,7 +133,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
"address-scope",
"availability_zone",
"network_availability_zone",
"default-subnetpools"]
"default-subnetpools",
"subnet-service-types"]
@property
def supported_extension_aliases(self):

View File

@ -17,9 +17,11 @@
"create_subnet": "rule:admin_or_network_owner",
"create_subnet:segment_id": "rule:admin_only",
"create_subnet:service_types": "rule:admin_only",
"get_subnet": "rule:admin_or_owner or rule:shared",
"get_subnet:segment_id": "rule:admin_only",
"update_subnet": "rule:admin_or_network_owner",
"update_subnet:service_types": "rule:admin_only",
"delete_subnet": "rule:admin_or_network_owner",
"create_subnetpool": "",

View File

@ -332,7 +332,8 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
for arg in ('ip_version', 'tenant_id', 'subnetpool_id', 'prefixlen',
'enable_dhcp', 'allocation_pools', 'segment_id',
'dns_nameservers', 'host_routes',
'shared', 'ipv6_ra_mode', 'ipv6_address_mode'):
'shared', 'ipv6_ra_mode', 'ipv6_address_mode',
'service_types'):
# Arg must be present and not null (but can be false)
if kwargs.get(arg) is not None:
data['subnet'][arg] = kwargs[arg]
@ -625,6 +626,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
ipv6_ra_mode=None,
ipv6_address_mode=None,
tenant_id=None,
service_types=None,
set_context=False):
with optional_ctx(network, self.network,
set_context=set_context,

View File

@ -0,0 +1,165 @@
# 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 webob.exc
from neutron.db import db_base_plugin_v2
from neutron.extensions import subnet_service_types
from neutron.tests.unit.db import test_db_base_plugin_v2
class SubnetServiceTypesExtensionManager(object):
def get_resources(self):
return []
def get_actions(self):
return []
def get_request_extensions(self):
return []
def get_extended_resources(self, version):
return subnet_service_types.get_extended_resources(version)
class SubnetServiceTypesExtensionTestPlugin(
db_base_plugin_v2.NeutronDbPluginV2):
"""Test plugin to mixin the subnet service_types extension.
"""
supported_extension_aliases = ["subnet-service-types"]
class SubnetServiceTypesExtensionTestCase(
test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
"""Test API extension subnet_service_types attributes.
"""
CIDR = '10.0.0.0/8'
IP_VERSION = 4
def setUp(self):
plugin = ('neutron.tests.unit.extensions.test_subnet_service_types.' +
'SubnetServiceTypesExtensionTestPlugin')
ext_mgr = SubnetServiceTypesExtensionManager()
super(SubnetServiceTypesExtensionTestCase,
self).setUp(plugin=plugin, ext_mgr=ext_mgr)
def _create_service_subnet(self, service_types=None, network=None):
if not network:
with self.network() as network:
pass
network = network['network']
args = {'net_id': network['id'],
'tenant_id': network['tenant_id'],
'cidr': self.CIDR,
'ip_version': self.IP_VERSION}
if service_types:
args['service_types'] = service_types
return self._create_subnet(self.fmt, **args)
def _test_create_subnet(self, service_types, expect_fail=False):
res = self._create_service_subnet(service_types)
if expect_fail:
self.assertEqual(webob.exc.HTTPClientError.code,
res.status_int)
else:
subnet = self.deserialize('json', res)
subnet = subnet['subnet']
self.assertEqual(len(service_types),
len(subnet['service_types']))
for service in service_types:
self.assertIn(service, subnet['service_types'])
def test_create_subnet_blank_type(self):
self._test_create_subnet([])
def test_create_subnet_bar_type(self):
self._test_create_subnet(['network:bar'])
def test_create_subnet_foo_type(self):
self._test_create_subnet(['compute:foo'])
def test_create_subnet_bar_and_foo_type(self):
self._test_create_subnet(['network:bar', 'compute:foo'])
def test_create_subnet_invalid_type(self):
self._test_create_subnet(['foo'], expect_fail=True)
def test_create_subnet_no_type(self):
res = self._create_service_subnet()
subnet = self.deserialize('json', res)
subnet = subnet['subnet']
self.assertFalse(subnet['service_types'])
def _test_update_subnet(self, subnet, service_types, expect_fail=False):
data = {'subnet': {'service_types': service_types}}
req = self.new_update_request('subnets', data, subnet['id'])
res = self.deserialize(self.fmt, req.get_response(self.api))
if expect_fail:
self.assertEqual('InvalidSubnetServiceType',
res['NeutronError']['type'])
else:
subnet = res['subnet']
self.assertEqual(len(service_types),
len(subnet['service_types']))
for service in service_types:
self.assertIn(service, subnet['service_types'])
def test_update_subnet_zero_to_one(self):
service_types = ['network:foo']
# Create a subnet with no service type
res = self._create_service_subnet()
subnet = self.deserialize('json', res)['subnet']
# Update it with a single service type
self._test_update_subnet(subnet, service_types)
def test_update_subnet_one_to_two(self):
service_types = ['network:foo']
# Create a subnet with one service type
res = self._create_service_subnet(service_types)
subnet = self.deserialize('json', res)['subnet']
# Update it with two service types
service_types.append('compute:bar')
self._test_update_subnet(subnet, service_types)
def test_update_subnet_two_to_one(self):
service_types = ['network:foo', 'compute:bar']
# Create a subnet with two service types
res = self._create_service_subnet(service_types)
subnet = self.deserialize('json', res)['subnet']
# Update it with one service type
service_types = ['network:foo']
self._test_update_subnet(subnet, service_types)
def test_update_subnet_one_to_zero(self):
service_types = ['network:foo']
# Create a subnet with one service type
res = self._create_service_subnet(service_types)
subnet = self.deserialize('json', res)['subnet']
# Update it with zero service types
service_types = []
self._test_update_subnet(subnet, service_types)
def test_update_subnet_invalid_type(self):
service_types = ['foo']
# Create a subnet with no service type
res = self._create_service_subnet()
subnet = self.deserialize('json', res)['subnet']
# Update it with an invalid service type
self._test_update_subnet(subnet, service_types, expect_fail=True)
class SubnetServiceTypesExtensionTestCasev6(
SubnetServiceTypesExtensionTestCase):
CIDR = '2001:db8::/64'
IP_VERSION = 6