diff --git a/neutron_lib/api/definitions/__init__.py b/neutron_lib/api/definitions/__init__.py index cd861d2d3..9e533d4db 100644 --- a/neutron_lib/api/definitions/__init__.py +++ b/neutron_lib/api/definitions/__init__.py @@ -12,6 +12,7 @@ from neutron_lib.api.definitions import address_scope from neutron_lib.api.definitions import agent +from neutron_lib.api.definitions import allowedaddresspairs from neutron_lib.api.definitions import auto_allocated_topology from neutron_lib.api.definitions import availability_zone from neutron_lib.api.definitions import bgpvpn @@ -49,6 +50,7 @@ from neutron_lib.api.definitions import trunk_details _ALL_API_DEFINITIONS = { address_scope, agent, + allowedaddresspairs, auto_allocated_topology, availability_zone, bgpvpn, diff --git a/neutron_lib/api/definitions/allowedaddresspairs.py b/neutron_lib/api/definitions/allowedaddresspairs.py new file mode 100644 index 000000000..cec881912 --- /dev/null +++ b/neutron_lib/api/definitions/allowedaddresspairs.py @@ -0,0 +1,53 @@ +# Copyright 2013 VMware, Inc. 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. + +from neutron_lib.api import converters +from neutron_lib.api.definitions import port +from neutron_lib.api import validators +from neutron_lib.api.validators import allowedaddresspairs as addr_validation +from neutron_lib import constants + +validators.add_validator('allowed_address_pairs', + addr_validation._validate_allowed_address_pairs) + + +ADDRESS_PAIRS = 'allowed_address_pairs' +ALIAS = 'allowed-address-pairs' +LABEL = ALIAS +IS_SHIM_EXTENSION = False +IS_STANDARD_ATTR_EXTENSION = False +NAME = 'Allowed Address Pairs' +API_PREFIX = '' +DESCRIPTION = 'Provides allowed address pairs' +UPDATED_TIMESTAMP = '2013-07-23T10:00:00-00:00' +RESOURCE_NAME = port.RESOURCE_NAME +COLLECTION_NAME = port.COLLECTION_NAME +RESOURCE_ATTRIBUTE_MAP = { + COLLECTION_NAME: { + ADDRESS_PAIRS: { + 'allow_post': True, 'allow_put': True, + 'convert_to': converters.convert_none_to_empty_list, + 'convert_list_to': + converters.convert_kvp_list_to_dict, + 'validate': {'type:allowed_address_pairs': None}, + 'enforce_policy': True, + 'default': constants.ATTR_NOT_SPECIFIED, + 'is_visible': True}, + } +} +SUB_RESOURCE_ATTRIBUTE_MAP = {} +ACTION_MAP = {} +REQUIRED_EXTENSIONS = [] +OPTIONAL_EXTENSIONS = [] +ACTION_STATUS = {} diff --git a/neutron_lib/api/validators/allowedaddresspairs.py b/neutron_lib/api/validators/allowedaddresspairs.py new file mode 100644 index 000000000..af64bd378 --- /dev/null +++ b/neutron_lib/api/validators/allowedaddresspairs.py @@ -0,0 +1,81 @@ +# Copyright 2013 VMware, Inc. 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. + +from oslo_config import cfg +from webob import exc + +from neutron_lib._i18n import _ +from neutron_lib.api import validators +from neutron_lib.exceptions import allowedaddresspairs as exceptions + + +def _validate_allowed_address_pairs(address_pairs, valid_values=None): + """Validates a list of allowed address pair dicts. + + Validation herein requires the caller to have registered the + max_allowed_address_pair oslo config option in the global CONF prior + to having this validator used. + + :param address_pairs: A list of address pair dicts to validate. + :param valid_values: Not used. + :returns: None + :raises: AllowedAddressPairExhausted if the address pairs requested + exceeds cfg.CONF.max_allowed_address_pair. AllowedAddressPairsMissingIP + if any address pair dicts are missing and IP address. + DuplicateAddressPairInRequest if duplicated IPs are in the list of + address pair dicts. Otherwise a HTTPBadRequest is raised if any of + the address pairs are invalid. + """ + unique_check = {} + if not isinstance(address_pairs, list): + raise exc.HTTPBadRequest( + _("Allowed address pairs must be a list.")) + if len(address_pairs) > cfg.CONF.max_allowed_address_pair: + raise exceptions.AllowedAddressPairExhausted( + quota=cfg.CONF.max_allowed_address_pair) + + for address_pair in address_pairs: + msg = validators.validate_dict(address_pair) + if msg: + return msg + # mac_address is optional, if not set we use the mac on the port + if 'mac_address' in address_pair: + msg = validators.validate_mac_address(address_pair['mac_address']) + if msg: + raise exc.HTTPBadRequest(msg) + if 'ip_address' not in address_pair: + raise exceptions.AllowedAddressPairsMissingIP() + + mac = address_pair.get('mac_address') + ip_address = address_pair['ip_address'] + if (mac, ip_address) not in unique_check: + unique_check[(mac, ip_address)] = None + else: + raise exceptions.DuplicateAddressPairInRequest( + mac_address=mac, ip_address=ip_address) + + invalid_attrs = set(address_pair.keys()) - set(['mac_address', + 'ip_address']) + if invalid_attrs: + msg = (_("Unrecognized attribute(s) '%s'") % + ', '.join(set(address_pair.keys()) - + set(['mac_address', 'ip_address']))) + raise exc.HTTPBadRequest(msg) + + if '/' in ip_address: + msg = validators.validate_subnet(ip_address) + else: + msg = validators.validate_ip_address(ip_address) + if msg: + raise exc.HTTPBadRequest(msg) diff --git a/neutron_lib/exceptions/allowedaddresspairs.py b/neutron_lib/exceptions/allowedaddresspairs.py new file mode 100644 index 000000000..556ffb90e --- /dev/null +++ b/neutron_lib/exceptions/allowedaddresspairs.py @@ -0,0 +1,35 @@ +# Copyright 2013 VMware, Inc. 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. + +from neutron_lib._i18n import _ +from neutron_lib import exceptions + + +class AllowedAddressPairsMissingIP(exceptions.InvalidInput): + message = _("AllowedAddressPair must contain ip_address") + + +class AddressPairAndPortSecurityRequired(exceptions.Conflict): + message = _("Port Security must be enabled in order to have allowed " + "address pairs on a port.") + + +class DuplicateAddressPairInRequest(exceptions.InvalidInput): + message = _("Request contains duplicate address pair: " + "mac_address %(mac_address)s ip_address %(ip_address)s.") + + +class AllowedAddressPairExhausted(exceptions.BadRequest): + message = _("The number of allowed address pair " + "exceeds the maximum %(quota)s.") diff --git a/neutron_lib/tests/unit/api/definitions/test_allowedaddresspairs.py b/neutron_lib/tests/unit/api/definitions/test_allowedaddresspairs.py new file mode 100644 index 000000000..915fd9975 --- /dev/null +++ b/neutron_lib/tests/unit/api/definitions/test_allowedaddresspairs.py @@ -0,0 +1,20 @@ +# 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 allowedaddresspairs +from neutron_lib.tests.unit.api.definitions import base + + +class AllowedAddressPairsDefinitionTestCase(base.DefinitionBaseTestCase): + extension_module = allowedaddresspairs + extension_resources = () + extension_attributes = (allowedaddresspairs.ADDRESS_PAIRS,) diff --git a/neutron_lib/tests/unit/api/validators/test_allowedaddresspairs.py b/neutron_lib/tests/unit/api/validators/test_allowedaddresspairs.py new file mode 100644 index 000000000..b472f23af --- /dev/null +++ b/neutron_lib/tests/unit/api/validators/test_allowedaddresspairs.py @@ -0,0 +1,95 @@ +# 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 mock +from oslo_config import cfg +from webob import exc + +from neutron_lib.api.validators import allowedaddresspairs as validator +from neutron_lib.exceptions import allowedaddresspairs as addr_exc +from neutron_lib.tests import _base as base + + +class TestAllowedAddressPairs(base.BaseTestCase): + + def test__validate_allowed_address_pairs_not_a_list(self): + for d in [{}, set(), 'abc', True, 1]: + self.assertRaisesRegex( + exc.HTTPBadRequest, 'must be a list', + validator._validate_allowed_address_pairs, d) + + @mock.patch.object(cfg, 'CONF') + def test__validate_allowed_address_pairs_exhausted(self, mock_conf): + mock_conf.max_allowed_address_pair = 1 + self.assertRaises( + addr_exc.AllowedAddressPairExhausted, + validator._validate_allowed_address_pairs, + [{}, {}]) + + @mock.patch.object(cfg, 'CONF') + def test__validate_allowed_address_pairs_invalid_mac(self, mock_conf): + mock_conf.max_allowed_address_pair = 3 + self.assertRaisesRegex( + exc.HTTPBadRequest, 'is not a valid MAC address', + validator._validate_allowed_address_pairs, + [{'mac_address': 1}]) + + @mock.patch.object(cfg, 'CONF') + def test__validate_allowed_address_pairs_missing_ip(self, mock_conf): + mock_conf.max_allowed_address_pair = 3 + self.assertRaises( + addr_exc.AllowedAddressPairsMissingIP, + validator._validate_allowed_address_pairs, + [{'ip_adress': '192.168.1.11'}]) + + @mock.patch.object(cfg, 'CONF') + def test__validate_allowed_address_pairs_duplicate(self, mock_conf): + mock_conf.max_allowed_address_pair = 3 + self.assertRaises( + addr_exc.DuplicateAddressPairInRequest, + validator._validate_allowed_address_pairs, + [{'ip_address': '192.168.1.11'}, + {'ip_address': '192.168.1.11'}]) + + @mock.patch.object(cfg, 'CONF') + def test__validate_allowed_address_pairs_invalid_attrs(self, mock_conf): + mock_conf.max_allowed_address_pair = 3 + self.assertRaisesRegex( + exc.HTTPBadRequest, 'Unrecognized attribute', + validator._validate_allowed_address_pairs, + [{'ip_address': '192.168.1.11'}, + {'ip_address': '192.168.1.12', 'idk': True}]) + + @mock.patch.object(cfg, 'CONF') + def test__validate_allowed_address_pairs_invalid_subnet(self, mock_conf): + mock_conf.max_allowed_address_pair = 3 + self.assertRaisesRegex( + exc.HTTPBadRequest, 'is not a valid IP subnet', + validator._validate_allowed_address_pairs, + [{'ip_address': '192.168.1.11'}, + {'ip_address': '192.168.1.0/a'}]) + + @mock.patch.object(cfg, 'CONF') + def test__validate_allowed_address_pairs_invalid_ip_address( + self, mock_conf): + mock_conf.max_allowed_address_pair = 3 + self.assertRaisesRegex( + exc.HTTPBadRequest, 'is not a valid IP address', + validator._validate_allowed_address_pairs, + [{'ip_address': '192.168.1.a'}, + {'ip_address': '192.168.1.2'}]) + + @mock.patch.object(cfg, 'CONF') + def test__validate_allowed_address_pairs_good_data(self, mock_conf): + mock_conf.max_allowed_address_pair = 3 + self.assertIsNone(validator._validate_allowed_address_pairs( + [{'ip_address': '192.1.1.11'}, {'ip_address': '19.1.1.11'}])) diff --git a/releasenotes/notes/rehome-allowedaddrpairs-apidef-cd342b9a57a2dfdf.yaml b/releasenotes/notes/rehome-allowedaddrpairs-apidef-cd342b9a57a2dfdf.yaml new file mode 100644 index 000000000..d1de5cd5a --- /dev/null +++ b/releasenotes/notes/rehome-allowedaddrpairs-apidef-cd342b9a57a2dfdf.yaml @@ -0,0 +1,8 @@ +--- +features: + - The ``allowed-address-pairs`` API definition is now available in + ``neutron_lib.api.definitions.allowedaddresspairs``. + - The address pair validation is now available via the + ``type:allowed_address_pairs`` validation type. + - Address pair API definition exceptions are available in + ``neutron_lib.exceptions.allowedaddresspairs``.