diff --git a/NEWS b/NEWS
index ee5c2c393..c22466818 100644
--- a/NEWS
+++ b/NEWS
@@ -18,6 +18,11 @@ OVN v20.09.0 - xx xxx xxxx
    - Added support for external ip based NAT. Now, besides the logical ip,
      external ips will also decide if a packet will be NATed or not.
    - Added support for VXLAN encapsulation (not just for ramp/VTEP switches).
+   - Added support for multiple ovn-controller instances on the same host
+     (virtual chassis). Now all external-ids:* configuration options can be
+     customized for each controller instance running on the same host. The only
+     option that is not available per chassis is external-ids:system-id, which
+     stands for the chassis name and can be passed via config file or CLI (-n).
 
 OVN v20.06.0
 --------------------------
diff --git a/controller/chassis.c b/controller/chassis.c
index a365188e8..989ec5e1a 100644
--- a/controller/chassis.c
+++ b/controller/chassis.c
@@ -125,9 +125,10 @@ chassis_register_ovs_idl(struct ovsdb_idl *ovs_idl)
 }
 
 static const char *
-get_hostname(const struct smap *ext_ids)
+get_hostname(const struct smap *ext_ids, const char *chassis_id)
 {
-    const char *hostname = smap_get_def(ext_ids, "hostname", "");
+    const char *hostname = get_chassis_external_id_value(
+        ext_ids, chassis_id, "hostname", "");
 
     if (strlen(hostname) == 0) {
         static char hostname_[HOST_NAME_MAX + 1];
@@ -143,39 +144,45 @@ get_hostname(const struct smap *ext_ids)
 }
 
 static const char *
-get_bridge_mappings(const struct smap *ext_ids)
+get_bridge_mappings(const struct smap *ext_ids, const char *chassis_id)
 {
-    return smap_get_def(ext_ids, "ovn-bridge-mappings", "");
+    return get_chassis_external_id_value(
+        ext_ids, chassis_id, "ovn-bridge-mappings", "");
 }
 
 const char *
-get_chassis_mac_mappings(const struct smap *ext_ids)
+get_chassis_mac_mappings(const struct smap *ext_ids, const char *chassis_id)
 {
-    return smap_get_def(ext_ids, "ovn-chassis-mac-mappings", "");
+    return get_chassis_external_id_value(
+        ext_ids, chassis_id, "ovn-chassis-mac-mappings", "");
 }
 
 static const char *
-get_cms_options(const struct smap *ext_ids)
+get_cms_options(const struct smap *ext_ids, const char *chassis_id)
 {
-    return smap_get_def(ext_ids, "ovn-cms-options", "");
+    return get_chassis_external_id_value(
+        ext_ids, chassis_id, "ovn-cms-options", "");
 }
 
 static const char *
-get_monitor_all(const struct smap *ext_ids)
+get_monitor_all(const struct smap *ext_ids, const char *chassis_id)
 {
-    return smap_get_def(ext_ids, "ovn-monitor-all", "false");
+    return get_chassis_external_id_value(
+        ext_ids, chassis_id, "ovn-monitor-all", "false");
 }
 
 static const char *
-get_enable_lflow_cache(const struct smap *ext_ids)
+get_enable_lflow_cache(const struct smap *ext_ids, const char *chassis_id)
 {
-    return smap_get_def(ext_ids, "ovn-enable-lflow-cache", "true");
+    return get_chassis_external_id_value(
+        ext_ids, chassis_id, "ovn-enable-lflow-cache", "true");
 }
 
 static const char *
-get_encap_csum(const struct smap *ext_ids)
+get_encap_csum(const struct smap *ext_ids, const char *chassis_id)
 {
-    return smap_get_def(ext_ids, "ovn-encap-csum", "true");
+    return get_chassis_external_id_value(
+        ext_ids, chassis_id, "ovn-encap-csum", "true");
 }
 
 static const char *
@@ -189,9 +196,10 @@ get_datapath_type(const struct ovsrec_bridge *br_int)
 }
 
 static bool
-get_is_interconn(const struct smap *ext_ids)
+get_is_interconn(const struct smap *ext_ids, const char *chassis_id)
 {
-    return smap_get_bool(ext_ids, "ovn-is-interconn", false);
+    return get_chassis_external_id_value_bool(
+        ext_ids, chassis_id, "ovn-is-interconn", false);
 }
 
 static void
@@ -278,22 +286,27 @@ chassis_parse_ovs_config(const struct ovsrec_open_vswitch_table *ovs_table,
         return false;
     }
 
-    const char *encap_type = smap_get(&cfg->external_ids, "ovn-encap-type");
-    const char *encap_ips = smap_get(&cfg->external_ids, "ovn-encap-ip");
+    const char *chassis_id = get_ovs_chassis_id(cfg);
+    const struct smap *ext_ids = &cfg->external_ids;
+
+    const char *encap_type = get_chassis_external_id_value(
+        ext_ids, chassis_id, "ovn-encap-type", NULL);
+    const char *encap_ips = get_chassis_external_id_value(
+        ext_ids, chassis_id, "ovn-encap-ip", NULL);
     if (!encap_type || !encap_ips) {
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
         VLOG_INFO_RL(&rl, "Need to specify an encap type and ip");
         return false;
     }
 
-    ovs_cfg->hostname = get_hostname(&cfg->external_ids);
-    ovs_cfg->bridge_mappings = get_bridge_mappings(&cfg->external_ids);
+    ovs_cfg->hostname = get_hostname(ext_ids, chassis_id);
+    ovs_cfg->bridge_mappings = get_bridge_mappings(ext_ids, chassis_id);
     ovs_cfg->datapath_type = get_datapath_type(br_int);
-    ovs_cfg->encap_csum = get_encap_csum(&cfg->external_ids);
-    ovs_cfg->cms_options = get_cms_options(&cfg->external_ids);
-    ovs_cfg->monitor_all = get_monitor_all(&cfg->external_ids);
-    ovs_cfg->chassis_macs = get_chassis_mac_mappings(&cfg->external_ids);
-    ovs_cfg->enable_lflow_cache = get_enable_lflow_cache(&cfg->external_ids);
+    ovs_cfg->encap_csum = get_encap_csum(ext_ids, chassis_id);
+    ovs_cfg->cms_options = get_cms_options(ext_ids, chassis_id);
+    ovs_cfg->monitor_all = get_monitor_all(ext_ids, chassis_id);
+    ovs_cfg->chassis_macs = get_chassis_mac_mappings(ext_ids, chassis_id);
+    ovs_cfg->enable_lflow_cache = get_enable_lflow_cache(ext_ids, chassis_id);
 
     if (!chassis_parse_ovs_encap_type(encap_type, &ovs_cfg->encap_type_set)) {
         return false;
@@ -311,7 +324,7 @@ chassis_parse_ovs_config(const struct ovsrec_open_vswitch_table *ovs_table,
         sset_destroy(&ovs_cfg->encap_ip_set);
     }
 
-    ovs_cfg->is_interconn = get_is_interconn(&cfg->external_ids);
+    ovs_cfg->is_interconn = get_is_interconn(ext_ids, chassis_id);
 
     return true;
 }
@@ -348,7 +361,7 @@ chassis_other_config_changed(const char *bridge_mappings,
                              const struct sbrec_chassis *chassis_rec)
 {
     const char *chassis_bridge_mappings =
-        get_bridge_mappings(&chassis_rec->other_config);
+        get_bridge_mappings(&chassis_rec->other_config, NULL);
 
     if (strcmp(bridge_mappings, chassis_bridge_mappings)) {
         return true;
@@ -362,28 +375,28 @@ chassis_other_config_changed(const char *bridge_mappings,
     }
 
     const char *chassis_cms_options =
-        get_cms_options(&chassis_rec->other_config);
+        get_cms_options(&chassis_rec->other_config, NULL);
 
     if (strcmp(cms_options, chassis_cms_options)) {
         return true;
     }
 
     const char *chassis_monitor_all =
-        get_monitor_all(&chassis_rec->other_config);
+        get_monitor_all(&chassis_rec->other_config, NULL);
 
     if (strcmp(monitor_all, chassis_monitor_all)) {
         return true;
     }
 
     const char *chassis_enable_lflow_cache =
-        get_enable_lflow_cache(&chassis_rec->other_config);
+        get_enable_lflow_cache(&chassis_rec->other_config, NULL);
 
     if (strcmp(enable_lflow_cache, chassis_enable_lflow_cache)) {
         return true;
     }
 
     const char *chassis_mac_mappings =
-        get_chassis_mac_mappings(&chassis_rec->other_config);
+        get_chassis_mac_mappings(&chassis_rec->other_config, NULL);
     if (strcmp(chassis_macs, chassis_mac_mappings)) {
         return true;
     }
@@ -791,7 +804,7 @@ chassis_get_mac(const struct sbrec_chassis *chassis_rec,
                 struct eth_addr *chassis_mac)
 {
     const char *tokens
-        = get_chassis_mac_mappings(&chassis_rec->other_config);
+        = get_chassis_mac_mappings(&chassis_rec->other_config, NULL);
     if (!tokens[0]) {
        return false;
     }
diff --git a/controller/chassis.h b/controller/chassis.h
index 220f726b9..c7345f0fa 100644
--- a/controller/chassis.h
+++ b/controller/chassis.h
@@ -49,7 +49,8 @@ bool chassis_get_mac(const struct sbrec_chassis *chassis,
                      const char *bridge_mapping,
                      struct eth_addr *chassis_mac);
 const char *chassis_get_id(void);
-const char * get_chassis_mac_mappings(const struct smap *ext_ids);
+const char * get_chassis_mac_mappings(const struct smap *ext_ids,
+                                      const char *chassis_id);
 
 
 #endif /* controller/chassis.h */
diff --git a/controller/encaps.c b/controller/encaps.c
index 7eac4bb06..7387d4d5d 100644
--- a/controller/encaps.c
+++ b/controller/encaps.c
@@ -291,9 +291,31 @@ chassis_tzones_overlap(const struct sset *transport_zones,
     return false;
 }
 
+static bool
+is_tunnel_type(const char *port_type)
+{
+    static const char *tunnel_types[3] = { "geneve", "vxlan", "stt" };
+    for (size_t t = 0; t < 3; t++) {
+        if (!strcmp(port_type, tunnel_types[t])) {
+            return true;
+        }
+    }
+    return false;
+}
+
+static bool
+is_tunnel_port(const struct ovsrec_port *port)
+{
+    for (size_t i = 0; i < port->n_interfaces; i++) {
+        if (is_tunnel_type(port->interfaces[i]->type)) {
+            return true;
+        }
+    }
+    return false;
+}
+
 void
 encaps_run(struct ovsdb_idl_txn *ovs_idl_txn,
-           const struct ovsrec_bridge_table *bridge_table,
            const struct ovsrec_bridge *br_int,
            const struct sbrec_chassis_table *chassis_table,
            const struct sbrec_chassis *this_chassis,
@@ -305,7 +327,6 @@ encaps_run(struct ovsdb_idl_txn *ovs_idl_txn,
     }
 
     const struct sbrec_chassis *chassis_rec;
-    const struct ovsrec_bridge *br;
 
     struct tunnel_ctx tc = {
         .chassis = SHASH_INITIALIZER(&tc.chassis),
@@ -320,28 +341,29 @@ encaps_run(struct ovsdb_idl_txn *ovs_idl_txn,
 
     /* Collect all port names into tc.port_names.
      *
-     * Collect all the OVN-created tunnels into tc.tunnel_hmap. */
-    OVSREC_BRIDGE_TABLE_FOR_EACH (br, bridge_table) {
-        for (size_t i = 0; i < br->n_ports; i++) {
-            const struct ovsrec_port *port = br->ports[i];
-            sset_add(&tc.port_names, port->name);
-
-            /*
-             * note that the id here is not just the chassis name, but the
-             * combination of <chassis_name><delim><encap_ip>
-             */
-            const char *id = smap_get(&port->external_ids, "ovn-chassis-id");
-            if (id) {
-                if (!shash_find(&tc.chassis, id)) {
-                    struct chassis_node *chassis = xzalloc(sizeof *chassis);
-                    chassis->bridge = br;
-                    chassis->port = port;
-                    shash_add_assert(&tc.chassis, id, chassis);
-                } else {
-                    /* Duplicate port for ovn-chassis-id.  Arbitrarily choose
-                     * to delete this one. */
-                    ovsrec_bridge_update_ports_delvalue(br, port);
-                }
+     * Collect all OVN-created tunnels of the bridge into tc.tunnel_hmap. */
+    for (size_t i = 0; i < br_int->n_ports; i++) {
+        const struct ovsrec_port *port = br_int->ports[i];
+        if (!is_tunnel_port(port)) {
+            continue;
+        }
+        sset_add(&tc.port_names, port->name);
+
+        /*
+         * note that the id here is not just the chassis name, but the
+         * combination of <chassis_name><delim><encap_ip>
+         */
+        const char *id = smap_get(&port->external_ids, "ovn-chassis-id");
+        if (id) {
+            if (!shash_find(&tc.chassis, id)) {
+                struct chassis_node *chassis = xzalloc(sizeof *chassis);
+                chassis->bridge = br_int;
+                chassis->port = port;
+                shash_add_assert(&tc.chassis, id, chassis);
+            } else {
+                /* Duplicate port for ovn-chassis-id.  Arbitrarily choose
+                 * to delete this one. */
+                ovsrec_bridge_update_ports_delvalue(br_int, port);
             }
         }
     }
@@ -381,6 +403,7 @@ encaps_run(struct ovsdb_idl_txn *ovs_idl_txn,
         shash_delete(&tc.chassis, node);
         free(chassis);
     }
+
     shash_destroy(&tc.chassis);
     sset_destroy(&tc.port_names);
 }
@@ -400,6 +423,9 @@ encaps_cleanup(struct ovsdb_idl_txn *ovs_idl_txn,
         = xmalloc(sizeof *br_int->ports * br_int->n_ports);
     size_t n = 0;
     for (size_t i = 0; i < br_int->n_ports; i++) {
+        if (!is_tunnel_port(br_int->ports[i])) {
+            continue;
+        }
         if (!smap_get(&br_int->ports[i]->external_ids, "ovn-chassis-id")) {
             ports[n++] = br_int->ports[i];
         }
diff --git a/controller/encaps.h b/controller/encaps.h
index f488393c4..aff85097f 100644
--- a/controller/encaps.h
+++ b/controller/encaps.h
@@ -30,7 +30,6 @@ struct sset;
 
 void encaps_register_ovs_idl(struct ovsdb_idl *);
 void encaps_run(struct ovsdb_idl_txn *ovs_idl_txn,
-                const struct ovsrec_bridge_table *,
                 const struct ovsrec_bridge *br_int,
                 const struct sbrec_chassis_table *,
                 const struct sbrec_chassis *,
diff --git a/controller/ovn-controller.8.xml b/controller/ovn-controller.8.xml
index 16bc47b20..c181a0fa6 100644
--- a/controller/ovn-controller.8.xml
+++ b/controller/ovn-controller.8.xml
@@ -235,6 +235,19 @@
       </dd>
     </dl>
 
+    <p>
+      Note that every <code>external_ids:*</code> key listed above has its
+      <code>external_ids:*-chassis_name</code> counterpart keys that allow to
+      configure values specific to chassis running on the same OVSDB. For
+      example, if two chassis named <code>blue</code> and <code>red</code> are
+      available on the same host, then an admin may configure different
+      <code>ovn-cms-options</code> for each of them by setting
+      <code>external_ids:ovn-cms-options-blue</code> and
+      <code>external_ids:ovn-cms-options-red</code> keys in the database. The
+      only key that is not available for per-chassis configuration is
+      <code>external_ids:system-id</code>.
+    </p>
+
     <p>
       <code>ovn-controller</code> reads the following values from the
       <code>Open_vSwitch</code> database of the local OVS instance:
@@ -286,7 +299,9 @@
         The presence of this key identifies a tunnel port within the
         integration bridge as one created by <code>ovn-controller</code> to
         reach a remote chassis.  Its value is the chassis ID of the remote
-        chassis.
+        chassis. Alternatively, for patch ports, the key identifies the name of
+        the chassis that owns it, in case of multiple virtual chassis running
+        on the same host.
       </dd>
 
       <dt>
diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index 8d8c678e5..16dc9e28f 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -18,10 +18,14 @@
 #include "ovn-controller.h"
 
 #include <errno.h>
+#include <fcntl.h>
 #include <getopt.h>
 #include <signal.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
 
 #include "bfd.h"
 #include "binding.h"
@@ -46,6 +50,7 @@
 #include "lib/extend-table.h"
 #include "lib/ip-mcast-index.h"
 #include "lib/mcast-group-index.h"
+#include "lib/ovn-dirs.h"
 #include "lib/ovn-sb-idl.h"
 #include "lib/ovn-util.h"
 #include "patch.h"
@@ -85,6 +90,12 @@ static unixctl_cb_func debug_delay_nb_cfg_report;
 
 #define CONTROLLER_LOOP_STOPWATCH_NAME "ovn-controller-flow-generation"
 
+/* These variables never change after initialization and can be safely used in
+ * I-P engine. If later we decide to allow to dynamically change them, I-P
+ * machinery will need some adjustments. */
+static char *controller_chassis = NULL;
+static char *system_id_override = NULL;
+
 static char *parse_options(int argc, char *argv[]);
 OVS_NO_RETURN static void usage(void);
 
@@ -260,7 +271,9 @@ out:
 static const char *
 br_int_name(const struct ovsrec_open_vswitch *cfg)
 {
-    return smap_get_def(&cfg->external_ids, "ovn-bridge", DEFAULT_BRIDGE_NAME);
+    return get_chassis_external_id_value(
+        &cfg->external_ids, get_ovs_chassis_id(cfg),
+        "ovn-bridge", DEFAULT_BRIDGE_NAME);
 }
 
 static const struct ovsrec_bridge *
@@ -361,8 +374,9 @@ process_br_int(struct ovsdb_idl_txn *ovs_idl_txn,
         const struct ovsrec_open_vswitch *cfg;
         cfg = ovsrec_open_vswitch_table_first(ovs_table);
         ovs_assert(cfg);
-        const char *datapath_type = smap_get(&cfg->external_ids,
-                                             "ovn-bridge-datapath-type");
+        const char *datapath_type = get_chassis_external_id_value(
+            &cfg->external_ids, get_ovs_chassis_id(cfg),
+            "ovn-bridge-datapath-type", NULL);
         /* Check for the datapath_type and set it only if it is defined in
          * cfg. */
         if (datapath_type && strcmp(br_int->datapath_type, datapath_type)) {
@@ -372,17 +386,46 @@ process_br_int(struct ovsdb_idl_txn *ovs_idl_txn,
     return br_int;
 }
 
-static const char *
-get_ovs_chassis_id(const struct ovsrec_open_vswitch_table *ovs_table)
+static char *get_file_system_id_override(void)
 {
-    const struct ovsrec_open_vswitch *cfg
-        = ovsrec_open_vswitch_table_first(ovs_table);
+    char *ret = NULL;
+    char *filename = xasprintf("%s/system-id-override", ovn_sysconfdir());
+    errno = 0;
+    int fd = open(filename, O_RDONLY);
+    if (fd != -1) {
+        char file_system_id[64];
+        int nread = read(fd, file_system_id, sizeof file_system_id);
+        if (nread) {
+            file_system_id[nread] = '\0';
+            if (file_system_id[nread - 1] == '\n') {
+                file_system_id[nread - 1] = '\0';
+            }
+            ret = xstrdup(file_system_id);
+        }
+        close(fd);
+    }
+
+    free(filename);
+    return ret;
+}
+
+const char *
+get_ovs_chassis_id(const struct ovsrec_open_vswitch *cfg)
+{
+    if (controller_chassis) {
+        return controller_chassis;
+    }
+
+    if (system_id_override) {
+        return system_id_override;
+    }
+
     const char *chassis_id = cfg ? smap_get(&cfg->external_ids, "system-id")
                                  : NULL;
-
     if (!chassis_id) {
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "'system-id' in Open_vSwitch database is missing.");
+        VLOG_WARN_RL(&rl, "Failed to detect system-id, "
+                          "configuration not found.");
     }
 
     return chassis_id;
@@ -477,10 +520,12 @@ static int
 get_ofctrl_probe_interval(struct ovsdb_idl *ovs_idl)
 {
     const struct ovsrec_open_vswitch *cfg = ovsrec_open_vswitch_first(ovs_idl);
-    return !cfg ? OFCTRL_DEFAULT_PROBE_INTERVAL_SEC :
-                  smap_get_int(&cfg->external_ids,
-                               "ovn-openflow-probe-interval",
-                               OFCTRL_DEFAULT_PROBE_INTERVAL_SEC);
+    if (!cfg) {
+        return OFCTRL_DEFAULT_PROBE_INTERVAL_SEC;
+    }
+    return get_chassis_external_id_value_int(
+        &cfg->external_ids, get_ovs_chassis_id(cfg),
+        "ovn-openflow-probe-interval", OFCTRL_DEFAULT_PROBE_INTERVAL_SEC);
 }
 
 /* Retrieves the pointer to the OVN Southbound database from 'ovs_idl' and
@@ -496,18 +541,21 @@ update_sb_db(struct ovsdb_idl *ovs_idl, struct ovsdb_idl *ovnsb_idl,
     }
 
     /* Set remote based on user configuration. */
-    const char *remote = smap_get(&cfg->external_ids, "ovn-remote");
+    const char *chassis_id = get_ovs_chassis_id(cfg);
+    const char *remote = get_chassis_external_id_value(
+        &cfg->external_ids, chassis_id, "ovn-remote", NULL);
     ovsdb_idl_set_remote(ovnsb_idl, remote, true);
 
     /* Set probe interval, based on user configuration and the remote. */
     int default_interval = (remote && !stream_or_pstream_needs_probes(remote)
                             ? 0 : DEFAULT_PROBE_INTERVAL_MSEC);
-    int interval = smap_get_int(&cfg->external_ids,
-                                "ovn-remote-probe-interval", default_interval);
+    int interval = get_chassis_external_id_value_int(
+        &cfg->external_ids, chassis_id, "ovn-remote-probe-interval",
+        default_interval);
     ovsdb_idl_set_probe_interval(ovnsb_idl, interval);
 
-    bool monitor_all = smap_get_bool(&cfg->external_ids, "ovn-monitor-all",
-                                     false);
+    bool monitor_all = get_chassis_external_id_value_bool(
+        &cfg->external_ids, chassis_id, "ovn-monitor-all", false);
     if (monitor_all) {
         /* Always call update_sb_monitors when monitor_all is true.
          * Otherwise, don't call it here, because there would be unnecessary
@@ -1166,7 +1214,9 @@ init_binding_ctx(struct engine_node *node,
     struct ovsrec_bridge_table *bridge_table =
         (struct ovsrec_bridge_table *)EN_OVSDB_GET(
             engine_get_input("OVS_bridge", node));
-    const char *chassis_id = get_ovs_chassis_id(ovs_table);
+    const struct ovsrec_open_vswitch *cfg =
+        ovsrec_open_vswitch_table_first(ovs_table);
+    const char *chassis_id = get_ovs_chassis_id(cfg);
     const struct ovsrec_bridge *br_int = get_br_int(bridge_table, ovs_table);
 
     ovs_assert(br_int && chassis_id);
@@ -2432,6 +2482,10 @@ main(int argc, char *argv[])
     exiting = false;
     restart = false;
     bool sb_monitor_all = false;
+
+    /* Read from system-id-override file once on startup. */
+    system_id_override = get_file_system_id_override();
+
     while (!exiting) {
         /* If we're paused just run the unixctl server and skip most of the
          * processing loop.
@@ -2498,7 +2552,9 @@ main(int argc, char *argv[])
                 sbrec_chassis_private_table_get(ovnsb_idl_loop.idl);
             const struct ovsrec_bridge *br_int =
                 process_br_int(ovs_idl_txn, bridge_table, ovs_table);
-            const char *chassis_id = get_ovs_chassis_id(ovs_table);
+            const struct ovsrec_open_vswitch *cfg =
+                ovsrec_open_vswitch_table_first(ovs_table);
+            const char *chassis_id = get_ovs_chassis_id(cfg);
             const struct sbrec_chassis *chassis = NULL;
             const struct sbrec_chassis_private *chassis_private = NULL;
             if (chassis_id) {
@@ -2518,7 +2574,7 @@ main(int argc, char *argv[])
 
                 if (chassis) {
                     encaps_run(ovs_idl_txn,
-                               bridge_table, br_int,
+                               br_int,
                                sbrec_chassis_table_get(ovnsb_idl_loop.idl),
                                chassis,
                                sbrec_sb_global_first(ovnsb_idl_loop.idl),
@@ -2779,6 +2835,8 @@ loop_done:
     ovsdb_idl_loop_destroy(&ovs_idl_loop);
     ovsdb_idl_loop_destroy(&ovnsb_idl_loop);
 
+    free(controller_chassis);
+    free(system_id_override);
     free(ovs_remote);
     service_stop();
 
@@ -2804,6 +2862,7 @@ parse_options(int argc, char *argv[])
         STREAM_SSL_LONG_OPTIONS,
         {"peer-ca-cert", required_argument, NULL, OPT_PEER_CA_CERT},
         {"bootstrap-ca-cert", required_argument, NULL, OPT_BOOTSTRAP_CA_CERT},
+        {"chassis", required_argument, NULL, 'n'},
         {NULL, 0, NULL, 0}
     };
     char *short_options = ovs_cmdl_long_options_to_short_options(long_options);
@@ -2836,6 +2895,10 @@ parse_options(int argc, char *argv[])
             stream_ssl_set_ca_cert_file(optarg, true);
             break;
 
+        case 'n':
+            controller_chassis = xstrdup(optarg);
+            break;
+
         case '?':
             exit(EXIT_FAILURE);
 
diff --git a/controller/ovn-controller.h b/controller/ovn-controller.h
index 5d9466880..9994dd777 100644
--- a/controller/ovn-controller.h
+++ b/controller/ovn-controller.h
@@ -21,6 +21,7 @@
 #include "lib/ovn-sb-idl.h"
 
 struct ovsrec_bridge_table;
+struct ovsrec_open_vswitch;
 
 /* Linux supports a maximum of 64K zones, which seems like a fine default. */
 #define MAX_CT_ZONES 65535
@@ -87,4 +88,7 @@ enum chassis_tunnel_type {
 
 uint32_t get_tunnel_type(const char *name);
 
+const char *get_ovs_chassis_id(const struct ovsrec_open_vswitch *cfg);
+bool is_concurrent_chassis(const struct ovsrec_open_vswitch *cfg);
+
 #endif /* controller/ovn-controller.h */
diff --git a/controller/patch.c b/controller/patch.c
index a2a7bcd79..3b3df278c 100644
--- a/controller/patch.c
+++ b/controller/patch.c
@@ -76,6 +76,7 @@ create_patch_port(struct ovsdb_idl_txn *ovs_idl_txn,
                   const char *key, const char *value,
                   const struct ovsrec_bridge *src, const char *src_name,
                   const struct ovsrec_bridge *dst, const char *dst_name,
+                  const char *chassis_name,
                   struct shash *existing_ports)
 {
     for (size_t i = 0; i < src->n_ports; i++) {
@@ -101,7 +102,8 @@ create_patch_port(struct ovsdb_idl_txn *ovs_idl_txn,
     port = ovsrec_port_insert(ovs_idl_txn);
     ovsrec_port_set_name(port, src_name);
     ovsrec_port_set_interfaces(port, &iface, 1);
-    const struct smap ids = SMAP_CONST1(&ids, key, value);
+    const struct smap ids = SMAP_CONST2(&ids, key, value,
+                                        "ovn-chassis-id", chassis_name);
     ovsrec_port_set_external_ids(port, &ids);
 
     struct ovsrec_port **ports;
@@ -157,7 +159,9 @@ add_ovs_bridge_mappings(const struct ovsrec_open_vswitch_table *ovs_table,
         const char *mappings_cfg;
         char *cur, *next, *start;
 
-        mappings_cfg = smap_get(&cfg->external_ids, "ovn-bridge-mappings");
+        mappings_cfg = get_chassis_external_id_value(
+            &cfg->external_ids, get_ovs_chassis_id(cfg),
+            "ovn-bridge-mappings", NULL);
         if (!mappings_cfg || !mappings_cfg[0]) {
             return;
         }
@@ -269,9 +273,11 @@ add_bridge_mappings(struct ovsdb_idl_txn *ovs_idl_txn,
         char *name1 = patch_port_name(br_int->name, binding->logical_port);
         char *name2 = patch_port_name(binding->logical_port, br_int->name);
         create_patch_port(ovs_idl_txn, patch_port_id, binding->logical_port,
-                          br_int, name1, br_ln, name2, existing_ports);
+                          br_int, name1, br_ln, name2, chassis->name,
+                          existing_ports);
         create_patch_port(ovs_idl_txn, patch_port_id, binding->logical_port,
-                          br_ln, name2, br_int, name1, existing_ports);
+                          br_ln, name2, br_int, name1, chassis->name,
+                          existing_ports);
         free(name1);
         free(name2);
     }
@@ -323,6 +329,12 @@ patch_run(struct ovsdb_idl_txn *ovs_idl_txn,
     SHASH_FOR_EACH_SAFE (port_node, port_next_node, &existing_ports) {
         port = port_node->data;
         shash_delete(&existing_ports, port_node);
+
+        const char *port_chassis = smap_get(&port->external_ids,
+                                            "ovn-chassis-id");
+        if (port_chassis && strcmp(port_chassis, chassis->name)) {
+            continue;
+        }
         remove_port(bridge_table, port);
     }
     shash_destroy(&existing_ports);
diff --git a/controller/physical.c b/controller/physical.c
index a7f3efd2f..306f7f912 100644
--- a/controller/physical.c
+++ b/controller/physical.c
@@ -432,7 +432,7 @@ populate_remote_chassis_macs(const struct sbrec_chassis *my_chassis,
         }
 
         const char *tokens
-            = get_chassis_mac_mappings(&chassis->other_config);
+            = get_chassis_mac_mappings(&chassis->other_config, NULL);
 
         if (!strlen(tokens)) {
             continue;
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index cdb5e18fb..3193b73db 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -641,3 +641,53 @@ str_tolower(const char *orig)
 
     return copy;
 }
+
+const char *
+get_chassis_external_id_value(const struct smap *external_ids,
+                              const char *chassis_id, const char *option_key,
+                              const char *def)
+{
+    const char *option_value = NULL;
+    if (chassis_id != NULL) {
+        char *chassis_option_key = xasprintf("%s-%s", option_key, chassis_id);
+        option_value = smap_get(external_ids, chassis_option_key);
+        free(chassis_option_key);
+    }
+    if (!option_value) {
+        option_value = smap_get_def(external_ids, option_key, def);
+    }
+    return option_value;
+}
+
+int
+get_chassis_external_id_value_int(const struct smap *external_ids,
+                                  const char *chassis_id,
+                                  const char *option_key,
+                                  int def)
+{
+    const char *value = get_chassis_external_id_value(
+        external_ids, chassis_id, option_key, NULL);
+
+    int i_value;
+    if (!value || !str_to_int(value, 10, &i_value)) {
+        return def;
+    }
+
+    return i_value;
+}
+
+bool
+get_chassis_external_id_value_bool(const struct smap *external_ids,
+                                   const char *chassis_id,
+                                   const char *option_key,
+                                   bool def)
+{
+    const char *value = get_chassis_external_id_value(
+        external_ids, chassis_id, option_key, "");
+
+    if (def) {
+        return strcasecmp("false", value) != 0;
+    } else {
+        return !strcasecmp("true", value);
+    }
+}
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index d9aadcbc0..a1b8f47b5 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -18,6 +18,7 @@
 
 #include "lib/packets.h"
 #include "include/ovn/version.h"
+#include "smap.h"
 
 #define ovn_set_program_name(name) \
     ovs_set_program_name(name, OVN_PACKAGE_VERSION)
@@ -152,6 +153,23 @@ char *normalize_ipv4_prefix(ovs_be32 ipv4, unsigned int plen);
 char *normalize_ipv6_prefix(struct in6_addr ipv6, unsigned int plen);
 char *normalize_v46_prefix(const struct v46_ip *prefix, unsigned int plen);
 
+const char *
+get_chassis_external_id_value(const struct smap *external_ids,
+                              const char *chassis_id, const char *option_key,
+                              const char *def);
+
+int
+get_chassis_external_id_value_int(const struct smap *external_ids,
+                                  const char *chassis_id,
+                                  const char *option_key,
+                                  int def);
+
+bool
+get_chassis_external_id_value_bool(const struct smap *external_ids,
+                                   const char *chassis_id,
+                                   const char *option_key,
+                                   bool def);
+
 /* Returns a lowercase copy of orig.
  * Caller must free the returned string.
  */
diff --git a/ovn-sb.xml b/ovn-sb.xml
index 59888a155..3fe75b723 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -240,10 +240,12 @@
 
     <column name="name">
       OVN does not prescribe a particular format for chassis names.
-      ovn-controller populates this column using <ref key="system-id"
-      table="Open_vSwitch" column="external_ids" db="Open_vSwitch"/>
-      in the Open_vSwitch database's <ref table="Open_vSwitch"
-      db="Open_vSwitch"/> table.  ovn-controller-vtep populates this
+      ovn-controller populates this column using the <code>-n</code>
+      CLI argument, or <code>system-id-override</code> configuration file, or
+      <ref key="system-id" table="Open_vSwitch" column="external_ids"
+      db="Open_vSwitch"/> in the Open_vSwitch database's
+      <ref table="Open_vSwitch" db="Open_vSwitch"/> table.
+      ovn-controller-vtep populates this
       column with <ref table="Physical_Switch" column="name"
       db="hardware_vtep"/> in the hardware_vtep database's
       <ref table="Physical_Switch" db="hardware_vtep"/> table.
diff --git a/tests/ovn-controller.at b/tests/ovn-controller.at
index d8061345f..efb48c057 100644
--- a/tests/ovn-controller.at
+++ b/tests/ovn-controller.at
@@ -50,8 +50,7 @@ patch
 # is mirrored into the Chassis record in the OVN_Southbound db.
 check_bridge_mappings () {
     local_mappings=$1
-    sysid=$(ovs-vsctl get Open_vSwitch . external_ids:system-id)
-    OVS_WAIT_UNTIL([test x"${local_mappings}" = x$(ovn-sbctl get Chassis ${sysid} other_config:ovn-bridge-mappings | sed -e 's/\"//g')])
+    OVS_WAIT_UNTIL([test x"${local_mappings}" = x$(ovn-sbctl get Chassis ${sandbox} other_config:ovn-bridge-mappings | sed -e 's/\"//g')])
 }
 
 # Initially there should be no patch ports.
@@ -133,13 +132,13 @@ ovs-vsctl \
     -- add-br br-eth2
 ovn_attach n1 br-phys 192.168.0.1
 
-sysid=$(ovs-vsctl get Open_vSwitch . external_ids:system-id)
+sysid=${sandbox}
 
 # Make sure that the datapath_type set in the Bridge table
 # is mirrored into the Chassis record in the OVN_Southbound db.
 check_datapath_type () {
     datapath_type=$1
-    chassis_datapath_type=$(ovn-sbctl get Chassis ${sysid} other_config:datapath-type | sed -e 's/"//g') #"
+    chassis_datapath_type=$(ovn-sbctl get Chassis ${sandbox} other_config:datapath-type | sed -e 's/"//g') #"
     test "${datapath_type}" = "${chassis_datapath_type}"
 }
 
@@ -187,7 +186,7 @@ OVS_WAIT_UNTIL([
     test "${expected_iface_types}" = "${chassis_iface_types}"
 ])
 
-# Change the value of external_ids:system-id and make sure it's mirrored
+# Set the value of external_ids:system-id and make sure it's mirrored
 # in the Chassis record in the OVN_Southbound database.
 sysid=${sysid}-foo
 ovs-vsctl set Open_vSwitch . external-ids:system-id="${sysid}"
diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
index a6719be83..f846c6336 100644
--- a/tests/ovn-macros.at
+++ b/tests/ovn-macros.at
@@ -215,7 +215,7 @@ net_attach () {
 
 # ovn_az_attach AZ NETWORK BRIDGE IP [MASKLEN]
 ovn_az_attach() {
-    local az=$1 net=$2 bridge=$3 ip=$4 masklen=${5-24} encap=${6-geneve,vxlan}
+    local az=$1 net=$2 bridge=$3 ip=$4 masklen=${5-24} encap=${6-geneve,vxlan} intbr=${7-br-int} chassis=$8
     net_attach $net $bridge || return 1
 
     mac=`ovs-vsctl get Interface $bridge mac_in_use | sed s/\"//g`
@@ -229,15 +229,48 @@ ovn_az_attach() {
     else
         ovn_remote=unix:$ovs_base/$az/ovn-sb/ovn-sb.sock
     fi
+
+    if [[ -n "${chassis}" ]]; then
+        bridge_key=ovn-bridge-${chassis}
+        remote_key=ovn-remote-${chassis}
+        encap_type_key=ovn-encap-type-${chassis}
+        encap_ip_key=ovn-encap-ip-${chassis}
+        chassis_args="-n $chassis"
+        chassis_vsctl_args=
+    else
+        bridge_key=ovn-bridge
+        remote_key=ovn-remote
+        encap_type_key=ovn-encap-type
+        encap_ip_key=ovn-encap-ip
+        chassis=$sandbox
+        chassis_args=
+        chassis_vsctl_args="-- set Open_vSwitch . external-ids:system-id=$chassis"
+    fi
+
     ovs-vsctl \
-        -- set Open_vSwitch . external-ids:system-id=$sandbox \
-        -- set Open_vSwitch . external-ids:ovn-remote=$ovn_remote \
-        -- set Open_vSwitch . external-ids:ovn-encap-type=$encap \
-        -- set Open_vSwitch . external-ids:ovn-encap-ip=$ip \
-        -- --may-exist add-br br-int \
-        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true \
+        $chassis_vsctl_args \
+        -- set Open_vSwitch . external-ids:$bridge_key=$intbr \
+        -- set Open_vSwitch . external-ids:$remote_key=$ovn_remote \
+        -- set Open_vSwitch . external-ids:$encap_type_key=$encap \
+        -- set Open_vSwitch . external-ids:$encap_ip_key=$ip \
+        -- --may-exist add-br ${intbr} \
+        -- set bridge ${intbr} fail-mode=secure other-config:disable-in-band=true \
         || return 1
-    start_daemon ovn-controller || return 1
+
+    if [[ "${intbr}" = br-int ]]; then
+        pidfile="${OVS_RUNDIR}/ovn-controller.pid"
+        logfile="${OVS_LOGDIR}/ovn-controller.log"
+    else
+        pidfile="${OVS_RUNDIR}/ovn-controller-${intbr}.pid"
+        logfile="${OVS_LOGDIR}/ovn-controller-${chassis}.log"
+    fi
+
+    ovn-controller \
+        ${chassis_args} \
+        -vconsole:off --detach --no-chdir \
+        --pidfile=${pidfile} \
+        --log-file=${logfile} || return 1
+    on_exit "test -e \"$pidfile\" && kill \`cat \"$pidfile\"\`"
 }
 
 # ovn_attach NETWORK BRIDGE IP [MASKLEN]
diff --git a/tests/ovn.at b/tests/ovn.at
index de1df3b6a..3bac087a4 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1727,7 +1727,107 @@ AT_CLEANUP
 
 AT_BANNER([OVN end-to-end tests])
 
-# 3 hypervisors, one logical switch, 3 logical ports per hypervisor
+AT_SETUP([ovn -- 3 virtual hosts, same node])
+AT_KEYWORDS([ovn])
+ovn_start
+ovn-nbctl ls-add lsw0
+net_add n1
+sim_add hv
+
+as hv
+for i in 1 2 3; do
+    chassis=host-$i
+    ovs-vsctl add-br br-phys-$i
+    ovn_attach n1 br-phys-$i 192.168.0.$i 24 geneve br-int-$i $chassis
+
+    for j in 1 2 3; do
+        lpname=lp$i$j
+        ovn-nbctl lsp-add lsw0 $lpname
+        ovn-nbctl --wait=hv --timeout=3 lsp-set-options $lpname requested-chassis=$chassis
+        ovs-vsctl add-port br-int-$i vif$i$j -- set Interface vif$i$j external-ids:iface-id=$lpname
+        OVS_WAIT_UNTIL([test x`ovn-nbctl lsp-get-up $lpname` = xup])
+
+        pb_chassis_id=$(ovn-sbctl --bare --columns chassis list port_binding $lpname)
+        pb_chassis_name=$(ovn-sbctl get chassis $pb_chassis_id name)
+        AT_FAIL_IF([test x$pb_chassis_name != x$chassis])
+    done
+done
+
+for i in 1 2 3; do
+    > expout
+    for vif in 1 2 3; do
+        echo vif$i$vif >> expout
+    done
+    AT_CHECK([ovs-vsctl list-ports br-int-$i | grep vif], [0], [expout])
+done
+
+AT_CLEANUP
+
+AT_SETUP([ovn -- system-id in file])
+AT_KEYWORDS([ovn])
+
+ovn_start
+net_add n1
+sim_add hv
+
+as hv
+
+echo otherid > ${OVN_SYSCONFDIR}/system-id-override
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+
+# system-id-override file overrides chassis name selected via cli
+echo otherid > expout
+AT_CHECK([ovn-sbctl --bare --columns name list chassis], [0], [expout])
+
+AT_CLEANUP
+
+AT_SETUP([ovn -- concurrent controllers avoid fighting for each others' resources])
+AT_KEYWORDS([ovn])
+
+ovn_start
+sim_add hv
+
+for i in 1 2; do
+    net_add n-$i
+done
+
+as hv
+for i in 1 2; do
+    AT_CHECK([ovn-nbctl ls-add ls-$i])
+    AT_CHECK([ovn-nbctl lsp-add ls-$i ln_port-$i])
+    AT_CHECK([ovn-nbctl lsp-set-addresses ln_port-$i unknown])
+    AT_CHECK([ovn-nbctl lsp-set-type ln_port-$i localnet])
+    AT_CHECK([ovn-nbctl --wait=hv lsp-set-options ln_port-$i network_name=phys-$i])
+done
+
+for i in 1 2; do
+    as hv
+    ovs-vsctl add-br br-phys-$i
+    ovs-vsctl set open . external-ids:ovn-bridge-mappings-hv-$i=phys-$i:br-phys-$i
+    ovn_attach n-$i br-phys-$i 192.168.0.$i 24 geneve br-int-$i hv-$i
+
+    ovs-vsctl add-port br-int-$i vif-$i -- set Interface vif-$i external-ids:iface-id=lp-$i
+    ovn-nbctl lsp-add ls-$i lp-$i
+    OVS_WAIT_UNTIL([test x`ovn-nbctl lsp-get-up lp-$i` = xup])
+done
+
+# check that both patch ports are present
+AT_CHECK([ovs-vsctl --bare --columns=name find interface type="patch" | awk NF | sort], [0],
+[[patch-br-int-1-to-ln_port-1
+patch-br-int-2-to-ln_port-2
+patch-ln_port-1-to-br-int-1
+patch-ln_port-2-to-br-int-2
+]])
+
+# check that both tunnel endpoints are present
+AT_CHECK([ovs-vsctl --bare --columns=name find interface type="geneve" | awk NF | sort], [0],
+[[ovn-hv-1-0
+ovn-hv-2-0
+]])
+
+AT_CLEANUP
+
 AT_SETUP([ovn -- 3 HVs, 1 LS, 3 lports/HV])
 AT_KEYWORDS([ovnarp])
 ovn_start
@@ -6967,6 +7067,72 @@ OVN_CLEANUP([hv1])
 
 AT_CLEANUP
 
+AT_SETUP([ovn -- obsolete patch ports and tunnel endpoints removed])
+AT_KEYWORDS([cleanup-test])
+ovn_start
+
+net_add n1
+net_add n2
+
+for i in 1 2; do
+    ovs-vsctl add-br br-phys$i
+    ovs-vsctl set open . external-ids:ovn-bridge-mappings-hv$i=physnet$i:br-phys$i
+    ovn_attach n$i br-phys$i 192.168.0.$i 24 geneve br-int$i hv$i
+done
+
+# create irrelevant patch and tunnel ports
+for i in 1 2; do
+    # patch without chassis owner set
+    ovs-vsctl add-port br-int$i fakepatch$i external-ids:ovn-logical-patch-port=fakeport$i -- \
+              set Interface fakepatch$i type=patch
+
+    # patch marked as owned by the chassis
+    ovs-vsctl add-port br-int$i owned_fakepatch$i external-ids:ovn-logical-patch-port=owned_fakeport$i \
+                                                  external-ids:ovn-chassis-id=hv$i -- \
+              set Interface owned_fakepatch$i type=patch
+
+    # patch marked as owned by some other chassis
+    ovs-vsctl add-port br-int$i alien_fakepatch$i external-ids:ovn-logical-patch-port=alien_fakeport$i \
+                                                  external-ids:ovn-chassis-id=alien_hv$i -- \
+              set Interface alien_fakepatch$i type=patch
+
+    # OVN tunnel endpoint on a bridge owned by a controller
+    ovs-vsctl add-port br-int$i faketunnel$i external-ids:ovn-chassis-id=fakechassis -- \
+              set Interface faketunnel$i type=geneve
+done
+
+# tunnel endpoint on a bridge NOT owned by a controller
+ovs-vsctl add-br alien_br
+ovs-vsctl add-port alien_br alien_tunnel external-ids:ovn-chassis-id=fakechassis -- \
+          set Interface alien_tunnel type=geneve
+
+AT_CHECK([ovn-nbctl ls-add lsw0])
+AT_CHECK([ovn-nbctl lsp-add lsw0 lnport])
+AT_CHECK([ovn-nbctl lsp-set-addresses lnport unknown])
+AT_CHECK([ovn-nbctl lsp-set-type lnport localnet])
+AT_CHECK([ovn-nbctl --wait=hv lsp-set-options lnport network_name=physnet1])
+
+ovs-vsctl add-port br-int1 vif -- set Interface vif external-ids:iface-id=lp0
+ovn-nbctl lsp-add lsw0 lp0
+OVS_WAIT_UNTIL([test x`ovn-nbctl lsp-get-up lp0` = xup])
+
+# check that only patch ports that belong to some other chassis and those for localnet ports are present
+AT_CHECK([ovs-vsctl --bare --columns=name find interface type="patch" | awk NF | sort], [0],
+[[alien_fakepatch1
+alien_fakepatch2
+patch-br-int1-to-lnport
+patch-lnport-to-br-int1
+]])
+
+# check that only controller tunnel endpoints and a tunnel on another bridge are present
+AT_CHECK([ovs-vsctl --bare --columns=name find interface type="geneve" | awk NF | sort], [0],
+[[alien_tunnel
+ovn-hv1-0
+ovn-hv2-0
+]])
+
+AT_CLEANUP
+
 AT_SETUP([ovn -- nd_na ])
 ovn_start
 
diff --git a/tests/ovs-macros.at b/tests/ovs-macros.at
index 3dcf8f96d..7b812e5dd 100644
--- a/tests/ovs-macros.at
+++ b/tests/ovs-macros.at
@@ -53,6 +53,7 @@ ovs_setenv() {
     OVS_LOGDIR=$ovs_dir; export OVS_LOGDIR
     OVS_DBDIR=$ovs_dir; export OVS_DBDIR
     OVS_SYSCONFDIR=$ovs_dir; export OVS_SYSCONFDIR
+    OVN_SYSCONFDIR=$ovs_dir; export OVN_SYSCONFDIR
     OVS_PKGDATADIR=$ovs_dir; export OVS_PKGDATADIR
 }
 
