diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index c615e8ab2..f5516a879 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -937,6 +937,7 @@ ctrl_register_ovs_idl(struct ovsdb_idl *ovs_idl)
     binding_register_ovs_idl(ovs_idl);
     bfd_register_ovs_idl(ovs_idl);
     physical_register_ovs_idl(ovs_idl);
+    plug_register_ovs_idl(ovs_idl);
 }
 
 #define SB_NODES \
@@ -2977,6 +2978,171 @@ flow_output_lflow_output_handler(struct engine_node *node,
     return true;
 }
 
+static void *
+en_plug_provider_lookup_init(struct engine_node *node OVS_UNUSED,
+                             struct engine_arg *arg OVS_UNUSED)
+{
+    return NULL;
+}
+
+static void
+en_plug_provider_lookup_cleanup(void *data OVS_UNUSED)
+{
+
+}
+
+static void
+en_plug_provider_lookup_run(struct engine_node *node,
+                            void *data OVS_UNUSED)
+{
+    VLOG_INFO("en_plug_provider_lookup_run");
+    if (!plug_provider_has_providers()) {
+        engine_set_node_state(node, EN_UNCHANGED);
+        return;
+    }
+
+    if (plug_provider_run_all()) {
+        engine_set_node_state(node, EN_UPDATED);
+    } else {
+        engine_set_node_state(node, EN_UNCHANGED);
+
+    }
+}
+
+
+struct ed_type_plug_provider {
+    struct shash deleted_iface_ids;
+    struct shash changed_iface_ids;
+};
+
+static void *
+en_plug_provider_init(struct engine_node *node OVS_UNUSED,
+                      struct engine_arg *arg OVS_UNUSED)
+{
+    struct ed_type_plug_provider *data = xzalloc(sizeof *data);
+
+    shash_init(&data->deleted_iface_ids);
+    shash_init(&data->changed_iface_ids);
+    return data;
+}
+
+static void
+en_plug_provider_cleanup(void *data)
+{
+    struct ed_type_plug_provider *plug_provider_data = data;
+
+    shash_destroy_free_data(&plug_provider_data->deleted_iface_ids);
+    shash_destroy_free_data(&plug_provider_data->changed_iface_ids);
+}
+
+static void
+init_plug_ctx(struct engine_node *node,
+              void *data,
+              struct plug_ctx_in *plug_ctx_in,
+              struct plug_ctx_out *plug_ctx_out)
+{
+    struct ovsrec_open_vswitch_table *ovs_table =
+        (struct ovsrec_open_vswitch_table *)EN_OVSDB_GET(
+            engine_get_input("OVS_open_vswitch", 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_bridge *br_int = get_br_int(bridge_table, ovs_table);
+
+    ovs_assert(br_int && chassis_id);
+
+    struct ovsdb_idl_index *sbrec_chassis_by_name =
+        engine_ovsdb_node_get_index(
+                engine_get_input("SB_chassis", node),
+                "name");
+
+    const struct sbrec_chassis *chassis
+        = chassis_lookup_by_name(sbrec_chassis_by_name, chassis_id);
+    ovs_assert(chassis);
+
+    struct ovsrec_interface_table *iface_table =
+        (struct ovsrec_interface_table *)EN_OVSDB_GET(
+            engine_get_input("OVS_interface", node));
+
+    struct sbrec_port_binding_table *pb_table =
+        (struct sbrec_port_binding_table *)EN_OVSDB_GET(
+            engine_get_input("SB_port_binding", node));
+
+    struct ovsdb_idl_index *sbrec_port_binding_by_name =
+        engine_ovsdb_node_get_index(
+                engine_get_input("SB_port_binding", node),
+                "name");
+
+    struct ovsdb_idl_index *ovsrec_port_by_interfaces =
+        engine_ovsdb_node_get_index(
+                engine_get_input("OVS_port", node),
+                "interfaces");
+
+    struct ed_type_runtime_data *rt_data =
+        engine_get_input_data("runtime_data", node);
+
+    plug_ctx_in->ovs_idl_txn = engine_get_context()->ovs_idl_txn;
+    plug_ctx_in->sbrec_port_binding_by_name = sbrec_port_binding_by_name;
+    plug_ctx_in->ovsrec_port_by_interfaces = ovsrec_port_by_interfaces;
+    plug_ctx_in->ovs_table = ovs_table;
+    plug_ctx_in->br_int = br_int;
+    plug_ctx_in->iface_table = iface_table;
+    plug_ctx_in->chassis_rec = chassis;
+    plug_ctx_in->port_binding_table = pb_table;
+    plug_ctx_in->local_bindings = &rt_data->lbinding_data.bindings;
+
+    struct ed_type_plug_provider *plug_provider_data = data;
+    plug_ctx_out->deleted_iface_ids = &plug_provider_data->deleted_iface_ids;
+    plug_ctx_out->changed_iface_ids = &plug_provider_data->changed_iface_ids;
+}
+
+static void
+en_plug_provider_run(struct engine_node *node,
+                     void *data)
+{
+    if (!plug_provider_has_providers()) {
+        engine_set_node_state(node, EN_UNCHANGED);
+        return;
+    }
+    struct plug_ctx_in plug_ctx_in;
+    struct plug_ctx_out plug_ctx_out;
+    init_plug_ctx(node, data, &plug_ctx_in, &plug_ctx_out);
+    plug_run(&plug_ctx_in, &plug_ctx_out);
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+static bool
+plug_provider_port_binding_handler(struct engine_node *node,
+                                   void *data)
+{
+    if (!plug_provider_has_providers()) {
+        engine_set_node_state(node, EN_UNCHANGED);
+        return true;
+    }
+    struct plug_ctx_in plug_ctx_in;
+    struct plug_ctx_out plug_ctx_out;
+    init_plug_ctx(node, data, &plug_ctx_in, &plug_ctx_out);
+
+    return plug_handle_port_binding_changes(&plug_ctx_in, &plug_ctx_out);
+}
+
+static bool
+plug_provider_ovs_interface_handler(struct engine_node *node,
+                                    void *data)
+{
+    if (!plug_provider_has_providers()) {
+        engine_set_node_state(node, EN_UNCHANGED);
+        return true;
+    }
+
+    struct plug_ctx_in plug_ctx_in;
+    struct plug_ctx_out plug_ctx_out;
+    init_plug_ctx(node, data, &plug_ctx_in, &plug_ctx_out);
+
+    return plug_handle_ovs_interface_changes(&plug_ctx_in, &plug_ctx_out);
+}
+
 struct ovn_controller_exit_args {
     bool *exiting;
     bool *restart;
@@ -3213,6 +3379,11 @@ main(int argc, char *argv[])
     ENGINE_NODE(flow_output, "flow_output");
     ENGINE_NODE(addr_sets, "addr_sets");
     ENGINE_NODE_WITH_CLEAR_TRACK_DATA(port_groups, "port_groups");
+    /* The plug provider has two engine nodes.  One that checks for and reacts
+     * to change to plug provider lookup tables, and another that reacts to
+     * change to OVS interface, OVN Port_Binding and runtime data */
+    ENGINE_NODE(plug_provider_lookup, "plug_provider_lookup");
+    ENGINE_NODE(plug_provider, "plug_provider");
 
 #define SB_NODE(NAME, NAME_STR) ENGINE_NODE_SB(NAME, NAME_STR);
     SB_NODES
@@ -3239,6 +3410,24 @@ main(int argc, char *argv[])
     engine_add_input(&en_non_vif_data, &en_ovs_interface,
                      non_vif_data_ovs_iface_handler);
 
+    engine_add_input(&en_plug_provider, &en_plug_provider_lookup, NULL);
+    engine_add_input(&en_plug_provider, &en_ovs_open_vswitch, NULL);
+    engine_add_input(&en_plug_provider, &en_ovs_bridge, NULL);
+    engine_add_input(&en_plug_provider, &en_sb_chassis, NULL);
+    engine_add_input(&en_plug_provider, &en_runtime_data,
+                     engine_noop_handler);
+    engine_add_input(&en_plug_provider, &en_sb_port_binding,
+                     plug_provider_port_binding_handler);
+    engine_add_input(&en_plug_provider, &en_ovs_port,
+                     engine_noop_handler);
+    engine_add_input(&en_plug_provider, &en_ovs_interface,
+                     plug_provider_ovs_interface_handler);
+    /* The node needs somewhere to output to run */
+    engine_add_input(&en_flow_output, &en_plug_provider,
+                     engine_noop_handler);
+
+
+
     /* Note: The order of inputs is important, all OVS interface changes must
      * be handled before any ct_zone changes.
      */
@@ -3812,6 +4001,12 @@ main(int argc, char *argv[])
                     }
                 }
             }
+            struct ed_type_plug_provider *plug_provider_data = engine_get_data(
+                &en_plug_provider);
+            if (plug_provider_data) {
+                plug_finish_deleted(&plug_provider_data->deleted_iface_ids);
+                plug_finish_changed(&plug_provider_data->changed_iface_ids);
+            }
         }
 
         ovsdb_idl_track_clear(ovnsb_idl_loop.idl);
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 390cc5a44..2774eb255 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -1028,6 +1028,27 @@
             DHCP reply.
           </p>
         </column>
+
+        <group title="VIF Plugging Options">
+          <column name="options" key="plug-type">
+            If set, OVN will attempt to perform plugging of this VIF.  In order
+            to get this port plugged by the OVN controller, OVN must be built
+            with support for VIF plugging.  The default behavior is for the CMS
+            to do the VIF plugging.  Each plug provider have their own options
+            namespaced by name, for example "plug:representor:key".
+
+            Please refer to the plug provider documentation located in
+            Documentation/topics/plug_providers/ for more information.
+          </column>
+
+          <column name="options" key="plug-mtu-request">
+            Requested MTU for plugged interfaces.  When set the OVN controller
+            will fill the <ref table="Interface" column="mtu_request"/> column
+            of the Open vSwitch database's
+            <ref table="Interface" db="vswitch"/> table.  This in turn will
+            make OVS vswitchd update the MTU of the linked interface.
+          </column>
+        </group>
       </group>
 
       <group title="Virtual port Options">
diff --git a/tests/ovn-controller.at b/tests/ovn-controller.at
index a1169c569..2400aa1b7 100644
--- a/tests/ovn-controller.at
+++ b/tests/ovn-controller.at
@@ -795,6 +795,91 @@ OVN_CLEANUP_SBOX([hv])
 OVN_CLEANUP_VSWITCH([main])
 as ovn-sb
 OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn-controller - plugging])
+AT_KEYWORDS([plugging])
+
+ovn_start
+
+net_add n1
+sim_add hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovn-appctl -t ovn-controller vlog/set
+
+sim_add hv2
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.2
+ovn-appctl -t ovn-controller vlog/set
+
+check ovn-nbctl ls-add lsw0
 
+check ovn-nbctl lsp-add lsw0 lsp1
+check ovn-nbctl lsp-set-addresses lsp1 "f0:00:00:00:00:01 172.16.0.101"
+check ovn-nbctl --wait=sb set Logical_Switch_Port lsp1 \
+    options:requested-chassis=hv1 \
+    options:plug-type=dummy \
+    options:plug-mtu-request=42
+
+check ovn-nbctl lsp-add lsw0 lsp2
+check ovn-nbctl lsp-set-addresses lsp2 "f0:00:00:00:00:02 172.16.0.102"
+check ovn-nbctl --wait=sb set Logical_Switch_Port lsp2 \
+    options:requested-chassis=hv2 \
+    options:plug-type=dummy \
+    options:plug-mtu-request=42
+
+wait_column "true" Port_Binding up logical_port=lsp1
+wait_column "true" Port_Binding up logical_port=lsp2
+
+as hv1
+
+# Check that the lport was plugged
+AT_CHECK([as hv1 ovs-vsctl find interface name=lsp1 options:plug-dummy-option=value | grep -q "options.*value"])
+AT_CHECK([as hv1 ovs-vsctl find interface name=lsp1 mtu_request=42 | grep -q "mtu_request.*42"])
+
+# Check that updating the lport updates the local iface
+check ovn-nbctl --wait=sb set Logical_Switch_Port lsp1 \
+    options:requested-chassis=hv1 \
+    options:plug-type=dummy \
+    options:plug-mtu-request=43
+OVS_WAIT_UNTIL([
+    as hv1 ovs-vsctl find interface mtu_request=43 | grep -q "mtu_request.*43"
+])
+
+# Check that local modification of iface will trigger ovn-controller to update
+# the iface record
+iface_uuid=$(as hv1 ovs-vsctl --bare --columns _uuid find interface name=lsp1)
+as hv1 ovs-vsctl set interface ${iface_uuid} mtu_request=44
+OVS_WAIT_UNTIL([
+    as hv1 ovs-vsctl find interface mtu_request=43 | grep -q "mtu_request.*43"
+])
+
+lsp_uuid=$(ovn-nbctl --bare --columns _uuid find Logical_Switch_Port name=lsp1)
+iface_uuid=$(as hv1 ovs-vsctl --bare --columns _uuid find Interface name=lsp1)
+# Check that pointing requested-chassis somewhere else will unplug the port
+check ovn-nbctl --wait=sb set Logical_Switch_Port lsp1 \
+    options:requested-chassis=non-existent-chassis
+OVS_WAIT_UNTIL([
+    grep -q "Releasing lport lsp1" hv1/ovn-controller.log
+])
+OVS_WAIT_UNTIL([
+    grep -q "Unplugging port lsp1" hv1/ovn-controller.log
+])
+as hv1 ovs-vsctl find interface name=lsp1
+OVS_WAIT_UNTIL([
+    ! as hv1 ovs-vsctl get Interface ${iface_uuid} _uuid
+])
+
+# Check that removing an lport will unplug it
+lsp_uuid=$(ovn-nbctl --bare --columns _uuid find Logical_Switch_Port name=lsp2)
+iface_uuid=$(as hv2 ovs-vsctl --bare --columns _uuid find Interface name=lsp1)
+check ovn-nbctl --wait=sb lsp-del ${lsp_uuid}
+OVS_WAIT_UNTIL([
+    ! as hv2 ovs-vsctl get Interface ${iface_uuid} _uuid
+])
+OVN_CLEANUP([hv1],[hv2])
 AT_CLEANUP
 ])
diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at
index f06f2e68e..b3c2d72fa 100644
--- a/tests/ovn-macros.at
+++ b/tests/ovn-macros.at
@@ -327,7 +327,7 @@ ovn_az_attach() {
         -- --may-exist add-br br-int \
         -- set bridge br-int fail-mode=secure other-config:disable-in-band=true \
         || return 1
-    start_daemon ovn-controller || return 1
+    start_daemon ovn-controller --enable-dummy-plug || return 1
 }
 
 # ovn_attach NETWORK BRIDGE IP [MASKLEN]
