Merge "Add cookie infrastructure to Dragonflow"

This commit is contained in:
Jenkins 2017-02-12 10:07:53 +00:00 committed by Gerrit Code Review
commit c67d036b69
8 changed files with 285 additions and 31 deletions

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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__)

View File

@ -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.router_port_rarp_cache.clear()
@ -317,8 +319,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,
@ -425,8 +429,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,
@ -514,10 +521,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,

View File

@ -29,8 +29,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',
@ -75,6 +75,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):
@ -179,10 +180,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):
"""
@ -570,9 +569,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,
@ -644,9 +644,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,
@ -673,9 +674,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)

View File

@ -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)