diff --git a/etc/policy.json b/etc/policy.json index 930b1a6be42..3d9830d610a 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -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": "", diff --git a/neutron/db/ipam_backend_mixin.py b/neutron/db/ipam_backend_mixin.py index 0db227326d6..eb179d84508 100644 --- a/neutron/db/ipam_backend_mixin.py +++ b/neutron/db/ipam_backend_mixin.py @@ -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): diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 78a932967a0..ee7f95f6512 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -030a959ceafa +a5648cfeeadf diff --git a/neutron/db/migration/alembic_migrations/versions/newton/expand/a5648cfeeadf_add_subnet_service_types.py b/neutron/db/migration/alembic_migrations/versions/newton/expand/a5648cfeeadf_add_subnet_service_types.py new file mode 100644 index 00000000000..bcbfd87a72f --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/newton/expand/a5648cfeeadf_add_subnet_service_types.py @@ -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') + ) diff --git a/neutron/db/subnet_service_type_db_models.py b/neutron/db/subnet_service_type_db_models.py new file mode 100644 index 00000000000..341b4641f12 --- /dev/null +++ b/neutron/db/subnet_service_type_db_models.py @@ -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]) diff --git a/neutron/extensions/subnet_service_types.py b/neutron/extensions/subnet_service_types.py new file mode 100644 index 00000000000..773aa2381b1 --- /dev/null +++ b/neutron/extensions/subnet_service_types.py @@ -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 {} diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 2c616b62a5f..c5f67f84feb 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -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): diff --git a/neutron/tests/etc/policy.json b/neutron/tests/etc/policy.json index 930b1a6be42..3d9830d610a 100644 --- a/neutron/tests/etc/policy.json +++ b/neutron/tests/etc/policy.json @@ -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": "", diff --git a/neutron/tests/unit/db/test_db_base_plugin_v2.py b/neutron/tests/unit/db/test_db_base_plugin_v2.py index 9497ef34f4a..f93ed38cd3a 100644 --- a/neutron/tests/unit/db/test_db_base_plugin_v2.py +++ b/neutron/tests/unit/db/test_db_base_plugin_v2.py @@ -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, diff --git a/neutron/tests/unit/extensions/test_subnet_service_types.py b/neutron/tests/unit/extensions/test_subnet_service_types.py new file mode 100644 index 00000000000..c53d345e292 --- /dev/null +++ b/neutron/tests/unit/extensions/test_subnet_service_types.py @@ -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