diff --git a/NEWS b/NEWS
index 8b170bcba6fb..a98529ac4ebe 100644
--- a/NEWS
+++ b/NEWS
@@ -7,7 +7,9 @@ Post-v21.03.0
     (This may take testing and tuning to be effective.)  This version of OVN
     requires DDLog 0.36.
   - Introduce ovn-controller incremetal processing engine statistics
-  - ovn-nbctl daemon mode is no longer considered experimental.
+  - Utilities:
+    * ovn-nbctl daemon mode is no longer considered experimental.
+    * ovn-sbctl now also supports daemon mode.
 
 OVN v21.03.0 - 12 Mar 2021
 -------------------------
diff --git a/manpages.mk b/manpages.mk
index 44e544681424..3334b38f943d 100644
--- a/manpages.mk
+++ b/manpages.mk
@@ -10,20 +10,3 @@ lib/common-syn.man:
 lib/common.man:
 lib/ovs.tmac:
 
-utilities/ovn-sbctl.8: \
-	utilities/ovn-sbctl.8.in \
-	lib/common.man \
-	lib/db-ctl-base.man \
-	lib/ovs.tmac \
-	lib/ssl-bootstrap.man \
-	lib/ssl.man \
-	lib/table.man \
-	lib/vlog.man
-utilities/ovn-sbctl.8.in:
-lib/common.man:
-lib/db-ctl-base.man:
-lib/ovs.tmac:
-lib/ssl-bootstrap.man:
-lib/ssl.man:
-lib/table.man:
-lib/vlog.man:
diff --git a/tests/ovn-sbctl.at b/tests/ovn-sbctl.at
index 2712cc15490c..9334762fd313 100644
--- a/tests/ovn-sbctl.at
+++ b/tests/ovn-sbctl.at
@@ -1,9 +1,14 @@
 AT_BANNER([ovn-sbctl])
 
+OVS_START_SHELL_HELPERS
 # OVN_SBCTL_TEST_START
 m4_define([OVN_SBCTL_TEST_START],
-  [dnl Create databases (ovn-nb, ovn-sb).
-   AT_KEYWORDS([ovn])
+  [AT_KEYWORDS([ovn])
+   AT_CAPTURE_FILE([ovsdb-server.log])
+   AT_CAPTURE_FILE([ovn-northd.log])
+   ovn_sbctl_test_start $1])
+ovn_sbctl_test_start() {
+   dnl Create databases (ovn-nb, ovn-sb).
    for daemon in ovn-nb ovn-sb; do
       AT_CHECK([ovsdb-tool create $daemon.db $abs_top_srcdir/${daemon}.ovsschema])
    done
@@ -15,27 +20,54 @@ m4_define([OVN_SBCTL_TEST_START],
    AT_CHECK([[sed < stderr '
 /vlog|INFO|opened log file/d
 /ovsdb_server|INFO|ovsdb-server (Open vSwitch)/d']])
-   AT_CAPTURE_FILE([ovsdb-server.log])
 
    dnl Start ovn-northd.
    AT_CHECK([ovn-northd --detach --no-chdir --pidfile --log-file --ovnnb-db=unix:$OVS_RUNDIR/ovnnb_db.sock --ovnsb-db=unix:$OVS_RUNDIR/ovnsb_db.sock], [0], [], [stderr])
    on_exit "kill `cat ovn-northd.pid`"
    AT_CHECK([[sed < stderr '
 /vlog|INFO|opened log file/d']])
-   AT_CAPTURE_FILE([ovn-northd.log])
-])
+
+   AS_CASE([$1],
+     [daemon],
+       [export OVN_SB_DAEMON=$(ovn-sbctl --pidfile --detach --no-chdir --log-file -vsocket_util:off)
+        on_exit "kill `cat ovn-sbctl.pid`"],
+     [direct], [],
+     [*], [AT_FAIL_IF(:)])
+}
 
 # OVN_SBCTL_TEST_STOP
-m4_define([OVN_SBCTL_TEST_STOP],
-  [AT_CHECK([check_logs "$1"])
-   OVS_APP_EXIT_AND_WAIT([ovn-northd])
-   OVS_APP_EXIT_AND_WAIT_BY_TARGET([$OVS_RUNDIR/ovnnb_db.ctl], [$OVS_RUNDIR/ovnnb_db.pid])
-   OVS_APP_EXIT_AND_WAIT_BY_TARGET([$OVS_RUNDIR/ovnsb_db.ctl], [$OVS_RUNDIR/ovnsb_db.pid])])
+m4_define([OVN_SBCTL_TEST_STOP], [ovn_sbctl_test_stop])
+ovn_sbctl_test_stop() {
+  AT_CHECK([check_logs "$1"])
+  OVS_APP_EXIT_AND_WAIT([ovn-northd])
+  OVS_APP_EXIT_AND_WAIT_BY_TARGET([$OVS_RUNDIR/ovnnb_db.ctl], [$OVS_RUNDIR/ovnnb_db.pid])
+  OVS_APP_EXIT_AND_WAIT_BY_TARGET([$OVS_RUNDIR/ovnsb_db.ctl], [$OVS_RUNDIR/ovnsb_db.pid])
+}
+OVS_END_SHELL_HELPERS
+
+# OVN_SBCTL_TEST(NAME, TITLE, COMMANDS)
+m4_define([OVN_SBCTL_TEST],
+   [OVS_START_SHELL_HELPERS
+    $1() {
+      $3
+    }
+    OVS_END_SHELL_HELPERS
+
+    AT_SETUP([ovn-sbctl - $2 - direct])
+    OVN_SBCTL_TEST_START direct
+    $1
+    OVN_SBCTL_TEST_STOP
+    AT_CLEANUP
+
+    AT_SETUP([ovn-sbctl - $2 - daemon])
+    OVN_SBCTL_TEST_START daemon
+    $1
+    OVN_SBCTL_TEST_STOP
+    AT_CLEANUP])
 
 dnl ---------------------------------------------------------------------
 
-AT_SETUP([ovn-sbctl - chassis commands])
-OVN_SBCTL_TEST_START
+OVN_SBCTL_TEST([ovn_sbctl_chassis_commands], [ovn-sbctl - chassis commands], [
 ovn_init_db ovn-sb
 
 AT_CHECK([ovn-sbctl chassis-add ch0 geneve 1.2.3.4])
@@ -61,16 +93,14 @@ AT_CHECK([ovn-sbctl -f csv -d bare --no-headings --columns ip,type list encap |
 1.2.3.5,vxlan
 ])
 
-OVN_SBCTL_TEST_STOP
 as ovn-sb
 OVS_APP_EXIT_AND_WAIT([ovsdb-server])
-AT_CLEANUP
+as
+])
 
 dnl ---------------------------------------------------------------------
 
-AT_SETUP([ovn-sbctl])
-OVN_SBCTL_TEST_START
-
+OVN_SBCTL_TEST([ovn_sbctl_commands], [ovn-sbctl], [
 AT_CHECK([ovn-nbctl ls-add br-test])
 AT_CHECK([ovn-nbctl lsp-add br-test vif0])
 AT_CHECK([ovn-nbctl lsp-set-addresses vif0 f0:ab:cd:ef:01:02])
@@ -131,20 +161,14 @@ mac                 : [[]]
 type                : vtep
 options             : {vtep_logical_switch=l0, vtep_physical_switch=p0}
 ])
-
-OVN_SBCTL_TEST_STOP
-AT_CLEANUP
+])
 
 dnl ---------------------------------------------------------------------
 
-AT_SETUP([ovn-sbctl - connection])
-OVN_SBCTL_TEST_START
-
+OVN_SBCTL_TEST([ovn_sbctl_connection], [ovn-sbctl - connection], [
 AT_CHECK([ovn-sbctl --inactivity-probe=30000 set-connection ptcp:6641:127.0.0.1 punix:$OVS_RUNDIR/ovnsb_db.sock])
 AT_CHECK([ovn-sbctl list connection | grep inactivity_probe], [0], [dnl
 inactivity_probe    : 30000
 inactivity_probe    : 30000
 ])
-
-OVN_SBCTL_TEST_STOP
-AT_CLEANUP
+])
\ No newline at end of file
diff --git a/utilities/automake.mk b/utilities/automake.mk
index 50c0cfded018..a03892f2055a 100644
--- a/utilities/automake.mk
+++ b/utilities/automake.mk
@@ -14,7 +14,6 @@ man_MANS += \
     utilities/ovn-appctl.8
 
 MAN_ROOTS += \
-    utilities/ovn-sbctl.8.in \
     utilities/ovn-detrace.1.in
 
 # Docker drivers
@@ -30,6 +29,7 @@ EXTRA_DIST += \
     utilities/ovn-docker-overlay-driver.in \
     utilities/ovn-docker-underlay-driver.in \
     utilities/ovn-nbctl.8.xml \
+    utilities/ovn-sbctl.8.xml \
     utilities/ovn-ic-nbctl.8.xml \
     utilities/ovn-ic-sbctl.8.xml \
     utilities/ovn-appctl.8.xml \
@@ -79,7 +79,10 @@ utilities_ovn_nbctl_LDADD = lib/libovn.la $(OVSDB_LIBDIR)/libovsdb.la $(OVS_LIBD
 
 # ovn-sbctl
 bin_PROGRAMS += utilities/ovn-sbctl
-utilities_ovn_sbctl_SOURCES = utilities/ovn-sbctl.c
+utilities_ovn_sbctl_SOURCES = \
+    utilities/ovn-dbctl.c \
+    utilities/ovn-dbctl.h \
+    utilities/ovn-sbctl.c
 utilities_ovn_sbctl_LDADD = lib/libovn.la $(OVSDB_LIBDIR)/libovsdb.la $(OVS_LIBDIR)/libopenvswitch.la
 
 # ovn-ic-nbctl
diff --git a/utilities/ovn-dbctl.c b/utilities/ovn-dbctl.c
index d815dc5c8c5f..ffe85ce6d5d9 100644
--- a/utilities/ovn-dbctl.c
+++ b/utilities/ovn-dbctl.c
@@ -327,7 +327,8 @@ enum {
 };
 
 static char * OVS_WARN_UNUSED_RESULT
-handle_main_loop_option(int opt, const char *arg, bool *handled)
+handle_main_loop_option(const struct ovn_dbctl_options *dbctl_options,
+                        int opt, const char *arg, bool *handled)
 {
     ovs_assert(handled);
     *handled = true;
@@ -338,11 +339,16 @@ handle_main_loop_option(int opt, const char *arg, bool *handled)
         break;
 
     case OPT_NO_WAIT:
+        if (!dbctl_options->allow_wait) {
+            return xstrdup("--no-wait not supported");
+        }
         wait_type = NBCTL_WAIT_NONE;
         break;
 
     case OPT_WAIT:
-        if (!strcmp(arg, "none")) {
+        if (!dbctl_options->allow_wait) {
+            return xstrdup("--wait not supported");
+        } else if (!strcmp(arg, "none")) {
             wait_type = NBCTL_WAIT_NONE;
         } else if (!strcmp(arg, "sb")) {
             wait_type = NBCTL_WAIT_SB;
@@ -355,6 +361,9 @@ handle_main_loop_option(int opt, const char *arg, bool *handled)
         break;
 
     case OPT_PRINT_WAIT_TIME:
+        if (!dbctl_options->allow_wait) {
+            return xstrdup("--print-wait-time not supported");
+        }
         print_wait_time = true;
         break;
 
@@ -486,7 +495,8 @@ apply_options_direct(const struct ovn_dbctl_options *dbctl_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);
+        char *error = handle_main_loop_option(dbctl_options,
+                                              po->o->val, po->arg, &handled);
         if (error) {
             ctl_fatal("%s", error);
         }
@@ -834,7 +844,8 @@ find_option_by_value(const struct option *options, int value)
 }
 
 static char * OVS_WARN_UNUSED_RESULT
-server_parse_options(int argc, char *argv[], struct shash *local_options,
+server_parse_options(const struct ovn_dbctl_options *dbctl_options,
+                     int argc, char *argv[], struct shash *local_options,
                      int *n_options_p)
 {
     static const struct option global_long_options[] = {
@@ -865,7 +876,7 @@ server_parse_options(int argc, char *argv[], struct shash *local_options,
         }
 
         bool handled;
-        error = handle_main_loop_option(c, optarg, &handled);
+        error = handle_main_loop_option(dbctl_options, c, optarg, &handled);
         if (error) {
             goto out;
         }
@@ -967,7 +978,8 @@ server_cmd_run(struct unixctl_conn *conn, int argc, const char **argv_,
     /* Parse commands & options. */
     char *args = process_escape_args(argv);
     shash_init(&local_options);
-    error = server_parse_options(argc, argv, &local_options, &n_options);
+    error = server_parse_options(dbctl_options,
+                                 argc, argv, &local_options, &n_options);
     if (error) {
         unixctl_command_reply_error(conn, error);
         goto out;
diff --git a/utilities/ovn-dbctl.h b/utilities/ovn-dbctl.h
index 5accf3c5e028..a1fbede6b5ce 100644
--- a/utilities/ovn-dbctl.h
+++ b/utilities/ovn-dbctl.h
@@ -15,7 +15,7 @@
 #ifndef OVN_DBCTL_H
 #define OVN_DBCTL_H 1
 
-/* ovn-nbctl infrastructure code. */
+/* Common code for ovn-sbctl and ovn-nbctl. */
 
 #include <stdbool.h>
 #include "ovsdb-idl.h"
@@ -31,6 +31,7 @@ enum nbctl_wait_type {
 struct ovn_dbctl_options {
     const char *db_version;     /* Database schema version. */
     const char *default_db;     /* Default database remote. */
+    bool allow_wait;            /* Allow --wait and related options? */
 
     /* Names of important environment variables. */
     const char *options_env_var_name; /* OVN_??_OPTIONS. */
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index 42841bfb8890..ff7438a49d4b 100644
--- a/utilities/ovn-nbctl.c
+++ b/utilities/ovn-nbctl.c
@@ -5982,6 +5982,7 @@ main(int argc, char *argv[])
     struct ovn_dbctl_options dbctl_options = {
         .db_version = nbrec_get_db_version(),
         .default_db = default_nb_db(),
+        .allow_wait = true,
 
         .options_env_var_name = "OVN_NBCTL_OPTIONS",
         .daemon_env_var_name = "OVN_NB_DAEMON",
diff --git a/utilities/ovn-sbctl.8.in b/utilities/ovn-sbctl.8.in
deleted file mode 100644
index 153e72e6c28d..000000000000
--- a/utilities/ovn-sbctl.8.in
+++ /dev/null
@@ -1,317 +0,0 @@
-.\" -*- nroff -*-
-.so lib/ovs.tmac
-.TH ovn\-sbctl 8 "@VERSION@" "OVN" "OVN Manual"
-.\" This program's name:
-.ds PN ovn\-sbctl
-.
-.SH NAME
-ovn\-sbctl \- utility for querying and configuring \fBOVN_Southbound\fR database
-.
-.SH SYNOPSIS
-\fBovn\-sbctl\fR [\fIoptions\fR] \fB\-\-\fR [\fIoptions\fR] \fIcommand
-\fR[\fIargs\fR] [\fB\-\-\fR [\fIoptions\fR] \fIcommand \fR[\fIargs\fR]]...
-.
-.SH DESCRIPTION
-The \fBovn\-sbctl\fR program configures the \fBOVN_Southbound\fR database
-by providing a high\-level interface to its configuration database.  See
-\fBovn\-sb\fR(5) for comprehensive documentation of the database schema.
-.PP
-\fBovn\-sbctl\fR connects to an \fBovsdb\-server\fR process that
-maintains an OVN_Southbound configuration database.  Using this
-connection, it queries and possibly applies changes to the database,
-depending on the supplied commands.
-.PP
-\fBovn\-sbctl\fR can perform any number of commands in a single run,
-implemented as a single atomic transaction against the database.
-.PP
-The \fBovn\-sbctl\fR command line begins with global options (see
-\fBOPTIONS\fR below for details).  The global options are followed by
-one or more commands.  Each command should begin with \fB\-\-\fR by
-itself as a command-line argument, to separate it from the following
-commands.  (The \fB\-\-\fR before the first command is optional.)  The
-command
-itself starts with command-specific options, if any, followed by the
-command name and any arguments.
-.
-.SH OPTIONS
-.
-The following options affect the behavior of \fBovn\-sbctl\fR as a
-whole.  Some individual commands also accept their own options, which
-are given just before the command name.  If the first command on the
-command line has options, then those options must be separated from
-the global options by \fB\-\-\fR.
-.
-.IP "\fB\-\-db=\fIserver\fR"
-The OVSDB database remote to contact.  If the \fBOVN_SB_DB\fR
-environment variable is set, its value is used as the default.
-Otherwise, the default is \fBunix:@RUNDIR@/ovnsb_db.sock\fR, but this
-default is unlikely to be useful outside of single-machine OVN test
-environments.
-.IP
-\fIserver\fR may be an OVSDB active or passive connection method,
-e.g. \fBssl:192.168.10.5:6640\fR, as described in \fBovsdb\fR(7).
-.
-.IP "\fB\-\-leader\-only\fR"
-.IQ "\fB\-\-no\-leader\-only\fR"
-By default, or with \fB\-\-leader\-only\fR, when the database server
-is a clustered database, \fBovn\-sbctl\fR will avoid servers other
-than the cluster leader.  This ensures that any data that
-\fBovn\-sbctl\fR reads and reports is up-to-date.  With
-\fB\-\-no\-leader\-only\fR, \fBovn\-sbctl\fR will use any server in
-the cluster, which means that for read-only transactions it can report
-and act on stale data (transactions that modify the database are
-always serialized even with \fB\-\-no\-leader\-only\fR).  Refer to
-\fBUnderstanding Cluster Consistency\fR in \fBovsdb\fR(7) for more
-information.
-.
-.IP "\fB\-\-no\-syslog\fR"
-By default, \fBovn\-sbctl\fR logs its arguments and the details of any
-changes that it makes to the system log.  This option disables this
-logging.
-.IP
-This option is equivalent to \fB\-\-verbose=sbctl:syslog:warn\fR.
-.
-.IP "\fB\-\-oneline\fR"
-Modifies the output format so that the output for each command is printed
-on a single line.  New-line characters that would otherwise separate
-lines are printed as \fB\\n\fR, and any instances of \fB\\\fR that
-would otherwise appear in the output are doubled.
-Prints a blank line for each command that has no output.
-This option does not affect the formatting of output from the
-\fBlist\fR or \fBfind\fR commands; see \fBTable Formatting Options\fR
-below.
-.
-.IP "\fB\-\-dry\-run\fR"
-Prevents \fBovn\-sbctl\fR from actually modifying the database.
-.
-.IP "\fB\-t \fIsecs\fR"
-.IQ "\fB\-\-timeout=\fIsecs\fR"
-By default, or with a \fIsecs\fR of \fB0\fR, \fBovn\-sbctl\fR waits
-forever for a response from the database.  This option limits runtime
-to approximately \fIsecs\fR seconds.  If the timeout expires,
-\fBovn\-sbctl\fR will exit with a \fBSIGALRM\fR signal.  (A timeout
-would normally happen only if the database cannot be contacted, or if
-the system is overloaded.)
-.
-.IP "\fBOVN_SBCTL_OPTIONS\fR"
-User can set one or more options using \fBOVN_SBCTL_OPTIONS\fR environment
-variable. Under the Bourne shell this might be done like this:
-export \fBOVN_SBCTL_OPTIONS\fR"="--db=unix:sb1.ovsdb --no-leader-only".
-However user can still over-ride environment options by passing different
-options in cli. When the environment variable is no longer needed, unset it,
-e.g.: unset \fBOVN_SBCTL_OPTIONS\fR"
-.
-.so lib/vlog.man
-.so lib/common.man
-.
-.SS "Table Formatting Options"
-These options control the format of output from the \fBlist\fR and
-\fBfind\fR commands.
-.so lib/table.man
-.
-.SS "Public Key Infrastructure Options"
-.so lib/ssl-bootstrap.man
-.so lib/ssl.man
-.
-.SH COMMANDS
-The commands implemented by \fBovn\-sbctl\fR are described in the
-sections below.
-.SS "OVN_Southbound Commands"
-These commands work with an \fBOVN_Southbound\fR database as a whole.
-.
-.IP "\fBinit\fR"
-Initializes the database, if it is empty.  If the database has already
-been initialized, this command has no effect.
-.
-.IP "\fBshow\fR"
-Prints a brief overview of the database contents.
-.
-.SS "Chassis Commands"
-These commands manipulate \fBOVN_Southbound\fR chassis.
-.
-.IP "[\fB\-\-may\-exist\fR] \fBchassis\-add \fIchassis\fR \fIencap-type\fR \fIencap-ip\fR"
-Creates a new chassis named \fIchassis\fR.  \fIencap-type\fR is a
-comma-separated list of tunnel types.  The chassis will have
-one encap entry for each specified tunnel type with \fIencap-ip\fR
-as the destination IP for each.
-.IP
-Without \fB\-\-may\-exist\fR, attempting to create a chassis that
-exists is an error.  With \fB\-\-may\-exist\fR, this command does
-nothing if \fIchassis\fR already exists.
-.
-.IP "[\fB\-\-if\-exists\fR] \fBchassis\-del \fIchassis\fR"
-Deletes \fIchassis\fR and its \fIencaps\fR and \fIgateway_ports\fR.
-.IP
-Without \fB\-\-if\-exists\fR, attempting to delete a chassis that does
-not exist is an error.  With \fB\-\-if\-exists\fR, attempting to
-delete a chassis that does not exist has no effect.
-.
-.SS "Port binding Commands"
-.
-These commands manipulate \fBOVN_Southbound\fR port bindings.
-.
-.IP "[\fB\-\-may\-exist\fR] \fBlsp\-bind \fIlogical-port\fR \fIchassis\fR"
-Binds the logical port named \fIlogical-port\fR to \fIchassis\fR.
-.IP
-Without \fB\-\-may\-exist\fR, attempting to bind a logical port that
-has already been bound is an error.  With \fB\-\-may\-exist\fR, this
-command does nothing if \fIlogical-port\fR has already been bound to
-a chassis.
-.
-.IP "[\fB\-\-if\-exists\fR] \fBlsp\-unbind\fR \fIlogical-port\fR"
-Resets the binding of \fIlogical-port\fR to \fINULL\fR.
-.IP
-Without \fB\-\-if\-exists\fR, attempting to unbind a logical port
-that is not bound is an error.  With \fB\-\-if\-exists\fR, attempting
-to unbind logical port that is not bound has no effect.
-.
-.SS "Logical Flow Commands"
-.
-.IP "[\fB\-\-uuid\fR] [\fB\-\-ovs\fR[\fB=\fIremote\fR]] [\fB\-\-stats\fR] [\fB\-\-vflows\fR] \fBlflow\-list\fR [\fIlogical-datapath\fR] [\fIlflow\fR...]"
-List logical flows.  If \fIlogical-datapath\fR is specified, only list
-flows for that logical datapath.  The \fIlogical-datapath\fR may be
-given as a UUID or as a datapath name (reporting an error if multiple
-datapaths have the same name).
-.IP
-If at least one \fIlflow\fR is given, only matching logical flows, if
-any, are listed.  Each \fIlflow\fR may be specified as a UUID or the
-first few characters of a UUID, optionally prefixed by \fB0x\fR.
-(Because \fBovn\-controller\fR sets OpenFlow flow cookies to the first
-32 bits of the corresponding logical flow's UUID, this makes it easy
-to look up the logical flow that generated a particular OpenFlow
-flow.)
-.IP
-If \fB\-\-uuid\fR is specified, the output includes the first 32 bits
-of each logical flow's UUID.  This makes it easier to find the
-OpenFlow flows that correspond to a given logical flow.
-.IP
-If \fB\-\-ovs\fR is included, \fBovn\-sbctl\fR attempts to obtain and
-display the OpenFlow flows that correspond to each OVN logical flow.
-To do so, \fBovn\-sbctl\fR connects to \fIremote\fR (by default,
-\fBunix:@RUNDIR@/br-int.mgmt\fR) over OpenFlow and retrieves the
-flows.  If \fIremote\fR is specified, it must be an active OpenFlow
-connection method described in \fBovsdb\fR(7).  Please see the
-discussion of the similar \fB\-\-ovs\fR option in \fBovn-trace\fR(8)
-for more information about the OpenFlow flow output.
-.IP
-By default, OpenFlow flow output includes only match and actions.  Add
-\fB\-\-stats\fR to include all OpenFlow information, such as packet
-and byte counters, duration, and timeouts.
-.IP
-If \fB\-\-vflows\fR is included, other southbound database records directly
-used for generating OpenFlow flows are also listed. This includes:
-\fIport-bindings\fR, \fImac-bindings\fR, \fImulticast-groups\fR,
-\fIchassis\fR.  The \fB\-\-ovs\fR and \fB\-\-stats\fR can also be used in
-conjunction with \fB\-\-vflows\fR.
-.
-.IP "[\fB\-\-uuid\fR] \fBdump\-flows\fR [\fIlogical-datapath\fR]"
-Alias for \fBlflow\-list\fB.
-.
-.SS "Remote Connectivity Commands"
-.
-These commands manipulate the \fBconnections\fR column in the \fBSB_Global\fR
-table and rows in the \fBConnection\fR table.  When \fBovsdb\-server\fR
-is configured to use the \fBconnections\fR column for OVSDB connections,
-this allows the administrator to use \fBovn\-sbctl\fR to configure database
-connections.
-.
-.IP "\fBget\-connection\fR"
-Prints the configured connection(s).
-.
-.IP "\fBdel\-connection\fR"
-Deletes the configured connection(s).
-.
-.IP "\fBset\-connection\fR [\fIaccess\-specifier\fR] \fItarget\fR\&..."
-Sets the configured manager target or targets.  Each \fItarget\fR may
-may be an OVSDB active or passive connection method,
-e.g. \fBpssl:6640\fR, as described in \fBovsdb\fR(7),
-optionally preceded by an optional access-specifier (\fBread\-only\fR or
-\fBread\-write\fR).
-If provided, the effect of the access specifier persists for subsequent
-targets until changed by another access specifier.
-.
-.SS "SSL Configuration"
-When \fBovsdb\-server\fR is configured to connect using SSL, the
-following parameters are required:
-.TP
-\fIprivate-key\fR
-Specifies a PEM file containing the private key used for SSL connections.
-.TP
-\fIcertificate\fR
-Specifies a PEM file containing a certificate, signed by the
-certificate authority (CA) used by the connection peers, that
-certifies the private key, identifying a trustworthy peer.
-.TP
-\fIca-cert\fR
-Specifies a PEM file containing the CA certificate used to verify that
-the connection peers are trustworthy.
-.PP
-These SSL settings apply to all SSL connections made by the southbound
-database server.
-.
-.IP "\fBget\-ssl\fR"
-Prints the SSL configuration.
-.
-.IP "\fBdel\-ssl\fR"
-Deletes the current SSL configuration.
-.
-.IP "[\fB\-\-bootstrap\fR] \fBset\-ssl\fR \fIprivate-key\fR \fIcertificate\fR \fIca-cert\fR [\fIssl-protocol-list\fR [\fIssl-cipher-list\fR]]"
-Sets the SSL configuration.  The \fB\-\-bootstrap\fR option is described
-below.
-.
-.ST "CA Certificate Bootstrap"
-.PP
-Ordinarily, all of the files named in the SSL configuration must exist
-before SSL connectivity can be used.  However, if the \fIca-cert\fR file
-does not exist and the \fB\-\-bootstrap\fR
-option is given, then \fBovsdb\-server\fR will attempt to obtain the
-CA certificate from the target on its first SSL connection and
-save it to the named PEM file.  If it is successful, it will
-immediately drop the connection and reconnect, and from then on all
-SSL connections must be authenticated by a certificate signed by the
-CA certificate thus obtained.
-.PP
-\fBThis option exposes the SSL connection to a man-in-the-middle
-attack obtaining the initial CA certificate\fR, but it may be useful
-for bootstrapping.
-.PP
-This option is only useful if the SSL peer sends its CA certificate
-as part of the SSL certificate chain.  The SSL protocol does not
-require the controller to send the CA certificate.
-.
-.SS "Database Commands"
-.
-These commands query and modify the contents of \fBovsdb\fR tables.
-They are a slight abstraction of the \fBovsdb\fR interface and as such
-they operate at a lower level than other \fBovs\-sbctl\fR commands.
-.PP
-.ST "Identifying Tables, Records, and Columns"
-.PP
-Each of these commands has a \fItable\fR parameter to identify a table
-within the database.  Many of them also take a \fIrecord\fR parameter
-that identifies a particular record within a table.  The \fIrecord\fR
-parameter may be the UUID for a record, and many tables offer
-additional ways to identify records.  Some commands also take
-\fIcolumn\fR parameters that identify a particular field within the
-records in a table.
-.PP
-For a list of tables and their columns, see \fBovn\-sb\fR(5) or
-see the table listing from the \fB--help\fR option.
-.PP
-Record names must be specified in full and with correct
-capitalization, except that UUIDs may be abbreviated to their first 4
-(or more) hex digits, as long as that is unique within the table.
-Names of tables and columns are not case-sensitive, and \fB\-\fR and
-\fB_\fR are treated interchangeably.  Unique abbreviations of table
-and column names are acceptable, e.g. \fBaddr\fR or \fBa\fR is
-sufficient to identify the \fBAddress_Set\fR table.
-.
-.so lib/db-ctl-base.man
-.SH "EXIT STATUS"
-.IP "0"
-Successful program execution.
-.IP "1"
-Usage, syntax, or configuration file error.
-.SH "SEE ALSO"
-.
-.BR ovn\-sb (5).
diff --git a/utilities/ovn-sbctl.8.xml b/utilities/ovn-sbctl.8.xml
new file mode 100644
index 000000000000..4e6b21c47369
--- /dev/null
+++ b/utilities/ovn-sbctl.8.xml
@@ -0,0 +1,580 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manpage program="ovn-sbctl" section="8" title="ovn-sbctl">
+    <h1>Name</h1>
+    <p>ovn-sbctl -- Open Virtual Network southbound db management utility</p>
+
+    <h1>Synopsis</h1>
+    <p><code>ovn-sbctl</code> [<var>options</var>] <var>command</var> [<var>arg</var>...]</p>
+
+    <h1>Description</h1>
+
+    <p>
+      The <code>ovn-sbctl</code> program configures the
+      <code>OVN_Southbound</code> database by providing a high-level interface
+      to its configuration database.  See <code>ovn-sb</code>(5) for
+      comprehensive documentation of the database schema.
+    </p>
+
+    <p>
+      <code>ovn-sbctl</code> connects to an <code>ovsdb-server</code> process
+      that maintains an OVN_Southbound configuration database.  Using this
+      connection, it queries and possibly applies changes to the database,
+      depending on the supplied commands.
+    </p>
+
+    <p>
+      <code>ovn-sbctl</code> can perform any number of commands in a single
+      run, implemented as a single atomic transaction against the database.
+    </p>
+
+    <p>
+      The <code>ovn-sbctl</code> command line begins with global options (see
+      <code>OPTIONS</code> below for details).  The global options are followed
+      by one or more commands.  Each command should begin with <code>--</code>
+      by itself as a command-line argument, to separate it from the following
+      commands.  (The <code>--</code> before the first command is optional.)
+      The command itself starts with command-specific options, if any, followed
+      by the command name and any arguments.
+    </p>
+
+    <h1>Daemon Mode</h1>
+
+    <p>
+      When it is invoked in the most ordinary way, <code>ovn-sbctl</code>
+      connects to an OVSDB server that hosts the southbound database, retrieves
+      a partial copy of the database that is complete enough to do its work,
+      sends a transaction request to the server, and receives and processes the
+      server's reply.  In common interactive use, this is fine, but if the
+      database is large, the step in which <code>ovn-sbctl</code> retrieves a
+      partial copy of the database can take a long time, which yields poor
+      performance overall.
+    </p>
+
+    <p>
+      To improve performance in such a case, <code>ovn-sbctl</code> offers a
+      "daemon mode," in which the user first starts <code>ovn-sbctl</code>
+      running in the background and afterward uses the daemon to execute
+      operations.  Over several <code>ovn-sbctl</code> command invocations,
+      this performs better overall because it retrieves a copy of the database
+      only once at the beginning, not once per program run.
+    </p>
+
+    <p>
+      Use the <code>--detach</code> option to start an <code>ovn-sbctl</code>
+      daemon.  With this option, <code>ovn-sbctl</code> prints the name of a
+      control socket to stdout.  The client should save this name in
+      environment variable <env>OVN_SB_DAEMON</env>.  Under the Bourne shell
+      this might be done like this:
+    </p>
+
+    <pre fixed="yes">
+      export OVN_SB_DAEMON=$(ovn-sbctl --pidfile --detach)
+    </pre>
+
+    <p>
+      When <env>OVN_SB_DAEMON</env> is set, <code>ovn-sbctl</code>
+      automatically and transparently uses the daemon to execute its commands.
+    </p>
+
+    <p>
+      When the daemon is no longer needed, kill it and unset the environment
+      variable, e.g.:
+    </p>
+
+    <pre fixed="yes">
+      kill $(cat $OVN_RUNDIR/ovn-sbctl.pid)
+      unset OVN_SB_DAEMON
+    </pre>
+
+    <p>
+      When using daemon mode, an alternative to the <env>OVN_SB_DAEMON</env>
+      environment variable is to specify a path for the Unix socket. When
+      starting the ovn-sbctl daemon, specify the <code>-u</code> option with a
+      full path to the location of the socket file. Here is an exmple:
+    </p>
+
+    <pre fixed="yes">
+      ovn-sbctl --detach -u /tmp/mysock.ctl
+    </pre>
+
+    <p>
+      Then to connect to the running daemon, use the <code>-u</code> option
+      with the full path to the socket created when the daemon was started:
+    </p>
+
+    <pre fixed="yes">
+      ovn-sbctl -u /tmp/mysock.ctl show
+    </pre>
+
+    <h3>Daemon Commands</h3>
+
+    <p>
+      Daemon mode is internally implemented using the same mechanism used by
+      <code>ovn-appctl</code>.  One may also use <code>ovn-appctl</code>
+      directly with the following commands:
+    </p>
+
+    <dl>
+      <dt>
+        <code>run</code> [<var>options</var>] <var>command</var>
+        [<var>arg</var>...] [<code>--</code> [<var>options</var>]
+        <var>command</var> [<var>arg</var>...] ...]
+      </dt>
+      <dd>
+        Instructs the daemon process to run one or more <code>ovn-sbctl</code>
+        commands described above and reply with the results of running these
+        commands. Accepts the <code>--no-wait</code>, <code>--wait</code>,
+        <code>--timeout</code>, <code>--dry-run</code>, <code>--oneline</code>,
+        and the options described under <code>Table Formatting Options</code>
+        in addition to the the command-specific options.
+      </dd>
+
+      <dt><code>exit</code></dt>
+      <dd>Causes <code>ovn-sbctl</code> to gracefully terminate.</dd>
+    </dl>
+
+    <h1>Options</h1>
+
+    <p>
+      The options listed below affect the behavior of <code>ovn-sbctl</code> as
+      a whole.  Some individual commands also accept their own options, which
+      are given just before the command name.  If the first command on the
+      command line has options, then those options must be separated from the
+      global options by <code>--</code>.
+    </p>
+
+    <p>
+      <code>ovn-sbctl</code> also accepts options from the
+      <env>OVN_SBCTL_OPTIONS</env> environment variable, in the same format as
+      on the command line.  Options from the command line override those in the
+      environment.
+    </p>
+
+    <dl>
+      <dt><code>--db</code> <var>database</var></dt>
+      <dd>
+        The OVSDB database remote to contact.  If the <env>OVN_SB_DB</env>
+        environment variable is set, its value is used as the default.
+        Otherwise, the default is <code>unix:@RUNDIR@/ovnsb_db.sock</code>, but
+        this default is unlikely to be useful outside of single-machine OVN
+        test environments.
+      </dd>
+
+      <dt><code>--leader-only</code></dt>
+      <dt><code>--no-leader-only</code></dt>
+      <dd>
+        By default, or with <code>--leader-only</code>, when the database
+        server is a clustered database, <code>ovn-sbctl</code> will avoid
+        servers other than the cluster leader.  This ensures that any data that
+        <code>ovn-sbctl</code> reads and reports is up-to-date.  With
+        <code>--no-leader-only</code>, <code>ovn-sbctl</code> will use any
+        server in the cluster, which means that for read-only transactions it
+        can report and act on stale data (transactions that modify the database
+        are always serialized even with <code>--no-leader-only</code>).  Refer
+        to <code>Understanding Cluster Consistency</code> in
+        <code>ovsdb</code>(7) for more information.
+      </dd>
+
+      <dt><code>--shuffle-remotes</code></dt>
+      <dt><code>--no-shuffle-remotes</code></dt>
+      <dd>
+        By default, or with <code>--shuffle-remotes</code>, when there are
+        multiple remotes specified in the OVSDB connection string specified by
+        <code>--db</code> or the <env>OVN_SB_DB</env> environment variable, the
+        order of the remotes will be shuffled before the client tries to
+        connect.  The remotes will be shuffled only once to a new order before
+        the first connection attempt.  The following retries, if any, will
+        follow the same new order.  The default behavior is to make sure
+        clients of a clustered database can distribute evenly to all memembers
+        of the cluster.  With <code>--no-shuffle-remotes</code>,
+        <code>ovn-sbctl</code> will use the original order specified in the
+        connection string to connect.  This allows user to specify the
+        preferred order, which is particularly useful for testing.
+      </dd>
+
+      <dt><code>--no-syslog</code></dt>
+      <dd>
+        <p>
+          By default, <code>ovn-sbctl</code> logs its arguments and the details
+          of any changes that it makes to the system log.  This option disables
+          this logging.
+        </p>
+
+        <p>
+          This option is equivalent to
+          <code>--verbose=sbctl:syslog:warn</code>.
+        </p>
+      </dd>
+
+      <dt><code>--oneline</code></dt>
+      <dd>
+        Modifies the output format so that the output for each command is
+        printed on a single line.  New-line characters that would otherwise
+        separate lines are printed as \fB\\n\fR, and any instances of \fB\\\fR
+        that would otherwise appear in the output are doubled.  Prints a blank
+        line for each command that has no output.  This option does not affect
+        the formatting of output from the <code>list</code> or
+        <code>find</code> commands; see <code>Table Formatting Options</code>
+        below.
+      </dd>
+
+      <dt><code>--dry-run</code></dt>
+      <dd>
+        Prevents <code>ovn-sbctl</code> from actually modifying the database.
+      </dd>
+
+      <dt><code>-t <var>secs</var></code></dt>
+      <dt><code>--timeout=<var>secs</var></code></dt>
+      <dd>
+        By default, or with a <var>secs</var> of <code>0</code>,
+        <code>ovn-sbctl</code> waits forever for a response from the database.
+        This option limits runtime to approximately <var>secs</var> seconds.
+        If the timeout expires, <code>ovn-sbctl</code> will exit with a
+        <code>SIGALRM</code> signal.  (A timeout would normally happen only if
+        the database cannot be contacted, or if the system is overloaded.)
+      </dd>
+    </dl>
+
+    <h2>Daemon Options</h2>
+    <xi:include href="lib/daemon.xml" xmlns:xi="http://www.w3.org/2003/XInclude"/>
+
+    <h2>Logging options</h2>
+    <xi:include href="lib/vlog.xml" xmlns:xi="http://www.w3.org/2003/XInclude"/>
+
+    <h2>Table Formatting Options</h2>
+    These options control the format of output from the <code>list</code> and
+    <code>find</code> commands.
+    <xi:include href="lib/table.xml" xmlns:xi="http://www.w3.org/2003/XInclude"/>
+
+    <h2>PKI Options</h2>
+    <p>
+      PKI configuration is required to use SSL for the connection to the
+      database.
+    </p>
+    <xi:include href="lib/ssl.xml" xmlns:xi="http://www.w3.org/2003/XInclude"/>
+    <xi:include href="lib/ssl-bootstrap.xml" xmlns:xi="http://www.w3.org/2003/XInclude"/>
+
+    <h2>Other Options</h2>
+
+    <xi:include href="lib/common.xml" xmlns:xi="http://www.w3.org/2003/XInclude"/>
+
+    <h1>Commands</h1>
+
+    <p>
+      The following sections describe the commands that <code>ovn-sbctl</code>
+      supports.
+    </p>
+
+    <h2>OVN_Southbound Commands</h2>
+
+    <p>
+      These commands work with an <code>OVN_Southbound</code> database as a
+      whole.
+    </p>
+
+    <dl>
+      <dt><code>init</code></dt>
+      <dd>
+        Initializes the database, if it is empty.  If the database has already
+        been initialized, this command has no effect.
+      </dd>
+
+      <dt><code>show</code></dt>
+      <dd>
+        Prints a brief overview of the database contents.
+      </dd>
+    </dl>
+
+    <h2>Chassis Commands</h2>
+
+    <p>
+      These commands manipulate <code>OVN_Southbound</code> chassis.
+    </p>
+
+    <dl>
+      <dt>[<code>--may-exist</code>] <code>chassis-add <var>chassis</var> <var>encap-type</var> <var>encap-ip</var></code></dt>
+
+      <dd>
+        <p>
+          Creates a new chassis named <var>chassis</var>.
+          <var>encap-type</var> is a comma-separated list of tunnel types.  The
+          chassis will have one encap entry for each specified tunnel type with
+          <var>encap-ip</var> as the destination IP for each.
+        </p>
+
+        <p>
+          Without \fB\-\-may\-exist\fR, attempting to create a chassis that
+          exists is an error.  With \fB\-\-may\-exist\fR, this command does
+          nothing if <var>chassis</var> already exists.
+        </p>
+      </dd>
+
+      <dt>[<code>--if-exists</code>] <var>chassis-del <var>chassis</var></var></dt>
+      <dd>
+        <p>
+          Deletes <var>chassis</var> and its <var>encaps</var> and
+          <var>gateway_ports</var>.
+        </p>
+
+        <p>
+          Without <code>--if-exists</code>, attempting to delete a chassis that
+          does not exist is an error.  With <code>--if-exists</code> attempting
+          to delete a chassis that does not exist has no effect.
+        </p>
+      </dd>
+    </dl>
+
+    <h2>Port Binding Commands</h2>
+
+    <p>
+      These commands manipulate <code>OVN_Southbound</code> port bindings.
+    </p>
+
+    <dl>
+      <dt>[<code>--may-exist</code>] <code>lsp-bind <var>logical-port</var> <var>chassis</var></code></dt>
+      <dd>
+        <p>
+          Binds the logical port named <var>logical-port</var> to
+          <var>chassis</var>.
+        </p>
+
+        <p>
+          Without <code>--may-exist</code>, attempting to bind a logical port
+          that has already been bound is an error.  With
+          <code>--may-exist</code>, this command does nothing if
+          <var>logical-port</var> has already been bound to a chassis.
+        </p>
+      </dd>
+
+      <dt>[<code>--if-exists</code>] <code>lsp-unbind <var>logical-port</var></code></dt>
+      <dd>
+        <p>
+          Removes the binding of <var>logical-port</var>.
+        </p>
+
+        <p>
+          Without <code>--if-exists</code>, attempting to unbind a logical port
+          that is not bound is an error.  With <code>--if-exists</code>,
+          attempting to unbind logical port that is not bound has no effect.
+        </p>
+      </dd>
+    </dl>
+
+    <h2>Logical Flow Commands</h2>
+
+    <dl>
+      <dt>[<code>--uuid</code>] [<code>--ovs</code>[<code>=<var>remote</var>]</code>] [<code>--stats</code>] [<code>--vflows</code>] <code>lflow-list</code> [<var>logical-datapath</var>] [<var>lflow</var>...]</dt>
+
+      <dd>
+        <p>
+          List logical flows.  If <var>logical-datapath</var> is specified,
+          only list flows for that logical datapath.  The
+          <var>logical-datapath</var> may be given as a UUID or as a datapath
+          name (reporting an error if multiple datapaths have the same name).
+        </p>
+
+        <p>
+          If at least one <var>lflow</var> is given, only matching logical
+          flows, if any, are listed.  Each <var>lflow</var> may be specified as
+          a UUID or the first few characters of a UUID, optionally prefixed by
+          <code>0x</code>.  (Because <code>ovn-controller</code> sets OpenFlow
+          flow cookies to the first 32 bits of the corresponding logical flow's
+          UUID, this makes it easy to look up the logical flow that generated a
+          particular OpenFlow flow.)
+        </p>
+
+        <p>
+          If <code>--uuid</code> is specified, the output includes the first 32
+          bits of each logical flow's UUID.  This makes it easier to find the
+          OpenFlow flows that correspond to a given logical flow.
+        </p>
+
+        <p>
+          If <code>--ovs</code> is included, <code>ovn-sbctl</code> attempts to
+          obtain and display the OpenFlow flows that correspond to each OVN
+          logical flow.  To do so, <code>ovn-sbctl</code> connects to
+          <var>remote</var> (by default,
+          <code>unix:@RUNDIR@/br-int.mgmt</code>) over OpenFlow and retrieves
+          the flows.  If <var>remote</var> is specified, it must be an active
+          OpenFlow connection method described in <code>ovsdb</code>(7).
+          Please see the discussion of the similar <code>--ovs</code> option in
+          <code>ovn-trace</code>(8) for more information about the OpenFlow
+          flow output.
+        </p>
+
+        <p>
+          By default, OpenFlow flow output includes only match and actions.
+          Add <code>--stats</code> to include all OpenFlow information, such as
+          packet and byte counters, duration, and timeouts.
+        </p>
+
+        <p>
+          If <code>--vflows</code> is included, other southbound database
+          records directly used for generating OpenFlow flows are also
+          listed. This includes: <var>port-bindings</var>,
+          <var>mac-bindings</var>, <var>multicast-groups</var>,
+          <var>chassis</var>.  The <code>--ovs</code> and <code>--stats</code>
+          can also be used in conjunction with <code>--vflows</code>.
+        </p>
+      </dd>
+
+      <dt>[<code>--uuid</code>] <code>dump-flows</code> [<var>logical-datapath</var>]</dt>
+      <dd>Alias for <code>lflow-list</code>.</dd>
+    </dl>
+
+    <h2>Remote Connectivity Commands</h2>
+
+    <p>
+      These commands manipulate the <code>connections</code> column in the
+      <code>SB_Global</code> table and rows in the <code>Connection</code>
+      table.  When <code>ovsdb-server</code> is configured to use the
+      <code>connections</code> column for OVSDB connections, this allows the
+      administrator to use \fBovn\-sbctl\fR to configure database connections.
+    </p>
+
+    <dl>
+      <dt><code>get-connection</code></dt>
+      <dd>
+      Prints the configured connection(s).
+      </dd>
+
+      <dt><code>del-connection</code></dt>
+      <dd>
+      Deletes the configured connection(s).
+      </dd>
+
+      <dt>[<code>--inactivity-probe=</code><var>msecs</var>] <code>set-connection</code> <var>target</var>...</dt>
+      <dd>
+        Sets the configured manager target or targets.  Use
+        <code>--inactivity-probe=</code><var>msecs</var> to override the
+        default idle connection inactivity probe time.  Use 0 to disable
+        inactivity probes.
+      </dd>
+    </dl>
+
+    <h2>SSL Configuration Commands</h2>
+    <p>
+      When <code>ovsdb-server</code> is configured to connect using SSL, the
+      following parameters are required:
+    </p>
+
+    <dl>
+      <dt><var>private-key</var></dt>
+      <dd>
+        Specifies a PEM file containing the private key used for SSL
+        connections.
+      </dd>
+
+      <dt><var>certificate</var></dt>
+      <dd>
+        Specifies a PEM file containing a certificate, signed by the
+        certificate authority (CA) used by the connection peers, that
+        certifies the private key, identifying a trustworthy peer.
+      </dd>
+
+      <dt><var>ca-cert</var></dt>
+      <dd>
+        Specifies a PEM file containing the CA certificate used to verify that
+        the connection peers are trustworthy.
+      </dd>
+    </dl>
+
+    <p>
+      These SSL settings apply to all SSL connections made by the southbound
+      database server.
+    </p>
+
+    <dl>
+      <dt><code>get-ssl</code></dt>
+      <dd>
+      Prints the SSL configuration.
+      </dd>
+
+      <dt><code>del-ssl</code></dt>
+      <dd>
+      Deletes the current SSL configuration.
+      </dd>
+
+      <dt>[<code>--bootstrap</code>] <code>set-ssl</code>
+         <var>private-key</var> <var>certificate</var> <var>ca-cert</var>
+         [<var>ssl-protocol-list</var> [<var>ssl-cipher-list</var>]]</dt>
+      <dd>
+      Sets the SSL configuration.
+      </dd>
+    </dl>
+
+    <h2>Database Commands</h2>
+    <p>
+      These commands query and modify the contents of <code>ovsdb</code>
+      tables.  They are a slight abstraction of the <code>ovsdb</code>
+      interface and as such they operate at a lower level than other
+      <code>ovn-sbctl</code> commands.
+    </p>
+
+    <p><var>Identifying Tables, Records, and Columns</var></p>
+
+    <p>
+      Each of these commands has a <var>table</var> parameter to identify a
+      table within the database.  Many of them also take a <var>record</var>
+      parameter that identifies a particular record within a table.  The
+      <var>record</var> parameter may be the UUID for a record, which may be
+      abbreviated to its first 4 (or more) hex digits, as long as that is
+      unique.  Many tables offer additional ways to identify records.  Some
+      commands also take <var>column</var> parameters that identify a
+      particular field within the records in a table.
+    </p>
+
+    <p>
+      For a list of tables and their columns, see <code>ovn-sb</code>(5) or
+      see the table listing from the <code>--help</code> option.
+    </p>
+
+    <p>
+      Record names must be specified in full and with correct capitalization,
+      except that UUIDs may be abbreviated to their first 4 (or more) hex
+      digits, as long as that is unique within the table.  Names of tables and
+      columns are not case-sensitive, and <code>-</code> and <code>_</code> are
+      treated interchangeably.  Unique abbreviations of table and column names
+      are acceptable, e.g. <code>d</code> or <code>dhcp</code> is sufficient
+      to identify the <code>DHCP_Options</code> table.
+    </p>
+
+    <xi:include href="lib/db-ctl-base.xml" xmlns:xi="http://www.w3.org/2003/XInclude"/>
+
+    <h1>Environment</h1>
+
+    <dl>
+      <dt><env>OVN_SB_DAEMON</env></dt>
+      <dd>
+        If set, this should name the Unix domain socket for an
+        <code>ovn-sbctl</code> server process.  See <code>Daemon Mode</code>,
+        above, for more information.
+      </dd>
+
+      <dt><env>OVN_SBCTL_OPTIONS</env></dt>
+      <dd>
+        If set, a set of options for <code>ovn-sbctl</code> to apply
+        automatically, in the same form as on the command line.
+      </dd>
+
+      <dt><env>OVN_SB_DB</env></dt>
+      <dd>
+        If set, the default database to contact when the <code>--db</code>
+        option is not used.
+      </dd>
+    </dl>
+
+    <h1>Exit Status</h1>
+    <dl>
+      <dt>0</dt>
+      <dd>Successful program execution.</dd>
+
+      <dt>1</dt>
+      <dd>Usage, syntax, or network error.</dd>
+    </dl>
+
+    <h1>See Also</h1>
+    <code>ovn-sb</code>(5),
+    <code>ovn-appctl</code>(8).
+
+</manpage>
diff --git a/utilities/ovn-sbctl.c b/utilities/ovn-sbctl.c
index e3aa7a68e680..197504fe4015 100644
--- a/utilities/ovn-sbctl.c
+++ b/utilities/ovn-sbctl.c
@@ -31,8 +31,10 @@
 #include "command-line.h"
 #include "compiler.h"
 #include "db-ctl-base.h"
+#include "daemon.h"
 #include "dirs.h"
 #include "fatal-signal.h"
+#include "jsonrpc.h"
 #include "openvswitch/dynamic-string.h"
 #include "openvswitch/json.h"
 #include "openvswitch/ofp-actions.h"
@@ -43,276 +45,45 @@
 #include "openvswitch/vlog.h"
 #include "lib/ovn-sb-idl.h"
 #include "lib/ovn-util.h"
+#include "memory.h"
+#include "ovn-dbctl.h"
 #include "ovsdb-data.h"
 #include "ovsdb-idl.h"
 #include "openvswitch/poll-loop.h"
 #include "process.h"
+#include "simap.h"
 #include "sset.h"
 #include "stream-ssl.h"
 #include "stream.h"
 #include "table.h"
+#include "timer.h"
 #include "timeval.h"
+#include "unixctl.h"
 #include "util.h"
 #include "svec.h"
 
 VLOG_DEFINE_THIS_MODULE(sbctl);
 
-struct sbctl_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 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 sbctl_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 sbctl_exit(int status);
-
-/* --leader-only, --no-leader-only: Only accept the leader in a cluster. */
-static int leader_only = true;
-
-static void sbctl_cmd_init(void);
-OVS_NO_RETURN static void usage(void);
-static void parse_options(int argc, char *argv[], struct shash *local_options);
-static void run_prerequisites(struct ctl_command[], size_t n_commands,
-                              struct ovsdb_idl *);
-static bool do_sbctl(const char *args, struct ctl_command *, size_t n,
-                     struct ovsdb_idl *);
-
-int
-main(int argc, char *argv[])
+static void
+sbctl_add_base_prerequisites(struct ovsdb_idl *idl,
+                             enum nbctl_wait_type wait_type OVS_UNUSED)
 {
-    struct ovsdb_idl *idl;
-    struct ctl_command *commands;
-    struct shash local_options;
-    unsigned int seqno;
-    size_t n_commands;
-
-    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");
-
-    sbctl_cmd_init();
-
-    /* Check if options are set via env var. */
-    char **argv_ = ovs_cmdl_env_parse_all(&argc, argv,
-                                          getenv("OVN_SBCTL_OPTIONS"));
-
-    /* Parse command line. */
-    char *args = process_escape_args(argv_);
-    shash_init(&local_options);
-    parse_options(argc, argv_, &local_options);
-    char *error = ctl_parse_commands(argc - optind, argv_ + optind,
-                                     &local_options, &commands, &n_commands);
-    if (error) {
-        ctl_fatal("%s", error);
-    }
-    VLOG(ctl_might_write_to_db(commands, n_commands) ? VLL_INFO : VLL_DBG,
-         "Called as %s", args);
-
-    ctl_timeout_setup(timeout);
-
-    /* Initialize IDL. */
-    idl = the_idl = ovsdb_idl_create(db, &sbrec_idl_class, false, true);
-    ovsdb_idl_set_leader_only(idl, leader_only);
-    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_sbctl(args, commands, n_commands, idl)) {
-                break;
-            }
-        }
-
-        if (seqno == ovsdb_idl_get_seqno(idl)) {
-            ovsdb_idl_wait(idl);
-            poll_block();
-        }
-    }
-
-    for (int i = 0; i < argc; i++) {
-        free(argv_[i]);
-    }
-    free(argv_);
-    free(args);
-    exit(EXIT_SUCCESS);
+    ovsdb_idl_add_table(idl, &sbrec_table_sb_global);
 }
 
 static void
-parse_options(int argc, char *argv[], struct shash *local_options)
+sbctl_pre_execute(struct ovsdb_idl *idl, struct ovsdb_idl_txn *txn,
+                  enum nbctl_wait_type wait_type OVS_UNUSED)
 {
-    enum {
-        OPT_DB = UCHAR_MAX + 1,
-        OPT_ONELINE,
-        OPT_NO_SYSLOG,
-        OPT_DRY_RUN,
-        OPT_LOCAL,
-        OPT_COMMANDS,
-        OPT_OPTIONS,
-        OPT_BOOTSTRAP_CA_CERT,
-        VLOG_OPTION_ENUMS,
-        TABLE_OPTION_ENUMS,
-        SSL_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},
-        {"leader-only", no_argument, &leader_only, true},
-        {"no-leader-only", no_argument, &leader_only, false},
-        {"version", no_argument, NULL, 'V'},
-        VLOG_LONG_OPTIONS,
-        STREAM_SSL_LONG_OPTIONS,
-        {"bootstrap-ca-cert", required_argument, NULL, OPT_BOOTSTRAP_CA_CERT},
-        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);
-
-    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();
-
-        case OPT_COMMANDS:
-            ctl_print_commands();
-            /* fall through */
-
-        case OPT_OPTIONS:
-            ctl_print_options(global_long_options);
-            /* fall through */
-
-        case 'V':
-            ovn_print_version(0, 0);
-            printf("DB Schema %s\n", sbrec_get_db_version());
-            exit(EXIT_SUCCESS);
-
-        case 't':
-            if (!str_to_uint(optarg, 10, &timeout) || !timeout) {
-                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 OPT_BOOTSTRAP_CA_CERT:
-            stream_ssl_set_ca_cert_file(optarg, true);
-            break;
-
-        case '?':
-            exit(EXIT_FAILURE);
-
-        default:
-            abort();
-
-        case 0:
-            break;
-        }
-    }
-    free(short_options);
-
-    if (!db) {
-        db = default_sb_db();
-    }
-
-    for (i = n_global_long_options; options[i].name; i++) {
-        free(CONST_CAST(char *, options[i].name));
+    const struct sbrec_sb_global *sb = sbrec_sb_global_first(idl);
+    if (!sb) {
+        /* XXX add verification that table is empty */
+        sb = sbrec_sb_global_insert(txn);
     }
-    free(options);
 }
 
 static void
-usage(void)
+sbctl_usage(void)
 {
     printf("\
 %s: OVN southbound DB management utility\n\
@@ -372,8 +143,12 @@ Other options:\n\
     stream_usage("database", true, true, true);
     exit(EXIT_SUCCESS);
 }
-
 
+/* One should not use ctl_fatal() within commands because it will kill the
+ * daemon if we're in daemon mode.  Use ctl_error() instead and return
+ * gracefully.  */
+#define ctl_fatal dont_use_ctl_fatal_use_ctl_error_and_return
+
 /* ovs-sbctl specific context.  Inherits the 'struct ctl_context' as base. */
 struct sbctl_context {
     struct ctl_context base;
@@ -420,18 +195,20 @@ sbctl_context_invalidate_cache(struct ctl_context *ctx)
     shash_destroy_free_data(&sbctl_ctx->port_bindings);
 }
 
-static void
-sbctl_context_populate_cache(struct ctl_context *ctx)
+/* Casts 'base' into 'struct sbctl_context' and initializes it if needed. */
+static struct sbctl_context *
+sbctl_context_get(struct ctl_context *ctx)
 {
-    struct sbctl_context *sbctl_ctx = sbctl_context_cast(ctx);
+    struct sbctl_context *sbctl_ctx
+        = CONTAINER_OF(ctx, struct sbctl_context, base);
+    if (sbctl_ctx->cache_valid) {
+        return sbctl_ctx;
+    }
+
     const struct sbrec_chassis *chassis_rec;
     const struct sbrec_port_binding *port_binding_rec;
     struct sset chassis, port_bindings;
 
-    if (sbctl_ctx->cache_valid) {
-        /* Cache is already populated. */
-        return;
-    }
     sbctl_ctx->cache_valid = true;
     shash_init(&sbctl_ctx->chassis);
     shash_init(&sbctl_ctx->port_bindings);
@@ -468,46 +245,63 @@ sbctl_context_populate_cache(struct ctl_context *ctx)
                   bd);
     }
     sset_destroy(&port_bindings);
+
+    return sbctl_ctx;
+}
+
+static struct ctl_context *
+sbctl_ctx_create(void)
+{
+    struct sbctl_context *sbctx = xmalloc(sizeof *sbctx);
+    *sbctx = (struct sbctl_context) {
+        .cache_valid = false,
+    };
+    return &sbctx->base;
 }
 
 static void
-check_conflicts(struct sbctl_context *sbctl_ctx, const char *name,
-                char *msg)
+sbctl_ctx_destroy(struct ctl_context *ctx)
 {
+    sbctl_context_invalidate_cache(ctx);
+    free(ctx);
+}
+
+static bool
+check_conflicts(struct ctl_context *ctx, const char *name, char *msg)
+{
+    struct sbctl_context *sbctl_ctx = sbctl_context_get(ctx);
     if (shash_find(&sbctl_ctx->chassis, name)) {
-        ctl_fatal("%s because a chassis named %s already exists",
-                    msg, name);
+        ctl_error(&sbctl_ctx->base,
+                  "%s because a chassis named %s already exists",
+                  msg, name);
+        return false;
     }
     free(msg);
+
+    return true;
 }
 
 static struct sbctl_chassis *
-find_chassis(struct sbctl_context *sbctl_ctx, const char *name,
-             bool must_exist)
+find_chassis(struct ctl_context *ctx, const char *name, bool must_exist)
 {
-    struct sbctl_chassis *sbctl_ch;
-
-    ovs_assert(sbctl_ctx->cache_valid);
-
-    sbctl_ch = shash_find_data(&sbctl_ctx->chassis, name);
+    struct sbctl_context *sbctl_ctx = sbctl_context_get(ctx);
+    struct sbctl_chassis *sbctl_ch = shash_find_data(&sbctl_ctx->chassis,
+                                                     name);
     if (must_exist && !sbctl_ch) {
-        ctl_fatal("no chassis named %s", name);
+        ctl_error(ctx, "no chassis named %s", name);
     }
 
     return sbctl_ch;
 }
 
 static struct sbctl_port_binding *
-find_port_binding(struct sbctl_context *sbctl_ctx, const char *name,
-                  bool must_exist)
+find_port_binding(struct ctl_context *ctx, const char *name, bool must_exist)
 {
-    struct sbctl_port_binding *bd;
-
-    ovs_assert(sbctl_ctx->cache_valid);
-
-    bd = shash_find_data(&sbctl_ctx->port_bindings, name);
+    struct sbctl_context *sbctl_ctx = sbctl_context_get(ctx);
+    struct sbctl_port_binding *bd = shash_find_data(&sbctl_ctx->port_bindings,
+                                                    name);
     if (must_exist && !bd) {
-        ctl_fatal("no port named %s", name);
+        ctl_error(&sbctl_ctx->base, "no port named %s", name);
     }
 
     return bd;
@@ -588,7 +382,6 @@ sbctl_init(struct ctl_context *ctx OVS_UNUSED)
 static void
 cmd_chassis_add(struct ctl_context *ctx)
 {
-    struct sbctl_context *sbctl_ctx = sbctl_context_cast(ctx);
     bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
     const char *ch_name, *encap_types, *encap_ip;
 
@@ -596,17 +389,17 @@ cmd_chassis_add(struct ctl_context *ctx)
     encap_types = ctx->argv[2];
     encap_ip = ctx->argv[3];
 
-    sbctl_context_populate_cache(ctx);
     if (may_exist) {
-        struct sbctl_chassis *sbctl_ch;
-
-        sbctl_ch = find_chassis(sbctl_ctx, ch_name, false);
+        struct sbctl_chassis *sbctl_ch = find_chassis(ctx, ch_name, false);
         if (sbctl_ch) {
             return;
         }
     }
-    check_conflicts(sbctl_ctx, ch_name,
-                    xasprintf("cannot create a chassis named %s", ch_name));
+    if (!check_conflicts(ctx, ch_name,
+                         xasprintf("cannot create a chassis named %s",
+                                   ch_name))) {
+        return;
+    }
 
     struct sset encap_set;
     sset_from_delimited_string(&encap_set, encap_types, ",");
@@ -642,8 +435,7 @@ cmd_chassis_del(struct ctl_context *ctx)
     bool must_exist = !shash_find(&ctx->options, "--if-exists");
     struct sbctl_chassis *sbctl_ch;
 
-    sbctl_context_populate_cache(ctx);
-    sbctl_ch = find_chassis(sbctl_ctx, ctx->argv[1], must_exist);
+    sbctl_ch = find_chassis(ctx, ctx->argv[1], must_exist);
     if (sbctl_ch) {
         if (sbctl_ch->ch_cfg) {
             size_t i;
@@ -672,17 +464,21 @@ cmd_lsp_bind(struct ctl_context *ctx)
     lport_name = ctx->argv[1];
     ch_name = ctx->argv[2];
 
-    sbctl_context_populate_cache(ctx);
-    sbctl_bd = find_port_binding(sbctl_ctx, lport_name, true);
-    sbctl_ch = find_chassis(sbctl_ctx, ch_name, true);
+    sbctl_bd = find_port_binding(ctx, lport_name, true);
+    if (!sbctl_ctx) {
+        return;
+    }
+    sbctl_ch = find_chassis(ctx, ch_name, true);
+    if (!sbctl_ch) {
+        return;
+    }
 
     if (sbctl_bd->bd_cfg->chassis) {
-        if (may_exist && sbctl_bd->bd_cfg->chassis == sbctl_ch->ch_cfg) {
-            return;
-        } else {
-            ctl_fatal("lport (%s) has already been binded to chassis (%s)",
+        if (!may_exist || sbctl_bd->bd_cfg->chassis != sbctl_ch->ch_cfg) {
+            ctl_error(ctx, "lport (%s) has already been binded to chassis (%s)",
                       lport_name, sbctl_bd->bd_cfg->chassis->name);
         }
+        return;
     }
     sbrec_port_binding_set_chassis(sbctl_bd->bd_cfg, sbctl_ch->ch_cfg);
     sbrec_port_binding_set_up(sbctl_bd->bd_cfg, &up, 1);
@@ -692,14 +488,12 @@ cmd_lsp_bind(struct ctl_context *ctx)
 static void
 cmd_lsp_unbind(struct ctl_context *ctx)
 {
-    struct sbctl_context *sbctl_ctx = sbctl_context_cast(ctx);
     bool must_exist = !shash_find(&ctx->options, "--if-exists");
     struct sbctl_port_binding *sbctl_bd;
     char *lport_name;
 
     lport_name = ctx->argv[1];
-    sbctl_context_populate_cache(ctx);
-    sbctl_bd = find_port_binding(sbctl_ctx, lport_name, must_exist);
+    sbctl_bd = find_port_binding(ctx, lport_name, must_exist);
     if (sbctl_bd) {
         sbrec_port_binding_set_chassis(sbctl_bd->bd_cfg, NULL);
         sbrec_port_binding_set_up(sbctl_bd->bd_cfg, NULL, 0);
@@ -1123,7 +917,9 @@ cmd_lflow_list(struct ctl_context *ctx)
         char *error = ctl_get_row(ctx, &sbrec_table_datapath_binding,
                                   ctx->argv[1], false, &row);
         if (error) {
-            ctl_fatal("%s", error);
+            ctl_error(ctx, "%s", error);
+            free(error);
+            return;
         }
 
         datapath = (const struct sbrec_datapath_binding *)row;
@@ -1136,8 +932,9 @@ cmd_lflow_list(struct ctl_context *ctx)
     for (size_t i = 1; i < ctx->argc; i++) {
         char *s = parse_partial_uuid(ctx->argv[i]);
         if (!s) {
-            ctl_fatal("%s is not a UUID or the beginning of a UUID",
+            ctl_error(ctx, "%s is not a UUID or the beginning of a UUID",
                       ctx->argv[i]);
+            return;
         }
         ctx->argv[i] = s;
     }
@@ -1272,12 +1069,15 @@ sbctl_ip_mcast_flush(struct ctl_context *ctx)
         char *error = ctl_get_row(ctx, &sbrec_table_datapath_binding,
                                   ctx->argv[1], false, &row);
         if (error) {
-            ctl_fatal("%s", error);
+            ctl_error(ctx, "%s", error);
+            free(error);
+            return;
         }
 
         dp = (const struct sbrec_datapath_binding *)row;
         if (!dp) {
-            ctl_fatal("%s is not a valid datapath", ctx->argv[1]);
+            ctl_error(ctx, "%s is not a valid datapath", ctx->argv[1]);
+            return;
         }
 
         sbctl_ip_mcast_flush_switch(ctx, dp);
@@ -1564,248 +1364,6 @@ static const struct ctl_table_class tables[SBREC_N_TABLES] = {
     = {&sbrec_load_balancer_col_name, NULL, NULL},
 };
 
-
-static void
-sbctl_context_init_command(struct sbctl_context *sbctl_ctx,
-                           struct ctl_command *command)
-{
-    ctl_context_init_command(&sbctl_ctx->base, command);
-}
-
-static void
-sbctl_context_init(struct sbctl_context *sbctl_ctx,
-                   struct ctl_command *command, struct ovsdb_idl *idl,
-                   struct ovsdb_idl_txn *txn,
-                   struct ovsdb_symbol_table *symtab)
-{
-    ctl_context_init(&sbctl_ctx->base, command, idl, txn, symtab,
-                     sbctl_context_invalidate_cache);
-    sbctl_ctx->cache_valid = false;
-}
-
-static void
-sbctl_context_done_command(struct sbctl_context *sbctl_ctx,
-                           struct ctl_command *command)
-{
-    ctl_context_done_command(&sbctl_ctx->base, command);
-}
-
-static void
-sbctl_context_done(struct sbctl_context *sbctl_ctx,
-                   struct ctl_command *command)
-{
-    ctl_context_done(&sbctl_ctx->base, command);
-}
-
-static void
-run_prerequisites(struct ctl_command *commands, size_t n_commands,
-                  struct ovsdb_idl *idl)
-{
-    ovsdb_idl_add_table(idl, &sbrec_table_sb_global);
-
-    for (struct ctl_command *c = commands; c < &commands[n_commands]; c++) {
-        if (c->syntax->prerequisites) {
-            struct sbctl_context sbctl_ctx;
-
-            ds_init(&c->output);
-            c->table = NULL;
-
-            sbctl_context_init(&sbctl_ctx, c, idl, NULL, NULL);
-            (c->syntax->prerequisites)(&sbctl_ctx.base);
-            if (sbctl_ctx.base.error) {
-                ctl_fatal("%s", sbctl_ctx.base.error);
-            }
-            sbctl_context_done(&sbctl_ctx, c);
-
-            ovs_assert(!c->output.string);
-            ovs_assert(!c->table);
-        }
-    }
-}
-
-static bool
-do_sbctl(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 sbctl_context sbctl_ctx;
-    struct ctl_command *c;
-    struct shash_node *node;
-
-    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-sbctl: %s", args);
-
-    const struct sbrec_sb_global *sb = sbrec_sb_global_first(idl);
-    if (!sb) {
-        /* XXX add verification that table is empty */
-        sbrec_sb_global_insert(txn);
-    }
-
-    symtab = ovsdb_symbol_table_create();
-    for (c = commands; c < &commands[n_commands]; c++) {
-        ds_init(&c->output);
-        c->table = NULL;
-    }
-    sbctl_context_init(&sbctl_ctx, NULL, idl, txn, symtab);
-    for (c = commands; c < &commands[n_commands]; c++) {
-        sbctl_context_init_command(&sbctl_ctx, c);
-        if (c->syntax->run) {
-            (c->syntax->run)(&sbctl_ctx.base);
-        }
-        if (sbctl_ctx.base.error) {
-            ctl_fatal("%s", sbctl_ctx.base.error);
-        }
-        sbctl_context_done_command(&sbctl_ctx, c);
-
-        if (sbctl_ctx.base.try_again) {
-            sbctl_context_done(&sbctl_ctx, NULL);
-            goto try_again;
-        }
-    }
-    sbctl_context_done(&sbctl_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 {
-                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) {
-                sbctl_context_init(&sbctl_ctx, c, idl, txn, symtab);
-                (c->syntax->postprocess)(&sbctl_ctx.base);
-                if (sbctl_ctx.base.error) {
-                    ctl_fatal("%s", sbctl_ctx.base.error);
-                }
-                sbctl_context_done(&sbctl_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(). */
-        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", ovsdb_idl_txn_get_error(txn));
-
-    case TXN_NOT_LOCKED:
-        /* Should not happen--we never call ovsdb_idl_set_lock(). */
-        ctl_fatal("database not locked");
-
-    default:
-        OVS_NOT_REACHED();
-    }
-
-    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. */
-    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);
-    }
-    return false;
-}
-
-/* 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
-sbctl_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 sbctl_commands[] = {
     { "init", 0, 0, "", NULL, sbctl_init, NULL, "", RW },
 
@@ -1850,11 +1408,31 @@ static const struct ctl_command_syntax sbctl_commands[] = {
     {NULL, 0, 0, NULL, NULL, NULL, NULL, NULL, RO},
 };
 
-/* Registers sbctl and common db commands. */
-static void
-sbctl_cmd_init(void)
+int
+main(int argc, char *argv[])
 {
-    ctl_init(&sbrec_idl_class, sbrec_table_classes, tables,
-             cmd_show_tables, sbctl_exit);
-    ctl_register_commands(sbctl_commands);
+    struct ovn_dbctl_options dbctl_options = {
+        .db_version = sbrec_get_db_version(),
+        .default_db = default_sb_db(),
+        .allow_wait = false,
+
+        .options_env_var_name = "OVN_SBCTL_OPTIONS",
+        .daemon_env_var_name = "OVN_SB_DAEMON",
+
+        .idl_class = &sbrec_idl_class,
+        .tables = tables,
+        .cmd_show_table = cmd_show_tables,
+        .commands = sbctl_commands,
+
+        .usage = sbctl_usage,
+        .add_base_prerequisites = sbctl_add_base_prerequisites,
+        .pre_execute = sbctl_pre_execute,
+        .post_execute = NULL,
+
+        .ctx_create = sbctl_ctx_create,
+        .ctx_destroy = sbctl_ctx_destroy,
+    };
+
+    return ovn_dbctl_main(argc, argv, &dbctl_options);
 }
+
