get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/patches/1558026/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 1558026,
    "url": "http://patchwork.ozlabs.org/api/patches/1558026/?format=api",
    "web_url": "http://patchwork.ozlabs.org/project/openvswitch/patch/20211122112256.2011194-8-amorenoz@redhat.com/",
    "project": {
        "id": 47,
        "url": "http://patchwork.ozlabs.org/api/projects/47/?format=api",
        "name": "Open vSwitch",
        "link_name": "openvswitch",
        "list_id": "ovs-dev.openvswitch.org",
        "list_email": "ovs-dev@openvswitch.org",
        "web_url": "http://openvswitch.org/",
        "scm_url": "git@github.com:openvswitch/ovs.git",
        "webscm_url": "https://github.com/openvswitch/ovs",
        "list_archive_url": "",
        "list_archive_url_format": "",
        "commit_url_format": ""
    },
    "msgid": "<20211122112256.2011194-8-amorenoz@redhat.com>",
    "list_archive_url": null,
    "date": "2021-11-22T11:22:45",
    "name": "[ovs-dev,v1,07/18] python: introduce OpenFlow Flow parsing",
    "commit_ref": null,
    "pull_url": null,
    "state": "changes-requested",
    "archived": false,
    "hash": "f20510f899e7f9c49f8ab3a6d5b0cc297c2f7ee1",
    "submitter": {
        "id": 77477,
        "url": "http://patchwork.ozlabs.org/api/people/77477/?format=api",
        "name": "Adrian Moreno",
        "email": "amorenoz@redhat.com"
    },
    "delegate": null,
    "mbox": "http://patchwork.ozlabs.org/project/openvswitch/patch/20211122112256.2011194-8-amorenoz@redhat.com/mbox/",
    "series": [
        {
            "id": 273222,
            "url": "http://patchwork.ozlabs.org/api/series/273222/?format=api",
            "web_url": "http://patchwork.ozlabs.org/project/openvswitch/list/?series=273222",
            "date": "2021-11-22T11:22:39",
            "name": "python: add flow parsing library",
            "version": 1,
            "mbox": "http://patchwork.ozlabs.org/series/273222/mbox/"
        }
    ],
    "comments": "http://patchwork.ozlabs.org/api/patches/1558026/comments/",
    "check": "fail",
    "checks": "http://patchwork.ozlabs.org/api/patches/1558026/checks/",
    "tags": {},
    "related": [],
    "headers": {
        "Return-Path": "<ovs-dev-bounces@openvswitch.org>",
        "X-Original-To": [
            "incoming@patchwork.ozlabs.org",
            "dev@openvswitch.org"
        ],
        "Delivered-To": [
            "patchwork-incoming@bilbo.ozlabs.org",
            "ovs-dev@lists.linuxfoundation.org"
        ],
        "Authentication-Results": [
            "bilbo.ozlabs.org;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=Ki/fWvbW;\n\tdkim-atps=neutral",
            "ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org\n (client-ip=140.211.166.137; helo=smtp4.osuosl.org;\n envelope-from=ovs-dev-bounces@openvswitch.org; receiver=<UNKNOWN>)",
            "smtp2.osuosl.org (amavisd-new);\n dkim=pass (1024-bit key) header.d=redhat.com",
            "relay.mimecast.com;\n auth=pass smtp.auth=CUSA124A263 smtp.mailfrom=amorenoz@redhat.com"
        ],
        "Received": [
            "from smtp4.osuosl.org (smtp4.osuosl.org [140.211.166.137])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest\n SHA256)\n\t(No client certificate requested)\n\tby bilbo.ozlabs.org (Postfix) with ESMTPS id 4HyQ0C2bmcz9sRR\n\tfor <incoming@patchwork.ozlabs.org>; Mon, 22 Nov 2021 22:24:27 +1100 (AEDT)",
            "from localhost (localhost [127.0.0.1])\n\tby smtp4.osuosl.org (Postfix) with ESMTP id 0F44341C67;\n\tMon, 22 Nov 2021 11:24:25 +0000 (UTC)",
            "from smtp4.osuosl.org ([127.0.0.1])\n\tby localhost (smtp4.osuosl.org [127.0.0.1]) (amavisd-new, port 10024)\n\twith ESMTP id IWGGgtfjGlSg; Mon, 22 Nov 2021 11:24:19 +0000 (UTC)",
            "from lists.linuxfoundation.org (lf-lists.osuosl.org\n [IPv6:2605:bc80:3010:104::8cd3:938])\n\tby smtp4.osuosl.org (Postfix) with ESMTPS id A985641C44;\n\tMon, 22 Nov 2021 11:24:17 +0000 (UTC)",
            "from lf-lists.osuosl.org (localhost [127.0.0.1])\n\tby lists.linuxfoundation.org (Postfix) with ESMTP id 8259AC003C;\n\tMon, 22 Nov 2021 11:24:15 +0000 (UTC)",
            "from smtp2.osuosl.org (smtp2.osuosl.org [140.211.166.133])\n by lists.linuxfoundation.org (Postfix) with ESMTP id 40E6AC0037\n for <dev@openvswitch.org>; Mon, 22 Nov 2021 11:24:14 +0000 (UTC)",
            "from localhost (localhost [127.0.0.1])\n by smtp2.osuosl.org (Postfix) with ESMTP id 202AB4041B\n for <dev@openvswitch.org>; Mon, 22 Nov 2021 11:23:39 +0000 (UTC)",
            "from smtp2.osuosl.org ([127.0.0.1])\n by localhost (smtp2.osuosl.org [127.0.0.1]) (amavisd-new, port 10024)\n with ESMTP id OnQc-x03_nTY for <dev@openvswitch.org>;\n Mon, 22 Nov 2021 11:23:35 +0000 (UTC)",
            "from us-smtp-delivery-124.mimecast.com\n (us-smtp-delivery-124.mimecast.com [170.10.133.124])\n by smtp2.osuosl.org (Postfix) with ESMTPS id C9F87403AB\n for <dev@openvswitch.org>; Mon, 22 Nov 2021 11:23:34 +0000 (UTC)",
            "from mimecast-mx01.redhat.com (mimecast-mx01.redhat.com\n [209.132.183.4]) by relay.mimecast.com with ESMTP with STARTTLS\n (version=TLSv1.2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id\n us-mta-140-kkXvIrzMOgq16Eu0OswAoQ-1; Mon, 22 Nov 2021 06:23:30 -0500",
            "from smtp.corp.redhat.com (int-mx03.intmail.prod.int.phx2.redhat.com\n [10.5.11.13])\n (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits))\n (No client certificate requested)\n by mimecast-mx01.redhat.com (Postfix) with ESMTPS id DB1C91966320\n for <dev@openvswitch.org>; Mon, 22 Nov 2021 11:23:29 +0000 (UTC)",
            "from amorenoz.users.ipa.redhat.com (unknown [10.2.16.196])\n by smtp.corp.redhat.com (Postfix) with ESMTP id C1CB760862;\n Mon, 22 Nov 2021 11:23:28 +0000 (UTC)"
        ],
        "X-Virus-Scanned": [
            "amavisd-new at osuosl.org",
            "amavisd-new at osuosl.org"
        ],
        "X-Greylist": "domain auto-whitelisted by SQLgrey-1.8.0",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n s=mimecast20190719; t=1637580213;\n h=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n to:to:cc:cc:mime-version:mime-version:content-type:content-type:\n content-transfer-encoding:content-transfer-encoding:\n in-reply-to:in-reply-to:references:references;\n bh=5NFJ54aaJPVAm6bi/zGHyu+wgeN5VJNkW76Q/ud1v0Q=;\n b=Ki/fWvbWt20ZYzObTkW41sWAvfMvFJdlRiVuPMvQ4JOOv9FDtVCtQ2B3fUIFJHHvqWY5p8\n Un9UMZ4X3Am925UWHA7mvuuidvZPzz2nuSERgZTc5BJJYFnpOztK/XZ4P5imnxX+fogmVv\n vE7FwptmaDjn+IupGAsseh+57LnW4GU=",
        "X-MC-Unique": "kkXvIrzMOgq16Eu0OswAoQ-1",
        "From": "Adrian Moreno <amorenoz@redhat.com>",
        "To": "dev@openvswitch.org",
        "Date": "Mon, 22 Nov 2021 12:22:45 +0100",
        "Message-Id": "<20211122112256.2011194-8-amorenoz@redhat.com>",
        "In-Reply-To": "<20211122112256.2011194-1-amorenoz@redhat.com>",
        "References": "<20211122112256.2011194-1-amorenoz@redhat.com>",
        "MIME-Version": "1.0",
        "X-Scanned-By": "MIMEDefang 2.79 on 10.5.11.13",
        "X-Mimecast-Spam-Score": "0",
        "X-Mimecast-Originator": "redhat.com",
        "Subject": "[ovs-dev] [PATCH v1 07/18] python: introduce OpenFlow Flow parsing",
        "X-BeenThere": "ovs-dev@openvswitch.org",
        "X-Mailman-Version": "2.1.15",
        "Precedence": "list",
        "List-Id": "<ovs-dev.openvswitch.org>",
        "List-Unsubscribe": "<https://mail.openvswitch.org/mailman/options/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=unsubscribe>",
        "List-Archive": "<http://mail.openvswitch.org/pipermail/ovs-dev/>",
        "List-Post": "<mailto:ovs-dev@openvswitch.org>",
        "List-Help": "<mailto:ovs-dev-request@openvswitch.org?subject=help>",
        "List-Subscribe": "<https://mail.openvswitch.org/mailman/listinfo/ovs-dev>,\n <mailto:ovs-dev-request@openvswitch.org?subject=subscribe>",
        "Content-Type": "text/plain; charset=\"us-ascii\"",
        "Content-Transfer-Encoding": "7bit",
        "Errors-To": "ovs-dev-bounces@openvswitch.org",
        "Sender": "\"dev\" <ovs-dev-bounces@openvswitch.org>"
    },
    "content": "Introduce OFPFlow class and all its decoders.\n\nMost of the decoders are generic (from decoders.py). Some have special\nsyntax and need a specific implementation.\n\nDecoders for nat are moved to the common decoders.py because it's syntax\nis shared with other types of flows (e.g: dpif flows).\n\nSigned-off-by: Adrian Moreno <amorenoz@redhat.com>\n---\n python/automake.mk           |   4 +-\n python/ovs/flows/decoders.py |  93 ++++++++\n python/ovs/flows/ofp.py      | 400 +++++++++++++++++++++++++++++++++++\n python/ovs/flows/ofp_act.py  | 233 ++++++++++++++++++++\n 4 files changed, 729 insertions(+), 1 deletion(-)\n create mode 100644 python/ovs/flows/ofp.py\n create mode 100644 python/ovs/flows/ofp_act.py",
    "diff": "diff --git a/python/automake.mk b/python/automake.mk\nindex 136da26bd..d1464d7f6 100644\n--- a/python/automake.mk\n+++ b/python/automake.mk\n@@ -46,7 +46,9 @@ ovs_pyfiles = \\\n \tpython/ovs/flows/decoders.py \\\n \tpython/ovs/flows/kv.py \\\n \tpython/ovs/flows/list.py \\\n-\tpython/ovs/flows/flow.py\n+\tpython/ovs/flows/flow.py \\\n+\tpython/ovs/flows/ofp.py \\\n+\tpython/ovs/flows/ofp_act.py\n \n # These python files are used at build time but not runtime,\n # so they are not installed.\ndiff --git a/python/ovs/flows/decoders.py b/python/ovs/flows/decoders.py\nindex bf7a94ae8..3def9f279 100644\n--- a/python/ovs/flows/decoders.py\n+++ b/python/ovs/flows/decoders.py\n@@ -6,6 +6,7 @@ object.\n \"\"\"\n \n import netaddr\n+import re\n \n \n class Decoder:\n@@ -358,3 +359,95 @@ class IPMask(Decoder):\n \n     def to_json(self):\n         return str(self)\n+\n+\n+def decode_free_output(value):\n+    \"\"\"Decodes the output value when found free\n+    (without the 'output' keyword)\"\"\"\n+    try:\n+        return \"output\", {\"port\": int(value)}\n+    except ValueError:\n+        return \"output\", {\"port\": value.strip('\"')}\n+\n+\n+ipv4 = r\"[\\d\\.]+\"\n+ipv4_capture = r\"({ipv4})\".format(ipv4=ipv4)\n+ipv6 = r\"[\\w:]+\"\n+ipv6_capture = r\"(?:\\[*)?({ipv6})(?:\\]*)?\".format(ipv6=ipv6)\n+port_range = r\":(\\d+)(?:-(\\d+))?\"\n+ip_range_regexp = r\"{ip_cap}(?:-{ip_cap})?(?:{port_range})?\"\n+ipv4_port_regex = re.compile(\n+    ip_range_regexp.format(ip_cap=ipv4_capture, port_range=port_range)\n+)\n+ipv6_port_regex = re.compile(\n+    ip_range_regexp.format(ip_cap=ipv6_capture, port_range=port_range)\n+)\n+\n+\n+def decode_ip_port_range(value):\n+    \"\"\"\n+    Decodes an IP and port range:\n+        {ip_start}-{ip-end}:{port_start}-{port_end}\n+\n+    IPv6 addresses are surrounded by \"[\" and \"]\" if port ranges are also\n+    present\n+\n+    Returns the following dictionary:\n+        {\n+            \"addrs\": {\n+                \"start\": {ip_start}\n+                \"end\": {ip_end}\n+            }\n+            \"ports\": {\n+                \"start\": {port_start},\n+                \"end\": {port_end}\n+        }\n+        (the \"ports\" key might be omitted)\n+    \"\"\"\n+    if value.count(\":\") > 1:\n+        match = ipv6_port_regex.match(value)\n+    else:\n+        match = ipv4_port_regex.match(value)\n+\n+    ip_start = match.group(1)\n+    ip_end = match.group(2)\n+    port_start = match.group(3)\n+    port_end = match.group(4)\n+\n+    result = {\n+        \"addrs\": {\n+            \"start\": netaddr.IPAddress(ip_start),\n+            \"end\": netaddr.IPAddress(ip_end or ip_start),\n+        }\n+    }\n+    if port_start:\n+        result[\"ports\"] = {\n+            \"start\": int(port_start),\n+            \"end\": int(port_end or port_start),\n+        }\n+\n+    return result\n+\n+\n+def decode_nat(value):\n+    \"\"\"Decodes the 'nat' keyword of the ct action\"\"\"\n+    if not value:\n+        return True\n+\n+    result = dict()\n+    type_parts = value.split(\"=\")\n+    result[\"type\"] = type_parts[0]\n+\n+    if len(type_parts) > 1:\n+        value_parts = type_parts[1].split(\",\")\n+        if len(type_parts) != 2:\n+            raise ValueError(\"Malformed nat action: %s\" % value)\n+\n+        ip_port_range = decode_ip_port_range(value_parts[0])\n+\n+        result = {\"type\": type_parts[0], **ip_port_range}\n+\n+        for flag in value_parts[1:]:\n+            result[flag] = True\n+\n+    return result\ndiff --git a/python/ovs/flows/ofp.py b/python/ovs/flows/ofp.py\nnew file mode 100644\nindex 000000000..e56b08967\n--- /dev/null\n+++ b/python/ovs/flows/ofp.py\n@@ -0,0 +1,400 @@\n+\"\"\" Defines the parsers needed to parse ofproto flows\n+\"\"\"\n+\n+import functools\n+\n+from ovs.flows.kv import KVParser, KVDecoders, nested_kv_decoder\n+from ovs.flows.ofp_fields import field_decoders\n+from ovs.flows.flow import Flow, Section\n+from ovs.flows.list import ListDecoders, nested_list_decoder\n+from ovs.flows.decoders import (\n+    decode_default,\n+    decode_flag,\n+    decode_int,\n+    decode_time,\n+    decode_mask,\n+    IPMask,\n+    EthMask,\n+    decode_free_output,\n+    decode_nat,\n+)\n+from ovs.flows.ofp_act import (\n+    decode_output,\n+    decode_field,\n+    decode_controller,\n+    decode_bundle,\n+    decode_bundle_load,\n+    decode_encap_ethernet,\n+    decode_load_field,\n+    decode_set_field,\n+    decode_move_field,\n+    decode_dec_ttl,\n+    decode_chk_pkt_larger,\n+    decode_zone,\n+    decode_exec,\n+    decode_learn,\n+)\n+\n+\n+class OFPFlow(Flow):\n+    \"\"\"OFPFLow represents an OpenFlow Flow\"\"\"\n+\n+    def __init__(self, sections, orig=\"\", id=None):\n+        \"\"\"Constructor\"\"\"\n+        super(OFPFlow, self).__init__(sections, orig, id)\n+\n+    def __str__(self):\n+        if self._orig:\n+            return self._orig\n+        else:\n+            return self.to_string()\n+\n+    def to_string(self):\n+        \"\"\"Print a text representation of the flow\"\"\"\n+        string = \"Info: {}\\n\" + self.info\n+        string += \"Match : {}\\n\" + self.match\n+        string += \"Actions: {}\\n \" + self.actions\n+        return string\n+\n+\n+class OFPFlowFactory:\n+    \"\"\"OpenFlow Flow Factory is a class capable of creating OFPFLow objects\"\"\"\n+\n+    def __init__(self):\n+        self.info_decoders = self._info_decoders()\n+        self.match_decoders = KVDecoders(\n+            {**self._field_decoders(), **self._flow_match_decoders()}\n+        )\n+        self.act_decoders = self._act_decoders()\n+\n+    def from_string(self, ofp_string, id=None):\n+        \"\"\"Parse a ofproto flow string\n+\n+        The string is expected to have the follwoing format:\n+            [flow data] [match] actions=[actions]\n+\n+        :param ofp_string: a ofproto string as dumped by ovs-ofctl tool\n+        :type ofp_string: str\n+\n+        :return: an OFPFlow with the content of the flow string\n+        :rtype: OFPFlow\n+        \"\"\"\n+        if \" reply \" in ofp_string:\n+            return None\n+\n+        sections = list()\n+        parts = ofp_string.split(\"actions=\")\n+        if len(parts) != 2:\n+            raise ValueError(\"malformed ofproto flow: %s\" % ofp_string)\n+\n+        actions = parts[1]\n+\n+        field_parts = parts[0].rstrip(\" \").rpartition(\" \")\n+        if len(field_parts) != 3:\n+            raise ValueError(\"malformed ofproto flow: %s\" % ofp_string)\n+\n+        info = field_parts[0]\n+        match = field_parts[2]\n+\n+        iparser = KVParser(self.info_decoders)\n+        iparser.parse(info)\n+        isection = Section(\n+            name=\"info\",\n+            pos=ofp_string.find(info),\n+            string=info,\n+            data=iparser.kv(),\n+        )\n+        sections.append(isection)\n+\n+        mparser = KVParser(self.match_decoders)\n+        mparser.parse(match)\n+        msection = Section(\n+            name=\"match\",\n+            pos=ofp_string.find(match),\n+            string=match,\n+            data=mparser.kv(),\n+        )\n+        sections.append(msection)\n+\n+        aparser = KVParser(self.act_decoders)\n+        aparser.parse(actions)\n+        asection = Section(\n+            name=\"actions\",\n+            pos=ofp_string.find(actions),\n+            string=actions,\n+            data=aparser.kv(),\n+            is_list=True,\n+        )\n+        sections.append(asection)\n+\n+        return OFPFlow(sections, ofp_string, id)\n+\n+    @classmethod\n+    def _info_decoders(cls):\n+        \"\"\"Generate the match decoders\"\"\"\n+        info = {\n+            \"table\": decode_int,\n+            \"duration\": decode_time,\n+            \"n_packet\": decode_int,\n+            \"n_bytes\": decode_int,\n+            \"cookie\": decode_int,\n+            \"idle_timeout\": decode_time,\n+            \"hard_timeout\": decode_time,\n+            \"hard_age\": decode_time,\n+        }\n+        return KVDecoders(info)\n+\n+    @classmethod\n+    def _flow_match_decoders(cls):\n+        \"\"\"Returns the decoders for key-values that are part of the flow match\n+        but not a flow field\"\"\"\n+        return {\n+            \"priority\": decode_int,\n+        }\n+\n+    @classmethod\n+    def _field_decoders(cls):\n+        shorthands = [\n+            \"eth\",\n+            \"ip\",\n+            \"ipv6\",\n+            \"icmp\",\n+            \"icmp6\",\n+            \"tcp\",\n+            \"tcp6\",\n+            \"udp\",\n+            \"udp6\",\n+            \"sctp\",\n+            \"arp\",\n+            \"rarp\",\n+            \"mpls\",\n+            \"mplsm\",\n+        ]\n+\n+        fields = {**field_decoders, **{key: decode_flag for key in shorthands}}\n+\n+        # vlan_vid field is special. Although it is technically 12 bit wide,\n+        # bit 12 is allowed to be set to 1 to indicate that the vlan header is\n+        # present (see section VLAN FIELDS in\n+        # http://www.openvswitch.org/support/dist-docs/ovs-fields.7.txt)\n+        # Therefore, override the generated vlan_vid field size\n+        fields[\"vlan_vid\"] = decode_mask(13)\n+        return fields\n+\n+    @classmethod\n+    def _output_actions_decoders(cls):\n+        \"\"\"Returns the decoders for the output actions\"\"\"\n+        return {\n+            \"output\": decode_output,\n+            \"drop\": decode_flag,\n+            \"controller\": decode_controller,\n+            \"enqueue\": nested_list_decoder(\n+                ListDecoders([(\"port\", decode_default), (\"queue\", int)]),\n+                delims=[\",\", \":\"],\n+            ),\n+            \"bundle\": decode_bundle,\n+            \"bundle_load\": decode_bundle_load,\n+            \"group\": decode_default,\n+        }\n+\n+    @classmethod\n+    def _encap_actions_decoders(cls):\n+        \"\"\"Returns the decoders for the encap actions\"\"\"\n+\n+        return {\n+            \"pop_vlan\": decode_flag,\n+            \"strip_vlan\": decode_flag,\n+            \"push_vlan\": decode_default,\n+            \"decap\": decode_flag,\n+            \"encap\": nested_kv_decoder(\n+                KVDecoders(\n+                    {\n+                        \"nsh\": nested_kv_decoder(\n+                            KVDecoders(\n+                                {\n+                                    \"md_type\": decode_default,\n+                                    \"tlv\": nested_list_decoder(\n+                                        ListDecoders(\n+                                            [\n+                                                (\"class\", decode_int),\n+                                                (\"type\", decode_int),\n+                                                (\"value\", decode_int),\n+                                            ]\n+                                        )\n+                                    ),\n+                                }\n+                            )\n+                        ),\n+                    },\n+                    default=None,\n+                    default_free=decode_encap_ethernet,\n+                )\n+            ),\n+        }\n+\n+    @classmethod\n+    def _field_action_decoders(cls):\n+        \"\"\"Returns the decoders for the field modification actions\"\"\"\n+        # Field modification actions\n+        field_default_decoders = [\n+            \"set_mpls_label\",\n+            \"set_mpls_tc\",\n+            \"set_mpls_ttl\",\n+            \"mod_nw_tos\",\n+            \"mod_nw_ecn\",\n+            \"mod_tcp_src\",\n+            \"mod_tcp_dst\",\n+        ]\n+        return {\n+            \"load\": decode_load_field,\n+            \"set_field\": functools.partial(\n+                decode_set_field, KVDecoders(cls._field_decoders())\n+            ),\n+            \"move\": decode_move_field,\n+            \"mod_dl_dst\": EthMask,\n+            \"mod_dl_src\": EthMask,\n+            \"mod_nw_dst\": IPMask,\n+            \"mod_nw_src\": IPMask,\n+            \"dec_ttl\": decode_dec_ttl,\n+            \"dec_mpls_ttl\": decode_flag,\n+            \"dec_nsh_ttl\": decode_flag,\n+            \"check_pkt_larger\": decode_chk_pkt_larger,\n+            **{field: decode_default for field in field_default_decoders},\n+        }\n+\n+    @classmethod\n+    def _meta_action_decoders(cls):\n+        \"\"\"Returns the decoders for the metadata actions\"\"\"\n+        meta_default_decoders = [\"set_tunnel\", \"set_tunnel64\", \"set_queue\"]\n+        return {\n+            \"pop_queue\": decode_flag,\n+            **{field: decode_default for field in meta_default_decoders},\n+        }\n+\n+    @classmethod\n+    def _fw_action_decoders(cls):\n+        \"\"\"Returns the decoders for the Firewalling actions\"\"\"\n+        return {\n+            \"ct\": nested_kv_decoder(\n+                KVDecoders(\n+                    {\n+                        \"commit\": decode_flag,\n+                        \"zone\": decode_zone,\n+                        \"table\": decode_int,\n+                        \"nat\": decode_nat,\n+                        \"force\": decode_flag,\n+                        \"exec\": functools.partial(\n+                            decode_exec,\n+                            KVDecoders(\n+                                {\n+                                    **cls._encap_actions_decoders(),\n+                                    **cls._field_action_decoders(),\n+                                    **cls._meta_action_decoders(),\n+                                }\n+                            ),\n+                        ),\n+                        \"alg\": decode_default,\n+                    }\n+                )\n+            ),\n+            \"ct_clear\": decode_flag,\n+        }\n+\n+    @classmethod\n+    def _control_action_decoders(cls):\n+        return {\n+            \"resubmit\": nested_list_decoder(\n+                ListDecoders(\n+                    [\n+                        (\"port\", decode_default),\n+                        (\"table\", decode_int),\n+                        (\"ct\", decode_flag),\n+                    ]\n+                )\n+            ),\n+            \"push\": decode_field,\n+            \"pop\": decode_field,\n+            \"exit\": decode_flag,\n+            \"multipath\": nested_list_decoder(\n+                ListDecoders(\n+                    [\n+                        (\"fields\", decode_default),\n+                        (\"basis\", decode_int),\n+                        (\"algorithm\", decode_default),\n+                        (\"n_links\", decode_int),\n+                        (\"arg\", decode_int),\n+                        (\"dst\", decode_field),\n+                    ]\n+                )\n+            ),\n+        }\n+\n+    @classmethod\n+    def _clone_actions_decoders(cls, action_decoders):\n+        \"\"\"Generate the decoders for clone actions\n+\n+        Args:\n+            action_decoders (dict): The decoders of the supported nested\n+                actions\n+        \"\"\"\n+        return {\n+            \"learn\": decode_learn(\n+                {\n+                    **action_decoders,\n+                    \"fin_timeout\": nested_kv_decoder(\n+                        KVDecoders(\n+                            {\n+                                \"idle_timeout\": decode_time,\n+                                \"hard_timeout\": decode_time,\n+                            }\n+                        )\n+                    ),\n+                }\n+            ),\n+            \"clone\": functools.partial(\n+                decode_exec, KVDecoders(action_decoders)\n+            ),\n+        }\n+\n+    @classmethod\n+    def _other_action_decoders(cls):\n+        \"\"\"Recoders for other actions (see man(7) ovs-actions)\"\"\"\n+        return {\n+            \"conjunction\": nested_list_decoder(\n+                ListDecoders(\n+                    [(\"id\", decode_int), (\"k\", decode_int), (\"n\", decode_int)]\n+                ),\n+                delims=[\",\", \"/\"],\n+            ),\n+            \"note\": decode_default,\n+            \"sample\": nested_kv_decoder(\n+                KVDecoders(\n+                    {\n+                        \"probability\": decode_int,\n+                        \"collector_set_id\": decode_int,\n+                        \"obs_domain_id\": decode_int,\n+                        \"obs_point_id\": decode_int,\n+                        \"sampling_port\": decode_default,\n+                        \"ingress\": decode_flag,\n+                        \"egress\": decode_flag,\n+                    }\n+                )\n+            ),\n+        }\n+\n+    @classmethod\n+    def _act_decoders(cls):\n+        \"\"\"Generate the actions decoders\"\"\"\n+\n+        actions = {\n+            **cls._output_actions_decoders(),\n+            **cls._encap_actions_decoders(),\n+            **cls._field_action_decoders(),\n+            **cls._meta_action_decoders(),\n+            **cls._fw_action_decoders(),\n+            **cls._control_action_decoders(),\n+            **cls._other_action_decoders(),\n+        }\n+        clone_actions = cls._clone_actions_decoders(actions)\n+        actions.update(clone_actions)\n+        return KVDecoders(actions, default_free=decode_free_output)\ndiff --git a/python/ovs/flows/ofp_act.py b/python/ovs/flows/ofp_act.py\nnew file mode 100644\nindex 000000000..bc6574999\n--- /dev/null\n+++ b/python/ovs/flows/ofp_act.py\n@@ -0,0 +1,233 @@\n+\"\"\" Defines decoders for openflow actions\n+\"\"\"\n+\n+import functools\n+\n+from ovs.flows.kv import nested_kv_decoder, KVDecoders, KeyValue, KVParser\n+from ovs.flows.decoders import (\n+    decode_default,\n+    decode_time,\n+    decode_flag,\n+    decode_int,\n+)\n+from ovs.flows.ofp_fields import field_decoders\n+\n+\n+def decode_output(value):\n+    \"\"\"Decodes the output value\n+\n+    Does not support field specification\n+    \"\"\"\n+    if len(value.split(\",\")) > 1:\n+        return nested_kv_decoder()(value)\n+    try:\n+        return {\"port\": int(value)}\n+    except ValueError:\n+        return {\"port\": value.strip('\"')}\n+\n+\n+def decode_controller(value):\n+    \"\"\"Decodes the controller action\"\"\"\n+    if not value:\n+        return KeyValue(\"output\", \"controller\")\n+    else:\n+        # Try controller:max_len\n+        try:\n+            max_len = int(value)\n+            return {\n+                \"max_len\": max_len,\n+            }\n+        except ValueError:\n+            pass\n+        # controller(key[=val], ...)\n+        return nested_kv_decoder()(value)\n+\n+\n+def decode_bundle_load(value):\n+    return decode_bundle(value, True)\n+\n+\n+def decode_bundle(value, load=False):\n+    \"\"\"Decode bundle action\"\"\"\n+    result = {}\n+    keys = [\"fields\", \"basis\", \"algorithm\", \"ofport\"]\n+    if load:\n+        keys.append(\"dst\")\n+\n+    for key in keys:\n+        parts = value.partition(\",\")\n+        nvalue = parts[0]\n+        value = parts[2]\n+        if key == \"ofport\":\n+            continue\n+        result[key] = decode_default(nvalue)\n+\n+    # Handle members:\n+    mvalues = value.split(\"members:\")\n+    result[\"members\"] = [int(port) for port in mvalues[1].split(\",\")]\n+    return result\n+\n+\n+def decode_encap_ethernet(value):\n+    \"\"\"Decodes encap ethernet value\"\"\"\n+    return \"ethernet\", int(value, 0)\n+\n+\n+def decode_field(value):\n+    \"\"\"Decodes a field as defined in the 'Field Specification' of the actions\n+    man page: http://www.openvswitch.org/support/dist-docs/ovs-actions.7.txt\n+    \"\"\"\n+    parts = value.strip(\"]\\n\\r\").split(\"[\")\n+    result = {\n+        \"field\": parts[0],\n+    }\n+\n+    if len(parts) > 1 and parts[1]:\n+        field_range = parts[1].split(\"..\")\n+        start = field_range[0]\n+        end = field_range[1] if len(field_range) > 1 else start\n+        if start:\n+            result[\"start\"] = int(start)\n+        if end:\n+            result[\"end\"] = int(end)\n+\n+    return result\n+\n+\n+def decode_load_field(value):\n+    \"\"\"Decodes 'load:value->dst' actions\"\"\"\n+    parts = value.split(\"->\")\n+    if len(parts) != 2:\n+        raise ValueError(\"Malformed load action : %s\" % value)\n+\n+    # If the load action is performed within a learn() action,\n+    # The value can be specified as another field.\n+    try:\n+        return {\"value\": int(parts[0], 0), \"dst\": decode_field(parts[1])}\n+    except ValueError:\n+        return {\"src\": decode_field(parts[0]), \"dst\": decode_field(parts[1])}\n+\n+\n+def decode_set_field(field_decoders, value):\n+    \"\"\"Decodes 'set_field:value/mask->dst' actions\n+\n+    The value is decoded by field_decoders which is a KVDecoders instance\n+    Args:\n+        field_decoders\n+    \"\"\"\n+    parts = value.split(\"->\")\n+    if len(parts) != 2:\n+        raise ValueError(\"Malformed set_field action : %s\" % value)\n+\n+    val = parts[0]\n+    dst = parts[1]\n+\n+    val_result = field_decoders.decode(dst, val)\n+\n+    return {\n+        \"value\": {val_result[0]: val_result[1]},\n+        \"dst\": decode_field(dst),\n+    }\n+\n+\n+def decode_move_field(value):\n+    \"\"\"Decodes 'move:src->dst' actions\"\"\"\n+    parts = value.split(\"->\")\n+    if len(parts) != 2:\n+        raise ValueError(\"Malformed move action : %s\" % value)\n+\n+    return {\n+        \"src\": decode_field(parts[0]),\n+        \"dst\": decode_field(parts[1]),\n+    }\n+\n+\n+def decode_dec_ttl(value):\n+    \"\"\"Decodes dec_ttl and dec_ttl(id, id[2], ...) actions\"\"\"\n+    if not value:\n+        return True\n+    return [int(idx) for idx in value.split(\",\")]\n+\n+\n+def decode_chk_pkt_larger(value):\n+    \"\"\"Decodes 'check_pkt_larger(pkt_len)->dst' actions\"\"\"\n+    parts = value.split(\"->\")\n+    if len(parts) != 2:\n+        raise ValueError(\"Malformed check_pkt_larger action : %s\" % value)\n+\n+    pkt_len = int(parts[0].strip(\"()\"))\n+    dst = decode_field(parts[1])\n+    return {\"pkt_len\": pkt_len, \"dst\": dst}\n+\n+\n+# CT decoders\n+def decode_zone(value):\n+    \"\"\"Decodes the 'zone' keyword of the ct action\"\"\"\n+    try:\n+        return int(value, 0)\n+    except ValueError:\n+        pass\n+    return decode_field(value)\n+\n+\n+def decode_exec(action_decoders, value):\n+    \"\"\"Decodes the 'exec' keyword of the ct action\n+\n+    Args:\n+        decode_actions (KVDecoders): the decoders to be used to decode the\n+            nested exec\n+        value (string): the string to be decoded\n+    \"\"\"\n+    exec_parser = KVParser(action_decoders)\n+    exec_parser.parse(value)\n+    return [{kv.key: kv.value} for kv in exec_parser.kv()]\n+\n+\n+def decode_learn(action_decoders):\n+    \"\"\"Create the decoder to be used to decode the 'learn' action.\n+\n+    The learn action can include any nested action, therefore we need decoders\n+    for all possible actions.\n+\n+    Args:\n+        action_decoders (dict): dictionary of decoders to be used in nested\n+            action decoding\n+\n+    \"\"\"\n+\n+    def decode_learn_field(decoder, value):\n+        \"\"\"Generates a decoder to be used for the 'field' argument of the\n+        'learn' action.\n+\n+        The field can hold a value that should be decoded, either as a field,\n+        or as a the value (see man(7) ovs-actions)\n+\n+        Args:\n+            decoder (callable): The decoder\n+\n+        \"\"\"\n+        if value in field_decoders.keys():\n+            # It's a field\n+            return value\n+        else:\n+            return decoder(value)\n+\n+    learn_field_decoders = {\n+        field: functools.partial(decode_learn_field, decoder)\n+        for field, decoder in field_decoders.items()\n+    }\n+    learn_decoders = {\n+        **action_decoders,\n+        **learn_field_decoders,\n+        \"idle_timeout\": decode_time,\n+        \"hard_timeout\": decode_time,\n+        \"priority\": decode_int,\n+        \"cooke\": decode_int,\n+        \"send_flow_rem\": decode_flag,\n+        \"table\": decode_int,\n+        \"delete_learned\": decode_flag,\n+        \"limit\": decode_int,\n+        \"result_dst\": decode_field,\n+    }\n+\n+    return functools.partial(decode_exec, KVDecoders(learn_decoders))\n",
    "prefixes": [
        "ovs-dev",
        "v1",
        "07/18"
    ]
}