diff --git a/setup.cfg b/setup.cfg
index 862e5efa..7f75f4e1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -26,6 +26,7 @@ setup-hooks =
 packages =
     os_vif
     vif_plug_linux_bridge
+    vif_plug_ovs
 
 [egg_info]
 tag_build =
@@ -57,3 +58,5 @@ output_file = os_vif/locale/os-vif.pot
 [entry_points]
 os_vif =
     linux_bridge = vif_plug_linux_bridge.linux_bridge:LinuxBridgePlugin
+    ovs = vif_plug_ovs.ovs:OvsBridgePlugin
+    ovs_hybrid = vif_plug_ovs.ovs_hybrid:OvsHybridPlugin
diff --git a/vif_plug_ovs/__init__.py b/vif_plug_ovs/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vif_plug_ovs/exception.py b/vif_plug_ovs/exception.py
new file mode 100644
index 00000000..66dc4937
--- /dev/null
+++ b/vif_plug_ovs/exception.py
@@ -0,0 +1,28 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from os_vif.i18n import _
+
+from os_vif import exception as osv_exception
+
+
+class AgentError(osv_exception.ExceptionBase):
+    msg_fmt = _('Error during following call to agent: %(method)s')
+
+
+class MissingPortProfile(osv_exception.ExceptionBase):
+    msg_fmt = _('A port profile is mandatory for the OpenVSwitch plugin')
+
+
+class WrongPortProfile(osv_exception.ExceptionBase):
+    msg_fmt = _('Port profile %(profile)s is not a subclass '
+                'of VIFPortProfileOpenVSwitch')
diff --git a/vif_plug_ovs/i18n.py b/vif_plug_ovs/i18n.py
new file mode 100644
index 00000000..8fcbf345
--- /dev/null
+++ b/vif_plug_ovs/i18n.py
@@ -0,0 +1,49 @@
+# Copyright 2014 IBM Corp.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""oslo.i18n integration module.
+
+See http://docs.openstack.org/developer/oslo.i18n/usage.html .
+
+"""
+
+import oslo_i18n
+
+# Normally this would be the plugin specific name
+# eg 'vif_plug_ovs', but since the OVS plugin is
+# in-tree, this is a special case
+DOMAIN = 'os_vif'
+
+_translators = oslo_i18n.TranslatorFactory(domain=DOMAIN)
+
+# The primary translation function using the well-known name "_"
+_ = _translators.primary
+
+# Translators for log levels.
+#
+# The abbreviated names are meant to reflect the usual use of a short
+# name like '_'. The "L" is for "log" and the other letter comes from
+# the level.
+_LI = _translators.log_info
+_LW = _translators.log_warning
+_LE = _translators.log_error
+_LC = _translators.log_critical
+
+
+def translate(value, user_locale):
+    return oslo_i18n.translate(value, user_locale)
+
+
+def get_available_languages():
+    return oslo_i18n.get_available_languages(DOMAIN)
diff --git a/vif_plug_ovs/linux_net.py b/vif_plug_ovs/linux_net.py
new file mode 100644
index 00000000..95534129
--- /dev/null
+++ b/vif_plug_ovs/linux_net.py
@@ -0,0 +1,104 @@
+# Derived from nova/network/linux_net.py
+#
+# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Implements vlans, bridges using linux utilities."""
+
+import os
+
+from oslo_concurrency import processutils
+from oslo_log import log as logging
+from oslo_utils import excutils
+
+from vif_plug_ovs import exception
+from vif_plug_ovs.i18n import _LE
+
+LOG = logging.getLogger(__name__)
+
+
+def _ovs_vsctl(args, timeout=None):
+    full_args = ['ovs-vsctl']
+    if timeout is not None:
+        full_args += ['--timeout=%s' % timeout]
+    full_args += args
+    try:
+        return processutils.execute(*full_args, run_as_root=True)
+    except Exception as e:
+        LOG.error(_LE("Unable to execute %(cmd)s. Exception: %(exception)s"),
+                  {'cmd': full_args, 'exception': e})
+        raise exception.AgentError(method=full_args)
+
+
+def create_ovs_vif_port(bridge, dev, iface_id, mac, instance_id, mtu,
+                        timeout=None):
+    _ovs_vsctl(['--', '--if-exists', 'del-port', dev, '--',
+                'add-port', bridge, dev,
+                '--', 'set', 'Interface', dev,
+                'external-ids:iface-id=%s' % iface_id,
+                'external-ids:iface-status=active',
+                'external-ids:attached-mac=%s' % mac,
+                'external-ids:vm-uuid=%s' % instance_id],
+                timeout=timeout)
+    _set_device_mtu(dev, mtu)
+
+
+def delete_ovs_vif_port(bridge, dev, timeout=None):
+    _ovs_vsctl(['--', '--if-exists', 'del-port', bridge, dev],
+               timeout=timeout)
+    delete_net_dev(dev)
+
+
+def device_exists(device):
+    """Check if ethernet device exists."""
+    return os.path.exists('/sys/class/net/%s' % device)
+
+
+def delete_net_dev(dev):
+    """Delete a network device only if it exists."""
+    if device_exists(dev):
+        try:
+            processutils.execute('ip', 'link', 'delete', dev,
+                                 check_exit_code=[0, 2, 254],
+                                 run_as_root=True)
+            LOG.debug("Net device removed: '%s'", dev)
+        except processutils.ProcessExecutionError:
+            with excutils.save_and_reraise_exception():
+                LOG.error(_LE("Failed removing net device: '%s'"), dev)
+
+
+def create_veth_pair(dev1_name, dev2_name, mtu):
+    """Create a pair of veth devices with the specified names,
+    deleting any previous devices with those names.
+    """
+    for dev in [dev1_name, dev2_name]:
+        delete_net_dev(dev)
+
+    processutils.execute('ip', 'link', 'add', dev1_name,
+                         'type', 'veth', 'peer', 'name', dev2_name,
+                         run_as_root=True)
+    for dev in [dev1_name, dev2_name]:
+        processutils.execute('ip', 'link', 'set', dev, 'up',
+                             run_as_root=True)
+        processutils.execute('ip', 'link', 'set', dev, 'promisc', 'on')
+        _set_device_mtu(dev, mtu)
+
+
+def _set_device_mtu(dev, mtu):
+    """Set the device MTU."""
+    processutils.execute('ip', 'link', 'set', dev, 'mtu', mtu,
+                         check_exit_code=[0, 2, 254])
diff --git a/vif_plug_ovs/ovs.py b/vif_plug_ovs/ovs.py
new file mode 100644
index 00000000..e73f8bb1
--- /dev/null
+++ b/vif_plug_ovs/ovs.py
@@ -0,0 +1,41 @@
+# Derived from nova/virt/libvirt/vif.py
+#
+# Copyright (C) 2011 Midokura KK
+# Copyright (C) 2011 Nicira, Inc
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from os_vif import objects
+from os_vif import plugin
+
+
+class OvsBridgePlugin(plugin.PluginBase):
+    """An OVS VIF type that uses a standard Linux bridge for integration."""
+
+    def describe(self):
+        return plugin.PluginInfo(
+            [
+                plugin.PluginVIFInfo(
+                    objects.vif.VIFOpenVSwitch,
+                    "1.0", "1.0")
+            ])
+
+    def plug(self, vif, instance_info):
+        # Nothing required to plug an OVS port...
+        pass
+
+    def unplug(self, vif, instance_info):
+        # Nothing required to unplug an OVS port...
+        pass
diff --git a/vif_plug_ovs/ovs_hybrid.py b/vif_plug_ovs/ovs_hybrid.py
new file mode 100644
index 00000000..82c391bf
--- /dev/null
+++ b/vif_plug_ovs/ovs_hybrid.py
@@ -0,0 +1,135 @@
+# Derived from nova/virt/libvirt/vif.py
+#
+# Copyright (C) 2011 Midokura KK
+# Copyright (C) 2011 Nicira, Inc
+# Copyright 2011 OpenStack Foundation
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from os_vif import objects
+from os_vif import plugin
+from oslo_config import cfg
+
+from oslo_concurrency import processutils
+
+from vif_plug_ovs import exception
+from vif_plug_ovs import linux_net
+
+
+class OvsHybridPlugin(plugin.PluginBase):
+    """
+    An OVS VIF type that uses a pair of devices in order to allow
+    security group rules to be applied to traffic coming in or out of
+    a virtual machine.
+    """
+
+    NIC_NAME_LEN = 14
+
+    CONFIG_OPTS = (
+        cfg.IntOpt('network_device_mtu',
+                   default=1500,
+                   help='MTU setting for network interface.',
+                   deprecated_group="DEFAULT"),
+        cfg.IntOpt('ovs_vsctl_timeout',
+                   default=120,
+                   help='Amount of time, in seconds, that ovs_vsctl should '
+                   'wait for a response from the database. 0 is to wait '
+                   'forever.',
+                   deprecated_group="DEFAULT"),
+    )
+
+    @staticmethod
+    def get_veth_pair_names(vif):
+        iface_id = vif.id
+        return (("qvb%s" % iface_id)[:OvsHybridPlugin.NIC_NAME_LEN],
+                ("qvo%s" % iface_id)[:OvsHybridPlugin.NIC_NAME_LEN])
+
+    def describe(self):
+        return plugin.PluginInfo(
+            [
+                plugin.PluginVIFInfo(
+                    objects.vif.VIFBridge,
+                    "1.0", "1.0")
+            ])
+
+    def plug(self, vif, instance_info):
+        """Plug using hybrid strategy
+
+        Create a per-VIF linux bridge, then link that bridge to the OVS
+        integration bridge via a veth device, setting up the other end
+        of the veth device just like a normal OVS port. Then boot the
+        VIF on the linux bridge using standard libvirt mechanisms.
+        """
+
+        if not hasattr(vif, "port_profile"):
+            raise exception.MissingPortProfile()
+        if not isinstance(vif.port_profile,
+                          objects.vif.VIFPortProfileOpenVSwitch):
+            raise exception.WrongPortProfile(
+                profile=vif.port_profile.__class__.__name__)
+
+        v1_name, v2_name = self.get_veth_pair_names(vif)
+
+        if not linux_net.device_exists(vif.bridge_name):
+            processutils.execute('brctl', 'addbr', vif.bridge_name,
+                                 run_as_root=True)
+            processutils.execute('brctl', 'setfd', vif.bridge_name, 0,
+                                 run_as_root=True)
+            processutils.execute('brctl', 'stp', vif.bridge_name, 'off',
+                                 run_as_root=True)
+            syspath = '/sys/class/net/%s/bridge/multicast_snooping'
+            syspath = syspath % vif.bridge_name
+            processutils.execute('tee', syspath, process_input='0',
+                                 check_exit_code=[0, 1],
+                                 run_as_root=True)
+
+        if not linux_net.device_exists(v2_name):
+            linux_net.create_veth_pair(v1_name, v2_name,
+                                       self.config.network_device_mtu)
+            processutils.execute('ip', 'link', 'set', vif.bridge_name, 'up',
+                                 run_as_root=True)
+            processutils.execute('brctl', 'addif', vif.bridge_name, v1_name,
+                                 run_as_root=True)
+            linux_net.create_ovs_vif_port(
+                vif.network.bridge,
+                v2_name,
+                vif.port_profile.interface_id,
+                vif.address, instance_info.uuid,
+                timeout=self.config.ovs_vsctl_timeout)
+
+    def unplug(self, vif, instance_info):
+        """UnPlug using hybrid strategy
+
+        Unhook port from OVS, unhook port from bridge, delete
+        bridge, and delete both veth devices.
+        """
+        if not hasattr(vif, "port_profile"):
+            raise exception.MissingPortProfile()
+        if not isinstance(vif.port_profile,
+                          objects.vif.VIFPortProfileOpenVSwitch):
+            raise exception.WrongPortProfile(
+                profile=vif.port_profile.__class__.__name__)
+
+        v1_name, v2_name = self.get_veth_pair_names(vif)
+
+        if linux_net.device_exists(vif.bridge_name):
+            processutils.execute('brctl', 'delif', vif.bridge_name, v1_name,
+                                 run_as_root=True)
+            processutils.execute('ip', 'link', 'set', vif.bridge_name, 'down',
+                                 run_as_root=True)
+            processutils.execute('brctl', 'delbr', vif.bridge_name,
+                                 run_as_root=True)
+
+        linux_net.delete_ovs_vif_port(vif.network.bridge, v2_name,
+                                      timeout=self.config.ovs_vsctl_timeout)
diff --git a/vif_plug_ovs/tests/__init__.py b/vif_plug_ovs/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vif_plug_ovs/tests/test_plugin.py b/vif_plug_ovs/tests/test_plugin.py
new file mode 100644
index 00000000..128d692f
--- /dev/null
+++ b/vif_plug_ovs/tests/test_plugin.py
@@ -0,0 +1,155 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import contextlib
+import mock
+import six
+import testtools
+
+from os_vif import objects
+
+from oslo_concurrency import processutils
+
+from vif_plug_ovs import linux_net
+from vif_plug_ovs import ovs_hybrid
+
+
+if six.PY2:
+    nested = contextlib.nested
+else:
+    @contextlib.contextmanager
+    def nested(*contexts):
+        with contextlib.ExitStack() as stack:
+            yield [stack.enter_context(c) for c in contexts]
+
+
+class PluginTest(testtools.TestCase):
+
+    def __init__(self, *args, **kwargs):
+        super(PluginTest, self).__init__(*args, **kwargs)
+
+        objects.register_all()
+
+        self.subnet_bridge_4 = objects.subnet.Subnet(
+            cidr='101.168.1.0/24',
+            dns=['8.8.8.8'],
+            gateway='101.168.1.1',
+            dhcp_server='191.168.1.1')
+
+        self.subnet_bridge_6 = objects.subnet.Subnet(
+            cidr='101:1db9::/64',
+            gateway='101:1db9::1')
+
+        self.subnets = objects.subnet.SubnetList(
+            objects=[self.subnet_bridge_4,
+                     self.subnet_bridge_6])
+
+        self.network_ovs = objects.network.Network(
+            id='network-id-xxx-yyy-zzz',
+            bridge='br0',
+            subnets=self.subnets,
+            vlan=99)
+
+        self.profile_ovs = objects.vif.VIFPortProfileOpenVSwitch(
+            interface_id='aaa-bbb-ccc')
+        self.vif_ovs = objects.vif.VIFBridge(
+            id='vif-xxx-yyy-zzz',
+            address='ca:fe:de:ad:be:ef',
+            network=self.network_ovs,
+            dev_name='tap-xxx-yyy-zzz',
+            bridge_name="qbrvif-xxx-yyy",
+            port_profile=self.profile_ovs)
+
+        self.instance = objects.instance_info.InstanceInfo(
+            name='demo',
+            uuid='f0000000-0000-0000-0000-000000000001')
+
+    def test_plug_ovs_hybrid(self):
+        calls = {
+            'device_exists': [mock.call('qbrvif-xxx-yyy'),
+                              mock.call('qvovif-xxx-yyy')],
+            '_create_veth_pair': [mock.call('qvbvif-xxx-yyy',
+                                            'qvovif-xxx-yyy',
+                                            1500)],
+            'execute': [mock.call('brctl', 'addbr', 'qbrvif-xxx-yyy',
+                                  run_as_root=True),
+                        mock.call('brctl', 'setfd', 'qbrvif-xxx-yyy', 0,
+                                  run_as_root=True),
+                        mock.call('brctl', 'stp', 'qbrvif-xxx-yyy', 'off',
+                                  run_as_root=True),
+                        mock.call('tee', ('/sys/class/net/qbrvif-xxx-yyy'
+                                          '/bridge/multicast_snooping'),
+                                  process_input='0', run_as_root=True,
+                                  check_exit_code=[0, 1]),
+                        mock.call('ip', 'link', 'set', 'qbrvif-xxx-yyy', 'up',
+                                  run_as_root=True),
+                        mock.call('brctl', 'addif', 'qbrvif-xxx-yyy',
+                                  'qvbvif-xxx-yyy', run_as_root=True)],
+            'create_ovs_vif_port': [mock.call(
+                                    'br0', 'qvovif-xxx-yyy', 'aaa-bbb-ccc',
+                                    'ca:fe:de:ad:be:ef',
+                                    'f0000000-0000-0000-0000-000000000001',
+                                    timeout=120)]
+        }
+        with nested(
+                mock.patch.object(linux_net, 'device_exists',
+                                  return_value=False),
+                mock.patch.object(processutils, 'execute'),
+                mock.patch.object(linux_net, 'create_veth_pair'),
+                mock.patch.object(linux_net, 'create_ovs_vif_port')
+        ) as (device_exists, execute, _create_veth_pair, create_ovs_vif_port):
+            plugin = ovs_hybrid.OvsHybridPlugin.load("ovs_hybrid")
+            plugin.plug(self.vif_ovs, self.instance)
+            device_exists.assert_has_calls(calls['device_exists'])
+            _create_veth_pair.assert_has_calls(calls['_create_veth_pair'])
+            execute.assert_has_calls(calls['execute'])
+            create_ovs_vif_port.assert_has_calls(calls['create_ovs_vif_port'])
+
+    def test_unplug_ovs_hybrid(self):
+        calls = {
+            'device_exists': [mock.call('qbrvif-xxx-yyy')],
+            'execute': [mock.call('brctl', 'delif', 'qbrvif-xxx-yyy',
+                                  'qvbvif-xxx-yyy', run_as_root=True),
+                        mock.call('ip', 'link', 'set',
+                                  'qbrvif-xxx-yyy', 'down', run_as_root=True),
+                        mock.call('brctl', 'delbr',
+                                  'qbrvif-xxx-yyy', run_as_root=True)],
+            'delete_ovs_vif_port': [mock.call('br0', 'qvovif-xxx-yyy',
+                                    timeout=120)]
+        }
+        with nested(
+                mock.patch.object(linux_net, 'device_exists',
+                                  return_value=True),
+                mock.patch.object(processutils, 'execute'),
+                mock.patch.object(linux_net, 'delete_ovs_vif_port')
+        ) as (device_exists, execute, delete_ovs_vif_port):
+            plugin = ovs_hybrid.OvsHybridPlugin.load("ovs_hybrid")
+            plugin.unplug(self.vif_ovs, self.instance)
+            device_exists.assert_has_calls(calls['device_exists'])
+            execute.assert_has_calls(calls['execute'])
+            delete_ovs_vif_port.assert_has_calls(calls['delete_ovs_vif_port'])
+
+    def test_unplug_ovs_hybrid_bridge_does_not_exist(self):
+        calls = {
+            'device_exists': [mock.call('qbrvif-xxx-yyy')],
+            'delete_ovs_vif_port': [mock.call('br0', 'qvovif-xxx-yyy',
+                                              timeout=120)]
+        }
+        with nested(
+                mock.patch.object(linux_net, 'device_exists',
+                                  return_value=False),
+                mock.patch.object(linux_net, 'delete_ovs_vif_port')
+        ) as (device_exists, delete_ovs_vif_port):
+            plugin = ovs_hybrid.OvsHybridPlugin.load("ovs_hybrid")
+            plugin.unplug(self.vif_ovs, self.instance)
+            device_exists.assert_has_calls(calls['device_exists'])
+            delete_ovs_vif_port.assert_has_calls(calls['delete_ovs_vif_port'])