diff --git a/utilities/automake.mk b/utilities/automake.mk
index c4a6d248c274..50c0cfded018 100644
--- a/utilities/automake.mk
+++ b/utilities/automake.mk
@@ -71,7 +71,10 @@ utilities/ovn-lib: $(top_builddir)/config.status
 
 # ovn-nbctl
 bin_PROGRAMS += utilities/ovn-nbctl
-utilities_ovn_nbctl_SOURCES = utilities/ovn-nbctl.c
+utilities_ovn_nbctl_SOURCES = \
+    utilities/ovn-dbctl.c \
+    utilities/ovn-dbctl.h \
+    utilities/ovn-nbctl.c
 utilities_ovn_nbctl_LDADD = lib/libovn.la $(OVSDB_LIBDIR)/libovsdb.la $(OVS_LIBDIR)/libopenvswitch.la
 
 # ovn-sbctl
diff --git a/utilities/ovn-dbctl.c b/utilities/ovn-dbctl.c
new file mode 100644
index 000000000000..730e84f5e770
--- /dev/null
+++ b/utilities/ovn-dbctl.c
@@ -0,0 +1,1215 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+#include "ovn-dbctl.h"
+
+#include <getopt.h>
+
+#include "command-line.h"
+#include "daemon.h"
+#include "db-ctl-base.h"
+#include "fatal-signal.h"
+#include "jsonrpc.h"
+#include "memory.h"
+#include "openvswitch/poll-loop.h"
+#include "openvswitch/vlog.h"
+#include "ovn-util.h"
+#include "ovsdb-idl.h"
+#include "process.h"
+#include "simap.h"
+#include "stream-ssl.h"
+#include "svec.h"
+#include "table.h"
+#include "timer.h"
+#include "unixctl.h"
+#include "util.h"
+
+VLOG_DEFINE_THIS_MODULE(ovn_dbctl);
+
+/* --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;
+
+/* --wait=TYPE: Wait for configuration change to take effect? */
+static enum nbctl_wait_type wait_type = NBCTL_WAIT_NONE;
+
+static bool print_wait_time = false;
+
+/* --timeout: Time to wait for a connection to 'db'. */
+static unsigned 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
+ * ovn_dbctl_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;
+
+/* --leader-only, --no-leader-only: Only accept the leader in a cluster. */
+static int leader_only = true;
+
+/* --shuffle-remotes, --no-shuffle-remotes: Shuffle the order of remotes that
+ * are specified in the connetion method string. */
+static int shuffle_remotes = true;
+
+/* --unixctl-path: Path to use for unixctl server, for "monitor" and "snoop"
+     commands. */
+static char *unixctl_path;
+
+static unixctl_cb_func server_cmd_exit;
+static unixctl_cb_func server_cmd_run;
+
+static struct option *get_all_options(void);
+static bool has_option(const struct ovs_cmdl_parsed_option *, size_t n,
+                       int option);
+static void dbctl_client(const struct ovn_dbctl_options *dbctl_options,
+                         const char *socket_name,
+                         const struct ovs_cmdl_parsed_option *, size_t n,
+                         int argc, char *argv[]);
+static bool will_detach(const struct ovs_cmdl_parsed_option *, size_t n);
+static void apply_options_direct(const struct ovn_dbctl_options *dbctl_options,
+                                 const struct ovs_cmdl_parsed_option *,
+                                 size_t n, struct shash *local_options);
+static char * OVS_WARN_UNUSED_RESULT run_prerequisites(
+    const struct ovn_dbctl_options *dbctl_options,
+    struct ctl_command[], size_t n_commands, struct ovsdb_idl *);
+static char * OVS_WARN_UNUSED_RESULT do_dbctl(
+    const struct ovn_dbctl_options *dbctl_options,
+    const char *args, struct ctl_command *, size_t n,
+    struct ovsdb_idl *, const struct timer *, bool *retry);
+static char * OVS_WARN_UNUSED_RESULT main_loop(
+    const struct ovn_dbctl_options *, const char *args,
+    struct ctl_command *commands, size_t n_commands,
+    struct ovsdb_idl *idl, const struct timer *);
+static void server_loop(const struct ovn_dbctl_options *dbctl_options,
+                        struct ovsdb_idl *idl, int argc, char *argv[]);
+static void ovn_dbctl_exit(int status);
+
+int
+ovn_dbctl_main(int argc, char *argv[],
+               const struct ovn_dbctl_options *dbctl_options)
+{
+    struct ovsdb_idl *idl;
+    struct shash local_options;
+
+    ovn_set_program_name(argv[0]);
+    fatal_ignore_sigpipe();
+    vlog_set_levels(NULL, VLF_CONSOLE, VLL_WARN);
+    vlog_set_levels_from_string_assert("reconnect:warn");
+
+    ctl_init__(dbctl_options->idl_class,
+               dbctl_options->tables,
+               dbctl_options->cmd_show_table,
+               ovn_dbctl_exit);
+    ctl_register_commands(dbctl_options->commands);
+
+    /* Check if options are set via env var. */
+    char **argv_ = ovs_cmdl_env_parse_all(
+        &argc, argv, getenv(dbctl_options->options_env_var_name));
+
+    /* This utility has three operation modes:
+     *
+     *    - Direct: Executes commands by contacting ovsdb-server directly.
+     *
+     *    - Server: Runs in the background as a daemon waiting for requests
+     *      from a process running in client mode.
+     *
+     *    - Client: Executes commands by passing them to a process running in
+     *      the server mode.
+     *
+     * At this point we don't know what mode we're running in.  The mode partly
+     * depends on the command line.  So, for now we transform the command line
+     * into a parsed form, and figure out what to do with it later.
+     */
+    struct ovs_cmdl_parsed_option *parsed_options;
+    size_t n_parsed_options;
+    char *error_s = ovs_cmdl_parse_all(argc, argv_, get_all_options(),
+                                       &parsed_options, &n_parsed_options);
+    if (error_s) {
+        ctl_fatal("%s", error_s);
+    }
+
+    /* Now figure out the operation mode:
+     *
+     *    - A --detach option implies server mode.
+     *
+     *    - An OVN_??_DAEMON environment variable implies client mode.
+     *
+     *    - Otherwise, we're in direct mode. */
+    const char *socket_name = (unixctl_path ? unixctl_path
+                               : getenv(dbctl_options->daemon_env_var_name));
+    if (((socket_name && socket_name[0])
+         || has_option(parsed_options, n_parsed_options, 'u'))
+        && !will_detach(parsed_options, n_parsed_options)) {
+        dbctl_client(dbctl_options, socket_name,
+                     parsed_options, n_parsed_options, argc, argv_);
+    }
+
+    /* Parse command line. */
+    shash_init(&local_options);
+    apply_options_direct(dbctl_options,
+                         parsed_options, n_parsed_options, &local_options);
+    free(parsed_options);
+
+    bool daemon_mode = false;
+    if (get_detach()) {
+        if (argc != optind) {
+            ctl_fatal("non-option arguments not supported with --detach "
+                      "(use --help for help)");
+        }
+        daemon_mode = true;
+    }
+    /* Initialize IDL. */
+    idl = the_idl = ovsdb_idl_create_unconnected(dbctl_options->idl_class,
+                                                 true);
+    ovsdb_idl_set_shuffle_remotes(idl, shuffle_remotes);
+    /* "retry" is true iff in daemon mode. */
+    ovsdb_idl_set_remote(idl, db, daemon_mode);
+    ovsdb_idl_set_leader_only(idl, leader_only);
+
+    if (daemon_mode) {
+        server_loop(dbctl_options, idl, argc, argv_);
+    } else {
+        struct ctl_command *commands;
+        size_t n_commands;
+        char *error;
+
+        error = ctl_parse_commands(argc - optind, argv_ + optind,
+                                   &local_options, &commands, &n_commands);
+        if (error) {
+            ctl_fatal("%s", error);
+        }
+
+        char *args = process_escape_args(argv_);
+        VLOG(ctl_might_write_to_db(commands, n_commands) ? VLL_INFO : VLL_DBG,
+             "Called as %s", args);
+
+        ctl_timeout_setup(timeout);
+
+        error = run_prerequisites(dbctl_options, commands, n_commands, idl);
+        if (error) {
+            goto cleanup;
+        }
+
+        error = main_loop(dbctl_options, args, commands, n_commands, idl, NULL);
+
+cleanup:
+        free(args);
+
+        struct ctl_command *c;
+        for (c = commands; c < &commands[n_commands]; c++) {
+            ds_destroy(&c->output);
+            table_destroy(c->table);
+            free(c->table);
+            shash_destroy_free_data(&c->options);
+        }
+        free(commands);
+        if (error) {
+            ctl_fatal("%s", error);
+        }
+    }
+
+    ovsdb_idl_destroy(idl);
+    idl = the_idl = NULL;
+
+    for (int i = 0; i < argc; i++) {
+        free(argv_[i]);
+    }
+    free(argv_);
+    exit(EXIT_SUCCESS);
+}
+
+static char *
+main_loop(const struct ovn_dbctl_options *dbctl_options,
+          const char *args, struct ctl_command *commands, size_t n_commands,
+          struct ovsdb_idl *idl, const struct timer *wait_timeout)
+{
+    unsigned int seqno;
+    bool idl_ready;
+
+    /* 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);
+
+    /* IDL might have already obtained the database copy during previous
+     * invocation. If so, we can't expect the sequence number to change before
+     * we issue any new requests. */
+    idl_ready = ovsdb_idl_has_ever_connected(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 (idl_ready || seqno != ovsdb_idl_get_seqno(idl)) {
+            idl_ready = false;
+            seqno = ovsdb_idl_get_seqno(idl);
+
+            bool retry;
+            char *error = do_dbctl(dbctl_options,
+                                   args, commands, n_commands, idl,
+                                   wait_timeout, &retry);
+            if (error) {
+                return error;
+            }
+            if (!retry) {
+                return NULL;
+            }
+        }
+
+        if (seqno == ovsdb_idl_get_seqno(idl)) {
+            ovsdb_idl_wait(idl);
+            poll_block();
+        }
+    }
+
+    return NULL;
+}
+
+/* All options that affect the main loop and are not external. */
+#define MAIN_LOOP_OPTION_ENUMS                  \
+        OPT_NO_WAIT,                            \
+        OPT_WAIT,                               \
+        OPT_PRINT_WAIT_TIME,                    \
+        OPT_DRY_RUN,                            \
+        OPT_ONELINE
+
+#define MAIN_LOOP_LONG_OPTIONS                                          \
+        {"no-wait", no_argument, NULL, OPT_NO_WAIT},                    \
+        {"wait", required_argument, NULL, OPT_WAIT},                    \
+        {"print-wait-time", no_argument, NULL, OPT_PRINT_WAIT_TIME},    \
+        {"dry-run", no_argument, NULL, OPT_DRY_RUN},                    \
+        {"oneline", no_argument, NULL, OPT_ONELINE},                    \
+        {"timeout", required_argument, NULL, 't'}
+
+enum {
+    OPT_DB = UCHAR_MAX + 1,
+    OPT_NO_SYSLOG,
+    OPT_LOCAL,
+    OPT_COMMANDS,
+    OPT_OPTIONS,
+    OPT_LEADER_ONLY,
+    OPT_NO_LEADER_ONLY,
+    OPT_SHUFFLE_REMOTES,
+    OPT_NO_SHUFFLE_REMOTES,
+    OPT_BOOTSTRAP_CA_CERT,
+    MAIN_LOOP_OPTION_ENUMS,
+    OVN_DAEMON_OPTION_ENUMS,
+    VLOG_OPTION_ENUMS,
+    TABLE_OPTION_ENUMS,
+    SSL_OPTION_ENUMS,
+};
+
+static char * OVS_WARN_UNUSED_RESULT
+handle_main_loop_option(int opt, const char *arg, bool *handled)
+{
+    ovs_assert(handled);
+    *handled = true;
+
+    switch (opt) {
+    case OPT_ONELINE:
+        oneline = true;
+        break;
+
+    case OPT_NO_WAIT:
+        wait_type = NBCTL_WAIT_NONE;
+        break;
+
+    case OPT_WAIT:
+        if (!strcmp(arg, "none")) {
+            wait_type = NBCTL_WAIT_NONE;
+        } else if (!strcmp(arg, "sb")) {
+            wait_type = NBCTL_WAIT_SB;
+        } else if (!strcmp(arg, "hv")) {
+            wait_type = NBCTL_WAIT_HV;
+        } else {
+            return xstrdup("argument to --wait must be "
+                           "\"none\", \"sb\", or \"hv\"");
+        }
+        break;
+
+    case OPT_PRINT_WAIT_TIME:
+        print_wait_time = true;
+        break;
+
+    case OPT_DRY_RUN:
+        dry_run = true;
+        break;
+
+    case 't':
+        if (!str_to_uint(arg, 10, &timeout) || !timeout) {
+            return xasprintf("value %s on -t or --timeout is invalid", arg);
+        }
+        break;
+
+    default:
+        *handled = false;
+        break;
+    }
+
+    return NULL;
+}
+
+static char * OVS_WARN_UNUSED_RESULT
+build_short_options(const struct option *long_options, bool print_errors)
+{
+    char *tmp, *short_options;
+
+    tmp = ovs_cmdl_long_options_to_short_options(long_options);
+    short_options = xasprintf("+%s%s", print_errors ? "" : ":", tmp);
+    free(tmp);
+
+    return short_options;
+}
+
+static struct option * OVS_WARN_UNUSED_RESULT
+append_command_options(const struct option *options, int opt_val)
+{
+    struct option *o;
+    size_t n_allocated;
+    size_t n_existing;
+    int i;
+
+    for (i = 0; options[i].name; i++) {
+        ;
+    }
+    n_allocated = i + 1;
+    n_existing = i;
+
+    /* 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. */
+    o = xmemdup(options, n_allocated * sizeof *options);
+    ctl_add_cmd_options(&o, &n_existing, &n_allocated, opt_val);
+
+    return o;
+}
+
+static struct option *
+get_all_options(void)
+{
+    static const struct option global_long_options[] = {
+        {"db", required_argument, NULL, OPT_DB},
+        {"no-syslog", no_argument, NULL, OPT_NO_SYSLOG},
+        {"help", no_argument, NULL, 'h'},
+        {"commands", no_argument, NULL, OPT_COMMANDS},
+        {"options", no_argument, NULL, OPT_OPTIONS},
+        {"leader-only", no_argument, NULL, OPT_LEADER_ONLY},
+        {"no-leader-only", no_argument, NULL, OPT_NO_LEADER_ONLY},
+        {"shuffle-remotes", no_argument, NULL, OPT_SHUFFLE_REMOTES},
+        {"no-shuffle-remotes", no_argument, NULL, OPT_NO_SHUFFLE_REMOTES},
+        {"version", no_argument, NULL, 'V'},
+        {"unixctl", required_argument, NULL, 'u'},
+        MAIN_LOOP_LONG_OPTIONS,
+        OVN_DAEMON_LONG_OPTIONS,
+        VLOG_LONG_OPTIONS,
+        STREAM_SSL_LONG_OPTIONS,
+        {"bootstrap-ca-cert", required_argument, NULL, OPT_BOOTSTRAP_CA_CERT},
+        TABLE_LONG_OPTIONS,
+        {NULL, 0, NULL, 0},
+    };
+
+    static struct option *options;
+    if (!options) {
+        options = append_command_options(global_long_options, OPT_LOCAL);
+    }
+
+    return options;
+}
+
+static bool
+has_option(const struct ovs_cmdl_parsed_option *parsed_options, size_t n,
+           int option)
+{
+    for (const struct ovs_cmdl_parsed_option *po = parsed_options;
+         po < &parsed_options[n]; po++) {
+        if (po->o->val == option) {
+            return true;
+        }
+    }
+    return false;
+}
+
+static bool
+will_detach(const struct ovs_cmdl_parsed_option *parsed_options, size_t n)
+{
+    return has_option(parsed_options, n, OVN_OPT_DETACH);
+}
+
+static char * OVS_WARN_UNUSED_RESULT
+add_local_option(const char *name, const char *arg,
+                 struct shash *local_options)
+{
+    char *full_name = xasprintf("--%s", name);
+    if (shash_find(local_options, full_name)) {
+        char *error = xasprintf("'%s' option specified multiple times",
+                                full_name);
+        free(full_name);
+        return error;
+    }
+    shash_add_nocopy(local_options, full_name, nullable_xstrdup(arg));
+    return NULL;
+}
+
+static void
+apply_options_direct(const struct ovn_dbctl_options *dbctl_options,
+                     const struct ovs_cmdl_parsed_option *parsed_options,
+                     size_t n, struct shash *local_options)
+{
+    for (const struct ovs_cmdl_parsed_option *po = parsed_options;
+         po < &parsed_options[n]; po++) {
+        bool handled;
+        char *error = handle_main_loop_option(po->o->val, po->arg, &handled);
+        if (error) {
+            ctl_fatal("%s", error);
+        }
+        if (handled) {
+            continue;
+        }
+
+        optarg = po->arg;
+        switch (po->o->val) {
+        case OPT_DB:
+            db = po->arg;
+            break;
+
+        case OPT_NO_SYSLOG:
+            vlog_set_levels(&this_module, VLF_SYSLOG, VLL_WARN);
+            break;
+
+        case OPT_LOCAL:
+            error = add_local_option(po->o->name, po->arg, local_options);
+            if (error) {
+                ctl_fatal("%s", error);
+            }
+            break;
+
+        case 'h':
+            dbctl_options->usage();
+            exit(EXIT_SUCCESS);
+
+        case OPT_COMMANDS:
+            ctl_print_commands();
+            /* fall through */
+
+        case OPT_OPTIONS:
+            ctl_print_options(get_all_options());
+            /* fall through */
+
+        case OPT_LEADER_ONLY:
+            leader_only = true;
+            break;
+
+        case OPT_NO_LEADER_ONLY:
+            leader_only = false;
+            break;
+
+        case OPT_SHUFFLE_REMOTES:
+            shuffle_remotes = true;
+            break;
+
+        case OPT_NO_SHUFFLE_REMOTES:
+            shuffle_remotes = false;
+            break;
+
+        case 'u':
+            unixctl_path = optarg;
+            break;
+
+        case 'V':
+            ovn_print_version(0, 0);
+            printf("DB Schema %s\n", dbctl_options->db_version);
+            exit(EXIT_SUCCESS);
+
+        OVN_DAEMON_OPTION_HANDLERS
+        VLOG_OPTION_HANDLERS
+        TABLE_OPTION_HANDLERS(&table_style)
+        STREAM_SSL_OPTION_HANDLERS
+
+        case OPT_BOOTSTRAP_CA_CERT:
+            stream_ssl_set_ca_cert_file(po->arg, true);
+            break;
+
+        case '?':
+            exit(EXIT_FAILURE);
+
+        default:
+            abort();
+
+        case 0:
+            break;
+        }
+    }
+
+    if (!db) {
+        db = dbctl_options->default_db;
+    }
+}
+
+static char *
+run_prerequisites(const struct ovn_dbctl_options *dbctl_options,
+                  struct ctl_command *commands, size_t n_commands,
+                  struct ovsdb_idl *idl)
+{
+    dbctl_options->add_base_prerequisites(idl, wait_type);
+
+    for (struct ctl_command *c = commands; c < &commands[n_commands]; c++) {
+        if (c->syntax->prerequisites) {
+            struct ctl_context ctx;
+
+            ds_init(&c->output);
+            c->table = NULL;
+
+            ctl_context_init(&ctx, c, idl, NULL, NULL, NULL);
+            (c->syntax->prerequisites)(&ctx);
+            if (ctx.error) {
+                char *error = xstrdup(ctx.error);
+                ctl_context_done(&ctx, c);
+                return error;
+            }
+            ctl_context_done(&ctx, c);
+
+            ovs_assert(!c->output.string);
+            ovs_assert(!c->table);
+        }
+    }
+
+    return NULL;
+}
+
+static void
+oneline_format(struct ds *lines, struct ds *s)
+{
+    size_t j;
+
+    ds_chomp(lines, '\n');
+    for (j = 0; j < lines->length; j++) {
+        int ch = lines->string[j];
+        switch (ch) {
+        case '\n':
+            ds_put_cstr(s, "\\n");
+            break;
+
+        case '\\':
+            ds_put_cstr(s, "\\\\");
+            break;
+
+        default:
+            ds_put_char(s, ch);
+        }
+    }
+    ds_put_char(s, '\n');
+}
+
+static void
+oneline_print(struct ds *lines)
+{
+    struct ds s = DS_EMPTY_INITIALIZER;
+    oneline_format(lines, &s);
+    fputs(ds_cstr(&s), stdout);
+    ds_destroy(&s);
+}
+
+static char *
+do_dbctl(const struct ovn_dbctl_options *dbctl_options,
+         const char *args, struct ctl_command *commands, size_t n_commands,
+         struct ovsdb_idl *idl, const struct timer *wait_timeout, bool *retry)
+{
+    struct ovsdb_idl_txn *txn;
+    enum ovsdb_idl_txn_status status;
+    struct ovsdb_symbol_table *symtab;
+    struct ctl_command *c;
+    struct shash_node *node;
+    char *error = NULL;
+
+    ovs_assert(retry);
+
+    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, "%s: %s", program_name, args);
+
+    dbctl_options->pre_execute(idl, txn, wait_type);
+
+    symtab = ovsdb_symbol_table_create();
+    for (c = commands; c < &commands[n_commands]; c++) {
+        ds_init(&c->output);
+        c->table = NULL;
+    }
+    struct ctl_context *ctx = dbctl_options->ctx_create();
+    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);
+        }
+        if (ctx->error) {
+            error = xstrdup(ctx->error);
+            ctl_context_done(ctx, c);
+            goto out_error;
+        }
+        ctl_context_done_command(ctx, c);
+
+        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) {
+            error = xasprintf("row id \"%s\" is referenced but never created "
+                              "(e.g. with \"-- --id=%s create ...\")",
+                              node->name, node->name);
+            goto out_error;
+        }
+        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 {
+                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);
+            }
+        }
+    }
+
+    long long int start_time = time_wall_msec();
+    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);
+                if (ctx->error) {
+                    error = xstrdup(ctx->error);
+                    ctl_context_done(ctx, c);
+                    goto out_error;
+                }
+                ctl_context_done(ctx, c);
+            }
+        }
+    }
+
+    switch (status) {
+    case TXN_UNCOMMITTED:
+    case TXN_INCOMPLETE:
+        OVS_NOT_REACHED();
+
+    case TXN_ABORTED:
+        /* Should not happen--we never call ovsdb_idl_txn_abort(). */
+        error = xstrdup("transaction aborted");
+        goto out_error;
+
+    case TXN_UNCHANGED:
+    case TXN_SUCCESS:
+        break;
+
+    case TXN_TRY_AGAIN:
+        goto try_again;
+
+    case TXN_ERROR:
+        error = xasprintf("transaction error: %s",
+                          ovsdb_idl_txn_get_error(txn));
+        goto out_error;
+
+    case TXN_NOT_LOCKED:
+        /* Should not happen--we never call ovsdb_idl_set_lock(). */
+        error = xstrdup("database not locked");
+        goto out_error;
+
+    default:
+        OVS_NOT_REACHED();
+    }
+
+    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) {
+            oneline_print(ds);
+        } else {
+            fputs(ds_cstr(ds), stdout);
+        }
+    }
+
+    if (dbctl_options->post_execute) {
+        error = dbctl_options->post_execute(idl, txn, status, wait_type,
+                                            wait_timeout, start_time,
+                                            print_wait_time);
+        if (error) {
+            goto out_error;
+        }
+    }
+    
+    dbctl_options->ctx_destroy(ctx);
+    ovsdb_symbol_table_destroy(symtab);
+    ovsdb_idl_txn_destroy(txn);
+    the_idl_txn = NULL;
+
+    *retry = false;
+    return NULL;
+
+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. */
+    *retry = true;
+
+out_error:
+    ovsdb_idl_txn_abort(txn);
+    ovsdb_idl_txn_destroy(txn);
+    the_idl_txn = NULL;
+
+    dbctl_options->ctx_destroy(ctx);
+    ovsdb_symbol_table_destroy(symtab);
+    return error;
+}
+
+/* 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
+ovn_dbctl_exit(int status)
+{
+    if (the_idl_txn) {
+        ovsdb_idl_txn_abort(the_idl_txn);
+        ovsdb_idl_txn_destroy(the_idl_txn);
+    }
+    ovsdb_idl_destroy(the_idl);
+    exit(status);
+}
+
+/* Server implementation. */
+
+#undef ctl_fatal
+
+static const struct option *
+find_option_by_value(const struct option *options, int value)
+{
+    const struct option *o;
+
+    for (o = options; o->name; o++) {
+        if (o->val == value) {
+            return o;
+        }
+    }
+    return NULL;
+}
+
+static char * OVS_WARN_UNUSED_RESULT
+server_parse_options(int argc, char *argv[], struct shash *local_options,
+                     int *n_options_p)
+{
+    static const struct option global_long_options[] = {
+        VLOG_LONG_OPTIONS,
+        MAIN_LOOP_LONG_OPTIONS,
+        TABLE_LONG_OPTIONS,
+        {NULL, 0, NULL, 0},
+    };
+    const int n_global_long_options = ARRAY_SIZE(global_long_options) - 1;
+    char *short_options;
+    struct option *options;
+    char *error = NULL;
+
+    ovs_assert(n_options_p);
+
+    short_options = build_short_options(global_long_options, false);
+    options = append_command_options(global_long_options, OPT_LOCAL);
+
+    optind = 0;
+    opterr = 0;
+    for (;;) {
+        int idx;
+        int c;
+
+        c = getopt_long(argc, argv, short_options, options, &idx);
+        if (c == -1) {
+            break;
+        }
+
+        bool handled;
+        error = handle_main_loop_option(c, optarg, &handled);
+        if (error) {
+            goto out;
+        }
+        if (handled) {
+            continue;
+        }
+
+        switch (c) {
+        case OPT_LOCAL:
+            error = add_local_option(options[idx].name, optarg, local_options);
+            if (error) {
+                goto out;
+            }
+            break;
+
+        VLOG_OPTION_HANDLERS
+        TABLE_OPTION_HANDLERS(&table_style)
+
+        case '?':
+            if (find_option_by_value(options, optopt)) {
+                error = xasprintf("option '%s' doesn't allow an argument",
+                                  argv[optind-1]);
+            } else if (optopt) {
+                error = xasprintf("unrecognized option '%c'", optopt);
+            } else {
+                error = xasprintf("unrecognized option '%s'", argv[optind-1]);
+            }
+            goto out;
+            break;
+
+        case ':':
+            error = xasprintf("option '%s' requires an argument",
+                              argv[optind-1]);
+            goto out;
+            break;
+
+        case 0:
+            break;
+
+        default:
+            error = xasprintf("unhandled option '%c'", c);
+            goto out;
+            break;
+        }
+    }
+    *n_options_p = optind;
+
+out:
+    for (int i = n_global_long_options; options[i].name; i++) {
+        free(CONST_CAST(char *, options[i].name));
+    }
+    free(options);
+    free(short_options);
+
+    return error;
+}
+
+static void
+server_cmd_exit(struct unixctl_conn *conn, int argc OVS_UNUSED,
+                const char *argv[] OVS_UNUSED, void *exiting_)
+{
+    bool *exiting = exiting_;
+    *exiting = true;
+    unixctl_command_reply(conn, NULL);
+}
+
+struct server_cmd_run_ctx {
+    struct ovsdb_idl *idl;
+    const struct ovn_dbctl_options *dbctl_options;
+};
+
+static void
+server_cmd_run(struct unixctl_conn *conn, int argc, const char **argv_,
+               void *ctx_)
+{
+    struct server_cmd_run_ctx *ctx = ctx_;
+    struct ovsdb_idl *idl = ctx->idl;
+    const struct ovn_dbctl_options *dbctl_options = ctx->dbctl_options;
+
+    struct ctl_command *commands = NULL;
+    struct shash local_options;
+    size_t n_commands = 0;
+    int n_options = 0;
+    char *error = NULL;
+
+    /* Copy args so that getopt() can permute them. Leave last entry NULL. */
+    char **argv = xcalloc(argc + 1, sizeof *argv);
+    for (int i = 0; i < argc; i++) {
+        argv[i] = xstrdup(argv_[i]);
+    }
+
+    /* Reset global state. */
+    oneline = false;
+    dry_run = false;
+    wait_type = NBCTL_WAIT_NONE;
+    timeout = 0;
+    table_style = table_style_default;
+
+    /* Parse commands & options. */
+    char *args = process_escape_args(argv);
+    shash_init(&local_options);
+    error = server_parse_options(argc, argv, &local_options, &n_options);
+    if (error) {
+        unixctl_command_reply_error(conn, error);
+        goto out;
+    }
+    error = ctl_parse_commands(argc - n_options, argv + n_options,
+                               &local_options, &commands, &n_commands);
+    if (error) {
+        unixctl_command_reply_error(conn, error);
+        goto out;
+    }
+    VLOG(ctl_might_write_to_db(commands, n_commands) ? VLL_INFO : VLL_DBG,
+         "Running command %s", args);
+
+    struct timer *wait_timeout = NULL;
+    struct timer wait_timeout_;
+    if (timeout) {
+        wait_timeout = &wait_timeout_;
+        timer_set_duration(wait_timeout, timeout * 1000);
+    }
+
+    error = run_prerequisites(dbctl_options, commands, n_commands, idl);
+    if (error) {
+        unixctl_command_reply_error(conn, error);
+        goto out;
+    }
+    error = main_loop(dbctl_options, args, commands, n_commands, idl, wait_timeout);
+    if (error) {
+        unixctl_command_reply_error(conn, error);
+        goto out;
+    }
+
+    struct ds output = DS_EMPTY_INITIALIZER;
+    table_format_reset();
+    for (struct ctl_command *c = commands; c < &commands[n_commands]; c++) {
+        if (c->table) {
+            table_format(c->table, &table_style, &output);
+        } else if (oneline) {
+            oneline_format(&c->output, &output);
+        } else {
+            ds_put_cstr(&output, ds_cstr_ro(&c->output));
+        }
+    }
+    unixctl_command_reply(conn, ds_cstr_ro(&output));
+    ds_destroy(&output);
+
+out:
+    free(error);
+
+    struct ctl_command *c;
+    for (c = commands; c < &commands[n_commands]; c++) {
+        ds_destroy(&c->output);
+        table_destroy(c->table);
+        free(c->table);
+        shash_destroy_free_data(&c->options);
+    }
+    free(commands);
+    shash_destroy_free_data(&local_options);
+    free(args);
+    for (int i = 0; i < argc; i++) {
+        free(argv[i]);
+    }
+    free(argv);
+}
+
+static void
+server_loop(const struct ovn_dbctl_options *dbctl_options,
+            struct ovsdb_idl *idl, int argc, char *argv[])
+{
+    struct unixctl_server *server = NULL;
+    bool exiting = false;
+
+    service_start(&argc, &argv);
+    daemonize_start(false);
+
+    char *abs_unixctl_path = get_abs_unix_ctl_path(unixctl_path);
+    int error = unixctl_server_create(abs_unixctl_path, &server);
+    free(abs_unixctl_path);
+
+    if (error) {
+        ctl_fatal("failed to create unixctl server (%s)",
+                  ovs_retval_to_string(error));
+    }
+    puts(unixctl_server_get_path(server));
+    fflush(stdout);
+
+    struct server_cmd_run_ctx server_cmd_run_ctx = {
+        .idl = idl,
+        .dbctl_options = dbctl_options
+    };
+    unixctl_command_register("run", "", 0, INT_MAX, server_cmd_run,
+                             &server_cmd_run_ctx);
+    unixctl_command_register("exit", "", 0, 0, server_cmd_exit, &exiting);
+
+    for (;;) {
+        memory_run();
+        if (memory_should_report()) {
+            struct simap usage = SIMAP_INITIALIZER(&usage);
+
+            /* Nothing special to report yet. */
+            memory_report(&usage);
+            simap_destroy(&usage);
+        }
+
+        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 (ovsdb_idl_has_ever_connected(idl)) {
+            daemonize_complete();
+        }
+        unixctl_server_run(server);
+
+        if (exiting) {
+            break;
+        }
+
+        memory_wait();
+        ovsdb_idl_wait(idl);
+        unixctl_server_wait(server);
+        poll_block();
+    }
+
+    unixctl_server_destroy(server);
+}
+
+static void
+dbctl_client(const struct ovn_dbctl_options *dbctl_options,
+             const char *socket_name,
+             const struct ovs_cmdl_parsed_option *parsed_options, size_t n,
+             int argc, char *argv[])
+{
+    struct svec args = SVEC_EMPTY_INITIALIZER;
+
+    for (const struct ovs_cmdl_parsed_option *po = parsed_options;
+         po < &parsed_options[n]; po++) {
+        optarg = po->arg;
+        switch (po->o->val) {
+        case OPT_DB:
+            VLOG_WARN("not using %s daemon because of %s option",
+                      program_name, po->o->name);
+            svec_destroy(&args);
+            return;
+
+        case OPT_NO_SYSLOG:
+            vlog_set_levels(&this_module, VLF_SYSLOG, VLL_WARN);
+            break;
+
+        case 'h':
+            dbctl_options->usage();
+            exit(EXIT_SUCCESS);
+
+        case OPT_COMMANDS:
+            ctl_print_commands();
+            /* fall through */
+
+        case OPT_OPTIONS:
+            ctl_print_options(get_all_options());
+            /* fall through */
+
+        case OPT_LEADER_ONLY:
+        case OPT_NO_LEADER_ONLY:
+        case OPT_SHUFFLE_REMOTES:
+        case OPT_NO_SHUFFLE_REMOTES:
+        case OPT_BOOTSTRAP_CA_CERT:
+        STREAM_SSL_CASES
+        OVN_DAEMON_OPTION_CASES
+            VLOG_INFO("using %s daemon, ignoring %s option",
+                      program_name, po->o->name);
+            break;
+
+        case 'u':
+            socket_name = optarg;
+            break;
+
+        case 'V':
+            ovs_print_version(0, 0);
+            printf("DB Schema %s\n", dbctl_options->db_version);
+            exit(EXIT_SUCCESS);
+
+        case 't':
+            if (!str_to_uint(po->arg, 10, &timeout) || !timeout) {
+                ctl_fatal("value %s on -t or --timeout is invalid", po->arg);
+            }
+            break;
+
+        VLOG_OPTION_HANDLERS
+
+        case OPT_LOCAL:
+        default:
+            if (po->arg) {
+                svec_add_nocopy(&args,
+                                xasprintf("--%s=%s", po->o->name, po->arg));
+            } else {
+                svec_add_nocopy(&args, xasprintf("--%s", po->o->name));
+            }
+            break;
+        }
+    }
+
+    ovs_assert(socket_name && socket_name[0]);
+
+    svec_add(&args, "--");
+    for (int i = optind; i < argc; i++) {
+        svec_add(&args, argv[i]);
+    }
+
+    ctl_timeout_setup(timeout);
+
+    struct jsonrpc *client;
+    int error = unixctl_client_create(socket_name, &client);
+    if (error) {
+        ctl_fatal("%s: could not connect to %s daemon (%s); "
+                  "unset %s to avoid using daemon",
+                  socket_name, program_name, ovs_strerror(error),
+                  dbctl_options->daemon_env_var_name);
+    }
+
+    char *cmd_result;
+    char *cmd_error;
+    error = unixctl_client_transact(client, "run",
+                                    args.n, args.names,
+                                    &cmd_result, &cmd_error);
+    if (error) {
+        ctl_fatal("%s: transaction error (%s)",
+                  socket_name, ovs_strerror(error));
+    }
+    svec_destroy(&args);
+
+    int exit_status;
+    if (cmd_error) {
+        exit_status = EXIT_FAILURE;
+        fprintf(stderr, "%s: %s", program_name, cmd_error);
+    } else {
+        exit_status = EXIT_SUCCESS;
+        fputs(cmd_result, stdout);
+    }
+    free(cmd_result);
+    free(cmd_error);
+    jsonrpc_close(client);
+    exit(exit_status);
+}
diff --git a/utilities/ovn-dbctl.h b/utilities/ovn-dbctl.h
new file mode 100644
index 000000000000..5accf3c5e028
--- /dev/null
+++ b/utilities/ovn-dbctl.h
@@ -0,0 +1,60 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OVN_DBCTL_H
+#define OVN_DBCTL_H 1
+
+/* ovn-nbctl infrastructure code. */
+
+#include <stdbool.h>
+#include "ovsdb-idl.h"
+
+struct timer;
+
+enum nbctl_wait_type {
+    NBCTL_WAIT_NONE,            /* Do not wait. */
+    NBCTL_WAIT_SB,              /* Wait for southbound database updates. */
+    NBCTL_WAIT_HV               /* Wait for hypervisors to catch up. */
+};
+
+struct ovn_dbctl_options {
+    const char *db_version;     /* Database schema version. */
+    const char *default_db;     /* Default database remote. */
+
+    /* Names of important environment variables. */
+    const char *options_env_var_name; /* OVN_??_OPTIONS. */
+    const char *daemon_env_var_name;  /* OVN_??_DAEMON. */
+
+    const struct ovsdb_idl_class *idl_class;
+    const struct ctl_table_class *tables;
+    struct cmd_show_table *cmd_show_table;
+    const struct ctl_command_syntax *commands;
+
+    void (*usage)(void);
+
+    void (*add_base_prerequisites)(struct ovsdb_idl *, enum nbctl_wait_type);
+    void (*pre_execute)(struct ovsdb_idl *, struct ovsdb_idl_txn *,
+                        enum nbctl_wait_type);
+    char *(*post_execute)(struct ovsdb_idl *, struct ovsdb_idl_txn *,
+                          enum ovsdb_idl_txn_status, enum nbctl_wait_type,
+                          const struct timer *wait_timeout,
+                          long long int start_time, bool print_wait_time);
+
+    struct ctl_context *(*ctx_create)(void);
+    void (*ctx_destroy)(struct ctl_context *);
+};
+
+int ovn_dbctl_main(int argc, char *argv[], const struct ovn_dbctl_options *);
+
+#endif  /* ovn-dbctl.h */
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index 2c77f4ba7645..8e8a5f8cfde9 100644
--- a/utilities/ovn-nbctl.c
+++ b/utilities/ovn-nbctl.c
@@ -30,6 +30,7 @@
 #include "lib/ovn-nb-idl.h"
 #include "lib/ovn-util.h"
 #include "memory.h"
+#include "ovn-dbctl.h"
 #include "packets.h"
 #include "openvswitch/poll-loop.h"
 #include "process.h"
@@ -48,84 +49,109 @@
 
 VLOG_DEFINE_THIS_MODULE(nbctl);
 
-/* --db: The database server to contact. */
-static const char *db;
+/* Should we wait (if specified by 'wait_type') even if the commands don't
+ * change the database at all? */
+static bool force_wait = false;
 
-/* --oneline: Write each command's output as a single line? */
-static bool oneline;
+static void
+nbctl_add_base_prerequisites(struct ovsdb_idl *idl,
+                             enum nbctl_wait_type wait_type)
+{
+    force_wait = false;
 
-/* --dry-run: Do not commit any changes. */
-static bool dry_run;
+    ovsdb_idl_add_table(idl, &nbrec_table_nb_global);
+    if (wait_type == NBCTL_WAIT_SB) {
+        ovsdb_idl_add_column(idl, &nbrec_nb_global_col_sb_cfg);
+        ovsdb_idl_add_column(idl, &nbrec_nb_global_col_sb_cfg_timestamp);
+    } else if (wait_type == NBCTL_WAIT_HV) {
+        ovsdb_idl_add_column(idl, &nbrec_nb_global_col_hv_cfg);
+        ovsdb_idl_add_column(idl, &nbrec_nb_global_col_hv_cfg_timestamp);
+    }
+}
 
-/* --wait=TYPE: Wait for configuration change to take effect? */
-enum nbctl_wait_type {
-    NBCTL_WAIT_NONE,            /* Do not wait. */
-    NBCTL_WAIT_SB,              /* Wait for southbound database updates. */
-    NBCTL_WAIT_HV               /* Wait for hypervisors to catch up. */
-};
-static enum nbctl_wait_type wait_type = NBCTL_WAIT_NONE;
+static void
+nbctl_pre_execute(struct ovsdb_idl *idl, struct ovsdb_idl_txn *txn,
+                  enum nbctl_wait_type wait_type)
+{
+    const struct nbrec_nb_global *nb = nbrec_nb_global_first(idl);
+    if (!nb) {
+        /* XXX add verification that table is empty */
+        nb = nbrec_nb_global_insert(txn);
+    }
 
-static bool print_wait_time = false;
+    /* Deal with potential overflows. */
+    if (nb->nb_cfg == LLONG_MAX) {
+        nbrec_nb_global_set_nb_cfg(nb, 0);
+    }
 
-/* Should we wait (if specified by 'wait_type') even if the commands don't
- * change the database at all? */
-static bool force_wait = false;
+    if (wait_type != NBCTL_WAIT_NONE) {
+        ovsdb_idl_txn_increment(txn, &nb->header_, &nbrec_nb_global_col_nb_cfg,
+                                force_wait);
+    }
+}
+
+static char *
+nbctl_post_execute(struct ovsdb_idl *idl, struct ovsdb_idl_txn *txn,
+                   enum ovsdb_idl_txn_status status,
+                   enum nbctl_wait_type wait_type,
+                   const struct timer *wait_timeout, long long int start_time,
+                   bool print_wait_time)
+{
+    if (wait_type == NBCTL_WAIT_NONE) {
+        if (force_wait) {
+            VLOG_INFO("\"sync\" command has no effect without --wait");
+        }
+        return NULL;
+    }
+    if (status == TXN_UNCHANGED) {
+        return NULL;
+    }
+
+    ovs_assert(status == TXN_SUCCESS);
+    int64_t next_cfg = ovsdb_idl_txn_get_increment_new_value(txn);
+    ovsdb_idl_enable_reconnect(idl);
+    for (;;) {
+        ovsdb_idl_run(idl);
+
+        const struct nbrec_nb_global *nb;
+        NBREC_NB_GLOBAL_FOR_EACH (nb, idl) {
+            int64_t cur_cfg = (wait_type == NBCTL_WAIT_SB
+                               ? nb->sb_cfg
+                               : MIN(nb->sb_cfg, nb->hv_cfg));
+            if (cur_cfg >= next_cfg) {
+                if (print_wait_time) {
+                    printf("Time spent on processing nb_cfg %"PRId64":\n",
+                           next_cfg);
+
+                    long long int nb_timestamp = nb->nb_cfg_timestamp;
+                    long long int sb_timestamp = nb->sb_cfg_timestamp;
+                    long long int hv_timestamp = nb->hv_cfg_timestamp;
+                    printf("\tovn-northd delay before processing:"
+                           "\t%lldms\n", nb_timestamp - start_time);
+                    printf("\tovn-northd completion:"
+                           "\t\t\t%lldms\n", sb_timestamp - start_time);
+                    if (wait_type == NBCTL_WAIT_HV) {
+                        printf("\tovn-controller(s) completion:"
+                               "\t\t%lldms\n", hv_timestamp - start_time);
+                    }
+                }
+                return NULL;
+            }
+        }
+        ovsdb_idl_wait(idl);
+        if (wait_timeout) {
+            timer_wait(wait_timeout);
+        }
+        poll_block();
+        if (wait_timeout && timer_expired(wait_timeout)) {
+            return xstrdup("timeout expired");
+        }
+    }
+}
 
-/* --timeout: Time to wait for a connection to 'db'. */
-static unsigned 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);
-
-/* --leader-only, --no-leader-only: Only accept the leader in a cluster. */
-static int leader_only = true;
-
-/* --shuffle-remotes, --no-shuffle-remotes: Shuffle the order of remotes that
- * are specified in the connetion method string. */
-static int shuffle_remotes = true;
-
-/* --unixctl-path: Path to use for unixctl server, for "monitor" and "snoop"
-     commands. */
-static char *unixctl_path;
-
-static unixctl_cb_func server_cmd_exit;
-static unixctl_cb_func server_cmd_run;
-
-static void nbctl_cmd_init(void);
-OVS_NO_RETURN static void usage(void);
-static struct option *get_all_options(void);
-static bool has_option(const struct ovs_cmdl_parsed_option *, size_t n,
-                       int option);
-static void nbctl_client(const char *socket_name,
-                         const struct ovs_cmdl_parsed_option *, size_t n,
-                         int argc, char *argv[]);
-static bool will_detach(const struct ovs_cmdl_parsed_option *, size_t n);
-static void apply_options_direct(const struct ovs_cmdl_parsed_option *,
-                                 size_t n, struct shash *local_options);
-static char * OVS_WARN_UNUSED_RESULT run_prerequisites(struct ctl_command[],
-                                                       size_t n_commands,
-                                                       struct ovsdb_idl *);
-static char * OVS_WARN_UNUSED_RESULT do_nbctl(const char *args,
-                                              struct ctl_command *, size_t n,
-                                              struct ovsdb_idl *,
-                                              const struct timer *,
-                                              bool *retry);
 static char * OVS_WARN_UNUSED_RESULT dhcp_options_get(
     struct ctl_context *ctx, const char *id, bool must_exist,
     const struct nbrec_dhcp_options **);
-static char * OVS_WARN_UNUSED_RESULT main_loop(const char *args,
-                                               struct ctl_command *commands,
-                                               size_t n_commands,
-                                               struct ovsdb_idl *idl,
-                                               const struct timer *);
-static void server_loop(struct ovsdb_idl *idl, int argc, char *argv[]);
 
 /* A context for keeping track of which switch/router certain ports are
  * connected to.
@@ -135,35 +161,41 @@ static void server_loop(struct ovsdb_idl *idl, int argc, char *argv[]);
  * until transaction is committed and updates received from the server. */
 struct nbctl_context {
     struct ctl_context base;
+
+    bool context_valid;
     struct shash lsp_to_ls_map;
     struct shash lrp_to_lr_map;
-    bool context_valid;
 };
 
-static void
-nbctl_context_init(struct nbctl_context *nbctx)
+static struct ctl_context *
+nbctl_ctx_create(void)
 {
-    nbctx->context_valid = false;
-    shash_init(&nbctx->lsp_to_ls_map);
-    shash_init(&nbctx->lrp_to_lr_map);
+    struct nbctl_context *nbctx = xmalloc(sizeof *nbctx);
+    *nbctx = (struct nbctl_context) {
+        .context_valid = false,
+        .lsp_to_ls_map = SHASH_INITIALIZER(&nbctx->lsp_to_ls_map),
+        .lrp_to_lr_map = SHASH_INITIALIZER(&nbctx->lrp_to_lr_map),
+    };
+    return &nbctx->base;
 }
 
 static void
-nbctl_context_destroy(struct nbctl_context *nbctx)
+nbctl_ctx_destroy(struct ctl_context *base)
 {
+    struct nbctl_context *nbctx
+        = CONTAINER_OF(base, struct nbctl_context, base);
     nbctx->context_valid = false;
     shash_destroy(&nbctx->lsp_to_ls_map);
     shash_destroy(&nbctx->lrp_to_lr_map);
+    free(nbctx);
 }
 
 /* Casts 'base' into 'struct nbctl_context' and initializes it if needed. */
 static struct nbctl_context *
 nbctl_context_get(struct ctl_context *base)
 {
-    struct nbctl_context *nbctx;
-
-    nbctx = CONTAINER_OF(base, struct nbctl_context, base);
-
+    struct nbctl_context *nbctx
+        = CONTAINER_OF(base, struct nbctl_context, base);
     if (nbctx->context_valid) {
         return nbctx;
     }
@@ -186,466 +218,8 @@ nbctl_context_get(struct ctl_context *base)
     return nbctx;
 }
 
-int
-main(int argc, char *argv[])
-{
-    struct ovsdb_idl *idl;
-    struct shash local_options;
-
-    ovn_set_program_name(argv[0]);
-    fatal_ignore_sigpipe();
-    vlog_set_levels(NULL, VLF_CONSOLE, VLL_WARN);
-    vlog_set_levels_from_string_assert("reconnect:warn");
-
-    nbctl_cmd_init();
-
-    /* Check if options are set via env var. */
-    char **argv_ = ovs_cmdl_env_parse_all(&argc, argv,
-                                          getenv("OVN_NBCTL_OPTIONS"));
-
-    /* ovn-nbctl has three operation modes:
-     *
-     *    - Direct: Executes commands by contacting ovsdb-server directly.
-     *
-     *    - Server: Runs in the background as a daemon waiting for requests
-     *      from ovn-nbctl running in client mode.
-     *
-     *    - Client: Executes commands by passing them to an ovn-nbctl running
-     *      in the server mode.
-     *
-     * At this point we don't know what mode we're running in.  The mode partly
-     * depends on the command line.  So, for now we transform the command line
-     * into a parsed form, and figure out what to do with it later.
-     */
-    struct ovs_cmdl_parsed_option *parsed_options;
-    size_t n_parsed_options;
-    char *error_s = ovs_cmdl_parse_all(argc, argv_, get_all_options(),
-                                       &parsed_options, &n_parsed_options);
-    if (error_s) {
-        ctl_fatal("%s", error_s);
-    }
-
-    /* Now figure out the operation mode:
-     *
-     *    - A --detach option implies server mode.
-     *
-     *    - An OVN_NB_DAEMON environment variable implies client mode.
-     *
-     *    - Otherwise, we're in direct mode. */
-    char *socket_name = unixctl_path ?: getenv("OVN_NB_DAEMON");
-    if (((socket_name && socket_name[0])
-         || has_option(parsed_options, n_parsed_options, 'u'))
-        && !will_detach(parsed_options, n_parsed_options)) {
-        nbctl_client(socket_name, parsed_options, n_parsed_options,
-                     argc, argv_);
-    }
-
-    /* Parse command line. */
-    shash_init(&local_options);
-    apply_options_direct(parsed_options, n_parsed_options, &local_options);
-    free(parsed_options);
-
-    bool daemon_mode = false;
-    if (get_detach()) {
-        if (argc != optind) {
-            ctl_fatal("non-option arguments not supported with --detach "
-                      "(use --help for help)");
-        }
-        daemon_mode = true;
-    }
-    /* Initialize IDL. */
-    idl = the_idl = ovsdb_idl_create_unconnected(&nbrec_idl_class, true);
-    ovsdb_idl_set_shuffle_remotes(idl, shuffle_remotes);
-    /* "retry" is true iff in daemon mode. */
-    ovsdb_idl_set_remote(idl, db, daemon_mode);
-    ovsdb_idl_set_leader_only(idl, leader_only);
-
-    if (daemon_mode) {
-        server_loop(idl, argc, argv_);
-    } else {
-        struct ctl_command *commands;
-        size_t n_commands;
-        char *error;
-
-        error = ctl_parse_commands(argc - optind, argv_ + optind,
-                                   &local_options, &commands, &n_commands);
-        if (error) {
-            ctl_fatal("%s", error);
-        }
-
-        char *args = process_escape_args(argv_);
-        VLOG(ctl_might_write_to_db(commands, n_commands) ? VLL_INFO : VLL_DBG,
-             "Called as %s", args);
-
-        ctl_timeout_setup(timeout);
-
-        error = run_prerequisites(commands, n_commands, idl);
-        if (error) {
-            goto cleanup;
-        }
-
-        error = main_loop(args, commands, n_commands, idl, NULL);
-
-cleanup:
-        free(args);
-
-        struct ctl_command *c;
-        for (c = commands; c < &commands[n_commands]; c++) {
-            ds_destroy(&c->output);
-            table_destroy(c->table);
-            free(c->table);
-            shash_destroy_free_data(&c->options);
-        }
-        free(commands);
-        if (error) {
-            ctl_fatal("%s", error);
-        }
-    }
-
-    ovsdb_idl_destroy(idl);
-    idl = the_idl = NULL;
-
-    for (int i = 0; i < argc; i++) {
-        free(argv_[i]);
-    }
-    free(argv_);
-    exit(EXIT_SUCCESS);
-}
-
-static char *
-main_loop(const char *args, struct ctl_command *commands, size_t n_commands,
-          struct ovsdb_idl *idl, const struct timer *wait_timeout)
-{
-    unsigned int seqno;
-    bool idl_ready;
-
-    /* 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);
-
-    /* IDL might have already obtained the database copy during previous
-     * invocation. If so, we can't expect the sequence number to change before
-     * we issue any new requests. */
-    idl_ready = ovsdb_idl_has_ever_connected(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 (idl_ready || seqno != ovsdb_idl_get_seqno(idl)) {
-            idl_ready = false;
-            seqno = ovsdb_idl_get_seqno(idl);
-
-            bool retry;
-            char *error = do_nbctl(args, commands, n_commands, idl,
-                                   wait_timeout, &retry);
-            if (error) {
-                return error;
-            }
-            if (!retry) {
-                return NULL;
-            }
-        }
-
-        if (seqno == ovsdb_idl_get_seqno(idl)) {
-            ovsdb_idl_wait(idl);
-            poll_block();
-        }
-    }
-
-    return NULL;
-}
-
-/* All options that affect the main loop and are not external. */
-#define MAIN_LOOP_OPTION_ENUMS                  \
-        OPT_NO_WAIT,                            \
-        OPT_WAIT,                               \
-        OPT_PRINT_WAIT_TIME,                    \
-        OPT_DRY_RUN,                            \
-        OPT_ONELINE
-
-#define MAIN_LOOP_LONG_OPTIONS                                          \
-        {"no-wait", no_argument, NULL, OPT_NO_WAIT},                    \
-        {"wait", required_argument, NULL, OPT_WAIT},                    \
-        {"print-wait-time", no_argument, NULL, OPT_PRINT_WAIT_TIME},    \
-        {"dry-run", no_argument, NULL, OPT_DRY_RUN},                    \
-        {"oneline", no_argument, NULL, OPT_ONELINE},                    \
-        {"timeout", required_argument, NULL, 't'}
-
-enum {
-    OPT_DB = UCHAR_MAX + 1,
-    OPT_NO_SYSLOG,
-    OPT_LOCAL,
-    OPT_COMMANDS,
-    OPT_OPTIONS,
-    OPT_LEADER_ONLY,
-    OPT_NO_LEADER_ONLY,
-    OPT_SHUFFLE_REMOTES,
-    OPT_NO_SHUFFLE_REMOTES,
-    OPT_BOOTSTRAP_CA_CERT,
-    MAIN_LOOP_OPTION_ENUMS,
-    OVN_DAEMON_OPTION_ENUMS,
-    VLOG_OPTION_ENUMS,
-    TABLE_OPTION_ENUMS,
-    SSL_OPTION_ENUMS,
-};
-
-static char * OVS_WARN_UNUSED_RESULT
-handle_main_loop_option(int opt, const char *arg, bool *handled)
-{
-    ovs_assert(handled);
-    *handled = true;
-
-    switch (opt) {
-    case OPT_ONELINE:
-        oneline = true;
-        break;
-
-    case OPT_NO_WAIT:
-        wait_type = NBCTL_WAIT_NONE;
-        break;
-
-    case OPT_WAIT:
-        if (!strcmp(arg, "none")) {
-            wait_type = NBCTL_WAIT_NONE;
-        } else if (!strcmp(arg, "sb")) {
-            wait_type = NBCTL_WAIT_SB;
-        } else if (!strcmp(arg, "hv")) {
-            wait_type = NBCTL_WAIT_HV;
-        } else {
-            return xstrdup("argument to --wait must be "
-                           "\"none\", \"sb\", or \"hv\"");
-        }
-        break;
-
-    case OPT_PRINT_WAIT_TIME:
-        print_wait_time = true;
-        break;
-
-    case OPT_DRY_RUN:
-        dry_run = true;
-        break;
-
-    case 't':
-        if (!str_to_uint(arg, 10, &timeout) || !timeout) {
-            return xasprintf("value %s on -t or --timeout is invalid", arg);
-        }
-        break;
-
-    default:
-        *handled = false;
-        break;
-    }
-
-    return NULL;
-}
-
-static char * OVS_WARN_UNUSED_RESULT
-build_short_options(const struct option *long_options, bool print_errors)
-{
-    char *tmp, *short_options;
-
-    tmp = ovs_cmdl_long_options_to_short_options(long_options);
-    short_options = xasprintf("+%s%s", print_errors ? "" : ":", tmp);
-    free(tmp);
-
-    return short_options;
-}
-
-static struct option * OVS_WARN_UNUSED_RESULT
-append_command_options(const struct option *options, int opt_val)
-{
-    struct option *o;
-    size_t n_allocated;
-    size_t n_existing;
-    int i;
-
-    for (i = 0; options[i].name; i++) {
-        ;
-    }
-    n_allocated = i + 1;
-    n_existing = i;
-
-    /* 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. */
-    o = xmemdup(options, n_allocated * sizeof *options);
-    ctl_add_cmd_options(&o, &n_existing, &n_allocated, opt_val);
-
-    return o;
-}
-
-static struct option *
-get_all_options(void)
-{
-    static const struct option global_long_options[] = {
-        {"db", required_argument, NULL, OPT_DB},
-        {"no-syslog", no_argument, NULL, OPT_NO_SYSLOG},
-        {"help", no_argument, NULL, 'h'},
-        {"commands", no_argument, NULL, OPT_COMMANDS},
-        {"options", no_argument, NULL, OPT_OPTIONS},
-        {"leader-only", no_argument, NULL, OPT_LEADER_ONLY},
-        {"no-leader-only", no_argument, NULL, OPT_NO_LEADER_ONLY},
-        {"shuffle-remotes", no_argument, NULL, OPT_SHUFFLE_REMOTES},
-        {"no-shuffle-remotes", no_argument, NULL, OPT_NO_SHUFFLE_REMOTES},
-        {"version", no_argument, NULL, 'V'},
-        {"unixctl", required_argument, NULL, 'u'},
-        MAIN_LOOP_LONG_OPTIONS,
-        OVN_DAEMON_LONG_OPTIONS,
-        VLOG_LONG_OPTIONS,
-        STREAM_SSL_LONG_OPTIONS,
-        {"bootstrap-ca-cert", required_argument, NULL, OPT_BOOTSTRAP_CA_CERT},
-        TABLE_LONG_OPTIONS,
-        {NULL, 0, NULL, 0},
-    };
-
-    static struct option *options;
-    if (!options) {
-        options = append_command_options(global_long_options, OPT_LOCAL);
-    }
-
-    return options;
-}
-
-static bool
-has_option(const struct ovs_cmdl_parsed_option *parsed_options, size_t n,
-           int option)
-{
-    for (const struct ovs_cmdl_parsed_option *po = parsed_options;
-         po < &parsed_options[n]; po++) {
-        if (po->o->val == option) {
-            return true;
-        }
-    }
-    return false;
-}
-
-static bool
-will_detach(const struct ovs_cmdl_parsed_option *parsed_options, size_t n)
-{
-    return has_option(parsed_options, n, OVN_OPT_DETACH);
-}
-
-static char * OVS_WARN_UNUSED_RESULT
-add_local_option(const char *name, const char *arg,
-                 struct shash *local_options)
-{
-    char *full_name = xasprintf("--%s", name);
-    if (shash_find(local_options, full_name)) {
-        char *error = xasprintf("'%s' option specified multiple times",
-                                full_name);
-        free(full_name);
-        return error;
-    }
-    shash_add_nocopy(local_options, full_name, nullable_xstrdup(arg));
-    return NULL;
-}
-
 static void
-apply_options_direct(const struct ovs_cmdl_parsed_option *parsed_options,
-                     size_t n, struct shash *local_options)
-{
-    for (const struct ovs_cmdl_parsed_option *po = parsed_options;
-         po < &parsed_options[n]; po++) {
-        bool handled;
-        char *error = handle_main_loop_option(po->o->val, po->arg, &handled);
-        if (error) {
-            ctl_fatal("%s", error);
-        }
-        if (handled) {
-            continue;
-        }
-
-        optarg = po->arg;
-        switch (po->o->val) {
-        case OPT_DB:
-            db = po->arg;
-            break;
-
-        case OPT_NO_SYSLOG:
-            vlog_set_levels(&this_module, VLF_SYSLOG, VLL_WARN);
-            break;
-
-        case OPT_LOCAL:
-            error = add_local_option(po->o->name, po->arg, local_options);
-            if (error) {
-                ctl_fatal("%s", error);
-            }
-            break;
-
-        case 'h':
-            usage();
-            exit(EXIT_SUCCESS);
-
-        case OPT_COMMANDS:
-            ctl_print_commands();
-            /* fall through */
-
-        case OPT_OPTIONS:
-            ctl_print_options(get_all_options());
-            /* fall through */
-
-        case OPT_LEADER_ONLY:
-            leader_only = true;
-            break;
-
-        case OPT_NO_LEADER_ONLY:
-            leader_only = false;
-            break;
-
-        case OPT_SHUFFLE_REMOTES:
-            shuffle_remotes = true;
-            break;
-
-        case OPT_NO_SHUFFLE_REMOTES:
-            shuffle_remotes = false;
-            break;
-
-        case 'u':
-            unixctl_path = optarg;
-            break;
-
-        case 'V':
-            ovn_print_version(0, 0);
-            printf("DB Schema %s\n", nbrec_get_db_version());
-            exit(EXIT_SUCCESS);
-
-        OVN_DAEMON_OPTION_HANDLERS
-        VLOG_OPTION_HANDLERS
-        TABLE_OPTION_HANDLERS(&table_style)
-        STREAM_SSL_OPTION_HANDLERS
-
-        case OPT_BOOTSTRAP_CA_CERT:
-            stream_ssl_set_ca_cert_file(po->arg, true);
-            break;
-
-        case '?':
-            exit(EXIT_FAILURE);
-
-        default:
-            abort();
-
-        case 0:
-            break;
-        }
-    }
-
-    if (!db) {
-        db = default_nb_db();
-    }
-}
-
-static void
-usage(void)
+nbctl_usage(void)
 {
     printf("\
 %s: OVN northbound DB management utility\n\
@@ -1213,13 +787,9 @@ nbctl_init(struct ctl_context *ctx OVS_UNUSED)
 }
 
 static void
-nbctl_pre_sync(struct ctl_context *ctx OVS_UNUSED)
+nbctl_pre_sync(struct ctl_context *base OVS_UNUSED)
 {
-    if (wait_type != NBCTL_WAIT_NONE) {
-        force_wait = true;
-    } else {
-        VLOG_INFO("\"sync\" command has no effect without --wait");
-    }
+    force_wait = true;
 }
 
 static void
@@ -6205,305 +5775,6 @@ static const struct ctl_table_class tables[NBREC_N_TABLES] = {
     = {&nbrec_connection_col_target, NULL, NULL},
 };
 
-static char *
-run_prerequisites(struct ctl_command *commands, size_t n_commands,
-                  struct ovsdb_idl *idl)
-{
-    ovsdb_idl_add_table(idl, &nbrec_table_nb_global);
-    if (wait_type == NBCTL_WAIT_SB) {
-        ovsdb_idl_add_column(idl, &nbrec_nb_global_col_sb_cfg);
-        ovsdb_idl_add_column(idl, &nbrec_nb_global_col_sb_cfg_timestamp);
-    } else if (wait_type == NBCTL_WAIT_HV) {
-        ovsdb_idl_add_column(idl, &nbrec_nb_global_col_hv_cfg);
-        ovsdb_idl_add_column(idl, &nbrec_nb_global_col_hv_cfg_timestamp);
-    }
-
-    for (struct ctl_command *c = commands; c < &commands[n_commands]; c++) {
-        if (c->syntax->prerequisites) {
-            struct ctl_context ctx;
-
-            ds_init(&c->output);
-            c->table = NULL;
-
-            ctl_context_init(&ctx, c, idl, NULL, NULL, NULL);
-            (c->syntax->prerequisites)(&ctx);
-            if (ctx.error) {
-                char *error = xstrdup(ctx.error);
-                ctl_context_done(&ctx, c);
-                return error;
-            }
-            ctl_context_done(&ctx, c);
-
-            ovs_assert(!c->output.string);
-            ovs_assert(!c->table);
-        }
-    }
-
-    return NULL;
-}
-
-static void
-oneline_format(struct ds *lines, struct ds *s)
-{
-    size_t j;
-
-    ds_chomp(lines, '\n');
-    for (j = 0; j < lines->length; j++) {
-        int ch = lines->string[j];
-        switch (ch) {
-        case '\n':
-            ds_put_cstr(s, "\\n");
-            break;
-
-        case '\\':
-            ds_put_cstr(s, "\\\\");
-            break;
-
-        default:
-            ds_put_char(s, ch);
-        }
-    }
-    ds_put_char(s, '\n');
-}
-
-static void
-oneline_print(struct ds *lines)
-{
-    struct ds s = DS_EMPTY_INITIALIZER;
-    oneline_format(lines, &s);
-    fputs(ds_cstr(&s), stdout);
-    ds_destroy(&s);
-}
-
-static char *
-do_nbctl(const char *args, struct ctl_command *commands, size_t n_commands,
-         struct ovsdb_idl *idl, const struct timer *wait_timeout, bool *retry)
-{
-    struct ovsdb_idl_txn *txn;
-    enum ovsdb_idl_txn_status status;
-    struct ovsdb_symbol_table *symtab;
-    struct nbctl_context ctx;
-    struct ctl_command *c;
-    struct shash_node *node;
-    int64_t next_cfg = 0;
-    char *error = NULL;
-    int64_t start_time = 0;
-
-    ovs_assert(retry);
-
-    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);
-
-    const struct nbrec_nb_global *nb = nbrec_nb_global_first(idl);
-    if (!nb) {
-        /* XXX add verification that table is empty */
-        nb = nbrec_nb_global_insert(txn);
-    }
-
-    /* Deal with potential overflows. */
-    if (nb->nb_cfg == LLONG_MAX) {
-        nbrec_nb_global_set_nb_cfg(nb, 0);
-    }
-
-    if (wait_type != NBCTL_WAIT_NONE) {
-        ovsdb_idl_txn_increment(txn, &nb->header_, &nbrec_nb_global_col_nb_cfg,
-                                force_wait);
-    }
-
-    symtab = ovsdb_symbol_table_create();
-    for (c = commands; c < &commands[n_commands]; c++) {
-        ds_init(&c->output);
-        c->table = NULL;
-    }
-    nbctl_context_init(&ctx);
-    ctl_context_init(&ctx.base, NULL, idl, txn, symtab, NULL);
-    for (c = commands; c < &commands[n_commands]; c++) {
-        ctl_context_init_command(&ctx.base, c);
-        if (c->syntax->run) {
-            (c->syntax->run)(&ctx.base);
-        }
-        if (ctx.base.error) {
-            error = xstrdup(ctx.base.error);
-            ctl_context_done(&ctx.base, c);
-            goto out_error;
-        }
-        ctl_context_done_command(&ctx.base, c);
-
-        if (ctx.base.try_again) {
-            ctl_context_done(&ctx.base, NULL);
-            goto try_again;
-        }
-    }
-    ctl_context_done(&ctx.base, NULL);
-
-    SHASH_FOR_EACH (node, &symtab->sh) {
-        struct ovsdb_symbol *symbol = node->data;
-        if (!symbol->created) {
-            error = xasprintf("row id \"%s\" is referenced but never created "
-                              "(e.g. with \"-- --id=%s create ...\")",
-                              node->name, node->name);
-            goto out_error;
-        }
-        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 {
-                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);
-            }
-        }
-    }
-
-    start_time = time_wall_msec();
-    status = ovsdb_idl_txn_commit_block(txn);
-    if (wait_type != NBCTL_WAIT_NONE && status == TXN_SUCCESS) {
-        next_cfg = ovsdb_idl_txn_get_increment_new_value(txn);
-    }
-    if (status == TXN_UNCHANGED || status == TXN_SUCCESS) {
-        for (c = commands; c < &commands[n_commands]; c++) {
-            if (c->syntax->postprocess) {
-                ctl_context_init(&ctx.base, c, idl, txn, symtab, NULL);
-                (c->syntax->postprocess)(&ctx.base);
-                if (ctx.base.error) {
-                    error = xstrdup(ctx.base.error);
-                    ctl_context_done(&ctx.base, c);
-                    goto out_error;
-                }
-                ctl_context_done(&ctx.base, c);
-            }
-        }
-    }
-
-    switch (status) {
-    case TXN_UNCOMMITTED:
-    case TXN_INCOMPLETE:
-        OVS_NOT_REACHED();
-
-    case TXN_ABORTED:
-        /* Should not happen--we never call ovsdb_idl_txn_abort(). */
-        error = xstrdup("transaction aborted");
-        goto out_error;
-
-    case TXN_UNCHANGED:
-    case TXN_SUCCESS:
-        break;
-
-    case TXN_TRY_AGAIN:
-        goto try_again;
-
-    case TXN_ERROR:
-        error = xasprintf("transaction error: %s",
-                          ovsdb_idl_txn_get_error(txn));
-        goto out_error;
-
-    case TXN_NOT_LOCKED:
-        /* Should not happen--we never call ovsdb_idl_set_lock(). */
-        error = xstrdup("database not locked");
-        goto out_error;
-
-    default:
-        OVS_NOT_REACHED();
-    }
-
-    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) {
-            oneline_print(ds);
-        } else {
-            fputs(ds_cstr(ds), stdout);
-        }
-    }
-
-    if (wait_type != NBCTL_WAIT_NONE && status != TXN_UNCHANGED) {
-        ovsdb_idl_enable_reconnect(idl);
-        for (;;) {
-            ovsdb_idl_run(idl);
-            NBREC_NB_GLOBAL_FOR_EACH (nb, idl) {
-                int64_t cur_cfg = (wait_type == NBCTL_WAIT_SB
-                                   ? nb->sb_cfg
-                                   : MIN(nb->sb_cfg, nb->hv_cfg));
-                if (cur_cfg >= next_cfg) {
-                    if (print_wait_time) {
-                        printf("Time spent on processing nb_cfg %"PRId64":\n",
-                               next_cfg);
-                        printf("\tovn-northd delay before processing:"
-                               "\t%"PRId64"ms\n",
-                               nb->nb_cfg_timestamp - start_time);
-                        printf("\tovn-northd completion:"
-                               "\t\t\t%"PRId64"ms\n",
-                               nb->sb_cfg_timestamp - start_time);
-                        if (wait_type == NBCTL_WAIT_HV) {
-                            printf("\tovn-controller(s) completion:"
-                                   "\t\t%"PRId64"ms\n",
-                                   nb->hv_cfg_timestamp - start_time);
-                        }
-                    }
-                    goto done;
-                }
-            }
-            ovsdb_idl_wait(idl);
-            if (wait_timeout) {
-                timer_wait(wait_timeout);
-            }
-            poll_block();
-            if (wait_timeout && timer_expired(wait_timeout)) {
-                error = xstrdup("timeout expired");
-                goto out_error;
-            }
-        }
-    done: ;
-    }
-
-    nbctl_context_destroy(&ctx);
-    ovsdb_symbol_table_destroy(symtab);
-    ovsdb_idl_txn_destroy(txn);
-    the_idl_txn = NULL;
-
-    *retry = false;
-    return NULL;
-
-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. */
-    *retry = true;
-
-out_error:
-    ovsdb_idl_txn_abort(txn);
-    ovsdb_idl_txn_destroy(txn);
-    the_idl_txn = NULL;
-
-    nbctl_context_destroy(&ctx);
-    ovsdb_symbol_table_destroy(symtab);
-    return error;
-}
-
-/* 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(the_idl);
-    exit(status);
-}
-
 static const struct ctl_command_syntax nbctl_commands[] = {
     { "init", 0, 0, "", NULL, nbctl_init, NULL, "", RW },
     { "sync", 0, 0, "", nbctl_pre_sync, nbctl_sync, NULL, "", RO },
@@ -6705,397 +5976,30 @@ static const struct ctl_command_syntax nbctl_commands[] = {
     {NULL, 0, 0, NULL, NULL, NULL, NULL, "", RO},
 };
 
-/* Registers nbctl and common db commands. */
-static void
-nbctl_cmd_init(void)
+int
+main(int argc, char *argv[])
 {
-    ctl_init(&nbrec_idl_class, nbrec_table_classes, tables, NULL, nbctl_exit);
-    ctl_register_commands(nbctl_commands);
-}
-
-/* Server implementation. */
+    struct ovn_dbctl_options dbctl_options = {
+        .db_version = nbrec_get_db_version(),
+        .default_db = default_nb_db(),
 
-#undef ctl_fatal
+        .options_env_var_name = "OVN_NBCTL_OPTIONS",
+        .daemon_env_var_name = "OVN_NBCTL_DAEMON",
 
-static const struct option *
-find_option_by_value(const struct option *options, int value)
-{
-    const struct option *o;
+        .idl_class = &nbrec_idl_class,
+        .tables = tables,
+        .cmd_show_table = NULL,
+        .commands = nbctl_commands,
 
-    for (o = options; o->name; o++) {
-        if (o->val == value) {
-            return o;
-        }
-    }
-    return NULL;
-}
+        .usage = nbctl_usage,
+        .add_base_prerequisites = nbctl_add_base_prerequisites,
+        .pre_execute = nbctl_pre_execute,
+        .post_execute = nbctl_post_execute,
 
-static char * OVS_WARN_UNUSED_RESULT
-server_parse_options(int argc, char *argv[], struct shash *local_options,
-                     int *n_options_p)
-{
-    static const struct option global_long_options[] = {
-        VLOG_LONG_OPTIONS,
-        MAIN_LOOP_LONG_OPTIONS,
-        TABLE_LONG_OPTIONS,
-        {NULL, 0, NULL, 0},
+        .ctx_create = nbctl_ctx_create,
+        .ctx_destroy = nbctl_ctx_destroy,
     };
-    const int n_global_long_options = ARRAY_SIZE(global_long_options) - 1;
-    char *short_options;
-    struct option *options;
-    char *error = NULL;
-
-    ovs_assert(n_options_p);
-
-    short_options = build_short_options(global_long_options, false);
-    options = append_command_options(global_long_options, OPT_LOCAL);
-
-    optind = 0;
-    opterr = 0;
-    for (;;) {
-        int idx;
-        int c;
-
-        c = getopt_long(argc, argv, short_options, options, &idx);
-        if (c == -1) {
-            break;
-        }
-
-        bool handled;
-        error = handle_main_loop_option(c, optarg, &handled);
-        if (error) {
-            goto out;
-        }
-        if (handled) {
-            continue;
-        }
-
-        switch (c) {
-        case OPT_LOCAL:
-            error = add_local_option(options[idx].name, optarg, local_options);
-            if (error) {
-                goto out;
-            }
-            break;
-
-        VLOG_OPTION_HANDLERS
-        TABLE_OPTION_HANDLERS(&table_style)
 
-        case '?':
-            if (find_option_by_value(options, optopt)) {
-                error = xasprintf("option '%s' doesn't allow an argument",
-                                  argv[optind-1]);
-            } else if (optopt) {
-                error = xasprintf("unrecognized option '%c'", optopt);
-            } else {
-                error = xasprintf("unrecognized option '%s'", argv[optind-1]);
-            }
-            goto out;
-            break;
-
-        case ':':
-            error = xasprintf("option '%s' requires an argument",
-                              argv[optind-1]);
-            goto out;
-            break;
-
-        case 0:
-            break;
-
-        default:
-            error = xasprintf("unhandled option '%c'", c);
-            goto out;
-            break;
-        }
-    }
-    *n_options_p = optind;
-
-out:
-    for (int i = n_global_long_options; options[i].name; i++) {
-        free(CONST_CAST(char *, options[i].name));
-    }
-    free(options);
-    free(short_options);
-
-    return error;
-}
-
-static void
-server_cmd_exit(struct unixctl_conn *conn, int argc OVS_UNUSED,
-                const char *argv[] OVS_UNUSED, void *exiting_)
-{
-    bool *exiting = exiting_;
-    *exiting = true;
-    unixctl_command_reply(conn, NULL);
-}
-
-static void
-server_cmd_run(struct unixctl_conn *conn, int argc, const char **argv_,
-               void *idl_)
-{
-    struct ovsdb_idl *idl = idl_;
-    struct ctl_command *commands = NULL;
-    struct shash local_options;
-    size_t n_commands = 0;
-    int n_options = 0;
-    char *error = NULL;
-
-    /* Copy args so that getopt() can permute them. Leave last entry NULL. */
-    char **argv = xcalloc(argc + 1, sizeof *argv);
-    for (int i = 0; i < argc; i++) {
-        argv[i] = xstrdup(argv_[i]);
-    }
-
-    /* Reset global state. */
-    oneline = false;
-    dry_run = false;
-    wait_type = NBCTL_WAIT_NONE;
-    force_wait = false;
-    timeout = 0;
-    table_style = table_style_default;
-
-    /* Parse commands & options. */
-    char *args = process_escape_args(argv);
-    shash_init(&local_options);
-    error = server_parse_options(argc, argv, &local_options, &n_options);
-    if (error) {
-        unixctl_command_reply_error(conn, error);
-        goto out;
-    }
-    error = ctl_parse_commands(argc - n_options, argv + n_options,
-                               &local_options, &commands, &n_commands);
-    if (error) {
-        unixctl_command_reply_error(conn, error);
-        goto out;
-    }
-    VLOG(ctl_might_write_to_db(commands, n_commands) ? VLL_INFO : VLL_DBG,
-         "Running command %s", args);
-
-    struct timer *wait_timeout = NULL;
-    struct timer wait_timeout_;
-    if (timeout) {
-        wait_timeout = &wait_timeout_;
-        timer_set_duration(wait_timeout, timeout * 1000);
-    }
-
-    error = run_prerequisites(commands, n_commands, idl);
-    if (error) {
-        unixctl_command_reply_error(conn, error);
-        goto out;
-    }
-    error = main_loop(args, commands, n_commands, idl, wait_timeout);
-    if (error) {
-        unixctl_command_reply_error(conn, error);
-        goto out;
-    }
-
-    struct ds output = DS_EMPTY_INITIALIZER;
-    table_format_reset();
-    for (struct ctl_command *c = commands; c < &commands[n_commands]; c++) {
-        if (c->table) {
-            table_format(c->table, &table_style, &output);
-        } else if (oneline) {
-            oneline_format(&c->output, &output);
-        } else {
-            ds_put_cstr(&output, ds_cstr_ro(&c->output));
-        }
-    }
-    unixctl_command_reply(conn, ds_cstr_ro(&output));
-    ds_destroy(&output);
-
-out:
-    free(error);
-
-    struct ctl_command *c;
-    for (c = commands; c < &commands[n_commands]; c++) {
-        ds_destroy(&c->output);
-        table_destroy(c->table);
-        free(c->table);
-        shash_destroy_free_data(&c->options);
-    }
-    free(commands);
-    shash_destroy_free_data(&local_options);
-    free(args);
-    for (int i = 0; i < argc; i++) {
-        free(argv[i]);
-    }
-    free(argv);
+    return ovn_dbctl_main(argc, argv, &dbctl_options);
 }
 
-static void
-server_cmd_init(struct ovsdb_idl *idl, bool *exiting)
-{
-    unixctl_command_register("exit", "", 0, 0, server_cmd_exit, exiting);
-    unixctl_command_register("run", "", 0, INT_MAX, server_cmd_run, idl);
-}
-
-static void
-server_loop(struct ovsdb_idl *idl, int argc, char *argv[])
-{
-    struct unixctl_server *server = NULL;
-    bool exiting = false;
-
-    service_start(&argc, &argv);
-    daemonize_start(false);
-
-    char *abs_unixctl_path = get_abs_unix_ctl_path(unixctl_path);
-    int error = unixctl_server_create(abs_unixctl_path, &server);
-    free(abs_unixctl_path);
-
-    if (error) {
-        ctl_fatal("failed to create unixctl server (%s)",
-                  ovs_retval_to_string(error));
-    }
-    puts(unixctl_server_get_path(server));
-    fflush(stdout);
-    server_cmd_init(idl, &exiting);
-
-    for (;;) {
-        memory_run();
-        if (memory_should_report()) {
-            struct simap usage = SIMAP_INITIALIZER(&usage);
-
-            /* Nothing special to report yet. */
-            memory_report(&usage);
-            simap_destroy(&usage);
-        }
-
-        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 (ovsdb_idl_has_ever_connected(idl)) {
-            daemonize_complete();
-        }
-        unixctl_server_run(server);
-
-        if (exiting) {
-            break;
-        }
-
-        memory_wait();
-        ovsdb_idl_wait(idl);
-        unixctl_server_wait(server);
-        poll_block();
-    }
-
-    unixctl_server_destroy(server);
-}
-
-static void
-nbctl_client(const char *socket_name,
-             const struct ovs_cmdl_parsed_option *parsed_options, size_t n,
-             int argc, char *argv[])
-{
-    struct svec args = SVEC_EMPTY_INITIALIZER;
-
-    for (const struct ovs_cmdl_parsed_option *po = parsed_options;
-         po < &parsed_options[n]; po++) {
-        optarg = po->arg;
-        switch (po->o->val) {
-        case OPT_DB:
-            VLOG_WARN("not using ovn-nbctl daemon because of %s option",
-                      po->o->name);
-            svec_destroy(&args);
-            return;
-
-        case OPT_NO_SYSLOG:
-            vlog_set_levels(&this_module, VLF_SYSLOG, VLL_WARN);
-            break;
-
-        case 'h':
-            usage();
-            exit(EXIT_SUCCESS);
-
-        case OPT_COMMANDS:
-            ctl_print_commands();
-            /* fall through */
-
-        case OPT_OPTIONS:
-            ctl_print_options(get_all_options());
-            /* fall through */
-
-        case OPT_LEADER_ONLY:
-        case OPT_NO_LEADER_ONLY:
-        case OPT_SHUFFLE_REMOTES:
-        case OPT_NO_SHUFFLE_REMOTES:
-        case OPT_BOOTSTRAP_CA_CERT:
-        STREAM_SSL_CASES
-        OVN_DAEMON_OPTION_CASES
-            VLOG_INFO("using ovn-nbctl daemon, ignoring %s option",
-                      po->o->name);
-            break;
-
-        case 'u':
-            socket_name = optarg;
-            break;
-
-        case 'V':
-            ovs_print_version(0, 0);
-            printf("DB Schema %s\n", nbrec_get_db_version());
-            exit(EXIT_SUCCESS);
-
-        case 't':
-            if (!str_to_uint(po->arg, 10, &timeout) || !timeout) {
-                ctl_fatal("value %s on -t or --timeout is invalid", po->arg);
-            }
-            break;
-
-        VLOG_OPTION_HANDLERS
-
-        case OPT_LOCAL:
-        default:
-            if (po->arg) {
-                svec_add_nocopy(&args,
-                                xasprintf("--%s=%s", po->o->name, po->arg));
-            } else {
-                svec_add_nocopy(&args, xasprintf("--%s", po->o->name));
-            }
-            break;
-        }
-    }
-
-    ovs_assert(socket_name && socket_name[0]);
-
-    svec_add(&args, "--");
-    for (int i = optind; i < argc; i++) {
-        svec_add(&args, argv[i]);
-    }
-
-    ctl_timeout_setup(timeout);
-
-    struct jsonrpc *client;
-    int error = unixctl_client_create(socket_name, &client);
-    if (error) {
-        ctl_fatal("%s: could not connect to ovn-nb daemon (%s); "
-                  "unset OVN_NB_DAEMON to avoid using daemon",
-                  socket_name, ovs_strerror(error));
-    }
-
-    char *cmd_result;
-    char *cmd_error;
-    error = unixctl_client_transact(client, "run",
-                                    args.n, args.names,
-                                    &cmd_result, &cmd_error);
-    if (error) {
-        ctl_fatal("%s: transaction error (%s)",
-                  socket_name, ovs_strerror(error));
-    }
-    svec_destroy(&args);
-
-    int exit_status;
-    if (cmd_error) {
-        exit_status = EXIT_FAILURE;
-        fprintf(stderr, "%s: %s", program_name, cmd_error);
-    } else {
-        exit_status = EXIT_SUCCESS;
-        fputs(cmd_result, stdout);
-    }
-    free(cmd_result);
-    free(cmd_error);
-    jsonrpc_close(client);
-    exit(exit_status);
-}
