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:
parent
87517709f2
commit
eead641242
|
@ -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": "",
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -1 +1 @@
|
|||
030a959ceafa
|
||||
a5648cfeeadf
|
||||
|
|
|
@ -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')
|
||||
)
|
|
@ -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])
|
|
@ -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 {}
|
|
@ -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):
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue