With this change, "ovsdb-client convert" can be used to convert a database
from one schema to another without taking the database offline.

Signed-off-by: Ben Pfaff <[email protected]>
---
 NEWS                    |   2 +
 lib/ovsdb-data.c        |  13 +++
 lib/ovsdb-data.h        |   6 ++
 ovsdb/file.c            | 135 +++++++++++++++++++++++++--
 ovsdb/file.h            |   4 +
 ovsdb/jsonrpc-server.c  | 120 +++++++++++-------------
 ovsdb/jsonrpc-server.h  |   3 +-
 ovsdb/log.c             | 144 ++++++++++++++++++++++++----
 ovsdb/log.h             |  14 ++-
 ovsdb/monitor.c         |  19 +++-
 ovsdb/monitor.h         |   2 +
 ovsdb/ovsdb-client.1.in |  39 ++++++++
 ovsdb/ovsdb-client.c    | 108 +++++++++++++++------
 ovsdb/ovsdb-server.1.in |  43 +++++++++
 ovsdb/ovsdb-server.c    |  56 ++++++-----
 ovsdb/ovsdb.7.xml       |   9 +-
 ovsdb/ovsdb.c           |  26 +++++-
 ovsdb/ovsdb.h           |   3 +-
 ovsdb/transaction.c     |  37 +++++---
 ovsdb/transaction.h     |   5 +
 ovsdb/trigger.c         | 132 ++++++++++++++++++++------
 ovsdb/trigger.h         |  17 ++--
 tests/ovsdb-log.at      |   2 +-
 tests/ovsdb-monitor.at  |   4 +-
 tests/ovsdb-server.at   | 243 +++++++++++++++++++++++++++++++++++++++++++++++-
 tests/test-ovsdb.c      |  16 ++--
 26 files changed, 984 insertions(+), 218 deletions(-)

diff --git a/NEWS b/NEWS
index 31f4b884bf55..f678e7caedb9 100644
--- a/NEWS
+++ b/NEWS
@@ -3,6 +3,8 @@ Post-v2.8.0
    - OVSDB:
      * New high-level documentation in ovsdb(7).
      * New file format documentation for developers in ovsdb(5).
+     * ovsdb-server now supports online schema conversion via
+       "ovsdb-client convert".
      * ovsdb-server now always hosts a built-in database named _Server.  See
        ovsdb-server(5) for more details.
    - ovs-vsctl and other commands that display data in tables now support a
diff --git a/lib/ovsdb-data.c b/lib/ovsdb-data.c
index 711d84d9badc..b4ea3dcac227 100644
--- a/lib/ovsdb-data.c
+++ b/lib/ovsdb-data.c
@@ -1650,6 +1650,19 @@ ovsdb_datum_from_smap(struct ovsdb_datum *datum, const 
struct smap *smap)
     ovsdb_datum_sort_unique(datum, OVSDB_TYPE_STRING, OVSDB_TYPE_STRING);
 }
 
+struct ovsdb_error * OVS_WARN_UNUSED_RESULT
+ovsdb_datum_convert(struct ovsdb_datum *dst,
+                    const struct ovsdb_type *dst_type,
+                    const struct ovsdb_datum *src,
+                    const struct ovsdb_type *src_type)
+{
+    struct json *json = ovsdb_datum_to_json(src, src_type);
+    struct ovsdb_error *error = ovsdb_datum_from_json(dst, dst_type, json,
+                                                      NULL);
+    json_destroy(json);
+    return error;
+}
+
 static uint32_t
 hash_atoms(enum ovsdb_atomic_type type, const union ovsdb_atom *atoms,
            unsigned int n, uint32_t basis)
diff --git a/lib/ovsdb-data.h b/lib/ovsdb-data.h
index 257e58e2a653..835d0a8a14e7 100644
--- a/lib/ovsdb-data.h
+++ b/lib/ovsdb-data.h
@@ -188,6 +188,12 @@ void ovsdb_datum_to_bare(const struct ovsdb_datum *,
 
 void ovsdb_datum_from_smap(struct ovsdb_datum *, const struct smap *);
 
+struct ovsdb_error *ovsdb_datum_convert(struct ovsdb_datum *dst,
+                                        const struct ovsdb_type *dst_type,
+                                        const struct ovsdb_datum *src,
+                                        const struct ovsdb_type *src_type)
+    OVS_WARN_UNUSED_RESULT;
+
 /* Comparison. */
 uint32_t ovsdb_datum_hash(const struct ovsdb_datum *,
                           const struct ovsdb_type *, uint32_t basis);
diff --git a/ovsdb/file.c b/ovsdb/file.c
index 6b2adad2759c..54e5df15a2cd 100644
--- a/ovsdb/file.c
+++ b/ovsdb/file.c
@@ -559,22 +559,31 @@ ovsdb_file_change_cb(const struct ovsdb_row *old,
     return true;
 }
 
-struct ovsdb_error *
-ovsdb_file_commit(struct ovsdb_file *file,
-                  const struct ovsdb_txn *txn, bool durable)
+/* Returns 'txn' transformed into the JSON format that is used in OVSDB files.
+ * (But the caller must use ovsdb_file_txn_annotate() to add the _comment the
+ * _date members.)  If 'txn' doesn't actually change anything, returns NULL */
+static struct json *
+ovsdb_file_txn_to_json(const struct ovsdb_txn *txn)
 {
     struct ovsdb_file_txn ftxn;
-    struct ovsdb_error *error;
 
     ovsdb_file_txn_init(&ftxn);
     ovsdb_txn_for_each_change(txn, ovsdb_file_change_cb, &ftxn);
-    if (!ftxn.json) {
+    return ftxn.json;
+}
+
+struct ovsdb_error *
+ovsdb_file_commit(struct ovsdb_file *file,
+                  const struct ovsdb_txn *txn, bool durable)
+{
+    struct json *txn_json = ovsdb_file_txn_to_json(txn);
+    if (!txn_json) {
         /* Nothing to commit. */
         return NULL;
     }
 
-    error = ovsdb_file_txn_commit(ftxn.json, ovsdb_txn_get_comment(txn),
-                                  durable, file->log);
+    struct ovsdb_error *error = ovsdb_file_txn_commit(
+        txn_json, ovsdb_txn_get_comment(txn), durable, file->log);
     if (error) {
         return error;
     }
@@ -846,3 +855,115 @@ ovsdb_file_txn_commit(struct json *json, const char 
*comment,
 
     return NULL;
 }
+
+static struct ovsdb_error * OVS_WARN_UNUSED_RESULT
+ovsdb_convert_table(struct ovsdb_txn *txn,
+                    const struct ovsdb_table *src_table,
+                    struct ovsdb_table *dst_table)
+{
+    const struct ovsdb_row *src_row;
+    HMAP_FOR_EACH (src_row, hmap_node, &src_table->rows) {
+        struct ovsdb_row *dst_row = ovsdb_row_create(dst_table);
+        *ovsdb_row_get_uuid_rw(dst_row) = *ovsdb_row_get_uuid(src_row);
+
+        struct shash_node *node;
+        SHASH_FOR_EACH (node, &src_table->schema->columns) {
+            const struct ovsdb_column *src_column = node->data;
+            if (src_column->index == OVSDB_COL_UUID ||
+                src_column->index == OVSDB_COL_VERSION) {
+                continue;
+            }
+
+            const struct ovsdb_column *dst_column
+                = shash_find_data(&dst_table->schema->columns,
+                                  src_column->name);
+            if (!dst_column) {
+                continue;
+            }
+
+            struct ovsdb_error *error = ovsdb_datum_convert(
+                &dst_row->fields[dst_column->index], &dst_column->type,
+                &src_row->fields[src_column->index], &src_column->type);
+            if (error) {
+                ovsdb_row_destroy(dst_row);
+                return error;
+            }
+        }
+
+        ovsdb_txn_row_insert(txn, dst_row);
+    }
+    return NULL;
+}
+
+struct ovsdb_error * OVS_WARN_UNUSED_RESULT
+ovsdb_file_convert(const struct ovsdb_file *file,
+                   const struct ovsdb_schema *new_schema)
+{
+    struct ovsdb *new_db = ovsdb_create(ovsdb_schema_clone(new_schema));
+    struct ovsdb_txn *txn = ovsdb_txn_create(new_db);
+    struct ovsdb_error *error = NULL;
+
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &file->db->tables) {
+        const char *table_name = node->name;
+        const struct ovsdb_table *src_table = node->data;
+        struct ovsdb_table *dst_table = shash_find_data(&new_db->tables,
+                                                        table_name);
+        if (!dst_table) {
+            continue;
+        }
+
+        error = ovsdb_convert_table(txn, src_table, dst_table);
+        if (error) {
+            goto error;
+        }
+    }
+
+    error = ovsdb_txn_start_commit(txn);
+    if (error) {
+        goto error;
+    }
+
+    struct ovsdb_log *new;
+    error = ovsdb_log_replace_start(file->log, &new);
+    if (error) {
+        goto error;
+    }
+
+    /* Write schema. */
+    struct json *schema_json = ovsdb_schema_to_json(new_schema);
+    error = ovsdb_log_write(new, schema_json);
+    json_destroy(schema_json);
+    if (error) {
+        goto error;
+    }
+
+    /* Write data. */
+    struct json *txn_json = ovsdb_file_txn_to_json(txn);
+    if (txn_json) {
+        error = ovsdb_log_write(new, txn_json);
+        json_destroy(txn_json);
+        if (error) {
+            goto error;
+        }
+    }
+
+    error = ovsdb_log_replace_commit(file->log, new);
+    if (error) {
+        goto error;
+    }
+
+    error = ovsdb_txn_finish_commit(txn, true);
+    ovs_assert(!error);         /* Can't happen. */
+
+    ovsdb_replace(file->db, new_db);
+
+    return NULL;
+
+error:
+    ovsdb_destroy(new_db);
+    if (txn) {
+        ovsdb_txn_abort(txn);
+    }
+    return error;
+}
diff --git a/ovsdb/file.h b/ovsdb/file.h
index d4b8c2015815..30f211c431dc 100644
--- a/ovsdb/file.h
+++ b/ovsdb/file.h
@@ -49,4 +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 ovsdb_error *ovsdb_file_convert(const struct ovsdb_file *,
+                                       const struct ovsdb_schema *)
+    OVS_WARN_UNUSED_RESULT;
+
 #endif /* ovsdb/file.h */
diff --git a/ovsdb/jsonrpc-server.c b/ovsdb/jsonrpc-server.c
index a667dbe67f5f..8b2a4648bb1d 100644
--- a/ovsdb/jsonrpc-server.c
+++ b/ovsdb/jsonrpc-server.c
@@ -82,7 +82,7 @@ static void ovsdb_jsonrpc_session_send(struct 
ovsdb_jsonrpc_session *,
 /* Triggers. */
 static void ovsdb_jsonrpc_trigger_create(struct ovsdb_jsonrpc_session *,
                                          struct ovsdb *,
-                                         struct json *id, struct json *params);
+                                         struct jsonrpc_msg *request);
 static struct ovsdb_jsonrpc_trigger *ovsdb_jsonrpc_trigger_find(
     struct ovsdb_jsonrpc_session *, const struct json *id, size_t hash);
 static void ovsdb_jsonrpc_trigger_complete(struct ovsdb_jsonrpc_trigger *);
@@ -936,17 +936,6 @@ ovsdb_jsonrpc_session_unlock(struct ovsdb_jsonrpc_session 
*s,
 }
 
 static struct jsonrpc_msg *
-execute_transaction(struct ovsdb_jsonrpc_session *s, struct ovsdb *db,
-                    struct jsonrpc_msg *request)
-{
-    ovsdb_jsonrpc_trigger_create(s, db, request->id, request->params);
-    request->id = NULL;
-    request->params = NULL;
-    jsonrpc_msg_destroy(request);
-    return NULL;
-}
-
-static struct jsonrpc_msg *
 ovsdb_jsonrpc_session_set_db_change_aware(struct ovsdb_jsonrpc_session *s,
                                           const struct jsonrpc_msg *request)
 {
@@ -967,10 +956,11 @@ ovsdb_jsonrpc_session_got_request(struct 
ovsdb_jsonrpc_session *s,
 {
     struct jsonrpc_msg *reply;
 
-    if (!strcmp(request->method, "transact")) {
+    if (!strcmp(request->method, "transact") ||
+        !strcmp(request->method, "convert_db")) {
         struct ovsdb *db = ovsdb_jsonrpc_lookup_db(s, request, &reply);
         if (!reply) {
-            reply = execute_transaction(s, db, request);
+            ovsdb_jsonrpc_trigger_create(s, db, request);
         }
     } else if (!strcmp(request->method, "monitor") ||
                (monitor_cond_enable__ && !strcmp(request->method,
@@ -1082,37 +1072,35 @@ struct ovsdb_jsonrpc_trigger {
 
 static void
 ovsdb_jsonrpc_trigger_create(struct ovsdb_jsonrpc_session *s, struct ovsdb *db,
-                             struct json *id, struct json *params)
+                             struct jsonrpc_msg *request)
 {
-    struct ovsdb_jsonrpc_trigger *t;
-    size_t hash;
-
     /* Check for duplicate ID. */
-    hash = json_hash(id, 0);
-    t = ovsdb_jsonrpc_trigger_find(s, id, hash);
+    size_t hash = json_hash(request->id, 0);
+    struct ovsdb_jsonrpc_trigger *t
+        = ovsdb_jsonrpc_trigger_find(s, request->id, hash);
     if (t) {
-        struct jsonrpc_msg *msg;
-
-        msg = jsonrpc_create_error(json_string_create("duplicate request ID"),
-                                   id);
-        ovsdb_jsonrpc_session_send(s, msg);
-        json_destroy(id);
-        json_destroy(params);
+        ovsdb_jsonrpc_session_send(
+            s, syntax_error_reply(request, "duplicate request ID"));
+        jsonrpc_msg_destroy(request);
         return;
     }
 
     /* Insert into trigger table. */
     t = xmalloc(sizeof *t);
-    ovsdb_trigger_init(&s->up, db, &t->trigger, params, time_msec(),
-                       s->read_only, s->remote->role,
-                       jsonrpc_session_get_id(s->js));
-    t->id = id;
+    bool disconnect_all = ovsdb_trigger_init(
+        &s->up, db, &t->trigger, request, time_msec(), s->read_only,
+        s->remote->role, jsonrpc_session_get_id(s->js));
+    t->id = json_clone(request->id);
     hmap_insert(&s->triggers, &t->hmap_node, hash);
 
     /* Complete early if possible. */
     if (ovsdb_trigger_is_complete(&t->trigger)) {
         ovsdb_jsonrpc_trigger_complete(t);
     }
+
+    if (disconnect_all) {
+        ovsdb_jsonrpc_server_reconnect(s->remote->server, false);
+    }
 }
 
 static struct ovsdb_jsonrpc_trigger *
@@ -1139,12 +1127,9 @@ ovsdb_jsonrpc_trigger_complete(struct 
ovsdb_jsonrpc_trigger *t)
 
     if (jsonrpc_session_is_connected(s->js)) {
         struct jsonrpc_msg *reply;
-        struct json *result;
 
-        result = ovsdb_trigger_steal_result(&t->trigger);
-        if (result) {
-            reply = jsonrpc_create_reply(result, t->id);
-        } else {
+        reply = ovsdb_trigger_steal_reply(&t->trigger);
+        if (!reply) {
             reply = jsonrpc_create_error(json_string_create("canceled"),
                                          t->id);
         }
@@ -1159,7 +1144,7 @@ ovsdb_jsonrpc_trigger_complete(struct 
ovsdb_jsonrpc_trigger *t)
 
 static void
 ovsdb_jsonrpc_trigger_remove__(struct ovsdb_jsonrpc_session *s,
-                                   struct ovsdb *db)
+                               struct ovsdb *db)
 {
     struct ovsdb_jsonrpc_trigger *t, *next;
     HMAP_FOR_EACH_SAFE (t, next, hmap_node, &s->triggers) {
@@ -1189,11 +1174,9 @@ ovsdb_jsonrpc_trigger_complete_all(struct 
ovsdb_jsonrpc_session *s)
 static void
 ovsdb_jsonrpc_trigger_complete_done(struct ovsdb_jsonrpc_session *s)
 {
-    while (!ovs_list_is_empty(&s->up.completions)) {
-        struct ovsdb_jsonrpc_trigger *t
-            = CONTAINER_OF(s->up.completions.next,
-                           struct ovsdb_jsonrpc_trigger, trigger.node);
-        ovsdb_jsonrpc_trigger_complete(t);
+    struct ovsdb_jsonrpc_trigger *trigger, *next;
+    LIST_FOR_EACH_SAFE (trigger, next, trigger.node, &s->up.completions) {
+        ovsdb_jsonrpc_trigger_complete(trigger);
     }
 }
 
@@ -1441,7 +1424,7 @@ ovsdb_jsonrpc_monitor_create(struct ovsdb_jsonrpc_session 
*s, struct ovsdb *db,
 
 error:
     if (m) {
-        ovsdb_jsonrpc_monitor_destroy(m);
+        ovsdb_jsonrpc_monitor_destroy(m, false);
     }
 
     return jsonrpc_create_error(ovsdb_error_to_json_free(error), request_id);
@@ -1599,32 +1582,12 @@ ovsdb_jsonrpc_monitor_cancel(struct 
ovsdb_jsonrpc_session *s,
             return jsonrpc_create_error(json_string_create("unknown monitor"),
                                         request_id);
         } else {
-            ovsdb_jsonrpc_monitor_destroy(m);
+            ovsdb_jsonrpc_monitor_destroy(m, false);
             return jsonrpc_create_reply(json_object_create(), request_id);
         }
     }
 }
 
-static void
-ovsdb_jsonrpc_monitor_remove__(struct ovsdb_jsonrpc_session *s,
-                               struct ovsdb *db)
-{
-    struct ovsdb_jsonrpc_monitor *m, *next;
-
-    HMAP_FOR_EACH_SAFE (m, next, node, &s->monitors) {
-        if (!db || m->db == db) {
-            if (db && jsonrpc_session_is_connected(s->js)
-                && s->db_change_aware) {
-                struct jsonrpc_msg *notify = jsonrpc_create_notify(
-                    "monitor_canceled",
-                    json_array_create_1(json_clone(m->monitor_id)));
-                ovsdb_jsonrpc_session_send(s, notify);
-            }
-            ovsdb_jsonrpc_monitor_destroy(m);
-        }
-    }
-}
-
 /* Database 'db' is about to be removed from the database server.  To prepare,
  * this function removes all references from monitors in 's' to 'db'. */
 static void
@@ -1632,14 +1595,24 @@ ovsdb_jsonrpc_monitor_preremove_db(struct 
ovsdb_jsonrpc_session *s,
                                    struct ovsdb *db)
 {
     ovs_assert(db);
-    ovsdb_jsonrpc_monitor_remove__(s, db);
+
+    struct ovsdb_jsonrpc_monitor *m, *next;
+    HMAP_FOR_EACH_SAFE (m, next, node, &s->monitors) {
+        if (m->db == db) {
+            ovsdb_jsonrpc_monitor_destroy(m, true);
+        }
+    }
 }
 
 /* Cancels all monitors in 's'. */
 static void
 ovsdb_jsonrpc_monitor_remove_all(struct ovsdb_jsonrpc_session *s)
 {
-    ovsdb_jsonrpc_monitor_remove__(s, NULL);
+    struct ovsdb_jsonrpc_monitor *m, *next;
+
+    HMAP_FOR_EACH_SAFE (m, next, node, &s->monitors) {
+        ovsdb_jsonrpc_monitor_destroy(m, false);
+    }
 }
 
 static struct json *
@@ -1670,8 +1643,19 @@ ovsdb_jsonrpc_monitor_needs_flush(struct 
ovsdb_jsonrpc_session *s)
 }
 
 void
-ovsdb_jsonrpc_monitor_destroy(struct ovsdb_jsonrpc_monitor *m)
-{
+ovsdb_jsonrpc_monitor_destroy(struct ovsdb_jsonrpc_monitor *m,
+                              bool notify_cancellation)
+{
+    if (notify_cancellation) {
+        struct ovsdb_jsonrpc_session *s = m->session;
+        if (jsonrpc_session_is_connected(s->js) && s->db_change_aware) {
+            struct jsonrpc_msg *notify = jsonrpc_create_notify(
+                "monitor_canceled",
+                json_array_create_1(json_clone(m->monitor_id)));
+            ovsdb_jsonrpc_session_send(s, notify);
+        }
+    }
+
     json_destroy(m->monitor_id);
     hmap_remove(&m->session->monitors, &m->node);
     ovsdb_monitor_remove_jsonrpc_monitor(m->dbmon, m, m->unflushed);
diff --git a/ovsdb/jsonrpc-server.h b/ovsdb/jsonrpc-server.h
index 50a8b879c5a9..0fc16f21b2d9 100644
--- a/ovsdb/jsonrpc-server.h
+++ b/ovsdb/jsonrpc-server.h
@@ -80,7 +80,8 @@ const struct uuid *ovsdb_jsonrpc_server_get_uuid(
     const struct ovsdb_jsonrpc_server *);
 
 struct ovsdb_jsonrpc_monitor;
-void ovsdb_jsonrpc_monitor_destroy(struct ovsdb_jsonrpc_monitor *);
+void ovsdb_jsonrpc_monitor_destroy(struct ovsdb_jsonrpc_monitor *,
+                                   bool notify_cancellation);
 void ovsdb_jsonrpc_disable_monitor_cond(void);
 
 #endif /* ovsdb/jsonrpc-server.h */
diff --git a/ovsdb/log.c b/ovsdb/log.c
index 4b0d9640cafd..a223d30ac28b 100644
--- a/ovsdb/log.c
+++ b/ovsdb/log.c
@@ -92,26 +92,24 @@ ovsdb_log_open(const char *name, const char *magic,
         lockfile = NULL;
     }
 
-    if (open_mode == OVSDB_LOG_READ_ONLY) {
+    switch (open_mode) {
+    case OVSDB_LOG_READ_ONLY:
         flags = O_RDONLY;
-    } else if (open_mode == OVSDB_LOG_READ_WRITE) {
+        break;
+
+    case OVSDB_LOG_READ_WRITE:
         flags = O_RDWR;
-    } else if (open_mode == OVSDB_LOG_CREATE) {
-#ifndef _WIN32
-        if (stat(name, &s) == -1 && errno == ENOENT
-            && lstat(name, &s) == 0 && S_ISLNK(s.st_mode)) {
-            /* 'name' is a dangling symlink.  We want to create the file that
-             * the symlink points to, but POSIX says that open() with O_EXCL
-             * must fail with EEXIST if the named file is a symlink.  So, we
-             * have to leave off O_EXCL and accept the race. */
-            flags = O_RDWR | O_CREAT;
-        } else {
-            flags = O_RDWR | O_CREAT | O_EXCL;
-        }
-#else
+        break;
+
+    case OVSDB_LOG_CREATE_EXCL:
         flags = O_RDWR | O_CREAT | O_EXCL;
-#endif
-    } else {
+        break;
+
+    case OVSDB_LOG_CREATE:
+        flags = O_RDWR | O_CREAT;
+        break;
+
+    default:
         OVS_NOT_REACHED();
     }
 #ifdef _WIN32
@@ -119,7 +117,9 @@ ovsdb_log_open(const char *name, const char *magic,
 #endif
     fd = open(name, flags, 0666);
     if (fd < 0) {
-        const char *op = open_mode == OVSDB_LOG_CREATE ? "create" : "open";
+        const char *op = (open_mode == OVSDB_LOG_CREATE_EXCL ? "create"
+            : open_mode == OVSDB_LOG_CREATE ? "create or open"
+            : "open");
         error = ovsdb_io_error(errno, "%s: %s failed", name, op);
         goto error_unlock;
     }
@@ -182,7 +182,9 @@ ovsdb_log_close(struct ovsdb_log *file)
 {
     if (file) {
         free(file->name);
-        fclose(file->stream);
+        if (file->stream) {
+            fclose(file->stream);
+        }
         lockfile_unlock(file->lockfile);
         ovsdb_error_destroy(file->read_error);
         free(file);
@@ -433,3 +435,107 @@ ovsdb_log_get_offset(const struct ovsdb_log *log)
 {
     return log->offset;
 }
+
+struct ovsdb_error * OVS_WARN_UNUSED_RESULT
+ovsdb_log_replace(struct ovsdb_log *log, struct json **entries, size_t n)
+{
+    struct ovsdb_error *error;
+    struct ovsdb_log *new;
+
+    error = ovsdb_log_replace_start(log, &new);
+    if (error) {
+        return error;
+    }
+
+    for (size_t i = 0; i < n; i++) {
+        error = ovsdb_log_write(new, entries[i]);
+        if (error) {
+            ovsdb_log_replace_abort(new);
+            return error;
+        }
+    }
+
+    return ovsdb_log_replace_commit(log, new);
+}
+
+struct ovsdb_error * OVS_WARN_UNUSED_RESULT
+ovsdb_log_replace_start(struct ovsdb_log *old,
+                        struct ovsdb_log **newp)
+{
+    char *tmp_name = xasprintf("%s.tmp", old->name);
+    struct ovsdb_error *error;
+
+    ovs_assert(old->lockfile);
+
+    /* Remove temporary file.  (It might not exist.) */
+    if (unlink(tmp_name) < 0 && errno != ENOENT) {
+        error = ovsdb_io_error(errno, "failed to remove %s", tmp_name);
+        free(tmp_name);
+        *newp = NULL;
+        return error;
+    }
+
+    /* Create temporary file. */
+    error = ovsdb_log_open(tmp_name, old->magic, OVSDB_LOG_CREATE_EXCL,
+                           false, newp);
+    if (error) {
+        free(tmp_name);
+        *newp = NULL;
+    }
+    return error;
+}
+
+struct ovsdb_error * OVS_WARN_UNUSED_RESULT
+ovsdb_log_replace_commit(struct ovsdb_log *old, struct ovsdb_log *new)
+{
+    struct ovsdb_error *error = ovsdb_log_commit(new);
+    if (error) {
+        ovsdb_log_close(new);
+        return error;
+    }
+
+    /* Replace old file by new file on-disk. */
+    if (rename(new->name, old->name)) {
+        error = ovsdb_io_error(errno, "failed to rename \"%s\" to \"%s\"",
+                               new->name, old->name);
+        ovsdb_log_close(new);
+        return error;
+    }
+    fsync_parent_dir(old->name);
+
+    /* Replace 'old' by 'new' in memory.
+     *
+     * 'old' transitions to OVSDB_LOG_WRITE (it was probably in that mode
+     * anyway). */
+    /* prev_offset only matters for OVSDB_LOG_READ. */
+    old->offset = new->offset;
+    /* Keep old->name and old->rel_name. */
+    free(old->magic);
+    old->magic = new->magic;
+    new->magic = NULL;
+    /* Keep old->lockfile. */
+    fclose(old->stream);
+    old->stream = new->stream;
+    new->stream = NULL;
+    /* read_error only matters for OVSDB_LOG_READ. */
+    old->write_error = new->write_error;
+    old->mode = OVSDB_LOG_WRITE;
+
+    /* Free 'new'. */
+    ovsdb_log_close(new);
+
+    return NULL;
+}
+
+void
+ovsdb_log_replace_abort(struct ovsdb_log *new)
+{
+    if (new) {
+        /* Unlink the new file, but only after we close it (for Windows
+         * compatibility). */
+        char *name = xstrdup(new->name);
+        ovsdb_log_close(new);
+        unlink(name);
+        free(name);
+    }
+}
diff --git a/ovsdb/log.h b/ovsdb/log.h
index fd495e518dd0..439487ade12e 100644
--- a/ovsdb/log.h
+++ b/ovsdb/log.h
@@ -26,7 +26,8 @@ struct ovsdb_log;
 enum ovsdb_log_open_mode {
     OVSDB_LOG_READ_ONLY,        /* Open existing file, read-only. */
     OVSDB_LOG_READ_WRITE,       /* Open existing file, read/write. */
-    OVSDB_LOG_CREATE            /* Create new file, read/write. */
+    OVSDB_LOG_CREATE_EXCL,      /* Create new file, read/write. */
+    OVSDB_LOG_CREATE            /* Create or open file, read/write. */
 };
 
 #define OVSDB_MAGIC "OVSDB JSON"
@@ -48,4 +49,15 @@ struct ovsdb_error *ovsdb_log_commit(struct ovsdb_log *)
 
 off_t ovsdb_log_get_offset(const struct ovsdb_log *);
 
+struct ovsdb_error *ovsdb_log_replace(struct ovsdb_log *,
+                                      struct json **entries, size_t n)
+    OVS_WARN_UNUSED_RESULT;
+struct ovsdb_error *ovsdb_log_replace_start(struct ovsdb_log *old,
+                                            struct ovsdb_log **newp)
+    OVS_WARN_UNUSED_RESULT;
+struct ovsdb_error *ovsdb_log_replace_commit(struct ovsdb_log *old,
+                                             struct ovsdb_log *new)
+    OVS_WARN_UNUSED_RESULT;
+void ovsdb_log_replace_abort(struct ovsdb_log *new);
+
 #endif /* ovsdb/log.h */
diff --git a/ovsdb/monitor.c b/ovsdb/monitor.c
index 3e58c3fbd274..97706932614c 100644
--- a/ovsdb/monitor.c
+++ b/ovsdb/monitor.c
@@ -1613,7 +1613,7 @@ ovsdb_monitors_remove(struct ovsdb *db)
          * end monitor will also destroy the corresponding 'ovsdb_monitor'.
          * ovsdb monitor will also be destroied.  */
         LIST_FOR_EACH_SAFE (jm, next_jm, node, &m->jsonrpc_monitors) {
-            ovsdb_jsonrpc_monitor_destroy(jm->jsonrpc_monitor);
+            ovsdb_jsonrpc_monitor_destroy(jm->jsonrpc_monitor, false);
         }
     }
 }
@@ -1630,3 +1630,20 @@ ovsdb_monitor_get_memory_usage(struct simap *usage)
         simap_increase(usage, "json-caches", hmap_count(&dbmon->json_cache));
     }
 }
+
+void
+ovsdb_monitor_prereplace_db(struct ovsdb *db)
+{
+    struct ovsdb_monitor *m, *next_m;
+
+    LIST_FOR_EACH_SAFE (m, next_m, list_node, &db->monitors) {
+        struct jsonrpc_monitor_node *jm, *next_jm;
+
+        /* Delete all front end monitors. Removing the last front
+         * end monitor will also destroy the corresponding 'ovsdb_monitor'.
+         * ovsdb monitor will also be destroied.  */
+        LIST_FOR_EACH_SAFE (jm, next_jm, node, &m->jsonrpc_monitors) {
+            ovsdb_jsonrpc_monitor_destroy(jm->jsonrpc_monitor, true);
+        }
+    }
+}
diff --git a/ovsdb/monitor.h b/ovsdb/monitor.h
index 99d43c45dff9..eb3ff270c9f3 100644
--- a/ovsdb/monitor.h
+++ b/ovsdb/monitor.h
@@ -49,6 +49,8 @@ struct ovsdb_monitor *ovsdb_monitor_create(struct ovsdb *db,
 void ovsdb_monitors_remove(struct ovsdb *);
 void ovsdb_monitors_commit(struct ovsdb *, const struct ovsdb_txn *);
 
+void ovsdb_monitor_prereplace_db(struct ovsdb *);
+
 struct ovsdb_monitor *ovsdb_monitor_add(struct ovsdb_monitor *dbmon);
 
 void ovsdb_monitor_add_jsonrpc_monitor(struct ovsdb_monitor *dbmon,
diff --git a/ovsdb/ovsdb-client.1.in b/ovsdb/ovsdb-client.1.in
index 4b543d96e552..f8630ba40f25 100644
--- a/ovsdb/ovsdb-client.1.in
+++ b/ovsdb/ovsdb-client.1.in
@@ -22,6 +22,9 @@ ovsdb\-client \- command-line interface to 
\fBovsdb-server\fR(1)
 .br
 \fBovsdb\-client \fR[\fIoptions\fR] \fBlist\-columns\fI \fR[\fIserver\fR] 
\fR[\fIdatabase\fR] [\fItable\fR]
 .IP "Database Version Management Commands:"
+\fBovsdb\-client \fR[\fIoptions\fR] \fBconvert \fR[\fIserver\fR] \fIschema\fR
+.br
+\fBovsdb\-client \fR[\fIoptions\fR] \fBneeds\-conversion \fR[\fIserver\fR] 
\fIschema\fR
 .br
 \fBovsdb\-client \fR[\fIoptions\fR] \fBget\-schema\-version\fI 
\fR[\fIserver\fR] \fR[\fIdatabase\fR]
 .IP "Data Management Commands:"
@@ -110,6 +113,42 @@ listed; otherwise, the tables include columns in all 
tables.
 These commands work with different versions of OVSDB schemas and
 databases.
 .
+.IP "\fBconvert \fR[\fIserver\fR] \fIschema\fR"
+Reads an OVSDB schema in JSON format, as specified in the OVSDB
+specification, from \fIschema\fR, then connects to \fIserver\fR and
+requests the server to convert the database whose name is specified in
+\fIschema\fR to the schema also specified in \fIschema\fR.
+.IP
+The conversion is atomic, consistent, isolated, and durable.
+Following the schema change, the server notifies clients that use the
+\fBset_db_change_aware\fR RPC introduced in Open vSwitch 2.9 and
+cancel their outstanding transactions and monitors.  The server
+disconnects other clients, enabling them to notify the change when
+they reconnect.
+.IP
+This command can do simple ``upgrades'' and ``downgrades'' on a
+database's schema.  The data in the database must be valid when
+interpreted under \fIschema\fR, with only one exception: data for
+tables and columns that do not exist in \fIschema\fR are ignored.
+Columns that exist in \fIschema\fR but not in the database are set to
+their default values.  All of \fIschema\fR's constraints apply in
+full.
+.IP
+Some uses of this command can cause unrecoverable data loss.  For
+example, converting a database from a schema that has a given column
+or table to one that does not will delete all data in that column or
+table.  Back up critical databases before converting them.
+.IP
+This command works with clustered and standalone databases.
+Standalone databases may also be converted (offline) with
+\fBovsdb\-tool\fR's \fBconvert\fR command.
+.
+.IP "\fBneeds\-conversion \fR[\fIserver\fR] \fIschema\fR"
+Reads the schema from \fIschema\fR, then connects to \fIserver\fR and
+requests the schema from the database whose name is specified in
+\fIschema\fR.  If the two schemas are the same, prints \fBno\fR on
+stdout; if they differ, prints \fByes\fR.
+.
 .IP "\fBget\-schema\-version\fI \fR[\fIserver\fR] \fR[\fIdatabase\fR]"
 Connects to \fIserver\fR, retrieves the schema for \fIdatabase\fR, and
 prints its version number on stdout.
diff --git a/ovsdb/ovsdb-client.c b/ovsdb/ovsdb-client.c
index cecc0346eda1..e12702200616 100644
--- a/ovsdb/ovsdb-client.c
+++ b/ovsdb/ovsdb-client.c
@@ -71,11 +71,17 @@ struct ovsdb_client_command {
 /* --timestamp: Print a timestamp before each update on "monitor" command? */
 static bool timestamp;
 
-/* --db-change-aware: Enable db_change_aware feature for "monitor" command?
+/* --db-change-aware, --no-db-change-aware: Enable db_change_aware feature for
+ * "monitor" command?
  *
- * (This option is undocumented because it is expected to be useful only for
- * testing that the db_change_aware feature actually works.) */
-static bool db_change_aware;
+ * -1 (default): Use db_change_aware if available.
+ * 0: Disable db_change_aware.
+ * 1: Require db_change_aware.
+ *
+ * (This option is undocumented because anything other than the default is
+ * expected to be useful only for testing that the db_change_aware feature
+ * actually works.) */
+static int db_change_aware = -1;
 
 /* Format for table output. */
 static struct table_style table_style = TABLE_STYLE_DEFAULT;
@@ -188,7 +194,6 @@ parse_options(int argc, char *argv[])
     enum {
         OPT_BOOTSTRAP_CA_CERT = UCHAR_MAX + 1,
         OPT_TIMESTAMP,
-        OPT_DB_CHANGE_AWARE,
         VLOG_OPTION_ENUMS,
         DAEMON_OPTION_ENUMS,
         TABLE_OPTION_ENUMS,
@@ -198,7 +203,8 @@ parse_options(int argc, char *argv[])
         {"help", no_argument, NULL, 'h'},
         {"version", no_argument, NULL, 'V'},
         {"timestamp", no_argument, NULL, OPT_TIMESTAMP},
-        {"db-change-aware", no_argument, NULL, OPT_DB_CHANGE_AWARE},
+        {"db-change-aware", no_argument, &db_change_aware, 1},
+        {"no-db-change-aware", no_argument, &db_change_aware, 0},
         VLOG_LONG_OPTIONS,
         DAEMON_LONG_OPTIONS,
 #ifdef HAVE_OPENSSL
@@ -241,10 +247,6 @@ parse_options(int argc, char *argv[])
             timestamp = true;
             break;
 
-        case OPT_DB_CHANGE_AWARE:
-            db_change_aware = true;
-            break;
-
         case '?':
             exit(EXIT_FAILURE);
 
@@ -288,6 +290,8 @@ usage(void)
            "    DATABASE on SERVER.\n"
            "    COLUMNs may include !initial, !insert, !delete, !modify\n"
            "    to avoid seeing the specified kinds of changes.\n"
+           "\n  convert [SERVER] SCHEMA\n"
+           "    convert database on SERVER named in SCHEMA to SCHEMA.\n"
            "\n  monitor [SERVER] [DATABASE] ALL\n"
            "    monitor all changes to all columns in all tables\n"
            "    in DATBASE on SERVER.\n"
@@ -530,6 +534,28 @@ do_list_columns(struct jsonrpc *rpc, const char *database,
 }
 
 static void
+send_db_change_aware(struct jsonrpc *rpc)
+{
+    if (db_change_aware != 0) {
+        struct jsonrpc_msg *request = jsonrpc_create_request(
+            "set_db_change_aware",
+            json_array_create_1(json_boolean_create(true)),
+            NULL);
+        struct jsonrpc_msg *reply;
+        int error = jsonrpc_transact_block(rpc, request, &reply);
+        if (error) {
+            ovs_fatal(error, "%s: error setting db_change_aware",
+                      jsonrpc_get_name(rpc));
+        }
+        if (reply->type == JSONRPC_ERROR && db_change_aware == 1) {
+            ovs_fatal(0, "%s: set_db_change_aware failed (%s)",
+                      jsonrpc_get_name(rpc), json_to_string(reply->error, 0));
+        }
+        jsonrpc_msg_destroy(reply);
+    }
+}
+
+static void
 do_transact(struct jsonrpc *rpc, const char *database OVS_UNUSED,
             int argc OVS_UNUSED, char *argv[])
 {
@@ -538,6 +564,13 @@ do_transact(struct jsonrpc *rpc, const char *database 
OVS_UNUSED,
 
     transaction = parse_json(argv[0]);
 
+    if (db_change_aware == 1) {
+        send_db_change_aware(rpc);
+    }
+    daemon_save_fd(STDOUT_FILENO);
+    daemon_save_fd(STDERR_FILENO);
+    daemonize();
+
     request = jsonrpc_create_request("transact", transaction, NULL);
     check_txn(jsonrpc_transact_block(rpc, request, &reply), &reply);
     print_json(reply->result);
@@ -971,6 +1004,7 @@ do_monitor__(struct jsonrpc *rpc, const char *database,
     ovs_assert(version < OVSDB_MONITOR_VERSION_MAX);
 
     daemon_save_fd(STDOUT_FILENO);
+    daemon_save_fd(STDERR_FILENO);
     daemonize_start(false);
     if (get_detach()) {
         int error;
@@ -1028,22 +1062,7 @@ do_monitor__(struct jsonrpc *rpc, const char *database,
         free(nodes);
     }
 
-    if (db_change_aware) {
-        struct jsonrpc_msg *request = jsonrpc_create_request(
-            "set_db_change_aware",
-            json_array_create_1(json_boolean_create(true)),
-            NULL);
-        struct jsonrpc_msg *reply;
-        int error = jsonrpc_transact_block(rpc, request, &reply);
-        if (error) {
-            ovs_fatal(error, "%s: error setting db_change_aware", server);
-        }
-        if (reply->type == JSONRPC_ERROR) {
-            ovs_fatal(0, "%s: set_db_change_aware failed (%s)",
-                      server, json_to_string(reply->error, 0));
-        }
-        jsonrpc_msg_destroy(reply);
-    }
+    send_db_change_aware(rpc);
 
     monitor = json_array_create_3(json_string_create(database),
                                   json_null_create(), monitor_requests);
@@ -1105,6 +1124,10 @@ do_monitor__(struct jsonrpc *rpc, const char *database,
                     monitor2_print(params->u.array.elems[1], mts, n_mts);
                     fflush(stdout);
                 }
+            } else if (msg->type == JSONRPC_NOTIFY
+                       && !strcmp(msg->method, "monitor_canceled")) {
+                ovs_fatal(0, "%s: %s database was removed",
+                          server, database);
             }
             jsonrpc_msg_destroy(msg);
         }
@@ -1160,6 +1183,37 @@ do_monitor_cond(struct jsonrpc *rpc, const char 
*database,
     ovsdb_schema_destroy(schema);
 }
 
+static void
+do_convert(struct jsonrpc *rpc, const char *database OVS_UNUSED,
+           int argc OVS_UNUSED, char *argv[])
+{
+    struct ovsdb_schema *new_schema;
+    check_ovsdb_error(ovsdb_schema_from_file(argv[0], &new_schema));
+
+    struct jsonrpc_msg *request, *reply;
+    request = jsonrpc_create_request(
+        "convert_db",
+        json_array_create_2(json_string_create(new_schema->name),
+                            ovsdb_schema_to_json(new_schema)), NULL);
+    check_txn(jsonrpc_transact_block(rpc, request, &reply), &reply);
+    jsonrpc_msg_destroy(reply);
+}
+
+static void
+do_needs_conversion(struct jsonrpc *rpc, const char *database OVS_UNUSED,
+                    int argc OVS_UNUSED, char *argv[])
+{
+    struct ovsdb_schema *schema1;
+    check_ovsdb_error(ovsdb_schema_from_file(argv[0], &schema1));
+
+    struct ovsdb_schema *schema2 = fetch_schema(rpc, schema1->name);
+    if (ovsdb_schema_equal(schema1, schema2)) {
+        puts(ovsdb_schema_equal(schema1, schema2) ? "no" : "yes");
+    }
+    ovsdb_schema_destroy(schema1);
+    ovsdb_schema_destroy(schema2);
+}
+
 struct dump_table_aux {
     struct ovsdb_datum **data;
     const struct ovsdb_column **columns;
@@ -1617,6 +1671,8 @@ static const struct ovsdb_client_command all_commands[] = 
{
     { "transact",           NEED_RPC,      1, 1,       do_transact },
     { "monitor",            NEED_DATABASE, 1, INT_MAX, do_monitor },
     { "monitor-cond",       NEED_DATABASE, 2, 3,       do_monitor_cond },
+    { "convert",            NEED_RPC,      1, 1,       do_convert },
+    { "needs-conversion",   NEED_RPC,      1, 1,       do_needs_conversion },
     { "dump",               NEED_DATABASE, 0, INT_MAX, do_dump },
     { "lock",               NEED_RPC,      1, 1,       do_lock_create },
     { "steal",              NEED_RPC,      1, 1,       do_lock_steal },
diff --git a/ovsdb/ovsdb-server.1.in b/ovsdb/ovsdb-server.1.in
index 9bfd36edd351..02a887b94935 100644
--- a/ovsdb/ovsdb-server.1.in
+++ b/ovsdb/ovsdb-server.1.in
@@ -761,6 +761,49 @@ restores the default behavior.  The reply is always the 
same:
 .fi
 .RE
 .
+.IP "4.1.17. Online Database Schema Conversion"
+.IP
+Before Open vSwitch 2.9, a database's schema could be changed only by
+taking its server offline, converting it using \fBovsdb\-tool
+convert\fR, and then bringing it back online.  Open vSwitch 2.9
+introduces the following RPC to convert the schema of a database
+without taking it offline.
+.PP
+.RS
+.nf
+"method": "convert_db"
+"params": [<database-schema>]
+"id": <nonnull-json-value>
+.fi
+.RE
+.IP
+If the server successfully converts the database, the server replies
+with:
+.PP
+.RS
+.nf
+"result": {}
+"error": null
+"id": same "id" as request
+.fi
+.RE
+.IP
+If the server cannot convert the database, it replies with an error:
+.PP
+.RS
+.nf
+"result": null
+"error": <error>
+"id": same "id" as request
+.fi
+.RE
+.IP
+If the new schema and the old schema are identical, the server does
+not disrupt clients.  Otherwise, when a database's schema changes, the
+server disconnects all clients that are not database change aware and
+sends \fBcancel\fR RPCs for outstanding transactions and
+\fBmonitor_cancel\fR RPCs for ongoing monitors against the database
+that changed.
 .IP "5.1. Notation"
 For <condition>, RFC 7047 only allows the use of \fB!=\fR, \fB==\fR,
 \fBincludes\fR, and \fBexcludes\fR operators with set types.  Open
diff --git a/ovsdb/ovsdb-server.c b/ovsdb/ovsdb-server.c
index e28fca5989fc..65cf61ade954 100644
--- a/ovsdb/ovsdb-server.c
+++ b/ovsdb/ovsdb-server.c
@@ -111,10 +111,11 @@ static char *open_db(struct server_config *config, const 
char *filename);
 static void add_server_db(struct server_config *);
 static void close_db(struct db *db);
 
-static void parse_options(int *argc, char **argvp[],
-                          struct sset *remotes, char **unixctl_pathp,
-                          char **run_command, char **sync_from,
-                          char **sync_exclude, bool *is_backup);
+static void parse_options(int argc, char *argvp[],
+                          struct sset *db_filenames, struct sset *remotes,
+                          char **unixctl_pathp, char **run_command,
+                          char **sync_from, char **sync_exclude,
+                          bool *is_backup);
 OVS_NO_RETURN static void usage(void);
 
 static char *reconfigure_remotes(struct ovsdb_jsonrpc_server *,
@@ -202,7 +203,9 @@ main_loop(struct ovsdb_jsonrpc_server *jsonrpc, struct 
shash *all_dbs,
 
         SHASH_FOR_EACH(node, all_dbs) {
             struct db *db = node->data;
-            ovsdb_trigger_run(db->db, time_msec());
+            if (ovsdb_trigger_run(db->db, time_msec())) {
+                ovsdb_jsonrpc_server_reconnect(jsonrpc, false);
+            }
         }
         if (run_process) {
             process_run();
@@ -265,7 +268,6 @@ main(int argc, char *argv[])
     struct shash all_dbs;
     struct shash_node *node, *next;
     char *error;
-    int i;
 
     ovs_cmdl_proctitle_init(argc, argv);
     set_program_name(argv[0]);
@@ -274,8 +276,8 @@ main(int argc, char *argv[])
     process_init();
 
     bool active = false;
-    parse_options(&argc, &argv, &remotes, &unixctl_path, &run_command,
-                  &sync_from, &sync_exclude, &active);
+    parse_options(argc, argv, &db_filenames, &remotes, &unixctl_path,
+                  &run_command, &sync_from, &sync_exclude, &active);
     is_backup = sync_from && !active;
 
     daemon_become_new_user(false);
@@ -290,17 +292,6 @@ main(int argc, char *argv[])
         ovs_fatal(errno, "failed to create temporary file");
     }
 
-    sset_init(&db_filenames);
-    if (argc > 0) {
-        for (i = 0; i < argc; i++) {
-            sset_add(&db_filenames, argv[i]);
-         }
-    } else {
-        char *default_db = xasprintf("%s/conf.db", ovs_dbdir());
-        sset_add(&db_filenames, default_db);
-        free(default_db);
-    }
-
     server_config.remotes = &remotes;
     server_config.config_tmpfile = config_tmpfile;
 
@@ -1463,8 +1454,9 @@ ovsdb_server_get_sync_status(struct unixctl_conn *conn, 
int argc OVS_UNUSED,
 }
 
 static void
-parse_options(int *argcp, char **argvp[],
-              struct sset *remotes, char **unixctl_pathp, char **run_command,
+parse_options(int argc, char *argv[],
+              struct sset *db_filenames, struct sset *remotes,
+              char **unixctl_pathp, char **run_command,
               char **sync_from, char **sync_exclude, bool *active)
 {
     enum {
@@ -1476,10 +1468,12 @@ parse_options(int *argcp, char **argvp[],
         OPT_SYNC_FROM,
         OPT_SYNC_EXCLUDE,
         OPT_ACTIVE,
+        OPT_NO_DBS,
         VLOG_OPTION_ENUMS,
         DAEMON_OPTION_ENUMS,
         SSL_OPTION_ENUMS,
     };
+
     static const struct option long_options[] = {
         {"remote",      required_argument, NULL, OPT_REMOTE},
         {"unixctl",     required_argument, NULL, OPT_UNIXCTL},
@@ -1496,14 +1490,15 @@ parse_options(int *argcp, char **argvp[],
         {"sync-from",   required_argument, NULL, OPT_SYNC_FROM},
         {"sync-exclude-tables", required_argument, NULL, OPT_SYNC_EXCLUDE},
         {"active", no_argument, NULL, OPT_ACTIVE},
+        {"no-dbs", no_argument, NULL, OPT_NO_DBS},
         {NULL, 0, NULL, 0},
     };
     char *short_options = ovs_cmdl_long_options_to_short_options(long_options);
-    int argc = *argcp;
-    char **argv = *argvp;
+    bool add_default_db = true;
 
     *sync_from = NULL;
     *sync_exclude = NULL;
+    sset_init(db_filenames);
     sset_init(remotes);
     for (;;) {
         int c;
@@ -1582,6 +1577,10 @@ parse_options(int *argcp, char **argvp[],
             *active = true;
             break;
 
+        case OPT_NO_DBS:
+            add_default_db = false;
+            break;
+
         case '?':
             exit(EXIT_FAILURE);
 
@@ -1591,8 +1590,15 @@ parse_options(int *argcp, char **argvp[],
     }
     free(short_options);
 
-    *argcp -= optind;
-    *argvp += optind;
+    argc -= optind;
+    argv += optind;
+    if (argc > 0) {
+        for (int i = 0; i < argc; i++) {
+            sset_add(db_filenames, argv[i]);
+        }
+    } else if (add_default_db) {
+        sset_add_and_free(db_filenames, xasprintf("%s/conf.db", ovs_dbdir()));
+    }
 }
 
 static void
diff --git a/ovsdb/ovsdb.7.xml b/ovsdb/ovsdb.7.xml
index 8d9a6ec97caf..efd7a622e2e0 100644
--- a/ovsdb/ovsdb.7.xml
+++ b/ovsdb/ovsdb.7.xml
@@ -461,11 +461,10 @@
     OVSDB also supports online database schema conversion, for any of its
     database service models.  To convert a database online, use
     <code>ovsdb-client convert</code>.  The conversion is atomic, consistent,
-    isolated, and durable.  The OVSDB protocol does not provide a way for the
-    server to notify a client that a database's schema has changed, so
-    currently <code>ovsdb-server</code> disconnects any clients connected when
-    the conversion takes place.  Upon reconnction, clients will discover that
-    the schema has changed.
+    isolated, and durable.  <code>ovsdb-server</code> disconnects any clients
+    connected when the conversion takes place (except clients that use the
+    <code>set_db_change_aware</code> Open vSwitch extension RPC).  Upon
+    reconnection, clients will discover that the schema has changed.
   </p>
 
   <p>
diff --git a/ovsdb/ovsdb.c b/ovsdb/ovsdb.c
index 19755e673861..89f530bcccfb 100644
--- a/ovsdb/ovsdb.c
+++ b/ovsdb/ovsdb.c
@@ -27,6 +27,7 @@
 #include "simap.h"
 #include "table.h"
 #include "transaction.h"
+#include "trigger.h"
 
 struct ovsdb_schema *
 ovsdb_schema_create(const char *name, const char *version, const char *cksum)
@@ -162,7 +163,7 @@ root_set_size(const struct ovsdb_schema *schema)
 }
 
 struct ovsdb_error *
-ovsdb_schema_from_json(struct json *json, struct ovsdb_schema **schemap)
+ovsdb_schema_from_json(const struct json *json, struct ovsdb_schema **schemap)
 {
     struct ovsdb_schema *schema;
     const struct json *name, *tables, *version_json, *cksum;
@@ -361,6 +362,29 @@ ovsdb_create(struct ovsdb_schema *schema)
 }
 
 void
+ovsdb_replace(struct ovsdb *dst, struct ovsdb *src)
+{
+    /* Cancel monitors. */
+    ovsdb_monitor_prereplace_db(dst);
+
+    /* Cancel triggers. */
+    struct ovsdb_trigger *trigger, *next;
+    LIST_FOR_EACH_SAFE (trigger, next, node, &dst->triggers) {
+        ovsdb_trigger_prereplace_db(trigger);
+    }
+
+    struct ovsdb_schema *tmp_schema = dst->schema;
+    dst->schema = src->schema;
+    src->schema = tmp_schema;
+
+    shash_swap(&dst->tables, &src->tables);
+
+    dst->rbac_role = ovsdb_get_table(dst, "RBAC_Role");
+
+    ovsdb_destroy(src);
+}
+
+void
 ovsdb_destroy(struct ovsdb *db)
 {
     if (db) {
diff --git a/ovsdb/ovsdb.h b/ovsdb/ovsdb.h
index 9d915f0f15ae..c3e8f2091e35 100644
--- a/ovsdb/ovsdb.h
+++ b/ovsdb/ovsdb.h
@@ -45,7 +45,7 @@ void ovsdb_schema_destroy(struct ovsdb_schema *);
 struct ovsdb_error *ovsdb_schema_from_file(const char *file_name,
                                            struct ovsdb_schema **)
     OVS_WARN_UNUSED_RESULT;
-struct ovsdb_error *ovsdb_schema_from_json(struct json *,
+struct ovsdb_error *ovsdb_schema_from_json(const struct json *,
                                            struct ovsdb_schema **)
     OVS_WARN_UNUSED_RESULT;
 struct json *ovsdb_schema_to_json(const struct ovsdb_schema *);
@@ -68,6 +68,7 @@ struct ovsdb {
 };
 
 struct ovsdb *ovsdb_create(struct ovsdb_schema *);
+void ovsdb_replace(struct ovsdb *dst, struct ovsdb *src);
 void ovsdb_destroy(struct ovsdb *);
 
 void ovsdb_get_memory_usage(const struct ovsdb *, struct simap *usage);
diff --git a/ovsdb/transaction.c b/ovsdb/transaction.c
index e6e13f650070..e38a174109bb 100644
--- a/ovsdb/transaction.c
+++ b/ovsdb/transaction.c
@@ -806,8 +806,14 @@ update_version(struct ovsdb_txn *txn OVS_UNUSED, struct 
ovsdb_txn_row *txn_row)
     return NULL;
 }
 
-static struct ovsdb_error *
-ovsdb_txn_commit_(struct ovsdb_txn *txn, bool durable)
+static bool
+ovsdb_txn_is_empty(const struct ovsdb_txn *txn)
+{
+    return ovs_list_is_empty(&txn->txn_tables);
+}
+
+struct ovsdb_error * OVS_WARN_UNUSED_RESULT
+ovsdb_txn_start_commit(struct ovsdb_txn *txn)
 {
     struct ovsdb_error *error;
 
@@ -817,29 +823,25 @@ ovsdb_txn_commit_(struct ovsdb_txn *txn, bool durable)
     if (error) {
         return OVSDB_WRAP_BUG("can't happen", error);
     }
-    if (ovs_list_is_empty(&txn->txn_tables)) {
-        ovsdb_txn_abort(txn);
+    if (ovsdb_txn_is_empty(txn)) {
         return NULL;
     }
 
     /* Update reference counts and check referential integrity. */
     error = update_ref_counts(txn);
     if (error) {
-        ovsdb_txn_abort(txn);
         return error;
     }
 
     /* Delete unreferenced, non-root rows. */
     error = for_each_txn_row(txn, collect_garbage);
     if (error) {
-        ovsdb_txn_abort(txn);
         return OVSDB_WRAP_BUG("can't happen", error);
     }
 
     /* Check maximum rows table constraints. */
     error = check_max_rows(txn);
     if (error) {
-        ovsdb_txn_abort(txn);
         return error;
     }
 
@@ -847,14 +849,12 @@ ovsdb_txn_commit_(struct ovsdb_txn *txn, bool durable)
      * integrity. */
     error = for_each_txn_row(txn, assess_weak_refs);
     if (error) {
-        ovsdb_txn_abort(txn);
         return error;
     }
 
     /* Verify that the indexes will still be unique post-transaction. */
     error = for_each_txn_row(txn, check_index_uniqueness);
     if (error) {
-        ovsdb_txn_abort(txn);
         return error;
     }
 
@@ -864,9 +864,16 @@ ovsdb_txn_commit_(struct ovsdb_txn *txn, bool durable)
         return OVSDB_WRAP_BUG("can't happen", error);
     }
 
+    return NULL;
+}
+
+struct ovsdb_error *
+ovsdb_txn_finish_commit(struct ovsdb_txn *txn, bool durable)
+{
     /* Send the commit to each replica. */
     if (txn->db->file) {
-        error = ovsdb_file_commit(txn->db->file, txn, durable);
+        struct ovsdb_error *error = ovsdb_file_commit(txn->db->file, txn,
+                                                      durable);
         if (error) {
             ovsdb_txn_abort(txn);
             return error;
@@ -886,10 +893,12 @@ ovsdb_txn_commit_(struct ovsdb_txn *txn, bool durable)
 struct ovsdb_error *
 ovsdb_txn_commit(struct ovsdb_txn *txn, bool durable)
 {
-   struct ovsdb_error *err;
-
-   PERF(__func__, err = ovsdb_txn_commit_(txn, durable));
-   return err;
+    struct ovsdb_error *error = ovsdb_txn_start_commit(txn);
+    if (error || ovsdb_txn_is_empty(txn)) {
+        ovsdb_txn_abort(txn);
+        return error;
+    }
+    return ovsdb_txn_finish_commit(txn, durable);
 }
 
 void
diff --git a/ovsdb/transaction.h b/ovsdb/transaction.h
index 1ecd15a56a8d..f9b886411bf4 100644
--- a/ovsdb/transaction.h
+++ b/ovsdb/transaction.h
@@ -26,6 +26,11 @@ struct uuid;
 
 struct ovsdb_txn *ovsdb_txn_create(struct ovsdb *);
 void ovsdb_txn_abort(struct ovsdb_txn *);
+
+struct ovsdb_error *ovsdb_txn_start_commit(struct ovsdb_txn *)
+    OVS_WARN_UNUSED_RESULT;
+struct ovsdb_error *ovsdb_txn_finish_commit(struct ovsdb_txn *, bool durable)
+    OVS_WARN_UNUSED_RESULT;
 struct ovsdb_error *ovsdb_txn_commit(struct ovsdb_txn *, bool durable)
     OVS_WARN_UNUSED_RESULT;
 
diff --git a/ovsdb/trigger.c b/ovsdb/trigger.c
index 1b960f5723ec..10b155f8bcb4 100644
--- a/ovsdb/trigger.c
+++ b/ovsdb/trigger.c
@@ -19,41 +19,47 @@
 
 #include <limits.h>
 
+#include "file.h"
+#include "log.h"
 #include "openvswitch/json.h"
 #include "jsonrpc.h"
 #include "ovsdb.h"
+#include "ovsdb-error.h"
 #include "poll-loop.h"
 #include "server.h"
 
+
 static bool ovsdb_trigger_try(struct ovsdb_trigger *, long long int now);
-static void ovsdb_trigger_complete(struct ovsdb_trigger *);
+static void trigger_error(struct ovsdb_trigger *, struct ovsdb_error *);
+static void trigger_success(struct ovsdb_trigger *, struct json *result);
 
-void
+bool
 ovsdb_trigger_init(struct ovsdb_session *session, struct ovsdb *db,
                    struct ovsdb_trigger *trigger,
-                   struct json *request, long long int now,
-                   bool read_only, const char *role,
-                   const char *id)
+                   struct jsonrpc_msg *request, long long int now,
+                   bool read_only, const char *role, const char *id)
 {
+    ovs_assert(!strcmp(request->method, "transact") ||
+               !strcmp(request->method, "convert_db"));
     trigger->session = session;
     trigger->db = db;
     ovs_list_push_back(&trigger->db->triggers, &trigger->node);
     trigger->request = request;
-    trigger->result = NULL;
+    trigger->reply = NULL;
     trigger->created = now;
     trigger->timeout_msec = LLONG_MAX;
     trigger->read_only = read_only;
     trigger->role = nullable_xstrdup(role);
     trigger->id = nullable_xstrdup(id);
-    ovsdb_trigger_try(trigger, now);
+    return ovsdb_trigger_try(trigger, now);
 }
 
 void
 ovsdb_trigger_destroy(struct ovsdb_trigger *trigger)
 {
     ovs_list_remove(&trigger->node);
-    json_destroy(trigger->request);
-    json_destroy(trigger->result);
+    jsonrpc_msg_destroy(trigger->request);
+    jsonrpc_msg_destroy(trigger->reply);
     free(trigger->role);
     free(trigger->id);
 }
@@ -61,30 +67,53 @@ ovsdb_trigger_destroy(struct ovsdb_trigger *trigger)
 bool
 ovsdb_trigger_is_complete(const struct ovsdb_trigger *trigger)
 {
-    return trigger->result != NULL;
+    return trigger->reply != NULL;
 }
 
-struct json *
-ovsdb_trigger_steal_result(struct ovsdb_trigger *trigger)
+struct jsonrpc_msg *
+ovsdb_trigger_steal_reply(struct ovsdb_trigger *trigger)
 {
-    struct json *result = trigger->result;
-    trigger->result = NULL;
-    return result;
+    struct jsonrpc_msg *reply = trigger->reply;
+    trigger->reply = NULL;
+    return reply;
 }
 
 void
+ovsdb_trigger_prereplace_db(struct ovsdb_trigger *trigger)
+{
+    if (!strcmp(trigger->request->method, "transact")) {
+        trigger_error(trigger, ovsdb_error("canceled", NULL));
+    } else if (!strcmp(trigger->request->method, "convert_db")) {
+        /* We don't cancel convert_db requests when a database is being
+         * replaced for two reasons.  First, we expect the administrator to do
+         * some kind of sensible synchronization on conversion requests, that
+         * is, it only really makes sense for the admin to do a single
+         * conversion at a time at a scheduled point.  Second, if we did then
+         * every convert_db request would end up getting canceled since
+         * convert_db itself causes the database to be replaced. */
+    } else {
+        OVS_NOT_REACHED();
+    }
+}
+
+bool
 ovsdb_trigger_run(struct ovsdb *db, long long int now)
 {
     struct ovsdb_trigger *t, *next;
-    bool run_triggers;
 
-    run_triggers = db->run_triggers;
+    bool run_triggers = db->run_triggers;
     db->run_triggers = false;
+
+    bool disconnect_all = false;
+
     LIST_FOR_EACH_SAFE (t, next, node, &db->triggers) {
         if (run_triggers || now - t->created >= t->timeout_msec) {
-            ovsdb_trigger_try(t, now);
+            if (ovsdb_trigger_try(t, now)) {
+                disconnect_all = true;
+            }
         }
     }
+    return disconnect_all;
 }
 
 void
@@ -117,22 +146,71 @@ ovsdb_trigger_wait(struct ovsdb *db, long long int now)
 static bool
 ovsdb_trigger_try(struct ovsdb_trigger *t, long long int now)
 {
-    t->result = ovsdb_execute(t->db, t->session,
-                              t->request, t->read_only,
-                              t->role, t->id,
-                              now - t->created, &t->timeout_msec);
-    if (t->result) {
-        ovsdb_trigger_complete(t);
+    if (!strcmp(t->request->method, "transact")) {
+        struct json *result = ovsdb_execute(t->db, t->session,
+                                            t->request->params, t->read_only,
+                                            t->role, t->id, now - t->created,
+                                            &t->timeout_msec);
+        if (result) {
+            trigger_success(t, result);
+        }
+        return false;
+    } else if (!strcmp(t->request->method, "convert_db")) {
+        /* Validate parameters. */
+        const struct json *params = t->request->params;
+        if (params->type != JSON_ARRAY || params->u.array.n != 2) {
+            trigger_error(t, ovsdb_syntax_error(params, NULL,
+                                                "array expected"));
+            return false;
+        }
+
+        /* Parse new schema and make a converted copy. */
+        const struct json *new_schema_json = params->u.array.elems[1];
+        struct ovsdb_schema *new_schema;
+        struct ovsdb_error *error = ovsdb_schema_from_json(new_schema_json,
+                                                           &new_schema);
+        if (!error && strcmp(new_schema->name, t->db->schema->name)) {
+            error = ovsdb_error(
+                "invalid parameters",
+                "new schema name (%s) does not match database name (%s)",
+                new_schema->name, t->db->schema->name);
+        }
+        if (!error) {
+            error = ovsdb_file_convert(t->db->file, new_schema);
+        }
+        ovsdb_schema_destroy(new_schema);
+        if (error) {
+            trigger_error(t, error);
+            return false;
+        }
+
+        trigger_success(t, json_object_create());
         return true;
     } else {
-        return false;
+        OVS_NOT_REACHED();
     }
 }
 
 static void
-ovsdb_trigger_complete(struct ovsdb_trigger *t)
+ovsdb_trigger_complete(struct ovsdb_trigger *t, struct jsonrpc_msg *reply)
 {
-    ovs_assert(t->result != NULL);
+    ovs_assert(reply && !t->reply);
+    t->reply = reply;
     ovs_list_remove(&t->node);
     ovs_list_push_back(&t->session->completions, &t->node);
 }
+
+static void
+trigger_error(struct ovsdb_trigger *t, struct ovsdb_error *error)
+{
+    struct jsonrpc_msg *reply = jsonrpc_create_error(
+        ovsdb_error_to_json_free(error), t->request->id);
+    ovsdb_trigger_complete(t, reply);
+}
+
+static void
+trigger_success(struct ovsdb_trigger *t, struct json *result)
+{
+    struct jsonrpc_msg *reply = jsonrpc_create_reply(result, t->request->id);
+    ovsdb_trigger_complete(t, reply);
+}
diff --git a/ovsdb/trigger.h b/ovsdb/trigger.h
index 90246a4a42bd..d9df97f31222 100644
--- a/ovsdb/trigger.h
+++ b/ovsdb/trigger.h
@@ -25,8 +25,8 @@ struct ovsdb_trigger {
     struct ovsdb *db;           /* Database on which trigger acts. */
     struct ovs_list node;       /* !result: in db->triggers;
                                  * result: in session->completions. */
-    struct json *request;       /* Database request. */
-    struct json *result;        /* Result (null if none yet). */
+    struct jsonrpc_msg *request; /* Database request. */
+    struct jsonrpc_msg *reply;   /* Result (null if none yet).. */
     long long int created;      /* Time created. */
     long long int timeout_msec; /* Max wait duration. */
     bool read_only;             /* Database is in read only mode. */
@@ -34,17 +34,18 @@ struct ovsdb_trigger {
     char *id;                   /* ID, for role-based access controls. */
 };
 
-void ovsdb_trigger_init(struct ovsdb_session *, struct ovsdb *,
+bool ovsdb_trigger_init(struct ovsdb_session *, struct ovsdb *,
                         struct ovsdb_trigger *,
-                        struct json *request, long long int now,
-                        bool read_only, const char *role,
-                        const char *id);
+                        struct jsonrpc_msg *request, long long int now,
+                        bool read_only, const char *role, const char *id);
 void ovsdb_trigger_destroy(struct ovsdb_trigger *);
 
 bool ovsdb_trigger_is_complete(const struct ovsdb_trigger *);
-struct json *ovsdb_trigger_steal_result(struct ovsdb_trigger *);
+struct jsonrpc_msg *ovsdb_trigger_steal_reply(struct ovsdb_trigger *);
 
-void ovsdb_trigger_run(struct ovsdb *, long long int now);
+void ovsdb_trigger_prereplace_db(struct ovsdb_trigger *);
+
+bool ovsdb_trigger_run(struct ovsdb *, long long int now);
 void ovsdb_trigger_wait(struct ovsdb *, long long int now);
 
 #endif /* ovsdb/trigger.h */
diff --git a/tests/ovsdb-log.at b/tests/ovsdb-log.at
index fb9af12c657d..d295cedd639f 100644
--- a/tests/ovsdb-log.at
+++ b/tests/ovsdb-log.at
@@ -46,7 +46,7 @@ AT_CHECK(
 file: read: {"x":1}
 ]], [ignore])
 AT_CHECK(
-  [test-ovsdb log-io file create read], [1],
+  [test-ovsdb log-io file create-excl read], [1],
   [], [test-ovsdb: I/O error: file: create failed (File exists)
 ])
 AT_CHECK([test -f .file.~lock~])
diff --git a/tests/ovsdb-monitor.at b/tests/ovsdb-monitor.at
index 256868b0248f..e12f4dc326db 100644
--- a/tests/ovsdb-monitor.at
+++ b/tests/ovsdb-monitor.at
@@ -29,11 +29,11 @@ m4_define([OVSDB_CHECK_MONITOR],
    on_exit 'kill `cat ovsdb-server.pid`'
    AT_CAPTURE_FILE([ovsdb-client-log])
    if test "$IS_WIN32" = "yes"; then
-     AT_CHECK([ovsdb-client -vjsonrpc --pidfile 
--log-file="`pwd`"/ovsdb-client-log -d json monitor --format=csv unix:socket $4 
$5 $8 > output 2>/dev/null &],
+     AT_CHECK([ovsdb-client -vjsonrpc --detach --pidfile 
--log-file="`pwd`"/ovsdb-client-log -d json monitor --format=csv unix:socket $4 
$5 $8 > output],
               [0], [ignore], [ignore])
      sleep 1
    else
-     AT_CHECK([ovsdb-client -vjsonrpc --detach --no-chdir --pidfile 
--log-file="`pwd`"/ovsdb-client-log -d json monitor --format=csv unix:socket $4 
$5 $8 > output],
+     AT_CHECK([ovsdb-client -vjsonrpc --detach --no-chdir --pidfile 
--log-file="`pwd`"/ovsdb-client-log -d json monitor --format=csv unix:socket $4 
$5 $8 > output 2>/dev/null],
             [0], [ignore], [ignore])
    fi
    on_exit 'kill `cat ovsdb-client.pid`'
diff --git a/tests/ovsdb-server.at b/tests/ovsdb-server.at
index 795427c5b1e5..280e37fd8525 100644
--- a/tests/ovsdb-server.at
+++ b/tests/ovsdb-server.at
@@ -176,7 +176,7 @@ AT_CHECK([ovsdb-tool create db1 schema1], [0], [ignore], 
[ignore])
 AT_CHECK([ovsdb-tool create db2 schema2], [0], [ignore], [ignore])
 
 # Start ovsdb-server with just a single database - db1.
-AT_CHECK([ovsdb-server --detach --no-chdir --pidfile --remote=punix:db.sock 
db1], [0])
+AT_CHECK([ovsdb-server -vfile -vvlog:off --log-file --detach --no-chdir 
--pidfile --remote=punix:db.sock db1], [0])
 CHECK_DBS([ordinals
 ])
 
@@ -185,8 +185,8 @@ AT_CHECK([ovs-appctl -t ovsdb-server ovsdb-server/remove-db 
ordinals], [0])
 CHECK_DBS([])
 
 # Start monitoring processes.
-AT_CHECK([ovsdb-client --detach --pidfile=ovsdb-client-1.pid --no-headings 
monitor _Server Database name > db-changes-unaware])
-AT_CHECK([ovsdb-client --detach --pidfile=ovsdb-client-2.pid --db-change-aware 
--no-headings monitor _Server Database name > db-changes-aware])
+AT_CHECK([ovsdb-client --detach --pidfile=ovsdb-client-1.pid 
--no-db-change-aware --no-headings monitor _Server Database name > 
db-change-unaware 2>/dev/null])
+AT_CHECK([ovsdb-client --detach --pidfile=ovsdb-client-2.pid --db-change-aware 
--no-headings monitor _Server Database name > db-change-aware 2>/dev/null])
 
 # Add the first database back.
 AT_CHECK([ovs-appctl -t ovsdb-server ovsdb-server/add-db db1], [0])
@@ -258,7 +258,7 @@ CHECK_DBS([constraints
 AT_CHECK([ovsdb-client list-tables unix:db.sock constraints], [0], [ignore], 
[ignore])
 
 # Check the monitoring results.
-AT_CHECK([${PERL} $srcdir/uuidfilt.pl db-changes-aware], [0], [dnl
+AT_CHECK([${PERL} $srcdir/uuidfilt.pl db-change-aware], [0], [dnl
 <0> initial _Server
 
 <1> insert ordinals
@@ -271,7 +271,7 @@ AT_CHECK([${PERL} $srcdir/uuidfilt.pl db-changes-aware], 
[0], [dnl
 
 <3> insert constraints
 ])
-AT_CHECK([${PERL} $srcdir/uuidfilt.pl db-changes-unaware], [0], [dnl
+AT_CHECK([${PERL} $srcdir/uuidfilt.pl db-change-unaware], [0], [dnl
 <0> initial _Server
 ])
 
@@ -781,6 +781,239 @@ _uuid                                name  number
 OVSDB_SERVER_SHUTDOWN
 AT_CLEANUP
 
+AT_SETUP([schema conversion online -- removing a column])
+AT_KEYWORDS([ovsdb server convert])
+on_exit 'kill `cat *.pid`'
+ordinal_schema > schema
+AT_DATA([new-schema], 
+  [[{"name": "ordinals",
+     "tables": {
+       "ordinals": {
+         "columns": {
+           "number": {"type": "integer"}}}}}
+]])
+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 Start two monitors on the 'ordinals' db, one that is database
+dnl change aware and one that is not.
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach 
--pidfile=monitor-ordinals-aware.pid --log-file=monitor-ordinals-aware.log 
--db-change-aware --no-headings monitor ordinals ordinals number name > 
monitor-ordinals-aware.stdout 2> monitor-ordinals-aware.stderr])
+AT_CAPTURE_FILE([monitor-ordinals-aware.stdout])
+AT_CAPTURE_FILE([monitor-ordinals-aware.log])
+AT_CAPTURE_FILE([monitor-ordinals-aware.stderr])
+
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach 
--pidfile=monitor-ordinals-unaware.pid --log-file=monitor-ordinals-unaware.log 
--no-db-change-aware --no-headings monitor ordinals ordinals number name > 
monitor-ordinals-unaware.stdout 2> monitor-ordinals-unaware.stderr])
+AT_CAPTURE_FILE([monitor-ordinals-unaware.stdout])
+AT_CAPTURE_FILE([monitor-ordinals-unaware.log])
+AT_CAPTURE_FILE([monitor-ordinals-unaware.stderr])
+
+dnl Start two monitors on the '_Server' db, one that is database
+dnl change aware and one that is not.
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach 
--pidfile=monitor-server-aware.pid --log-file=monitor-server-aware.log 
--db-change-aware --no-headings monitor _Server Database name > 
monitor-server-aware.stdout 2> monitor-server-aware.stderr])
+AT_CAPTURE_FILE([monitor-server-aware.stdout])
+AT_CAPTURE_FILE([monitor-server-aware.log])
+AT_CAPTURE_FILE([monitor-server-aware.stderr])
+
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach 
--pidfile=monitor-server-unaware.pid --log-file=monitor-server-unaware.log 
--no-db-change-aware --no-headings monitor _Server Database name > 
monitor-server-unaware.stdout 2> monitor-server-unaware.stderr])
+AT_CAPTURE_FILE([monitor-server-unaware.stdout])
+AT_CAPTURE_FILE([monitor-server-unaware.log])
+AT_CAPTURE_FILE([monitor-server-unaware.stderr])
+
+dnl Start two long-running transactions (triggers) on the 'ordinals' db,
+dnl one that is database change aware and one that is not.
+ordinals_txn='[["ordinals",
+               {"op": "wait",
+                "table": "ordinals",
+                "where": [["name", "==", "seven"]],
+                "columns": ["name", "number"],
+                "rows": [],
+                "until": "!="}]]'
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach 
--pidfile=trigger-ordinals-aware.pid --log-file=trigger-ordinals-aware.log 
--db-change-aware transact "$ordinals_txn"  > trigger-ordinals-aware.stdout 2> 
trigger-ordinals-aware.stderr])
+AT_CAPTURE_FILE([trigger-ordinals-aware.stdout])
+AT_CAPTURE_FILE([trigger-ordinals-aware.log])
+AT_CAPTURE_FILE([trigger-ordinals-aware.stderr])
+
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach 
--pidfile=trigger-ordinals-unaware.pid --log-file=trigger-ordinals-unaware.log 
--no-db-change-aware transact  "$ordinals_txn" > 
trigger-ordinals-unaware.stdout 2> trigger-ordinals-unaware.stderr])
+AT_CAPTURE_FILE([trigger-ordinals-unaware.stdout])
+AT_CAPTURE_FILE([trigger-ordinals-unaware.log])
+AT_CAPTURE_FILE([trigger-ordinals-unaware.stderr])
+
+dnl Start two long-running transactions (triggers) on the _Server db,
+dnl one that is database change aware and one that is not.
+server_txn='[["_Server",
+             {"op": "wait",
+              "table": "Database",
+              "where": [["name", "==", "xyzzy"]],
+              "columns": ["name"],
+              "rows": [],
+              "until": "!="}]]'
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach 
--pidfile=trigger-server-aware.pid --log-file=trigger-server-aware.log 
--db-change-aware transact "$server_txn"  > trigger-server-aware.stdout 2> 
trigger-server-aware.stderr])
+AT_CAPTURE_FILE([trigger-server-aware.stdout])
+AT_CAPTURE_FILE([trigger-server-aware.log])
+AT_CAPTURE_FILE([trigger-server-aware.stderr])
+
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach 
--pidfile=trigger-server-unaware.pid --log-file=trigger-server-unaware.log 
--no-db-change-aware transact  "$server_txn" > trigger-server-unaware.stdout 2> 
trigger-server-unaware.stderr])
+AT_CAPTURE_FILE([trigger-server-unaware.stdout])
+AT_CAPTURE_FILE([trigger-server-unaware.log])
+AT_CAPTURE_FILE([trigger-server-unaware.stderr])
+
+dnl Dump out and check the actual database contents.
+AT_CHECK([ovsdb-client dump unix:db.sock ordinals], [0], [stdout])
+AT_CHECK([${PERL} $srcdir/uuidfilt.pl stdout], [0], [dnl
+ordinals table
+_uuid                                name  number
+------------------------------------ ----- ------
+<0> five  5
+<1> four  4
+<2> one   1
+<3> three 3
+<4> two   2
+<5> zero  0
+])
+
+dnl Convert the database.
+AT_CHECK([ovsdb-client convert new-schema])
+
+dnl Verify that the "ordinals" monitors behaved as they should have.
+dnl Both should have exited, for different reasons.
+dnl The db-aware _Server monitor should still be running, but not the unaware
+dnl one.
+for x in unaware aware; do
+    OVS_WAIT_WHILE([test -e monitor-ordinals-$x.pid])
+    AT_CHECK([sort -k 3 monitor-ordinals-$x.stdout | ${PERL} 
$srcdir/uuidfilt.pl], [0],
+[<0> initial 0 zero
+<1> initial 1 one
+<2> initial 2 two
+<3> initial 3 three
+<4> initial 4 four
+<5> initial 5 five
+])
+done
+AT_CHECK([sed 's/.*: //' monitor-ordinals-unaware.stderr], [0], [receive 
failed (End of file)
+])
+AT_CHECK([sed 's/.*: //' monitor-ordinals-aware.stderr], [0], [ordinals 
database was removed
+])
+
+dnl Verify that the _Server monitors behaved as they should have.
+dnl The db-aware monitor should still be running, but not the unaware one.
+for x in aware unaware; do
+    AT_CHECK([sort -k 3 monitor-server-$x.stdout | ${PERL} 
$srcdir/uuidfilt.pl], [0],
+[<0> initial _Server
+<1> initial ordinals
+])
+done
+OVS_WAIT_WHILE([test -e monitor-server-unaware.pid])
+AT_CHECK([sed 's/.*: //' monitor-ordinals-unaware.stderr], [0], [receive 
failed (End of file)
+])
+AT_CHECK([test -e monitor-server-aware.pid])
+
+dnl Verify that the "ordinals" triggers behaved as they should have:
+dnl Both should have exited, for different reasons.
+for x in unaware aware; do
+    OVS_WAIT_WHILE([test -e trigger-ordinals-$x.pid])
+    AT_CHECK([cat trigger-ordinals-$x.stdout])
+done
+AT_CHECK([cat trigger-ordinals-unaware.stderr], [0], [ovsdb-client: 
transaction failed (End of file)
+])
+AT_CHECK([cat trigger-ordinals-aware.stderr], [0], [ovsdb-client: transaction 
returned error: {"error":"canceled"}
+])
+
+dnl Verify that the _Server triggers behaved as they should have:
+dnl The db-aware trigger should still be waiting, but not the unaware one.
+for x in aware unaware; do
+    AT_CHECK([cat trigger-server-$x.stdout])
+done
+OVS_WAIT_WHILE([test -e trigger-server-unaware.pid])
+AT_CHECK([sed 's/.*: //' trigger-ordinals-unaware.stderr], [0], [transaction 
failed (End of file)
+])
+AT_CHECK([test -e trigger-server-aware.pid])
+
+dnl We can't fully re-check the contents of the database log, because the
+dnl order of the records is not predictable, but there should only be 4 lines
+dnl in it now.
+AT_CAPTURE_FILE([db])
+AT_CHECK([test `wc -l < db` -eq 4])
+dnl And check that the dumped data is the same except for the removed column:
+AT_CHECK([ovsdb-client dump unix:db.sock ordinals | ${PERL} 
$srcdir/uuidfilt.pl], [0], [dnl
+ordinals table
+_uuid                                number
+------------------------------------ ------
+<0> 0
+<1> 1
+<2> 2
+<3> 3
+<4> 4
+<5> 5
+])
+dnl Now check that the converted database is still online and can be modified,
+dnl then check that the database log has one more record and that the data
+dnl is as expected.
+AT_CHECK(
+  [[ovsdb-client transact '
+     ["ordinals",
+      {"op": "insert",
+       "table": "ordinals",
+       "row": {"number": 6}},
+      {"op": "comment",
+       "comment": "add row for 6"}]' | ${PERL} $srcdir/uuidfilt.pl]], [0],
+  [[[{"uuid":["uuid","<0>"]},{}]
+]])
+AT_CHECK([test `wc -l < db` -eq 6])
+AT_CHECK([ovsdb-client dump unix:db.sock ordinals | ${PERL} 
$srcdir/uuidfilt.pl], [0], [dnl
+ordinals table
+_uuid                                number
+------------------------------------ ------
+<0> 0
+<1> 1
+<2> 2
+<3> 3
+<4> 4
+<5> 5
+<6> 6
+])
+dnl Now kill and restart the database server to ensure that the data is
+dnl correct on disk as well as in memory.
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+AT_CHECK([[ovsdb-server -vfile -vvlog:off --detach --no-chdir --pidfile 
--log-file --remote=punix:db.sock db]],
+  [0])
+AT_CHECK([ovsdb-client dump unix:db.sock ordinals | ${PERL} 
$srcdir/uuidfilt.pl], [0], [dnl
+ordinals table
+_uuid                                number
+------------------------------------ ------
+<0> 0
+<1> 1
+<2> 2
+<3> 3
+<4> 4
+<5> 5
+<6> 6
+])
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+AT_CLEANUP
+
 AT_SETUP([ovsdb-server combines updates on backlogged connections])
 on_exit 'kill `cat *.pid`'
 
diff --git a/tests/test-ovsdb.c b/tests/test-ovsdb.c
index 9527c60d3620..1b0ec094c5e1 100644
--- a/tests/test-ovsdb.c
+++ b/tests/test-ovsdb.c
@@ -321,6 +321,8 @@ do_log_io(struct ovs_cmdl_context *ctx)
         mode = OVSDB_LOG_READ_WRITE;
     } else if (!strcmp(mode_string, "create")) {
         mode = OVSDB_LOG_CREATE;
+    } else if (!strcmp(mode_string, "create-excl")) {
+        mode = OVSDB_LOG_CREATE_EXCL;
     } else {
         ovs_fatal(0, "unknown log-io open mode \"%s\"", mode_string);
     }
@@ -1477,14 +1479,14 @@ struct test_trigger {
 static void
 do_trigger_dump(struct test_trigger *t, long long int now, const char *title)
 {
-    struct json *result;
+    struct jsonrpc_msg *reply;
     char *s;
 
-    result = ovsdb_trigger_steal_result(&t->trigger);
-    s = json_to_string(result, JSSF_SORT);
+    reply = ovsdb_trigger_steal_reply(&t->trigger);
+    s = json_to_string(reply->result, JSSF_SORT);
     printf("t=%lld: trigger %d (%s): %s\n", now, t->number, title, s);
     free(s);
-    json_destroy(result);
+    jsonrpc_msg_destroy(reply);
     ovsdb_trigger_destroy(&t->trigger);
     free(t);
 }
@@ -1524,8 +1526,10 @@ do_trigger(struct ovs_cmdl_context *ctx)
             json_destroy(params);
         } else {
             struct test_trigger *t = xmalloc(sizeof *t);
-            ovsdb_trigger_init(&session, db, &t->trigger, params, now, false,
-                               NULL, NULL);
+            ovsdb_trigger_init(&session, db, &t->trigger,
+                               jsonrpc_create_request("transact", params,
+                                                      NULL),
+                               now, false, NULL, NULL);
             t->number = number++;
             if (ovsdb_trigger_is_complete(&t->trigger)) {
                 do_trigger_dump(t, now, "immediate");
-- 
2.10.2

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to