diff mbox series

[ovs-dev,RFC,48/52] ovsdb-client: Add new "backup" command.

Message ID 20170919220125.32535-49-blp@ovn.org
State RFC
Headers show
Series clustering implementation | expand

Commit Message

Ben Pfaff Sept. 19, 2017, 10:01 p.m. UTC
Signed-off-by: Ben Pfaff <blp@ovn.org>
---
 NEWS                    |   1 +
 manpages.mk             | 299 ------------------------------------------------
 ovsdb/file.c            |  22 ++--
 ovsdb/file.h            |   2 +
 ovsdb/log.c             |  58 ++++++----
 ovsdb/log.h             |   4 +
 ovsdb/ovsdb-client.1.in |  18 +++
 ovsdb/ovsdb-client.c    | 134 ++++++++++++++++++++++
 ovsdb/ovsdb.7.xml       |   6 +
 tests/ovsdb-client.at   |  55 ++++++++-
 10 files changed, 265 insertions(+), 334 deletions(-)
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index 066f88215627..686c387d782a 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,7 @@  Post-v2.8.0
      * ovsdb-server now always hosts a built-in database named _Server.  See
        ovsdb-server(5) for more details.
      * ovsdb-client: New "get-schema-cksum" command.
+     * ovsdb-client: New "backup" command.
    - ovs-vsctl and other commands that display data in tables now support a
      --max-column-width option to limit column width.
    - OVN:
diff --git a/manpages.mk b/manpages.mk
index 7d6a507e0039..e69de29bb2d1 100644
--- a/manpages.mk
+++ b/manpages.mk
@@ -1,299 +0,0 @@ 
-# Generated automatically -- do not modify!    -*- buffer-read-only: t -*-
-
-ovn/utilities/ovn-detrace.1: \
-	ovn/utilities/ovn-detrace.1.in \
-	lib/common-syn.man \
-	lib/common.man
-ovn/utilities/ovn-detrace.1.in:
-lib/common-syn.man:
-lib/common.man:
-
-ovn/utilities/ovn-sbctl.8: \
-	ovn/utilities/ovn-sbctl.8.in \
-	lib/common.man \
-	lib/db-ctl-base.man \
-	lib/ssl-bootstrap.man \
-	lib/ssl-peer-ca-cert.man \
-	lib/ssl.man \
-	lib/table.man \
-	lib/vlog.man
-ovn/utilities/ovn-sbctl.8.in:
-lib/common.man:
-lib/db-ctl-base.man:
-lib/ssl-bootstrap.man:
-lib/ssl-peer-ca-cert.man:
-lib/ssl.man:
-lib/table.man:
-lib/vlog.man:
-
-ovsdb/ovsdb-client.1: \
-	ovsdb/ovsdb-client.1.in \
-	lib/common-syn.man \
-	lib/common.man \
-	lib/daemon-syn.man \
-	lib/daemon.man \
-	lib/ssl-bootstrap-syn.man \
-	lib/ssl-bootstrap.man \
-	lib/ssl-connect-syn.man \
-	lib/ssl-connect.man \
-	lib/ssl-syn.man \
-	lib/ssl.man \
-	lib/table.man \
-	lib/vlog-syn.man \
-	lib/vlog.man \
-	ovsdb/ovsdb-schemas.man
-ovsdb/ovsdb-client.1.in:
-lib/common-syn.man:
-lib/common.man:
-lib/daemon-syn.man:
-lib/daemon.man:
-lib/ssl-bootstrap-syn.man:
-lib/ssl-bootstrap.man:
-lib/ssl-connect-syn.man:
-lib/ssl-connect.man:
-lib/ssl-syn.man:
-lib/ssl.man:
-lib/table.man:
-lib/vlog-syn.man:
-lib/vlog.man:
-ovsdb/ovsdb-schemas.man:
-
-ovsdb/ovsdb-server.1: \
-	ovsdb/ovsdb-server.1.in \
-	lib/common-syn.man \
-	lib/common.man \
-	lib/coverage-unixctl.man \
-	lib/daemon-syn.man \
-	lib/daemon.man \
-	lib/memory-unixctl.man \
-	lib/service-syn.man \
-	lib/service.man \
-	lib/ssl-bootstrap-syn.man \
-	lib/ssl-bootstrap.man \
-	lib/ssl-connect-syn.man \
-	lib/ssl-connect.man \
-	lib/ssl-peer-ca-cert-syn.man \
-	lib/ssl-peer-ca-cert.man \
-	lib/ssl-syn.man \
-	lib/ssl.man \
-	lib/unixctl-syn.man \
-	lib/unixctl.man \
-	lib/vlog-syn.man \
-	lib/vlog-unixctl.man \
-	lib/vlog.man
-ovsdb/ovsdb-server.1.in:
-lib/common-syn.man:
-lib/common.man:
-lib/coverage-unixctl.man:
-lib/daemon-syn.man:
-lib/daemon.man:
-lib/memory-unixctl.man:
-lib/service-syn.man:
-lib/service.man:
-lib/ssl-bootstrap-syn.man:
-lib/ssl-bootstrap.man:
-lib/ssl-connect-syn.man:
-lib/ssl-connect.man:
-lib/ssl-peer-ca-cert-syn.man:
-lib/ssl-peer-ca-cert.man:
-lib/ssl-syn.man:
-lib/ssl.man:
-lib/unixctl-syn.man:
-lib/unixctl.man:
-lib/vlog-syn.man:
-lib/vlog-unixctl.man:
-lib/vlog.man:
-
-ovsdb/ovsdb-tool.1: \
-	ovsdb/ovsdb-tool.1.in \
-	lib/common-syn.man \
-	lib/common.man \
-	lib/vlog-syn.man \
-	lib/vlog.man \
-	ovsdb/ovsdb-schemas.man
-ovsdb/ovsdb-tool.1.in:
-lib/common-syn.man:
-lib/common.man:
-lib/vlog-syn.man:
-lib/vlog.man:
-ovsdb/ovsdb-schemas.man:
-
-utilities/bugtool/ovs-bugtool.8: \
-	utilities/bugtool/ovs-bugtool.8.in
-utilities/bugtool/ovs-bugtool.8.in:
-
-utilities/ovs-appctl.8: \
-	utilities/ovs-appctl.8.in \
-	lib/common.man
-utilities/ovs-appctl.8.in:
-lib/common.man:
-
-utilities/ovs-dpctl-top.8: \
-	utilities/ovs-dpctl-top.8.in
-utilities/ovs-dpctl-top.8.in:
-
-utilities/ovs-dpctl.8: \
-	utilities/ovs-dpctl.8.in \
-	lib/common.man \
-	lib/dpctl.man \
-	lib/vlog.man
-utilities/ovs-dpctl.8.in:
-lib/common.man:
-lib/dpctl.man:
-lib/vlog.man:
-
-utilities/ovs-l3ping.8: \
-	utilities/ovs-l3ping.8.in \
-	lib/common-syn.man \
-	lib/common.man
-utilities/ovs-l3ping.8.in:
-lib/common-syn.man:
-lib/common.man:
-
-utilities/ovs-ofctl.8: \
-	utilities/ovs-ofctl.8.in \
-	lib/colors.man \
-	lib/common.man \
-	lib/daemon.man \
-	lib/ofp-version.man \
-	lib/ssl.man \
-	lib/unixctl.man \
-	lib/vconn-active.man \
-	lib/vlog.man
-utilities/ovs-ofctl.8.in:
-lib/colors.man:
-lib/common.man:
-lib/daemon.man:
-lib/ofp-version.man:
-lib/ssl.man:
-lib/unixctl.man:
-lib/vconn-active.man:
-lib/vlog.man:
-
-utilities/ovs-pcap.1: \
-	utilities/ovs-pcap.1.in \
-	lib/common-syn.man \
-	lib/common.man
-utilities/ovs-pcap.1.in:
-lib/common-syn.man:
-lib/common.man:
-
-utilities/ovs-pki.8: \
-	utilities/ovs-pki.8.in
-utilities/ovs-pki.8.in:
-
-utilities/ovs-tcpdump.8: \
-	utilities/ovs-tcpdump.8.in \
-	lib/common.man
-utilities/ovs-tcpdump.8.in:
-lib/common.man:
-
-utilities/ovs-tcpundump.1: \
-	utilities/ovs-tcpundump.1.in \
-	lib/common-syn.man \
-	lib/common.man
-utilities/ovs-tcpundump.1.in:
-lib/common-syn.man:
-lib/common.man:
-
-utilities/ovs-testcontroller.8: \
-	utilities/ovs-testcontroller.8.in \
-	lib/common.man \
-	lib/daemon.man \
-	lib/ofp-version.man \
-	lib/ssl-peer-ca-cert.man \
-	lib/ssl.man \
-	lib/unixctl.man \
-	lib/vconn-active.man \
-	lib/vconn-passive.man \
-	lib/vlog.man
-utilities/ovs-testcontroller.8.in:
-lib/common.man:
-lib/daemon.man:
-lib/ofp-version.man:
-lib/ssl-peer-ca-cert.man:
-lib/ssl.man:
-lib/unixctl.man:
-lib/vconn-active.man:
-lib/vconn-passive.man:
-lib/vlog.man:
-
-utilities/ovs-vlan-bug-workaround.8: \
-	utilities/ovs-vlan-bug-workaround.8.in \
-	lib/common.man \
-	utilities/ovs-vlan-bugs.man
-utilities/ovs-vlan-bug-workaround.8.in:
-lib/common.man:
-utilities/ovs-vlan-bugs.man:
-
-utilities/ovs-vsctl.8: \
-	utilities/ovs-vsctl.8.in \
-	lib/common.man \
-	lib/db-ctl-base.man \
-	lib/ssl-bootstrap.man \
-	lib/ssl-peer-ca-cert.man \
-	lib/ssl.man \
-	lib/table.man \
-	lib/vconn-active.man \
-	lib/vconn-passive.man \
-	lib/vlog.man
-utilities/ovs-vsctl.8.in:
-lib/common.man:
-lib/db-ctl-base.man:
-lib/ssl-bootstrap.man:
-lib/ssl-peer-ca-cert.man:
-lib/ssl.man:
-lib/table.man:
-lib/vconn-active.man:
-lib/vconn-passive.man:
-lib/vlog.man:
-
-vswitchd/ovs-vswitchd.8: \
-	vswitchd/ovs-vswitchd.8.in \
-	lib/common.man \
-	lib/coverage-unixctl.man \
-	lib/daemon.man \
-	lib/dpctl.man \
-	lib/memory-unixctl.man \
-	lib/service.man \
-	lib/ssl-bootstrap.man \
-	lib/ssl.man \
-	lib/unixctl.man \
-	lib/vlog-unixctl.man \
-	lib/vlog.man \
-	ofproto/ofproto-dpif-unixctl.man \
-	ofproto/ofproto-tnl-unixctl.man \
-	ofproto/ofproto-unixctl.man
-vswitchd/ovs-vswitchd.8.in:
-lib/common.man:
-lib/coverage-unixctl.man:
-lib/daemon.man:
-lib/dpctl.man:
-lib/memory-unixctl.man:
-lib/service.man:
-lib/ssl-bootstrap.man:
-lib/ssl.man:
-lib/unixctl.man:
-lib/vlog-unixctl.man:
-lib/vlog.man:
-ofproto/ofproto-dpif-unixctl.man:
-ofproto/ofproto-tnl-unixctl.man:
-ofproto/ofproto-unixctl.man:
-
-vtep/vtep-ctl.8: \
-	vtep/vtep-ctl.8.in \
-	lib/common.man \
-	lib/db-ctl-base.man \
-	lib/ssl-bootstrap.man \
-	lib/ssl-peer-ca-cert.man \
-	lib/ssl.man \
-	lib/table.man \
-	lib/vlog.man
-vtep/vtep-ctl.8.in:
-lib/common.man:
-lib/db-ctl-base.man:
-lib/ssl-bootstrap.man:
-lib/ssl-peer-ca-cert.man:
-lib/ssl.man:
-lib/table.man:
-lib/vlog.man:
diff --git a/ovsdb/file.c b/ovsdb/file.c
index 54e5df15a2cd..d33bce83a1ea 100644
--- a/ovsdb/file.c
+++ b/ovsdb/file.c
@@ -572,6 +572,19 @@  ovsdb_file_txn_to_json(const struct ovsdb_txn *txn)
     return ftxn.json;
 }
 
+struct json *
+ovsdb_file_txn_annotate(struct json *json, const char *comment)
+{
+    if (!json) {
+        json = json_object_create();
+    }
+    if (comment) {
+        json_object_put_string(json, "_comment", comment);
+    }
+    json_object_put(json, "_date", json_integer_create(time_wall_msec()));
+    return json;
+}
+
 struct ovsdb_error *
 ovsdb_file_commit(struct ovsdb_file *file,
                   const struct ovsdb_txn *txn, bool durable)
@@ -832,14 +845,7 @@  ovsdb_file_txn_commit(struct json *json, const char *comment,
 {
     struct ovsdb_error *error;
 
-    if (!json) {
-        json = json_object_create();
-    }
-    if (comment) {
-        json_object_put_string(json, "_comment", comment);
-    }
-    json_object_put(json, "_date", json_integer_create(time_wall_msec()));
-
+    json = ovsdb_file_txn_annotate(json, comment);
     error = ovsdb_log_write(log, json);
     json_destroy(json);
     if (error) {
diff --git a/ovsdb/file.h b/ovsdb/file.h
index 30f211c431dc..bc9b32cf6c33 100644
--- a/ovsdb/file.h
+++ b/ovsdb/file.h
@@ -49,6 +49,8 @@  struct ovsdb_error *ovsdb_file_commit(struct ovsdb_file *,
                                       const struct ovsdb_txn *, bool durable);
 void ovsdb_file_destroy(struct ovsdb_file *);
 
+struct json *ovsdb_file_txn_annotate(struct json *, const char *comment);
+
 struct ovsdb_error *ovsdb_file_convert(const struct ovsdb_file *,
                                        const struct ovsdb_schema *)
     OVS_WARN_UNUSED_RESULT;
diff --git a/ovsdb/log.c b/ovsdb/log.c
index a223d30ac28b..adc14761cd8a 100644
--- a/ovsdb/log.c
+++ b/ovsdb/log.c
@@ -24,6 +24,7 @@ 
 #include <sys/stat.h>
 #include <unistd.h>
 
+#include "openvswitch/dynamic-string.h"
 #include "openvswitch/json.h"
 #include "lockfile.h"
 #include "ovsdb.h"
@@ -352,16 +353,30 @@  ovsdb_log_unread(struct ovsdb_log *file)
     file->offset = file->prev_offset;
 }
 
+void
+ovsdb_log_compose_record(const struct json *json,
+                         const char *magic, struct ds *header, struct ds *data)
+{
+    ovs_assert(json->type == JSON_OBJECT || json->type == JSON_ARRAY);
+    ovs_assert(!header->length);
+    ovs_assert(!data->length);
+
+    /* Compose content.  Add a new-line (replacing the null terminator) to make
+     * the file easier to read, even though it has no semantic value.  */
+    json_to_ds(json, 0, data);
+    ds_put_char(data, '\n');
+
+    /* Compose header. */
+    uint8_t sha1[SHA1_DIGEST_SIZE];
+    sha1_bytes(data->string, data->length, sha1);
+    ds_put_format(header, "%s %"PRIuSIZE" "SHA1_FMT"\n",
+                  magic, data->length, SHA1_ARGS(sha1));
+}
+
 struct ovsdb_error *
 ovsdb_log_write(struct ovsdb_log *file, const struct json *json)
 {
-    uint8_t sha1[SHA1_DIGEST_SIZE];
     struct ovsdb_error *error;
-    char *json_string;
-    char header[128];
-    size_t length;
-
-    json_string = NULL;
 
     if (file->mode == OVSDB_LOG_READ || file->write_error) {
         file->mode = OVSDB_LOG_WRITE;
@@ -383,38 +398,31 @@  ovsdb_log_write(struct ovsdb_log *file, const struct json *json)
         goto error;
     }
 
-    /* Compose content.  Add a new-line (replacing the null terminator) to make
-     * the file easier to read, even though it has no semantic value.  */
-    json_string = json_to_string(json, 0);
-    length = strlen(json_string) + 1;
-    json_string[length - 1] = '\n';
-
-    /* Compose header. */
-    sha1_bytes(json_string, length, sha1);
-    snprintf(header, sizeof header, "%s %"PRIuSIZE" "SHA1_FMT"\n",
-             file->magic, length, SHA1_ARGS(sha1));
+    struct ds header = DS_EMPTY_INITIALIZER;
+    struct ds data = DS_EMPTY_INITIALIZER;
+    ovsdb_log_compose_record(json, file->magic, &header, &data);
+    size_t total_length = header.length + data.length;
 
     /* Write. */
-    if (fwrite(header, strlen(header), 1, file->stream) != 1
-        || fwrite(json_string, length, 1, file->stream) != 1
-        || fflush(file->stream))
-    {
-        error = ovsdb_io_error(errno, "%s: write failed", file->name);
-
+    bool ok = (fwrite(header.string, header.length, 1, file->stream) == 1
+               && fwrite(data.string, data.length, 1, file->stream) == 1
+               && fflush(file->stream) == 0);
+    ds_destroy(&header);
+    ds_destroy(&data);
+    if (!ok) {
         /* Remove any partially written data, ignoring errors since there is
          * nothing further we can do. */
         ignore(ftruncate(fileno(file->stream), file->offset));
 
+        error = ovsdb_io_error(errno, "%s: write failed", file->name);
         goto error;
     }
 
-    file->offset += strlen(header) + length;
-    free(json_string);
+    file->offset += total_length;
     return NULL;
 
 error:
     file->write_error = true;
-    free(json_string);
     return error;
 }
 
diff --git a/ovsdb/log.h b/ovsdb/log.h
index 439487ade12e..5be7eb91b165 100644
--- a/ovsdb/log.h
+++ b/ovsdb/log.h
@@ -19,6 +19,7 @@ 
 #include <sys/types.h>
 #include "compiler.h"
 
+struct ds;
 struct json;
 struct ovsdb_log;
 
@@ -42,6 +43,9 @@  struct ovsdb_error *ovsdb_log_read(struct ovsdb_log *, struct json **)
     OVS_WARN_UNUSED_RESULT;
 void ovsdb_log_unread(struct ovsdb_log *);
 
+void ovsdb_log_compose_record(const struct json *, const char *magic,
+                              struct ds *header, struct ds *data);
+
 struct ovsdb_error *ovsdb_log_write(struct ovsdb_log *, const struct json *)
     OVS_WARN_UNUSED_RESULT;
 struct ovsdb_error *ovsdb_log_commit(struct ovsdb_log *)
diff --git a/ovsdb/ovsdb-client.1.in b/ovsdb/ovsdb-client.1.in
index e8e1c69eedd3..26f007258c09 100644
--- a/ovsdb/ovsdb-client.1.in
+++ b/ovsdb/ovsdb-client.1.in
@@ -33,6 +33,9 @@  ovsdb\-client \- command-line interface to \fBovsdb-server\fR(1)
 \fBovsdb\-client \fR[\fIoptions\fR] \fBdump\fI \fR[\fIserver\fR] \fR[\fIdatabase\fR]\fR [\fItable\fR
 [\fIcolumn\fR...]]
 .br
+\fBovsdb\-client \fR[\fIoptions\fR]
+\fBbackup\fI \fR[\fIserver\fR] \fR[\fIdatabase\fR] > \fIsnapshot\fR
+.br
 \fBovsdb\-client \fR[\fIoptions\fR] \fBmonitor\fI \fR[\fIserver\fR] \fR[\fIdatabase\fR] \fItable\fR
 [\fIcolumn\fR[\fB,\fIcolumn\fR]...]...
 .br
@@ -185,6 +188,21 @@  and prints it on stdout as a series of tables. If \fItable\fR is
 specified, only that table is retrieved.  If at least one \fIcolumn\fR
 is specified, only those columns are retrieved.
 .
+.IP "\fBbackup\fI \fR[\fIserver\fR] \fR[\fIdatabase\fR] > \fIsnapshot\fR"
+Connects to \fIserver\fR, retrieves a snapshot of the schema and data
+in \fIdatabase\fR, and prints it on stdout in the format used for
+OVSDB standalone and active-backup database.  This is an appropriate
+way to back up a remote database.  The database snapshot that it
+outputs is suitable to be served up directly by \fBovsdb\-server\fR or
+used as the input to \fBovsdb\-client restore\fR.
+.IP
+Another way to back up a standalone or active-backup database is to
+copy its database file, e.g. with \fBcp\fR.  This is safe even if the
+database is in use.
+.IP
+The output does not include ephemeral columns, which by design do not
+survive across restarts of \fBovsdb\-server\fR.
+.
 .IP "\fBmonitor\fI \fR[\fIserver\fR] \fR[\fIdatabase\fR] \fItable\fR [\fIcolumn\fR[\fB,\fIcolumn\fR]...]..."
 .IQ "\fBmonitor\-cond\fI \fR[\fIserver\fR] \fR[\fIdatabase\fR] \fIconditions\fR \fItable\fR [\fIcolumn\fR[\fB,\fIcolumn\fR]...]..."
 Connects to \fIserver\fR and monitors the contents of rows that match conditions in
diff --git a/ovsdb/ovsdb-client.c b/ovsdb/ovsdb-client.c
index 7ef0716ea3af..bfffc08effb0 100644
--- a/ovsdb/ovsdb-client.c
+++ b/ovsdb/ovsdb-client.c
@@ -32,9 +32,11 @@ 
 #include "dirs.h"
 #include "openvswitch/dynamic-string.h"
 #include "fatal-signal.h"
+#include "file.h"
 #include "openvswitch/json.h"
 #include "jsonrpc.h"
 #include "lib/table.h"
+#include "log.h"
 #include "ovsdb.h"
 #include "ovsdb-data.h"
 #include "ovsdb-error.h"
@@ -300,6 +302,8 @@  usage(void)
            "    in DATBASE on SERVER.\n"
            "\n  dump [SERVER] [DATABASE]\n"
            "    dump contents of DATABASE on SERVER to stdout\n"
+           "\n  backup [SERVER] [DATABASE] > DB\n"
+           "    dump database contents in the form of a database file\n"
            "\n  lock [SERVER] LOCK\n"
            "    create or wait for LOCK in SERVER\n"
            "\n  steal [SERVER] LOCK\n"
@@ -1474,6 +1478,135 @@  do_dump(struct jsonrpc *rpc, const char *database,
 }
 
 static void
+print_and_free_log_record(struct json *record)
+{
+    struct ds header = DS_EMPTY_INITIALIZER;
+    struct ds data = DS_EMPTY_INITIALIZER;
+    ovsdb_log_compose_record(record, OVSDB_MAGIC, &header, &data);
+    fwrite(header.string, header.length, 1, stdout);
+    fwrite(data.string, data.length, 1, stdout);
+    ds_destroy(&data);
+    ds_destroy(&header);
+    json_destroy(record);
+}
+
+static void
+do_backup(struct jsonrpc *rpc, const char *database,
+          int argc OVS_UNUSED, char *argv[] OVS_UNUSED)
+{
+    if (isatty(STDOUT_FILENO)) {
+        ovs_fatal(0, "not writing backup to a terminal; "
+                  "please redirect stdout to a file");
+    }
+
+    /* Get schema. */
+    struct ovsdb_schema *schema = fetch_schema(rpc, database);
+
+    /* Construct transaction to retrieve all tables. */
+    struct json *txn = json_array_create_1(json_string_create(database));
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &schema->tables) {
+        const char *table_name = node->name;
+        const struct ovsdb_table_schema *table = node->data;
+
+        /* Get all the columns except _version and the ephemeral ones.
+         *
+         * We don't omit tables that only have ephemeral columns because of the
+         * possibility that other tables references rows in those tables; that
+         * is, even if all the columns are ephemeral, the rows themselves are
+         * not. */
+        struct json *columns = json_array_create_empty();
+        struct shash_node *node2;
+        SHASH_FOR_EACH (node2, &table->columns) {
+            const struct ovsdb_column *column = node2->data;
+
+            if (column->persistent) {
+                if (!columns) {
+                    columns = json_array_create_empty();
+                }
+                json_array_add(columns, json_string_create(column->name));
+            }
+        }
+
+        struct json *op = json_object_create();
+        json_object_put_string(op, "op", "select");
+        json_object_put_string(op, "table", table_name);
+        json_object_put(op, "where", json_array_create_empty());
+        json_object_put(op, "columns", columns);
+        json_array_add(txn, op);
+    }
+
+    /* Send request, get reply. */
+    struct jsonrpc_msg *rq = jsonrpc_create_request("transact", txn, NULL);
+    struct jsonrpc_msg *reply;
+    check_txn(jsonrpc_transact_block(rpc, rq, &reply), &reply);
+
+    /* Print schema record. */
+    print_and_free_log_record(ovsdb_schema_to_json(schema));
+
+    /* Print database transaction record. */
+    if (reply->result->type != JSON_ARRAY
+        || reply->result->u.array.n != shash_count(&schema->tables)) {
+        ovs_fatal(0, "reply is not array of %"PRIuSIZE" elements: %s",
+                  shash_count(&schema->tables),
+                  json_to_string(reply->result, 0));
+    }
+    struct json *output_txn = json_object_create();
+
+    size_t i = 0;
+    SHASH_FOR_EACH (node, &schema->tables) {
+        const char *table_name = node->name;
+        const struct ovsdb_table_schema *table = node->data;
+        const struct json *op_result = reply->result->u.array.elems[i++];
+        struct json *rows;
+
+        if (op_result->type != JSON_OBJECT
+            || !(rows = shash_find_data(json_object(op_result), "rows"))
+            || rows->type != JSON_ARRAY) {
+            ovs_fatal(0, "%s table reply is not an object with a \"rows\" "
+                      "member array: %s",
+                      table->name, json_to_string(op_result, 0));
+        }
+
+        if (!rows->u.array.n) {
+            continue;
+        }
+
+        struct json *output_rows = json_object_create();
+        for (size_t j = 0; j < rows->u.array.n; j++) {
+            struct json *row = rows->u.array.elems[j];
+            if (row->type != JSON_OBJECT) {
+                ovs_fatal(0, "%s table reply row is not an object: %s",
+                          table_name, json_to_string(row, 0));
+            }
+
+            struct json *uuid_json = shash_find_and_delete(json_object(row),
+                                                           "_uuid");
+            if (!uuid_json) {
+                ovs_fatal(0, "%s table reply row lacks _uuid member: %s",
+                          table_name, json_to_string(row, 0));
+            }
+
+            const struct ovsdb_base_type uuid_base = OVSDB_BASE_UUID_INIT;
+            union ovsdb_atom atom;
+            check_ovsdb_error(ovsdb_atom_from_json(&atom, &uuid_base,
+                                                   uuid_json, NULL));
+
+            char uuid_s[UUID_LEN + 1];
+            snprintf(uuid_s, sizeof uuid_s, UUID_FMT, UUID_ARGS(&atom.uuid));
+            json_object_put(output_rows, uuid_s, json_clone(row));
+        }
+        json_object_put(output_txn, table_name, output_rows);
+    }
+    output_txn = ovsdb_file_txn_annotate(
+        output_txn, "produced by \"ovsdb-client backup\"");
+    print_and_free_log_record(output_txn);
+
+    ovsdb_schema_destroy(schema);
+    jsonrpc_msg_destroy(reply);
+}
+
+static void
 do_help(struct jsonrpc *rpc OVS_UNUSED, const char *database OVS_UNUSED,
         int argc OVS_UNUSED, char *argv[] OVS_UNUSED)
 {
@@ -1687,6 +1820,7 @@  static const struct ovsdb_client_command all_commands[] = {
     { "convert",            NEED_RPC,      1, 1,       do_convert },
     { "needs-conversion",   NEED_RPC,      1, 1,       do_needs_conversion },
     { "dump",               NEED_DATABASE, 0, INT_MAX, do_dump },
+    { "backup",             NEED_DATABASE, 0, 0,       do_backup },
     { "lock",               NEED_RPC,      1, 1,       do_lock_create },
     { "steal",              NEED_RPC,      1, 1,       do_lock_steal },
     { "unlock",             NEED_RPC,      1, 1,       do_lock_unlock },
diff --git a/ovsdb/ovsdb.7.xml b/ovsdb/ovsdb.7.xml
index efd7a622e2e0..5461f252a03a 100644
--- a/ovsdb/ovsdb.7.xml
+++ b/ovsdb/ovsdb.7.xml
@@ -404,6 +404,12 @@ 
   </p>
 
   <p>
+    Another way to make a backup is to use <code>ovsdb-client backup</code>,
+    which connects to a running database server and outputs an atomic snapshot
+    of its schema and content, in the same format used for on-disk databases.
+  </p>
+
+  <p>
     To restore from a backup, stop the database server or servers, overwrite
     the database file with the backup (e.g. with <code>cp</code>), and then
     restart the servers.
diff --git a/tests/ovsdb-client.at b/tests/ovsdb-client.at
index 3bce96b23fc8..18dcba8188ae 100644
--- a/tests/ovsdb-client.at
+++ b/tests/ovsdb-client.at
@@ -1,7 +1,7 @@ 
 AT_BANNER([OVSDB -- ovsdb-client commands])
 
 AT_SETUP([ovsdb-client get-schema-version])
-AT_KEYWORDS([ovsdb server positive])
+AT_KEYWORDS([ovsdb client positive])
 ordinal_schema > schema
 AT_CHECK([ovsdb-tool create db schema], [0], [ignore], [ignore])
 AT_CHECK([ovsdb-server --detach --no-chdir --pidfile --remote=punix:socket db], [0], [ignore], [ignore])
@@ -11,7 +11,7 @@  OVSDB_SERVER_SHUTDOWN
 AT_CLEANUP
 
 AT_SETUP([ovsdb-client get-schema-version - tcp socket])
-AT_KEYWORDS([ovsdb server positive tcp])
+AT_KEYWORDS([ovsdb client positive tcp])
 ordinal_schema > schema
 AT_CHECK([ovsdb-tool create db schema], [0], [ignore], [ignore])
 AT_CHECK([ovsdb-server --log-file --detach --no-chdir --pidfile --remote=ptcp:0:127.0.0.1 db], [0], [ignore], [ignore])
@@ -20,3 +20,54 @@  AT_CHECK([ovsdb-client get-schema-version tcp:127.0.0.1:$TCP_PORT ordinals], [0]
 ])
 OVSDB_SERVER_SHUTDOWN
 AT_CLEANUP])
+
+AT_SETUP([ovsdb-client backup])
+AT_KEYWORDS([ovsdb client positive])
+
+on_exit 'kill `cat *.pid`'
+
+dnl Create a database.
+ordinal_schema > schema
+touch .db.~lock~
+AT_CHECK([ovsdb-tool create db schema])
+
+dnl Put some data in the database.
+AT_CHECK(
+  [[for pair in 'zero 0' 'one 1' 'two 2' 'three 3' 'four 4' 'five 5'; do
+      set -- $pair
+      ovsdb-tool transact db '
+        ["ordinals",
+         {"op": "insert",
+          "table": "ordinals",
+          "row": {"name": "'$1'", "number": '$2'}},
+         {"op": "comment",
+          "comment": "add row for '"$pair"'"}]'
+    done | ${PERL} $srcdir/uuidfilt.pl]], [0],
+[[[{"uuid":["uuid","<0>"]},{}]
+[{"uuid":["uuid","<1>"]},{}]
+[{"uuid":["uuid","<2>"]},{}]
+[{"uuid":["uuid","<3>"]},{}]
+[{"uuid":["uuid","<4>"]},{}]
+[{"uuid":["uuid","<5>"]},{}]
+]], [ignore])
+
+dnl Start the database server.
+AT_CHECK([ovsdb-server -vfile -vvlog:off --detach --no-chdir --pidfile --log-file --remote=punix:db.sock db], [0])
+AT_CAPTURE_FILE([ovsdb-server.log])
+
+dnl Dump a copy of the data and a backup of it.
+AT_CHECK([ovsdb-client dump > dump1])
+AT_CHECK([ovsdb-client backup > backup])
+
+dnl Stop the database server, then re-start it based on the backup.
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+AT_CHECK([ovsdb-server -vfile -vvlog:off --detach --no-chdir --pidfile --log-file --remote=punix:db.sock backup], [0])
+
+dnl Dump a new copy of the data.
+AT_CHECK([ovsdb-client dump > dump2])
+sort dump2 > expout
+
+dnl Verify that the two dumps are the same.
+AT_CHECK([sort dump1], [0], [expout])
+
+AT_CLEANUP