netflow: Add basic unit tests.
authorBen Pfaff <blp@nicira.com>
Mon, 19 Dec 2011 22:46:16 +0000 (14:46 -0800)
committerBen Pfaff <blp@nicira.com>
Mon, 19 Dec 2011 22:54:12 +0000 (14:54 -0800)
These tests would have caught the flow statistics bug introduced by commit
501f8d1fd75 (ofproto-dpif: Batch interacting with the dpif on flow miss
operations.)

Signed-off-by: Ben Pfaff <blp@nicira.com>
tests/automake.mk
tests/ofproto-dpif.at
tests/test-netflow.c [new file with mode: 0644]

index 8649b80480d89f80dd5f347c976f37d2809795e3..9ae4c5cdce5962a59ec936aa1cca3ba5c54e23eb 100644 (file)
@@ -261,6 +261,10 @@ noinst_PROGRAMS += tests/test-stp
 tests_test_stp_SOURCES = tests/test-stp.c
 tests_test_stp_LDADD = lib/libopenvswitch.a
 
+noinst_PROGRAMS += tests/test-netflow
+tests_test_netflow_SOURCES = tests/test-netflow.c
+tests_test_netflow_LDADD = lib/libopenvswitch.a
+
 noinst_PROGRAMS += tests/test-unix-socket
 tests_test_unix_socket_SOURCES = tests/test-unix-socket.c
 tests_test_unix_socket_LDADD = lib/libopenvswitch.a
index d733f9b572d27b80ea51ad6b32d9d18163e3fe10..55d1500d3468abc9b4d9f39f46acd6ef1d663456 100644 (file)
@@ -758,3 +758,131 @@ AT_CHECK_UNQUOTED([ovs-appctl fdb/show br1 | sed 's/[[0-9]]$/?/'], [0], [dnl
 
 OVS_VSWITCHD_STOP
 AT_CLEANUP
+
+dnl Test that basic NetFlow reports flow statistics correctly:
+dnl - The initial packet of a flow are correctly accounted.
+dnl - Later packets within a flow are correctly accounted.
+dnl - Flow actions changing (in this case, due to MAC learning)
+dnl   cause a record to be sent.
+AT_SETUP([ofproto-dpif - NetFlow flow expiration])
+
+AT_SKIP_IF([test "x$RANDOM" = x])
+NETFLOW_PORT=`expr 32767 + \( $RANDOM % 32767 \)`
+
+OVS_VSWITCHD_START(
+  [set Bridge br0 fail-mode=standalone -- \
+   add-port br0 p1 -- set Interface p1 type=dummy -- \
+   add-port br0 p2 -- set Interface p2 type=dummy -- \
+   set Bridge br0 netflow=@nf -- \
+   --id=@nf create NetFlow targets=\"127.0.0.1:$NETFLOW_PORT\" \
+     engine_id=1 engine_type=2 active_timeout=30 \
+     add-id-to-interface=false], [<0>
+])
+
+AT_CHECK([test-netflow --detach --pidfile $NETFLOW_PORT:127.0.0.1 > netflow.log])AT_CAPTURE_FILE([netflow.log])
+
+for delay in 1000 30000; do
+    ovs-appctl netdev-dummy/receive p1 'in_port(2),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0,ttl=64,frag=no),icmp(type=8,code=0)'
+    ovs-appctl netdev-dummy/receive p2 'in_port(1),eth(src=50:54:00:00:00:07,dst=50:54:00:00:00:05),eth_type(0x0800),ipv4(src=192.168.0.2,dst=192.168.0.1,proto=1,tos=0,ttl=64,frag=no),icmp(type=0,code=0)'
+
+    ovs-appctl time/warp $delay
+done
+
+OVS_VSWITCHD_STOP
+ovs-appctl -t test-netflow exit
+
+AT_CHECK([[sed -e 's/, uptime [0-9]*//
+s/, now [0-9.]*//
+s/time \([0-9]*\)\.\.\.\1/time <moment>/
+s/time [0-9]*\.\.\.[0-9]*/time <range>/
+' netflow.log]], [0],
+  [header: v5, seq 0, engine 2,1
+rec: 192.168.0.1 > 192.168.0.2, if 1 > 65535, 1 pkts, 60 bytes, ICMP 8:0, time <moment>
+
+header: v5, seq 1, engine 2,1
+rec: 192.168.0.2 > 192.168.0.1, if 2 > 1, 2 pkts, 120 bytes, ICMP 0:0, time <range>
+rec: 192.168.0.1 > 192.168.0.2, if 1 > 2, 1 pkts, 60 bytes, ICMP 8:0, time <moment>
+])
+AT_CLEANUP
+
+dnl Test that basic NetFlow reports active expirations correctly.
+AT_SETUP([ofproto-dpif - NetFlow active expiration])
+
+AT_SKIP_IF([test "x$RANDOM" = x])
+NETFLOW_PORT=`expr 32767 + \( $RANDOM % 32767 \)`
+
+OVS_VSWITCHD_START(
+  [set Bridge br0 fail-mode=standalone -- \
+   add-port br0 p1 -- set Interface p1 type=dummy -- \
+   add-port br0 p2 -- set Interface p2 type=dummy -- \
+   set Bridge br0 netflow=@nf -- \
+   --id=@nf create NetFlow targets=\"127.0.0.1:$NETFLOW_PORT\" \
+     engine_id=1 engine_type=2 active_timeout=10 \
+     add-id-to-interface=false], [<0>
+])
+
+AT_CHECK([test-netflow --detach --pidfile $NETFLOW_PORT:127.0.0.1 > netflow.log])AT_CAPTURE_FILE([netflow.log])
+
+n=1
+while test $n -le 60; do
+    n=`expr $n + 1`
+
+    ovs-appctl netdev-dummy/receive p1 'in_port(2),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=6,tos=0,ttl=64,frag=no),tcp(src=1234,dst=80)'
+    ovs-appctl netdev-dummy/receive p2 'in_port(1),eth(src=50:54:00:00:00:07,dst=50:54:00:00:00:05),eth_type(0x0800),ipv4(src=192.168.0.2,dst=192.168.0.1,proto=6,tos=0,ttl=64,frag=no),tcp(src=80,dst=1234)'
+
+    ovs-appctl time/warp 1000
+done
+
+ovs-appctl time/warp 10000
+
+OVS_VSWITCHD_STOP
+ovs-appctl -t test-netflow exit
+
+# Count the number of reported packets:
+# - From source to destination before MAC learning kicks in (just one).
+# - From source to destination after that.
+# - From destination to source.
+n_learn=0
+n_in=0
+n_out=0
+n_other=0
+n_recs=0
+none=0
+while read line; do
+    pkts=`echo "$line" | sed 's/.*, \([[0-9]]*\) pkts,.*/\1/'`
+    case $pkts in
+         [[0-9]]*) ;;
+        *) continue ;;
+    esac
+
+    case $line in
+        "rec: 192.168.0.1 > 192.168.0.2, if 1 > 65535, "*" pkts, "*" bytes, TCP 1234 > 80, time "*)
+            counter=n_learn
+           ;;
+       "rec: 192.168.0.1 > 192.168.0.2, if 1 > 2, "*" pkts, "*" bytes, TCP 1234 > 80, time "*)
+           counter=n_in
+           ;;
+       "rec: 192.168.0.2 > 192.168.0.1, if 2 > 1, "*" pkts, "*" bytes, TCP 80 > 1234, time "*)
+           counter=n_out
+           ;;
+       *)
+           counter=n_other
+           ;;
+    esac
+    eval $counter=\`expr \$$counter + \$pkts\`
+    n_recs=`expr $n_recs + 1`
+done < netflow.log
+
+# There should be exactly 1 MAC learning packet,
+# exactly 59 other packets in that direction,
+# and exactly 60 packets in the other direction.
+AT_CHECK([echo $n_learn $n_in $n_out $n_other], [0], [1 59 60 0
+])
+
+# There should be 1 expiration for MAC learning,
+# at least 5 active and a final expiration in one direction,
+# and at least 5 active and a final expiration in the other direction.
+echo $n_recs
+AT_CHECK([test $n_recs -ge 13])
+
+AT_CLEANUP
diff --git a/tests/test-netflow.c b/tests/test-netflow.c
new file mode 100644 (file)
index 0000000..5a88ada
--- /dev/null
@@ -0,0 +1,323 @@
+/*
+ * Copyright (c) 2011 Nicira Networks.
+ *
+ * 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.
+ */
+
+#include <config.h>
+
+#include <errno.h>
+#include <getopt.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "command-line.h"
+#include "daemon.h"
+#include "netflow.h"
+#include "ofpbuf.h"
+#include "packets.h"
+#include "poll-loop.h"
+#include "socket-util.h"
+#include "unixctl.h"
+#include "util.h"
+#include "vlog.h"
+
+static void usage(void) NO_RETURN;
+static void parse_options(int argc, char *argv[]);
+
+static unixctl_cb_func test_netflow_exit;
+
+static void
+print_netflow(struct ofpbuf *buf)
+{
+    const struct netflow_v5_header *hdr;
+    int i;
+
+    hdr = ofpbuf_try_pull(buf, sizeof *hdr);
+    if (!hdr) {
+        printf("truncated NetFlow packet header\n");
+        return;
+    }
+    printf("header: v%"PRIu16", "
+           "uptime %"PRIu32", "
+           "now %"PRIu32".%09"PRIu32", "
+           "seq %"PRIu32", "
+           "engine %"PRIu8",%"PRIu8,
+           ntohs(hdr->version),
+           ntohl(hdr->sysuptime),
+           ntohl(hdr->unix_secs), ntohl(hdr->unix_nsecs),
+           ntohl(hdr->flow_seq),
+           hdr->engine_type, hdr->engine_id);
+    if (hdr->sampling_interval != htons(0)) {
+        printf(", interval %"PRIu16, ntohs(hdr->sampling_interval));
+    }
+    putchar('\n');
+
+    for (i = 0; i < ntohs(hdr->count); i++) {
+        struct netflow_v5_record *rec;
+
+        rec = ofpbuf_try_pull(buf, sizeof *rec);
+        if (!rec) {
+            printf("truncated NetFlow records\n");
+            return;
+        }
+
+        printf("rec: "IP_FMT" > "IP_FMT,
+               IP_ARGS(&rec->src_addr), IP_ARGS(&rec->dst_addr));
+
+        printf(", if %"PRIu16" > %"PRIu16,
+               ntohs(rec->input), ntohs(rec->output));
+
+        printf(", %"PRIu32" pkts, %"PRIu32" bytes",
+               ntohl(rec->packet_count), ntohl(rec->byte_count));
+
+        switch (rec->ip_proto) {
+        case IPPROTO_TCP:
+            printf(", TCP %"PRIu16" > %"PRIu16,
+                   ntohs(rec->src_port), ntohs(rec->dst_port));
+            if (rec->tcp_flags) {
+                putchar(' ');
+                if (rec->tcp_flags & TCP_SYN) {
+                    putchar('S');
+                }
+                if (rec->tcp_flags & TCP_FIN) {
+                    putchar('F');
+                }
+                if (rec->tcp_flags & TCP_PSH) {
+                    putchar('P');
+                }
+                if (rec->tcp_flags & TCP_RST) {
+                    putchar('R');
+                }
+                if (rec->tcp_flags & TCP_URG) {
+                    putchar('U');
+                }
+                if (rec->tcp_flags & TCP_ACK) {
+                    putchar('.');
+                }
+                if (rec->tcp_flags & 0x40) {
+                    printf("[40]");
+                }
+                if (rec->tcp_flags & 0x80) {
+                    printf("[80]");
+                }
+            }
+            break;
+
+        case IPPROTO_UDP:
+            printf(", UDP %"PRIu16" > %"PRIu16,
+                   ntohs(rec->src_port), ntohs(rec->dst_port));
+            break;
+
+        case IPPROTO_ICMP:
+            printf(", ICMP %"PRIu16":%"PRIu16,
+                   ntohs(rec->dst_port) >> 8,
+                   ntohs(rec->dst_port) & 0xff);
+            if (rec->src_port != htons(0)) {
+                printf(", src_port=%"PRIu16, ntohs(rec->src_port));
+            }
+            break;
+
+        default:
+            printf(", proto %"PRIu8, rec->ip_proto);
+            break;
+        }
+
+        if (rec->ip_proto != IPPROTO_TCP && rec->tcp_flags != 0) {
+            printf(", flags %"PRIx8, rec->tcp_flags);
+        }
+
+        if (rec->ip_proto != IPPROTO_TCP &&
+            rec->ip_proto != IPPROTO_UDP &&
+            rec->ip_proto != IPPROTO_ICMP) {
+            if (rec->src_port != htons(0)) {
+                printf(", src_port %"PRIu16, ntohs(rec->src_port));
+            }
+            if (rec->dst_port != htons(0)) {
+                printf(", dst_port %"PRIu16, ntohs(rec->dst_port));
+            }
+        }
+
+        if (rec->ip_tos) {
+            printf(", TOS %"PRIx8, rec->ip_tos);
+        }
+
+        printf(", time %"PRIu32"...%"PRIu32,
+               ntohl(rec->init_time), ntohl(rec->used_time));
+
+        if (rec->nexthop != htonl(0)) {
+            printf(", nexthop "IP_FMT, IP_ARGS(&rec->nexthop));
+        }
+        if (rec->src_as != htons(0) || rec->dst_as != htons(0)) {
+            printf(", AS %"PRIu16" > %"PRIu16,
+                   ntohs(rec->src_as), ntohs(rec->dst_as));
+        }
+        if (rec->src_mask != 0 || rec->dst_mask != 0) {
+            printf(", mask %"PRIu8" > %"PRIu8, rec->src_mask, rec->dst_mask);
+        }
+        if (rec->pad1) {
+            printf(", pad1 %"PRIu8, rec->pad1);
+        }
+        if (rec->pad[0] || rec->pad[1]) {
+            printf(", pad %"PRIu8", %"PRIu8, rec->pad[0], rec->pad[1]);
+        }
+        putchar('\n');
+    }
+
+    if (buf->size) {
+        printf("%zu extra bytes after last record\n", buf->size);
+    }
+}
+
+int
+main(int argc, char *argv[])
+{
+    struct unixctl_server *server;
+    enum { MAX_RECV = 1500 };
+    const char *target;
+    struct ofpbuf buf;
+    bool exiting = false;
+    int error;
+    int sock;
+    int fd;
+    int n;
+
+    proctitle_init(argc, argv);
+    set_program_name(argv[0]);
+    parse_options(argc, argv);
+
+    if (argc - optind != 1) {
+        ovs_fatal(0, "exactly one non-option argument required "
+                  "(use --help for help)");
+    }
+    target = argv[optind];
+
+    sock = inet_open_passive(SOCK_DGRAM, target, 0, NULL);
+    if (sock < 0) {
+        ovs_fatal(0, "%s: failed to open (%s)", argv[1], strerror(-sock));
+    }
+
+    /* Daemonization will close stdout but we really want to keep it, so make a
+     * copy. */
+    fd = dup(STDOUT_FILENO);
+
+    daemonize_start();
+
+    error = unixctl_server_create(NULL, &server);
+    if (error) {
+        ovs_fatal(error, "failed to create unixctl server");
+    }
+    unixctl_command_register("exit", "", 0, 0, test_netflow_exit, &exiting);
+
+    daemonize_complete();
+
+    /* Now get stdout back. */
+    dup2(fd, STDOUT_FILENO);
+
+    ofpbuf_init(&buf, MAX_RECV);
+    n = 0;
+    for (;;) {
+        int retval;
+
+        unixctl_server_run(server);
+
+        ofpbuf_clear(&buf);
+        do {
+            retval = read(sock, buf.data, buf.allocated);
+        } while (retval < 0 && errno == EINTR);
+        if (retval > 0) {
+            ofpbuf_put_uninit(&buf, retval);
+            if (n++ > 0) {
+                putchar('\n');
+            }
+            print_netflow(&buf);
+            fflush(stdout);
+        }
+
+        if (exiting) {
+            break;
+        }
+
+        poll_fd_wait(sock, POLLIN);
+        unixctl_server_wait(server);
+        poll_block();
+    }
+
+    return 0;
+}
+
+static void
+parse_options(int argc, char *argv[])
+{
+    enum {
+        DAEMON_OPTION_ENUMS
+    };
+    static struct option long_options[] = {
+        {"verbose", optional_argument, NULL, 'v'},
+        {"help", no_argument, NULL, 'h'},
+        DAEMON_LONG_OPTIONS,
+        {NULL, 0, NULL, 0},
+    };
+    char *short_options = long_options_to_short_options(long_options);
+
+    for (;;) {
+        int c = getopt_long(argc, argv, short_options, long_options, NULL);
+        if (c == -1) {
+            break;
+        }
+
+        switch (c) {
+        case 'h':
+            usage();
+
+        case 'v':
+            vlog_set_verbosity(optarg);
+            break;
+
+        DAEMON_OPTION_HANDLERS
+
+        case '?':
+            exit(EXIT_FAILURE);
+
+        default:
+            abort();
+        }
+    }
+    free(short_options);
+}
+
+static void
+usage(void)
+{
+    printf("%s: netflow collector test utility\n"
+           "usage: %s [OPTIONS] PORT[:IP]\n"
+           "where PORT is the UDP port to listen on and IP is optionally\n"
+           "the IP address to listen on.\n",
+           program_name, program_name);
+    daemon_usage();
+    vlog_usage();
+    printf("\nOther options:\n"
+           "  -h, --help                  display this help message\n");
+    exit(EXIT_SUCCESS);
+}
+
+static void
+test_netflow_exit(struct unixctl_conn *conn,
+                  int argc OVS_UNUSED, const char *argv[] OVS_UNUSED,
+                  void *exiting_)
+{
+    bool *exiting = exiting_;
+    *exiting = true;
+    unixctl_command_reply(conn, 200, "");
+}