summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorAdrian Moreno <amorenoz@redhat.com>2022-07-08 20:03:06 +0200
committerIlya Maximets <i.maximets@ovn.org>2022-07-15 20:14:21 +0200
commit3923b9331d89636eb4d69ace42a4161556825530 (patch)
treefaeec041fdf9a0d58b48b4ab49bda762a45ba9d9 /python
parent1215cf13346aeaf92f7a861b105b9d562b889ecc (diff)
downloadopenvswitch-3923b9331d89636eb4d69ace42a4161556825530.tar.gz
python: Introduce OpenFlow Flow parsing.
Introduce OFPFlow class and all its decoders. Most of the decoders are generic (from decoders.py). Some have special syntax and need a specific implementation. Decoders for nat are moved to the common decoders.py because it's syntax is shared with other types of flows (e.g: dpif flows). Acked-by: Eelco Chaudron <echaudro@redhat.com> Signed-off-by: Adrian Moreno <amorenoz@redhat.com> Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
Diffstat (limited to 'python')
-rw-r--r--python/automake.mk2
-rw-r--r--python/ovs/flow/decoders.py108
-rw-r--r--python/ovs/flow/ofp.py428
-rw-r--r--python/ovs/flow/ofp_act.py306
4 files changed, 844 insertions, 0 deletions
diff --git a/python/automake.mk b/python/automake.mk
index 0a0a479a4..b426351d7 100644
--- a/python/automake.mk
+++ b/python/automake.mk
@@ -31,6 +31,8 @@ ovs_pyfiles = \
python/ovs/flow/flow.py \
python/ovs/flow/kv.py \
python/ovs/flow/list.py \
+ python/ovs/flow/ofp.py \
+ python/ovs/flow/ofp_act.py \
python/ovs/flow/ofp_fields.py \
python/ovs/json.py \
python/ovs/jsonrpc.py \
diff --git a/python/ovs/flow/decoders.py b/python/ovs/flow/decoders.py
index 883e61acf..73d28e057 100644
--- a/python/ovs/flow/decoders.py
+++ b/python/ovs/flow/decoders.py
@@ -6,6 +6,7 @@ object.
"""
import netaddr
+import re
class Decoder(object):
@@ -414,3 +415,110 @@ class IPMask(Decoder):
def to_json(self):
return str(self)
+
+
+def decode_free_output(value):
+ """The value of the output action can be found free, i.e: without the
+ 'output' keyword. This decoder decodes its value when found this way."""
+ try:
+ return "output", {"port": int(value)}
+ except ValueError:
+ return "output", {"port": value.strip('"')}
+
+
+ipv4 = r"(?:\d{1,3}.?){3}\d{1,3}"
+ipv4_capture = r"({ipv4})".format(ipv4=ipv4)
+ipv6 = r"[\w:\.]+"
+ipv6_capture = r"(?:\[*)?({ipv6})(?:\]*)?".format(ipv6=ipv6)
+port_range = r":(\d+)(?:-(\d+))?"
+ip_range_regexp = r"{ip_cap}(?:-{ip_cap})?(?:{port_range})?"
+ipv4_port_regex = re.compile(
+ ip_range_regexp.format(ip_cap=ipv4_capture, port_range=port_range)
+)
+ipv6_port_regex = re.compile(
+ ip_range_regexp.format(ip_cap=ipv6_capture, port_range=port_range)
+)
+
+
+def decode_ip_port_range(value):
+ """
+ Decodes an IP and port range:
+ {ip_start}-{ip-end}:{port_start}-{port_end}
+
+ IPv6 addresses are surrounded by "[" and "]" if port ranges are also
+ present
+
+ Returns the following dictionary:
+ {
+ "addrs": {
+ "start": {ip_start}
+ "end": {ip_end}
+ }
+ "ports": {
+ "start": {port_start},
+ "end": {port_end}
+ }
+ (the "ports" key might be omitted)
+ """
+ if value.count(":") > 1:
+ match = ipv6_port_regex.match(value)
+ else:
+ match = ipv4_port_regex.match(value)
+
+ ip_start = match.group(1)
+ ip_end = match.group(2)
+ port_start = match.group(3)
+ port_end = match.group(4)
+
+ result = {
+ "addrs": {
+ "start": netaddr.IPAddress(ip_start),
+ "end": netaddr.IPAddress(ip_end or ip_start),
+ }
+ }
+ if port_start:
+ result["ports"] = {
+ "start": int(port_start),
+ "end": int(port_end or port_start),
+ }
+
+ return result
+
+
+def decode_nat(value):
+ """Decodes the 'nat' keyword of the ct action.
+
+ The format is:
+ nat
+ Flag format.
+ nat(type=addrs[:ports][,flag]...)
+ Full format where the address-port range has the same format as
+ the one described in decode_ip_port_range.
+
+ Examples:
+ nat(src=0.0.0.0)
+ nat(src=0.0.0.0,persistent)
+ nat(dst=192.168.1.0-192.168.1.253:4000-5000)
+ nat(dst=192.168.1.0-192.168.1.253,hash)
+ nat(dst=[fe80::f150]-[fe80::f15f]:255-300)
+ """
+ if not value:
+ return True # If flag format, the value is True.
+
+ result = dict()
+ type_parts = value.split("=")
+ result["type"] = type_parts[0]
+
+ if len(type_parts) > 1:
+ value_parts = type_parts[1].split(",")
+ if len(type_parts) != 2:
+ raise ValueError("Malformed nat action: %s" % value)
+
+ ip_port_range = decode_ip_port_range(value_parts[0])
+
+ result = {"type": type_parts[0], **ip_port_range}
+
+ for flag in value_parts[1:]:
+ result[flag] = True
+
+ return result
diff --git a/python/ovs/flow/ofp.py b/python/ovs/flow/ofp.py
new file mode 100644
index 000000000..0bc110c57
--- /dev/null
+++ b/python/ovs/flow/ofp.py
@@ -0,0 +1,428 @@
+"""Defines the parsers needed to parse ofproto flows.
+"""
+
+import functools
+
+from ovs.flow.kv import KVParser, KVDecoders, nested_kv_decoder
+from ovs.flow.ofp_fields import field_decoders
+from ovs.flow.flow import Flow, Section
+from ovs.flow.list import ListDecoders, nested_list_decoder
+from ovs.flow.decoders import (
+ decode_default,
+ decode_flag,
+ decode_int,
+ decode_time,
+ decode_mask,
+ IPMask,
+ EthMask,
+ decode_free_output,
+ decode_nat,
+)
+from ovs.flow.ofp_act import (
+ decode_output,
+ decode_field,
+ decode_controller,
+ decode_bundle,
+ decode_bundle_load,
+ decode_encap,
+ decode_load_field,
+ decode_set_field,
+ decode_move_field,
+ decode_dec_ttl,
+ decode_chk_pkt_larger,
+ decode_zone,
+ decode_exec,
+ decode_learn,
+)
+
+
+class OFPFlow(Flow):
+ """OFPFLow represents an OpenFlow Flow.
+
+ Attributes:
+ info: The info section.
+ match: The match section.
+ actions: The actions section.
+ id: The id object given at construction time.
+ """
+
+ """
+ These class variables are used to cache the KVDecoders instances. This
+ will speed up subsequent flow parsings.
+ """
+ _info_decoders = None
+ _match_decoders = None
+ _action_decoders = None
+
+ @staticmethod
+ def info_decoders():
+ """Return the KVDecoders instance to parse the info section.
+
+ Uses the cached version if available.
+ """
+ if not OFPFlow._info_decoders:
+ OFPFlow._info_decoders = OFPFlow._gen_info_decoders()
+ return OFPFlow._info_decoders
+
+ @staticmethod
+ def match_decoders():
+ """Return the KVDecoders instance to parse the match section.
+
+ Uses the cached version if available.
+ """
+ if not OFPFlow._match_decoders:
+ OFPFlow._match_decoders = OFPFlow._gen_match_decoders()
+ return OFPFlow._match_decoders
+
+ @staticmethod
+ def action_decoders():
+ """Return the KVDecoders instance to parse the actions section.
+
+ Uses the cached version if available.
+ """
+ if not OFPFlow._action_decoders:
+ OFPFlow._action_decoders = OFPFlow._gen_action_decoders()
+ return OFPFlow._action_decoders
+
+ def __init__(self, ofp_string, id=None):
+ """Create a OFPFlow from a flow string.
+
+ The string is expected to have the followoing format:
+
+ [flow data] [match] actions=[actions]
+
+ Args:
+ ofp_string(str): An OpenFlow flow string.
+ id(Any): Optional; any object used to uniquely identify this flow
+ from the rest.
+
+ Returns
+ An OFPFlow with the content of the flow string or None if there is
+ no flow information but the string is expected to be found in a
+ flow dump.
+
+ Raises
+ ValueError if the string is malformed.
+ ParseError if an error in parsing occurs.
+ """
+ if " reply " in ofp_string:
+ return None
+
+ sections = list()
+ parts = ofp_string.split("actions=")
+ if len(parts) != 2:
+ raise ValueError("malformed ofproto flow: %s" % ofp_string)
+
+ actions = parts[1]
+
+ field_parts = parts[0].rstrip(" ").rpartition(" ")
+ if len(field_parts) != 3:
+ raise ValueError("malformed ofproto flow: %s" % ofp_string)
+
+ info = field_parts[0]
+ match = field_parts[2]
+
+ iparser = KVParser(info, OFPFlow.info_decoders())
+ iparser.parse()
+ isection = Section(
+ name="info",
+ pos=ofp_string.find(info),
+ string=info,
+ data=iparser.kv(),
+ )
+ sections.append(isection)
+
+ mparser = KVParser(match, OFPFlow.match_decoders())
+ mparser.parse()
+ msection = Section(
+ name="match",
+ pos=ofp_string.find(match),
+ string=match,
+ data=mparser.kv(),
+ )
+ sections.append(msection)
+
+ aparser = KVParser(actions, OFPFlow.action_decoders())
+ aparser.parse()
+ asection = Section(
+ name="actions",
+ pos=ofp_string.find(actions),
+ string=actions,
+ data=aparser.kv(),
+ is_list=True,
+ )
+ sections.append(asection)
+
+ super(OFPFlow, self).__init__(sections, ofp_string, id)
+
+ def __str__(self):
+ if self._orig:
+ return self._orig
+ else:
+ return self.to_string()
+
+ def to_string(self):
+ """Return a text representation of the flow."""
+ string = "Info: {} | ".format(self.info)
+ string += "Match : {} | ".format(self.match)
+ string += "Actions: {}".format(self.actions)
+ return string
+
+ @staticmethod
+ def _gen_info_decoders():
+ """Generate the info KVDecoders."""
+ args = {
+ "table": decode_int,
+ "duration": decode_time,
+ "n_packet": decode_int,
+ "n_bytes": decode_int,
+ "cookie": decode_int,
+ "idle_timeout": decode_time,
+ "hard_timeout": decode_time,
+ "hard_age": decode_time,
+ }
+ return KVDecoders(args)
+
+ @staticmethod
+ def _gen_match_decoders():
+ """Generate the match KVDecoders."""
+ args = {
+ **OFPFlow._field_decoder_args(),
+ **OFPFlow._extra_match_decoder_args(),
+ }
+
+ return KVDecoders(args)
+
+ @staticmethod
+ def _extra_match_decoder_args():
+ """Returns the extra KVDecoder arguments needed to decode the match
+ part of a flow (apart from the fields)."""
+ return {
+ "priority": decode_int,
+ }
+
+ @staticmethod
+ def _field_decoder_args():
+ """Returns the KVDecoder arguments needed to decode match fields."""
+ shorthands = [
+ "eth",
+ "ip",
+ "ipv6",
+ "icmp",
+ "icmp6",
+ "tcp",
+ "tcp6",
+ "udp",
+ "udp6",
+ "sctp",
+ "arp",
+ "rarp",
+ "mpls",
+ "mplsm",
+ ]
+
+ fields = {**field_decoders, **{key: decode_flag for key in shorthands}}
+
+ # vlan_vid field is special. Although it is technically 12 bit wide,
+ # bit 12 is allowed to be set to 1 to indicate that the vlan header is
+ # present (see section VLAN FIELDS in
+ # http://www.openvswitch.org/support/dist-docs/ovs-fields.7.txt)
+ # Therefore, override the generated vlan_vid field size.
+ fields["vlan_vid"] = decode_mask(13)
+ return fields
+
+ @staticmethod
+ def _gen_action_decoders():
+ """Generate the actions decoders."""
+
+ actions = {
+ **OFPFlow._output_actions_decoders_args(),
+ **OFPFlow._encap_actions_decoders_args(),
+ **OFPFlow._field_action_decoders_args(),
+ **OFPFlow._meta_action_decoders_args(),
+ **OFPFlow._fw_action_decoders_args(),
+ **OFPFlow._control_action_decoders_args(),
+ **OFPFlow._other_action_decoders_args(),
+ }
+ clone_actions = OFPFlow._clone_actions_decoders_args(actions)
+ actions.update(clone_actions)
+ return KVDecoders(actions, default_free=decode_free_output)
+
+ @staticmethod
+ def _output_actions_decoders_args():
+ """Returns the decoder arguments for the output actions."""
+ return {
+ "output": decode_output,
+ "drop": decode_flag,
+ "controller": decode_controller,
+ "enqueue": nested_list_decoder(
+ ListDecoders([("port", decode_default), ("queue", int)]),
+ delims=[",", ":"],
+ ),
+ "bundle": decode_bundle,
+ "bundle_load": decode_bundle_load,
+ "group": decode_default,
+ }
+
+ @staticmethod
+ def _encap_actions_decoders_args():
+ """Returns the decoders arguments for the encap actions."""
+
+ return {
+ "pop_vlan": decode_flag,
+ "strip_vlan": decode_flag,
+ "push_vlan": decode_default,
+ "decap": decode_flag,
+ "encap": decode_encap,
+ }
+
+ @staticmethod
+ def _field_action_decoders_args():
+ """Returns the decoders arguments for field-modification actions."""
+ # Field modification actions
+ field_default_decoders = [
+ "set_mpls_label",
+ "set_mpls_tc",
+ "set_mpls_ttl",
+ "mod_nw_tos",
+ "mod_nw_ecn",
+ "mod_tcp_src",
+ "mod_tcp_dst",
+ ]
+ return {
+ "load": decode_load_field,
+ "set_field": functools.partial(
+ decode_set_field, KVDecoders(OFPFlow._field_decoder_args())
+ ),
+ "move": decode_move_field,
+ "mod_dl_dst": EthMask,
+ "mod_dl_src": EthMask,
+ "mod_nw_dst": IPMask,
+ "mod_nw_src": IPMask,
+ "dec_ttl": decode_dec_ttl,
+ "dec_mpls_ttl": decode_flag,
+ "dec_nsh_ttl": decode_flag,
+ "check_pkt_larger": decode_chk_pkt_larger,
+ **{field: decode_default for field in field_default_decoders},
+ }
+
+ @staticmethod
+ def _meta_action_decoders_args():
+ """Returns the decoders arguments for the metadata actions."""
+ meta_default_decoders = ["set_tunnel", "set_tunnel64", "set_queue"]
+ return {
+ "pop_queue": decode_flag,
+ **{field: decode_default for field in meta_default_decoders},
+ }
+
+ @staticmethod
+ def _fw_action_decoders_args():
+ """Returns the decoders arguments for the firewalling actions."""
+ return {
+ "ct": nested_kv_decoder(
+ KVDecoders(
+ {
+ "commit": decode_flag,
+ "zone": decode_zone,
+ "table": decode_int,
+ "nat": decode_nat,
+ "force": decode_flag,
+ "exec": functools.partial(
+ decode_exec,
+ KVDecoders(
+ {
+ **OFPFlow._encap_actions_decoders_args(),
+ **OFPFlow._field_action_decoders_args(),
+ **OFPFlow._meta_action_decoders_args(),
+ }
+ ),
+ ),
+ "alg": decode_default,
+ }
+ )
+ ),
+ "ct_clear": decode_flag,
+ }
+
+ @staticmethod
+ def _control_action_decoders_args():
+ return {
+ "resubmit": nested_list_decoder(
+ ListDecoders(
+ [
+ ("port", decode_default),
+ ("table", decode_int),
+ ("ct", decode_flag),
+ ]
+ )
+ ),
+ "push": decode_field,
+ "pop": decode_field,
+ "exit": decode_flag,
+ "multipath": nested_list_decoder(
+ ListDecoders(
+ [
+ ("fields", decode_default),
+ ("basis", decode_int),
+ ("algorithm", decode_default),
+ ("n_links", decode_int),
+ ("arg", decode_int),
+ ("dst", decode_field),
+ ]
+ )
+ ),
+ }
+
+ @staticmethod
+ def _clone_actions_decoders_args(action_decoders):
+ """Generate the decoder arguments for the clone actions.
+
+ Args:
+ action_decoders (dict): The decoders of the supported nested
+ actions.
+ """
+ return {
+ "learn": decode_learn(
+ {
+ **action_decoders,
+ "fin_timeout": nested_kv_decoder(
+ KVDecoders(
+ {
+ "idle_timeout": decode_time,
+ "hard_timeout": decode_time,
+ }
+ )
+ ),
+ }
+ ),
+ "clone": functools.partial(
+ decode_exec, KVDecoders(action_decoders)
+ ),
+ }
+
+ @staticmethod
+ def _other_action_decoders_args():
+ """Generate the decoder arguments for other actions
+ (see man(7) ovs-actions)."""
+ return {
+ "conjunction": nested_list_decoder(
+ ListDecoders(
+ [("id", decode_int), ("k", decode_int), ("n", decode_int)]
+ ),
+ delims=[",", "/"],
+ ),
+ "note": decode_default,
+ "sample": nested_kv_decoder(
+ KVDecoders(
+ {
+ "probability": decode_int,
+ "collector_set_id": decode_int,
+ "obs_domain_id": decode_int,
+ "obs_point_id": decode_int,
+ "sampling_port": decode_default,
+ "ingress": decode_flag,
+ "egress": decode_flag,
+ }
+ )
+ ),
+ }
diff --git a/python/ovs/flow/ofp_act.py b/python/ovs/flow/ofp_act.py
new file mode 100644
index 000000000..acb16cd9a
--- /dev/null
+++ b/python/ovs/flow/ofp_act.py
@@ -0,0 +1,306 @@
+"""Defines decoders for OpenFlow actions.
+"""
+
+import functools
+
+from ovs.flow.decoders import (
+ decode_default,
+ decode_time,
+ decode_flag,
+ decode_int,
+)
+from ovs.flow.kv import nested_kv_decoder, KVDecoders, KeyValue, KVParser
+from ovs.flow.list import nested_list_decoder, ListDecoders
+from ovs.flow.ofp_fields import field_decoders
+
+
+def decode_output(value):
+ """Decodes the output value.
+
+ Does not support field specification.
+ """
+ if len(value.split(",")) > 1:
+ return nested_kv_decoder()(value)
+ try:
+ return {"port": int(value)}
+ except ValueError:
+ return {"port": value.strip('"')}
+
+
+def decode_controller(value):
+ """Decodes the controller action."""
+ if not value:
+ return KeyValue("output", "controller")
+ else:
+ # Try controller:max_len
+ try:
+ max_len = int(value)
+ return {
+ "max_len": max_len,
+ }
+ except ValueError:
+ pass
+ # controller(key[=val], ...)
+ return nested_kv_decoder()(value)
+
+
+def decode_bundle_load(value):
+ return decode_bundle(value, True)
+
+
+def decode_bundle(value, load=False):
+ """Decode bundle action."""
+ result = {}
+ keys = ["fields", "basis", "algorithm", "ofport"]
+ if load:
+ keys.append("dst")
+
+ for key in keys:
+ parts = value.partition(",")
+ nvalue = parts[0]
+ value = parts[2]
+ if key == "ofport":
+ continue
+ result[key] = decode_default(nvalue)
+
+ # Handle members:
+ mvalues = value.split("members:")
+ result["members"] = [int(port) for port in mvalues[1].split(",")]
+ return result
+
+
+def decode_encap(value):
+ """Decodes encap action. Examples:
+ encap(ethernet)
+ encap(nsh(md_type=2,tlv(0x1000,10,0x12345678)))
+
+ The generated dict has the following keys: "header", "props", e.g:
+ {
+ "header": "ethernet",
+ }
+ {
+ "header": "nsh",
+ "props": {
+ "md_type": 2,
+ "tlv": {
+ "class": 0x100,
+ "type": 10,
+ "value": 0x123456
+ }
+ }
+ }
+ """
+
+ def free_hdr_decoder(free_val):
+ if free_val not in ["ethernet", "mpls", "mpls_mc", "nsh"]:
+ raise ValueError(
+ "Malformed encap action. Unkown header: {}".format(free_val)
+ )
+ return "header", free_val
+
+ parser = KVParser(
+ value,
+ KVDecoders(
+ {
+ "nsh": nested_kv_decoder(
+ KVDecoders(
+ {
+ "md_type": decode_default,
+ "tlv": nested_list_decoder(
+ ListDecoders(
+ [
+ ("class", decode_int),
+ ("type", decode_int),
+ ("value", decode_int),
+ ]
+ )
+ ),
+ }
+ )
+ ),
+ },
+ default_free=free_hdr_decoder,
+ ),
+ )
+ parser.parse()
+ if len(parser.kv()) > 1:
+ raise ValueError("Malformed encap action: {}".format(value))
+
+ result = {}
+ if parser.kv()[0].key == "header":
+ result["header"] = parser.kv()[0].value
+ elif parser.kv()[0].key == "nsh":
+ result["header"] = "nsh"
+ result["props"] = parser.kv()[0].value
+
+ return result
+
+
+def decode_field(value):
+ """Decodes a field as defined in the 'Field Specification' of the actions
+ man page:
+ http://www.openvswitch.org/support/dist-docs/ovs-actions.7.txt."""
+ parts = value.strip("]\n\r").split("[")
+ result = {
+ "field": parts[0],
+ }
+
+ if len(parts) > 1 and parts[1]:
+ field_range = parts[1].split("..")
+ start = field_range[0]
+ end = field_range[1] if len(field_range) > 1 else start
+ if start:
+ result["start"] = int(start)
+ if end:
+ result["end"] = int(end)
+
+ return result
+
+
+def decode_load_field(value):
+ """Decodes LOAD actions such as: 'load:value->dst'."""
+ parts = value.split("->")
+ if len(parts) != 2:
+ raise ValueError("Malformed load action : %s" % value)
+
+ # If the load action is performed within a learn() action,
+ # The value can be specified as another field.
+ try:
+ return {"value": int(parts[0], 0), "dst": decode_field(parts[1])}
+ except ValueError:
+ return {"src": decode_field(parts[0]), "dst": decode_field(parts[1])}
+
+
+def decode_set_field(field_decoders, value):
+ """Decodes SET_FIELD actions such as: 'set_field:value/mask->dst'.
+
+ The value is decoded by field_decoders which is a KVDecoders instance.
+ Args:
+ field_decoders(KVDecoders): The KVDecoders to be used to decode the
+ field.
+ """
+ parts = value.split("->")
+ if len(parts) != 2:
+ raise ValueError("Malformed set_field action : %s" % value)
+
+ val = parts[0]
+ dst = parts[1]
+
+ val_result = field_decoders.decode(dst, val)
+
+ return {
+ "value": {val_result[0]: val_result[1]},
+ "dst": decode_field(dst),
+ }
+
+
+def decode_move_field(value):
+ """Decodes MOVE actions such as 'move:src->dst'."""
+ parts = value.split("->")
+ if len(parts) != 2:
+ raise ValueError("Malformed move action : %s" % value)
+
+ return {
+ "src": decode_field(parts[0]),
+ "dst": decode_field(parts[1]),
+ }
+
+
+def decode_dec_ttl(value):
+ """Decodes dec_ttl and dec_ttl(id, id[2], ...) actions."""
+ if not value:
+ return True
+ return [int(idx) for idx in value.split(",")]
+
+
+def decode_chk_pkt_larger(value):
+ """Decodes 'check_pkt_larger(pkt_len)->dst' actions."""
+ parts = value.split("->")
+ if len(parts) != 2:
+ raise ValueError("Malformed check_pkt_larger action : %s" % value)
+
+ pkt_len = int(parts[0].strip("()"))
+ dst = decode_field(parts[1])
+ return {"pkt_len": pkt_len, "dst": dst}
+
+
+# CT decoders
+def decode_zone(value):
+ """Decodes the value of the 'zone' keyword (part of the ct action)."""
+ try:
+ return int(value, 0)
+ except ValueError:
+ pass
+ return decode_field(value)
+
+
+def decode_exec(action_decoders, value):
+ """Decodes the value of the 'exec' keyword (part of the ct action).
+
+ Args:
+ decode_actions (KVDecoders): The decoders to be used to decode the
+ nested exec.
+ value (string): The string to be decoded.
+ """
+ exec_parser = KVParser(value, action_decoders)
+ exec_parser.parse()
+ return [{kv.key: kv.value} for kv in exec_parser.kv()]
+
+
+def decode_learn(action_decoders):
+ """Create the decoder to be used to decode the 'learn' action.
+
+ The learn action has two added complexities:
+ 1) It can hold any valid action key-value. Therefore we must take
+ the precalculated action_decoders and use them. That's why we require
+ them as argument.
+
+ 2) The way fields can be specified is augmented. Not only we have
+ 'field=value', but we also have:
+ - 'field=_src_' (where _src_ is another field name)
+ - and just 'field'
+ For this we need to create a wrapper of field_decoders that, for each
+ "field=X" key-value we check if X is a field_name or if it's actually
+ a value that we need to send to the appropriate field_decoder to
+ process.
+
+ Args:
+ action_decoders (dict): Dictionary of decoders to be used in nested
+ action decoding.
+ """
+
+ def decode_learn_field(decoder, value):
+ """Generates a decoder to be used for the 'field' argument of the
+ 'learn' action.
+
+ The field can hold a value that should be decoded, either as a field,
+ or as a the value (see man(7) ovs-actions).
+
+ Args:
+ decoder (callable): The decoder.
+ """
+ if value in field_decoders.keys():
+ # It's a field
+ return value
+ else:
+ return decoder(value)
+
+ learn_field_decoders = {
+ field: functools.partial(decode_learn_field, decoder)
+ for field, decoder in field_decoders.items()
+ }
+ learn_decoders = {
+ **action_decoders,
+ **learn_field_decoders,
+ "idle_timeout": decode_time,
+ "hard_timeout": decode_time,
+ "priority": decode_int,
+ "cookie": decode_int,
+ "send_flow_rem": decode_flag,
+ "table": decode_int,
+ "delete_learned": decode_flag,
+ "limit": decode_int,
+ "result_dst": decode_field,
+ }
+
+ return functools.partial(decode_exec, KVDecoders(learn_decoders))