]> git.proxmox.com Git - mirror_ovs.git/blobdiff - ovn/utilities/ovn-trace.c
lib: Move lib/poll-loop.h to include/openvswitch
[mirror_ovs.git] / ovn / utilities / ovn-trace.c
index 7863f70d64845c8c22ebf6f33ebcc085f31d96a4..7ff4a2682557f801b02804ed529a83c53e599542 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016 Nicira, Inc.
+ * Copyright (c) 2016, 2017 Nicira, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
 #include "flow.h"
 #include "nx-match.h"
 #include "openvswitch/dynamic-string.h"
+#include "openvswitch/json.h"
 #include "openvswitch/ofp-actions.h"
+#include "openvswitch/ofp-print.h"
+#include "openvswitch/vconn.h"
 #include "openvswitch/vlog.h"
 #include "ovn/actions.h"
 #include "ovn/expr.h"
 #include "ovn/lex.h"
+#include "ovn/lib/acl-log.h"
 #include "ovn/lib/logical-fields.h"
+#include "ovn/lib/ovn-l7.h"
 #include "ovn/lib/ovn-sb-idl.h"
 #include "ovn/lib/ovn-util.h"
 #include "ovsdb-idl.h"
-#include "poll-loop.h"
+#include "openvswitch/poll-loop.h"
 #include "stream-ssl.h"
 #include "stream.h"
 #include "unixctl.h"
@@ -62,6 +67,20 @@ static bool summary;
 /* --minimal: Show a trace with only minimal information. */
 static bool minimal;
 
+/* --ovs: OVS instance to contact to get OpenFlow flows. */
+static const char *ovs;
+static struct vconn *vconn;
+
+/* --ct: Connection tracking state to use for ct_next() actions. */
+static uint32_t *ct_states;
+static size_t n_ct_states;
+static size_t ct_state_idx;
+
+/* --friendly-names, --no-friendly-names: Whether to substitute human-friendly
+ * port and datapath names for the awkward UUIDs typically used in the actual
+ * logical flows. */
+static bool use_friendly_names = true;
+
 OVS_NO_RETURN static void usage(void);
 static void parse_options(int argc, char *argv[]);
 static char *trace(const char *datapath, const char *flow);
@@ -76,7 +95,6 @@ main(int argc, char *argv[])
     service_start(&argc, &argv);
     fatal_ignore_sigpipe();
     vlog_set_levels_from_string_assert("reconnect:warn");
-    sbrec_init();
 
     /* Parse command line. */
     parse_options(argc, argv);
@@ -143,6 +161,30 @@ main(int argc, char *argv[])
     }
 }
 
+static char *
+default_ovs(void)
+{
+    return xasprintf("unix:%s/br-int.mgmt", ovs_rundir());
+}
+
+static void
+parse_ct_option(const char *state_s_)
+{
+    uint32_t state;
+    struct ds ds = DS_EMPTY_INITIALIZER;
+
+    if (!parse_ct_state(state_s_, CS_TRACKED, &state, &ds)) {
+        ovs_fatal(0, "%s", ds_cstr(&ds));
+    }
+    if (!validate_ct_state(state, &ds)) {
+        VLOG_WARN("%s", ds_cstr(&ds));
+    }
+    ds_destroy(&ds);
+
+    ct_states = xrealloc(ct_states, (n_ct_states + 1) * sizeof *ct_states);
+    ct_states[n_ct_states++] = state;
+}
+
 static void
 parse_options(int argc, char *argv[])
 {
@@ -153,7 +195,12 @@ parse_options(int argc, char *argv[])
         OPT_SUMMARY,
         OPT_MINIMAL,
         OPT_ALL,
+        OPT_OVS,
+        OPT_CT,
+        OPT_FRIENDLY_NAMES,
+        OPT_NO_FRIENDLY_NAMES,
         DAEMON_OPTION_ENUMS,
+        SSL_OPTION_ENUMS,
         VLOG_OPTION_ENUMS
     };
     static const struct option long_options[] = {
@@ -163,6 +210,10 @@ parse_options(int argc, char *argv[])
         {"summary", no_argument, NULL, OPT_SUMMARY},
         {"minimal", no_argument, NULL, OPT_MINIMAL},
         {"all", no_argument, NULL, OPT_ALL},
+        {"ovs", optional_argument, NULL, OPT_OVS},
+        {"ct", required_argument, NULL, OPT_CT},
+        {"friendly-names", no_argument, NULL, OPT_FRIENDLY_NAMES},
+        {"no-friendly-names", no_argument, NULL, OPT_NO_FRIENDLY_NAMES},
         {"help", no_argument, NULL, 'h'},
         {"version", no_argument, NULL, 'V'},
         DAEMON_LONG_OPTIONS,
@@ -206,6 +257,22 @@ parse_options(int argc, char *argv[])
             detailed = summary = minimal = true;
             break;
 
+        case OPT_OVS:
+            ovs = optarg ? optarg : default_ovs();
+            break;
+
+        case OPT_CT:
+            parse_ct_option(optarg);
+            break;
+
+        case OPT_FRIENDLY_NAMES:
+            use_friendly_names = true;
+            break;
+
+        case OPT_NO_FRIENDLY_NAMES:
+            use_friendly_names = false;
+            break;
+
         case 'h':
             usage();
 
@@ -244,18 +311,28 @@ usage(void)
 usage: %s [OPTIONS] DATAPATH MICROFLOW\n\
        %s [OPTIONS] --detach\n\
 \n\
-Options:\n\
-  --db=DATABASE               connect to DATABASE\n\
-                              (default: %s)\n",
-           program_name, program_name, program_name, default_sb_db());
+Output format options:\n\
+  --detailed              table-by-table \"backtrace\" (default)\n\
+  --summary               less detailed, more parseable\n\
+  --minimal               minimum to explain externally visible behavior\n\
+  --all                   provide all forms of output\n\
+Output style options:\n\
+  --no-friendly-names     do not substitute human friendly names for UUIDs\n",
+           program_name, program_name, program_name);
     daemon_usage();
     vlog_usage();
     printf("\n\
 Other options:\n\
-  --unixctl=SOCKET            set control socket name\n\
-  -h, --help                  display this help message\n\
-  -V, --version               display version information\n");
+  --db=DATABASE           connect to DATABASE\n\
+                          (default: %s)\n\
+  --ovs[=REMOTE]          obtain corresponding OpenFlow flows from REMOTE\n\
+                          (default: %s)\n\
+  --unixctl=SOCKET        set control socket name\n\
+  -h, --help              display this help message\n\
+  -V, --version           display version information\n",
+           default_sb_db(), default_ovs());
     stream_usage("database", true, true, false);
+    vconn_usage(true, false, false);
     exit(EXIT_SUCCESS);
 }
 \f
@@ -264,6 +341,8 @@ struct ovntrace_datapath {
     struct uuid sb_uuid;
     struct uuid nb_uuid;
     char *name;
+    char *name2;
+    char *friendly_name;
     uint32_t tunnel_key;
 
     struct ovs_list mcgroups;   /* Contains "struct ovntrace_mcgroup"s. */
@@ -272,14 +351,20 @@ struct ovntrace_datapath {
     size_t n_flows, allocated_flows;
 
     struct hmap mac_bindings;   /* Contains "struct ovntrace_mac_binding"s. */
+
+    bool has_local_l3gateway;
 };
 
 struct ovntrace_port {
     struct ovntrace_datapath *dp;
+    struct uuid uuid;
     char *name;
+    char *name2;
+    const char *friendly_name;
     char *type;
     uint16_t tunnel_key;
     struct ovntrace_port *peer; /* Patch ports only. */
+    struct ovntrace_port *distributed_port; /* chassisredirect ports only. */
 };
 
 struct ovntrace_mcgroup {
@@ -294,10 +379,9 @@ struct ovntrace_mcgroup {
     size_t n_ports;
 };
 
-enum ovntrace_pipeline { P_INGRESS, P_EGRESS };
-
 struct ovntrace_flow {
-    enum ovntrace_pipeline pipeline;
+    struct uuid uuid;
+    enum ovnact_pipeline pipeline;
     int table_id;
     char *stage_name;
     char *source;
@@ -333,6 +417,11 @@ static struct shash symtab;
 /* Address sets. */
 static struct shash address_sets;
 
+/* DHCP options. */
+static struct hmap dhcp_opts;   /* Contains "struct gen_opts_map"s. */
+static struct hmap dhcpv6_opts; /* Contains "struct gen_opts_map"s. */
+static struct hmap nd_ra_opts; /* Contains "struct gen_opts_map"s. */
+
 static struct ovntrace_datapath *
 ovntrace_datapath_find_by_sb_uuid(const struct uuid *sb_uuid)
 {
@@ -349,19 +438,26 @@ ovntrace_datapath_find_by_sb_uuid(const struct uuid *sb_uuid)
 static const struct ovntrace_datapath *
 ovntrace_datapath_find_by_name(const char *name)
 {
-    struct uuid uuid;
-    bool is_uuid = uuid_from_string(&uuid, name);
-
     struct ovntrace_datapath *dp;
     HMAP_FOR_EACH (dp, sb_uuid_node, &datapaths) {
         if (!strcmp(name, dp->name)
-            || (is_uuid
-                && (uuid_equals(&uuid, &dp->sb_uuid) ||
-                    uuid_equals(&uuid, &dp->nb_uuid)))) {
+            || (dp->name2 && !strcmp(name, dp->name2))) {
             return dp;
         }
     }
-    return NULL;
+
+    struct ovntrace_datapath *match = NULL;
+    HMAP_FOR_EACH (dp, sb_uuid_node, &datapaths) {
+        if (uuid_is_partial_match(&dp->sb_uuid, name) >= 4 ||
+            uuid_is_partial_match(&dp->nb_uuid, name) >= 4) {
+            if (match) {
+                VLOG_WARN("name \"%s\" matches multiple datapaths", name);
+                return NULL;
+            }
+            match = dp;
+        }
+    }
+    return match;
 }
 
 static const struct ovntrace_port *
@@ -378,6 +474,16 @@ ovntrace_port_find_by_key(const struct ovntrace_datapath *dp,
     return NULL;
 }
 
+static const char *
+ovntrace_port_key_to_name(const struct ovntrace_datapath *dp,
+                          uint16_t key)
+{
+    const struct ovntrace_port *port = ovntrace_port_find_by_key(dp, key);
+    return (port ? port->friendly_name
+            : !key ? ""
+            : "(unnamed)");
+}
+
 static const struct ovntrace_mcgroup *
 ovntrace_mcgroup_find_by_key(const struct ovntrace_datapath *dp,
                              uint16_t tunnel_key)
@@ -418,6 +524,17 @@ ovntrace_mac_binding_find(const struct ovntrace_datapath *dp,
     return NULL;
 }
 
+/* If 's' ends with a UUID, returns a copy of it with the UUID truncated to
+ * just the first 6 characters; otherwise, returns a copy of 's'. */
+static char *
+shorten_uuid(const char *s)
+{
+    size_t len = strlen(s);
+    return (len >= UUID_LEN && uuid_is_partial_string(s + (len - UUID_LEN))
+            ? xmemdup0(s, len - (UUID_LEN - 6))
+            : xstrdup(s));
+}
+
 static void
 read_datapaths(void)
 {
@@ -438,6 +555,11 @@ read_datapaths(void)
                     ? xstrdup(name)
                     : xasprintf(UUID_FMT, UUID_ARGS(&dp->nb_uuid)));
 
+        dp->name2 = nullable_xstrdup(smap_get(ids, "name2"));
+        dp->friendly_name = (!use_friendly_names
+                             ? xasprintf(UUID_FMT, UUID_ARGS(&dp->nb_uuid))
+                             : shorten_uuid(dp->name2 ? dp->name2 : dp->name));
+
         dp->tunnel_key = sbdb->tunnel_key;
 
         ovs_list_init(&dp->mcgroups);
@@ -468,10 +590,16 @@ read_ports(void)
             continue;
         }
         port->dp = dp;
+        port->uuid = sbpb->header_.uuid;
         port->name = xstrdup(port_name);
         port->type = xstrdup(sbpb->type);
         port->tunnel_key = sbpb->tunnel_key;
 
+        port->name2 = nullable_xstrdup(smap_get(&sbpb->external_ids, "name"));
+        port->friendly_name = (!use_friendly_names ? xstrdup(port->name)
+                               : shorten_uuid(port->name2
+                                              ? port->name2 : port->name));
+
         if (!strcmp(sbpb->type, "patch")) {
             const char *peer_name = smap_get(&sbpb->options, "peer");
             if (peer_name) {
@@ -482,6 +610,27 @@ read_ports(void)
                     port->peer->peer = port;
                 }
             }
+        } else if (!strcmp(sbpb->type, "l3gateway")) {
+            /* Treat all gateways as local for our purposes. */
+            dp->has_local_l3gateway = true;
+        }
+    }
+
+    SBREC_PORT_BINDING_FOR_EACH (sbpb, ovnsb_idl) {
+        if (!strcmp(sbpb->type, "chassisredirect")) {
+            struct ovntrace_port *port
+                = shash_find_data(&ports, sbpb->logical_port);
+            if (port) {
+                const char *distributed_name = smap_get(&sbpb->options,
+                                                       "distributed-port");
+                if (distributed_name) {
+                    struct ovntrace_port *distributed_port
+                        = shash_find_data(&ports, distributed_name);
+                    if (distributed_port && distributed_port->dp == port->dp) {
+                        port->distributed_port = distributed_port;
+                    }
+                }
+            }
         }
     }
 }
@@ -547,12 +696,31 @@ read_address_sets(void)
 
     const struct sbrec_address_set *sbas;
     SBREC_ADDRESS_SET_FOR_EACH (sbas, ovnsb_idl) {
-        expr_macros_add(&address_sets, sbas->name,
-                        (const char *const *) sbas->addresses,
-                        sbas->n_addresses);
+        expr_addr_sets_add(&address_sets, sbas->name,
+                           (const char *const *) sbas->addresses,
+                           sbas->n_addresses);
     }
 }
 
+static bool
+ovntrace_is_chassis_resident(const void *aux OVS_UNUSED,
+                             const char *port_name)
+{
+    if (port_name[0] == '\0') {
+        return true;
+    }
+
+    const struct ovntrace_port *port = shash_find_data(&ports, port_name);
+    if (port) {
+        /* Since ovntrace is not chassis specific, assume any port
+         * that exists is resident. */
+        return true;
+    }
+
+    VLOG_WARN("%s: unknown logical port\n", port_name);
+    return false;
+}
+
 static int
 compare_flow(const void *a_, const void *b_)
 {
@@ -562,8 +730,8 @@ compare_flow(const void *a_, const void *b_)
     const struct ovntrace_flow *b = *bp;
 
     if (a->pipeline != b->pipeline) {
-        /* Sort P_INGRESS before P_EGRESS. */
-        return a->pipeline == P_EGRESS ? 1 : -1;
+        /* Sort OVNACT_P_INGRESS before OVNACT_P_EGRESS. */
+        return a->pipeline == OVNACT_P_EGRESS ? 1 : -1;
     } else if (a->table_id != b->table_id) {
         /* Sort in increasing order of table_id. */
         return a->table_id > b->table_id ? 1 : -1;
@@ -576,6 +744,40 @@ compare_flow(const void *a_, const void *b_)
     }
 }
 
+static char *
+ovntrace_make_names_friendly(const char *in)
+{
+    if (!use_friendly_names) {
+        return xstrdup(in);
+    }
+
+    struct ds out = DS_EMPTY_INITIALIZER;
+    while (*in) {
+        struct lex_token token;
+        const char *start;
+        const char *next;
+
+        next = lex_token_parse(&token, in, &start);
+        if (token.type == LEX_T_STRING) {
+            const struct ovntrace_port *port = shash_find_data(&ports,
+                                                               token.s);
+            if (port) {
+                ds_put_buffer(&out, in, start - in);
+                json_string_escape(port->friendly_name, &out);
+            } else {
+                ds_put_buffer(&out, in, next - in);
+            }
+        } else if (token.type != LEX_T_END) {
+            ds_put_buffer(&out, in, next - in);
+        } else {
+            break;
+        }
+        lex_token_destroy(&token);
+        in = next;
+    }
+    return ds_steal_cstr(&out);
+}
+
 static void
 read_flows(void)
 {
@@ -603,8 +805,13 @@ read_flows(void)
 
         struct ovnact_parse_params pp = {
             .symtab = &symtab,
-            .dhcp_opts = NULL /* XXX */,
-            .n_tables = 16,
+            .dhcp_opts = &dhcp_opts,
+            .dhcpv6_opts = &dhcpv6_opts,
+            .nd_ra_opts = &nd_ra_opts,
+            .pipeline = (!strcmp(sblf->pipeline, "ingress")
+                         ? OVNACT_P_INGRESS
+                         : OVNACT_P_EGRESS),
+            .n_tables = 24,
             .cur_ltable = sblf->table_id,
         };
         uint64_t stub[1024 / 8];
@@ -629,20 +836,21 @@ read_flows(void)
             continue;
         }
         if (match) {
-            match = expr_simplify(match);
+            match = expr_simplify(match, ovntrace_is_chassis_resident, NULL);
         }
 
         struct ovntrace_flow *flow = xzalloc(sizeof *flow);
+        flow->uuid = sblf->header_.uuid;
         flow->pipeline = (!strcmp(sblf->pipeline, "ingress")
-                          ? P_INGRESS
-                          : P_EGRESS);
+                          ? OVNACT_P_INGRESS
+                          : OVNACT_P_EGRESS);
         flow->table_id = sblf->table_id;
         flow->stage_name = nullable_xstrdup(smap_get(&sblf->external_ids,
                                                      "stage-name"));
         flow->source = nullable_xstrdup(smap_get(&sblf->external_ids,
                                                  "source"));
         flow->priority = sblf->priority;
-        flow->match_s = xstrdup(sblf->match);
+        flow->match_s = ovntrace_make_names_friendly(sblf->match);
         flow->match = match;
         flow->ovnacts_len = ovnacts.size;
         flow->ovnacts = ofpbuf_steal_data(&ovnacts);
@@ -660,6 +868,26 @@ read_flows(void)
     }
 }
 
+static void
+read_gen_opts(void)
+{
+    hmap_init(&dhcp_opts);
+    const struct sbrec_dhcp_options *sdo;
+    SBREC_DHCP_OPTIONS_FOR_EACH (sdo, ovnsb_idl) {
+        dhcp_opt_add(&dhcp_opts, sdo->name, sdo->code, sdo->type);
+    }
+
+
+    hmap_init(&dhcpv6_opts);
+    const struct sbrec_dhcpv6_options *sdo6;
+    SBREC_DHCPV6_OPTIONS_FOR_EACH(sdo6, ovnsb_idl) {
+       dhcp_opt_add(&dhcpv6_opts, sdo6->name, sdo6->code, sdo6->type);
+    }
+
+    hmap_init(&nd_ra_opts);
+    nd_ra_opts_init(&nd_ra_opts);
+}
+
 static void
 read_mac_bindings(void)
 {
@@ -708,10 +936,54 @@ read_db(void)
     read_ports();
     read_mcgroups();
     read_address_sets();
+    read_gen_opts();
     read_flows();
     read_mac_bindings();
 }
 
+static const struct ovntrace_port *
+ovntrace_port_lookup_by_name(const char *name)
+{
+    const struct ovntrace_port *port = shash_find_data(&ports, name);
+    if (port) {
+        return port;
+    }
+
+    const struct ovntrace_port *match = NULL;
+
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &ports) {
+        port = node->data;
+
+        if (port->name2 && !strcmp(port->name2, name)) {
+            if (match) {
+                VLOG_WARN("name \"%s\" matches multiple ports", name);
+                return NULL;
+            }
+            match = port;
+        }
+    }
+
+    if (uuid_is_partial_string(name) >= 4) {
+        SHASH_FOR_EACH (node, &ports) {
+            port = node->data;
+
+            struct uuid name_uuid;
+            if (uuid_is_partial_match(&port->uuid, name)
+                || (uuid_from_string(&name_uuid, port->name)
+                    && uuid_is_partial_match(&name_uuid, name))) {
+                if (match && match != port) {
+                    VLOG_WARN("name \"%s\" matches multiple ports", name);
+                    return NULL;
+                }
+                match = port;
+            }
+        }
+    }
+
+    return match;
+}
+
 static bool
 ovntrace_lookup_port(const void *dp_, const char *port_name,
                      unsigned int *portp)
@@ -723,7 +995,7 @@ ovntrace_lookup_port(const void *dp_, const char *port_name,
         return true;
     }
 
-    const struct ovntrace_port *port = shash_find_data(&ports, port_name);
+    const struct ovntrace_port *port = ovntrace_port_lookup_by_name(port_name);
     if (port) {
         if (port->dp == dp) {
             *portp = port->tunnel_key;
@@ -738,14 +1010,14 @@ ovntrace_lookup_port(const void *dp_, const char *port_name,
         return true;
     }
 
-    VLOG_WARN("%s: unknown logical port\n", port_name);
+    VLOG_WARN("%s: unknown logical port", port_name);
     return false;
 }
 
 static const struct ovntrace_flow *
 ovntrace_flow_lookup(const struct ovntrace_datapath *dp,
                      const struct flow *uflow,
-                     uint8_t table_id, enum ovntrace_pipeline pipeline)
+                     uint8_t table_id, enum ovnact_pipeline pipeline)
 {
     for (size_t i = 0; i < dp->n_flows; i++) {
         const struct ovntrace_flow *flow = dp->flows[i];
@@ -758,14 +1030,30 @@ ovntrace_flow_lookup(const struct ovntrace_datapath *dp,
     return NULL;
 }
 
+static char *
+ovntrace_stage_name(const struct ovntrace_datapath *dp,
+                    uint8_t table_id, enum ovnact_pipeline pipeline)
+{
+    for (size_t i = 0; i < dp->n_flows; i++) {
+        const struct ovntrace_flow *flow = dp->flows[i];
+        if (flow->pipeline == pipeline && flow->table_id == table_id) {
+            return xstrdup(flow->stage_name);;
+        }
+    }
+    return NULL;
+}
 
+/* Type of a node within a trace. */
 enum ovntrace_node_type {
+    /* Nodes that may have children (nonterminal nodes). */
     OVNTRACE_NODE_OUTPUT,
     OVNTRACE_NODE_MODIFY,
-    OVNTRACE_NODE_PIPELINE,
-    OVNTRACE_NODE_TABLE,
     OVNTRACE_NODE_ACTION,
     OVNTRACE_NODE_ERROR,
+
+    /* Nodes that never have children (terminal nodes). */
+    OVNTRACE_NODE_PIPELINE,
+    OVNTRACE_NODE_TABLE,
     OVNTRACE_NODE_TRANSFORMATION
 };
 
@@ -792,11 +1080,13 @@ struct ovntrace_node {
     struct ovs_list node;       /* In parent. */
 
     enum ovntrace_node_type type;
-    const char *name;
+    char *name;
     bool always_indent;
     struct ovs_list subs;       /* List of children. */
 };
 
+static void ovntrace_node_destroy(struct ovntrace_node *);
+
 static struct ovntrace_node * OVS_PRINTF_FORMAT(3, 4)
 ovntrace_node_append(struct ovs_list *super, enum ovntrace_node_type type,
                      const char *format, ...)
@@ -828,6 +1118,29 @@ ovntrace_node_clone(const struct ovs_list *old, struct ovs_list *new)
     }
 }
 
+static void
+ovntrace_node_list_destroy(struct ovs_list *list)
+{
+    if (list) {
+        struct ovntrace_node *node, *next;
+
+        LIST_FOR_EACH_SAFE (node, next, node, list) {
+            ovs_list_remove(&node->node);
+            ovntrace_node_destroy(node);
+        }
+    }
+}
+
+static void
+ovntrace_node_destroy(struct ovntrace_node *node)
+{
+    if (node) {
+        ovntrace_node_list_destroy(&node->subs);
+        free(node->name);
+        free(node);
+    }
+}
+
 static void
 ovntrace_node_print_details(struct ds *output,
                             const struct ovs_list *nodes, int level)
@@ -838,7 +1151,9 @@ ovntrace_node_print_details(struct ds *output,
             continue;
         }
 
-        bool more = sub->node.next != nodes || sub->always_indent || ovntrace_node_type_is_terminal(sub->type);
+        bool more = (sub->node.next != nodes
+                     || sub->always_indent
+                     || ovntrace_node_type_is_terminal(sub->type));
         bool title = (sub->type == OVNTRACE_NODE_PIPELINE ||
                       sub->type == OVNTRACE_NODE_TRANSFORMATION);
         if (title) {
@@ -864,8 +1179,10 @@ ovntrace_node_prune_summary(struct ovs_list *nodes)
         ovntrace_node_prune_summary(&sub->subs);
         if (sub->type == OVNTRACE_NODE_MODIFY ||
             sub->type == OVNTRACE_NODE_TABLE) {
+            /* Replace 'sub' by its children, if any, */
             ovs_list_remove(&sub->node);
             ovs_list_splice(&next->node, sub->subs.next, &sub->subs);
+            ovntrace_node_destroy(sub);
         }
     }
 }
@@ -906,8 +1223,10 @@ ovntrace_node_prune_hard(struct ovs_list *nodes)
             sub->type == OVNTRACE_NODE_PIPELINE ||
             sub->type == OVNTRACE_NODE_TABLE ||
             sub->type == OVNTRACE_NODE_OUTPUT) {
+            /* Replace 'sub' by its children, if any, */
             ovs_list_remove(&sub->node);
             ovs_list_splice(&next->node, sub->subs.next, &sub->subs);
+            ovntrace_node_destroy(sub);
         }
     }
 }
@@ -935,8 +1254,9 @@ execute_load(const struct ovnact_load *load,
             ovnacts_format(&load->ovnact, OVNACT_LOAD_SIZE, &s);
             ds_chomp(&s, ';');
 
-            ovntrace_node_append(super, OVNTRACE_NODE_MODIFY, "%s",
-                                 ds_cstr(&s));
+            char *friendly = ovntrace_make_names_friendly(ds_cstr(&s));
+            ovntrace_node_append(super, OVNTRACE_NODE_MODIFY, "%s", friendly);
+            free(friendly);
 
             ds_destroy(&s);
         }
@@ -962,7 +1282,7 @@ summarize_move(const struct mf_subfield *rsrc,
         if (rsrc->ofs == 0 && rsrc->n_bits >= rsrc->field->n_bits) {
             union mf_value value;
             mf_get_value(rsrc->field, uflow, &value);
-            mf_format(rsrc->field, &value, NULL, &s);
+            mf_format(rsrc->field, &value, NULL, NULL, &s);
         } else {
             union mf_subvalue cst;
             mf_read_subfield(rsrc, uflow, &cst);
@@ -998,17 +1318,17 @@ execute_exchange(const struct ovnact_move *move, struct flow *uflow,
 
 static void
 trace__(const struct ovntrace_datapath *dp, struct flow *uflow,
-        uint8_t table_id, enum ovntrace_pipeline pipeline,
+        uint8_t table_id, enum ovnact_pipeline pipeline,
         struct ovs_list *super);
 
 static void
 trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
               const struct ovntrace_datapath *dp, struct flow *uflow,
-              uint8_t table_id, enum ovntrace_pipeline pipeline,
+              uint8_t table_id, enum ovnact_pipeline pipeline,
               struct ovs_list *super);
 static void
 execute_output(const struct ovntrace_datapath *dp, struct flow *uflow,
-               enum ovntrace_pipeline pipeline, struct ovs_list *super)
+               enum ovnact_pipeline pipeline, struct ovs_list *super)
 {
     uint16_t key = uflow->regs[MFF_LOG_OUTPORT - MFF_REG0];
     if (!key) {
@@ -1020,7 +1340,7 @@ execute_output(const struct ovntrace_datapath *dp, struct flow *uflow,
     const struct ovntrace_port *port = ovntrace_port_find_by_key(dp, key);
     const struct ovntrace_mcgroup *mcgroup = ovntrace_mcgroup_find_by_key(dp,
                                                                           key);
-    const char *out_name = (port ? port->name
+    const char *out_name = (port ? port->friendly_name
                             : mcgroup ? mcgroup->name
                             : "(unnamed)");
     if (!port && !mcgroup) {
@@ -1029,7 +1349,7 @@ execute_output(const struct ovntrace_datapath *dp, struct flow *uflow,
                              key);
     }
 
-    if (pipeline == P_EGRESS) {
+    if (pipeline == OVNACT_P_EGRESS) {
         ovntrace_node_append(super, OVNTRACE_NODE_OUTPUT,
                              "/* output to \"%s\", type \"%s\" */",
                              out_name, port ? port->type : "");
@@ -1039,12 +1359,12 @@ execute_output(const struct ovntrace_datapath *dp, struct flow *uflow,
             struct ovntrace_node *node = ovntrace_node_append(
                 super, OVNTRACE_NODE_PIPELINE,
                 "ingress(dp=\"%s\", inport=\"%s\")",
-                peer->dp->name, peer->name);
+                peer->dp->friendly_name, peer->friendly_name);
 
             struct flow new_uflow = *uflow;
             new_uflow.regs[MFF_LOG_INPORT - MFF_REG0] = peer->tunnel_key;
             new_uflow.regs[MFF_LOG_OUTPORT - MFF_REG0] = 0;
-            trace__(peer->dp, &new_uflow, 0, P_INGRESS, &node->subs);
+            trace__(peer->dp, &new_uflow, 0, OVNACT_P_INGRESS, &node->subs);
         } else {
             ovntrace_node_append(super, OVNTRACE_NODE_MODIFY,
                                  "output(\"%s\")", out_name);
@@ -1062,8 +1382,7 @@ execute_output(const struct ovntrace_datapath *dp, struct flow *uflow,
     }
 
     uint16_t in_key = uflow->regs[MFF_LOG_INPORT - MFF_REG0];
-    const struct ovntrace_port *inport = ovntrace_port_find_by_key(dp, in_key);
-    const char *inport_name = !in_key ? "" : inport ? inport->name : "(unnamed)";
+    const char *inport_name = ovntrace_port_key_to_name(dp, in_key);
     uint32_t flags = uflow->regs[MFF_LOG_FLAGS - MFF_REG0];
     bool allow_loopback = (flags & MLF_ALLOW_LOOPBACK) != 0;
 
@@ -1071,42 +1390,78 @@ execute_output(const struct ovntrace_datapath *dp, struct flow *uflow,
         struct ovntrace_node *mcnode = ovntrace_node_append(
             super, OVNTRACE_NODE_PIPELINE,
             "multicast(dp=\"%s\", mcgroup=\"%s\")",
-            dp->name, mcgroup->name);
+            dp->friendly_name, mcgroup->name);
         for (size_t i = 0; i < mcgroup->n_ports; i++) {
             const struct ovntrace_port *p = mcgroup->ports[i];
 
             struct ovntrace_node *node = ovntrace_node_append(
                 &mcnode->subs, OVNTRACE_NODE_PIPELINE,
                 "egress(dp=\"%s\", inport=\"%s\", outport=\"%s\")",
-                dp->name, inport_name, p->name);
+                dp->friendly_name, inport_name, p->friendly_name);
 
             if (p->tunnel_key != in_key || allow_loopback) {
                 node->always_indent = true;
 
                 egress_uflow.regs[MFF_LOG_OUTPORT - MFF_REG0] = p->tunnel_key;
-                trace__(dp, &egress_uflow, 0, P_EGRESS, &node->subs);
+                trace__(dp, &egress_uflow, 0, OVNACT_P_EGRESS, &node->subs);
             } else {
                 ovntrace_node_append(&node->subs, OVNTRACE_NODE_OUTPUT,
                                      "/* omitting output because inport == outport && !flags.loopback */");
             }
         }
-    } else if (port->tunnel_key != in_key || allow_loopback) {
+        return;
+    }
+
+    if (port && !strcmp(port->type, "chassisredirect")) {
+        if (port->distributed_port) {
+            ovntrace_node_append(super, OVNTRACE_NODE_OUTPUT,
+                                 "/* Replacing type \"%s\" outport \"%s\""
+                                 " with distributed port \"%s\". */",
+                                 port->type, port->friendly_name,
+                                 port->distributed_port->friendly_name);
+            port = port->distributed_port;
+            out_name = port->friendly_name;
+            egress_uflow.regs[MFF_LOG_OUTPORT - MFF_REG0] = port->tunnel_key;
+        } else {
+            ovntrace_node_append(super, OVNTRACE_NODE_ERROR,
+                                 "*** output to type \"%s\" port \"%s\""
+                                 " with no or invalid distributed port",
+                                 port->type, out_name);
+            return;
+        }
+    }
+
+    if ((port && port->tunnel_key != in_key) || allow_loopback) {
         struct ovntrace_node *node = ovntrace_node_append(
             super, OVNTRACE_NODE_PIPELINE,
             "egress(dp=\"%s\", inport=\"%s\", outport=\"%s\")",
-            dp->name, inport_name, out_name);
+            dp->friendly_name, inport_name, out_name);
 
-        trace__(dp, &egress_uflow, 0, P_EGRESS, &node->subs);
+        trace__(dp, &egress_uflow, 0, OVNACT_P_EGRESS, &node->subs);
     } else {
         ovntrace_node_append(super, OVNTRACE_NODE_OUTPUT,
                              "/* omitting output because inport == outport && !flags.loopback */");
     }
 }
 
+static void
+execute_clone(const struct ovnact_nest *on, const struct ovntrace_datapath *dp,
+              const struct flow *uflow, uint8_t table_id,
+              enum ovnact_pipeline pipeline, struct ovs_list *super)
+{
+    struct flow cloned_flow = *uflow;
+
+    struct ovntrace_node *node = ovntrace_node_append(
+        super, OVNTRACE_NODE_TRANSFORMATION, "clone");
+
+    trace_actions(on->nested, on->nested_len, dp, &cloned_flow,
+                  table_id, pipeline, &node->subs);
+}
+
 static void
 execute_arp(const struct ovnact_nest *on, const struct ovntrace_datapath *dp,
             const struct flow *uflow, uint8_t table_id,
-            enum ovntrace_pipeline pipeline, struct ovs_list *super)
+            enum ovnact_pipeline pipeline, struct ovs_list *super)
 {
     struct flow arp_flow = *uflow;
 
@@ -1134,7 +1489,7 @@ execute_arp(const struct ovnact_nest *on, const struct ovntrace_datapath *dp,
 static void
 execute_nd_na(const struct ovnact_nest *on, const struct ovntrace_datapath *dp,
               const struct flow *uflow, uint8_t table_id,
-              enum ovntrace_pipeline pipeline, struct ovs_list *super)
+              enum ovnact_pipeline pipeline, struct ovs_list *super)
 {
     struct flow na_flow = *uflow;
 
@@ -1154,6 +1509,31 @@ execute_nd_na(const struct ovnact_nest *on, const struct ovntrace_datapath *dp,
                   table_id, pipeline, &node->subs);
 }
 
+static void
+execute_nd_ns(const struct ovnact_nest *on, const struct ovntrace_datapath *dp,
+              const struct flow *uflow, uint8_t table_id,
+              enum ovnact_pipeline pipeline, struct ovs_list *super)
+{
+    struct flow na_flow = *uflow;
+
+    /* Update fields for NA. */
+    na_flow.dl_src = uflow->dl_src;
+    na_flow.ipv6_src = uflow->ipv6_src;
+    na_flow.ipv6_dst = uflow->ipv6_dst;
+    struct in6_addr sn_addr;
+    in6_addr_solicited_node(&sn_addr, &uflow->ipv6_dst);
+    ipv6_multicast_to_ethernet(&na_flow.dl_dst, &sn_addr);
+    na_flow.tp_src = htons(135);
+    na_flow.arp_sha = eth_addr_zero;
+    na_flow.arp_tha = uflow->dl_dst;
+
+    struct ovntrace_node *node = ovntrace_node_append(
+        super, OVNTRACE_NODE_TRANSFORMATION, "nd_ns");
+
+    trace_actions(on->nested, on->nested_len, dp, &na_flow,
+                  table_id, pipeline, &node->subs);
+}
+
 static void
 execute_get_mac_bind(const struct ovnact_get_mac_bind *bind,
                      const struct ovntrace_datapath *dp,
@@ -1176,32 +1556,193 @@ execute_get_mac_bind(const struct ovnact_get_mac_bind *bind,
     const struct ovntrace_mac_binding *binding
         = ovntrace_mac_binding_find(dp, port_key, &ip);
 
-    const struct eth_addr mac = binding ? binding->mac : eth_addr_zero;
+    uflow->dl_dst = binding ? binding->mac : eth_addr_zero;
     if (binding) {
         ovntrace_node_append(super, OVNTRACE_NODE_ACTION,
                              "/* MAC binding to "ETH_ADDR_FMT". */",
-                             ETH_ADDR_ARGS(mac));
+                             ETH_ADDR_ARGS(uflow->dl_dst));
     } else {
         ovntrace_node_append(super, OVNTRACE_NODE_ACTION,
                              "/* No MAC binding. */");
     }
     ovntrace_node_append(super, OVNTRACE_NODE_MODIFY,
-                         "eth.dst = "ETH_ADDR_FMT, ETH_ADDR_ARGS(mac));
+                         "eth.dst = "ETH_ADDR_FMT,
+                         ETH_ADDR_ARGS(uflow->dl_dst));
 }
 
 static void
-execute_put_dhcp_opts(const struct ovnact_put_dhcp_opts *pdo,
-                      struct flow *uflow)
+execute_put_opts(const struct ovnact_put_opts *po,
+                 const char *name, struct flow *uflow,
+                 struct ovs_list *super)
 {
-    struct mf_subfield sf = expr_resolve_field(&pdo->dst);
+    /* Format the put_dhcp_opts action. */
+    struct ds s = DS_EMPTY_INITIALIZER;
+    for (const struct ovnact_gen_option *o = po->options;
+         o < &po->options[po->n_options]; o++) {
+        if (o != po->options) {
+            ds_put_cstr(&s, ", ");
+        }
+        ds_put_format(&s, "%s = ", o->option->name);
+        expr_constant_set_format(&o->value, &s);
+    }
+    ovntrace_node_append(super, OVNTRACE_NODE_MODIFY, "%s(%s)",
+                         name, ds_cstr(&s));
+
+    struct mf_subfield dst = expr_resolve_field(&po->dst);
+    if (!mf_is_register(dst.field->id)) {
+        /* Format assignment. */
+        ds_clear(&s);
+        expr_field_format(&po->dst, &s);
+        ovntrace_node_append(super, OVNTRACE_NODE_MODIFY,
+                             "%s = 1", ds_cstr(&s));
+    }
+    ds_destroy(&s);
+
+    struct mf_subfield sf = expr_resolve_field(&po->dst);
     union mf_subvalue sv = { .u8_val = 1 };
     mf_write_subfield_flow(&sf, &sv, uflow);
 }
 
+static void
+execute_put_dhcp_opts(const struct ovnact_put_opts *pdo,
+                      const char *name, struct flow *uflow,
+                      struct ovs_list *super)
+{
+    ovntrace_node_append(
+        super, OVNTRACE_NODE_ERROR,
+        "/* We assume that this packet is DHCPDISCOVER or DHCPREQUEST. */");
+    execute_put_opts(pdo, name, uflow, super);
+}
+
+static void
+execute_put_nd_ra_opts(const struct ovnact_put_opts *pdo,
+                       const char *name, struct flow *uflow,
+                       struct ovs_list *super)
+{
+    execute_put_opts(pdo, name, uflow, super);
+}
+
+static void
+execute_next(const struct ovnact_next *next,
+             const struct ovntrace_datapath *dp, struct flow *uflow,
+             enum ovnact_pipeline pipeline, struct ovs_list *super)
+{
+    if (pipeline != next->pipeline) {
+        ovs_assert(next->pipeline == OVNACT_P_INGRESS);
+
+        uint16_t in_key = uflow->regs[MFF_LOG_INPORT - MFF_REG0];
+        struct ovntrace_node *node = ovntrace_node_append(
+            super, OVNTRACE_NODE_PIPELINE, "ingress(dp=\"%s\", inport=\"%s\")",
+            dp->friendly_name, ovntrace_port_key_to_name(dp, in_key));
+        super = &node->subs;
+    }
+    trace__(dp, uflow, next->ltable, next->pipeline, super);
+}
+
+
+static void
+execute_dns_lookup(const struct ovnact_dns_lookup *dl, struct flow *uflow,
+                   struct ovs_list *super)
+{
+    struct mf_subfield sf = expr_resolve_field(&dl->dst);
+    union mf_subvalue sv = { .u8_val = 0 };
+    mf_write_subfield_flow(&sf, &sv, uflow);
+    ovntrace_node_append(super, OVNTRACE_NODE_ERROR,
+                         "*** dns_lookup action not implemented");
+}
+
+static void
+execute_ct_next(const struct ovnact_ct_next *ct_next,
+                const struct ovntrace_datapath *dp, struct flow *uflow,
+                enum ovnact_pipeline pipeline, struct ovs_list *super)
+{
+    /* Figure out ct_state. */
+    uint32_t state;
+    const char *comment;
+    if (ct_state_idx < n_ct_states) {
+        state = ct_states[ct_state_idx++];
+        comment = "";
+    } else {
+        state = CS_ESTABLISHED | CS_TRACKED;
+        comment = " /* default (use --ct to customize) */";
+    }
+
+    /* Make a sub-node for attaching the next table. */
+    struct ds s = DS_EMPTY_INITIALIZER;
+    format_flags(&s, ct_state_to_string, state, '|');
+    struct ovntrace_node *node = ovntrace_node_append(
+        super, OVNTRACE_NODE_TRANSFORMATION, "ct_next(ct_state=%s%s)",
+        ds_cstr(&s), comment);
+    ds_destroy(&s);
+
+    /* Trace the actions in the next table. */
+    struct flow ct_flow = *uflow;
+    ct_flow.ct_state = state;
+    trace__(dp, &ct_flow, ct_next->ltable, pipeline, &node->subs);
+
+    /* Upon return, we will trace the actions following the ct action in the
+     * original table.  The pipeline forked, so we're using the original
+     * flow, not ct_flow. */
+}
+
+static void
+execute_ct_nat(const struct ovnact_ct_nat *ct_nat,
+               const struct ovntrace_datapath *dp, struct flow *uflow,
+               enum ovnact_pipeline pipeline, struct ovs_list *super)
+{
+    bool is_dst = ct_nat->ovnact.type == OVNACT_CT_DNAT;
+    if (!is_dst && dp->has_local_l3gateway && !ct_nat->ip) {
+        /* "ct_snat;" has no visible effect in a gateway router. */
+        return;
+    }
+    const char *direction = is_dst ? "dst" : "src";
+
+    /* Make a sub-node for attaching the next table,
+     * and figure out the changes if any. */
+    struct flow ct_flow = *uflow;
+    struct ds s = DS_EMPTY_INITIALIZER;
+    ds_put_format(&s, "ct_%cnat", direction[0]);
+    if (ct_nat->ip) {
+        ds_put_format(&s, "(ip4.%s="IP_FMT")", direction, IP_ARGS(ct_nat->ip));
+        ovs_be32 *ip = is_dst ? &ct_flow.nw_dst : &ct_flow.nw_src;
+        *ip = ct_nat->ip;
+
+        uint8_t state = is_dst ? CS_DST_NAT : CS_SRC_NAT;
+        ct_flow.ct_state |= state;
+    } else {
+        ds_put_format(&s, " /* assuming no un-%cnat entry, so no change */",
+                      direction[0]);
+    }
+    struct ovntrace_node *node = ovntrace_node_append(
+        super, OVNTRACE_NODE_TRANSFORMATION, "%s", ds_cstr(&s));
+    ds_destroy(&s);
+
+    /* Trace the actions in the next table. */
+    trace__(dp, &ct_flow, ct_nat->ltable, pipeline, &node->subs);
+
+    /* Upon return, we will trace the actions following the ct action in the
+     * original table.  The pipeline forked, so we're using the original
+     * flow, not ct_flow. */
+}
+
+static void
+execute_log(const struct ovnact_log *log, struct flow *uflow,
+            struct ovs_list *super)
+{
+    char *packet_str = flow_to_string(uflow, NULL);
+    ovntrace_node_append(super, OVNTRACE_NODE_TRANSFORMATION,
+                    "LOG: ACL name=%s, verdict=%s, severity=%s, packet=\"%s\"",
+                    log->name ? log->name : "<unnamed>",
+                    log_verdict_to_string(log->verdict),
+                    log_severity_to_string(log->severity),
+                    packet_str);
+    free(packet_str);
+}
+
 static void
 trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
               const struct ovntrace_datapath *dp, struct flow *uflow,
-              uint8_t table_id, enum ovntrace_pipeline pipeline,
+              uint8_t table_id, enum ovnact_pipeline pipeline,
               struct ovs_list *super)
 {
     if (!ovnacts_len) {
@@ -1214,7 +1755,9 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
     OVNACT_FOR_EACH (a, ovnacts, ovnacts_len) {
         ds_clear(&s);
         ovnacts_format(a, sizeof *a * (ovnact_next(a) - a), &s);
-        ovntrace_node_append(super, OVNTRACE_NODE_ACTION, "%s", ds_cstr(&s));
+        char *friendly = ovntrace_make_names_friendly(ds_cstr(&s));
+        ovntrace_node_append(super, OVNTRACE_NODE_ACTION, "%s", friendly);
+        free(friendly);
 
         switch (a->type) {
         case OVNACT_OUTPUT:
@@ -1222,7 +1765,7 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
             break;
 
         case OVNACT_NEXT:
-            trace__(dp, uflow, table_id + 1, pipeline, super);
+            execute_next(ovnact_get_NEXT(a), dp, uflow, pipeline, super);
             break;
 
         case OVNACT_LOAD:
@@ -1254,12 +1797,33 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
             break;
 
         case OVNACT_CT_NEXT:
+            execute_ct_next(ovnact_get_CT_NEXT(a), dp, uflow, pipeline, super);
+            break;
+
         case OVNACT_CT_COMMIT:
+            /* Nothing to do. */
+            break;
+
         case OVNACT_CT_DNAT:
+            execute_ct_nat(ovnact_get_CT_DNAT(a), dp, uflow, pipeline, super);
+            break;
+
         case OVNACT_CT_SNAT:
+            execute_ct_nat(ovnact_get_CT_SNAT(a), dp, uflow, pipeline, super);
+            break;
+
         case OVNACT_CT_LB:
             ovntrace_node_append(super, OVNTRACE_NODE_ERROR,
-                                 "*** ct_* actions not implemented");
+                                 "*** ct_lb action not implemented");
+            break;
+
+        case OVNACT_CT_CLEAR:
+            flow_clear_conntrack(uflow);
+            break;
+
+        case OVNACT_CLONE:
+            execute_clone(ovnact_get_CLONE(a), dp, uflow, table_id, pipeline,
+                          super);
             break;
 
         case OVNACT_ARP:
@@ -1272,6 +1836,11 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
                           super);
             break;
 
+        case OVNACT_ND_NS:
+            execute_nd_ns(ovnact_get_ND_NS(a), dp, uflow, table_id, pipeline,
+                          super);
+            break;
+
         case OVNACT_GET_ARP:
             execute_get_mac_bind(ovnact_get_GET_ARP(a), dp, uflow, super);
             break;
@@ -1286,11 +1855,18 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
             break;
 
         case OVNACT_PUT_DHCPV4_OPTS:
-            execute_put_dhcp_opts(ovnact_get_PUT_DHCPV4_OPTS(a), uflow);
+            execute_put_dhcp_opts(ovnact_get_PUT_DHCPV4_OPTS(a),
+                                  "put_dhcp_opts", uflow, super);
             break;
 
         case OVNACT_PUT_DHCPV6_OPTS:
-            execute_put_dhcp_opts(ovnact_get_PUT_DHCPV6_OPTS(a), uflow);
+            execute_put_dhcp_opts(ovnact_get_PUT_DHCPV6_OPTS(a),
+                                  "put_dhcpv6_opts", uflow, super);
+            break;
+
+        case OVNACT_PUT_ND_RA_OPTS:
+            execute_put_nd_ra_opts(ovnact_get_PUT_DHCPV6_OPTS(a),
+                                   "put_nd_ra_opts", uflow, super);
             break;
 
         case OVNACT_SET_QUEUE:
@@ -1304,6 +1880,14 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
              * though, it would be easy enough to track the queue information
              * by adjusting uflow->skb_priority. */
             break;
+
+        case OVNACT_DNS_LOOKUP:
+            execute_dns_lookup(ovnact_get_DNS_LOOKUP(a), uflow, super);
+            break;
+
+        case OVNACT_LOG:
+            execute_log(ovnact_get_LOG(a), uflow, super);
+            break;
         }
 
     }
@@ -1320,9 +1904,53 @@ may_omit_stage(const struct ovntrace_flow *f, uint8_t table_id)
             && ovnact_get_NEXT(f->ovnacts)->ltable == table_id + 1);
 }
 
+static void
+trace_openflow(const struct ovntrace_flow *f, struct ovs_list *super)
+{
+    struct ofputil_flow_stats_request fsr = {
+        .cookie = htonll(f->uuid.parts[0]),
+        .cookie_mask = OVS_BE64_MAX,
+        .out_port = OFPP_ANY,
+        .out_group = OFPG_ANY,
+        .table_id = OFPTT_ALL,
+    };
+
+    struct ofputil_flow_stats *fses;
+    size_t n_fses;
+    int error = vconn_dump_flows(vconn, &fsr, OFPUTIL_P_OF13_OXM,
+                                 &fses, &n_fses);
+    if (error) {
+        ovntrace_node_append(super, OVNTRACE_NODE_ERROR,
+                             "*** error obtaining flow stats (%s)",
+                             ovs_strerror(error));
+        VLOG_WARN("%s: error obtaining flow stats (%s)",
+                  ovs, ovs_strerror(error));
+        return;
+    }
+
+    if (n_fses) {
+        struct ds s = DS_EMPTY_INITIALIZER;
+        for (size_t i = 0; i < n_fses; i++) {
+            ds_clear(&s);
+            ofp_print_flow_stats(&s, &fses[i], NULL, true);
+            ovntrace_node_append(super, OVNTRACE_NODE_ACTION,
+                                 "%s", ds_cstr(&s));
+        }
+        ds_destroy(&s);
+    } else {
+        ovntrace_node_append(super, OVNTRACE_NODE_ERROR,
+                             "*** no OpenFlow flows");
+    }
+
+    for (size_t i = 0; i < n_fses; i++) {
+        free(CONST_CAST(struct ofpact *, fses[i].ofpacts));
+    }
+    free(fses);
+}
+
 static void
 trace__(const struct ovntrace_datapath *dp, struct flow *uflow,
-        uint8_t table_id, enum ovntrace_pipeline pipeline,
+        uint8_t table_id, enum ovnact_pipeline pipeline,
         struct ovs_list *super)
 {
     const struct ovntrace_flow *f;
@@ -1344,15 +1972,23 @@ trace__(const struct ovntrace_datapath *dp, struct flow *uflow,
         } else if (f->source) {
             ds_put_format(&s, "(%s): ", f->source);
         }
-        ds_put_format(&s, "%s, priority %d", f->match_s, f->priority);
+        ds_put_format(&s, "%s, priority %d, uuid %08x",
+                      f->match_s, f->priority, f->uuid.parts[0]);
     } else {
-        ds_put_format(&s, "no match");
+        char *stage_name = ovntrace_stage_name(dp, table_id, pipeline);
+        ds_put_format(&s, "%s%sno match (implicit drop)",
+                      stage_name ? stage_name : "",
+                      stage_name ? ": " : "");
+        free(stage_name);
     }
     struct ovntrace_node *node = ovntrace_node_append(
         super, OVNTRACE_NODE_TABLE, "%s", ds_cstr(&s));
     ds_destroy(&s);
 
     if (f) {
+        if (vconn) {
+            trace_openflow(f, &node->subs);
+        }
         trace_actions(f->ovnacts, f->ovnacts_len, dp, uflow, table_id,
                       pipeline, &node->subs);
     }
@@ -1363,14 +1999,16 @@ trace(const char *dp_s, const char *flow_s)
 {
     const struct ovntrace_datapath *dp = ovntrace_datapath_find_by_name(dp_s);
     if (!dp) {
-        ovs_fatal(0, "unknown datapath \"%s\"", dp_s);
+        return xasprintf("unknown datapath \"%s\"\n", dp_s);
     }
 
     struct flow uflow;
     char *error = expr_parse_microflow(flow_s, &symtab, &address_sets,
                                        ovntrace_lookup_port, dp, &uflow);
     if (error) {
-        ovs_fatal(0, "error parsing flow: %s", error);
+        char *s = xasprintf("error parsing flow: %s\n", error);
+        free(error);
+        return s;
     }
 
     uint32_t in_key = uflow.regs[MFF_LOG_INPORT - MFF_REG0];
@@ -1378,19 +2016,26 @@ trace(const char *dp_s, const char *flow_s)
         VLOG_WARN("microflow does not specify ingress port");
     }
     const struct ovntrace_port *inport = ovntrace_port_find_by_key(dp, in_key);
-    const char *inport_name = inport ? inport->name : "(unnamed)";
+    const char *inport_name = inport ? inport->friendly_name : "(unnamed)";
 
     struct ds output = DS_EMPTY_INITIALIZER;
 
     ds_put_cstr(&output, "# ");
-    flow_format(&output, &uflow);
+    flow_format(&output, &uflow, NULL);
     ds_put_char(&output, '\n');
 
+    if (ovs) {
+        int retval = vconn_open_block(ovs, 1 << OFP13_VERSION, 0, &vconn);
+        if (retval) {
+            VLOG_WARN("%s: connection failed (%s)", ovs, ovs_strerror(retval));
+        }
+    }
+
     struct ovs_list root = OVS_LIST_INITIALIZER(&root);
     struct ovntrace_node *node = ovntrace_node_append(
         &root, OVNTRACE_NODE_PIPELINE, "ingress(dp=\"%s\", inport=\"%s\")",
-        dp->name, inport_name);
-    trace__(dp, &uflow, 0, P_INGRESS, &node->subs);
+        dp->friendly_name, inport_name);
+    trace__(dp, &uflow, 0, OVNACT_P_INGRESS, &node->subs);
 
     bool multiple = (detailed + summary + minimal) > 1;
     if (detailed) {
@@ -1408,6 +2053,7 @@ trace(const char *dp_s, const char *flow_s)
         ovntrace_node_clone(&root, &clone);
         ovntrace_node_prune_summary(&clone);
         ovntrace_node_print_summary(&output, &clone, 0);
+        ovntrace_node_list_destroy(&clone);
     }
 
     if (minimal) {
@@ -1417,6 +2063,11 @@ trace(const char *dp_s, const char *flow_s)
         ovntrace_node_prune_hard(&root);
         ovntrace_node_print_summary(&output, &root, 0);
     }
+
+    ovntrace_node_list_destroy(&root);
+
+    vconn_close(vconn);
+
     return ds_steal_cstr(&output);
 }
 \f