CSIT-233 IPv4 IPFIX - baseline tests 15/2415/11
authorselias <samelias@cisco.com>
Thu, 11 Aug 2016 08:02:33 +0000 (10:02 +0200)
committerJan Gelety <jgelety@cisco.com>
Thu, 25 Aug 2016 21:34:48 +0000 (21:34 +0000)
 - add scapy classes for parsing IPFIX packets
 - add vat scripts and keywords for settings up IPFIX
 - add IPv4 IPFIX test suite

Change-Id: I80ab76ca361c7920a01a46ad720b1c04acd0d147
Signed-off-by: selias <samelias@cisco.com>
12 files changed:
resources/libraries/python/Classify.py
resources/libraries/python/IPFIXSetup.py [new file with mode: 0644]
resources/libraries/python/IPFIXUtil.py [new file with mode: 0644]
resources/libraries/python/PacketVerifier.py
resources/libraries/robot/ipfix.robot [new file with mode: 0644]
resources/templates/vat/classify_add_session_generic.vat [new file with mode: 0644]
resources/templates/vat/ipfix_exporter_set.vat [new file with mode: 0644]
resources/templates/vat/ipfix_interface_enable.vat [new file with mode: 0644]
resources/templates/vat/ipfix_stream_set.vat [new file with mode: 0644]
resources/templates/vat/ipfix_table_add.vat [new file with mode: 0644]
resources/traffic_scripts/ipfix_check.py [new file with mode: 0755]
tests/func/ipfix/ipfix_ipv4.robot [new file with mode: 0644]

index dfa5c33..8dbe3fb 100644 (file)
@@ -37,6 +37,7 @@ class Classify(object):
         :rtype: tuple(int, int, int)
         :raises RuntimeError: If VPP can't create table.
         """
+
         output = VatExecutor.cmd_from_template(node, "classify_add_table.vat",
                                                ip_version=ip_version,
                                                direction=direction)
@@ -204,6 +205,43 @@ class Classify(object):
                 match_n=match_n,
                 hex_value=hex_value)
 
+    @staticmethod
+    def vpp_configures_classify_session_generic(node, session_type, table_index,
+                                                skip_n, match_n, match,
+                                                match2=''):
+        """Configuration of classify session.
+
+        :param node: VPP node to setup classify session.
+        :param session_type: Session type - hit-next, l2-hit-next, acl-hit-next
+        or policer-hit-next, and their respective parameters.
+        :param table_index: Classify table index.
+        :param skip_n: Number of skip vectors based on mask.
+        :param match_n: Number of match vectors based on mask.
+        :param match: Match value - l2, l3, l4 or hex, and their
+        respective parameters.
+        :param match2: Additional match values, to avoid using overly long
+        variables in RobotFramework.
+        :type node: dict
+        :type session_type: str
+        :type table_index: int
+        :type skip_n: int
+        :type match_n: int
+        :type match: str
+        :type match2: str
+        """
+
+        match = ' '.join((match, match2))
+
+        with VatTerminal(node) as vat:
+            vat.vat_terminal_exec_cmd_from_template(
+                "classify_add_session_generic.vat",
+                type=session_type,
+                table_index=table_index,
+                skip_n=skip_n,
+                match_n=match_n,
+                match=match,
+            )
+
     @staticmethod
     def compute_classify_hex_mask(ip_version, protocol, direction):
         """Compute classify hex mask for TCP or UDP packet matching.
diff --git a/resources/libraries/python/IPFIXSetup.py b/resources/libraries/python/IPFIXSetup.py
new file mode 100644 (file)
index 0000000..f0f35f3
--- /dev/null
@@ -0,0 +1,129 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""IPFIX setup library"""
+
+from resources.libraries.python.topology import Topology
+from resources.libraries.python.VatExecutor import VatTerminal
+
+
+class IPFIXSetup(object):
+    """Class contains methods for seting up IPFIX reporting on DUTs."""
+
+    def __init__(self):
+        """Initializer."""
+        pass
+
+    @staticmethod
+    def setup_ipfix_exporter(node, collector, source, fib=None, mtu=None,
+                             interval=None):
+        """Setup an IPFIX exporter on node to export collected flow data.
+
+        :param node: DUT node.
+        :param collector: IP address of flow data collector.
+        :param source: IP address of local interface to send flow data from.
+        :param fib: fib table ID.
+        :param mtu: Maximum transfer unit of path to collector.
+        :param interval: Frequency of sending template packets, in seconds.
+        :type node: dict
+        :type collector: str
+        :type source: str
+        :type fib: int
+        :type mtu: int
+        :type interval: int
+        """
+
+        fib = "vrf_id {0}".format(fib) if fib else ''
+        mtu = "path_mtu {0}".format(mtu) if mtu else ''
+        interval = "template_interval {0}".format(interval) if interval else ''
+
+        with VatTerminal(node, json_param=False) as vat:
+            vat.vat_terminal_exec_cmd_from_template('ipfix_exporter_set.vat',
+                                                    collector=collector,
+                                                    source=source,
+                                                    fib=fib,
+                                                    mtu=mtu,
+                                                    interval=interval)
+
+    @staticmethod
+    def assign_interface_to_flow_table(node, interface, table_id,
+                                       ip_version='ip4'):
+        """Assigns a VPP interface to the specified classify table for IPFIX
+        flow data collection.
+
+        :param node: DUT node.
+        :param interface: An interface on the DUT node.
+        :param table_id: ID of a classify table.
+        :param ip_version: Version of IP protocol. Valid options are ip4, ip6.
+        :type node: dict
+        :type interface: str or int
+        :type table_id: int
+        :type ip_version: str
+        """
+
+        if isinstance(interface, basestring):
+            sw_if_index = Topology.get_interface_sw_index(node, interface)
+        elif isinstance(interface, int):
+            sw_if_index = interface
+        else:
+            raise TypeError
+
+        table = "{0}-table {1}".format(ip_version, table_id)
+
+        with VatTerminal(node, json_param=False) as vat:
+            vat.vat_terminal_exec_cmd_from_template(
+                "ipfix_interface_enable.vat",
+                interface=sw_if_index,
+                table=table,
+                delete='')
+
+    @staticmethod
+    def set_ipfix_stream(node, domain=None, src_port=None):
+        """Set an IPFIX export stream. Can be used to break up IPFIX reports
+        into separate reporting domains.
+
+        :param node: DUT node.
+        :param domain: Desired index number of exporting domain.
+        :param src_port: Source port to use when sending IPFIX packets. Default
+        is the standard IPFIX port 4739.
+        :type node: dict
+        :type domain: int
+        :type src_port: int
+        """
+
+        domain = "domain {0}".format(domain) if domain else ''
+        src_port = "src_port {0}".format(src_port) if src_port else ''
+
+        with VatTerminal(node, json_param=False) as vat:
+            vat.vat_terminal_exec_cmd_from_template("ipfix_stream_set.vat",
+                                                    domain=domain,
+                                                    src_port=src_port)
+
+    @staticmethod
+    def assign_classify_table_to_exporter(node, table_id, ip_version='ip4'):
+        """Assign a classify table to an IPFIX exporter. Classified packets will
+         be included in the IPFIX flow report.
+
+         :param node: DUT node.
+         :param table_id: ID of a classify table.
+         :param ip_version: Version of IP protocol. Valid options are ip4, ip6.
+         :type node: dict
+         :type table_id: int
+         :type ip_version: str
+         """
+
+        with VatTerminal(node, json_param=False) as vat:
+            vat.vat_terminal_exec_cmd_from_template("ipfix_table_add.vat",
+                                                    table=table_id,
+                                                    ip_version=ip_version,
+                                                    add_del='add')
diff --git a/resources/libraries/python/IPFIXUtil.py b/resources/libraries/python/IPFIXUtil.py
new file mode 100644 (file)
index 0000000..f3247a8
--- /dev/null
@@ -0,0 +1,102 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""IPFIX utilities library. Provides classes that allow scapy to work
+with IPFIX packets.
+
+ Note:
+ Template and data sets in one packet are not supported.
+ Option template sets (Set_ID = 3) are not supported.
+  """
+
+
+from scapy.all import Packet, bind_layers
+from scapy.fields import *
+from scapy.layers.inet import UDP
+from scapy.contrib.ppi_geotag import UTCTimeField
+
+
+class IPFIXHandler(object):
+    """Class for handling IPFIX packets. To use, create instance of class before
+     dissecting IPFIX packets with scapy, then run update_template every time
+     an IPFIX template packet is received."""
+
+    template_elements = {
+        4: ByteField("Protocol_ID", 0x00),
+        7: ShortField("src_port", 0),
+        8: IPField("IPv4_src", ""),
+        11: ShortField("dst_port", 0),
+        12: IPField("IPv4_dst", ""),
+        86: LongField("packetTotalCount", 0),
+        180: ShortField("udp_src_port", 0),
+        181: ShortField("udp_dst_port", 0),
+        182: ShortField("tcp_src_port", 0),
+        183: ShortField("tcp_dst_port", 0),
+    }
+
+    def __init__(self):
+        """Initializer, registers IPFIX header and template layers with scapy.
+        """
+        bind_layers(UDP, IPFIXHeader, dport=4739)
+        bind_layers(IPFIXHeader, IPFIXTemplate, Set_ID=2)
+
+    def update_template(self, packet):
+        """Updates IPFIXData class with new data template. Registers IPFIX data
+        layer with scapy using the new template.
+
+        :param packet: Packet containing an IPFIX template.
+        :type packet: scapy.Ether
+        """
+        template_list = packet['IPFIX template'].Template
+        template_id = packet['IPFIX template'].Template_ID
+
+        IPFIXData.fields_desc = []
+        for item in template_list[::2]:
+            try:
+                IPFIXData.fields_desc.append(self.template_elements[item])
+            except KeyError:
+                raise KeyError(
+                    "Unknown IPFIX template element with ID {0}".format(item))
+        bind_layers(IPFIXHeader, IPFIXData, Set_ID=template_id)
+        # if the packet doesn't end here, assume it contains more data sets
+        bind_layers(IPFIXData, IPFIXData)
+
+
+class IPFIXHeader(Packet):
+    """Class for IPFIX header."""
+    name = "IPFIX header"
+    fields_desc = [StrFixedLenField("Version", 0x000a, length=2),
+                   ShortField("Message Length", 0),
+                   UTCTimeField("Timestamp(UTC)", ""),
+                   IntField("Sequence Number", 0),
+                   IntField("Observation Domain ID", 0),
+                   ShortField("Set_ID", 0),
+                   ShortField("Set_Length", 0)
+                   ]
+
+
+class IPFIXTemplate(Packet):
+    """Class for IPFIX template layer."""
+    name = "IPFIX template"
+    fields_desc = [ShortField("Template_ID", 256),
+                   ShortField("nFields", 2),
+                   FieldListField("Template", [], ShortField("type_len", ""),
+                                  count_from=lambda p: p.nFields*2)
+                   ]
+
+
+class IPFIXData(Packet):
+    """Class for IPFIX data layer. Needs to be updated with
+    a template before use."""
+    name = "IPFIX flow data"
+    fields_desc = []
index 78c3670..59ea2db 100644 (file)
@@ -204,7 +204,7 @@ class RxQueue(PacketVerifier):
     def __init__(self, interface_name):
         PacketVerifier.__init__(self, interface_name)
 
-    def recv(self, timeout=3, ignore=None):
+    def recv(self, timeout=3, ignore=None, verbose=True):
         """Read next received packet.
 
         Returns scapy's Ether() object created from next packet in the queue.
@@ -212,9 +212,11 @@ class RxQueue(PacketVerifier):
         arrives in given timeout queue.Empty exception will be risen.
 
         :param timeout: How many seconds to wait for next packet.
-        :param ignore: Packet list that should be ignored.
+        :param ignore: List of packets that should be ignored.
+        :param verbose: Used to suppress detailed logging of received packets.
         :type timeout: int
         :type ignore: list
+        :type verbose: bool
 
         :return: Ether() initialized object from packet data.
         :rtype: scapy.Ether
@@ -226,8 +228,9 @@ class RxQueue(PacketVerifier):
         pkt = self._sock.recv(0x7fff)
         pkt_pad = auto_pad(pkt)
         print 'Received packet on {0} of len {1}'.format(self._ifname, len(pkt))
-        Ether(pkt).show2()
-        print
+        if verbose:
+            Ether(pkt).show2()
+            print
 
         if ignore is not None:
             for i, ig_pkt in enumerate(ignore):
@@ -238,7 +241,7 @@ class RxQueue(PacketVerifier):
                     # Found the packet in ignore list, get another one
                     # TODO: subtract timeout - time_spent in here
                     ignore.remove(ig_pkt)
-                    return self.recv(timeout, ignore)
+                    return self.recv(timeout, ignore, verbose)
 
         return Ether(pkt)
 
@@ -254,16 +257,19 @@ class TxQueue(PacketVerifier):
     def __init__(self, interface_name):
         PacketVerifier.__init__(self, interface_name)
 
-    def send(self, pkt):
+    def send(self, pkt, verbose=True):
         """Send packet out of the bound interface.
 
         :param pkt: Packet to send.
+        :param verbose: Used to supress detailed logging of sent packets.
         :type pkt: string or scapy Packet derivative.
+        :type verbose: bool
         """
         print 'Sending packet out of {0} of len {1}'.format(self._ifname,
                                                             len(pkt))
-        Ether(str(pkt)).show2()
-        print
+        if verbose:
+            Ether(str(pkt)).show2()
+            print
 
         pkt = auto_pad(str(pkt))
         self._sock.send(pkt)
diff --git a/resources/libraries/robot/ipfix.robot b/resources/libraries/robot/ipfix.robot
new file mode 100644 (file)
index 0000000..a5adacf
--- /dev/null
@@ -0,0 +1,67 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Traffic keywords"""
+
+*** Settings ***
+| Library | resources.libraries.python.TrafficScriptExecutor
+| Library | resources.libraries.python.InterfaceUtil
+| Resource | resources/libraries/robot/default.robot
+| Documentation | Traffic keywords
+
+*** Keywords ***
+| Send packets and verify IPFIX
+| | [Documentation] | Send simple TCP or UDP packets from source interface\
+| | ... | to destination interface. Listen for IPFIX flow report on source\
+| | ... | interface and verify received report against number of packets sent.
+| | ...
+| | ... | *Arguments:*
+| | ...
+| | ... | - tg_node - TG node. Type: dictionary
+| | ... | - dst_node - Destination node. Type: dictionary
+| | ... | - src_int - Source interface. Type: string
+| | ... | - dst_int - Destination interface. Type: string
+| | ... | - src_ip - Source IP address. Type: string
+| | ... | - dst_ip - Destination IP address. Type: string
+| | ... | - protocol - TCP or UDP (Optional, default is TCP). Type: string
+| | ... | - port - Source and destination ports to use
+| | ... | (Optional, default is port 20). Type: integer
+| | ... | - count - Number of packets to send
+| | ... | (Optional, default is one packet). Type: integer
+| | ... | - timeout - Timeout value in seconds (Optional, default is 10 sec).
+| | ... | Should be at least twice the configured IPFIX flow report interval.
+| | ... | Type: integer
+| | ...
+| | ... | *Return:*
+| | ...
+| | ... | - No value returned
+| | ...
+| | ... | *Example:*
+| | ...
+| | ... | \| Send packets and verify IPFIX \| ${nodes['TG']} | ${nodes['DUT1']}\
+| | ... | \| eth1 \| GigabitEthernet0/8/0 \| 16.0.0.1 \| 192.168.0.2 \| UDP \
+| | ... | \| ${20} \| ${5} \| ${10} \|
+| | ... |
+| | [Arguments] | ${tg_node} | ${dst_node} | ${src_int} | ${dst_int} |
+| | ... | ${src_ip} | ${dst_ip} | ${protocol}=tcp | ${port}=20 | ${count}=1
+| | ... | ${timeout}=${10}
+| | ${src_mac}= | Get Interface Mac | ${tg_node} | ${src_int}
+| | ${dst_mac}= | Get Interface Mac | ${dst_node} | ${dst_int}
+| | ${src_int_name}= | Get interface name | ${tg_node} | ${src_int}
+| | ${dst_int_name}= | Get interface name | ${dst_node} | ${dst_int}
+| | ${args}= | Traffic Script Gen Arg | ${dst_int_name} | ${src_int_name}
+| | ... | ${src_mac} | ${dst_mac} | ${src_ip} | ${dst_ip}
+| | ${args}= | Set Variable
+| | ... | ${args} --protocol ${protocol} --port ${port} --count ${count}
+| | Run Traffic Script On Node | ipfix_check.py | ${tg_node} | ${args}
+| | ... | ${timeout}
diff --git a/resources/templates/vat/classify_add_session_generic.vat b/resources/templates/vat/classify_add_session_generic.vat
new file mode 100644 (file)
index 0000000..b100629
--- /dev/null
@@ -0,0 +1 @@
+classify_add_del_session {type} table-index {table_index} skip_n {skip_n} match_n {match_n} match {match}
\ No newline at end of file
diff --git a/resources/templates/vat/ipfix_exporter_set.vat b/resources/templates/vat/ipfix_exporter_set.vat
new file mode 100644 (file)
index 0000000..9217375
--- /dev/null
@@ -0,0 +1 @@
+set_ipfix_exporter collector_address {collector} src_address {source} {fib} {mtu} {interval}
\ No newline at end of file
diff --git a/resources/templates/vat/ipfix_interface_enable.vat b/resources/templates/vat/ipfix_interface_enable.vat
new file mode 100644 (file)
index 0000000..5432a69
--- /dev/null
@@ -0,0 +1 @@
+flow_classify_set_interface sw_if_index {interface} {table} {delete}
\ No newline at end of file
diff --git a/resources/templates/vat/ipfix_stream_set.vat b/resources/templates/vat/ipfix_stream_set.vat
new file mode 100644 (file)
index 0000000..97d84be
--- /dev/null
@@ -0,0 +1 @@
+set_ipfix_classify_stream {domain} {src_port}
\ No newline at end of file
diff --git a/resources/templates/vat/ipfix_table_add.vat b/resources/templates/vat/ipfix_table_add.vat
new file mode 100644 (file)
index 0000000..87b0cc1
--- /dev/null
@@ -0,0 +1 @@
+ipfix_classify_table_add_del table {table} {ip_version} {add_del}
\ No newline at end of file
diff --git a/resources/traffic_scripts/ipfix_check.py b/resources/traffic_scripts/ipfix_check.py
new file mode 100755 (executable)
index 0000000..14b5a07
--- /dev/null
@@ -0,0 +1,198 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+"""Traffic script - IPFIX listener."""
+
+import sys
+from ipaddress import IPv4Address, IPv6Address, AddressValueError
+
+from scapy.layers.inet import IP, TCP, UDP
+from scapy.layers.inet6 import IPv6
+from scapy.layers.l2 import Ether
+
+from resources.libraries.python.IPFIXUtil import IPFIXHandler, IPFIXData
+from resources.libraries.python.PacketVerifier import RxQueue, TxQueue, auto_pad
+from resources.libraries.python.TrafficScriptArg import TrafficScriptArg
+
+
+def valid_ipv4(ip):
+    """Check if IP address has the correct IPv4 address format.
+
+    :param ip: IP address.
+    :type ip: str
+    :return: True in case of correct IPv4 address format,
+    otherwise return false.
+    :rtype: bool
+    """
+    try:
+        IPv4Address(unicode(ip))
+        return True
+    except (AttributeError, AddressValueError):
+        return False
+
+
+def valid_ipv6(ip):
+    """Check if IP address has the correct IPv6 address format.
+
+    :param ip: IP address.
+    :type ip: str
+    :return: True in case of correct IPv6 address format,
+    otherwise return false.
+    :rtype: bool
+    """
+    try:
+        IPv6Address(unicode(ip))
+        return True
+    except (AttributeError, AddressValueError):
+        return False
+
+
+def main():
+    """Send packets to VPP, then listen for IPFIX flow report. Verify that
+    the correct packet count was reported."""
+    args = TrafficScriptArg(
+        ['src_mac', 'dst_mac', 'src_ip', 'dst_ip', 'protocol', 'port', 'count']
+    )
+
+    dst_mac = args.get_arg('dst_mac')
+    src_mac = args.get_arg('src_mac')
+    src_ip = args.get_arg('src_ip')
+    dst_ip = args.get_arg('dst_ip')
+    tx_if = args.get_arg('tx_if')
+
+    protocol = args.get_arg('protocol')
+    source_port = int(args.get_arg('port'))
+    destination_port = int(args.get_arg('port'))
+    count = int(args.get_arg('count'))
+
+    txq = TxQueue(tx_if)
+    rxq = RxQueue(tx_if)
+
+    # generate simple packet based on arguments
+    ip_version = None
+    if valid_ipv4(src_ip) and valid_ipv4(dst_ip):
+        ip_version = IP
+    elif valid_ipv6(src_ip) and valid_ipv6(dst_ip):
+        ip_version = IPv6
+    else:
+        raise ValueError("Invalid IP version!")
+
+    if protocol.upper() == 'TCP':
+        protocol = TCP
+    elif protocol.upper() == 'UDP':
+        protocol = UDP
+    else:
+        raise ValueError("Invalid type of protocol!")
+
+    pkt_raw = (Ether(src=src_mac, dst=dst_mac) /
+               ip_version(src=src_ip, dst=dst_ip) /
+               protocol(sport=int(source_port),
+                        dport=int(destination_port)))
+
+    # do not print details for sent packets when sending more than one
+    if count > 1:
+        verbose = False
+        print("Sending more than one packet. Details will be filtered for "
+              "all packets sent.")
+    else:
+        verbose = True
+
+    pkt_pad = auto_pad(pkt_raw)
+    ignore = []
+    for _ in range(count):
+        txq.send(pkt_pad, verbose=verbose)
+        ignore.append(pkt_pad)
+
+    # allow scapy to recognize IPFIX headers and templates
+    ipfix = IPFIXHandler()
+
+    # clear receive buffer
+    while True:
+        pkt = rxq.recv(1, ignore=ignore, verbose=verbose)
+        if pkt is None:
+            break
+
+    data = None
+    # get IPFIX template and data
+    while True:
+        pkt = rxq.recv(5)
+        if pkt is None:
+            raise RuntimeError("RX timeout")
+        if pkt.haslayer("IPFIXHeader"):
+            if pkt.haslayer("IPFIXTemplate"):
+                # create or update template for IPFIX data packets
+                ipfix.update_template(pkt)
+            elif pkt.haslayer("IPFIXData"):
+                data = pkt.getlayer(IPFIXData).fields
+                break
+            else:
+                raise RuntimeError("Unable to parse IPFIX set after header.")
+        else:
+            raise RuntimeError("Received non-IPFIX packet or IPFIX header "
+                               "not recognized.")
+
+    # verify packet count
+    if data["packetTotalCount"] != count:
+        raise RuntimeError(
+            "IPFIX reported wrong packet count. Count was {0},"
+            " but should be {1}".format(data["packetTotalCount"], count))
+    # verify IP addresses
+    keys = data.keys()
+    err = "{0} mismatch. Packets used {1}, but were classified as {2}."
+    if ip_version == IP:
+        if "IPv4_src" in keys:
+            if data["IPv4_src"] != src_ip:
+                raise RuntimeError(
+                    err.format("Source IP", src_ip, data["IPv4_src"]))
+        if "IPv4_dst" in keys:
+            if data["IPv4_dst"] != dst_ip:
+                raise RuntimeError(
+                    err.format("Destination IP", dst_ip, data["IPv4_dst"]))
+    else:
+        if "IPv6_src" in keys:
+            if data["IPv6_src"] != src_ip:
+                raise RuntimeError(
+                    err.format("Source IP", src_ip, data["IPv6_src"]))
+        if "IPv6_dst" in keys:
+            if data["IPv6_dst"] != dst_ip:
+                raise RuntimeError(
+                    err.format("Source IP", src_ip, data["IPv6_dst"]))
+    # verify port numbers
+    for item in ("src_port", "tcp_src_port", "udp_src_port"):
+        try:
+            if int(data[item]) != source_port:
+                raise RuntimeError(
+                    err.format("Source port", source_port, data[item]))
+        except KeyError:
+            pass
+    for item in ("dst_port", "tcp_dst_port", "udp_dst_port"):
+        try:
+            if int(data[item]) != destination_port:
+                raise RuntimeError(
+                    err.format("Source port", destination_port, data[item]))
+        except KeyError:
+            pass
+    # verify protocol ID
+    if "Protocol_ID" in keys:
+        if protocol == TCP and int(data["Protocol_ID"]) != 6:
+            raise RuntimeError("TCP Packets were classified as not TCP.")
+        if protocol == UDP and int(data["Protocol_ID"]) != 17:
+            raise RuntimeError("UDP Packets were classified as not UDP.")
+    sys.exit(0)
+
+
+if __name__ == "__main__":
+
+    main()
diff --git a/tests/func/ipfix/ipfix_ipv4.robot b/tests/func/ipfix/ipfix_ipv4.robot
new file mode 100644 (file)
index 0000000..3f2d753
--- /dev/null
@@ -0,0 +1,205 @@
+# Copyright (c) 2016 Cisco and/or its affiliates.
+# 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.
+
+*** Settings ***
+| Resource | resources/libraries/robot/default.robot
+| Resource | resources/libraries/robot/interfaces.robot
+| Resource | resources/libraries/robot/testing_path.robot
+| Resource | resources/libraries/robot/ipv4.robot
+| Resource | resources/libraries/robot/ipfix.robot
+| Library | resources.libraries.python.Classify.Classify
+| Library | resources.libraries.python.IPFIXSetup
+| Library | resources.libraries.python.Trace
+
+| Force Tags | HW_ENV | VM_ENV | 3_NODE_SINGLE_LINK_TOPO | EXPECTED_FAILING
+# TODO: Remove EXPECTED_FAILING tag once functionality is implemented (VPP-204)
+| Suite Setup | Run Keywords | Setup all TGs before traffic script
+| ...         | AND          | Update All Interface Data On All Nodes | ${nodes}
+| Test Setup | Setup all DUTs before test
+| Test Teardown | Run Keywords | Show packet trace on all DUTs | ${nodes}
+| ...           | AND          | Vpp Show Errors | ${nodes['DUT1']}
+| ...           | AND          | Show vpp trace dump on all DUTs
+| Documentation | *IPFIX ipv4 test cases*
+| ...
+| ... | IPFIX tests use 3-node topology TG - DUT1 - DUT2 - TG with
+| ... | one link between the nodes. DUT1 is configured with IPv4
+| ... | routing and static routes. IPFIX is configured on DUT1 with
+| ... | DUT1->TG interface as collector. Test packets are
+| ... | sent from TG to DUT1. TG listens for flow report packets
+| ... | and verifies that they contains flow record of test packets sent.
+
+*** Variables ***
+| ${dut1_to_tg_ip}= | 192.168.1.1
+| ${dut2_to_dut1_ip}= | 192.168.2.1
+| ${tg_to_dut1_ip}= | 16.0.0.1
+| ${prefix_length}= | 24
+| ${ip_version}= | ip4
+| ${port}= | 80
+
+*** Test Cases ***
+| TC01: DUT sends IPFIX template and data packets
+| | [Documentation]
+| | ... | [Top] TG-DUT1-DUT2-TG. [Cfg] On DUT1 configure IPFIX with TG interface
+| | ... | address as collector and a basic classify session.
+| | ... | [Ver] Make TG listen for IPFIX template and data packets, verify
+| | ... | that packet is received and correct. No packets are sent from TG.
+| | ... | [Ref] RFC 7011
+| | Given Path for 3-node testing is set
+| | ... | ${nodes['TG']} | ${nodes['DUT1']} | ${nodes['DUT2']} | ${nodes['TG']}
+| | And Interfaces in 3-node path are up
+| | And Set Interface Address | ${dut1_node}
+| | ... | ${dut1_to_tg} | ${dut1_to_tg_ip} | ${prefix_length}
+| | And Add ARP on DUT | ${dut1_node} | ${dut1_to_tg} | ${tg_to_dut1_ip}
+| | ... | ${tg_to_dut1_mac}
+| | ${table_index} | ${skip_n} | ${match_n}=
+| | ... | And VPP creates classify table L3 | ${dut1_node} | ${ip_version} | src
+| | And VPP configures classify session L3 | ${dut1_node} | permit
+| | ... | ${table_index} | ${skip_n} | ${match_n} | ${ip_version} | src
+| | ... | ${tg_to_dut1_ip}
+| | When Assign interface to flow table | ${dut1_node} | ${dut1_to_tg}
+| | ... | ${table_index} | ip_version=${ip_version}
+| | And Setup IPFIX exporter | ${dut1_node} | ${tg_to_dut1_ip}
+| | ... | ${dut1_to_tg_ip} | interval=5
+| | And Set IPFIX stream | ${dut1_node} | ${1}
+| | And Assign classify table to exporter | ${dut1_node} | ${table_index}
+| | ... | ${ip_version}
+| | Then Send packets and verify IPFIX | ${tg_node} | ${dut1_node}
+| | ... | ${tg_to_dut1} | ${dut1_to_tg} | ${tg_to_dut1_ip} | ${dut1_to_tg_ip}
+| | ... | count=0
+
+| TC02: DUT reports packet flow for traffic by source address
+| | [Documentation]
+| | ... | [Top] TG-DUT1-DUT2-TG. [Cfg] On DUT1 configure IPFIX with TG interface
+| | ... | address as collector and add classify session with TG source address.
+| | ... | [Ver] Make TG send a packet to DUT1, then listen for IPFIX template
+| | ... | and data packets, verify that IPFIX reported the received packet.
+| | ... | [Ref] RFC 7011
+| | Given Path for 3-node testing is set
+| | ... | ${nodes['TG']} | ${nodes['DUT1']} | ${nodes['DUT2']} | ${nodes['TG']}
+| | And Interfaces in 3-node path are up
+| | And Set Interface Address | ${dut1_node}
+| | ... | ${dut1_to_tg} | ${dut1_to_tg_ip} | ${prefix_length}
+| | And Add ARP on DUT | ${dut1_node} | ${dut1_to_tg} | ${tg_to_dut1_ip}
+| | ... | ${tg_to_dut1_mac}
+| | ${table_index} | ${skip_n} | ${match_n}=
+| | ... | And VPP creates classify table L3 | ${dut1_node} | ${ip_version} | src
+| | And VPP configures classify session L3 | ${dut1_node} | permit
+| | ... | ${table_index} | ${skip_n} | ${match_n} | ${ip_version} | src
+| | ... | ${tg_to_dut1_ip}
+| | When Assign interface to flow table | ${dut1_node} | ${dut1_to_tg}
+| | ... | ${table_index} | ip_version=${ip_version}
+| | And Setup IPFIX exporter | ${dut1_node} | ${tg_to_dut1_ip}
+| | ... | ${dut1_to_tg_ip} | interval=5
+| | And Set IPFIX stream | ${dut1_node} | ${1}
+| | And Assign classify table to exporter | ${dut1_node} | ${table_index}
+| | ... | ${ip_version}
+| | Then Send packets and verify IPFIX | ${tg_node} | ${dut1_node}
+| | ... | ${tg_to_dut1} | ${dut1_to_tg} | ${tg_to_dut1_ip} | ${dut1_to_tg_ip}
+
+| TC03: DUT reports packet flow for traffic with local destination address
+| | [Documentation]
+| | ... | [Top] TG-DUT1-DUT2-TG. [Cfg] On DUT1 configure IPFIX with TG interface
+| | ... | address as collector and add classify session with destination
+| | ... | address of DUT1.
+| | ... | [Ver] Make TG send a packet to DUT1, then listen for IPFIX template
+| | ... | and data packets, verify that IPFIX reported the received packet.
+| | ... | [Ref] RFC 7011
+| | Given Path for 3-node testing is set
+| | ... | ${nodes['TG']} | ${nodes['DUT1']} | ${nodes['DUT2']} | ${nodes['TG']}
+| | And Interfaces in 3-node path are up
+| | And Set Interface Address | ${dut1_node}
+| | ... | ${dut1_to_tg} | ${dut1_to_tg_ip} | ${prefix_length}
+| | And Add ARP on DUT | ${dut1_node} | ${dut1_to_tg} | ${tg_to_dut1_ip}
+| | ... | ${tg_to_dut1_mac}
+| | ${table_index} | ${skip_n} | ${match_n}=
+| | ... | And VPP creates classify table L3 | ${dut1_node} | ${ip_version} | dst
+| | And VPP configures classify session L3 | ${dut1_node} | permit
+| | ... | ${table_index} | ${skip_n} | ${match_n} | ${ip_version} | dst
+| | ... | ${dut1_to_tg_ip}
+| | When Assign interface to flow table | ${dut1_node} | ${dut1_to_tg}
+| | ... | ${table_index} | ip_version=${ip_version}
+| | And Setup IPFIX exporter | ${dut1_node} | ${tg_to_dut1_ip}
+| | ... | ${dut1_to_tg_ip} | interval=5
+| | And Set IPFIX stream | ${dut1_node} | ${1}
+| | And Assign classify table to exporter | ${dut1_node} | ${table_index}
+| | ... | ${ip_version}
+| | Then Send packets and verify IPFIX | ${tg_node} | ${dut1_node}
+| | ... | ${tg_to_dut1} | ${dut1_to_tg} | ${tg_to_dut1_ip} | ${dut1_to_tg_ip}
+
+| TC04: DUT reports packet flow for traffic with remote destination address
+| | [Documentation]
+| | ... | [Top] TG-DUT1-DUT2-TG. [Cfg] On DUT1 configure IPFIX with TG interface
+| | ... | address as collector and add classify session with destination
+| | ... | address of DUT2.
+| | ... | [Ver] Make TG send a packet to DUT2 through DUT1, then listen
+| | ... | for IPFIX template and data packets, verify that IPFIX reported
+| | ... | the received packet.
+| | ... | [Ref] RFC 7011
+| | Given Path for 3-node testing is set
+| | ... | ${nodes['TG']} | ${nodes['DUT1']} | ${nodes['DUT2']} | ${nodes['TG']}
+| | And Interfaces in 3-node path are up
+| | And Set Interface Address | ${dut1_node}
+| | ... | ${dut1_to_tg} | ${dut1_to_tg_ip} | ${prefix_length}
+| | And Add ARP on DUT | ${dut1_node} | ${dut1_to_tg} | ${tg_to_dut1_ip}
+| | ... | ${tg_to_dut1_mac}
+| | And Add ARP on DUT | ${dut1_node} | ${dut1_to_dut2} | ${dut2_to_dut1_ip}
+| | ... | ${dut2_to_dut1_mac}
+| | ${table_index} | ${skip_n} | ${match_n}=
+| | ... | And VPP creates classify table L3 | ${dut1_node} | ${ip_version} | dst
+| | And VPP configures classify session L3 | ${dut1_node} | permit
+| | ... | ${table_index} | ${skip_n} | ${match_n} | ${ip_version} | dst
+| | ... | ${dut2_to_dut1_ip}
+| | When Assign interface to flow table | ${dut1_node} | ${dut1_to_tg}
+| | ... | ${table_index} | ip_version=${ip_version}
+| | And Setup IPFIX exporter | ${dut1_node} | ${tg_to_dut1_ip}
+| | ... | ${dut1_to_tg_ip} | interval=5
+| | And Set IPFIX stream | ${dut1_node} | ${1}
+| | And Assign classify table to exporter | ${dut1_node} | ${table_index}
+| | ... | ${ip_version}
+| | Then Send packets and verify IPFIX | ${tg_node} | ${dut1_node}
+| | ... | ${tg_to_dut1} | ${dut1_to_tg} | ${tg_to_dut1_ip} | ${dut2_to_dut1_ip}
+
+| TC05: DUT reports packet flow for traffic by source and destination port
+| | [Documentation]
+| | ... | [Top] TG-DUT1-DUT2-TG. [Cfg] On DUT1 configure IPFIX with TG interface
+| | ... | address as collector and add classify session with TG source address
+| | ... | and source and destination ports.
+| | ... | [Ver] Make TG send a packet to DUT1, then listen for IPFIX template
+| | ... | and data packets, verify that IPFIX reported the received packet.
+| | ... | [Ref] RFC 7011
+| | Given Path for 3-node testing is set
+| | ... | ${nodes['TG']} | ${nodes['DUT1']} | ${nodes['DUT2']} | ${nodes['TG']}
+| | And Interfaces in 3-node path are up
+| | And Set Interface Address | ${dut1_node}
+| | ... | ${dut1_to_tg} | ${dut1_to_tg_ip} | ${prefix_length}
+| | And Add ARP on DUT | ${dut1_node} | ${dut1_to_tg} | ${tg_to_dut1_ip}
+| | ... | ${tg_to_dut1_mac}
+| | ${table_index} | ${skip_n} | ${match_n}=
+| | ... | And VPP creates classify table L3 | ${dut1_node} | ${ip_version}
+| | ... | src proto l4 src_port dst_port
+| | And VPP configures classify session generic | ${dut1_node}
+| | ... | acl-hit-next permit | ${table_index} | ${skip_n} | ${match_n}
+| | ... | l3 ${ip_version} src ${tg_to_dut1_ip}
+| | ... | proto 6 l4 src_port ${port} dst_port ${port}
+| | When Assign interface to flow table | ${dut1_node} | ${dut1_to_tg}
+| | ... | ${table_index} | ip_version=${ip_version}
+| | And Setup IPFIX exporter | ${dut1_node} | ${tg_to_dut1_ip}
+| | ... | ${dut1_to_tg_ip} | interval=5
+| | And Set IPFIX stream | ${dut1_node} | ${1}
+| | And Assign classify table to exporter | ${dut1_node} | ${table_index}
+| | ... | ${ip_version}
+| | Then Send packets and verify IPFIX | ${tg_node} | ${dut1_node}
+| | ... | ${tg_to_dut1} | ${dut1_to_tg} | ${tg_to_dut1_ip} | ${dut1_to_tg_ip}
+| | ... | port=${port}
+
+# TODO: DUT reports packet flow when ACL is configured with wildcards