{"id":815901,"url":"http://patchwork.ozlabs.org/api/patches/815901/?format=json","web_url":"http://patchwork.ozlabs.org/project/openvswitch/patch/20170919220125.32535-49-blp@ovn.org/","project":{"id":47,"url":"http://patchwork.ozlabs.org/api/projects/47/?format=json","name":"Open vSwitch","link_name":"openvswitch","list_id":"ovs-dev.openvswitch.org","list_email":"ovs-dev@openvswitch.org","web_url":"http://openvswitch.org/","scm_url":"git@github.com:openvswitch/ovs.git","webscm_url":"https://github.com/openvswitch/ovs","list_archive_url":"","list_archive_url_format":"","commit_url_format":""},"msgid":"<20170919220125.32535-49-blp@ovn.org>","list_archive_url":null,"date":"2017-09-19T22:01:21","name":"[ovs-dev,RFC,48/52] ovsdb-client: Add new \"backup\" command.","commit_ref":null,"pull_url":null,"state":"rfc","archived":false,"hash":"fdab565fc5aa83cd70f6db87fbd5d3683aa93e15","submitter":{"id":67603,"url":"http://patchwork.ozlabs.org/api/people/67603/?format=json","name":"Ben Pfaff","email":"blp@ovn.org"},"delegate":null,"mbox":"http://patchwork.ozlabs.org/project/openvswitch/patch/20170919220125.32535-49-blp@ovn.org/mbox/","series":[{"id":3975,"url":"http://patchwork.ozlabs.org/api/series/3975/?format=json","web_url":"http://patchwork.ozlabs.org/project/openvswitch/list/?series=3975","date":"2017-09-19T22:00:34","name":"clustering implementation","version":1,"mbox":"http://patchwork.ozlabs.org/series/3975/mbox/"}],"comments":"http://patchwork.ozlabs.org/api/patches/815901/comments/","check":"pending","checks":"http://patchwork.ozlabs.org/api/patches/815901/checks/","tags":{},"related":[],"headers":{"Return-Path":"<ovs-dev-bounces@openvswitch.org>","X-Original-To":["incoming@patchwork.ozlabs.org","dev@openvswitch.org"],"Delivered-To":["patchwork-incoming@bilbo.ozlabs.org","ovs-dev@mail.linuxfoundation.org"],"Authentication-Results":"ozlabs.org;\n\tspf=pass (mailfrom) smtp.mailfrom=openvswitch.org\n\t(client-ip=140.211.169.12; helo=mail.linuxfoundation.org;\n\tenvelope-from=ovs-dev-bounces@openvswitch.org;\n\treceiver=<UNKNOWN>)","Received":["from mail.linuxfoundation.org (mail.linuxfoundation.org\n\t[140.211.169.12])\n\t(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256\n\tbits)) (No client certificate requested)\n\tby ozlabs.org (Postfix) with ESMTPS id 3xxcsl24RSz9sBW\n\tfor <incoming@patchwork.ozlabs.org>;\n\tWed, 20 Sep 2017 08:26:19 +1000 (AEST)","from mail.linux-foundation.org (localhost [127.0.0.1])\n\tby mail.linuxfoundation.org (Postfix) with ESMTP id A46BDD53;\n\tTue, 19 Sep 2017 22:02:58 +0000 (UTC)","from smtp1.linuxfoundation.org (smtp1.linux-foundation.org\n\t[172.17.192.35])\n\tby mail.linuxfoundation.org (Postfix) with ESMTPS id 36304D49\n\tfor <dev@openvswitch.org>; Tue, 19 Sep 2017 22:02:57 +0000 (UTC)","from relay4-d.mail.gandi.net (relay4-d.mail.gandi.net\n\t[217.70.183.196])\n\tby smtp1.linuxfoundation.org (Postfix) with ESMTPS id 97F8E3D4\n\tfor <dev@openvswitch.org>; Tue, 19 Sep 2017 22:02:55 +0000 (UTC)","from sigabrt.benpfaff.org (unknown [208.91.2.3])\n\t(Authenticated sender: blp@ovn.org)\n\tby relay4-d.mail.gandi.net (Postfix) with ESMTPSA id 0DC18172094;\n\tWed, 20 Sep 2017 00:02:52 +0200 (CEST)"],"X-Greylist":"domain auto-whitelisted by SQLgrey-1.7.6","X-Originating-IP":"208.91.2.3","From":"Ben Pfaff <blp@ovn.org>","To":"dev@openvswitch.org","Date":"Tue, 19 Sep 2017 15:01:21 -0700","Message-Id":"<20170919220125.32535-49-blp@ovn.org>","X-Mailer":"git-send-email 2.10.2","In-Reply-To":"<20170919220125.32535-1-blp@ovn.org>","References":"<20170919220125.32535-1-blp@ovn.org>","X-Spam-Status":"No, score=-0.7 required=5.0 tests=RCVD_IN_DNSWL_LOW\n\tautolearn=disabled version=3.3.1","X-Spam-Checker-Version":"SpamAssassin 3.3.1 (2010-03-16) on\n\tsmtp1.linux-foundation.org","Cc":"Ben Pfaff <blp@ovn.org>","Subject":"[ovs-dev] [PATCH RFC 48/52] ovsdb-client: Add new \"backup\" command.","X-BeenThere":"ovs-dev@openvswitch.org","X-Mailman-Version":"2.1.12","Precedence":"list","List-Id":"<ovs-dev.openvswitch.org>","List-Unsubscribe":"<https://mail.openvswitch.org/mailman/options/ovs-dev>,\n\t<mailto:ovs-dev-request@openvswitch.org?subject=unsubscribe>","List-Archive":"<http://mail.openvswitch.org/pipermail/ovs-dev/>","List-Post":"<mailto:ovs-dev@openvswitch.org>","List-Help":"<mailto:ovs-dev-request@openvswitch.org?subject=help>","List-Subscribe":"<https://mail.openvswitch.org/mailman/listinfo/ovs-dev>,\n\t<mailto:ovs-dev-request@openvswitch.org?subject=subscribe>","MIME-Version":"1.0","Content-Type":"text/plain; charset=\"us-ascii\"","Content-Transfer-Encoding":"7bit","Sender":"ovs-dev-bounces@openvswitch.org","Errors-To":"ovs-dev-bounces@openvswitch.org"},"content":"Signed-off-by: Ben Pfaff <blp@ovn.org>\n---\n NEWS                    |   1 +\n manpages.mk             | 299 ------------------------------------------------\n ovsdb/file.c            |  22 ++--\n ovsdb/file.h            |   2 +\n ovsdb/log.c             |  58 ++++++----\n ovsdb/log.h             |   4 +\n ovsdb/ovsdb-client.1.in |  18 +++\n ovsdb/ovsdb-client.c    | 134 ++++++++++++++++++++++\n ovsdb/ovsdb.7.xml       |   6 +\n tests/ovsdb-client.at   |  55 ++++++++-\n 10 files changed, 265 insertions(+), 334 deletions(-)","diff":"diff --git a/NEWS b/NEWS\nindex 066f88215627..686c387d782a 100644\n--- a/NEWS\n+++ b/NEWS\n@@ -8,6 +8,7 @@ Post-v2.8.0\n      * ovsdb-server now always hosts a built-in database named _Server.  See\n        ovsdb-server(5) for more details.\n      * ovsdb-client: New \"get-schema-cksum\" command.\n+     * ovsdb-client: New \"backup\" command.\n    - ovs-vsctl and other commands that display data in tables now support a\n      --max-column-width option to limit column width.\n    - OVN:\ndiff --git a/manpages.mk b/manpages.mk\nindex 7d6a507e0039..e69de29bb2d1 100644\n--- a/manpages.mk\n+++ b/manpages.mk\n@@ -1,299 +0,0 @@\n-# Generated automatically -- do not modify!    -*- buffer-read-only: t -*-\n-\n-ovn/utilities/ovn-detrace.1: \\\n-\tovn/utilities/ovn-detrace.1.in \\\n-\tlib/common-syn.man \\\n-\tlib/common.man\n-ovn/utilities/ovn-detrace.1.in:\n-lib/common-syn.man:\n-lib/common.man:\n-\n-ovn/utilities/ovn-sbctl.8: \\\n-\tovn/utilities/ovn-sbctl.8.in \\\n-\tlib/common.man \\\n-\tlib/db-ctl-base.man \\\n-\tlib/ssl-bootstrap.man \\\n-\tlib/ssl-peer-ca-cert.man \\\n-\tlib/ssl.man \\\n-\tlib/table.man \\\n-\tlib/vlog.man\n-ovn/utilities/ovn-sbctl.8.in:\n-lib/common.man:\n-lib/db-ctl-base.man:\n-lib/ssl-bootstrap.man:\n-lib/ssl-peer-ca-cert.man:\n-lib/ssl.man:\n-lib/table.man:\n-lib/vlog.man:\n-\n-ovsdb/ovsdb-client.1: \\\n-\tovsdb/ovsdb-client.1.in \\\n-\tlib/common-syn.man \\\n-\tlib/common.man \\\n-\tlib/daemon-syn.man \\\n-\tlib/daemon.man \\\n-\tlib/ssl-bootstrap-syn.man \\\n-\tlib/ssl-bootstrap.man \\\n-\tlib/ssl-connect-syn.man \\\n-\tlib/ssl-connect.man \\\n-\tlib/ssl-syn.man \\\n-\tlib/ssl.man \\\n-\tlib/table.man \\\n-\tlib/vlog-syn.man \\\n-\tlib/vlog.man \\\n-\tovsdb/ovsdb-schemas.man\n-ovsdb/ovsdb-client.1.in:\n-lib/common-syn.man:\n-lib/common.man:\n-lib/daemon-syn.man:\n-lib/daemon.man:\n-lib/ssl-bootstrap-syn.man:\n-lib/ssl-bootstrap.man:\n-lib/ssl-connect-syn.man:\n-lib/ssl-connect.man:\n-lib/ssl-syn.man:\n-lib/ssl.man:\n-lib/table.man:\n-lib/vlog-syn.man:\n-lib/vlog.man:\n-ovsdb/ovsdb-schemas.man:\n-\n-ovsdb/ovsdb-server.1: \\\n-\tovsdb/ovsdb-server.1.in \\\n-\tlib/common-syn.man \\\n-\tlib/common.man \\\n-\tlib/coverage-unixctl.man \\\n-\tlib/daemon-syn.man \\\n-\tlib/daemon.man \\\n-\tlib/memory-unixctl.man \\\n-\tlib/service-syn.man \\\n-\tlib/service.man \\\n-\tlib/ssl-bootstrap-syn.man \\\n-\tlib/ssl-bootstrap.man \\\n-\tlib/ssl-connect-syn.man \\\n-\tlib/ssl-connect.man \\\n-\tlib/ssl-peer-ca-cert-syn.man \\\n-\tlib/ssl-peer-ca-cert.man \\\n-\tlib/ssl-syn.man \\\n-\tlib/ssl.man \\\n-\tlib/unixctl-syn.man \\\n-\tlib/unixctl.man \\\n-\tlib/vlog-syn.man \\\n-\tlib/vlog-unixctl.man \\\n-\tlib/vlog.man\n-ovsdb/ovsdb-server.1.in:\n-lib/common-syn.man:\n-lib/common.man:\n-lib/coverage-unixctl.man:\n-lib/daemon-syn.man:\n-lib/daemon.man:\n-lib/memory-unixctl.man:\n-lib/service-syn.man:\n-lib/service.man:\n-lib/ssl-bootstrap-syn.man:\n-lib/ssl-bootstrap.man:\n-lib/ssl-connect-syn.man:\n-lib/ssl-connect.man:\n-lib/ssl-peer-ca-cert-syn.man:\n-lib/ssl-peer-ca-cert.man:\n-lib/ssl-syn.man:\n-lib/ssl.man:\n-lib/unixctl-syn.man:\n-lib/unixctl.man:\n-lib/vlog-syn.man:\n-lib/vlog-unixctl.man:\n-lib/vlog.man:\n-\n-ovsdb/ovsdb-tool.1: \\\n-\tovsdb/ovsdb-tool.1.in \\\n-\tlib/common-syn.man \\\n-\tlib/common.man \\\n-\tlib/vlog-syn.man \\\n-\tlib/vlog.man \\\n-\tovsdb/ovsdb-schemas.man\n-ovsdb/ovsdb-tool.1.in:\n-lib/common-syn.man:\n-lib/common.man:\n-lib/vlog-syn.man:\n-lib/vlog.man:\n-ovsdb/ovsdb-schemas.man:\n-\n-utilities/bugtool/ovs-bugtool.8: \\\n-\tutilities/bugtool/ovs-bugtool.8.in\n-utilities/bugtool/ovs-bugtool.8.in:\n-\n-utilities/ovs-appctl.8: \\\n-\tutilities/ovs-appctl.8.in \\\n-\tlib/common.man\n-utilities/ovs-appctl.8.in:\n-lib/common.man:\n-\n-utilities/ovs-dpctl-top.8: \\\n-\tutilities/ovs-dpctl-top.8.in\n-utilities/ovs-dpctl-top.8.in:\n-\n-utilities/ovs-dpctl.8: \\\n-\tutilities/ovs-dpctl.8.in \\\n-\tlib/common.man \\\n-\tlib/dpctl.man \\\n-\tlib/vlog.man\n-utilities/ovs-dpctl.8.in:\n-lib/common.man:\n-lib/dpctl.man:\n-lib/vlog.man:\n-\n-utilities/ovs-l3ping.8: \\\n-\tutilities/ovs-l3ping.8.in \\\n-\tlib/common-syn.man \\\n-\tlib/common.man\n-utilities/ovs-l3ping.8.in:\n-lib/common-syn.man:\n-lib/common.man:\n-\n-utilities/ovs-ofctl.8: \\\n-\tutilities/ovs-ofctl.8.in \\\n-\tlib/colors.man \\\n-\tlib/common.man \\\n-\tlib/daemon.man \\\n-\tlib/ofp-version.man \\\n-\tlib/ssl.man \\\n-\tlib/unixctl.man \\\n-\tlib/vconn-active.man \\\n-\tlib/vlog.man\n-utilities/ovs-ofctl.8.in:\n-lib/colors.man:\n-lib/common.man:\n-lib/daemon.man:\n-lib/ofp-version.man:\n-lib/ssl.man:\n-lib/unixctl.man:\n-lib/vconn-active.man:\n-lib/vlog.man:\n-\n-utilities/ovs-pcap.1: \\\n-\tutilities/ovs-pcap.1.in \\\n-\tlib/common-syn.man \\\n-\tlib/common.man\n-utilities/ovs-pcap.1.in:\n-lib/common-syn.man:\n-lib/common.man:\n-\n-utilities/ovs-pki.8: \\\n-\tutilities/ovs-pki.8.in\n-utilities/ovs-pki.8.in:\n-\n-utilities/ovs-tcpdump.8: \\\n-\tutilities/ovs-tcpdump.8.in \\\n-\tlib/common.man\n-utilities/ovs-tcpdump.8.in:\n-lib/common.man:\n-\n-utilities/ovs-tcpundump.1: \\\n-\tutilities/ovs-tcpundump.1.in \\\n-\tlib/common-syn.man \\\n-\tlib/common.man\n-utilities/ovs-tcpundump.1.in:\n-lib/common-syn.man:\n-lib/common.man:\n-\n-utilities/ovs-testcontroller.8: \\\n-\tutilities/ovs-testcontroller.8.in \\\n-\tlib/common.man \\\n-\tlib/daemon.man \\\n-\tlib/ofp-version.man \\\n-\tlib/ssl-peer-ca-cert.man \\\n-\tlib/ssl.man \\\n-\tlib/unixctl.man \\\n-\tlib/vconn-active.man \\\n-\tlib/vconn-passive.man \\\n-\tlib/vlog.man\n-utilities/ovs-testcontroller.8.in:\n-lib/common.man:\n-lib/daemon.man:\n-lib/ofp-version.man:\n-lib/ssl-peer-ca-cert.man:\n-lib/ssl.man:\n-lib/unixctl.man:\n-lib/vconn-active.man:\n-lib/vconn-passive.man:\n-lib/vlog.man:\n-\n-utilities/ovs-vlan-bug-workaround.8: \\\n-\tutilities/ovs-vlan-bug-workaround.8.in \\\n-\tlib/common.man \\\n-\tutilities/ovs-vlan-bugs.man\n-utilities/ovs-vlan-bug-workaround.8.in:\n-lib/common.man:\n-utilities/ovs-vlan-bugs.man:\n-\n-utilities/ovs-vsctl.8: \\\n-\tutilities/ovs-vsctl.8.in \\\n-\tlib/common.man \\\n-\tlib/db-ctl-base.man \\\n-\tlib/ssl-bootstrap.man \\\n-\tlib/ssl-peer-ca-cert.man \\\n-\tlib/ssl.man \\\n-\tlib/table.man \\\n-\tlib/vconn-active.man \\\n-\tlib/vconn-passive.man \\\n-\tlib/vlog.man\n-utilities/ovs-vsctl.8.in:\n-lib/common.man:\n-lib/db-ctl-base.man:\n-lib/ssl-bootstrap.man:\n-lib/ssl-peer-ca-cert.man:\n-lib/ssl.man:\n-lib/table.man:\n-lib/vconn-active.man:\n-lib/vconn-passive.man:\n-lib/vlog.man:\n-\n-vswitchd/ovs-vswitchd.8: \\\n-\tvswitchd/ovs-vswitchd.8.in \\\n-\tlib/common.man \\\n-\tlib/coverage-unixctl.man \\\n-\tlib/daemon.man \\\n-\tlib/dpctl.man \\\n-\tlib/memory-unixctl.man \\\n-\tlib/service.man \\\n-\tlib/ssl-bootstrap.man \\\n-\tlib/ssl.man \\\n-\tlib/unixctl.man \\\n-\tlib/vlog-unixctl.man \\\n-\tlib/vlog.man \\\n-\tofproto/ofproto-dpif-unixctl.man \\\n-\tofproto/ofproto-tnl-unixctl.man \\\n-\tofproto/ofproto-unixctl.man\n-vswitchd/ovs-vswitchd.8.in:\n-lib/common.man:\n-lib/coverage-unixctl.man:\n-lib/daemon.man:\n-lib/dpctl.man:\n-lib/memory-unixctl.man:\n-lib/service.man:\n-lib/ssl-bootstrap.man:\n-lib/ssl.man:\n-lib/unixctl.man:\n-lib/vlog-unixctl.man:\n-lib/vlog.man:\n-ofproto/ofproto-dpif-unixctl.man:\n-ofproto/ofproto-tnl-unixctl.man:\n-ofproto/ofproto-unixctl.man:\n-\n-vtep/vtep-ctl.8: \\\n-\tvtep/vtep-ctl.8.in \\\n-\tlib/common.man \\\n-\tlib/db-ctl-base.man \\\n-\tlib/ssl-bootstrap.man \\\n-\tlib/ssl-peer-ca-cert.man \\\n-\tlib/ssl.man \\\n-\tlib/table.man \\\n-\tlib/vlog.man\n-vtep/vtep-ctl.8.in:\n-lib/common.man:\n-lib/db-ctl-base.man:\n-lib/ssl-bootstrap.man:\n-lib/ssl-peer-ca-cert.man:\n-lib/ssl.man:\n-lib/table.man:\n-lib/vlog.man:\ndiff --git a/ovsdb/file.c b/ovsdb/file.c\nindex 54e5df15a2cd..d33bce83a1ea 100644\n--- a/ovsdb/file.c\n+++ b/ovsdb/file.c\n@@ -572,6 +572,19 @@ ovsdb_file_txn_to_json(const struct ovsdb_txn *txn)\n     return ftxn.json;\n }\n \n+struct json *\n+ovsdb_file_txn_annotate(struct json *json, const char *comment)\n+{\n+    if (!json) {\n+        json = json_object_create();\n+    }\n+    if (comment) {\n+        json_object_put_string(json, \"_comment\", comment);\n+    }\n+    json_object_put(json, \"_date\", json_integer_create(time_wall_msec()));\n+    return json;\n+}\n+\n struct ovsdb_error *\n ovsdb_file_commit(struct ovsdb_file *file,\n                   const struct ovsdb_txn *txn, bool durable)\n@@ -832,14 +845,7 @@ ovsdb_file_txn_commit(struct json *json, const char *comment,\n {\n     struct ovsdb_error *error;\n \n-    if (!json) {\n-        json = json_object_create();\n-    }\n-    if (comment) {\n-        json_object_put_string(json, \"_comment\", comment);\n-    }\n-    json_object_put(json, \"_date\", json_integer_create(time_wall_msec()));\n-\n+    json = ovsdb_file_txn_annotate(json, comment);\n     error = ovsdb_log_write(log, json);\n     json_destroy(json);\n     if (error) {\ndiff --git a/ovsdb/file.h b/ovsdb/file.h\nindex 30f211c431dc..bc9b32cf6c33 100644\n--- a/ovsdb/file.h\n+++ b/ovsdb/file.h\n@@ -49,6 +49,8 @@ struct ovsdb_error *ovsdb_file_commit(struct ovsdb_file *,\n                                       const struct ovsdb_txn *, bool durable);\n void ovsdb_file_destroy(struct ovsdb_file *);\n \n+struct json *ovsdb_file_txn_annotate(struct json *, const char *comment);\n+\n struct ovsdb_error *ovsdb_file_convert(const struct ovsdb_file *,\n                                        const struct ovsdb_schema *)\n     OVS_WARN_UNUSED_RESULT;\ndiff --git a/ovsdb/log.c b/ovsdb/log.c\nindex a223d30ac28b..adc14761cd8a 100644\n--- a/ovsdb/log.c\n+++ b/ovsdb/log.c\n@@ -24,6 +24,7 @@\n #include <sys/stat.h>\n #include <unistd.h>\n \n+#include \"openvswitch/dynamic-string.h\"\n #include \"openvswitch/json.h\"\n #include \"lockfile.h\"\n #include \"ovsdb.h\"\n@@ -352,16 +353,30 @@ ovsdb_log_unread(struct ovsdb_log *file)\n     file->offset = file->prev_offset;\n }\n \n+void\n+ovsdb_log_compose_record(const struct json *json,\n+                         const char *magic, struct ds *header, struct ds *data)\n+{\n+    ovs_assert(json->type == JSON_OBJECT || json->type == JSON_ARRAY);\n+    ovs_assert(!header->length);\n+    ovs_assert(!data->length);\n+\n+    /* Compose content.  Add a new-line (replacing the null terminator) to make\n+     * the file easier to read, even though it has no semantic value.  */\n+    json_to_ds(json, 0, data);\n+    ds_put_char(data, '\\n');\n+\n+    /* Compose header. */\n+    uint8_t sha1[SHA1_DIGEST_SIZE];\n+    sha1_bytes(data->string, data->length, sha1);\n+    ds_put_format(header, \"%s %\"PRIuSIZE\" \"SHA1_FMT\"\\n\",\n+                  magic, data->length, SHA1_ARGS(sha1));\n+}\n+\n struct ovsdb_error *\n ovsdb_log_write(struct ovsdb_log *file, const struct json *json)\n {\n-    uint8_t sha1[SHA1_DIGEST_SIZE];\n     struct ovsdb_error *error;\n-    char *json_string;\n-    char header[128];\n-    size_t length;\n-\n-    json_string = NULL;\n \n     if (file->mode == OVSDB_LOG_READ || file->write_error) {\n         file->mode = OVSDB_LOG_WRITE;\n@@ -383,38 +398,31 @@ ovsdb_log_write(struct ovsdb_log *file, const struct json *json)\n         goto error;\n     }\n \n-    /* Compose content.  Add a new-line (replacing the null terminator) to make\n-     * the file easier to read, even though it has no semantic value.  */\n-    json_string = json_to_string(json, 0);\n-    length = strlen(json_string) + 1;\n-    json_string[length - 1] = '\\n';\n-\n-    /* Compose header. */\n-    sha1_bytes(json_string, length, sha1);\n-    snprintf(header, sizeof header, \"%s %\"PRIuSIZE\" \"SHA1_FMT\"\\n\",\n-             file->magic, length, SHA1_ARGS(sha1));\n+    struct ds header = DS_EMPTY_INITIALIZER;\n+    struct ds data = DS_EMPTY_INITIALIZER;\n+    ovsdb_log_compose_record(json, file->magic, &header, &data);\n+    size_t total_length = header.length + data.length;\n \n     /* Write. */\n-    if (fwrite(header, strlen(header), 1, file->stream) != 1\n-        || fwrite(json_string, length, 1, file->stream) != 1\n-        || fflush(file->stream))\n-    {\n-        error = ovsdb_io_error(errno, \"%s: write failed\", file->name);\n-\n+    bool ok = (fwrite(header.string, header.length, 1, file->stream) == 1\n+               && fwrite(data.string, data.length, 1, file->stream) == 1\n+               && fflush(file->stream) == 0);\n+    ds_destroy(&header);\n+    ds_destroy(&data);\n+    if (!ok) {\n         /* Remove any partially written data, ignoring errors since there is\n          * nothing further we can do. */\n         ignore(ftruncate(fileno(file->stream), file->offset));\n \n+        error = ovsdb_io_error(errno, \"%s: write failed\", file->name);\n         goto error;\n     }\n \n-    file->offset += strlen(header) + length;\n-    free(json_string);\n+    file->offset += total_length;\n     return NULL;\n \n error:\n     file->write_error = true;\n-    free(json_string);\n     return error;\n }\n \ndiff --git a/ovsdb/log.h b/ovsdb/log.h\nindex 439487ade12e..5be7eb91b165 100644\n--- a/ovsdb/log.h\n+++ b/ovsdb/log.h\n@@ -19,6 +19,7 @@\n #include <sys/types.h>\n #include \"compiler.h\"\n \n+struct ds;\n struct json;\n struct ovsdb_log;\n \n@@ -42,6 +43,9 @@ struct ovsdb_error *ovsdb_log_read(struct ovsdb_log *, struct json **)\n     OVS_WARN_UNUSED_RESULT;\n void ovsdb_log_unread(struct ovsdb_log *);\n \n+void ovsdb_log_compose_record(const struct json *, const char *magic,\n+                              struct ds *header, struct ds *data);\n+\n struct ovsdb_error *ovsdb_log_write(struct ovsdb_log *, const struct json *)\n     OVS_WARN_UNUSED_RESULT;\n struct ovsdb_error *ovsdb_log_commit(struct ovsdb_log *)\ndiff --git a/ovsdb/ovsdb-client.1.in b/ovsdb/ovsdb-client.1.in\nindex e8e1c69eedd3..26f007258c09 100644\n--- a/ovsdb/ovsdb-client.1.in\n+++ b/ovsdb/ovsdb-client.1.in\n@@ -33,6 +33,9 @@ ovsdb\\-client \\- command-line interface to \\fBovsdb-server\\fR(1)\n \\fBovsdb\\-client \\fR[\\fIoptions\\fR] \\fBdump\\fI \\fR[\\fIserver\\fR] \\fR[\\fIdatabase\\fR]\\fR [\\fItable\\fR\n [\\fIcolumn\\fR...]]\n .br\n+\\fBovsdb\\-client \\fR[\\fIoptions\\fR]\n+\\fBbackup\\fI \\fR[\\fIserver\\fR] \\fR[\\fIdatabase\\fR] > \\fIsnapshot\\fR\n+.br\n \\fBovsdb\\-client \\fR[\\fIoptions\\fR] \\fBmonitor\\fI \\fR[\\fIserver\\fR] \\fR[\\fIdatabase\\fR] \\fItable\\fR\n [\\fIcolumn\\fR[\\fB,\\fIcolumn\\fR]...]...\n .br\n@@ -185,6 +188,21 @@ and prints it on stdout as a series of tables. If \\fItable\\fR is\n specified, only that table is retrieved.  If at least one \\fIcolumn\\fR\n is specified, only those columns are retrieved.\n .\n+.IP \"\\fBbackup\\fI \\fR[\\fIserver\\fR] \\fR[\\fIdatabase\\fR] > \\fIsnapshot\\fR\"\n+Connects to \\fIserver\\fR, retrieves a snapshot of the schema and data\n+in \\fIdatabase\\fR, and prints it on stdout in the format used for\n+OVSDB standalone and active-backup database.  This is an appropriate\n+way to back up a remote database.  The database snapshot that it\n+outputs is suitable to be served up directly by \\fBovsdb\\-server\\fR or\n+used as the input to \\fBovsdb\\-client restore\\fR.\n+.IP\n+Another way to back up a standalone or active-backup database is to\n+copy its database file, e.g. with \\fBcp\\fR.  This is safe even if the\n+database is in use.\n+.IP\n+The output does not include ephemeral columns, which by design do not\n+survive across restarts of \\fBovsdb\\-server\\fR.\n+.\n .IP \"\\fBmonitor\\fI \\fR[\\fIserver\\fR] \\fR[\\fIdatabase\\fR] \\fItable\\fR [\\fIcolumn\\fR[\\fB,\\fIcolumn\\fR]...]...\"\n .IQ \"\\fBmonitor\\-cond\\fI \\fR[\\fIserver\\fR] \\fR[\\fIdatabase\\fR] \\fIconditions\\fR \\fItable\\fR [\\fIcolumn\\fR[\\fB,\\fIcolumn\\fR]...]...\"\n Connects to \\fIserver\\fR and monitors the contents of rows that match conditions in\ndiff --git a/ovsdb/ovsdb-client.c b/ovsdb/ovsdb-client.c\nindex 7ef0716ea3af..bfffc08effb0 100644\n--- a/ovsdb/ovsdb-client.c\n+++ b/ovsdb/ovsdb-client.c\n@@ -32,9 +32,11 @@\n #include \"dirs.h\"\n #include \"openvswitch/dynamic-string.h\"\n #include \"fatal-signal.h\"\n+#include \"file.h\"\n #include \"openvswitch/json.h\"\n #include \"jsonrpc.h\"\n #include \"lib/table.h\"\n+#include \"log.h\"\n #include \"ovsdb.h\"\n #include \"ovsdb-data.h\"\n #include \"ovsdb-error.h\"\n@@ -300,6 +302,8 @@ usage(void)\n            \"    in DATBASE on SERVER.\\n\"\n            \"\\n  dump [SERVER] [DATABASE]\\n\"\n            \"    dump contents of DATABASE on SERVER to stdout\\n\"\n+           \"\\n  backup [SERVER] [DATABASE] > DB\\n\"\n+           \"    dump database contents in the form of a database file\\n\"\n            \"\\n  lock [SERVER] LOCK\\n\"\n            \"    create or wait for LOCK in SERVER\\n\"\n            \"\\n  steal [SERVER] LOCK\\n\"\n@@ -1474,6 +1478,135 @@ do_dump(struct jsonrpc *rpc, const char *database,\n }\n \n static void\n+print_and_free_log_record(struct json *record)\n+{\n+    struct ds header = DS_EMPTY_INITIALIZER;\n+    struct ds data = DS_EMPTY_INITIALIZER;\n+    ovsdb_log_compose_record(record, OVSDB_MAGIC, &header, &data);\n+    fwrite(header.string, header.length, 1, stdout);\n+    fwrite(data.string, data.length, 1, stdout);\n+    ds_destroy(&data);\n+    ds_destroy(&header);\n+    json_destroy(record);\n+}\n+\n+static void\n+do_backup(struct jsonrpc *rpc, const char *database,\n+          int argc OVS_UNUSED, char *argv[] OVS_UNUSED)\n+{\n+    if (isatty(STDOUT_FILENO)) {\n+        ovs_fatal(0, \"not writing backup to a terminal; \"\n+                  \"please redirect stdout to a file\");\n+    }\n+\n+    /* Get schema. */\n+    struct ovsdb_schema *schema = fetch_schema(rpc, database);\n+\n+    /* Construct transaction to retrieve all tables. */\n+    struct json *txn = json_array_create_1(json_string_create(database));\n+    struct shash_node *node;\n+    SHASH_FOR_EACH (node, &schema->tables) {\n+        const char *table_name = node->name;\n+        const struct ovsdb_table_schema *table = node->data;\n+\n+        /* Get all the columns except _version and the ephemeral ones.\n+         *\n+         * We don't omit tables that only have ephemeral columns because of the\n+         * possibility that other tables references rows in those tables; that\n+         * is, even if all the columns are ephemeral, the rows themselves are\n+         * not. */\n+        struct json *columns = json_array_create_empty();\n+        struct shash_node *node2;\n+        SHASH_FOR_EACH (node2, &table->columns) {\n+            const struct ovsdb_column *column = node2->data;\n+\n+            if (column->persistent) {\n+                if (!columns) {\n+                    columns = json_array_create_empty();\n+                }\n+                json_array_add(columns, json_string_create(column->name));\n+            }\n+        }\n+\n+        struct json *op = json_object_create();\n+        json_object_put_string(op, \"op\", \"select\");\n+        json_object_put_string(op, \"table\", table_name);\n+        json_object_put(op, \"where\", json_array_create_empty());\n+        json_object_put(op, \"columns\", columns);\n+        json_array_add(txn, op);\n+    }\n+\n+    /* Send request, get reply. */\n+    struct jsonrpc_msg *rq = jsonrpc_create_request(\"transact\", txn, NULL);\n+    struct jsonrpc_msg *reply;\n+    check_txn(jsonrpc_transact_block(rpc, rq, &reply), &reply);\n+\n+    /* Print schema record. */\n+    print_and_free_log_record(ovsdb_schema_to_json(schema));\n+\n+    /* Print database transaction record. */\n+    if (reply->result->type != JSON_ARRAY\n+        || reply->result->u.array.n != shash_count(&schema->tables)) {\n+        ovs_fatal(0, \"reply is not array of %\"PRIuSIZE\" elements: %s\",\n+                  shash_count(&schema->tables),\n+                  json_to_string(reply->result, 0));\n+    }\n+    struct json *output_txn = json_object_create();\n+\n+    size_t i = 0;\n+    SHASH_FOR_EACH (node, &schema->tables) {\n+        const char *table_name = node->name;\n+        const struct ovsdb_table_schema *table = node->data;\n+        const struct json *op_result = reply->result->u.array.elems[i++];\n+        struct json *rows;\n+\n+        if (op_result->type != JSON_OBJECT\n+            || !(rows = shash_find_data(json_object(op_result), \"rows\"))\n+            || rows->type != JSON_ARRAY) {\n+            ovs_fatal(0, \"%s table reply is not an object with a \\\"rows\\\" \"\n+                      \"member array: %s\",\n+                      table->name, json_to_string(op_result, 0));\n+        }\n+\n+        if (!rows->u.array.n) {\n+            continue;\n+        }\n+\n+        struct json *output_rows = json_object_create();\n+        for (size_t j = 0; j < rows->u.array.n; j++) {\n+            struct json *row = rows->u.array.elems[j];\n+            if (row->type != JSON_OBJECT) {\n+                ovs_fatal(0, \"%s table reply row is not an object: %s\",\n+                          table_name, json_to_string(row, 0));\n+            }\n+\n+            struct json *uuid_json = shash_find_and_delete(json_object(row),\n+                                                           \"_uuid\");\n+            if (!uuid_json) {\n+                ovs_fatal(0, \"%s table reply row lacks _uuid member: %s\",\n+                          table_name, json_to_string(row, 0));\n+            }\n+\n+            const struct ovsdb_base_type uuid_base = OVSDB_BASE_UUID_INIT;\n+            union ovsdb_atom atom;\n+            check_ovsdb_error(ovsdb_atom_from_json(&atom, &uuid_base,\n+                                                   uuid_json, NULL));\n+\n+            char uuid_s[UUID_LEN + 1];\n+            snprintf(uuid_s, sizeof uuid_s, UUID_FMT, UUID_ARGS(&atom.uuid));\n+            json_object_put(output_rows, uuid_s, json_clone(row));\n+        }\n+        json_object_put(output_txn, table_name, output_rows);\n+    }\n+    output_txn = ovsdb_file_txn_annotate(\n+        output_txn, \"produced by \\\"ovsdb-client backup\\\"\");\n+    print_and_free_log_record(output_txn);\n+\n+    ovsdb_schema_destroy(schema);\n+    jsonrpc_msg_destroy(reply);\n+}\n+\n+static void\n do_help(struct jsonrpc *rpc OVS_UNUSED, const char *database OVS_UNUSED,\n         int argc OVS_UNUSED, char *argv[] OVS_UNUSED)\n {\n@@ -1687,6 +1820,7 @@ static const struct ovsdb_client_command all_commands[] = {\n     { \"convert\",            NEED_RPC,      1, 1,       do_convert },\n     { \"needs-conversion\",   NEED_RPC,      1, 1,       do_needs_conversion },\n     { \"dump\",               NEED_DATABASE, 0, INT_MAX, do_dump },\n+    { \"backup\",             NEED_DATABASE, 0, 0,       do_backup },\n     { \"lock\",               NEED_RPC,      1, 1,       do_lock_create },\n     { \"steal\",              NEED_RPC,      1, 1,       do_lock_steal },\n     { \"unlock\",             NEED_RPC,      1, 1,       do_lock_unlock },\ndiff --git a/ovsdb/ovsdb.7.xml b/ovsdb/ovsdb.7.xml\nindex efd7a622e2e0..5461f252a03a 100644\n--- a/ovsdb/ovsdb.7.xml\n+++ b/ovsdb/ovsdb.7.xml\n@@ -404,6 +404,12 @@\n   </p>\n \n   <p>\n+    Another way to make a backup is to use <code>ovsdb-client backup</code>,\n+    which connects to a running database server and outputs an atomic snapshot\n+    of its schema and content, in the same format used for on-disk databases.\n+  </p>\n+\n+  <p>\n     To restore from a backup, stop the database server or servers, overwrite\n     the database file with the backup (e.g. with <code>cp</code>), and then\n     restart the servers.\ndiff --git a/tests/ovsdb-client.at b/tests/ovsdb-client.at\nindex 3bce96b23fc8..18dcba8188ae 100644\n--- a/tests/ovsdb-client.at\n+++ b/tests/ovsdb-client.at\n@@ -1,7 +1,7 @@\n AT_BANNER([OVSDB -- ovsdb-client commands])\n \n AT_SETUP([ovsdb-client get-schema-version])\n-AT_KEYWORDS([ovsdb server positive])\n+AT_KEYWORDS([ovsdb client positive])\n ordinal_schema > schema\n AT_CHECK([ovsdb-tool create db schema], [0], [ignore], [ignore])\n AT_CHECK([ovsdb-server --detach --no-chdir --pidfile --remote=punix:socket db], [0], [ignore], [ignore])\n@@ -11,7 +11,7 @@ OVSDB_SERVER_SHUTDOWN\n AT_CLEANUP\n \n AT_SETUP([ovsdb-client get-schema-version - tcp socket])\n-AT_KEYWORDS([ovsdb server positive tcp])\n+AT_KEYWORDS([ovsdb client positive tcp])\n ordinal_schema > schema\n AT_CHECK([ovsdb-tool create db schema], [0], [ignore], [ignore])\n AT_CHECK([ovsdb-server --log-file --detach --no-chdir --pidfile --remote=ptcp:0:127.0.0.1 db], [0], [ignore], [ignore])\n@@ -20,3 +20,54 @@ AT_CHECK([ovsdb-client get-schema-version tcp:127.0.0.1:$TCP_PORT ordinals], [0]\n ])\n OVSDB_SERVER_SHUTDOWN\n AT_CLEANUP])\n+\n+AT_SETUP([ovsdb-client backup])\n+AT_KEYWORDS([ovsdb client positive])\n+\n+on_exit 'kill `cat *.pid`'\n+\n+dnl Create a database.\n+ordinal_schema > schema\n+touch .db.~lock~\n+AT_CHECK([ovsdb-tool create db schema])\n+\n+dnl Put some data in the database.\n+AT_CHECK(\n+  [[for pair in 'zero 0' 'one 1' 'two 2' 'three 3' 'four 4' 'five 5'; do\n+      set -- $pair\n+      ovsdb-tool transact db '\n+        [\"ordinals\",\n+         {\"op\": \"insert\",\n+          \"table\": \"ordinals\",\n+          \"row\": {\"name\": \"'$1'\", \"number\": '$2'}},\n+         {\"op\": \"comment\",\n+          \"comment\": \"add row for '\"$pair\"'\"}]'\n+    done | ${PERL} $srcdir/uuidfilt.pl]], [0],\n+[[[{\"uuid\":[\"uuid\",\"<0>\"]},{}]\n+[{\"uuid\":[\"uuid\",\"<1>\"]},{}]\n+[{\"uuid\":[\"uuid\",\"<2>\"]},{}]\n+[{\"uuid\":[\"uuid\",\"<3>\"]},{}]\n+[{\"uuid\":[\"uuid\",\"<4>\"]},{}]\n+[{\"uuid\":[\"uuid\",\"<5>\"]},{}]\n+]], [ignore])\n+\n+dnl Start the database server.\n+AT_CHECK([ovsdb-server -vfile -vvlog:off --detach --no-chdir --pidfile --log-file --remote=punix:db.sock db], [0])\n+AT_CAPTURE_FILE([ovsdb-server.log])\n+\n+dnl Dump a copy of the data and a backup of it.\n+AT_CHECK([ovsdb-client dump > dump1])\n+AT_CHECK([ovsdb-client backup > backup])\n+\n+dnl Stop the database server, then re-start it based on the backup.\n+OVS_APP_EXIT_AND_WAIT([ovsdb-server])\n+AT_CHECK([ovsdb-server -vfile -vvlog:off --detach --no-chdir --pidfile --log-file --remote=punix:db.sock backup], [0])\n+\n+dnl Dump a new copy of the data.\n+AT_CHECK([ovsdb-client dump > dump2])\n+sort dump2 > expout\n+\n+dnl Verify that the two dumps are the same.\n+AT_CHECK([sort dump1], [0], [expout])\n+\n+AT_CLEANUP\n","prefixes":["ovs-dev","RFC","48/52"]}