json: Move from lib to include/openvswitch.
[cascardo/ovs.git] / ovn / utilities / ovn-nbctl.c
index 0bdb3a3..d523eec 100644 (file)
 #include <stdio.h>
 
 #include "command-line.h"
+#include "db-ctl-base.h"
 #include "dirs.h"
 #include "fatal-signal.h"
+#include "openvswitch/json.h"
 #include "ovn/lib/ovn-nb-idl.h"
+#include "packets.h"
 #include "poll-loop.h"
 #include "process.h"
+#include "smap.h"
+#include "sset.h"
 #include "stream.h"
 #include "stream-ssl.h"
+#include "svec.h"
+#include "table.h"
+#include "timeval.h"
 #include "util.h"
 #include "openvswitch/vlog.h"
 
-VLOG_DEFINE_THIS_MODULE(ovn_nbctl);
+VLOG_DEFINE_THIS_MODULE(nbctl);
 
-struct nbctl_context {
+/* --db: The database server to contact. */
+static const char *db;
+
+/* --oneline: Write each command's output as a single line? */
+static bool oneline;
+
+/* --dry-run: Do not commit any changes. */
+static bool dry_run;
+
+/* --timeout: Time to wait for a connection to 'db'. */
+static int timeout;
+
+/* Format for table output. */
+static struct table_style table_style = TABLE_STYLE_DEFAULT;
+
+/* The IDL we're using and the current transaction, if any.
+ * This is for use by nbctl_exit() only, to allow it to clean up.
+ * Other code should use its context arguments. */
+static struct ovsdb_idl *the_idl;
+static struct ovsdb_idl_txn *the_idl_txn;
+OVS_NO_RETURN static void nbctl_exit(int status);
+
+static void nbctl_cmd_init(void);
+OVS_NO_RETURN static void usage(void);
+static void parse_options(int argc, char *argv[], struct shash *local_options);
+static const char *nbctl_default_db(void);
+static void run_prerequisites(struct ctl_command[], size_t n_commands,
+                              struct ovsdb_idl *);
+static bool do_nbctl(const char *args, struct ctl_command *, size_t n,
+                     struct ovsdb_idl *);
+
+int
+main(int argc, char *argv[])
+{
     struct ovsdb_idl *idl;
-    struct ovsdb_idl_txn *txn;
-};
+    struct ctl_command *commands;
+    struct shash local_options;
+    unsigned int seqno;
+    size_t n_commands;
+    char *args;
 
-static const char *db;
+    set_program_name(argv[0]);
+    fatal_ignore_sigpipe();
+    vlog_set_levels(NULL, VLF_CONSOLE, VLL_WARN);
+    vlog_set_levels_from_string_assert("reconnect:warn");
+    nbrec_init();
+
+    nbctl_cmd_init();
+
+    /* Log our arguments.  This is often valuable for debugging systems. */
+    args = process_escape_args(argv);
+    VLOG(ctl_might_write_to_db(argv) ? VLL_INFO : VLL_DBG,
+         "Called as %s", args);
+
+    /* Parse command line. */
+    shash_init(&local_options);
+    parse_options(argc, argv, &local_options);
+    commands = ctl_parse_commands(argc - optind, argv + optind, &local_options,
+                                  &n_commands);
+
+    if (timeout) {
+        time_alarm(timeout);
+    }
+
+    /* Initialize IDL. */
+    idl = the_idl = ovsdb_idl_create(db, &nbrec_idl_class, true, false);
+    run_prerequisites(commands, n_commands, idl);
+
+    /* Execute the commands.
+     *
+     * 'seqno' is the database sequence number for which we last tried to
+     * execute our transaction.  There's no point in trying to commit more than
+     * once for any given sequence number, because if the transaction fails
+     * it's because the database changed and we need to obtain an up-to-date
+     * view of the database before we try the transaction again. */
+    seqno = ovsdb_idl_get_seqno(idl);
+    for (;;) {
+        ovsdb_idl_run(idl);
+        if (!ovsdb_idl_is_alive(idl)) {
+            int retval = ovsdb_idl_get_last_error(idl);
+            ctl_fatal("%s: database connection failed (%s)",
+                        db, ovs_retval_to_string(retval));
+        }
+
+        if (seqno != ovsdb_idl_get_seqno(idl)) {
+            seqno = ovsdb_idl_get_seqno(idl);
+            if (do_nbctl(args, commands, n_commands, idl)) {
+                free(args);
+                exit(EXIT_SUCCESS);
+            }
+        }
+
+        if (seqno == ovsdb_idl_get_seqno(idl)) {
+            ovsdb_idl_wait(idl);
+            poll_block();
+        }
+    }
+}
+
+static const char *
+nbctl_default_db(void)
+{
+    static char *def;
+    if (!def) {
+        def = getenv("OVN_NB_DB");
+        if (!def) {
+            def = xasprintf("unix:%s/ovnnb_db.sock", ovs_rundir());
+        }
+    }
+    return def;
+}
+
+static void
+parse_options(int argc, char *argv[], struct shash *local_options)
+{
+    enum {
+        OPT_DB = UCHAR_MAX + 1,
+        OPT_NO_SYSLOG,
+        OPT_DRY_RUN,
+        OPT_ONELINE,
+        OPT_LOCAL,
+        OPT_COMMANDS,
+        OPT_OPTIONS,
+        VLOG_OPTION_ENUMS,
+        TABLE_OPTION_ENUMS
+    };
+    static const struct option global_long_options[] = {
+        {"db", required_argument, NULL, OPT_DB},
+        {"no-syslog", no_argument, NULL, OPT_NO_SYSLOG},
+        {"dry-run", no_argument, NULL, OPT_DRY_RUN},
+        {"oneline", no_argument, NULL, OPT_ONELINE},
+        {"timeout", required_argument, NULL, 't'},
+        {"help", no_argument, NULL, 'h'},
+        {"commands", no_argument, NULL, OPT_COMMANDS},
+        {"options", no_argument, NULL, OPT_OPTIONS},
+        {"version", no_argument, NULL, 'V'},
+        VLOG_LONG_OPTIONS,
+        STREAM_SSL_LONG_OPTIONS,
+        TABLE_LONG_OPTIONS,
+        {NULL, 0, NULL, 0},
+    };
+    const int n_global_long_options = ARRAY_SIZE(global_long_options) - 1;
+    char *tmp, *short_options;
+
+    struct option *options;
+    size_t allocated_options;
+    size_t n_options;
+    size_t i;
+
+    tmp = ovs_cmdl_long_options_to_short_options(global_long_options);
+    short_options = xasprintf("+%s", tmp);
+    free(tmp);
+
+    /* We want to parse both global and command-specific options here, but
+     * getopt_long() isn't too convenient for the job.  We copy our global
+     * options into a dynamic array, then append all of the command-specific
+     * options. */
+    options = xmemdup(global_long_options, sizeof global_long_options);
+    allocated_options = ARRAY_SIZE(global_long_options);
+    n_options = n_global_long_options;
+    ctl_add_cmd_options(&options, &n_options, &allocated_options, OPT_LOCAL);
+    table_style.format = TF_LIST;
 
-static const char *default_db(void);
+    for (;;) {
+        int idx;
+        int c;
+
+        c = getopt_long(argc, argv, short_options, options, &idx);
+        if (c == -1) {
+            break;
+        }
+
+        switch (c) {
+        case OPT_DB:
+            db = optarg;
+            break;
+
+        case OPT_ONELINE:
+            oneline = true;
+            break;
+
+        case OPT_NO_SYSLOG:
+            vlog_set_levels(&this_module, VLF_SYSLOG, VLL_WARN);
+            break;
+
+        case OPT_DRY_RUN:
+            dry_run = true;
+            break;
+
+        case OPT_LOCAL:
+            if (shash_find(local_options, options[idx].name)) {
+                ctl_fatal("'%s' option specified multiple times",
+                            options[idx].name);
+            }
+            shash_add_nocopy(local_options,
+                             xasprintf("--%s", options[idx].name),
+                             nullable_xstrdup(optarg));
+            break;
+
+        case 'h':
+            usage();
+            exit(EXIT_SUCCESS);
+
+        case OPT_COMMANDS:
+            ctl_print_commands();
+
+        case OPT_OPTIONS:
+            ctl_print_options(global_long_options);
+
+        case 'V':
+            ovs_print_version(0, 0);
+            printf("DB Schema %s\n", nbrec_get_db_version());
+            exit(EXIT_SUCCESS);
+
+        case 't':
+            timeout = strtoul(optarg, NULL, 10);
+            if (timeout < 0) {
+                ctl_fatal("value %s on -t or --timeout is invalid", optarg);
+            }
+            break;
+
+        VLOG_OPTION_HANDLERS
+        TABLE_OPTION_HANDLERS(&table_style)
+        STREAM_SSL_OPTION_HANDLERS
+
+        case '?':
+            exit(EXIT_FAILURE);
+
+        default:
+            abort();
+        }
+    }
+    free(short_options);
+
+    if (!db) {
+        db = nbctl_default_db();
+    }
+
+    for (i = n_global_long_options; options[i].name; i++) {
+        free(CONST_CAST(char *, options[i].name));
+    }
+    free(options);
+}
 
 static void
 usage(void)
@@ -50,983 +293,1919 @@ usage: %s [OPTIONS] COMMAND [ARG...]\n\
 \n\
 General commands:\n\
   show                      print overview of database contents\n\
-  show LSWITCH              print overview of database contents for LSWITCH\n\
+  show SWITCH               print overview of database contents for SWITCH\n\
+  show ROUTER               print overview of database contents for ROUTER\n\
 \n\
 Logical switch commands:\n\
-  lswitch-add [LSWITCH]     create a logical switch named LSWITCH\n\
-  lswitch-del LSWITCH       delete LSWITCH and all its ports\n\
-  lswitch-list              print the names of all logical switches\n\
-  lswitch-set-external-id LSWITCH KEY [VALUE]\n\
-                            set or delete an external-id on LSWITCH\n\
-  lswitch-get-external-id LSWITCH [KEY]\n\
-                            list one or all external-ids on LSWITCH\n\
+  ls-add [SWITCH]           create a logical switch named SWITCH\n\
+  ls-del SWITCH             delete SWITCH and all its ports\n\
+  ls-list                   print the names of all logical switches\n\
+\n\
+ACL commands:\n\
+  acl-add SWITCH DIRECTION PRIORITY MATCH ACTION [log]\n\
+                            add an ACL to SWITCH\n\
+  acl-del SWITCH [DIRECTION [PRIORITY MATCH]]\n\
+                            remove ACLs from SWITCH\n\
+  acl-list SWITCH           print ACLs for SWITCH\n\
 \n\
-Logical port commands:\n\
-  lport-add LSWITCH LPORT   add logical port LPORT on LSWITCH\n\
-  lport-add LSWITCH LPORT PARENT TAG\n\
-                            add logical port LPORT on LSWITCH with PARENT\n\
+Logical switch port commands:\n\
+  lsp-add SWITCH PORT       add logical port PORT on SWITCH\n\
+  lsp-add SWITCH PORT PARENT TAG\n\
+                            add logical port PORT on SWITCH with PARENT\n\
                             on TAG\n\
-  lport-del LPORT           delete LPORT from its attached switch\n\
-  lport-list LSWITCH        print the names of all logical ports on LSWITCH\n\
-  lport-get-parent LPORT    get the parent of LPORT if set\n\
-  lport-get-tag LPORT       get the LPORT's tag if set\n\
-  lport-set-external-id LPORT KEY [VALUE]\n\
-                            set or delete an external-id on LPORT\n\
-  lport-get-external-id LPORT [KEY]\n\
-                            list one or all external-ids on LPORT\n\
-  lport-set-macs LPORT [MAC]...\n\
-                            set MAC addresses for LPORT.\n\
-  lport-get-macs LPORT      get a list of MAC addresses on LPORT\n\
-  lport-set-port-security LPORT [ADDRS]...\n\
-                            set port security addresses for LPORT.\n\
-  lport-get-port-security LPORT    get LPORT's port security addresses\n\
-  lport-get-up LPORT        get state of LPORT ('up' or 'down')\n\
-  lport-set-enabled LPORT STATE\n\
-                            set administrative state LPORT\n\
+  lsp-del PORT              delete PORT from its attached switch\n\
+  lsp-list SWITCH           print the names of all logical ports on SWITCH\n\
+  lsp-get-parent PORT       get the parent of PORT if set\n\
+  lsp-get-tag PORT          get the PORT's tag if set\n\
+  lsp-set-addresses PORT [ADDRESS]...\n\
+                            set MAC or MAC+IP addresses for PORT.\n\
+  lsp-get-addresses PORT    get a list of MAC addresses on PORT\n\
+  lsp-set-port-security PORT [ADDRS]...\n\
+                            set port security addresses for PORT.\n\
+  lsp-get-port-security PORT    get PORT's port security addresses\n\
+  lsp-get-up PORT           get state of PORT ('up' or 'down')\n\
+  lsp-set-enabled PORT STATE\n\
+                            set administrative state PORT\n\
+                            ('enabled' or 'disabled')\n\
+  lsp-get-enabled PORT      get administrative state PORT\n\
+                            ('enabled' or 'disabled')\n\
+  lsp-set-type PORT TYPE    set the type for PORT\n\
+  lsp-get-type PORT         get the type for PORT\n\
+  lsp-set-options PORT KEY=VALUE [KEY=VALUE]...\n\
+                            set options related to the type of PORT\n\
+  lsp-get-options PORT      get the type specific options for PORT\n\
+\n\
+Logical router commands:\n\
+  lr-add [ROUTER]           create a logical router named ROUTER\n\
+  lr-del ROUTER             delete ROUTER and all its ports\n\
+  lr-list                   print the names of all logical routers\n\
+\n\
+Logical router port commands:\n\
+  lrp-add ROUTER PORT MAC NETWORK... [peer=PEER]\n\
+                            add logical port PORT on ROUTER\n\
+  lrp-del PORT              delete PORT from its attached router\n\
+  lrp-list ROUTER           print the names of all ports on ROUTER\n\
+  lrp-set-enabled PORT STATE\n\
+                            set administrative state PORT\n\
                             ('enabled' or 'disabled')\n\
-  lport-get-enabled LPORT   get administrative state LPORT\n\
+  lrp-get-enabled PORT      get administrative state PORT\n\
                             ('enabled' or 'disabled')\n\
-  lport-set-type LPORT TYPE Set the type for LPORT\n\
-  lport-get-type LPORT      Get the type for LPORT\n\
-  lport-set-options LPORT KEY=VALUE [KEY=VALUE]...\n\
-                            Set options related to the type of LPORT\n\
-  lport-get-options LPORT   Get the type specific options for LPORT\n\
+\n\
+Route commands:\n\
+  lr-route-add ROUTER PREFIX NEXTHOP [PORT]\n\
+                            add a route to ROUTER\n\
+  lr-route-del ROUTER [PREFIX]\n\
+                            remove routes from ROUTER\n\
+  lr-route-list ROUTER      print routes for ROUTER\n\
+\n\
+%s\
 \n\
 Options:\n\
-  --db=DATABASE             connect to DATABASE\n\
-                            (default: %s)\n\
-  -h, --help                display this help message\n\
-  -o, --options             list available options\n\
-  -V, --version             display version information\n\
-", program_name, program_name, default_db());
+  --db=DATABASE               connect to DATABASE\n\
+                              (default: %s)\n\
+  -t, --timeout=SECS          wait at most SECS seconds\n\
+  --dry-run                   do not commit changes to database\n\
+  --oneline                   print exactly one line of output per command\n",
+           program_name, program_name, ctl_get_db_cmd_usage(), nbctl_default_db());
     vlog_usage();
-    stream_usage("database", true, true, false);
+    printf("\
+  --no-syslog             equivalent to --verbose=nbctl:syslog:warn\n");
+    printf("\n\
+Other options:\n\
+  -h, --help                  display this help message\n\
+  -V, --version               display version information\n");
+    exit(EXIT_SUCCESS);
 }
 \f
-static const struct nbrec_logical_switch *
-lswitch_by_name_or_uuid(struct nbctl_context *nb_ctx, const char *id)
+
+/* Find a logical router given its id. */
+static const struct nbrec_logical_router *
+lr_by_name_or_uuid(struct ctl_context *ctx, const char *id,
+                        bool must_exist)
 {
-    const struct nbrec_logical_switch *lswitch = NULL;
+    const struct nbrec_logical_router *lr = NULL;
     bool is_uuid = false;
-    bool duplicate = false;
-    struct uuid lswitch_uuid;
+    struct uuid lr_uuid;
 
-    if (uuid_from_string(&lswitch_uuid, id)) {
+    if (uuid_from_string(&lr_uuid, id)) {
         is_uuid = true;
-        lswitch = nbrec_logical_switch_get_for_uuid(nb_ctx->idl,
-                                                    &lswitch_uuid);
+        lr = nbrec_logical_router_get_for_uuid(ctx->idl, &lr_uuid);
     }
 
-    if (!lswitch) {
-        const struct nbrec_logical_switch *iter;
+    if (!lr) {
+        const struct nbrec_logical_router *iter;
 
-        NBREC_LOGICAL_SWITCH_FOR_EACH(iter, nb_ctx->idl) {
+        NBREC_LOGICAL_ROUTER_FOR_EACH(iter, ctx->idl) {
             if (strcmp(iter->name, id)) {
                 continue;
             }
-            if (lswitch) {
-                VLOG_WARN("There is more than one logical switch named '%s'. "
-                        "Use a UUID.", id);
-                lswitch = NULL;
-                duplicate = true;
-                break;
+            if (lr) {
+                ctl_fatal("Multiple logical routers named '%s'.  "
+                          "Use a UUID.", id);
             }
-            lswitch = iter;
+            lr = iter;
         }
     }
 
-    if (!lswitch && !duplicate) {
-        VLOG_WARN("lswitch not found for %s: '%s'",
-                is_uuid ? "UUID" : "name", id);
+    if (!lr && must_exist) {
+        ctl_fatal("%s: router %s not found", id, is_uuid ? "UUID" : "name");
     }
 
-    return lswitch;
+    return lr;
 }
 
-static void
-print_lswitch(const struct nbrec_logical_switch *lswitch)
+static const struct nbrec_logical_switch *
+ls_by_name_or_uuid(struct ctl_context *ctx, const char *id, bool must_exist)
 {
-    printf("    lswitch "UUID_FMT" (%s)\n",
-           UUID_ARGS(&lswitch->header_.uuid), lswitch->name);
+    const struct nbrec_logical_switch *ls = NULL;
+
+    struct uuid ls_uuid;
+    bool is_uuid = uuid_from_string(&ls_uuid, id);
+    if (is_uuid) {
+        ls = nbrec_logical_switch_get_for_uuid(ctx->idl, &ls_uuid);
+    }
 
-    for (size_t i = 0; i < lswitch->n_ports; i++) {
-        const struct nbrec_logical_port *lport = lswitch->ports[i];
+    if (!ls) {
+        const struct nbrec_logical_switch *iter;
 
-        printf("        lport %s\n", lport->name);
-        if (lport->parent_name && lport->n_tag) {
-            printf("            parent: %s, tag:%"PRIu64"\n",
-                   lport->parent_name, lport->tag[0]);
-        }
-        if (lport->n_macs) {
-            printf("            macs:");
-            for (size_t j = 0; j < lport->n_macs; j++) {
-                printf(" %s", lport->macs[j]);
+        NBREC_LOGICAL_SWITCH_FOR_EACH(iter, ctx->idl) {
+            if (strcmp(iter->name, id)) {
+                continue;
+            }
+            if (ls) {
+                ctl_fatal("Multiple logical switches named '%s'.  "
+                          "Use a UUID.", id);
             }
-            printf("\n");
+            ls = iter;
         }
     }
+
+    if (!ls && must_exist) {
+        ctl_fatal("%s: switch %s not found", id, is_uuid ? "UUID" : "name");
+    }
+
+    return ls;
 }
 
+/* Given pointer to logical router, this routine prints the router
+ * information.  */
 static void
-do_show(struct ovs_cmdl_context *ctx)
+print_lr(const struct nbrec_logical_router *lr, struct ds *s)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const struct nbrec_logical_switch *lswitch;
-
-    if (ctx->argc == 2) {
-        lswitch = lswitch_by_name_or_uuid(nb_ctx, ctx->argv[1]);
-        if (lswitch) {
-            print_lswitch(lswitch);
+    ds_put_format(s, "    router "UUID_FMT" (%s)\n",
+                  UUID_ARGS(&lr->header_.uuid), lr->name);
+
+    for (size_t i = 0; i < lr->n_ports; i++) {
+        const struct nbrec_logical_router_port *lrp = lr->ports[i];
+        ds_put_format(s, "        port %s\n", lrp->name);
+        if (lrp->mac) {
+            ds_put_cstr(s, "            mac: ");
+            ds_put_format(s, "\"%s\"\n", lrp->mac);
         }
-    } else {
-        NBREC_LOGICAL_SWITCH_FOR_EACH(lswitch, nb_ctx->idl) {
-            print_lswitch(lswitch);
+        if (lrp->n_networks) {
+            ds_put_cstr(s, "            networks: [");
+            for (size_t j = 0; j < lrp->n_networks; j++) {
+                ds_put_format(s, "%s\"%s\"",
+                        j == 0 ? "" : ", ",
+                        lrp->networks[j]);
+            }
+            ds_put_cstr(s, "]\n");
         }
     }
 }
 
 static void
-do_lswitch_add(struct ovs_cmdl_context *ctx)
+print_ls(const struct nbrec_logical_switch *ls, struct ds *s)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    struct nbrec_logical_switch *lswitch;
+    ds_put_format(s, "    switch "UUID_FMT" (%s)\n",
+                  UUID_ARGS(&ls->header_.uuid), ls->name);
 
-    lswitch = nbrec_logical_switch_insert(nb_ctx->txn);
-    if (ctx->argc == 2) {
-        nbrec_logical_switch_set_name(lswitch, ctx->argv[1]);
+    for (size_t i = 0; i < ls->n_ports; i++) {
+        const struct nbrec_logical_switch_port *lsp = ls->ports[i];
+
+        ds_put_format(s, "        port %s\n", lsp->name);
+        if (lsp->parent_name) {
+            ds_put_format(s, "            parent: %s\n", lsp->parent_name);
+        }
+        if (lsp->n_tag) {
+            ds_put_format(s, "            tag: %"PRIu64"\n", lsp->tag[0]);
+        }
+        if (lsp->n_addresses) {
+            ds_put_cstr(s, "            addresses: [");
+            for (size_t j = 0; j < lsp->n_addresses; j++) {
+                ds_put_format(s, "%s\"%s\"",
+                        j == 0 ? "" : ", ",
+                        lsp->addresses[j]);
+            }
+            ds_put_cstr(s, "]\n");
+        }
     }
 }
 
 static void
-do_lswitch_del(struct ovs_cmdl_context *ctx)
+nbctl_show(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const char *id = ctx->argv[1];
-    const struct nbrec_logical_switch *lswitch;
+    const struct nbrec_logical_switch *ls;
 
-    lswitch = lswitch_by_name_or_uuid(nb_ctx, id);
-    if (!lswitch) {
-        return;
+    if (ctx->argc == 2) {
+        ls = ls_by_name_or_uuid(ctx, ctx->argv[1], false);
+        if (ls) {
+            print_ls(ls, &ctx->output);
+        }
+    } else {
+        NBREC_LOGICAL_SWITCH_FOR_EACH(ls, ctx->idl) {
+            print_ls(ls, &ctx->output);
+        }
     }
+    const struct nbrec_logical_router *lr;
 
-    nbrec_logical_switch_delete(lswitch);
+    if (ctx->argc == 2) {
+        lr = lr_by_name_or_uuid(ctx, ctx->argv[1], false);
+        if (lr) {
+            print_lr(lr, &ctx->output);
+        }
+    } else {
+        NBREC_LOGICAL_ROUTER_FOR_EACH(lr, ctx->idl) {
+            print_lr(lr, &ctx->output);
+        }
+    }
 }
 
 static void
-do_lswitch_list(struct ovs_cmdl_context *ctx)
+nbctl_ls_add(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const struct nbrec_logical_switch *lswitch;
+    const char *ls_name = ctx->argc == 2 ? ctx->argv[1] : NULL;
 
-    NBREC_LOGICAL_SWITCH_FOR_EACH(lswitch, nb_ctx->idl) {
-        printf(UUID_FMT " (%s)\n",
-               UUID_ARGS(&lswitch->header_.uuid), lswitch->name);
+    bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
+    bool add_duplicate = shash_find(&ctx->options, "--add-duplicate") != NULL;
+    if (may_exist && add_duplicate) {
+        ctl_fatal("--may-exist and --add-duplicate may not be used together");
     }
-}
-
-static void
-do_lswitch_set_external_id(struct ovs_cmdl_context *ctx)
-{
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const char *id = ctx->argv[1];
-    const struct nbrec_logical_switch *lswitch;
-    struct smap new_external_ids;
 
-    lswitch = lswitch_by_name_or_uuid(nb_ctx, id);
-    if (!lswitch) {
-        return;
+    if (ls_name) {
+        if (!add_duplicate) {
+            const struct nbrec_logical_switch *ls;
+            NBREC_LOGICAL_SWITCH_FOR_EACH (ls, ctx->idl) {
+                if (!strcmp(ls->name, ls_name)) {
+                    if (may_exist) {
+                        return;
+                    }
+                    ctl_fatal("%s: a switch with this name already exists",
+                              ls_name);
+                }
+            }
+        }
+    } else if (may_exist) {
+        ctl_fatal("--may-exist requires specifying a name");
+    } else if (add_duplicate) {
+        ctl_fatal("--add-duplicate requires specifying a name");
     }
 
-    smap_init(&new_external_ids);
-    smap_clone(&new_external_ids, &lswitch->external_ids);
-    if (ctx->argc == 4) {
-        smap_replace(&new_external_ids, ctx->argv[2], ctx->argv[3]);
-    } else {
-        smap_remove(&new_external_ids, ctx->argv[2]);
+    struct nbrec_logical_switch *ls;
+    ls = nbrec_logical_switch_insert(ctx->txn);
+    if (ls_name) {
+        nbrec_logical_switch_set_name(ls, ls_name);
     }
-    nbrec_logical_switch_set_external_ids(lswitch, &new_external_ids);
-    smap_destroy(&new_external_ids);
 }
 
 static void
-do_lswitch_get_external_id(struct ovs_cmdl_context *ctx)
+nbctl_ls_del(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
+    bool must_exist = !shash_find(&ctx->options, "--if-exists");
     const char *id = ctx->argv[1];
-    const struct nbrec_logical_switch *lswitch;
+    const struct nbrec_logical_switch *ls;
 
-    lswitch = lswitch_by_name_or_uuid(nb_ctx, id);
-    if (!lswitch) {
+    ls = ls_by_name_or_uuid(ctx, id, must_exist);
+    if (!ls) {
         return;
     }
 
-    if (ctx->argc == 3) {
-        const char *key = ctx->argv[2];
-        const char *value;
-
-        /* List one external ID */
-
-        value = smap_get(&lswitch->external_ids, key);
-        if (value) {
-            printf("%s\n", value);
-        }
-    } else {
-        struct smap_node *node;
+    nbrec_logical_switch_delete(ls);
+}
 
-        /* List all external IDs */
+static void
+nbctl_ls_list(struct ctl_context *ctx)
+{
+    const struct nbrec_logical_switch *ls;
+    struct smap lswitches;
 
-        SMAP_FOR_EACH(node, &lswitch->external_ids) {
-            printf("%s=%s\n", node->key, node->value);
-        }
+    smap_init(&lswitches);
+    NBREC_LOGICAL_SWITCH_FOR_EACH(ls, ctx->idl) {
+        smap_add_format(&lswitches, ls->name, UUID_FMT " (%s)",
+                        UUID_ARGS(&ls->header_.uuid), ls->name);
+    }
+    const struct smap_node **nodes = smap_sort(&lswitches);
+    for (size_t i = 0; i < smap_count(&lswitches); i++) {
+        const struct smap_node *node = nodes[i];
+        ds_put_format(&ctx->output, "%s\n", node->value);
     }
+    smap_destroy(&lswitches);
+    free(nodes);
 }
 \f
-static const struct nbrec_logical_port *
-lport_by_name_or_uuid(struct nbctl_context *nb_ctx, const char *id)
+static const struct nbrec_logical_switch_port *
+lsp_by_name_or_uuid(struct ctl_context *ctx, const char *id,
+                    bool must_exist)
 {
-    const struct nbrec_logical_port *lport = NULL;
-    bool is_uuid = false;
-    struct uuid lport_uuid;
+    const struct nbrec_logical_switch_port *lsp = NULL;
 
-    if (uuid_from_string(&lport_uuid, id)) {
-        is_uuid = true;
-        lport = nbrec_logical_port_get_for_uuid(nb_ctx->idl, &lport_uuid);
+    struct uuid lsp_uuid;
+    bool is_uuid = uuid_from_string(&lsp_uuid, id);
+    if (is_uuid) {
+        lsp = nbrec_logical_switch_port_get_for_uuid(ctx->idl, &lsp_uuid);
     }
 
-    if (!lport) {
-        NBREC_LOGICAL_PORT_FOR_EACH(lport, nb_ctx->idl) {
-            if (!strcmp(lport->name, id)) {
+    if (!lsp) {
+        NBREC_LOGICAL_SWITCH_PORT_FOR_EACH(lsp, ctx->idl) {
+            if (!strcmp(lsp->name, id)) {
                 break;
             }
         }
     }
 
-    if (!lport) {
-        VLOG_WARN("lport not found for %s: '%s'",
-                is_uuid ? "UUID" : "name", id);
+    if (!lsp && must_exist) {
+        ctl_fatal("%s: port %s not found", id, is_uuid ? "UUID" : "name");
     }
 
-    return lport;
+    return lsp;
 }
 
-static void
-do_lport_add(struct ovs_cmdl_context *ctx)
+/* Returns the logical switch that contains 'lsp'. */
+static const struct nbrec_logical_switch *
+lsp_to_ls(const struct ovsdb_idl *idl,
+               const struct nbrec_logical_switch_port *lsp)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    struct nbrec_logical_port *lport;
-    const struct nbrec_logical_switch *lswitch;
-    int64_t tag;
-
-    lswitch = lswitch_by_name_or_uuid(nb_ctx, ctx->argv[1]);
-    if (!lswitch) {
-        return;
+    const struct nbrec_logical_switch *ls;
+    NBREC_LOGICAL_SWITCH_FOR_EACH (ls, idl) {
+        for (size_t i = 0; i < ls->n_ports; i++) {
+            if (ls->ports[i] == lsp) {
+                return ls;
+            }
+        }
     }
 
-    if (ctx->argc != 3 && ctx->argc != 5) {
-        /* If a parent_name is specified, a tag must be specified as well. */
-        VLOG_WARN("Invalid arguments to lport-add.");
-        return;
+    /* Can't happen because of the database schema */
+    ctl_fatal("logical port %s is not part of any logical switch",
+              lsp->name);
+}
+
+static const char *
+ls_get_name(const struct nbrec_logical_switch *ls,
+                 char uuid_s[UUID_LEN + 1], size_t uuid_s_size)
+{
+    if (ls->name[0]) {
+        return ls->name;
     }
+    snprintf(uuid_s, uuid_s_size, UUID_FMT, UUID_ARGS(&ls->header_.uuid));
+    return uuid_s;
+}
 
-    if (ctx->argc == 5) {
+static void
+nbctl_lsp_add(struct ctl_context *ctx)
+{
+    bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
+
+    const struct nbrec_logical_switch *ls;
+    ls = ls_by_name_or_uuid(ctx, ctx->argv[1], true);
+
+    const char *parent_name;
+    int64_t tag;
+    if (ctx->argc == 3) {
+        parent_name = NULL;
+        tag = -1;
+    } else if (ctx->argc == 5) {
         /* Validate tag. */
-        if (!ovs_scan(ctx->argv[4], "%"SCNd64, &tag) || tag < 0 || tag > 4095) {
-            VLOG_WARN("Invalid tag '%s'", ctx->argv[4]);
-            return;
+        parent_name = ctx->argv[3];
+        if (!ovs_scan(ctx->argv[4], "%"SCNd64, &tag)
+            || tag < 0 || tag > 4095) {
+            ctl_fatal("%s: invalid tag", ctx->argv[4]);
+        }
+    } else {
+        ctl_fatal("lsp-add with parent must also specify a tag");
+    }
+
+    const char *lsp_name = ctx->argv[2];
+    const struct nbrec_logical_switch_port *lsp;
+    lsp = lsp_by_name_or_uuid(ctx, lsp_name, false);
+    if (lsp) {
+        if (!may_exist) {
+            ctl_fatal("%s: a port with this name already exists",
+                      lsp_name);
+        }
+
+        const struct nbrec_logical_switch *lsw;
+        lsw = lsp_to_ls(ctx->idl, lsp);
+        if (lsw != ls) {
+            char uuid_s[UUID_LEN + 1];
+            ctl_fatal("%s: port already exists but in switch %s", lsp_name,
+                      ls_get_name(lsw, uuid_s, sizeof uuid_s));
+        }
+
+        if (parent_name) {
+            if (!lsp->parent_name) {
+                ctl_fatal("%s: port already exists but has no parent",
+                          lsp_name);
+            } else if (strcmp(parent_name, lsp->parent_name)) {
+                ctl_fatal("%s: port already exists with different parent %s",
+                          lsp_name, lsp->parent_name);
+            }
+
+            if (!lsp->n_tag) {
+                ctl_fatal("%s: port already exists but has no tag",
+                          lsp_name);
+            } else if (lsp->tag[0] != tag) {
+                ctl_fatal("%s: port already exists with different "
+                          "tag %"PRId64, lsp_name, lsp->tag[0]);
+            }
+        } else {
+            if (lsp->parent_name) {
+                ctl_fatal("%s: port already exists but has parent %s",
+                          lsp_name, lsp->parent_name);
+            }
         }
+
+        return;
     }
 
     /* Create the logical port. */
-    lport = nbrec_logical_port_insert(nb_ctx->txn);
-    nbrec_logical_port_set_name(lport, ctx->argv[2]);
-    if (ctx->argc == 5) {
-        nbrec_logical_port_set_parent_name(lport, ctx->argv[3]);
-        nbrec_logical_port_set_tag(lport, &tag, 1);
+    lsp = nbrec_logical_switch_port_insert(ctx->txn);
+    nbrec_logical_switch_port_set_name(lsp, lsp_name);
+    if (tag >= 0) {
+        nbrec_logical_switch_port_set_parent_name(lsp, parent_name);
+        nbrec_logical_switch_port_set_tag(lsp, &tag, 1);
     }
 
     /* Insert the logical port into the logical switch. */
-    nbrec_logical_switch_verify_ports(lswitch);
-    struct nbrec_logical_port **new_ports = xmalloc(sizeof *new_ports *
-                                                    (lswitch->n_ports + 1));
-    memcpy(new_ports, lswitch->ports, sizeof *new_ports * lswitch->n_ports);
-    new_ports[lswitch->n_ports] = lport;
-    nbrec_logical_switch_set_ports(lswitch, new_ports, lswitch->n_ports + 1);
+    nbrec_logical_switch_verify_ports(ls);
+    struct nbrec_logical_switch_port **new_ports = xmalloc(sizeof *new_ports *
+                                                    (ls->n_ports + 1));
+    memcpy(new_ports, ls->ports, sizeof *new_ports * ls->n_ports);
+    new_ports[ls->n_ports] = CONST_CAST(struct nbrec_logical_switch_port *,
+                                             lsp);
+    nbrec_logical_switch_set_ports(ls, new_ports, ls->n_ports + 1);
     free(new_ports);
 }
 
-/* Removes lport 'lswitch->ports[idx]'. */
+/* Removes logical switch port 'ls->ports[idx]'. */
 static void
-remove_lport(const struct nbrec_logical_switch *lswitch, size_t idx)
+remove_lsp(const struct nbrec_logical_switch *ls, size_t idx)
 {
-    const struct nbrec_logical_port *lport = lswitch->ports[idx];
+    const struct nbrec_logical_switch_port *lsp = ls->ports[idx];
 
-    /* First remove 'lport' from the array of ports.  This is what will
+    /* First remove 'lsp' from the array of ports.  This is what will
      * actually cause the logical port to be deleted when the transaction is
      * sent to the database server (due to garbage collection). */
-    struct nbrec_logical_port **new_ports
-        = xmemdup(lswitch->ports, sizeof *new_ports * lswitch->n_ports);
-    new_ports[idx] = new_ports[lswitch->n_ports - 1];
-    nbrec_logical_switch_verify_ports(lswitch);
-    nbrec_logical_switch_set_ports(lswitch, new_ports, lswitch->n_ports - 1);
+    struct nbrec_logical_switch_port **new_ports
+        = xmemdup(ls->ports, sizeof *new_ports * ls->n_ports);
+    new_ports[idx] = new_ports[ls->n_ports - 1];
+    nbrec_logical_switch_verify_ports(ls);
+    nbrec_logical_switch_set_ports(ls, new_ports, ls->n_ports - 1);
     free(new_ports);
 
-    /* Delete 'lport' from the IDL.  This won't have a real effect on the
+    /* Delete 'lsp' from the IDL.  This won't have a real effect on the
      * database server (the IDL will suppress it in fact) but it means that it
-     * won't show up when we iterate with NBREC_LOGICAL_PORT_FOR_EACH later. */
-    nbrec_logical_port_delete(lport);
+     * won't show up when we iterate with NBREC_LOGICAL_SWITCH_PORT_FOR_EACH
+     * later. */
+    nbrec_logical_switch_port_delete(lsp);
 }
 
 static void
-do_lport_del(struct ovs_cmdl_context *ctx)
+nbctl_lsp_del(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const struct nbrec_logical_port *lport;
+    bool must_exist = !shash_find(&ctx->options, "--if-exists");
+    const struct nbrec_logical_switch_port *lsp;
 
-    lport = lport_by_name_or_uuid(nb_ctx, ctx->argv[1]);
-    if (!lport) {
+    lsp = lsp_by_name_or_uuid(ctx, ctx->argv[1], must_exist);
+    if (!lsp) {
         return;
     }
 
-    /* Find the switch that contains 'lport', then delete it. */
-    const struct nbrec_logical_switch *lswitch;
-    NBREC_LOGICAL_SWITCH_FOR_EACH (lswitch, nb_ctx->idl) {
-        for (size_t i = 0; i < lswitch->n_ports; i++) {
-            if (lswitch->ports[i] == lport) {
-                remove_lport(lswitch, i);
+    /* Find the switch that contains 'lsp', then delete it. */
+    const struct nbrec_logical_switch *ls;
+    NBREC_LOGICAL_SWITCH_FOR_EACH (lsctx->idl) {
+        for (size_t i = 0; i < ls->n_ports; i++) {
+            if (ls->ports[i] == lsp) {
+                remove_lsp(ls, i);
                 return;
             }
         }
     }
 
-    VLOG_WARN("logical port %s is not part of any logical switch",
+    /* Can't happen because of the database schema. */
+    ctl_fatal("logical port %s is not part of any logical switch",
               ctx->argv[1]);
 }
 
 static void
-do_lport_list(struct ovs_cmdl_context *ctx)
+nbctl_lsp_list(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
     const char *id = ctx->argv[1];
-    const struct nbrec_logical_switch *lswitch;
+    const struct nbrec_logical_switch *ls;
+    struct smap lsps;
+    size_t i;
 
-    lswitch = lswitch_by_name_or_uuid(nb_ctx, id);
-    if (!lswitch) {
-        return;
-    }
+    ls = ls_by_name_or_uuid(ctx, id, true);
 
-    for (size_t i = 0; i < lswitch->n_ports; i++) {
-        const struct nbrec_logical_port *lport = lswitch->ports[i];
-        printf(UUID_FMT " (%s)\n",
-               UUID_ARGS(&lport->header_.uuid), lport->name);
+    smap_init(&lsps);
+    for (i = 0; i < ls->n_ports; i++) {
+        const struct nbrec_logical_switch_port *lsp = ls->ports[i];
+        smap_add_format(&lsps, lsp->name, UUID_FMT " (%s)",
+                        UUID_ARGS(&lsp->header_.uuid), lsp->name);
+    }
+    const struct smap_node **nodes = smap_sort(&lsps);
+    for (i = 0; i < smap_count(&lsps); i++) {
+        const struct smap_node *node = nodes[i];
+        ds_put_format(&ctx->output, "%s\n", node->value);
     }
+    smap_destroy(&lsps);
+    free(nodes);
 }
 
 static void
-do_lport_get_parent(struct ovs_cmdl_context *ctx)
+nbctl_lsp_get_parent(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const struct nbrec_logical_port *lport;
+    const struct nbrec_logical_switch_port *lsp;
 
-    lport = lport_by_name_or_uuid(nb_ctx, ctx->argv[1]);
-    if (!lport) {
-        return;
-    }
-
-    if (lport->parent_name) {
-        printf("%s\n", lport->parent_name);
+    lsp = lsp_by_name_or_uuid(ctx, ctx->argv[1], true);
+    if (lsp->parent_name) {
+        ds_put_format(&ctx->output, "%s\n", lsp->parent_name);
     }
 }
 
 static void
-do_lport_get_tag(struct ovs_cmdl_context *ctx)
+nbctl_lsp_get_tag(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const struct nbrec_logical_port *lport;
-
-    lport = lport_by_name_or_uuid(nb_ctx, ctx->argv[1]);
-    if (!lport) {
-        return;
-    }
+    const struct nbrec_logical_switch_port *lsp;
 
-    if (lport->n_tag > 0) {
-        printf("%"PRId64"\n", lport->tag[0]);
+    lsp = lsp_by_name_or_uuid(ctx, ctx->argv[1], true);
+    if (lsp->n_tag > 0) {
+        ds_put_format(&ctx->output, "%"PRId64"\n", lsp->tag[0]);
     }
 }
 
 static void
-do_lport_set_external_id(struct ovs_cmdl_context *ctx)
+nbctl_lsp_set_addresses(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
     const char *id = ctx->argv[1];
-    const struct nbrec_logical_port *lport;
-    struct smap new_external_ids;
+    const struct nbrec_logical_switch_port *lsp;
 
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
-        return;
-    }
+    lsp = lsp_by_name_or_uuid(ctx, id, true);
 
-    smap_init(&new_external_ids);
-    smap_clone(&new_external_ids, &lport->external_ids);
-    if (ctx->argc == 4) {
-        smap_replace(&new_external_ids, ctx->argv[2], ctx->argv[3]);
-    } else {
-        smap_remove(&new_external_ids, ctx->argv[2]);
+    int i;
+    for (i = 2; i < ctx->argc; i++) {
+        struct eth_addr ea;
+
+        if (strcmp(ctx->argv[i], "unknown")
+            && !ovs_scan(ctx->argv[i], ETH_ADDR_SCAN_FMT,
+                         ETH_ADDR_SCAN_ARGS(ea))) {
+            ctl_fatal("%s: Invalid address format. See ovn-nb(5). "
+                      "Hint: An Ethernet address must be "
+                      "listed before an IP address, together as a single "
+                      "argument.", ctx->argv[i]);
+        }
     }
-    nbrec_logical_port_set_external_ids(lport, &new_external_ids);
-    smap_destroy(&new_external_ids);
+
+    nbrec_logical_switch_port_set_addresses(lsp,
+            (const char **) ctx->argv + 2, ctx->argc - 2);
 }
 
 static void
-do_lport_get_external_id(struct ovs_cmdl_context *ctx)
+nbctl_lsp_get_addresses(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
     const char *id = ctx->argv[1];
-    const struct nbrec_logical_port *lport;
-
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
-        return;
-    }
-
-    if (ctx->argc == 3) {
-        const char *key = ctx->argv[2];
-        const char *value;
-
-        /* List one external ID */
-
-        value = smap_get(&lport->external_ids, key);
-        if (value) {
-            printf("%s\n", value);
-        }
-    } else {
-        struct smap_node *node;
+    const struct nbrec_logical_switch_port *lsp;
+    struct svec addresses;
+    const char *mac;
+    size_t i;
 
-        /* List all external IDs */
+    lsp = lsp_by_name_or_uuid(ctx, id, true);
 
-        SMAP_FOR_EACH(node, &lport->external_ids) {
-            printf("%s=%s\n", node->key, node->value);
-        }
+    svec_init(&addresses);
+    for (i = 0; i < lsp->n_addresses; i++) {
+        svec_add(&addresses, lsp->addresses[i]);
+    }
+    svec_sort(&addresses);
+    SVEC_FOR_EACH(i, mac, &addresses) {
+        ds_put_format(&ctx->output, "%s\n", mac);
     }
+    svec_destroy(&addresses);
 }
 
 static void
-do_lport_set_macs(struct ovs_cmdl_context *ctx)
+nbctl_lsp_set_port_security(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
     const char *id = ctx->argv[1];
-    const struct nbrec_logical_port *lport;
-
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
-        return;
-    }
+    const struct nbrec_logical_switch_port *lsp;
 
-    nbrec_logical_port_set_macs(lport,
+    lsp = lsp_by_name_or_uuid(ctx, id, true);
+    nbrec_logical_switch_port_set_port_security(lsp,
             (const char **) ctx->argv + 2, ctx->argc - 2);
 }
 
 static void
-do_lport_get_macs(struct ovs_cmdl_context *ctx)
+nbctl_lsp_get_port_security(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
     const char *id = ctx->argv[1];
-    const struct nbrec_logical_port *lport;
+    const struct nbrec_logical_switch_port *lsp;
+    struct svec addrs;
+    const char *addr;
     size_t i;
 
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
-        return;
+    lsp = lsp_by_name_or_uuid(ctx, id, true);
+    svec_init(&addrs);
+    for (i = 0; i < lsp->n_port_security; i++) {
+        svec_add(&addrs, lsp->port_security[i]);
     }
-
-    for (i = 0; i < lport->n_macs; i++) {
-        printf("%s\n", lport->macs[i]);
+    svec_sort(&addrs);
+    SVEC_FOR_EACH(i, addr, &addrs) {
+        ds_put_format(&ctx->output, "%s\n", addr);
     }
+    svec_destroy(&addrs);
 }
 
 static void
-do_lport_set_port_security(struct ovs_cmdl_context *ctx)
+nbctl_lsp_get_up(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
     const char *id = ctx->argv[1];
-    const struct nbrec_logical_port *lport;
+    const struct nbrec_logical_switch_port *lsp;
 
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
-        return;
-    }
+    lsp = lsp_by_name_or_uuid(ctx, id, true);
+    ds_put_format(&ctx->output,
+                  "%s\n", (lsp->up && *lsp->up) ? "up" : "down");
+}
 
-    nbrec_logical_port_set_port_security(lport,
-            (const char **) ctx->argv + 2, ctx->argc - 2);
+static bool
+parse_enabled(const char *state)
+{
+    if (!strcasecmp(state, "enabled")) {
+        return true;
+    } else if (!strcasecmp(state, "disabled")) {
+        return false;
+    } else {
+        ctl_fatal("%s: state must be \"enabled\" or \"disabled\"", state);
+    }
 }
 
 static void
-do_lport_get_port_security(struct ovs_cmdl_context *ctx)
+nbctl_lsp_set_enabled(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
     const char *id = ctx->argv[1];
-    const struct nbrec_logical_port *lport;
+    const char *state = ctx->argv[2];
+    const struct nbrec_logical_switch_port *lsp;
+
+    lsp = lsp_by_name_or_uuid(ctx, id, true);
+    bool enabled = parse_enabled(state);
+    nbrec_logical_switch_port_set_enabled(lsp, &enabled, 1);
+}
+
+static void
+nbctl_lsp_get_enabled(struct ctl_context *ctx)
+{
+    const char *id = ctx->argv[1];
+    const struct nbrec_logical_switch_port *lsp;
+
+    lsp = lsp_by_name_or_uuid(ctx, id, true);
+    ds_put_format(&ctx->output, "%s\n",
+                  !lsp->enabled || *lsp->enabled ? "enabled" : "disabled");
+}
+
+static void
+nbctl_lsp_set_type(struct ctl_context *ctx)
+{
+    const char *id = ctx->argv[1];
+    const char *type = ctx->argv[2];
+    const struct nbrec_logical_switch_port *lsp;
+
+    lsp = lsp_by_name_or_uuid(ctx, id, true);
+    nbrec_logical_switch_port_set_type(lsp, type);
+}
+
+static void
+nbctl_lsp_get_type(struct ctl_context *ctx)
+{
+    const char *id = ctx->argv[1];
+    const struct nbrec_logical_switch_port *lsp;
+
+    lsp = lsp_by_name_or_uuid(ctx, id, true);
+    ds_put_format(&ctx->output, "%s\n", lsp->type);
+}
+
+static void
+nbctl_lsp_set_options(struct ctl_context *ctx)
+{
+    const char *id = ctx->argv[1];
+    const struct nbrec_logical_switch_port *lsp;
     size_t i;
+    struct smap options = SMAP_INITIALIZER(&options);
 
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
-        return;
+    lsp = lsp_by_name_or_uuid(ctx, id, true);
+    for (i = 2; i < ctx->argc; i++) {
+        char *key, *value;
+        value = xstrdup(ctx->argv[i]);
+        key = strsep(&value, "=");
+        if (value) {
+            smap_add(&options, key, value);
+        }
+        free(key);
     }
 
-    for (i = 0; i < lport->n_port_security; i++) {
-        printf("%s\n", lport->port_security[i]);
-    }
+    nbrec_logical_switch_port_set_options(lsp, &options);
+
+    smap_destroy(&options);
 }
 
 static void
-do_lport_get_up(struct ovs_cmdl_context *ctx)
+nbctl_lsp_get_options(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
     const char *id = ctx->argv[1];
-    const struct nbrec_logical_port *lport;
+    const struct nbrec_logical_switch_port *lsp;
+    struct smap_node *node;
 
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
-        return;
+    lsp = lsp_by_name_or_uuid(ctx, id, true);
+    SMAP_FOR_EACH(node, &lsp->options) {
+        ds_put_format(&ctx->output, "%s=%s\n", node->key, node->value);
+    }
+}
+
+enum {
+    DIR_FROM_LPORT,
+    DIR_TO_LPORT
+};
+
+static int
+dir_encode(const char *dir)
+{
+    if (!strcmp(dir, "from-lport")) {
+        return DIR_FROM_LPORT;
+    } else if (!strcmp(dir, "to-lport")) {
+        return DIR_TO_LPORT;
     }
 
-    printf("%s\n", (lport->up && *lport->up) ? "up" : "down");
+    OVS_NOT_REACHED();
+}
+
+static int
+acl_cmp(const void *acl1_, const void *acl2_)
+{
+    const struct nbrec_acl *const *acl1p = acl1_;
+    const struct nbrec_acl *const *acl2p = acl2_;
+    const struct nbrec_acl *acl1 = *acl1p;
+    const struct nbrec_acl *acl2 = *acl2p;
+
+    int dir1 = dir_encode(acl1->direction);
+    int dir2 = dir_encode(acl2->direction);
+
+    if (dir1 != dir2) {
+        return dir1 < dir2 ? -1 : 1;
+    } else if (acl1->priority != acl2->priority) {
+        return acl1->priority > acl2->priority ? -1 : 1;
+    } else {
+        return strcmp(acl1->match, acl2->match);
+    }
 }
 
 static void
-do_lport_set_enabled(struct ovs_cmdl_context *ctx)
+nbctl_acl_list(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const char *id = ctx->argv[1];
-    const char *state = ctx->argv[2];
-    const struct nbrec_logical_port *lport;
+    const struct nbrec_logical_switch *ls;
+    const struct nbrec_acl **acls;
+    size_t i;
 
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
-        return;
+    ls = ls_by_name_or_uuid(ctx, ctx->argv[1], true);
+
+    acls = xmalloc(sizeof *acls * ls->n_acls);
+    for (i = 0; i < ls->n_acls; i++) {
+        acls[i] = ls->acls[i];
     }
 
-    if (!strcasecmp(state, "enabled")) {
-        bool enabled = true;
-        nbrec_logical_port_set_enabled(lport, &enabled, 1);
-    } else if (!strcasecmp(state, "disabled")) {
-        bool enabled = false;
-        nbrec_logical_port_set_enabled(lport, &enabled, 1);
+    qsort(acls, ls->n_acls, sizeof *acls, acl_cmp);
+
+    for (i = 0; i < ls->n_acls; i++) {
+        const struct nbrec_acl *acl = acls[i];
+        ds_put_format(&ctx->output, "%10s %5"PRId64" (%s) %s%s\n",
+                      acl->direction, acl->priority,
+                      acl->match, acl->action, acl->log ? " log" : "");
+    }
+
+    free(acls);
+}
+
+static const char *
+parse_direction(const char *arg)
+{
+    /* Validate direction.  Only require the first letter. */
+    if (arg[0] == 't') {
+        return "to-lport";
+    } else if (arg[0] == 'f') {
+        return "from-lport";
     } else {
-        VLOG_ERR("Invalid state '%s' provided to lport-set-enabled", state);
+        ctl_fatal("%s: direction must be \"to-lport\" or \"from-lport\"", arg);
+    }
+}
+
+static int
+parse_priority(const char *arg)
+{
+    /* Validate priority. */
+    int64_t priority;
+    if (!ovs_scan(arg, "%"SCNd64, &priority)
+        || priority < 0 || priority > 32767) {
+        ctl_fatal("%s: priority must in range 0...32767", arg);
     }
+    return priority;
 }
 
 static void
-do_lport_get_enabled(struct ovs_cmdl_context *ctx)
+nbctl_acl_add(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const char *id = ctx->argv[1];
-    const struct nbrec_logical_port *lport;
+    const struct nbrec_logical_switch *ls;
+    const char *action = ctx->argv[5];
+
+    ls = ls_by_name_or_uuid(ctx, ctx->argv[1], true);
+
+    const char *direction = parse_direction(ctx->argv[2]);
+    int64_t priority = parse_priority(ctx->argv[3]);
 
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
+    /* Validate action. */
+    if (strcmp(action, "allow") && strcmp(action, "allow-related")
+        && strcmp(action, "drop") && strcmp(action, "reject")) {
+        ctl_fatal("%s: action must be one of \"allow\", \"allow-related\", "
+                  "\"drop\", and \"reject\"", action);
         return;
     }
 
-    printf("%s\n",
-           (!lport->enabled || *lport->enabled) ? "enabled" : "disabled");
+    /* Create the acl. */
+    struct nbrec_acl *acl = nbrec_acl_insert(ctx->txn);
+    nbrec_acl_set_priority(acl, priority);
+    nbrec_acl_set_direction(acl, direction);
+    nbrec_acl_set_match(acl, ctx->argv[4]);
+    nbrec_acl_set_action(acl, action);
+    if (shash_find(&ctx->options, "--log") != NULL) {
+        nbrec_acl_set_log(acl, true);
+    }
+
+    /* Insert the acl into the logical switch. */
+    nbrec_logical_switch_verify_acls(ls);
+    struct nbrec_acl **new_acls = xmalloc(sizeof *new_acls * (ls->n_acls + 1));
+    memcpy(new_acls, ls->acls, sizeof *new_acls * ls->n_acls);
+    new_acls[ls->n_acls] = acl;
+    nbrec_logical_switch_set_acls(ls, new_acls, ls->n_acls + 1);
+    free(new_acls);
 }
 
 static void
-do_lport_set_type(struct ovs_cmdl_context *ctx)
+nbctl_acl_del(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const char *id = ctx->argv[1];
-    const char *type = ctx->argv[2];
-    const struct nbrec_logical_port *lport;
+    const struct nbrec_logical_switch *ls;
+    ls = ls_by_name_or_uuid(ctx, ctx->argv[1], true);
+
+    if (ctx->argc != 2 && ctx->argc != 3 && ctx->argc != 5) {
+        ctl_fatal("cannot specify priority without match");
+    }
+
+    if (ctx->argc == 2) {
+        /* If direction, priority, and match are not specified, delete
+         * all ACLs. */
+        nbrec_logical_switch_verify_acls(ls);
+        nbrec_logical_switch_set_acls(ls, NULL, 0);
+        return;
+    }
+
+    const char *direction = parse_direction(ctx->argv[2]);
+
+    /* If priority and match are not specified, delete all ACLs with the
+     * specified direction. */
+    if (ctx->argc == 3) {
+        struct nbrec_acl **new_acls = xmalloc(sizeof *new_acls * ls->n_acls);
+
+        int n_acls = 0;
+        for (size_t i = 0; i < ls->n_acls; i++) {
+            if (strcmp(direction, ls->acls[i]->direction)) {
+                new_acls[n_acls++] = ls->acls[i];
+            }
+        }
 
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
+        nbrec_logical_switch_verify_acls(ls);
+        nbrec_logical_switch_set_acls(ls, new_acls, n_acls);
+        free(new_acls);
         return;
     }
 
-    nbrec_logical_port_set_type(lport, type);
+    int64_t priority = parse_priority(ctx->argv[3]);
+
+    /* Remove the matching rule. */
+    for (size_t i = 0; i < ls->n_acls; i++) {
+        struct nbrec_acl *acl = ls->acls[i];
+
+        if (priority == acl->priority && !strcmp(ctx->argv[4], acl->match) &&
+             !strcmp(direction, acl->direction)) {
+            struct nbrec_acl **new_acls
+                = xmemdup(ls->acls, sizeof *new_acls * ls->n_acls);
+            new_acls[i] = ls->acls[ls->n_acls - 1];
+            nbrec_logical_switch_verify_acls(ls);
+            nbrec_logical_switch_set_acls(ls, new_acls,
+                                          ls->n_acls - 1);
+            free(new_acls);
+            return;
+        }
+    }
+}
+\f
+static void
+nbctl_lr_add(struct ctl_context *ctx)
+{
+    const char *lr_name = ctx->argc == 2 ? ctx->argv[1] : NULL;
+
+    bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
+    bool add_duplicate = shash_find(&ctx->options, "--add-duplicate") != NULL;
+    if (may_exist && add_duplicate) {
+        ctl_fatal("--may-exist and --add-duplicate may not be used together");
+    }
+
+    if (lr_name) {
+        if (!add_duplicate) {
+            const struct nbrec_logical_router *lr;
+            NBREC_LOGICAL_ROUTER_FOR_EACH (lr, ctx->idl) {
+                if (!strcmp(lr->name, lr_name)) {
+                    if (may_exist) {
+                        return;
+                    }
+                    ctl_fatal("%s: a router with this name already exists",
+                              lr_name);
+                }
+            }
+        }
+    } else if (may_exist) {
+        ctl_fatal("--may-exist requires specifying a name");
+    } else if (add_duplicate) {
+        ctl_fatal("--add-duplicate requires specifying a name");
+    }
+
+    struct nbrec_logical_router *lr;
+    lr = nbrec_logical_router_insert(ctx->txn);
+    if (lr_name) {
+        nbrec_logical_router_set_name(lr, lr_name);
+    }
 }
 
 static void
-do_lport_get_type(struct ovs_cmdl_context *ctx)
+nbctl_lr_del(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
+    bool must_exist = !shash_find(&ctx->options, "--if-exists");
     const char *id = ctx->argv[1];
-    const struct nbrec_logical_port *lport;
+    const struct nbrec_logical_router *lr;
 
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
+    lr = lr_by_name_or_uuid(ctx, id, must_exist);
+    if (!lr) {
         return;
     }
 
-    printf("%s\n", lport->type);
+    nbrec_logical_router_delete(lr);
 }
 
 static void
-do_lport_set_options(struct ovs_cmdl_context *ctx)
+nbctl_lr_list(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const char *id = ctx->argv[1];
-    const struct nbrec_logical_port *lport;
-    size_t i;
-    struct smap options = SMAP_INITIALIZER(&options);
+    const struct nbrec_logical_router *lr;
+    struct smap lrs;
 
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
-        return;
+    smap_init(&lrs);
+    NBREC_LOGICAL_ROUTER_FOR_EACH(lr, ctx->idl) {
+        smap_add_format(&lrs, lr->name, UUID_FMT " (%s)",
+                        UUID_ARGS(&lr->header_.uuid), lr->name);
     }
+    const struct smap_node **nodes = smap_sort(&lrs);
+    for (size_t i = 0; i < smap_count(&lrs); i++) {
+        const struct smap_node *node = nodes[i];
+        ds_put_format(&ctx->output, "%s\n", node->value);
+    }
+    smap_destroy(&lrs);
+    free(nodes);
+}
 
-    for (i = 2; i < ctx->argc; i++) {
-        char *key, *value;
-        value = xstrdup(ctx->argv[i]);
-        key = strsep(&value, "=");
-        if (value) {
-            smap_add(&options, key, value);
+/* The caller must free the returned string. */
+static char *
+normalize_ipv4_prefix(ovs_be32 ipv4, unsigned int plen)
+{
+    ovs_be32 network = ipv4 & be32_prefix_mask(plen);
+    if (plen == 32) {
+        return xasprintf(IP_FMT, IP_ARGS(network));
+    } else {
+        return xasprintf(IP_FMT"/%d", IP_ARGS(network), plen);
+    }
+}
+
+/* The caller must free the returned string. */
+static char *
+normalize_ipv6_prefix(struct in6_addr ipv6, unsigned int plen)
+{
+    char network_s[INET6_ADDRSTRLEN];
+
+    struct in6_addr mask = ipv6_create_mask(plen);
+    struct in6_addr network = ipv6_addr_bitand(&ipv6, &mask);
+
+    inet_ntop(AF_INET6, &network, network_s, INET6_ADDRSTRLEN);
+    if (plen == 128) {
+        return xasprintf("%s", network_s);
+    } else {
+        return xasprintf("%s/%d", network_s, plen);
+    }
+}
+
+/* The caller must free the returned string. */
+static char *
+normalize_prefix_str(const char *orig_prefix)
+{
+    unsigned int plen;
+    ovs_be32 ipv4;
+    char *error;
+
+    error = ip_parse_cidr(orig_prefix, &ipv4, &plen);
+    if (!error) {
+        return normalize_ipv4_prefix(ipv4, plen);
+    } else {
+        struct in6_addr ipv6;
+        free(error);
+
+        error = ipv6_parse_cidr(orig_prefix, &ipv6, &plen);
+        if (error) {
+            free(error);
+            return NULL;
         }
-        free(key);
+        return normalize_ipv6_prefix(ipv6, plen);
     }
+}
+\f
+static void
+nbctl_lr_route_add(struct ctl_context *ctx)
+{
+    const struct nbrec_logical_router *lr;
+    lr = lr_by_name_or_uuid(ctx, ctx->argv[1], true);
+    char *prefix, *next_hop;
 
-    nbrec_logical_port_set_options(lport, &options);
+    prefix = normalize_prefix_str(ctx->argv[2]);
+    if (!prefix) {
+        ctl_fatal("bad prefix argument: %s", ctx->argv[2]);
+    }
 
-    smap_destroy(&options);
+    next_hop = normalize_prefix_str(ctx->argv[3]);
+    if (!next_hop) {
+        ctl_fatal("bad next hop argument: %s", ctx->argv[3]);
+    }
+
+    if (strchr(prefix, '.')) {
+        ovs_be32 hop_ipv4;
+        if (!ip_parse(ctx->argv[3], &hop_ipv4)) {
+            ctl_fatal("bad IPv4 nexthop argument: %s", ctx->argv[3]);
+        }
+    } else {
+        struct in6_addr hop_ipv6;
+        if (!ipv6_parse(ctx->argv[3], &hop_ipv6)) {
+            ctl_fatal("bad IPv6 nexthop argument: %s", ctx->argv[3]);
+        }
+    }
+
+    bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
+    for (int i = 0; i < lr->n_static_routes; i++) {
+        const struct nbrec_logical_router_static_route *route
+            = lr->static_routes[i];
+        char *rt_prefix;
+
+        rt_prefix = normalize_prefix_str(lr->static_routes[i]->ip_prefix);
+        if (!rt_prefix) {
+            /* Ignore existing prefix we couldn't parse. */
+            continue;
+        }
+
+        if (strcmp(rt_prefix, prefix)) {
+            free(rt_prefix);
+            continue;
+        }
+
+        if (!may_exist) {
+            ctl_fatal("duplicate prefix: %s", prefix);
+        }
+
+        /* Update the next hop for an existing route. */
+        nbrec_logical_router_verify_static_routes(lr);
+        nbrec_logical_router_static_route_verify_ip_prefix(route);
+        nbrec_logical_router_static_route_verify_nexthop(route);
+        nbrec_logical_router_static_route_set_ip_prefix(route, prefix);
+        nbrec_logical_router_static_route_set_nexthop(route, next_hop);
+        if (ctx->argc == 5) {
+            nbrec_logical_router_static_route_set_output_port(route,
+                                                              ctx->argv[4]);
+        }
+        free(rt_prefix);
+        free(next_hop);
+        free(prefix);
+        return;
+    }
+
+    struct nbrec_logical_router_static_route *route;
+    route = nbrec_logical_router_static_route_insert(ctx->txn);
+    nbrec_logical_router_static_route_set_ip_prefix(route, prefix);
+    nbrec_logical_router_static_route_set_nexthop(route, next_hop);
+    if (ctx->argc == 5) {
+        nbrec_logical_router_static_route_set_output_port(route, ctx->argv[4]);
+    }
+
+    nbrec_logical_router_verify_static_routes(lr);
+    struct nbrec_logical_router_static_route **new_routes
+        = xmalloc(sizeof *new_routes * (lr->n_static_routes + 1));
+    memcpy(new_routes, lr->static_routes,
+           sizeof *new_routes * lr->n_static_routes);
+    new_routes[lr->n_static_routes] = route;
+    nbrec_logical_router_set_static_routes(lr, new_routes,
+                                           lr->n_static_routes + 1);
+    free(new_routes);
+    free(next_hop);
+    free(prefix);
 }
 
 static void
-do_lport_get_options(struct ovs_cmdl_context *ctx)
+nbctl_lr_route_del(struct ctl_context *ctx)
 {
-    struct nbctl_context *nb_ctx = ctx->pvt;
-    const char *id = ctx->argv[1];
-    const struct nbrec_logical_port *lport;
-    struct smap_node *node;
+    const struct nbrec_logical_router *lr;
+    lr = lr_by_name_or_uuid(ctx, ctx->argv[1], true);
 
-    lport = lport_by_name_or_uuid(nb_ctx, id);
-    if (!lport) {
+    if (ctx->argc == 2) {
+        /* If a prefix is not specified, delete all routes. */
+        nbrec_logical_router_set_static_routes(lr, NULL, 0);
         return;
     }
 
-    SMAP_FOR_EACH(node, &lport->options) {
-        printf("%s=%s\n", node->key, node->value);
+    char *prefix = normalize_prefix_str(ctx->argv[2]);
+    if (!prefix) {
+        ctl_fatal("bad prefix argument: %s", ctx->argv[2]);
     }
+
+    for (int i = 0; i < lr->n_static_routes; i++) {
+        char *rt_prefix = normalize_prefix_str(lr->static_routes[i]->ip_prefix);
+        if (!rt_prefix) {
+            /* Ignore existing prefix we couldn't parse. */
+            continue;
+        }
+
+        if (!strcmp(prefix, rt_prefix)) {
+            struct nbrec_logical_router_static_route **new_routes
+                = xmemdup(lr->static_routes,
+                          sizeof *new_routes * lr->n_static_routes);
+
+            new_routes[i] = lr->static_routes[lr->n_static_routes - 1];
+            nbrec_logical_router_verify_static_routes(lr);
+            nbrec_logical_router_set_static_routes(lr, new_routes,
+                                                 lr->n_static_routes - 1);
+            free(new_routes);
+            free(rt_prefix);
+            free(prefix);
+            return;
+        }
+        free(rt_prefix);
+    }
+
+    if (!shash_find(&ctx->options, "--if-exists")) {
+        ctl_fatal("no matching prefix: %s", prefix);
+    }
+    free(prefix);
 }
 \f
+static const struct nbrec_logical_router_port *
+lrp_by_name_or_uuid(struct ctl_context *ctx, const char *id, bool must_exist)
+{
+    const struct nbrec_logical_router_port *lrp = NULL;
+
+    struct uuid lrp_uuid;
+    bool is_uuid = uuid_from_string(&lrp_uuid, id);
+    if (is_uuid) {
+        lrp = nbrec_logical_router_port_get_for_uuid(ctx->idl, &lrp_uuid);
+    }
+
+    if (!lrp) {
+        NBREC_LOGICAL_ROUTER_PORT_FOR_EACH(lrp, ctx->idl) {
+            if (!strcmp(lrp->name, id)) {
+                break;
+            }
+        }
+    }
+
+    if (!lrp && must_exist) {
+        ctl_fatal("%s: port %s not found", id, is_uuid ? "UUID" : "name");
+    }
+
+    return lrp;
+}
+
+/* Returns the logical router that contains 'lrp'. */
+static const struct nbrec_logical_router *
+lrp_to_lr(const struct ovsdb_idl *idl,
+               const struct nbrec_logical_router_port *lrp)
+{
+    const struct nbrec_logical_router *lr;
+    NBREC_LOGICAL_ROUTER_FOR_EACH (lr, idl) {
+        for (size_t i = 0; i < lr->n_ports; i++) {
+            if (lr->ports[i] == lrp) {
+                return lr;
+            }
+        }
+    }
+
+    /* Can't happen because of the database schema */
+    ctl_fatal("port %s is not part of any logical router",
+              lrp->name);
+}
+
+static const char *
+lr_get_name(const struct nbrec_logical_router *lr, char uuid_s[UUID_LEN + 1],
+            size_t uuid_s_size)
+{
+    if (lr->name[0]) {
+        return lr->name;
+    }
+    snprintf(uuid_s, uuid_s_size, UUID_FMT, UUID_ARGS(&lr->header_.uuid));
+    return uuid_s;
+}
+
 static void
-parse_options(int argc, char *argv[])
+nbctl_lrp_add(struct ctl_context *ctx)
 {
-    enum {
-        VLOG_OPTION_ENUMS,
-    };
-    static const struct option long_options[] = {
-        {"db", required_argument, NULL, 'd'},
-        {"help", no_argument, NULL, 'h'},
-        {"options", no_argument, NULL, 'o'},
-        {"version", no_argument, NULL, 'V'},
-        VLOG_LONG_OPTIONS,
-        STREAM_SSL_LONG_OPTIONS,
-        {NULL, 0, NULL, 0},
-    };
-    char *short_options = ovs_cmdl_long_options_to_short_options(long_options);
+    bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
 
-    for (;;) {
-        int c;
+    const struct nbrec_logical_router *lr;
+    lr = lr_by_name_or_uuid(ctx, ctx->argv[1], true);
 
-        c = getopt_long(argc, argv, short_options, long_options, NULL);
-        if (c == -1) {
+    const char *lrp_name = ctx->argv[2];
+    const char *mac = ctx->argv[3];
+    const char **networks = (const char **) &ctx->argv[4];
+
+    int n_networks = ctx->argc - 4;
+    for (int i = 4; i < ctx->argc; i++) {
+        if (strchr(ctx->argv[i], '=')) {
+            n_networks = i - 4;
             break;
         }
+    }
 
-        switch (c) {
-        VLOG_OPTION_HANDLERS;
-        STREAM_SSL_OPTION_HANDLERS;
+    if (!n_networks) {
+        ctl_fatal("%s: router port requires specifying a network", lrp_name);
+    }
 
-        case 'd':
-            db = optarg;
-            break;
+    char **settings = (char **) &ctx->argv[n_networks + 4];
+    int n_settings = ctx->argc - 4 - n_networks;
 
-        case 'h':
-            usage();
-            exit(EXIT_SUCCESS);
+    const struct nbrec_logical_router_port *lrp;
+    lrp = lrp_by_name_or_uuid(ctx, lrp_name, false);
+    if (lrp) {
+        if (!may_exist) {
+            ctl_fatal("%s: a port with this name already exists",
+                      lrp_name);
+        }
 
-        case 'o':
-            ovs_cmdl_print_options(long_options);
-            exit(EXIT_SUCCESS);
+        const struct nbrec_logical_router *bound_lr;
+        bound_lr = lrp_to_lr(ctx->idl, lrp);
+        if (bound_lr != lr) {
+            char uuid_s[UUID_LEN + 1];
+            ctl_fatal("%s: port already exists but in router %s", lrp_name,
+                      lr_get_name(bound_lr, uuid_s, sizeof uuid_s));
+        }
 
-        case 'V':
-            ovs_print_version(0, 0);
-            exit(EXIT_SUCCESS);
+        if (strcmp(mac, lrp->mac)) {
+            ctl_fatal("%s: port already exists with mac %s", lrp_name,
+                      lrp->mac);
+        }
 
-        default:
-            break;
+        struct sset new_networks = SSET_INITIALIZER(&new_networks);
+        for (int i = 0; i < n_networks; i++) {
+            sset_add(&new_networks, networks[i]);
         }
+
+        struct sset orig_networks = SSET_INITIALIZER(&orig_networks);
+        sset_add_array(&orig_networks, lrp->networks, lrp->n_networks);
+
+        if (!sset_equals(&orig_networks, &new_networks)) {
+            ctl_fatal("%s: port already exists with different network",
+                      lrp_name);
+        }
+
+        sset_destroy(&orig_networks);
+        sset_destroy(&new_networks);
+
+        /* Special-case sanity-check of peer ports. */
+        const char *peer = NULL;
+        for (int i = 0; i < n_settings; i++) {
+            if (!strncmp(settings[i], "peer=", 5)) {
+                peer = settings[i] + 5;
+                break;
+            }
+        }
+
+        if ((!peer != !lrp->peer) ||
+                (lrp->peer && strcmp(peer, lrp->peer))) {
+            ctl_fatal("%s: port already exists with mismatching peer",
+                      lrp_name);
+        }
+
+        return;
     }
 
-    if (!db) {
-        db = default_db();
+    struct eth_addr ea;
+    if (!ovs_scan(mac, ETH_ADDR_SCAN_FMT, ETH_ADDR_SCAN_ARGS(ea))) {
+        ctl_fatal("%s: invalid mac address %s", lrp_name, mac);
     }
 
-    free(short_options);
+    for (int i = 0; i < n_networks; i++) {
+        ovs_be32 ipv4;
+        unsigned int plen;
+        char *error = ip_parse_cidr(networks[i], &ipv4, &plen);
+        if (error) {
+            free(error);
+            struct in6_addr ipv6;
+            error = ipv6_parse_cidr(networks[i], &ipv6, &plen);
+            if (error) {
+                free(error);
+                ctl_fatal("%s: invalid network address: %s", lrp_name,
+                          networks[i]);
+            }
+        }
+    }
+
+    /* Create the logical port. */
+    lrp = nbrec_logical_router_port_insert(ctx->txn);
+    nbrec_logical_router_port_set_name(lrp, lrp_name);
+    nbrec_logical_router_port_set_mac(lrp, mac);
+    nbrec_logical_router_port_set_networks(lrp, networks, n_networks);
+
+    for (int i = 0; i < n_settings; i++) {
+        ctl_set_column("Logical_Router_Port", &lrp->header_, settings[i],
+                       ctx->symtab);
+    }
+
+    /* Insert the logical port into the logical router. */
+    nbrec_logical_router_verify_ports(lr);
+    struct nbrec_logical_router_port **new_ports = xmalloc(sizeof *new_ports *
+                                                        (lr->n_ports + 1));
+    memcpy(new_ports, lr->ports, sizeof *new_ports * lr->n_ports);
+    new_ports[lr->n_ports] = CONST_CAST(struct nbrec_logical_router_port *,
+                                             lrp);
+    nbrec_logical_router_set_ports(lr, new_ports, lr->n_ports + 1);
+    free(new_ports);
+}
+
+/* Removes logical router port 'lr->ports[idx]'. */
+static void
+remove_lrp(const struct nbrec_logical_router *lr, size_t idx)
+{
+    const struct nbrec_logical_router_port *lrp = lr->ports[idx];
+
+    /* First remove 'lrp' from the array of ports.  This is what will
+     * actually cause the logical port to be deleted when the transaction is
+     * sent to the database server (due to garbage collection). */
+    struct nbrec_logical_router_port **new_ports
+        = xmemdup(lr->ports, sizeof *new_ports * lr->n_ports);
+    new_ports[idx] = new_ports[lr->n_ports - 1];
+    nbrec_logical_router_verify_ports(lr);
+    nbrec_logical_router_set_ports(lr, new_ports, lr->n_ports - 1);
+    free(new_ports);
+
+    /* Delete 'lrp' from the IDL.  This won't have a real effect on
+     * the database server (the IDL will suppress it in fact) but it
+     * means that it won't show up when we iterate with
+     * NBREC_LOGICAL_ROUTER_PORT_FOR_EACH later. */
+    nbrec_logical_router_port_delete(lrp);
+}
+
+static void
+nbctl_lrp_del(struct ctl_context *ctx)
+{
+    bool must_exist = !shash_find(&ctx->options, "--if-exists");
+    const struct nbrec_logical_router_port *lrp;
+
+    lrp = lrp_by_name_or_uuid(ctx, ctx->argv[1], must_exist);
+    if (!lrp) {
+        return;
+    }
+
+    /* Find the router that contains 'lrp', then delete it. */
+    const struct nbrec_logical_router *lr;
+    NBREC_LOGICAL_ROUTER_FOR_EACH (lr, ctx->idl) {
+        for (size_t i = 0; i < lr->n_ports; i++) {
+            if (lr->ports[i] == lrp) {
+                remove_lrp(lr, i);
+                return;
+            }
+        }
+    }
+
+    /* Can't happen because of the database schema. */
+    ctl_fatal("logical port %s is not part of any logical router",
+              ctx->argv[1]);
+}
+
+/* Print a list of logical router ports. */
+static void
+nbctl_lrp_list(struct ctl_context *ctx)
+{
+    const char *id = ctx->argv[1];
+    const struct nbrec_logical_router *lr;
+    struct smap lrps;
+    size_t i;
+
+    lr = lr_by_name_or_uuid(ctx, id, true);
+
+    smap_init(&lrps);
+    for (i = 0; i < lr->n_ports; i++) {
+        const struct nbrec_logical_router_port *lrp = lr->ports[i];
+        smap_add_format(&lrps, lrp->name, UUID_FMT " (%s)",
+                        UUID_ARGS(&lrp->header_.uuid), lrp->name);
+    }
+    const struct smap_node **nodes = smap_sort(&lrps);
+    for (i = 0; i < smap_count(&lrps); i++) {
+        const struct smap_node *node = nodes[i];
+        ds_put_format(&ctx->output, "%s\n", node->value);
+    }
+    smap_destroy(&lrps);
+    free(nodes);
+}
+
+/* Set the logical router port admin-enabled state. */
+static void
+nbctl_lrp_set_enabled(struct ctl_context *ctx)
+{
+    const char *id = ctx->argv[1];
+    const char *state = ctx->argv[2];
+    const struct nbrec_logical_router_port *lrp;
+
+    lrp = lrp_by_name_or_uuid(ctx, id, true);
+    if (!lrp) {
+        return;
+    }
+
+    bool enabled = parse_enabled(state);
+    nbrec_logical_router_port_set_enabled(lrp, &enabled, 1);
+}
+
+/* Print admin-enabled state for logical router port. */
+static void
+nbctl_lrp_get_enabled(struct ctl_context *ctx)
+{
+    const char *id = ctx->argv[1];
+    const struct nbrec_logical_router_port *lrp;
+
+    lrp = lrp_by_name_or_uuid(ctx, id, true);
+    if (!lrp) {
+        return;
+    }
+
+    ds_put_format(&ctx->output, "%s\n",
+                  !lrp->enabled ||
+                  *lrp->enabled ? "enabled" : "disabled");
+}
+\f
+struct ipv4_route {
+    int plen;
+    ovs_be32 addr;
+    const struct nbrec_logical_router_static_route *route;
+};
+
+static int
+ipv4_route_cmp(const void *route1_, const void *route2_)
+{
+    const struct ipv4_route *route1p = route1_;
+    const struct ipv4_route *route2p = route2_;
+
+    if (route1p->plen != route2p->plen) {
+        return route1p->plen > route2p->plen ? -1 : 1;
+    } else if (route1p->addr != route2p->addr) {
+        return ntohl(route1p->addr) < ntohl(route2p->addr) ? -1 : 1;
+    } else {
+        return 0;
+    }
 }
 
-static const struct ovs_cmdl_command all_commands[] = {
-    {
-        .name = "show",
-        .usage = "[LSWITCH]",
-        .min_args = 0,
-        .max_args = 1,
-        .handler = do_show,
-    },
-    {
-        .name = "lswitch-add",
-        .usage = "[LSWITCH]",
-        .min_args = 0,
-        .max_args = 1,
-        .handler = do_lswitch_add,
-    },
-    {
-        .name = "lswitch-del",
-        .usage = "LSWITCH",
-        .min_args = 1,
-        .max_args = 1,
-        .handler = do_lswitch_del,
-    },
-    {
-        .name = "lswitch-list",
-        .usage = "",
-        .min_args = 0,
-        .max_args = 0,
-        .handler = do_lswitch_list,
-    },
-    {
-        .name = "lswitch-set-external-id",
-        .usage = "LSWITCH KEY [VALUE]",
-        .min_args = 2,
-        .max_args = 3,
-        .handler = do_lswitch_set_external_id,
-    },
-    {
-        .name = "lswitch-get-external-id",
-        .usage = "LSWITCH [KEY]",
-        .min_args = 1,
-        .max_args = 2,
-        .handler = do_lswitch_get_external_id,
-    },
-    {
-        .name = "lport-add",
-        .usage = "LSWITCH LPORT [PARENT] [TAG]",
-        .min_args = 2,
-        .max_args = 4,
-        .handler = do_lport_add,
-    },
-    {
-        .name = "lport-del",
-        .usage = "LPORT",
-        .min_args = 1,
-        .max_args = 1,
-        .handler = do_lport_del,
-    },
-    {
-        .name = "lport-list",
-        .usage = "LSWITCH",
-        .min_args = 1,
-        .max_args = 1,
-        .handler = do_lport_list,
-    },
-    {
-        .name = "lport-get-parent",
-        .usage = "LPORT",
-        .min_args = 1,
-        .max_args = 1,
-        .handler = do_lport_get_parent,
-    },
-    {
-        .name = "lport-get-tag",
-        .usage = "LPORT",
-        .min_args = 1,
-        .max_args = 1,
-        .handler = do_lport_get_tag,
-    },
-    {
-        .name = "lport-set-external-id",
-        .usage = "LPORT KEY [VALUE]",
-        .min_args = 2,
-        .max_args = 3,
-        .handler = do_lport_set_external_id,
-    },
-    {
-        .name = "lport-get-external-id",
-        .usage = "LPORT [KEY]",
-        .min_args = 1,
-        .max_args = 2,
-        .handler = do_lport_get_external_id,
-    },
-    {
-        .name = "lport-set-macs",
-        .usage = "LPORT [MAC]...",
-        .min_args = 1,
-        /* Accept however many arguments the system will allow. */
-        .max_args = INT_MAX,
-        .handler = do_lport_set_macs,
-    },
-    {
-        .name = "lport-get-macs",
-        .usage = "LPORT",
-        .min_args = 1,
-        .max_args = 1,
-        .handler = do_lport_get_macs,
-    },
-    {
-        .name = "lport-set-port-security",
-        .usage = "LPORT [ADDRS]...",
-        .min_args = 0,
-        /* Accept however many arguments the system will allow. */
-        .max_args = INT_MAX,
-        .handler = do_lport_set_port_security,
-    },
-    {
-        .name = "lport-get-port-security",
-        .usage = "LPORT",
-        .min_args = 1,
-        .max_args = 1,
-        .handler = do_lport_get_port_security,
-    },
-    {
-        .name = "lport-get-up",
-        .usage = "LPORT",
-        .min_args = 1,
-        .max_args = 1,
-        .handler = do_lport_get_up,
-    },
-    {
-        .name = "lport-set-enabled",
-        .usage = "LPORT STATE",
-        .min_args = 2,
-        .max_args = 2,
-        .handler = do_lport_set_enabled,
-    },
-    {
-        .name = "lport-get-enabled",
-        .usage = "LPORT",
-        .min_args = 1,
-        .max_args = 1,
-        .handler = do_lport_get_enabled,
-    },
-    {
-        .name = "lport-set-type",
-        .usage = "LPORT TYPE",
-        .min_args = 2,
-        .max_args = 2,
-        .handler = do_lport_set_type,
-    },
-    {
-        .name = "lport-get-type",
-        .usage = "LPORT",
-        .min_args = 1,
-        .max_args = 1,
-        .handler = do_lport_get_type,
-    },
-    {
-        .name = "lport-set-options",
-        .usage = "LPORT KEY=VALUE [KEY=VALUE]...",
-        .min_args = 1,
-        .max_args = INT_MAX,
-        .handler = do_lport_set_options
-    },
-    {
-        .name = "lport-get-options",
-        .usage = "LPORT",
-        .min_args = 1,
-        .max_args = 1,
-        .handler = do_lport_get_options,
-    },
-
-    {
-        /* sentinel */
-        .name = NULL,
-    },
+struct ipv6_route {
+    int plen;
+    struct in6_addr addr;
+    const struct nbrec_logical_router_static_route *route;
 };
 
-static const struct ovs_cmdl_command *
-get_all_commands(void)
+static int
+ipv6_route_cmp(const void *route1_, const void *route2_)
 {
-    return all_commands;
+    const struct ipv6_route *route1p = route1_;
+    const struct ipv6_route *route2p = route2_;
+
+    if (route1p->plen != route2p->plen) {
+        return route1p->plen > route2p->plen ? -1 : 1;
+    }
+    return memcmp(&route1p->addr, &route2p->addr, sizeof(route1p->addr));
 }
 
-static const char *
-default_db(void)
+static void
+print_route(const struct nbrec_logical_router_static_route *route, struct ds *s)
 {
-    static char *def;
-    if (!def) {
-        def = getenv("OVN_NB_DB");
-        if (!def) {
-            def = xasprintf("unix:%s/db.sock", ovs_rundir());
+
+    char *prefix = normalize_prefix_str(route->ip_prefix);
+    char *next_hop = normalize_prefix_str(route->nexthop);
+    ds_put_format(s, "%25s %25s", prefix, next_hop);
+    free(prefix);
+    free(next_hop);
+
+    if (route->output_port) {
+        ds_put_format(s, " %s", route->output_port);
+    }
+    ds_put_char(s, '\n');
+}
+
+static void
+nbctl_lr_route_list(struct ctl_context *ctx)
+{
+    const struct nbrec_logical_router *lr;
+    struct ipv4_route *ipv4_routes;
+    struct ipv6_route *ipv6_routes;
+    size_t n_ipv4_routes = 0;
+    size_t n_ipv6_routes = 0;
+
+    lr = lr_by_name_or_uuid(ctx, ctx->argv[1], true);
+
+    ipv4_routes = xmalloc(sizeof *ipv4_routes * lr->n_static_routes);
+    ipv6_routes = xmalloc(sizeof *ipv6_routes * lr->n_static_routes);
+
+    for (int i = 0; i < lr->n_static_routes; i++) {
+        const struct nbrec_logical_router_static_route *route
+            = lr->static_routes[i];
+        unsigned int plen;
+        ovs_be32 ipv4;
+        char *error;
+
+        error = ip_parse_cidr(route->ip_prefix, &ipv4, &plen);
+        if (!error) {
+            ipv4_routes[n_ipv4_routes].plen = plen;
+            ipv4_routes[n_ipv4_routes].addr = ipv4;
+            ipv4_routes[n_ipv4_routes].route = route;
+            n_ipv4_routes++;
+        } else {
+            free(error);
+
+            struct in6_addr ipv6;
+            error = ipv6_parse_cidr(route->ip_prefix, &ipv6, &plen);
+            if (!error) {
+                ipv6_routes[n_ipv6_routes].plen = plen;
+                ipv6_routes[n_ipv6_routes].addr = ipv6;
+                ipv6_routes[n_ipv6_routes].route = route;
+                n_ipv6_routes++;
+            } else {
+                /* Invalid prefix. */
+                VLOG_WARN("router "UUID_FMT" (%s) has invalid prefix: %s",
+                          UUID_ARGS(&lr->header_.uuid), lr->name,
+                          route->ip_prefix);
+                free(error);
+                continue;
+            }
         }
     }
-    return def;
+
+    qsort(ipv4_routes, n_ipv4_routes, sizeof *ipv4_routes, ipv4_route_cmp);
+    qsort(ipv6_routes, n_ipv6_routes, sizeof *ipv6_routes, ipv6_route_cmp);
+
+    if (n_ipv4_routes) {
+        ds_put_cstr(&ctx->output, "IPv4 Routes\n");
+    }
+    for (int i = 0; i < n_ipv4_routes; i++) {
+        print_route(ipv4_routes[i].route, &ctx->output);
+    }
+
+    if (n_ipv6_routes) {
+        ds_put_format(&ctx->output, "%sIPv6 Routes\n",
+                      n_ipv4_routes ?  "\n" : "");
+    }
+    for (int i = 0; i < n_ipv6_routes; i++) {
+        print_route(ipv6_routes[i].route, &ctx->output);
+    }
+
+    free(ipv4_routes);
+    free(ipv6_routes);
 }
 
-int
-main(int argc, char *argv[])
+static const struct ctl_table_class tables[] = {
+    {&nbrec_table_logical_switch,
+     {{&nbrec_table_logical_switch, &nbrec_logical_switch_col_name, NULL},
+      {NULL, NULL, NULL}}},
+
+    {&nbrec_table_logical_switch_port,
+     {{&nbrec_table_logical_switch_port, &nbrec_logical_switch_port_col_name,
+       NULL},
+      {NULL, NULL, NULL}}},
+
+    {&nbrec_table_acl,
+     {{NULL, NULL, NULL},
+      {NULL, NULL, NULL}}},
+
+    {&nbrec_table_load_balancer,
+     {{NULL, NULL, NULL},
+      {NULL, NULL, NULL}}},
+
+    {&nbrec_table_logical_router,
+     {{&nbrec_table_logical_router, &nbrec_logical_router_col_name, NULL},
+      {NULL, NULL, NULL}}},
+
+    {&nbrec_table_logical_router_port,
+     {{&nbrec_table_logical_router_port, &nbrec_logical_router_port_col_name,
+       NULL},
+      {NULL, NULL, NULL}}},
+
+    {&nbrec_table_logical_router_static_route,
+     {{&nbrec_table_logical_router_static_route, NULL,
+       NULL},
+      {NULL, NULL, NULL}}},
+
+    {&nbrec_table_nat,
+     {{&nbrec_table_nat, NULL,
+       NULL},
+      {NULL, NULL, NULL}}},
+
+    {&nbrec_table_address_set,
+     {{&nbrec_table_address_set, &nbrec_address_set_col_name, NULL},
+      {NULL, NULL, NULL}}},
+
+    {NULL, {{NULL, NULL, NULL}, {NULL, NULL, NULL}}}
+};
+\f
+static void
+run_prerequisites(struct ctl_command *commands, size_t n_commands,
+                  struct ovsdb_idl *idl)
 {
-    extern struct vlog_module VLM_reconnect;
-    struct ovs_cmdl_context ctx;
-    struct nbctl_context nb_ctx = { .idl = NULL, };
-    enum ovsdb_idl_txn_status txn_status;
-    unsigned int seqno;
-    int res = 0;
-    char *args;
+    struct ctl_command *c;
 
-    fatal_ignore_sigpipe();
-    set_program_name(argv[0]);
-    vlog_set_levels(NULL, VLF_CONSOLE, VLL_WARN);
-    vlog_set_levels(&VLM_reconnect, VLF_ANY_DESTINATION, VLL_WARN);
-    parse_options(argc, argv);
-    nbrec_init();
+    for (c = commands; c < &commands[n_commands]; c++) {
+        if (c->syntax->prerequisites) {
+            struct ctl_context ctx;
 
-    args = process_escape_args(argv);
+            ds_init(&c->output);
+            c->table = NULL;
 
-    nb_ctx.idl = ovsdb_idl_create(db, &nbrec_idl_class, true, false);
-    ctx.pvt = &nb_ctx;
-    ctx.argc = argc - optind;
-    ctx.argv = argv + optind;
+            ctl_context_init(&ctx, c, idl, NULL, NULL, NULL);
+            (c->syntax->prerequisites)(&ctx);
+            ctl_context_done(&ctx, c);
 
-    seqno = ovsdb_idl_get_seqno(nb_ctx.idl);
-    for (;;) {
-        ovsdb_idl_run(nb_ctx.idl);
+            ovs_assert(!c->output.string);
+            ovs_assert(!c->table);
+        }
+    }
+}
 
-        if (!ovsdb_idl_is_alive(nb_ctx.idl)) {
-            int retval = ovsdb_idl_get_last_error(nb_ctx.idl);
-            VLOG_ERR("%s: database connection failed (%s)",
-                    db, ovs_retval_to_string(retval));
-            res = 1;
-            break;
+static bool
+do_nbctl(const char *args, struct ctl_command *commands, size_t n_commands,
+         struct ovsdb_idl *idl)
+{
+    struct ovsdb_idl_txn *txn;
+    enum ovsdb_idl_txn_status status;
+    struct ovsdb_symbol_table *symtab;
+    struct ctl_context ctx;
+    struct ctl_command *c;
+    struct shash_node *node;
+    char *error = NULL;
+
+    txn = the_idl_txn = ovsdb_idl_txn_create(idl);
+    if (dry_run) {
+        ovsdb_idl_txn_set_dry_run(txn);
+    }
+
+    ovsdb_idl_txn_add_comment(txn, "ovs-nbctl: %s", args);
+
+    symtab = ovsdb_symbol_table_create();
+    for (c = commands; c < &commands[n_commands]; c++) {
+        ds_init(&c->output);
+        c->table = NULL;
+    }
+    ctl_context_init(&ctx, NULL, idl, txn, symtab, NULL);
+    for (c = commands; c < &commands[n_commands]; c++) {
+        ctl_context_init_command(&ctx, c);
+        if (c->syntax->run) {
+            (c->syntax->run)(&ctx);
         }
+        ctl_context_done_command(&ctx, c);
 
-        if (seqno != ovsdb_idl_get_seqno(nb_ctx.idl)) {
-            nb_ctx.txn = ovsdb_idl_txn_create(nb_ctx.idl);
-            ovsdb_idl_txn_add_comment(nb_ctx.txn, "ovn-nbctl: %s", args);
-            ovs_cmdl_run_command(&ctx, get_all_commands());
-            txn_status = ovsdb_idl_txn_commit_block(nb_ctx.txn);
-            if (txn_status == TXN_TRY_AGAIN) {
-                ovsdb_idl_txn_destroy(nb_ctx.txn);
-                nb_ctx.txn = NULL;
-                continue;
+        if (ctx.try_again) {
+            ctl_context_done(&ctx, NULL);
+            goto try_again;
+        }
+    }
+    ctl_context_done(&ctx, NULL);
+
+    SHASH_FOR_EACH (node, &symtab->sh) {
+        struct ovsdb_symbol *symbol = node->data;
+        if (!symbol->created) {
+            ctl_fatal("row id \"%s\" is referenced but never created (e.g. "
+                      "with \"-- --id=%s create ...\")",
+                      node->name, node->name);
+        }
+        if (!symbol->strong_ref) {
+            if (!symbol->weak_ref) {
+                VLOG_WARN("row id \"%s\" was created but no reference to it "
+                          "was inserted, so it will not actually appear in "
+                          "the database", node->name);
             } else {
-                break;
+                VLOG_WARN("row id \"%s\" was created but only a weak "
+                          "reference to it was inserted, so it will not "
+                          "actually appear in the database", node->name);
+            }
+        }
+    }
+
+    status = ovsdb_idl_txn_commit_block(txn);
+    if (status == TXN_UNCHANGED || status == TXN_SUCCESS) {
+        for (c = commands; c < &commands[n_commands]; c++) {
+            if (c->syntax->postprocess) {
+                ctl_context_init(&ctx, c, idl, txn, symtab, NULL);
+                (c->syntax->postprocess)(&ctx);
+                ctl_context_done(&ctx, c);
             }
         }
+    }
+    error = xstrdup(ovsdb_idl_txn_get_error(txn));
 
-        if (seqno == ovsdb_idl_get_seqno(nb_ctx.idl)) {
-            ovsdb_idl_wait(nb_ctx.idl);
-            poll_block();
+    switch (status) {
+    case TXN_UNCOMMITTED:
+    case TXN_INCOMPLETE:
+        OVS_NOT_REACHED();
+
+    case TXN_ABORTED:
+        /* Should not happen--we never call ovsdb_idl_txn_abort(). */
+        ctl_fatal("transaction aborted");
+
+    case TXN_UNCHANGED:
+    case TXN_SUCCESS:
+        break;
+
+    case TXN_TRY_AGAIN:
+        goto try_again;
+
+    case TXN_ERROR:
+        ctl_fatal("transaction error: %s", error);
+
+    case TXN_NOT_LOCKED:
+        /* Should not happen--we never call ovsdb_idl_set_lock(). */
+        ctl_fatal("database not locked");
+
+    default:
+        OVS_NOT_REACHED();
+    }
+    free(error);
+
+    ovsdb_symbol_table_destroy(symtab);
+
+    for (c = commands; c < &commands[n_commands]; c++) {
+        struct ds *ds = &c->output;
+
+        if (c->table) {
+            table_print(c->table, &table_style);
+        } else if (oneline) {
+            size_t j;
+
+            ds_chomp(ds, '\n');
+            for (j = 0; j < ds->length; j++) {
+                int ch = ds->string[j];
+                switch (ch) {
+                case '\n':
+                    fputs("\\n", stdout);
+                    break;
+
+                case '\\':
+                    fputs("\\\\", stdout);
+                    break;
+
+                default:
+                    putchar(ch);
+                }
+            }
+            putchar('\n');
+        } else {
+            fputs(ds_cstr(ds), stdout);
         }
+        ds_destroy(&c->output);
+        table_destroy(c->table);
+        free(c->table);
+
+        shash_destroy_free_data(&c->options);
+    }
+    free(commands);
+    ovsdb_idl_txn_destroy(txn);
+    ovsdb_idl_destroy(idl);
+
+    return true;
+
+try_again:
+    /* Our transaction needs to be rerun, or a prerequisite was not met.  Free
+     * resources and return so that the caller can try again. */
+    if (txn) {
+        ovsdb_idl_txn_abort(txn);
+        ovsdb_idl_txn_destroy(txn);
+        the_idl_txn = NULL;
+    }
+    ovsdb_symbol_table_destroy(symtab);
+    for (c = commands; c < &commands[n_commands]; c++) {
+        ds_destroy(&c->output);
+        table_destroy(c->table);
+        free(c->table);
     }
+    free(error);
+    return false;
+}
 
-    if (nb_ctx.txn) {
-        ovsdb_idl_txn_destroy(nb_ctx.txn);
+/* Frees the current transaction and the underlying IDL and then calls
+ * exit(status).
+ *
+ * Freeing the transaction and the IDL is not strictly necessary, but it makes
+ * for a clean memory leak report from valgrind in the normal case.  That makes
+ * it easier to notice real memory leaks. */
+static void
+nbctl_exit(int status)
+{
+    if (the_idl_txn) {
+        ovsdb_idl_txn_abort(the_idl_txn);
+        ovsdb_idl_txn_destroy(the_idl_txn);
     }
-    ovsdb_idl_destroy(nb_ctx.idl);
-    free(args);
+    ovsdb_idl_destroy(the_idl);
+    exit(status);
+}
+
+static const struct ctl_command_syntax nbctl_commands[] = {
+    { "show", 0, 1, "[SWITCH]", NULL, nbctl_show, NULL, "", RO },
+
+    /* logical switch commands. */
+    { "ls-add", 0, 1, "[SWITCH]", NULL, nbctl_ls_add, NULL,
+      "--may-exist,--add-duplicate", RW },
+    { "ls-del", 1, 1, "SWITCH", NULL, nbctl_ls_del, NULL, "--if-exists", RW },
+    { "ls-list", 0, 0, "", NULL, nbctl_ls_list, NULL, "", RO },
+
+    /* acl commands. */
+    { "acl-add", 5, 5, "SWITCH DIRECTION PRIORITY MATCH ACTION", NULL,
+      nbctl_acl_add, NULL, "--log", RW },
+    { "acl-del", 1, 4, "SWITCH [DIRECTION [PRIORITY MATCH]]", NULL,
+      nbctl_acl_del, NULL, "", RW },
+    { "acl-list", 1, 1, "SWITCH", NULL, nbctl_acl_list, NULL, "", RO },
+
+    /* logical switch port commands. */
+    { "lsp-add", 2, 4, "SWITCH PORT [PARENT] [TAG]", NULL, nbctl_lsp_add,
+      NULL, "--may-exist", RW },
+    { "lsp-del", 1, 1, "PORT", NULL, nbctl_lsp_del, NULL, "--if-exists", RW },
+    { "lsp-list", 1, 1, "SWITCH", NULL, nbctl_lsp_list, NULL, "", RO },
+    { "lsp-get-parent", 1, 1, "PORT", NULL, nbctl_lsp_get_parent, NULL,
+      "", RO },
+    { "lsp-get-tag", 1, 1, "PORT", NULL, nbctl_lsp_get_tag, NULL, "", RO },
+    { "lsp-set-addresses", 1, INT_MAX, "PORT [ADDRESS]...", NULL,
+      nbctl_lsp_set_addresses, NULL, "", RW },
+    { "lsp-get-addresses", 1, 1, "PORT", NULL, nbctl_lsp_get_addresses, NULL,
+      "", RO },
+    { "lsp-set-port-security", 0, INT_MAX, "PORT [ADDRS]...", NULL,
+      nbctl_lsp_set_port_security, NULL, "", RW },
+    { "lsp-get-port-security", 1, 1, "PORT", NULL,
+      nbctl_lsp_get_port_security, NULL, "", RO },
+    { "lsp-get-up", 1, 1, "PORT", NULL, nbctl_lsp_get_up, NULL, "", RO },
+    { "lsp-set-enabled", 2, 2, "PORT STATE", NULL, nbctl_lsp_set_enabled,
+      NULL, "", RW },
+    { "lsp-get-enabled", 1, 1, "PORT", NULL, nbctl_lsp_get_enabled, NULL,
+      "", RO },
+    { "lsp-set-type", 2, 2, "PORT TYPE", NULL, nbctl_lsp_set_type, NULL,
+      "", RW },
+    { "lsp-get-type", 1, 1, "PORT", NULL, nbctl_lsp_get_type, NULL, "", RO },
+    { "lsp-set-options", 1, INT_MAX, "PORT KEY=VALUE [KEY=VALUE]...", NULL,
+      nbctl_lsp_set_options, NULL, "", RW },
+    { "lsp-get-options", 1, 1, "PORT", NULL, nbctl_lsp_get_options, NULL,
+      "", RO },
+
+    /* logical router commands. */
+    { "lr-add", 0, 1, "[ROUTER]", NULL, nbctl_lr_add, NULL,
+      "--may-exist,--add-duplicate", RW },
+    { "lr-del", 1, 1, "ROUTER", NULL, nbctl_lr_del, NULL, "--if-exists", RW },
+    { "lr-list", 0, 0, "", NULL, nbctl_lr_list, NULL, "", RO },
+
+    /* logical router port commands. */
+    { "lrp-add", 4, INT_MAX,
+      "ROUTER PORT MAC NETWORK... [COLUMN[:KEY]=VALUE]...",
+      NULL, nbctl_lrp_add, NULL, "--may-exist", RW },
+    { "lrp-del", 1, 1, "PORT", NULL, nbctl_lrp_del, NULL, "--if-exists", RW },
+    { "lrp-list", 1, 1, "ROUTER", NULL, nbctl_lrp_list, NULL, "", RO },
+    { "lrp-set-enabled", 2, 2, "PORT STATE", NULL, nbctl_lrp_set_enabled,
+      NULL, "", RW },
+    { "lrp-get-enabled", 1, 1, "PORT", NULL, nbctl_lrp_get_enabled,
+      NULL, "", RO },
+
+    /* logical router route commands. */
+    { "lr-route-add", 3, 4, "ROUTER PREFIX NEXTHOP [PORT]", NULL,
+      nbctl_lr_route_add, NULL, "--may-exist", RW },
+    { "lr-route-del", 1, 2, "ROUTER [PREFIX]", NULL, nbctl_lr_route_del,
+      NULL, "--if-exists", RW },
+    { "lr-route-list", 1, 1, "ROUTER", NULL, nbctl_lr_route_list, NULL,
+      "", RO },
+
+    {NULL, 0, 0, NULL, NULL, NULL, NULL, "", RO},
+};
 
-    exit(res);
+/* Registers nbctl and common db commands. */
+static void
+nbctl_cmd_init(void)
+{
+    ctl_init(tables, NULL, nbctl_exit);
+    ctl_register_commands(nbctl_commands);
 }