From 5f07d7019c42f1b1c1801dc93206ef30b7d21691 Mon Sep 17 00:00:00 2001 From: Omer Anson Date: Mon, 30 Jan 2017 22:36:04 +0200 Subject: [PATCH] Add cookie infrastructure to Dragonflow Add a cookie infrastructure that allows global and local cookies to be allocated dynamically in dragonflow. Global cookies are cross-system. (e.g. l3 and security groups use these bits for the same thing) Local cookie space can be reused by applications (e.g. l3 and security groups can use the same bits for different things) Change-Id: I0e2dcc24ead244762cb5e9f3c18ad5f48652f914 Related-Bug: #1471552 --- dragonflow/common/exceptions.py | 13 +++ dragonflow/controller/common/constants.py | 11 +- dragonflow/controller/common/cookies.py | 131 ++++++++++++++++++++++ dragonflow/controller/common/utils.py | 14 ++- dragonflow/controller/df_base_app.py | 13 ++- dragonflow/controller/l3_proactive_app.py | 15 ++- dragonflow/controller/sg_app.py | 24 ++-- dragonflow/tests/unit/test_cookies.py | 95 ++++++++++++++++ 8 files changed, 285 insertions(+), 31 deletions(-) create mode 100644 dragonflow/controller/common/cookies.py create mode 100644 dragonflow/tests/unit/test_cookies.py diff --git a/dragonflow/common/exceptions.py b/dragonflow/common/exceptions.py index 313b7af7b..4b6be28a4 100644 --- a/dragonflow/common/exceptions.py +++ b/dragonflow/common/exceptions.py @@ -97,3 +97,16 @@ class UnknownResourceException(DragonflowException): class InvalidDBHostConfiguration(DragonflowException): message = _('The DB host string %(host)s is invalid.') + + +class OutOfCookieSpaceException(DragonflowException): + message = _('Out of cookie space.') + + +class MaskOverlapException(DragonflowException): + message = _('Cookie mask overlap for cookie %(app_name)s/%(name)s') + + +class CookieOverflowExcpetion(DragonflowException): + message = _('Cookie overflow: ' + 'Value: %(cookie)s Offset: %(offset)s Mask: %(mask)s') diff --git a/dragonflow/controller/common/constants.py b/dragonflow/controller/common/constants.py index c1e535e83..0d775159f 100644 --- a/dragonflow/controller/common/constants.py +++ b/dragonflow/controller/common/constants.py @@ -101,17 +101,8 @@ CT_ZONE_REG = 0x1d402 MIN_PORT = 1 MAX_PORT = 65535 -""" -Cookie Mask -global cookie is used by flows of all table, but local cookie is used -by flows of a small part of table. In order to avoid conflict, -global cookies should not overlapped with each other, but local cookies -could be overlapped for saving space of cookie. -all cookie's mask should be kept here to avoid conflict. -""" +#TODO(oanson) Remove once Aging app is fully updated to use cookie framework GLOBAL_AGING_COOKIE_MASK = 0x1 -SECURITY_GROUP_RULE_COOKIE_MASK = 0x1fffffffe -SECURITY_GROUP_RULE_COOKIE_SHIFT_LEN = 1 GLOBAL_INIT_AGING_COOKIE = 0x1 # These two globals are constant, as defined by the metadata service API. VMs diff --git a/dragonflow/controller/common/cookies.py b/dragonflow/controller/common/cookies.py new file mode 100644 index 000000000..d14f4f26d --- /dev/null +++ b/dragonflow/controller/common/cookies.py @@ -0,0 +1,131 @@ +# 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 collections + +from oslo_log import log + +from dragonflow._i18n import _LI, _LE +from dragonflow.common import exceptions + + +LOG = log.getLogger(__name__) + + +GLOBAL_APP_NAME = 'global cookie namespace' + + +"""Dictionary to hold a map from a task name to its cookie info""" +_cookies = {} +# Maximum number of bits that can be encoded. Taken from OVS +_cookie_max_bits = 64 +# Maximum number of bits allocated to global cookies +_cookie_max_bits_global = 32 +# Turn on all bits in the cookie mask. There are 64 (_cookie_max_bits) +# bits. -1 is all (infinite) bits on. Shift right and left again to have all +# bits but the least 64 bits on. Bitwise not to have only the 64 LSBits on. +_cookie_mask_all = ~((-1 >> _cookie_max_bits) << _cookie_max_bits) +# Maximum number of bits allocated to local cookies (total bits - global bits) +_cookie_max_bits_local = _cookie_max_bits - _cookie_max_bits_global +# Number of allocated bits for a given application (including global) +_cookies_used_bits = collections.defaultdict(int) + + +# A class holding the cookie's offset and bit-mask +CookieBitPair = collections.namedtuple('CookieBitPair', ('offset', 'mask')) + + +def register_cookie_bits(name, length, is_local=False, app_name=None): + """Register this many cookie bits for the given 'task'. + There are two types of cookies: global and local. + Global cookies are global accross all applications. All applications share + the information, and the cookie bits can only be assigned once. + Local cookies are local to a specific application. That application is + responsible to the data encoded in the cookie. Therefore, local cookie + bits can be reused between applications, i.e. different applications can + use the same local cookie bits to write different things. + This function raises an error if there are not enough bits to allocate. + :param name: The name of the 'task' + :type name: string + :param length: The length of the cookie to allocate + :type length: int + :param is_local: The cookie space is local, as defined above. + :type is_local: bool + :param app_name: Owner application of the cookie (None for global) + :type app_name: string + """ + if not is_local: + app_name = GLOBAL_APP_NAME + shift = 0 + max_bits = _cookie_max_bits_global + else: + shift = _cookie_max_bits_global + max_bits = _cookie_max_bits_local + if not app_name: + raise TypeError(_LE("app_name must be provided " + "if is_local is True")) + if (app_name, name) in _cookies: + LOG.info(_LI("Cookie for %(app_name)s/%(name)s already registered."), + {"app_name": app_name, "name": name}) + return + start = _cookies_used_bits[app_name] + if start + length > max_bits: + LOG.error(_LE("Out of cookie space: " + "offset: %(offset)d length: %(length)d"), + {"offset": start, "length": length}) + raise exceptions.OutOfCookieSpaceException() + _cookies_used_bits[app_name] = start + length + start += shift + mask = (_cookie_mask_all >> (_cookie_max_bits - length)) << start + _cookies[(app_name, name)] = CookieBitPair(start, mask) + LOG.info(_LI("Registered cookie for %(app_name)s/%(name)s, " + "mask: %(mask)x, offset: %(offset)d, length: %(length)d"), + {"app_name": app_name, "name": name, + "mask": mask, "offset": start, "length": length}) + + +def get_cookie(name, value, old_cookie=0, old_mask=0, + is_local=False, app_name=None): + """Encode the given cookie value as the registered cookie. i.e. shift + it to the correct location, and verify there are no overflows. + :param name: The name of the 'task' + :type name: string + :param value: The value of the cookie to encode + :type value: int + :param old_cookie: Encode this cookie alongside other cookie values + :type old_cookie: int + :param old_mask: The mask (i.e. encoded relevant bits) in old_cookie + :type old_mask: int + :param is_local: The cookie space is local, as defined in + register_cookie_bits + :type is_local: bool + :param app_name: Owner application of the cookie (None for global) + :type app_name: string + """ + if not is_local: + app_name = GLOBAL_APP_NAME + else: + if not app_name: + raise TypeError(_LE("app_name must be provided " + "if is_local is True")) + pair = _cookies[(app_name, name)] + mask_overlap = old_mask & pair.mask + if mask_overlap != 0: + if mask_overlap != pair.mask: + raise exceptions.MaskOverlapException(app_name=app_name, name=name) + return old_cookie, old_mask + result_unmasked = (value << pair.offset) + result = (result_unmasked & pair.mask) + if result != result_unmasked: + raise exceptions.CookieOverflowExcpetion(cookie=value, + offset=pair.offset, mask=pair.mask) + return result | (old_cookie & ~pair.mask), pair.mask | old_mask diff --git a/dragonflow/controller/common/utils.py b/dragonflow/controller/common/utils.py index ccea93b21..9d9cff8b5 100644 --- a/dragonflow/controller/common/utils.py +++ b/dragonflow/controller/common/utils.py @@ -21,6 +21,7 @@ from ryu.lib import addrconv from dragonflow._i18n import _LE from dragonflow.common import exceptions from dragonflow.controller.common import constants as const +from dragonflow.controller.common import cookies LOG = log.getLogger(__name__) @@ -29,6 +30,11 @@ ACTIVE_PORT_DETECTION_APP = \ "active_port_detection_app.ActivePortDetectionApp" +AGING_COOKIE_NAME = 'aging' +AGING_COOKIE_LEN = 1 +cookies.register_cookie_bits(AGING_COOKIE_NAME, AGING_COOKIE_LEN) + + def ipv4_text_to_int(ip_text): try: return struct.unpack('!I', addrconv.ipv4.text_to_bin(ip_text))[0] @@ -54,11 +60,9 @@ def get_aging_cookie(): return _aging_cookie -def set_aging_cookie_bits(cookie): - # clear aging bits before using - c = cookie & (~const.GLOBAL_AGING_COOKIE_MASK) - c |= (_aging_cookie & const.GLOBAL_AGING_COOKIE_MASK) - return c +def set_aging_cookie_bits(old_cookie, old_cookie_mask): + return cookies.get_cookie(AGING_COOKIE_NAME, _aging_cookie, + old_cookie, old_cookie_mask) def get_xor_cookie(cookie): diff --git a/dragonflow/controller/df_base_app.py b/dragonflow/controller/df_base_app.py index f7d4cd58b..9dac2965a 100644 --- a/dragonflow/controller/df_base_app.py +++ b/dragonflow/controller/df_base_app.py @@ -18,6 +18,7 @@ from ryu.lib.packet import ethernet from ryu.lib.packet import packet from ryu.ofproto import ether +from dragonflow.controller.common import cookies from dragonflow.controller.common import utils from dragonflow.controller import df_db_notifier @@ -101,7 +102,7 @@ class DFlowApp(df_db_notifier.DBNotifyInterface): if out_group is None: out_group = datapath.ofproto.OFPG_ANY - cookie = utils.set_aging_cookie_bits(cookie) + cookie, cookie_mask = utils.set_aging_cookie_bits(cookie, cookie_mask) message = datapath.ofproto_parser.OFPFlowMod(datapath, cookie, cookie_mask, @@ -142,3 +143,13 @@ class DFlowApp(df_db_notifier.DBNotifyInterface): dst_ip=dst_ip)) self.send_packet(port, arp_request_pkt) + + def register_local_cookie_bits(self, name, length): + cookies.register_cookie_bits(name, length, + True, self.__class__.__name__) + + def get_local_cookie(self, name, value, old_cookie=0, old_mask=0): + return cookies.get_cookie(name, value, + old_cookie=old_cookie, old_mask=old_mask, + is_local=True, + app_name=self.__class__.__name__) diff --git a/dragonflow/controller/l3_proactive_app.py b/dragonflow/controller/l3_proactive_app.py index 3d86f16af..7457243e8 100644 --- a/dragonflow/controller/l3_proactive_app.py +++ b/dragonflow/controller/l3_proactive_app.py @@ -30,6 +30,7 @@ from dragonflow.db import models ROUTE_TO_ADD = 'route_to_add' ROUTE_ADDED = 'route_added' +COOKIE_NAME = 'tunnel_key' LOG = log.getLogger(__name__) @@ -40,6 +41,7 @@ class L3ProactiveApp(df_base_app.DFlowApp): self.router_port_rarp_cache = {} self.api.register_table_handler(const.L3_LOOKUP_TABLE, self.packet_in_handler) + self.register_local_cookie_bits(COOKIE_NAME, 24) def switch_features_handler(self, ev): self.add_flow_go_to_table(const.L3_LOOKUP_TABLE, @@ -320,8 +322,10 @@ class L3ProactiveApp(df_base_app.DFlowApp): inst = [action_inst, goto_inst] + cookie, cookie_mask = self.get_local_cookie(COOKIE_NAME, tunnel_key) self.mod_flow( - cookie=tunnel_key, + cookie=cookie, + cookie_mask=cookie_mask, inst=inst, table_id=const.L3_LOOKUP_TABLE, priority=const.PRIORITY_VERY_HIGH, @@ -428,8 +432,11 @@ class L3ProactiveApp(df_base_app.DFlowApp): inst = [action_inst, goto_inst] + cookie, cookie_mask = self.get_local_cookie(COOKIE_NAME, + dst_router_tunnel_key) self.mod_flow( - cookie=dst_router_tunnel_key, + cookie=cookie, + cookie_mask=cookie_mask, inst=inst, table_id=const.L3_LOOKUP_TABLE, priority=const.PRIORITY_MEDIUM, @@ -517,10 +524,10 @@ class L3ProactiveApp(df_base_app.DFlowApp): match=match) match = parser.OFPMatch() - cookie = tunnel_key + cookie, cookie_mask = self.get_local_cookie(COOKIE_NAME, tunnel_key) self.mod_flow( cookie=cookie, - cookie_mask=cookie, + cookie_mask=cookie_mask, table_id=const.L3_LOOKUP_TABLE, command=ofproto.OFPFC_DELETE, priority=const.PRIORITY_MEDIUM, diff --git a/dragonflow/controller/sg_app.py b/dragonflow/controller/sg_app.py index b46ef5cff..4305df0cb 100644 --- a/dragonflow/controller/sg_app.py +++ b/dragonflow/controller/sg_app.py @@ -30,8 +30,8 @@ LOG = log.getLogger(__name__) SG_CT_STATE_MASK = const.CT_STATE_NEW | const.CT_STATE_EST | \ const.CT_STATE_REL | const.CT_STATE_INV | const.CT_STATE_TRK -COOKIE_FULLMASK = 0xffffffffffffffff SG_PRIORITY_OFFSET = 2 +COOKIE_NAME = 'sg rule' DEST_FIELD_NAME_BY_PROTOCOL_NUMBER = { n_const.PROTO_NUM_TCP: 'tcp_dst', @@ -76,6 +76,7 @@ class SGApp(df_base_app.DFlowApp): netaddr.IPSet ) self.secgroup_ip_refs = collections.defaultdict(set) + self.register_local_cookie_bits(COOKIE_NAME, 32) @staticmethod def _get_cidr_difference(cidr_set, new_cidr_set): @@ -180,10 +181,8 @@ class SGApp(df_base_app.DFlowApp): results.append(result) return results - @staticmethod - def _get_rule_cookie(rule_id): - rule_cookie = rule_id << const.SECURITY_GROUP_RULE_COOKIE_SHIFT_LEN - return rule_cookie & const.SECURITY_GROUP_RULE_COOKIE_MASK + def _get_rule_cookie(self, rule_id): + return self.get_local_cookie(COOKIE_NAME, rule_id) def _inc_ip_reference_and_check(self, secgroup_id, ip, lport_id): """ @@ -571,9 +570,10 @@ class SGApp(df_base_app.DFlowApp): parameters_merge[ipv4_match_item] = \ SGApp._get_network_and_mask(added_cidr_item) match = parser.OFPMatch(**parameters_merge) + cookie, cookie_mask = self._get_rule_cookie(rule_id) self.mod_flow( - cookie=SGApp._get_rule_cookie(rule_id), - cookie_mask=COOKIE_FULLMASK, + cookie=cookie, + cookie_mask=cookie_mask, inst=inst, table_id=table_id, priority=priority, @@ -645,9 +645,10 @@ class SGApp(df_base_app.DFlowApp): parameters_merge = match_item.copy() parameters_merge.update(address_item) match = parser.OFPMatch(**parameters_merge) + cookie, cookie_mask = self._get_rule_cookie(rule_id) self.mod_flow( - cookie=SGApp._get_rule_cookie(rule_id), - cookie_mask=COOKIE_FULLMASK, + cookie=cookie, + cookie_mask=cookie_mask, inst=inst, table_id=table_id, priority=priority, @@ -674,9 +675,10 @@ class SGApp(df_base_app.DFlowApp): rule_id) return + cookie, cookie_mask = self._get_rule_cookie(rule_id) self.mod_flow( - cookie=SGApp._get_rule_cookie(rule_id), - cookie_mask=const.SECURITY_GROUP_RULE_COOKIE_MASK, + cookie=cookie, + cookie_mask=cookie_mask, table_id=table_id, command=ofproto.OFPFC_DELETE) diff --git a/dragonflow/tests/unit/test_cookies.py b/dragonflow/tests/unit/test_cookies.py new file mode 100644 index 000000000..1b4dd927d --- /dev/null +++ b/dragonflow/tests/unit/test_cookies.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 collections +import mock + +from dragonflow.common import exceptions +from dragonflow.controller.common import cookies +from dragonflow.tests import base as tests_base + + +class TestCookies(tests_base.BaseTestCase): + @mock.patch.object(cookies, '_cookies_used_bits', + collections.defaultdict(int)) + @mock.patch.object(cookies, '_cookies', {}) + def test_register_cookie_bits(self): + _cookies = cookies._cookies + used_bits = cookies._cookies_used_bits + cookies.register_cookie_bits('test1', 3) + cookies.register_cookie_bits('test2', 5) + cookies.register_cookie_bits('test3', 5, True, 'app') + cookies.register_cookie_bits('test4', 4, True, 'app') + self.assertEqual(cookies.CookieBitPair(0, 0x7), + _cookies[(cookies.GLOBAL_APP_NAME, 'test1')]) + self.assertEqual(cookies.CookieBitPair(3, 0x1f << 3), + _cookies[(cookies.GLOBAL_APP_NAME, 'test2')]) + self.assertEqual(cookies.CookieBitPair(32, 0x1f << 32), + _cookies[('app', 'test3')]) + self.assertEqual(cookies.CookieBitPair(37, 0xf << (32 + 5)), + _cookies[('app', 'test4')]) + self.assertEqual(8, used_bits[cookies.GLOBAL_APP_NAME]) + self.assertEqual(9, used_bits['app']) + + @mock.patch.object(cookies, '_cookies_used_bits', + collections.defaultdict(int)) + @mock.patch.object(cookies, '_cookies', {}) + def test_register_and_get_cookies(self): + cookies.register_cookie_bits('test1', 3) + cookies.register_cookie_bits('test2', 5) + cookies.register_cookie_bits('test3', 5, True, 'app') + cookies.register_cookie_bits('test4', 4, True, 'app') + self.assertEqual((3, 0x7), cookies.get_cookie('test1', 3)) + self.assertEqual((5 << 3, 0x1f << 3), cookies.get_cookie('test2', 5)) + self.assertEqual((10 << 32, 0x1f << 32), + cookies.get_cookie('test3', 10, + is_local=True, app_name='app')) + self.assertEqual((13 << (32 + 5) | 10 << 32, 0xf << 37 | 0x1f << 32), + cookies.get_cookie('test4', 13, + old_cookie=10 << 32, + old_mask=0x1f << 32, + is_local=True, app_name='app')) + cookie, mask = cookies.get_cookie('test1', 2) + self.assertEqual((2, 0x7), + cookies.get_cookie('test1', 3, cookie, mask)) + + @mock.patch.object(cookies, '_cookies_used_bits', + collections.defaultdict(int)) + @mock.patch.object(cookies, '_cookies', {}) + def test_register_cookie_bits_errors(self): + self.assertRaises(TypeError, + cookies.register_cookie_bits, 't1', 3, True) + self.assertRaises(exceptions.OutOfCookieSpaceException, + cookies.register_cookie_bits, 't1', 33) + self.assertRaises(exceptions.OutOfCookieSpaceException, + cookies.register_cookie_bits, 't1', 33, True, 'app') + cookies.register_cookie_bits('t1', 10) + cookies.register_cookie_bits('t1', 10, True, 'app') + self.assertRaises(exceptions.OutOfCookieSpaceException, + cookies.register_cookie_bits, 't2', 23) + self.assertRaises(exceptions.OutOfCookieSpaceException, + cookies.register_cookie_bits, 't2', 23, True, 'app') + + @mock.patch.object(cookies, '_cookies_used_bits', + collections.defaultdict(int)) + @mock.patch.object(cookies, '_cookies', {}) + def test_get_cookies_errors(self): + cookies.register_cookie_bits('test1', 3) + cookies.register_cookie_bits('test2', 5) + cookies.register_cookie_bits('test3', 5, True, 'app') + cookies.register_cookie_bits('test4', 4, True, 'app') + self.assertRaises(TypeError, + cookies.get_cookie, 'test3', 3, is_local=True) + self.assertRaises(exceptions.CookieOverflowExcpetion, + cookies.get_cookie, 'test1', 9) + self.assertRaises(exceptions.MaskOverlapException, + cookies.get_cookie, 'test2', 9, 0, 0x8)