From dd20cab3713712126470145b22345a7217576faa Mon Sep 17 00:00:00 2001 From: Hang Yang Date: Fri, 26 Jun 2020 15:16:37 -0500 Subject: [PATCH] Support Address Group CRUD as extensions Add support for basic address group CRUD. Subsequent patches will be added to use address groups in security group rules. Implements: blueprint address-groups-in-sg-rules Change-Id: I4555c068ec6229b1d7ac1168d5687549370893b4 --- lower-constraints.txt | 2 +- neutron/agent/securitygroups_rpc.py | 2 + neutron/db/address_group_db.py | 124 +++++++++++ .../alembic_migrations/versions/EXPAND_HEAD | 2 +- .../expand/1ea5dab0897a_add_address_group.py | 53 +++++ neutron/db/models/address_group.py | 41 ++++ neutron/extensions/address_group.py | 80 +++++++ neutron/objects/address_group.py | 65 ++++++ neutron/plugins/ml2/plugin.py | 8 +- .../unit/extensions/test_address_group.py | 205 ++++++++++++++++++ .../tests/unit/objects/test_address_group.py | 50 +++++ neutron/tests/unit/objects/test_base.py | 12 + neutron/tests/unit/objects/test_objects.py | 2 + requirements.txt | 2 +- 14 files changed, 643 insertions(+), 5 deletions(-) create mode 100644 neutron/db/address_group_db.py create mode 100644 neutron/db/migration/alembic_migrations/versions/victoria/expand/1ea5dab0897a_add_address_group.py create mode 100644 neutron/db/models/address_group.py create mode 100644 neutron/extensions/address_group.py create mode 100644 neutron/objects/address_group.py create mode 100644 neutron/tests/unit/extensions/test_address_group.py create mode 100644 neutron/tests/unit/objects/test_address_group.py diff --git a/lower-constraints.txt b/lower-constraints.txt index c1e002de288..b71766043d2 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -51,7 +51,7 @@ msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 -neutron-lib==2.4.0 +neutron-lib==2.5.0 openstacksdk==0.31.2 os-client-config==1.28.0 os-ken==0.3.0 diff --git a/neutron/agent/securitygroups_rpc.py b/neutron/agent/securitygroups_rpc.py index 87365265b04..15a18652311 100644 --- a/neutron/agent/securitygroups_rpc.py +++ b/neutron/agent/securitygroups_rpc.py @@ -50,6 +50,8 @@ def disable_security_group_extension_by_config(aliases): _disable_extension(stateful_sg.ALIAS, aliases) LOG.info('Disabled allowed-address-pairs extension.') _disable_extension('allowed-address-pairs', aliases) + LOG.info('Disabled address-group extension.') + _disable_extension('address-group', aliases) class SecurityGroupAgentRpc(object): diff --git a/neutron/db/address_group_db.py b/neutron/db/address_group_db.py new file mode 100644 index 00000000000..087b32a3a72 --- /dev/null +++ b/neutron/db/address_group_db.py @@ -0,0 +1,124 @@ +# 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 +from neutron_lib import constants +from neutron_lib.db import resource_extend +from neutron_lib.db import utils as db_utils +from neutron_lib.exceptions import address_group as ag_exc +from oslo_utils import uuidutils + +from neutron.extensions import address_group as ag_ext +from neutron.objects import address_group as ag_obj +from neutron.objects import base as base_obj + + +@resource_extend.has_resource_extenders +class AddressGroupDbMixin(ag_ext.AddressGroupPluginBase): + """Mixin class to add address group to db_base_plugin_v2.""" + + __native_bulk_support = True + + @staticmethod + def _make_address_group_dict(address_group, fields=None): + res = address_group.to_dict() + res['addresses'] = [str(addr_assoc['address']) + for addr_assoc in address_group['addresses']] + return db_utils.resource_fields(res, fields) + + def _get_address_group(self, context, id): + obj = ag_obj.AddressGroup.get_object(context, id=id) + if obj is None: + raise ag_exc.AddressGroupNotFound(address_group_id=id) + return obj + + def _dedup_and_compare_addresses(self, ag_obj, req_addrs): + ag_addrs = set(self._make_address_group_dict( + ag_obj, fields=['addresses'])['addresses']) + req_addrs = set(str(netaddr.IPNetwork(addr)) for addr in req_addrs) + addrs_in_ag = [] + addrs_not_in_ag = [] + for req_addr in req_addrs: + if req_addr in ag_addrs: + addrs_in_ag.append(req_addr) + else: + addrs_not_in_ag.append(req_addr) + return addrs_in_ag, addrs_not_in_ag + + def add_addresses(self, context, address_group_id, addresses): + ag = self._get_address_group(context, address_group_id) + addrs_in_ag, addrs_not_in_ag = self._dedup_and_compare_addresses( + ag, addresses['addresses']) + if addrs_in_ag: + raise ag_exc.AddressesAlreadyExist( + addresses=addrs_in_ag, address_group_id=address_group_id) + for addr in addrs_not_in_ag: + addr = netaddr.IPNetwork(addr) + args = {'address_group_id': address_group_id, + 'address': addr} + addr_assoc = ag_obj.AddressAssociation(context, **args) + addr_assoc.create() + ag.update() # reload synthetic fields + return {'address_group': self._make_address_group_dict(ag)} + + def remove_addresses(self, context, address_group_id, addresses): + ag = self._get_address_group(context, address_group_id) + addrs_in_ag, addrs_not_in_ag = self._dedup_and_compare_addresses( + ag, addresses['addresses']) + if addrs_not_in_ag: + raise ag_exc.AddressesNotFound( + addresses=addrs_not_in_ag, address_group_id=address_group_id) + for addr in addrs_in_ag: + ag_obj.AddressAssociation.delete_objects( + context, address_group_id=address_group_id, address=addr) + ag.update() # reload synthetic fields + return {'address_group': self._make_address_group_dict(ag)} + + def create_address_group(self, context, address_group): + """Create an address group.""" + fields = address_group['address_group'] + args = {'project_id': fields['tenant_id'], + 'id': uuidutils.generate_uuid(), + 'name': fields['name'], + 'description': fields['description']} + ag = ag_obj.AddressGroup(context, **args) + ag.create() + if fields.get('addresses') is not constants.ATTR_NOT_SPECIFIED: + self.add_addresses(context, ag.id, fields) + ag.update() # reload synthetic fields + return self._make_address_group_dict(ag) + + def update_address_group(self, context, id, address_group): + fields = address_group['address_group'] + ag = self._get_address_group(context, id) + ag.update_fields(fields) + ag.update() + return self._make_address_group_dict(ag) + + def get_address_group(self, context, id, fields=None): + ag = self._get_address_group(context, id) + return self._make_address_group_dict(ag, fields) + + def get_address_groups(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + pager = base_obj.Pager(sorts, limit, page_reverse, marker) + address_groups = ag_obj.AddressGroup.get_objects( + context, _pager=pager, **filters) + return [ + self._make_address_group_dict(addr_group, fields) + for addr_group in address_groups + ] + + def delete_address_group(self, context, id): + address_group = self._get_address_group(context, id) + address_group.delete() diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 4410f68cd04..5c4ebf7fc34 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -fd6107509ccd +1ea5dab0897a diff --git a/neutron/db/migration/alembic_migrations/versions/victoria/expand/1ea5dab0897a_add_address_group.py b/neutron/db/migration/alembic_migrations/versions/victoria/expand/1ea5dab0897a_add_address_group.py new file mode 100644 index 00000000000..c49d31840f5 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/victoria/expand/1ea5dab0897a_add_address_group.py @@ -0,0 +1,53 @@ +# 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 alembic import op +from neutron_lib.db import constants as db_const +import sqlalchemy as sa + + +"""add address group + +Revision ID: 1ea5dab0897a +Revises: fd6107509ccd +Create Date: 2020-07-02 18:43:28.380941 + +""" + +# revision identifiers, used by Alembic. +revision = '1ea5dab0897a' +down_revision = 'fd6107509ccd' + + +def upgrade(): + op.create_table( + 'address_groups', + sa.Column('project_id', sa.String( + length=db_const.PROJECT_ID_FIELD_SIZE), index=True), + sa.Column('id', sa.String(length=db_const.UUID_FIELD_SIZE), + primary_key=True), + sa.Column('name', sa.String(length=db_const.NAME_FIELD_SIZE), + nullable=True), + sa.Column('description', sa.String( + length=db_const.LONG_DESCRIPTION_FIELD_SIZE), nullable=True) + ) + + op.create_table( + 'address_associations', + sa.Column('address', sa.String(length=db_const.IP_ADDR_FIELD_SIZE), + primary_key=True), + sa.Column('address_group_id', sa.String( + length=db_const.UUID_FIELD_SIZE), primary_key=True), + sa.ForeignKeyConstraint(['address_group_id'], ['address_groups.id'], + ondelete='CASCADE') + ) diff --git a/neutron/db/models/address_group.py b/neutron/db/models/address_group.py new file mode 100644 index 00000000000..cec2e218560 --- /dev/null +++ b/neutron/db/models/address_group.py @@ -0,0 +1,41 @@ +# 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.db import constants as db_const +from neutron_lib.db import model_base +import sqlalchemy as sa +from sqlalchemy import orm + + +class AddressAssociation(model_base.BASEV2): + """Represents a neutron address group's address association.""" + __tablename__ = "address_associations" + + address = sa.Column(sa.String(length=db_const.IP_ADDR_FIELD_SIZE), + nullable=False, primary_key=True) + address_group_id = sa.Column(sa.String(length=db_const.UUID_FIELD_SIZE), + sa.ForeignKey("address_groups.id", + ondelete="CASCADE"), + nullable=False, primary_key=True) + + +class AddressGroup(model_base.BASEV2, model_base.HasId, model_base.HasProject): + """Represents a neutron address group.""" + __tablename__ = "address_groups" + + name = sa.Column(sa.String(db_const.NAME_FIELD_SIZE)) + description = sa.Column(sa.String(db_const.LONG_DESCRIPTION_FIELD_SIZE)) + addresses = orm.relationship(AddressAssociation, + backref=orm.backref('address_groups', + load_on_pending=True), + lazy='subquery', + cascade='all, delete-orphan') diff --git a/neutron/extensions/address_group.py b/neutron/extensions/address_group.py new file mode 100644 index 00000000000..d91d0a50c13 --- /dev/null +++ b/neutron/extensions/address_group.py @@ -0,0 +1,80 @@ +# 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 abc + +from neutron_lib.api.definitions import address_group as apidef +from neutron_lib.api import extensions as api_extensions +from neutron_lib.plugins import directory + +from neutron.api import extensions +from neutron.api.v2 import base + + +class Address_group(api_extensions.APIExtensionDescriptor): + """Extension class supporting Address Groups.""" + api_definition = apidef + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + plugin = directory.get_plugin() + collection_name = apidef.COLLECTION_NAME.replace('_', '-') + params = apidef.RESOURCE_ATTRIBUTE_MAP.get( + apidef.COLLECTION_NAME, dict()) + controller = base.create_resource(collection_name, + apidef.RESOURCE_NAME, + plugin, params, + member_actions=apidef.ACTION_MAP[ + apidef.RESOURCE_NAME], + allow_bulk=True, + allow_pagination=True, + allow_sorting=True) + + ex = extensions.ResourceExtension(collection_name, controller, + member_actions=apidef.ACTION_MAP[ + apidef.RESOURCE_NAME], + attr_map=params) + return [ex] + + +class AddressGroupPluginBase(object, metaclass=abc.ABCMeta): + + @abc.abstractmethod + def create_address_group(self, context, address_group): + pass + + @abc.abstractmethod + def update_address_group(self, context, id, address_group): + pass + + @abc.abstractmethod + def get_address_group(self, context, id, fields=None): + pass + + @abc.abstractmethod + def get_address_groups(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + pass + + @abc.abstractmethod + def delete_address_group(self, context, id): + pass + + @abc.abstractmethod + def add_addresses(self, context, address_group_id, addresses): + pass + + @abc.abstractmethod + def remove_addresses(self, context, address_group_id, addresses): + pass diff --git a/neutron/objects/address_group.py b/neutron/objects/address_group.py new file mode 100644 index 00000000000..c063f130831 --- /dev/null +++ b/neutron/objects/address_group.py @@ -0,0 +1,65 @@ +# 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 +from neutron_lib.objects import common_types +from oslo_versionedobjects import fields as obj_fields + +from neutron.db.models import address_group as models +from neutron.objects import base + + +@base.NeutronObjectRegistry.register +class AddressGroup(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = models.AddressGroup + + fields = { + 'id': common_types.UUIDField(), + 'name': obj_fields.StringField(nullable=True), + 'description': obj_fields.StringField(nullable=True), + 'project_id': obj_fields.StringField(), + 'addresses': obj_fields.ListOfObjectsField('AddressAssociation', + nullable=True) + } + synthetic_fields = ['addresses'] + + +@base.NeutronObjectRegistry.register +class AddressAssociation(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = models.AddressAssociation + + fields = { + 'address': common_types.IPNetworkField(nullable=False), + 'address_group_id': common_types.UUIDField(nullable=False) + } + primary_keys = ['address', 'address_group_id'] + foreign_keys = {'AddressGroup': {'address_group_id': 'id'}} + + @classmethod + def modify_fields_to_db(cls, fields): + result = super(AddressAssociation, cls).modify_fields_to_db(fields) + if 'address' in result: + result['address'] = cls.filter_to_str(result['address']) + return result + + @classmethod + def modify_fields_from_db(cls, db_obj): + fields = super(AddressAssociation, cls).modify_fields_from_db(db_obj) + if 'address' in fields: + fields['address'] = netaddr.IPNetwork(fields['address']) + return fields diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 9e9e5cc4cfd..2e96bd9e9c3 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -18,6 +18,7 @@ import netaddr from netaddr.strategy import eui48 from neutron_lib.agent import constants as agent_consts from neutron_lib.agent import topics +from neutron_lib.api.definitions import address_group as addrgrp_def from neutron_lib.api.definitions import address_scope from neutron_lib.api.definitions import agent as agent_apidef from neutron_lib.api.definitions import agent_resources_synced @@ -95,6 +96,7 @@ from neutron.api.rpc.handlers import metadata_rpc from neutron.api.rpc.handlers import resources_rpc from neutron.api.rpc.handlers import securitygroups_rpc from neutron.common import utils +from neutron.db import address_group_db from neutron.db import address_scope_db from neutron.db import agents_db from neutron.db import agentschedulers_db @@ -157,7 +159,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, vlantransparent_db.Vlantransparent_db_mixin, extradhcpopt_db.ExtraDhcpOptMixin, address_scope_db.AddressScopeDbMixin, - subnet_service_type_mixin.SubnetServiceTypeMixin): + subnet_service_type_mixin.SubnetServiceTypeMixin, + address_group_db.AddressGroupDbMixin): """Implement the Neutron L2 abstractions using modules. @@ -209,7 +212,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, agent_resources_synced.ALIAS, subnet_onboard_def.ALIAS, subnetpool_prefix_ops_def.ALIAS, - stateful_security_group.ALIAS] + stateful_security_group.ALIAS, + addrgrp_def.ALIAS] # List of agent types for which all binding_failed ports should try to be # rebound when agent revive diff --git a/neutron/tests/unit/extensions/test_address_group.py b/neutron/tests/unit/extensions/test_address_group.py new file mode 100644 index 00000000000..4e3fde22bc1 --- /dev/null +++ b/neutron/tests/unit/extensions/test_address_group.py @@ -0,0 +1,205 @@ +# 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.definitions import address_group as apidef +from neutron_lib import context +import webob.exc + +from neutron.db import address_group_db +from neutron.db import db_base_plugin_v2 +from neutron.extensions import address_group as ag_ext +from neutron.tests.unit.db import test_db_base_plugin_v2 + + +DB_PLUGIN_KLASS = ('neutron.tests.unit.extensions.test_address_group.' + 'AddressGroupTestPlugin') + + +class AddressGroupTestExtensionManager(object): + + def get_resources(self): + return ag_ext.Address_group.get_resources() + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + +class AddressGroupTestCase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase): + + def _create_address_group(self, **kwargs): + address_group = {'address_group': {}} + for k, v in kwargs.items(): + if k != 'addresses': + v = str(v) + address_group['address_group'][k] = v + + req = self.new_create_request('address-groups', address_group) + neutron_context = context.Context('', kwargs.get('tenant_id', + self._tenant_id)) + req.environ['neutron.context'] = neutron_context + res = req.get_response(self.ext_api) + if res.status_int >= webob.exc.HTTPClientError.code: + raise webob.exc.HTTPClientError(code=res.status_int) + return res + + def _test_create_address_group(self, expected=None, **kwargs): + keys = kwargs.copy() + keys.setdefault('tenant_id', self._tenant_id) + res = self._create_address_group(**keys) + ag = self.deserialize(self.fmt, res) + self._validate_resource(ag, keys, 'address_group') + if expected: + self._compare_resource(ag, expected, 'address_group') + return ag + + def _test_update_address_group(self, addr_group_id, data, + expected=None, tenant_id=None): + update_req = self.new_update_request( + 'address-groups', data, addr_group_id) + update_req.environ['neutron.context'] = context.Context( + '', tenant_id or self._tenant_id) + + update_res = update_req.get_response(self.ext_api) + if expected: + addr_group = self.deserialize(self.fmt, update_res) + self._compare_resource(addr_group, expected, 'address_group') + return addr_group + + return update_res + + def _test_address_group_actions(self, addr_group_id, data, action, + expected=None, tenant_id=None): + act_req = self.new_action_request( + 'address-groups', data, addr_group_id, action) + act_req.environ['neutron.context'] = context.Context( + '', tenant_id or self._tenant_id) + + act_res = act_req.get_response(self.ext_api) + if expected: + addr_group = self.deserialize(self.fmt, act_res) + self._compare_resource(addr_group, expected, 'address_group') + return addr_group + + return act_res + + +class AddressGroupTestPlugin(db_base_plugin_v2.NeutronDbPluginV2, + address_group_db.AddressGroupDbMixin): + __native_pagination_support = True + __native_sorting_support = True + # address-group requires security-group extension + supported_extension_aliases = [apidef.ALIAS, 'security-group'] + + +class TestAddressGroup(AddressGroupTestCase): + + def setUp(self): + plugin = DB_PLUGIN_KLASS + ext_mgr = AddressGroupTestExtensionManager() + super(TestAddressGroup, self).setUp(plugin=plugin, ext_mgr=ext_mgr) + + def test_create_address_group_without_description_or_addresses(self): + expected_ag = {'name': 'foo', + 'tenant_id': self._tenant_id, + 'description': '', + 'addresses': []} + self._test_create_address_group(name='foo', + expected=expected_ag) + + def test_create_address_group_with_description_and_addresses(self): + expected_ag = {'name': 'foo', + 'description': 'bar', + 'tenant_id': self._tenant_id, + 'addresses': ['10.0.1.255/28', '192.168.0.1/32']} + self._test_create_address_group(name='foo', description='bar', + addresses=['10.0.1.255/28', + '192.168.0.1/32'], + expected=expected_ag) + + def test_create_address_group_empty_name(self): + expected_ag = {'name': ''} + self._test_create_address_group(name='', expected=expected_ag) + + def test_update_address_group_name_and_description(self): + ag = self._test_create_address_group(name='foo') + data = {'address_group': {'name': 'bar', 'description': 'bar'}} + self._test_update_address_group(ag['address_group']['id'], + data, expected=data['address_group']) + + def test_update_address_group_addresses(self): + ag = self._test_create_address_group(name='foo') + data = {'address_group': {'addresses': ['10.0.0.1/32']}} + res = self._test_update_address_group(ag['address_group']['id'], data) + self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int) + + def test_get_address_group(self): + ag = self._test_create_address_group(name='foo') + req = self.new_show_request('address-groups', + ag['address_group']['id']) + res = self.deserialize(self.fmt, req.get_response(self.ext_api)) + self.assertEqual(ag['address_group']['id'], + res['address_group']['id']) + + def test_list_address_groups(self): + self._test_create_address_group(name='foo') + self._test_create_address_group(name='bar') + res = self._list('address-groups') + self.assertEqual(2, len(res['address_groups'])) + + def test_delete_address_group(self): + ag = self._test_create_address_group(name='foo') + self._delete('address-groups', ag['address_group']['id']) + self._show('address-groups', ag['address_group']['id'], + expected_code=webob.exc.HTTPNotFound.code) + + def test_add_valid_addresses(self): + ag = self._test_create_address_group(name='foo') + data = {'addresses': ['10.0.0.1/32', '2001::/32']} + self._test_address_group_actions(ag['address_group']['id'], data, + 'add_addresses', expected=data) + + def test_add_invalid_addresses(self): + ag = self._test_create_address_group(name='foo') + data = {'addresses': ['123456']} + res = self._test_address_group_actions(ag['address_group']['id'], + data, 'add_addresses') + self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int) + + def test_add_duplicated_addresses(self): + ag = self._test_create_address_group(name='foo', + addresses=['10.0.0.1/32']) + data = {'addresses': ['10.0.0.1/32']} + res = self._test_address_group_actions(ag['address_group']['id'], + data, 'add_addresses') + self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int) + + def test_remove_valid_addresses(self): + ag = self._test_create_address_group(name='foo', + addresses=['10.0.0.1/32', + '2001::/32']) + data = {'addresses': ['10.0.0.1/32']} + self._test_address_group_actions(ag['address_group']['id'], + data, 'remove_addresses', + expected={ + 'addresses': ['2001::/32'] + }) + + def test_remove_absent_addresses(self): + ag = self._test_create_address_group(name='foo', + addresses=['10.0.0.1/32']) + data = {'addresses': ['2001::/32']} + res = self._test_address_group_actions(ag['address_group']['id'], + data, 'remove_addresses') + self.assertEqual(webob.exc.HTTPNotFound.code, res.status_int) diff --git a/neutron/tests/unit/objects/test_address_group.py b/neutron/tests/unit/objects/test_address_group.py new file mode 100644 index 00000000000..798d10f77c0 --- /dev/null +++ b/neutron/tests/unit/objects/test_address_group.py @@ -0,0 +1,50 @@ +# 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.objects import address_group +from neutron.tests.unit.objects import test_base as obj_test_base +from neutron.tests.unit import testlib_api + + +class AddressGroupIfaceObjectTestCase( + obj_test_base.BaseObjectIfaceTestCase): + + _test_class = address_group.AddressGroup + + +class AddressGroupDbObjectTestCase( + obj_test_base.BaseDbObjectTestCase, testlib_api.SqlTestCase): + + _test_class = address_group.AddressGroup + + def setUp(self): + super(AddressGroupDbObjectTestCase, self).setUp() + + +class AddressAssociationIfaceObjectTestCase( + obj_test_base.BaseObjectIfaceTestCase): + + _test_class = address_group.AddressAssociation + + +class AddressAssociationObjectTestCase( + obj_test_base.BaseDbObjectTestCase, testlib_api.SqlTestCase): + + _test_class = address_group.AddressAssociation + + def setUp(self): + super(AddressAssociationObjectTestCase, self).setUp() + self.update_obj_fields( + { + 'address_group_id': + lambda: self._create_test_address_group_id() + }) diff --git a/neutron/tests/unit/objects/test_base.py b/neutron/tests/unit/objects/test_base.py index 5f8f63d694d..c66ce654c29 100644 --- a/neutron/tests/unit/objects/test_base.py +++ b/neutron/tests/unit/objects/test_base.py @@ -38,6 +38,7 @@ from sqlalchemy import orm import testtools from neutron import objects +from neutron.objects import address_group from neutron.objects import agent from neutron.objects import base from neutron.objects.db import api as obj_db_api @@ -1640,6 +1641,17 @@ class BaseDbObjectTestCase(_BaseObjectTestCase, _securitygroup.create() return _securitygroup.id + def _create_test_address_group_id(self, fields=None): + ag_fields = self.get_random_object_fields(address_group.AddressGroup) + fields = fields or {} + for field, value in ((f, v) for (f, v) in fields.items() if + f in ag_fields): + ag_fields[field] = value + _address_group = address_group.AddressGroup( + self.context, **ag_fields) + _address_group.create() + return _address_group.id + def _create_test_agent_id(self): attrs = self.get_random_object_fields(obj_cls=agent.Agent) _agent = agent.Agent(self.context, **attrs) diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index b0fed7007d4..9e631af0c8d 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -26,6 +26,8 @@ from neutron.tests import base as test_base # corresponding version bump in the affected objects. Please keep the list in # alphabetic order. object_data = { + 'AddressAssociation': '1.0-b92160a3dd2fb7b951adcd2e6ae1665a', + 'AddressGroup': '1.0-a402a66e35d25e9381eab40e1e709907', 'AddressScope': '1.1-dd0dfdb67775892d3adc090e28e43bd8', 'AddressScopeRBAC': '1.0-192845c5ed0718e1c54fac36936fcd7d', 'Agent': '1.1-64b670752d57b3c7602cb136e0338507', diff --git a/requirements.txt b/requirements.txt index 8f5fb0433ea..045d5f92bc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ Jinja2>=2.10 # BSD License (3 clause) keystonemiddleware>=4.17.0 # Apache-2.0 netaddr>=0.7.18 # BSD netifaces>=0.10.4 # MIT -neutron-lib>=2.4.0 # Apache-2.0 +neutron-lib>=2.5.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 tenacity>=4.4.0 # Apache-2.0 SQLAlchemy>=1.2.0 # MIT