From 341401d916cc3f317cff2c84def4bd65a8bee5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Weing=C3=A4rtner?= Date: Fri, 14 Aug 2020 17:00:28 -0300 Subject: [PATCH] Add source_ip_prefix and destination_ip_prefix to metering label rules As proposed in the RFE and then approved in the spec, we are adding to the neutron metering rules two new parameters. The source IP prefix, and destination IP prefix. Partially-Implements: https://bugs.launchpad.net/neutron/+bug/1889431 RFE: https://bugs.launchpad.net/neutron/+bug/1889431 Depends-On: https://review.opendev.org/#/c/746203/ Depends-On: https://review.opendev.org/#/c/744702/ Depends-On: https://review.opendev.org/#/c/743828/ Depends-On: https://review.opendev.org/#/c/746142/ Change-Id: I38991de2b4937becd0f1f14f3a32dc39c590e0d9 (cherry picked from commit 10091f9346004d330925a5e0ab5425cda6256326) --- neutron/db/metering/metering_db.py | 57 ++-- .../alembic_migrations/versions/EXPAND_HEAD | 2 +- ...ination_ip_prefix_neutron_metering_rule.py | 38 +++ neutron/db/models/metering.py | 2 + .../metering_source_and_destination_fields.py | 42 +++ neutron/objects/metering.py | 32 ++- .../drivers/iptables/iptables_driver.py | 47 +++- neutron/services/metering/metering_plugin.py | 88 ++++++- .../unit/db/metering/test_metering_db.py | 125 ++++----- neutron/tests/unit/objects/test_objects.py | 2 +- .../services/metering/test_metering_plugin.py | 248 +++++++++++++++++- 11 files changed, 573 insertions(+), 110 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/victoria/expand/I38991de2b4_source_and_destination_ip_prefix_neutron_metering_rule.py create mode 100644 neutron/extensions/metering_source_and_destination_fields.py diff --git a/neutron/db/metering/metering_db.py b/neutron/db/metering/metering_db.py index 87e2afa888b..2a31aea9ad5 100644 --- a/neutron/db/metering/metering_db.py +++ b/neutron/db/metering/metering_db.py @@ -17,7 +17,9 @@ from neutron_lib import constants from neutron_lib.db import api as db_api from neutron_lib.db import utils as db_utils from neutron_lib.exceptions import metering as metering_exc + from oslo_db import exception as db_exc +from oslo_log import log as logging from oslo_utils import uuidutils from neutron.api.rpc.agentnotifiers import metering_rpc_agent_api @@ -27,6 +29,8 @@ from neutron.objects import base as base_obj from neutron.objects import metering as metering_objs from neutron.objects import router as l3_obj +LOG = logging.getLogger(__name__) + class MeteringDbMixin(metering.MeteringPluginBase): @@ -84,7 +88,10 @@ class MeteringDbMixin(metering.MeteringPluginBase): res = {'id': metering_label_rule['id'], 'metering_label_id': metering_label_rule['metering_label_id'], 'direction': metering_label_rule['direction'], - 'remote_ip_prefix': metering_label_rule['remote_ip_prefix'], + 'remote_ip_prefix': metering_label_rule.get('remote_ip_prefix'), + 'source_ip_prefix': metering_label_rule.get('source_ip_prefix'), + 'destination_ip_prefix': metering_label_rule.get( + 'destination_ip_prefix'), 'excluded': metering_label_rule['excluded']} return db_utils.resource_fields(res, fields) @@ -109,45 +116,33 @@ class MeteringDbMixin(metering.MeteringPluginBase): return self._make_metering_label_rule_dict( self._get_metering_label_rule(context, rule_id), fields) - def _validate_cidr(self, context, label_id, remote_ip_prefix, - direction, excluded): - r_ips = self.get_metering_label_rules(context, - filters={'metering_label_id': - [label_id], - 'direction': - [direction], - 'excluded': - [excluded]}, - fields=['remote_ip_prefix']) - - cidrs = [r['remote_ip_prefix'] for r in r_ips] - new_cidr_ipset = netaddr.IPSet([remote_ip_prefix]) - if (netaddr.IPSet(cidrs) & new_cidr_ipset): - raise metering_exc.MeteringLabelRuleOverlaps( - remote_ip_prefix=remote_ip_prefix) - def create_metering_label_rule(self, context, metering_label_rule): - m = metering_label_rule['metering_label_rule'] + label_id = metering_label_rule['metering_label_id'] + try: with db_api.CONTEXT_WRITER.using(context): - label_id = m['metering_label_id'] - ip_prefix = m['remote_ip_prefix'] - direction = m['direction'] - excluded = m['excluded'] - - self._validate_cidr(context, label_id, ip_prefix, direction, - excluded) rule = metering_objs.MeteringLabelRule( context, id=uuidutils.generate_uuid(), - metering_label_id=label_id, direction=direction, - excluded=m['excluded'], - remote_ip_prefix=netaddr.IPNetwork(ip_prefix)) + metering_label_id=label_id, + direction=metering_label_rule['direction'], + excluded=metering_label_rule['excluded'], + ) + if metering_label_rule.get('remote_ip_prefix'): + rule.remote_ip_prefix = netaddr.IPNetwork( + metering_label_rule['remote_ip_prefix']) + + if metering_label_rule.get('source_ip_prefix'): + rule.source_ip_prefix = netaddr.IPNetwork( + metering_label_rule['source_ip_prefix']) + + if metering_label_rule.get('destination_ip_prefix'): + rule.destination_ip_prefix = netaddr.IPNetwork( + metering_label_rule['destination_ip_prefix']) rule.create() + return self._make_metering_label_rule_dict(rule) except db_exc.DBReferenceError: raise metering_exc.MeteringLabelNotFound(label_id=label_id) - return self._make_metering_label_rule_dict(rule) - def delete_metering_label_rule(self, context, rule_id): with db_api.CONTEXT_WRITER.using(context): rule = self._get_metering_label_rule(context, rule_id) diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 0ca07ef7512..e0c729daf42 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -49d8622c5221 +I38991de2b4 diff --git a/neutron/db/migration/alembic_migrations/versions/victoria/expand/I38991de2b4_source_and_destination_ip_prefix_neutron_metering_rule.py b/neutron/db/migration/alembic_migrations/versions/victoria/expand/I38991de2b4_source_and_destination_ip_prefix_neutron_metering_rule.py new file mode 100644 index 00000000000..a39429b0038 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/victoria/expand/I38991de2b4_source_and_destination_ip_prefix_neutron_metering_rule.py @@ -0,0 +1,38 @@ +# Copyright 2020 OpenStack Foundation +# +# 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 + +import sqlalchemy as sa + +"""Add source and destination IP prefixes to neutron metering system +Revision ID: I38991de2b4 +Revises: fd6107509ccd +Create Date: 2020-08-20 10:00:00.000000 + +""" + +# revision identifiers, used by Alembic. +revision = 'I38991de2b4' +down_revision = '49d8622c5221' + +metering_label_rules_table_name = 'meteringlabelrules' + + +def upgrade(): + op.add_column(metering_label_rules_table_name, + sa.Column('source_ip_prefix', sa.String(64))) + op.add_column(metering_label_rules_table_name, + sa.Column('destination_ip_prefix', sa.String(64))) diff --git a/neutron/db/models/metering.py b/neutron/db/models/metering.py index eaf7b7ef5d5..954bfb4bbf7 100644 --- a/neutron/db/models/metering.py +++ b/neutron/db/models/metering.py @@ -23,6 +23,8 @@ class MeteringLabelRule(model_base.BASEV2, model_base.HasId): direction = sa.Column(sa.Enum('ingress', 'egress', name='meteringlabels_direction')) remote_ip_prefix = sa.Column(sa.String(64)) + source_ip_prefix = sa.Column(sa.String(64)) + destination_ip_prefix = sa.Column(sa.String(64)) metering_label_id = sa.Column(sa.String(36), sa.ForeignKey("meteringlabels.id", ondelete="CASCADE"), diff --git a/neutron/extensions/metering_source_and_destination_fields.py b/neutron/extensions/metering_source_and_destination_fields.py new file mode 100644 index 00000000000..acc8d1effeb --- /dev/null +++ b/neutron/extensions/metering_source_and_destination_fields.py @@ -0,0 +1,42 @@ +# Copyright (C) 2013 eNovance SAS +# +# 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 metering_source_and_destination_filters +from neutron_lib.api import extensions + + +class Metering_source_and_destination_fields( + extensions.APIExtensionDescriptor): + + api_definition = metering_source_and_destination_filters + + @classmethod + def get_extended_resources(cls, version): + sub_resource_map = super(Metering_source_and_destination_fields, cls + ).get_extended_resources(version) + + processed_sub_resource_map = {} + for value in sub_resource_map.values(): + parent_def = value['parent'] + collection_name = parent_def['collection_name'] + member_name = parent_def['member_name'] + + if collection_name == member_name: + processed_sub_resource_map[ + collection_name] = value['parameters'] + else: + processed_sub_resource_map[ + collection_name] = {member_name: value['parameters']} + + return processed_sub_resource_map diff --git a/neutron/objects/metering.py b/neutron/objects/metering.py index 877f90bcef2..3095188dd28 100644 --- a/neutron/objects/metering.py +++ b/neutron/objects/metering.py @@ -23,7 +23,8 @@ from neutron.objects import base @base.NeutronObjectRegistry.register class MeteringLabelRule(base.NeutronDbObject): # Version 1.0: Initial version - VERSION = '1.0' + # Version 2.0: Source and destination field for the metering label rule + VERSION = '2.0' db_model = metering_models.MeteringLabelRule @@ -33,6 +34,8 @@ class MeteringLabelRule(base.NeutronDbObject): 'id': common_types.UUIDField(), 'direction': common_types.FlowDirectionEnumField(nullable=True), 'remote_ip_prefix': common_types.IPNetworkField(nullable=True), + 'source_ip_prefix': common_types.IPNetworkField(nullable=True), + 'destination_ip_prefix': common_types.IPNetworkField(nullable=True), 'metering_label_id': common_types.UUIDField(), 'excluded': obj_fields.BooleanField(default=False), } @@ -42,19 +45,34 @@ class MeteringLabelRule(base.NeutronDbObject): @classmethod def modify_fields_from_db(cls, db_obj): result = super(MeteringLabelRule, cls).modify_fields_from_db(db_obj) - if 'remote_ip_prefix' in result: - result['remote_ip_prefix'] = net_utils.AuthenticIPNetwork( - result['remote_ip_prefix']) + + cls.ip_field_from_db(result, "remote_ip_prefix") + cls.ip_field_from_db(result, "source_ip_prefix") + cls.ip_field_from_db(result, "destination_ip_prefix") + return result + @classmethod + def ip_field_from_db(cls, result, attribute_name): + if attribute_name in result: + result[attribute_name] = net_utils.AuthenticIPNetwork( + result[attribute_name]) + @classmethod def modify_fields_to_db(cls, fields): result = super(MeteringLabelRule, cls).modify_fields_to_db(fields) - if 'remote_ip_prefix' in result: - result['remote_ip_prefix'] = cls.filter_to_str( - result['remote_ip_prefix']) + + cls.ip_field_to_db(result, "remote_ip_prefix") + cls.ip_field_to_db(result, "source_ip_prefix") + cls.ip_field_to_db(result, "destination_ip_prefix") + return result + @classmethod + def ip_field_to_db(cls, result, attribute_name): + if attribute_name in result: + result[attribute_name] = cls.filter_to_str(result[attribute_name]) + @base.NeutronObjectRegistry.register class MeteringLabel(base.NeutronDbObject): diff --git a/neutron/services/metering/drivers/iptables/iptables_driver.py b/neutron/services/metering/drivers/iptables/iptables_driver.py index 96e93f97f7d..a25d605e3da 100644 --- a/neutron/services/metering/drivers/iptables/iptables_driver.py +++ b/neutron/services/metering/drivers/iptables/iptables_driver.py @@ -213,6 +213,10 @@ class IptablesMeteringDriver(abstract_driver.MeteringAbstractDriver): def _add_rule_to_chain(self, ext_dev, rule, im, label_chain, rules_chain): ipt_rule = self._prepare_rule(ext_dev, rule, label_chain) + + LOG.debug("Adding IPtables rule [%s] for configurations [%s].", + ipt_rule, rule) + if rule['excluded']: im.ipv4['filter'].add_rule(rules_chain, ipt_rule, wrap=False, top=True) @@ -223,6 +227,10 @@ class IptablesMeteringDriver(abstract_driver.MeteringAbstractDriver): def _remove_rule_from_chain(self, ext_dev, rule, im, label_chain, rules_chain): ipt_rule = self._prepare_rule(ext_dev, rule, label_chain) + + LOG.debug("Removing IPtables rule [%s] for configurations [%s].", + ipt_rule, rule) + if rule['excluded']: im.ipv4['filter'].remove_rule(rules_chain, ipt_rule, wrap=False, top=True) @@ -231,16 +239,43 @@ class IptablesMeteringDriver(abstract_driver.MeteringAbstractDriver): wrap=False, top=False) def _prepare_rule(self, ext_dev, rule, label_chain): - remote_ip = rule['remote_ip_prefix'] - if rule['direction'] == 'egress': - dir_opt = '-s %s -o %s' % (remote_ip, ext_dev) + if rule.get('remote_ip_prefix'): + ipt_rule = IptablesMeteringDriver.\ + prepare_source_and_destination_rule_legacy(ext_dev, rule) else: - dir_opt = '-d %s -i %s' % (remote_ip, ext_dev) + ipt_rule = IptablesMeteringDriver.\ + prepare_source_and_destination_rule(ext_dev, rule) if rule['excluded']: - ipt_rule = '%s -j RETURN' % dir_opt + ipt_rule = '%s -j RETURN' % ipt_rule else: - ipt_rule = '%s -j %s' % (dir_opt, label_chain) + ipt_rule = '%s -j %s' % (ipt_rule, label_chain) + return ipt_rule + + @staticmethod + def prepare_source_and_destination_rule(ext_dev, rule): + if rule['direction'] == 'egress': + iptables_rule = '-o %s' % ext_dev + else: + iptables_rule = '-i %s' % ext_dev + + source_ip_prefix = rule.get('source_ip_prefix') + if source_ip_prefix: + iptables_rule = "-s %s %s" % (source_ip_prefix, iptables_rule) + + destination_ip_prefix = rule.get('destination_ip_prefix') + if destination_ip_prefix: + iptables_rule = "-d %s %s" % (destination_ip_prefix, iptables_rule) + + return iptables_rule + + @staticmethod + def prepare_source_and_destination_rule_legacy(ext_dev, rule): + remote_ip = rule['remote_ip_prefix'] + if rule['direction'] == 'egress': + ipt_rule = '-s %s -o %s' % (remote_ip, ext_dev) + else: + ipt_rule = '-d %s -i %s' % (remote_ip, ext_dev) return ipt_rule def _process_ns_specific_metering_label(self, router, ext_dev, im): diff --git a/neutron/services/metering/metering_plugin.py b/neutron/services/metering/metering_plugin.py index 727de098a67..6581fe2ee61 100644 --- a/neutron/services/metering/metering_plugin.py +++ b/neutron/services/metering/metering_plugin.py @@ -12,8 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. +import ipaddress +import netaddr + from neutron_lib.agent import topics from neutron_lib.api.definitions import metering as metering_apidef +from neutron_lib.api.definitions import metering_source_and_destination_filters +from neutron_lib.exceptions import metering as metering_exc + +from neutron_lib import exceptions as neutron_exc from neutron_lib import rpc as n_rpc from neutron.api.rpc.agentnotifiers import metering_rpc_agent_api @@ -28,7 +35,8 @@ LOG = logging.getLogger(__name__) class MeteringPlugin(metering_db.MeteringDbMixin): """Implementation of the Neutron Metering Service Plugin.""" - supported_extension_aliases = [metering_apidef.ALIAS] + supported_extension_aliases = [ + metering_apidef.ALIAS, metering_source_and_destination_filters.ALIAS] path_prefix = "/metering" __filter_validation_support = True @@ -66,6 +74,10 @@ class MeteringPlugin(metering_db.MeteringDbMixin): return label def create_metering_label_rule(self, context, metering_label_rule): + metering_label_rule = metering_label_rule['metering_label_rule'] + MeteringPlugin.validate_metering_label_rule(metering_label_rule) + self.check_for_rule_overlaps(context, metering_label_rule) + rule = super(MeteringPlugin, self).create_metering_label_rule( context, metering_label_rule) @@ -82,6 +94,80 @@ class MeteringPlugin(metering_db.MeteringDbMixin): return rule + @staticmethod + def validate_metering_label_rule(metering_label_rule): + MeteringPlugin.validate_metering_rule_ip_address( + metering_label_rule, "remote_ip_prefix") + MeteringPlugin.validate_metering_rule_ip_address( + metering_label_rule, "source_ip_prefix") + MeteringPlugin.validate_metering_rule_ip_address( + metering_label_rule, "destination_ip_prefix") + + if metering_label_rule.get("remote_ip_prefix"): + if metering_label_rule.get("source_ip_prefix") or \ + metering_label_rule.get("destination_ip_prefix"): + raise neutron_exc.Invalid( + "Cannot use 'remote-ip-prefix' in conjunction " + "with 'source-ip-prefix' or 'destination-ip-prefix'.") + + none_ip_prefix_informed = not metering_label_rule.get( + 'remote_ip_prefix') and not metering_label_rule.get( + 'source_ip_prefix') and not metering_label_rule.get( + 'destination_ip_prefix') + + if none_ip_prefix_informed: + raise neutron_exc.Invalid( + "You must define at least one of the following parameters " + "'remote_ip_prefix', or 'source_ip_prefix' or " + "'destination_ip_prefix'.") + + @staticmethod + def validate_metering_rule_ip_address(metering_label_rule, + ip_address_field): + try: + if metering_label_rule.get(ip_address_field): + ipaddress.ip_interface( + metering_label_rule.get(ip_address_field)) + except ValueError as exception: + raise neutron_exc.Invalid( + "%s: %s is invalid [%s]." % + (ip_address_field, + metering_label_rule.get(ip_address_field), + exception)) + + def check_for_rule_overlaps(self, context, metering_label_rule): + label_id = metering_label_rule['metering_label_id'] + direction = metering_label_rule['direction'] + excluded = metering_label_rule['excluded'] + + db_metering_rules = self.get_metering_label_rules( + context, filters={ + 'metering_label_id': [label_id], + 'direction': [direction], + 'excluded': [excluded]} + ) + for db_metering_rule in db_metering_rules: + MeteringPlugin.verify_rule_overlap( + db_metering_rule, metering_label_rule, "remote_ip_prefix") + + @staticmethod + def verify_rule_overlap(db_metering_rule, metering_label_rule, + attribute_name): + if db_metering_rule.get( + attribute_name) and metering_label_rule.get(attribute_name): + remote_ip_prefix = metering_label_rule[attribute_name] + cidr = [db_metering_rule.get(attribute_name)] + new_cidr_ipset = netaddr.IPSet([remote_ip_prefix]) + + if netaddr.IPSet(cidr) & new_cidr_ipset: + LOG.warning("The metering rule [%s] overlaps with" + " previously created rule [%s]. It is not an" + " expected use case, and people should use" + " it wisely.", metering_label_rule, + db_metering_rule) + raise metering_exc.MeteringLabelRuleOverlaps( + remote_ip_prefix=remote_ip_prefix) + def delete_metering_label_rule(self, context, rule_id): rule = super(MeteringPlugin, self).delete_metering_label_rule( context, rule_id) diff --git a/neutron/tests/unit/db/metering/test_metering_db.py b/neutron/tests/unit/db/metering/test_metering_db.py index 2dbc8270122..b36efd391c0 100644 --- a/neutron/tests/unit/db/metering/test_metering_db.py +++ b/neutron/tests/unit/db/metering/test_metering_db.py @@ -64,13 +64,29 @@ class MeteringPluginDbTestCaseMixin(object): return self.deserialize(fmt, res) def _create_metering_label_rule(self, fmt, metering_label_id, direction, - remote_ip_prefix, excluded, **kwargs): - data = {'metering_label_rule': - {'metering_label_id': metering_label_id, - 'tenant_id': kwargs.get('tenant_id', 'test-tenant'), - 'direction': direction, - 'excluded': excluded, - 'remote_ip_prefix': remote_ip_prefix}} + excluded, remote_ip_prefix=None, + source_ip_prefix=None, + destination_ip_prefix=None, + **kwargs): + data = { + 'metering_label_rule': { + 'metering_label_id': metering_label_id, + 'tenant_id': kwargs.get('tenant_id', 'test-tenant'), + 'direction': direction, + 'excluded': excluded, + } + } + + if remote_ip_prefix: + data['metering_label_rule']['remote_ip_prefix'] = remote_ip_prefix + + if source_ip_prefix: + data['metering_label_rule']['source_ip_prefix'] = source_ip_prefix + + if destination_ip_prefix: + data['metering_label_rule']['destination_ip_prefix'] =\ + destination_ip_prefix + req = self.new_create_request('metering-label-rules', data, fmt) @@ -82,10 +98,9 @@ class MeteringPluginDbTestCaseMixin(object): return req.get_response(self.ext_api) def _make_metering_label_rule(self, fmt, metering_label_id, direction, - remote_ip_prefix, excluded, **kwargs): + excluded, **kwargs): res = self._create_metering_label_rule(fmt, metering_label_id, - direction, remote_ip_prefix, - excluded, **kwargs) + direction, excluded, **kwargs) if res.status_int >= 400: raise webob.exc.HTTPClientError(code=res.status_int) return self.deserialize(fmt, res) @@ -101,15 +116,14 @@ class MeteringPluginDbTestCaseMixin(object): @contextlib.contextmanager def metering_label_rule(self, metering_label_id=None, direction='ingress', - remote_ip_prefix='10.0.0.0/24', - excluded='false', fmt=None): + excluded=False, fmt=None, **kwargs): if not fmt: fmt = self.fmt metering_label_rule = self._make_metering_label_rule(fmt, metering_label_id, direction, - remote_ip_prefix, - excluded) + excluded, + **kwargs) yield metering_label_rule @@ -212,10 +226,9 @@ class TestMetering(MeteringPluginDbTestCase): ('direction', direction), ('excluded', excluded), ('remote_ip_prefix', remote_ip_prefix)] - with self.metering_label_rule(metering_label_id, - direction, - remote_ip_prefix, - excluded) as label_rule: + with self.metering_label_rule( + metering_label_id, direction, excluded, + remote_ip_prefix=remote_ip_prefix) as label_rule: for k, v, in keys: self.assertEqual(label_rule['metering_label_rule'][k], v) @@ -224,11 +237,9 @@ class TestMetering(MeteringPluginDbTestCase): remote_ip_prefix = '192.168.0.0/24' excluded = True - res = self._create_metering_label_rule(self.fmt, - _fake_uuid(), - direction, - remote_ip_prefix, - excluded) + res = self._create_metering_label_rule( + self.fmt, _fake_uuid(), direction, excluded, + remote_ip_prefix=remote_ip_prefix) self.assertEqual(webob.exc.HTTPNotFound.code, res.status_int) def test_update_metering_label_rule(self): @@ -239,8 +250,8 @@ class TestMetering(MeteringPluginDbTestCase): data = {'metering_label_rule': {}} with self.metering_label(name, description) as metering_label, \ self.metering_label_rule( - metering_label['metering_label']['id'], - direction, remote_ip_prefix) as label_rule: + metering_label['metering_label']['id'], direction, + remote_ip_prefix=remote_ip_prefix) as label_rule: rule_id = label_rule['metering_label_rule']['id'] self._update('metering-label-rules', rule_id, data, webob.exc.HTTPNotImplemented.code) @@ -256,10 +267,9 @@ class TestMetering(MeteringPluginDbTestCase): remote_ip_prefix = '192.168.0.0/24' excluded = True - with self.metering_label_rule(metering_label_id, - direction, - remote_ip_prefix, - excluded) as label_rule: + with self.metering_label_rule( + metering_label_id, direction, excluded, + remote_ip_prefix=remote_ip_prefix) as label_rule: rule_id = label_rule['metering_label_rule']['id'] self._delete('metering-label-rules', rule_id, 204) @@ -274,14 +284,12 @@ class TestMetering(MeteringPluginDbTestCase): remote_ip_prefix = '192.168.0.0/24' excluded = True - with self.metering_label_rule(metering_label_id, - direction, - remote_ip_prefix, - excluded) as v1,\ - self.metering_label_rule(metering_label_id, - 'ingress', - remote_ip_prefix, - excluded) as v2: + with self.metering_label_rule( + metering_label_id, direction, excluded, + remote_ip_prefix=remote_ip_prefix) as v1,\ + self.metering_label_rule( + metering_label_id, 'ingress', excluded, + remote_ip_prefix=remote_ip_prefix) as v2: metering_label_rule = (v1, v2) self._test_list_resources('metering-label-rule', @@ -298,14 +306,12 @@ class TestMetering(MeteringPluginDbTestCase): remote_ip_prefix = '192.168.0.0/24' excluded = True - with self.metering_label_rule(metering_label_id, - direction, - remote_ip_prefix, - excluded) as v1,\ - self.metering_label_rule(metering_label_id, - direction, - n_consts.IPv4_ANY, - False) as v2: + with self.metering_label_rule( + metering_label_id, direction, excluded, + remote_ip_prefix=remote_ip_prefix) as v1,\ + self.metering_label_rule( + metering_label_id, direction, False, + remote_ip_prefix=n_consts.IPv4_ANY) as v2: metering_label_rule = (v1, v2) self._test_list_resources('metering-label-rule', @@ -323,15 +329,12 @@ class TestMetering(MeteringPluginDbTestCase): remote_ip_prefix2 = '192.168.0.0/16' excluded = True - with self.metering_label_rule(metering_label_id, - direction, - remote_ip_prefix1, - excluded): - res = self._create_metering_label_rule(self.fmt, - metering_label_id, - direction, - remote_ip_prefix2, - excluded) + with self.metering_label_rule( + metering_label_id, direction, excluded, + remote_ip_prefix=remote_ip_prefix1): + res = self._create_metering_label_rule( + self.fmt, metering_label_id, direction, excluded, + remote_ip_prefix=remote_ip_prefix2) self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) def test_create_metering_label_rule_two_labels(self): @@ -349,14 +352,12 @@ class TestMetering(MeteringPluginDbTestCase): remote_ip_prefix = '192.168.0.0/24' excluded = True - with self.metering_label_rule(metering_label_id1, - direction, - remote_ip_prefix, - excluded) as v1,\ - self.metering_label_rule(metering_label_id2, - direction, - remote_ip_prefix, - excluded) as v2: + with self.metering_label_rule( + metering_label_id1, direction, excluded, + remote_ip_prefix=remote_ip_prefix) as v1,\ + self.metering_label_rule( + metering_label_id2, direction, excluded, + remote_ip_prefix=remote_ip_prefix) as v2: metering_label_rule = (v1, v2) self._test_list_resources('metering-label-rule', diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index 021a1cacf2e..9aa814ea524 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -59,7 +59,7 @@ object_data = { 'L3HARouterNetwork': '1.0-87acea732853f699580179a94d2baf91', 'L3HARouterVRIdAllocation': '1.0-37502aebdbeadc4f9e3bd5e9da714ab9', 'MeteringLabel': '1.0-cc4b620a3425222447cbe459f62de533', - 'MeteringLabelRule': '1.0-b5c5717e7bab8d1af1623156012a5842', + 'MeteringLabelRule': '2.0-0ad09894c62e1ce6e868f725158959ba', 'Log': '1.0-6391351c0f34ed34375a19202f361d24', 'Network': '1.1-c3e9ecc0618ee934181d91b143a48901', 'NetworkDhcpAgentBinding': '1.1-d9443c88809ffa4c45a0a5a48134b54a', diff --git a/neutron/tests/unit/services/metering/test_metering_plugin.py b/neutron/tests/unit/services/metering/test_metering_plugin.py index beaeb22d4d8..f430c5894ac 100644 --- a/neutron/tests/unit/services/metering/test_metering_plugin.py +++ b/neutron/tests/unit/services/metering/test_metering_plugin.py @@ -22,6 +22,8 @@ from neutron_lib.plugins import constants from neutron_lib.plugins import directory from neutron_lib.tests import tools from neutron_lib.utils import net as net_utils + +from oslo_serialization import jsonutils from oslo_utils import uuidutils from neutron.api.rpc.agentnotifiers import metering_rpc_agent_api @@ -275,6 +277,8 @@ class TestMeteringPlugin(test_db_base_plugin_v2.NeutronDbPluginV2TestCase, 'remote_ip_prefix': net_utils.AuthenticIPNetwork( '10.0.0.0/24'), + 'destination_ip_prefix': None, + 'source_ip_prefix': None, 'direction': 'ingress', 'metering_label_id': self.uuid, 'excluded': False, @@ -293,6 +297,8 @@ class TestMeteringPlugin(test_db_base_plugin_v2.NeutronDbPluginV2TestCase, 'remote_ip_prefix': net_utils.AuthenticIPNetwork( '10.0.0.0/24'), + 'destination_ip_prefix': None, + 'source_ip_prefix': None, 'direction': 'ingress', 'metering_label_id': self.uuid, 'excluded': False, @@ -300,18 +306,258 @@ class TestMeteringPlugin(test_db_base_plugin_v2.NeutronDbPluginV2TestCase, 'id': self.uuid}], 'id': self.uuid}] + remote_ip_prefix = {'remote_ip_prefix': '10.0.0.0/24'} with self.router(tenant_id=self.tenant_id, set_context=True): with self.metering_label(tenant_id=self.tenant_id, set_context=True) as label: la = label['metering_label'] self.mock_uuid.return_value = second_uuid - with self.metering_label_rule(la['id']): + with self.metering_label_rule(la['id'], **remote_ip_prefix): self.mock_add_rule.assert_called_with(self.ctx, expected_add) self._delete('metering-label-rules', second_uuid) self.mock_remove_rule.assert_called_with(self.ctx, expected_del) + def test_add_and_remove_metering_label_rule_source_ip_only(self): + second_uuid = 'e27fe2df-376e-4ac7-ae13-92f050a21f84' + expected_add = [{'status': 'ACTIVE', + 'name': 'router1', + 'gw_port_id': None, + 'admin_state_up': True, + 'distributed': False, + 'tenant_id': self.tenant_id, + '_metering_labels': [ + {'rule': { + 'source_ip_prefix': + net_utils.AuthenticIPNetwork( + '10.0.0.0/24'), + 'destination_ip_prefix': None, + 'remote_ip_prefix': None, + 'direction': 'ingress', + 'metering_label_id': self.uuid, + 'excluded': False, + 'id': second_uuid}, + 'id': self.uuid}], + 'id': self.uuid}] + + expected_del = [{'status': 'ACTIVE', + 'name': 'router1', + 'gw_port_id': None, + 'admin_state_up': True, + 'distributed': False, + 'tenant_id': self.tenant_id, + '_metering_labels': [ + {'rule': { + 'source_ip_prefix': + net_utils.AuthenticIPNetwork( + '10.0.0.0/24'), + 'destination_ip_prefix': None, + 'remote_ip_prefix': None, + 'direction': 'ingress', + 'metering_label_id': self.uuid, + 'excluded': False, + 'id': second_uuid}, + 'id': self.uuid}], + 'id': self.uuid}] + + source_ip_prefix = {'source_ip_prefix': '10.0.0.0/24'} + with self.router(tenant_id=self.tenant_id, set_context=True): + with self.metering_label(tenant_id=self.tenant_id, + set_context=True) as label: + la = label['metering_label'] + self.mock_uuid.return_value = second_uuid + with self.metering_label_rule(la['id'], + **source_ip_prefix): + self.mock_add_rule.assert_called_with(self.ctx, + expected_add) + self._delete('metering-label-rules', second_uuid) + self.mock_remove_rule.assert_called_with(self.ctx, + expected_del) + + def test_add_and_remove_metering_label_rule_dest_ip_only(self): + second_uuid = 'e27fe2df-376e-4ac7-ae13-92f050a21f84' + expected_add = [{'status': 'ACTIVE', + 'name': 'router1', + 'gw_port_id': None, + 'admin_state_up': True, + 'distributed': False, + 'tenant_id': self.tenant_id, + '_metering_labels': [ + {'rule': { + 'destination_ip_prefix': + net_utils.AuthenticIPNetwork( + '10.0.0.0/24'), + 'source_ip_prefix': None, + 'remote_ip_prefix': None, + 'direction': 'ingress', + 'metering_label_id': self.uuid, + 'excluded': False, + 'id': second_uuid}, + 'id': self.uuid}], + 'id': self.uuid}] + + expected_del = [{'status': 'ACTIVE', + 'name': 'router1', + 'gw_port_id': None, + 'admin_state_up': True, + 'distributed': False, + 'tenant_id': self.tenant_id, + '_metering_labels': [ + {'rule': { + 'destination_ip_prefix': + net_utils.AuthenticIPNetwork( + '10.0.0.0/24'), + 'source_ip_prefix': None, + 'remote_ip_prefix': None, + 'direction': 'ingress', + 'metering_label_id': self.uuid, + 'excluded': False, + 'id': second_uuid}, + 'id': self.uuid}], + 'id': self.uuid}] + + source_ip_prefix = {'destination_ip_prefix': '10.0.0.0/24'} + with self.router(tenant_id=self.tenant_id, set_context=True): + with self.metering_label(tenant_id=self.tenant_id, + set_context=True) as label: + la = label['metering_label'] + self.mock_uuid.return_value = second_uuid + with self.metering_label_rule(la['id'], + **source_ip_prefix): + self.mock_add_rule.assert_called_with(self.ctx, + expected_add) + self._delete('metering-label-rules', second_uuid) + self.mock_remove_rule.assert_called_with(self.ctx, + expected_del) + + def test_add_and_remove_metering_label_rule_src_and_dest_ip_only(self): + second_uuid = 'e27fe2df-376e-4ac7-ae13-92f050a21f84' + expected_add = [{'status': 'ACTIVE', + 'name': 'router1', + 'gw_port_id': None, + 'admin_state_up': True, + 'distributed': False, + 'tenant_id': self.tenant_id, + '_metering_labels': [ + {'rule': { + 'destination_ip_prefix': + net_utils.AuthenticIPNetwork('0.0.0.0/0'), + 'source_ip_prefix': + net_utils.AuthenticIPNetwork( + '10.0.0.0/24'), + 'remote_ip_prefix': None, + 'direction': 'ingress', + 'metering_label_id': self.uuid, + 'excluded': False, + 'id': second_uuid}, + 'id': self.uuid}], + 'id': self.uuid}] + + expected_del = [{'status': 'ACTIVE', + 'name': 'router1', + 'gw_port_id': None, + 'admin_state_up': True, + 'distributed': False, + 'tenant_id': self.tenant_id, + '_metering_labels': [ + {'rule': { + 'destination_ip_prefix': + net_utils.AuthenticIPNetwork('0.0.0.0/0'), + 'source_ip_prefix': + net_utils.AuthenticIPNetwork( + '10.0.0.0/24'), + 'remote_ip_prefix': None, + 'direction': 'ingress', + 'metering_label_id': self.uuid, + 'excluded': False, + 'id': second_uuid}, + 'id': self.uuid}], + 'id': self.uuid}] + + ip_prefixes = {'source_ip_prefix': '10.0.0.0/24', + 'destination_ip_prefix': '00.0.0.0/0'} + with self.router(tenant_id=self.tenant_id, set_context=True): + with self.metering_label(tenant_id=self.tenant_id, + set_context=True) as label: + la = label['metering_label'] + self.mock_uuid.return_value = second_uuid + with self.metering_label_rule(la['id'], + **ip_prefixes): + self.mock_add_rule.assert_called_with(self.ctx, + expected_add) + self._delete('metering-label-rules', second_uuid) + self.mock_remove_rule.assert_called_with(self.ctx, + expected_del) + + def test_add_and_remove_metering_label_rule_src_and_remote_ip(self): + with self.router(tenant_id=self.tenant_id, set_context=True): + with self.metering_label(tenant_id=self.tenant_id, + set_context=True) as label: + la = label['metering_label'] + + res = self._create_metering_label_rule( + self.fmt, la['id'], 'ingress', False, + remote_ip_prefix='0.0.0.0/0', + source_ip_prefix='10.0.0.0/24') + + expected_error_code = 500 + self.assertEqual(expected_error_code, res.status_int) + + expected_error_message = "Cannot use 'remote-ip-prefix' in " \ + "conjunction with " \ + "'source-ip-prefix' or " \ + "'destination-ip-prefix'." + + self.assertEqual( + expected_error_message, jsonutils.loads(res.body)[ + "NeutronError"]["message"]) + + def test_add_and_remove_metering_label_rule_dest_and_remote_ip(self): + with self.router(tenant_id=self.tenant_id, set_context=True): + with self.metering_label(tenant_id=self.tenant_id, + set_context=True) as label: + la = label['metering_label'] + + res = self._create_metering_label_rule( + self.fmt, la['id'], 'ingress', False, + remote_ip_prefix='0.0.0.0/0', + destination_ip_prefix='8.8.8.8/32') + + expected_error_code = 500 + self.assertEqual(expected_error_code, res.status_int) + + expected_error_message = "Cannot use 'remote-ip-prefix' in " \ + "conjunction with " \ + "'source-ip-prefix' or " \ + "'destination-ip-prefix'." + + self.assertEqual( + expected_error_message, jsonutils.loads(res.body)[ + "NeutronError"]["message"]) + + def test_add_and_remove_metering_label_rule_no_ip_prefix_entered(self): + with self.router(tenant_id=self.tenant_id, set_context=True): + with self.metering_label(tenant_id=self.tenant_id, + set_context=True) as label: + la = label['metering_label'] + + res = self._create_metering_label_rule( + self.fmt, la['id'], 'ingress', False) + + expected_error_code = 500 + self.assertEqual(expected_error_code, res.status_int) + + expected_error_message = "You must define at least one of " \ + "the following parameters " \ + "'remote_ip_prefix', or " \ + "'source_ip_prefix' or " \ + "'destination_ip_prefix'." + + self.assertEqual( + expected_error_message, jsonutils.loads(res.body)[ + "NeutronError"]["message"]) + def test_delete_metering_label_does_not_clear_router_tenant_id(self): tenant_id = '654f6b9d-0f36-4ae5-bd1b-01616794ca60' with self.metering_label(tenant_id=tenant_id) as metering_label: