"""Abstraction of an OF table."""
# Copyright (C) 2015 Brad Cowie, Christopher Lorier and Joe Stringer.
# Copyright (C) 2015 Research and Education Advanced Network New Zealand Ltd.
# Copyright (C) 2015--2019 The Contributors
#
# 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 functools
import hashlib
import struct
from faucet import valve_of
from faucet.faucet_pipeline import ValveTableConfig
[docs]
class ValveTable: # pylint: disable=too-many-arguments,too-many-instance-attributes
"""Wrapper for an OpenFlow table."""
def __init__(
self,
name,
table_config,
flow_cookie,
notify_flow_removed=False,
next_tables=None,
):
self.name = name
self.table_config = table_config
self.table_id = self.table_config.table_id
self.set_fields = self.table_config.set_fields
self.exact_match = self.table_config.exact_match
self.match_types = None
self.metadata_match = self.table_config.metadata_match
self.metadata_write = self.table_config.metadata_write
if next_tables:
self.next_tables = next_tables
else:
self.next_tables = []
if self.table_config.match_types:
self.match_types = {}
for field, mask in self.table_config.match_types:
self.match_types[field] = mask
self.flow_cookie = flow_cookie
self.notify_flow_removed = notify_flow_removed
[docs]
def goto(self, next_table):
"""Add goto next table instruction."""
assert (
next_table.name in self.table_config.next_tables
), "%s not configured as next table in %s" % (next_table.name, self.name)
return valve_of.goto_table(next_table)
[docs]
def goto_this(self):
return valve_of.goto_table(self)
[docs]
def goto_miss(self, next_table):
"""Add miss goto table instruction."""
assert (
next_table.name == self.table_config.miss_goto
), "%s not configured as miss table in %s" % (next_table.name, self.name)
return valve_of.goto_table(next_table)
[docs]
@staticmethod
def set_field(**kwds):
"""Return set field action."""
# raise exception if unknown set field.
valve_of.match_from_dict(kwds)
return valve_of.set_field(**kwds)
[docs]
def set_external_forwarding_requested(self):
"""Set field for external forwarding requested."""
return self.set_field(
**{valve_of.EXTERNAL_FORWARDING_FIELD: valve_of.PCP_EXT_PORT_FLAG}
)
[docs]
def set_no_external_forwarding_requested(self):
"""Set field for no external forwarding requested."""
return self.set_field(
**{valve_of.EXTERNAL_FORWARDING_FIELD: valve_of.PCP_NONEXT_PORT_FLAG}
)
[docs]
def set_vlan_vid(self, vlan_vid):
"""Set VLAN VID with VID_PRESENT flag set.
Args:
vid (int): VLAN VID
Returns:
ryu.ofproto.ofproto_v1_3_parser.OFPActionSetField: set VID with VID_PRESENT.
"""
return self.set_field(vlan_vid=valve_of.vid_present(vlan_vid))
# TODO: verify actions
[docs]
@staticmethod
@functools.lru_cache(maxsize=1024)
def match(
in_port=None,
vlan=None, # pylint: disable=too-many-arguments
eth_type=None,
eth_src=None,
eth_dst=None,
eth_dst_mask=None,
icmpv6_type=None,
nw_proto=None,
nw_dst=None,
metadata=None,
metadata_mask=None,
vlan_pcp=None,
udp_src=None,
udp_dst=None,
):
"""Compose an OpenFlow match rule."""
match_dict = valve_of.build_match_dict(
in_port,
vlan,
eth_type,
eth_src,
eth_dst,
eth_dst_mask,
icmpv6_type,
nw_proto,
nw_dst,
metadata,
metadata_mask,
vlan_pcp,
udp_src,
udp_dst,
)
return valve_of.match(match_dict)
def _verify_flowmod(self, flowmod):
match_fields = flowmod.match.items()
if valve_of.is_flowdel(flowmod):
if self.table_id != valve_of.ofp.OFPTT_ALL:
for match_type, match_field in match_fields:
assert match_type in self.match_types, "%s match in table %s" % (
match_type,
self.name,
)
else:
# TODO: ACL builder should not use ALL table.
if self.table_id == valve_of.ofp.OFPTT_ALL:
return
assert not (
flowmod.priority == 0 and match_fields
), "default flow cannot have matches on table %s: %s" % (self.name, flowmod)
for match_type, match_field in match_fields:
assert match_type in self.match_types, "%s match in table %s" % (
match_type,
self.name,
)
config_mask = self.match_types[match_type]
flow_mask = isinstance(match_field, tuple)
assert config_mask or (
not config_mask and not flow_mask
), "%s configured mask %s but flow mask %s in table %s (%s)" % (
match_type,
config_mask,
flow_mask,
self.name,
flowmod,
)
if self.exact_match and match_fields:
assert len(self.match_types) == len(match_fields), (
"exact match table %s matches %s do not match flow matches %s (%s)"
% (self.name, self.match_types, match_fields, flowmod)
)
def _trim_actions(self, actions):
new_actions = []
pending_actions = []
for action in actions:
if action.type in (valve_of.ofp.OFPAT_GROUP, valve_of.ofp.OFPAT_OUTPUT):
new_actions.extend(pending_actions)
new_actions.append(action)
pending_actions = []
else:
pending_actions.append(action)
set_fields = {
action.key for action in new_actions if valve_of.is_set_field(action)
}
if self.table_id != valve_of.ofp.OFPTT_ALL and set_fields:
assert set_fields.issubset(
self.set_fields
), "unexpected set fields %s configured %s in %s" % (
set_fields,
self.set_fields,
self.name,
)
return new_actions
@functools.lru_cache()
def _trim_inst(self, inst):
"""Discard empty/actions on packets that are not output and not goto another table."""
inst_types = {instruction.type for instruction in inst}
if valve_of.ofp.OFPIT_APPLY_ACTIONS in inst_types:
goto_present = valve_of.ofp.OFPIT_GOTO_TABLE in inst_types
new_inst = []
for instruction in inst:
if instruction.type == valve_of.ofp.OFPIT_APPLY_ACTIONS:
recirc_present = any(
(
True
for action in instruction.actions
if valve_of.is_ct(action)
and hasattr(action, "recirc_table")
)
)
# If no goto present, this is the last set of actions that can take place
if not goto_present and not recirc_present:
instruction.actions = self._trim_actions(instruction.actions)
# Always drop an apply actions instruction with no actions.
if not instruction.actions:
continue
new_inst.append(instruction)
return tuple(new_inst)
return inst
[docs]
def flowmod(
self,
match=None,
priority=None, # pylint: disable=too-many-arguments
inst=None,
command=valve_of.ofp.OFPFC_ADD,
out_port=0,
out_group=0,
hard_timeout=0,
idle_timeout=0,
cookie=None,
):
"""Helper function to construct a flow mod message with cookie."""
if priority is None:
priority = 0 # self.dp.lowest_priority
if not match:
match = self.match()
if inst is None:
inst = ()
if cookie is None:
cookie = self.flow_cookie
flags = 0
if self.notify_flow_removed:
flags = valve_of.ofp.OFPFF_SEND_FLOW_REM
if inst:
inst = self._trim_inst(inst)
flowmod = valve_of.flowmod(
cookie,
command,
self.table_id,
priority,
out_port,
out_group,
match,
tuple(inst),
hard_timeout,
idle_timeout,
flags,
)
self._verify_flowmod(flowmod)
return flowmod
[docs]
def flowdel(
self, match=None, priority=None, out_port=valve_of.ofp.OFPP_ANY, strict=False
):
"""Delete matching flows from a table."""
command = valve_of.ofp.OFPFC_DELETE
if strict:
command = valve_of.ofp.OFPFC_DELETE_STRICT
return self.flowmod(
match=match,
priority=priority,
command=command,
out_port=out_port,
out_group=valve_of.ofp.OFPG_ANY,
)
[docs]
def flowdrop(self, match=None, priority=None, hard_timeout=0):
"""Add drop matching flow to a table."""
return self.flowmod(
match=match, priority=priority, hard_timeout=hard_timeout, inst=()
)
[docs]
def flowcontroller(self, match=None, priority=None, inst=None, max_len=96):
"""Add flow outputting to controller."""
if inst is None:
inst = ()
return self.flowmod(
match=match,
priority=priority,
inst=(valve_of.apply_actions((valve_of.output_controller(max_len),)),)
+ inst,
)
[docs]
class ValveGroupEntry:
"""Abstraction for a single OpenFlow group entry."""
def __init__(self, table, group_id, buckets):
self.table = table
self.group_id = group_id
self.update_buckets(buckets)
[docs]
def update_buckets(self, buckets):
"""Update entry with new buckets."""
self.buckets = tuple(buckets)
[docs]
def add(self):
"""Return flows to add this entry to the group table."""
ofmsgs = []
ofmsgs.extend(valve_of.groupadd(group_id=self.group_id, buckets=self.buckets))
self.table.entries[self.group_id] = self
return ofmsgs
[docs]
def delete(self):
"""Return flow to delete an existing group entry."""
if self.group_id in self.table.entries:
del self.table.entries[self.group_id]
return valve_of.groupdel(group_id=self.group_id)
[docs]
class ValveGroupTable:
"""Wrap access to group table."""
entries = None # type: dict
def __init__(self):
"""Constructs a new object"""
self.entries = {}
[docs]
@staticmethod
def group_id_from_str(key_str):
"""Return a group ID based on a string key."""
# TODO: does not handle collisions
digest = hashlib.sha256(key_str.encode("utf-8")).digest()
return struct.unpack("<L", digest[:4])[0]
[docs]
def get_entry(self, group_id, buckets):
"""Update entry with group_id with buckets, and return the entry."""
if group_id in self.entries:
self.entries[group_id].update_buckets(buckets)
else:
self.entries[group_id] = ValveGroupEntry(self, group_id, buckets)
return self.entries[group_id]
[docs]
def delete_all(self):
"""Delete all groups."""
self.entries = {}
return valve_of.groupdel()
wildcard_table = ValveTable(
"all", ValveTableConfig("all", valve_of.ofp.OFPTT_ALL), flow_cookie=0
)