Merge "Add duplicated idempotent_ids checks"
diff --git a/.zuul.yaml b/.zuul.yaml
index 7e8e532..ba7e158 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,9 +1,104 @@
-- project:
-    name: openstack/neutron-tempest-plugin
+- job:
+    name: neutron-tempest-plugin-api
+    parent: legacy-dsvm-base
+    run: playbooks/neutron-tempest-plugin-api/run.yaml
+    post-run: playbooks/neutron-tempest-plugin-api/post.yaml
+    timeout: 10000
+    required-projects:
+      - openstack-infra/devstack-gate
+      - openstack/neutron
+      - openstack/neutron-tempest-plugin
+      - openstack/tempest
+    irrelevant-files:
+      - ^(test-|)requirements.txt$
+      - ^releasenotes/.*$
+      - ^doc/.*$
+      - ^setup.cfg$
+
+- job:
+    name: neutron-tempest-plugin-scenario-linuxbridge
+    parent: legacy-dsvm-base
+    run: playbooks/neutron-tempest-plugin-scenario-linuxbridge/run.yaml
+    post-run: playbooks/neutron-tempest-plugin-scenario-linuxbridge/post.yaml
+    timeout: 10000
+    required-projects:
+      - openstack-infra/devstack-gate
+      - openstack/neutron
+      - openstack/neutron-tempest-plugin
+      - openstack/tempest
+    irrelevant-files:
+      - ^(test-|)requirements.txt$
+      - ^releasenotes/.*$
+      - ^doc/.*$
+      - ^setup.cfg$
+    voting: false
+
+- job:
+    name: neutron-tempest-plugin-dvr-multinode-scenario
+    parent: legacy-dsvm-base-multinode
+    run: playbooks/neutron-tempest-plugin-dvr-multinode-scenario/run.yaml
+    post-run: playbooks/neutron-tempest-plugin-dvr-multinode-scenario/post.yaml
+    nodeset: legacy-ubuntu-xenial-2-node
+    timeout: 10800
+    required-projects:
+      - openstack-infra/devstack-gate
+      - openstack/neutron
+      - openstack/neutron-tempest-plugin
+      - openstack/tempest
+    irrelevant-files:
+      - ^(test-|)requirements.txt$
+      - ^releasenotes/.*$
+      - ^doc/.*$
+      - ^setup.cfg$
+    voting: false
+
+- job:
+    name: neutron-tempest-plugin-designate-scenario
+    parent: devstack-tempest
+    description: Neutron designate integration scenario
+    required-projects:
+      - openstack/designate
+      - openstack/designate-dashboard
+      - openstack/designate-tempest-plugin
+      - openstack/neutron
+      - openstack/neutron-tempest-plugin
+      - openstack/tempest
+    timeout: 3600
+    roles:
+      - zuul: openstack-dev/devstack
+    vars:
+      devstack_localrc:
+        DESIGNATE_BACKEND_DRIVER: bind9
+      devstack_plugins:
+        designate: git://git.openstack.org/openstack/designate.git
+        neutron: git://git.openstack.org/openstack/neutron.git
+        neutron-tempest-plugin: git://git.openstack.org/openstack/neutron-tempest-plugin.git
+      devstack_services:
+        cinder: False
+        designate: True
+        q-dns: True
+        tempest: True
+      tempest_test_regex: '^neutron_tempest_plugin\.scenario\.test_dns_integration'
+      tox_venvlist: all-plugin
+    irrelevant-files:
+      - ^(test-|)requirements.txt$
+      - ^releasenotes/.*$
+      - ^setup.cfg$
+    voting: false
+
+- project-template:
+    name: neutron-tempest-plugin-jobs
     check:
       jobs:
-        # These jobs are defined in http://git.openstack.org/cgit/openstack/neutron/tree/.zuul.yaml
-        # They are all non-voting.
+        - neutron-tempest-plugin-api
+        - neutron-tempest-plugin-designate-scenario
         - neutron-tempest-plugin-dvr-multinode-scenario
         - neutron-tempest-plugin-scenario-linuxbridge
+    gate:
+      jobs:
         - neutron-tempest-plugin-api
+
+- project:
+    name: openstack/neutron-tempest-plugin
+    templates:
+      - neutron-tempest-plugin-jobs
diff --git a/devstack/README.rst b/devstack/README.rst
new file mode 100644
index 0000000..e605fcf
--- /dev/null
+++ b/devstack/README.rst
@@ -0,0 +1,21 @@
+====================
+Enabling in Devstack
+====================
+
+**WARNING**: the stack.sh script must be run in a disposable VM that is not
+being created automatically, see the README.md file in the "devstack"
+repository.  See contrib/vagrant to create a vagrant VM.
+
+1. Download DevStack::
+
+    git clone https://git.openstack.org/openstack-dev/devstack.git
+    cd devstack
+
+2. Add this repo as an external repository::
+
+     > cat local.conf
+     [[local|localrc]]
+     enable_plugin neutron-tempest-plugin https://git.openstack.org/openstack/neutron-tempest-plugin
+
+3. run ``stack.sh``
+
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
new file mode 100644
index 0000000..a2009ea
--- /dev/null
+++ b/devstack/plugin.sh
@@ -0,0 +1,13 @@
+# install_neutron_tempest_plugin
+function install_neutron_tempest_plugin {
+    setup_dev_lib "neutron-tempest-plugin"
+}
+
+if [[ "$1" == "stack" ]]; then
+    case "$2" in
+        install)
+            echo_summary "Installing neutron-tempest-plugin"
+            install_neutron_tempest_plugin
+            ;;
+    esac
+fi
diff --git a/devstack/settings b/devstack/settings
new file mode 100644
index 0000000..614376f
--- /dev/null
+++ b/devstack/settings
@@ -0,0 +1,3 @@
+GITREPO["neutron-tempest-plugin"]=${NEUTRON_TEMPEST_REPO:-${GIT_BASE}/openstack/neutron-tempest-plugin.git}
+GITDIR["neutron-tempest-plugin"]=$DEST/neutron-tempest-plugin
+GITBRANCH["neutron-tempest-plugin"]=master
diff --git a/neutron_tempest_plugin/api/admin/test_external_network_extension.py b/neutron_tempest_plugin/api/admin/test_external_network_extension.py
index b59d10e..cd4635c 100644
--- a/neutron_tempest_plugin/api/admin/test_external_network_extension.py
+++ b/neutron_tempest_plugin/api/admin/test_external_network_extension.py
@@ -110,6 +110,47 @@
         body = self.admin_client.show_network(net_id)['network']
         self.assertTrue(body['router:external'])
 
+    @decorators.idempotent_id('beddbe9d-304c-4577-8bbe-9b61d0a449b4')
+    def test_external_conversion_on_policy_delete(self):
+        net_id = self._create_network(external=False)['id']
+        policy = self.admin_client.create_rbac_policy(
+            object_type='network', object_id=net_id,
+            action='access_as_external',
+            target_tenant='*')
+        body = self.admin_client.show_network(net_id)['network']
+        self.assertTrue(body['router:external'])
+        self.admin_client.delete_rbac_policy(policy['rbac_policy']['id'])
+        body = self.admin_client.show_network(net_id)['network']
+        self.assertFalse(body['router:external'])
+
+    @decorators.idempotent_id('fb938b8e-d1d2-4bf8-823a-1b3c79c98a25')
+    def test_external_conversion_on_one_policy_delete(self):
+        net_id = self._create_network(external=False)['id']
+        self.admin_client.create_rbac_policy(
+            object_type='network', object_id=net_id,
+            action='access_as_external',
+            target_tenant=self.admin_client.tenant_id)
+        body = self.admin_client.show_network(net_id)['network']
+        self.assertTrue(body['router:external'])
+        policy2 = self.admin_client.create_rbac_policy(
+            object_type='network', object_id=net_id,
+            action='access_as_external',
+            target_tenant=self.client2.tenant_id)
+        self.admin_client.delete_rbac_policy(policy2['rbac_policy']['id'])
+        body = self.admin_client.show_network(net_id)['network']
+        self.assertTrue(body['router:external'])
+
+    @decorators.idempotent_id('046aff1a-3962-4e2b-9a3b-93a24f255fd0')
+    def test_external_network_on_shared_policy_delete(self):
+        net_id = self._create_network(external=True)['id']
+        policy = self.admin_client.create_rbac_policy(
+            object_type='network', object_id=net_id,
+            action='access_as_shared',
+            target_tenant='*')
+        self.admin_client.delete_rbac_policy(policy['rbac_policy']['id'])
+        body = self.admin_client.show_network(net_id)['network']
+        self.assertTrue(body['router:external'])
+
     @decorators.idempotent_id('01364c50-bfb6-46c4-b44c-edc4564d61cf')
     def test_policy_allows_tenant_to_allocate_floatingip(self):
         net = self._create_network(external=False)
diff --git a/neutron_tempest_plugin/api/admin/test_floating_ips_admin_actions.py b/neutron_tempest_plugin/api/admin/test_floating_ips_admin_actions.py
index 7601e7a..3607060 100644
--- a/neutron_tempest_plugin/api/admin/test_floating_ips_admin_actions.py
+++ b/neutron_tempest_plugin/api/admin/test_floating_ips_admin_actions.py
@@ -47,6 +47,7 @@
         body = self.client.create_floatingip(
             floating_network_id=self.ext_net_id)
         floating_ip = body['floatingip']
+        self.addCleanup(self.client.delete_floatingip, floating_ip['id'])
         project_id = self.create_project()['id']
 
         port = self.admin_client.create_port(network_id=self.network['id'],
diff --git a/neutron_tempest_plugin/api/admin/test_routers_ha.py b/neutron_tempest_plugin/api/admin/test_routers_ha.py
index fafe209..b8227bd 100644
--- a/neutron_tempest_plugin/api/admin/test_routers_ha.py
+++ b/neutron_tempest_plugin/api/admin/test_routers_ha.py
@@ -10,6 +10,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from tempest.common import utils as tutils
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 
@@ -19,6 +20,7 @@
 class RoutersTestHA(base.BaseRouterTest):
 
     required_extensions = ['router', 'l3-ha']
+    HA_NETWORK_NAME_TEMPL = "HA network tenant %s"
 
     @classmethod
     def resource_setup(cls):
@@ -31,8 +33,8 @@
         super(RoutersTestHA, cls).resource_setup()
         name = data_utils.rand_name('pretest-check')
         router = cls.admin_client.create_router(name)
+        cls.admin_client.delete_router(router['router']['id'])
         if 'ha' not in router['router']:
-            cls.admin_client.delete_router(router['router']['id'])
             msg = "'ha' attribute not found. HA Possibly not enabled"
             raise cls.skipException(msg)
 
@@ -46,10 +48,8 @@
         The router is created and the "ha" attribute is set to True
         """
         name = data_utils.rand_name('router')
-        router = self.admin_client.create_router(name, ha=True)
-        self.addCleanup(self.admin_client.delete_router,
-                        router['router']['id'])
-        self.assertTrue(router['router']['ha'])
+        router = self._create_admin_router(name, ha=True)
+        self.assertTrue(router['ha'])
 
     @decorators.idempotent_id('97b5f7ef-2192-4fa3-901e-979cd5c1097a')
     def test_legacy_router_creation(self):
@@ -63,10 +63,8 @@
         as opposed to a "High Availability Router"
         """
         name = data_utils.rand_name('router')
-        router = self.admin_client.create_router(name, ha=False)
-        self.addCleanup(self.admin_client.delete_router,
-                        router['router']['id'])
-        self.assertFalse(router['router']['ha'])
+        router = self.create_admin_router(name, ha=False)
+        self.assertFalse(router['ha'])
 
     @decorators.idempotent_id('5a6bfe82-5b23-45a4-b027-5160997d4753')
     def test_legacy_router_update_to_ha(self):
@@ -82,11 +80,36 @@
         """
         name = data_utils.rand_name('router')
         # router needs to be in admin state down in order to be upgraded to HA
-        router = self.admin_client.create_router(name, ha=False,
-                                                 admin_state_up=False)
-        self.addCleanup(self.admin_client.delete_router,
-                        router['router']['id'])
-        self.assertFalse(router['router']['ha'])
-        router = self.admin_client.update_router(router['router']['id'],
+        router = self._create_admin_router(name, ha=False,
+                                           admin_state_up=False)
+        self.assertFalse(router['ha'])
+        router = self.admin_client.update_router(router['id'],
                                                  ha=True)
         self.assertTrue(router['router']['ha'])
+
+    @decorators.idempotent_id('0d8c0c8f-3809-4acc-a2c8-e0941333ff6c')
+    @tutils.requires_ext(extension="provider", service="network")
+    def test_delete_ha_router_keeps_ha_network_segment_data(self):
+        """Test deleting an HA router keeps correct segment data for network.
+
+        Each tenant with HA router has an HA network. The HA network is a
+        normal tenant network with segmentation data like type (vxlan) and
+        segmenation id. This test makes sure that after an HA router is
+        deleted, those segmentation data are kept in HA network. This tests
+        regression of https://bugs.launchpad.net/neutron/+bug/1732543.
+        """
+        for i in range(2):
+            router = self._create_admin_router(
+                data_utils.rand_name('router%d' % i),
+                ha=True)
+        ha_net_name = self.HA_NETWORK_NAME_TEMPL % router['tenant_id']
+        ha_network_pre_delete = self.admin_client.list_networks(
+            name=ha_net_name)['networks'][0]
+        segmentation_id = ha_network_pre_delete['provider:segmentation_id']
+        self._delete_router(router['id'], self.admin_client)
+
+        ha_network_post_delete = self.admin_client.show_network(
+            ha_network_pre_delete['id'])['network']
+        self.assertEqual(
+            ha_network_post_delete['provider:segmentation_id'],
+            segmentation_id)
diff --git a/neutron_tempest_plugin/api/base.py b/neutron_tempest_plugin/api/base.py
index 8db5108..68b5680 100644
--- a/neutron_tempest_plugin/api/base.py
+++ b/neutron_tempest_plugin/api/base.py
@@ -69,6 +69,9 @@
             force_new=force_new
         )
         # Neutron uses a different clients manager than the one in the Tempest
+        # save the original in case mixed tests need it
+        if credential_type == 'primary':
+            cls.os_tempest = manager
         return clients.Manager(manager.credentials)
 
     @classmethod
@@ -412,17 +415,18 @@
         return qos_rule
 
     @classmethod
-    def delete_router(cls, router):
-        body = cls.client.list_router_interfaces(router['id'])
+    def delete_router(cls, router, client=None):
+        client = client or cls.client
+        body = client.list_router_interfaces(router['id'])
         interfaces = [port for port in body['ports']
                       if port['device_owner'] in const.ROUTER_INTERFACE_OWNERS]
         for i in interfaces:
             try:
-                cls.client.remove_router_interface_with_subnet_id(
+                client.remove_router_interface_with_subnet_id(
                     router['id'], i['fixed_ips'][0]['subnet_id'])
             except lib_exc.NotFound:
                 pass
-        cls.client.delete_router(router['id'])
+        client.delete_router(router['id'])
 
     @classmethod
     def create_address_scope(cls, name, is_admin=False, **kwargs):
diff --git a/neutron_tempest_plugin/api/base_routers.py b/neutron_tempest_plugin/api/base_routers.py
index c8d3783..52db742 100644
--- a/neutron_tempest_plugin/api/base_routers.py
+++ b/neutron_tempest_plugin/api/base_routers.py
@@ -13,6 +13,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from tempest.lib import exceptions
+
 from neutron_tempest_plugin.api import base
 
 
@@ -21,9 +23,12 @@
     # as some router operations, such as enabling or disabling SNAT
     # require admin credentials by default
 
-    def _cleanup_router(self, router):
-        self.delete_router(router)
-        self.routers.remove(router)
+    def _cleanup_router(self, router, client=None):
+        try:
+            self.delete_router(router, client)
+            self.routers.remove(router)
+        except exceptions.NotFound:
+            pass
 
     def _create_router(self, name, admin_state_up=False,
                        external_network_id=None, enable_snat=None):
@@ -33,6 +38,12 @@
         self.addCleanup(self._cleanup_router, router)
         return router
 
+    def _create_admin_router(self, *args, **kwargs):
+        router = self.create_admin_router(*args, **kwargs)
+        self.addCleanup(
+            self._cleanup_router, router, self.os_admin.network_client)
+        return router
+
     def _delete_router(self, router_id, network_client=None):
         client = network_client or self.client
         client.delete_router(router_id)
diff --git a/neutron_tempest_plugin/api/test_trunk.py b/neutron_tempest_plugin/api/test_trunk.py
index 6c781ab..e02cf92 100644
--- a/neutron_tempest_plugin/api/test_trunk.py
+++ b/neutron_tempest_plugin/api/test_trunk.py
@@ -54,8 +54,20 @@
         trunks_cleanup(cls.client, cls.trunks)
         super(TrunkTestJSONBase, cls).resource_cleanup()
 
-    def _create_trunk_with_network_and_parent(self, subports, **kwargs):
-        network = self.create_network()
+    @classmethod
+    def is_type_driver_enabled(cls, type_driver):
+        return (type_driver in
+                config.CONF.neutron_plugin_options.available_type_drivers)
+
+    def _create_trunk_with_network_and_parent(
+            self, subports, parent_network_type=None, **kwargs):
+        client = None
+        network_kwargs = {}
+        if parent_network_type:
+            client = self.admin_client
+            network_kwargs = {"provider:network_type": parent_network_type,
+                              "tenant_id": self.client.tenant_id}
+        network = self.create_network(client=client, **network_kwargs)
         parent_port = self.create_port(network)
         trunk = self.client.create_trunk(parent_port['id'], subports, **kwargs)
         self.trunks.append(trunk['trunk'])
@@ -266,9 +278,7 @@
     @classmethod
     def skip_checks(cls):
         super(TrunkTestMtusJSONBase, cls).skip_checks()
-        if any(t
-               not in config.CONF.neutron_plugin_options.available_type_drivers
-               for t in ['gre', 'vxlan']):
+        if not all(cls.is_type_driver_enabled(t) for t in ['gre', 'vxlan']):
             msg = "Either vxlan or gre type driver not enabled."
             raise cls.skipException(msg)
 
diff --git a/neutron_tempest_plugin/api/test_trunk_negative.py b/neutron_tempest_plugin/api/test_trunk_negative.py
index 699b26f..4d7ead1 100644
--- a/neutron_tempest_plugin/api/test_trunk_negative.py
+++ b/neutron_tempest_plugin/api/test_trunk_negative.py
@@ -13,6 +13,7 @@
 #    under the License.
 
 from oslo_utils import uuidutils
+from tempest.common import utils
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 import testtools
@@ -103,8 +104,14 @@
 
     @decorators.attr(type='negative')
     @decorators.idempotent_id('40aed9be-e976-47d0-dada-bde2c7e74e57')
+    @utils.requires_ext(extension="provider", service="network")
     def test_create_subport_invalid_inherit_network_segmentation_type(self):
-        trunk = self._create_trunk_with_network_and_parent([])
+        if not self.is_type_driver_enabled('vxlan'):
+            msg = "Vxlan type driver must be enabled for this test."
+            raise self.skipException(msg)
+
+        trunk = self._create_trunk_with_network_and_parent(
+            subports=[], parent_network_type='vxlan')
         subport_network = self.create_network()
         parent_port = self.create_port(subport_network)
         self.assertRaises(lib_exc.BadRequest, self.client.add_subports,
diff --git a/neutron_tempest_plugin/common/utils.py b/neutron_tempest_plugin/common/utils.py
index ecccd18..d6d0aee 100644
--- a/neutron_tempest_plugin/common/utils.py
+++ b/neutron_tempest_plugin/common/utils.py
@@ -19,6 +19,7 @@
 """Utilities and helper functions."""
 
 import eventlet
+import functools
 import threading
 import time
 
@@ -70,3 +71,19 @@
             #pylint: disable=raising-bad-type
             raise exception
         raise WaitTimeout("Timed out after %d seconds" % timeout)
+
+
+# TODO(haleyb): move to neutron-lib
+# code copied from neutron repository - neutron/tests/base.py
+def unstable_test(reason):
+    def decor(f):
+        @functools.wraps(f)
+        def inner(self, *args, **kwargs):
+            try:
+                return f(self, *args, **kwargs)
+            except Exception as e:
+                msg = ("%s was marked as unstable because of %s, "
+                       "failure was: %s") % (self.id(), reason, e)
+                raise self.skipTest(msg)
+        return inner
+    return decor
diff --git a/neutron_tempest_plugin/config.py b/neutron_tempest_plugin/config.py
index 804fece..d6db315 100644
--- a/neutron_tempest_plugin/config.py
+++ b/neutron_tempest_plugin/config.py
@@ -38,6 +38,11 @@
                help='The availability zone for all agents in the deployment. '
                     'Configure this only when the single value is used by '
                     'all agents in the deployment.'),
+    cfg.IntOpt('max_networks_per_project',
+               default=4,
+               help='Max number of networks per project. '
+                    'Configure this only when project is limited with real '
+                    'vlans in deployment.'),
 ]
 
 # TODO(amuller): Redo configuration options registration as part of the planned
diff --git a/neutron_tempest_plugin/scenario/base.py b/neutron_tempest_plugin/scenario/base.py
index 9fe6a94..2bb6344 100644
--- a/neutron_tempest_plugin/scenario/base.py
+++ b/neutron_tempest_plugin/scenario/base.py
@@ -76,27 +76,24 @@
                 zone, and Y is the compute host name.
         """
 
-        name = kwargs.get('name', data_utils.rand_name('server-test'))
-        security_groups = kwargs.get(
-            'security_groups', [{'name': 'default'}])
-        availability_zone = kwargs.get('availability_zone')
+        kwargs.setdefault('name', data_utils.rand_name('server-test'))
 
-        server_args = {
-            'name': name,
-            'flavorRef': flavor_ref,
-            'imageRef': image_ref,
-            'key_name': key_name,
-            'networks': networks,
-            'security_groups': security_groups
-        }
+        # We cannot use setdefault() here because caller could have passed
+        # security_groups=None and we don't want to pass None to
+        # client.create_server()
+        if not kwargs.get('security_groups'):
+            kwargs['security_groups'] = [{'name': 'default'}]
 
-        if availability_zone:
-            server_args['availability_zone'] = availability_zone
+        client = self.os_primary.servers_client
+        if kwargs.get('availability_zone'):
             client = self.os_admin.servers_client
-        else:
-            client = self.os_primary.servers_client
 
-        server = client.create_server(**server_args)
+        server = client.create_server(
+            flavorRef=flavor_ref,
+            imageRef=image_ref,
+            key_name=key_name,
+            networks=networks,
+            **kwargs)
 
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
                         waiters.wait_for_server_termination,
@@ -119,8 +116,8 @@
         return body['keypair']
 
     @classmethod
-    def create_secgroup_rules(cls, rule_list, client=None,
-                              secgroup_id=None):
+    def create_secgroup_rules(cls, rule_list, secgroup_id=None,
+                              client=None):
         client = client or cls.os_primary.network_client
         if not secgroup_id:
             sgs = client.list_security_groups()['security_groups']
@@ -153,7 +150,7 @@
                       'port_range_max': 22,
                       'remote_ip_prefix': '0.0.0.0/0'}]
         client = client or cls.os_primary.network_client
-        cls.create_secgroup_rules(rule_list, client,
+        cls.create_secgroup_rules(rule_list, client=client,
                                   secgroup_id=secgroup_id)
 
     @classmethod
@@ -168,7 +165,7 @@
                       'port_range_max': 0,  # code
                       'remote_ip_prefix': '0.0.0.0/0'}]
         client = client or cls.os_primary.network_client
-        cls.create_secgroup_rules(rule_list, client,
+        cls.create_secgroup_rules(rule_list, client=client,
                                   secgroup_id=secgroup_id)
 
     @classmethod
diff --git a/neutron_tempest_plugin/scenario/test_dns_integration.py b/neutron_tempest_plugin/scenario/test_dns_integration.py
new file mode 100644
index 0000000..923f013
--- /dev/null
+++ b/neutron_tempest_plugin/scenario/test_dns_integration.py
@@ -0,0 +1,123 @@
+# Copyright (c) 2017 x-ion GmbH
+# 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.
+
+import ipaddress
+
+import testtools
+
+from tempest.common import utils
+from tempest.common import waiters
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from neutron_tempest_plugin import config
+from neutron_tempest_plugin.scenario import base
+from neutron_tempest_plugin.scenario import constants
+
+
+CONF = config.CONF
+
+# Note(jh): Need to do a bit of juggling here in order to avoid failures
+# when designate_tempest_plugin is not available
+dns_base = testtools.try_import('designate_tempest_plugin.tests.base')
+dns_waiters = testtools.try_import('designate_tempest_plugin.common.waiters')
+if dns_base:
+    DNSMixin = dns_base.BaseDnsV2Test
+else:
+    DNSMixin = object
+
+
+class DNSIntegrationTests(base.BaseTempestTestCase, DNSMixin):
+    credentials = ['primary']
+
+    @classmethod
+    def setup_clients(cls):
+        super(DNSIntegrationTests, cls).setup_clients()
+        cls.dns_client = cls.os_tempest.zones_client
+        cls.query_client = cls.os_tempest.query_client
+        cls.query_client.build_timeout = 30
+
+    @classmethod
+    def skip_checks(cls):
+        super(DNSIntegrationTests, cls).skip_checks()
+        if not ('designate' in CONF.service_available and
+                CONF.service_available.designate):
+            raise cls.skipException("Designate support is required")
+        if not (dns_base and dns_waiters):
+            raise cls.skipException("Designate tempest plugin is missing")
+
+    @classmethod
+    @utils.requires_ext(extension="dns-integration", service="network")
+    def resource_setup(cls):
+        super(DNSIntegrationTests, cls).resource_setup()
+        _, cls.zone = cls.dns_client.create_zone()
+        cls.addClassResourceCleanup(cls.dns_client.delete_zone,
+            cls.zone['id'], ignore_errors=lib_exc.NotFound)
+        dns_waiters.wait_for_zone_status(
+            cls.dns_client, cls.zone['id'], 'ACTIVE')
+
+        cls.network = cls.create_network(dns_domain=cls.zone['name'])
+        cls.subnet = cls.create_subnet(cls.network)
+        cls.router = cls.create_router_by_client()
+        cls.create_router_interface(cls.router['id'], cls.subnet['id'])
+        cls.keypair = cls.create_keypair()
+
+    def _create_floatingip_with_dns(self, dns_name):
+        fip = self.os_primary.network_client.create_floatingip(
+            CONF.network.public_network_id, dns_name=dns_name,
+            dns_domain=self.zone['name'])['floatingip']
+        self.floating_ips.append(fip)
+        return fip
+
+    def _create_server(self, name=None):
+        port = self.create_port(self.network)
+        server = self.create_server(
+            flavor_ref=CONF.compute.flavor_ref,
+            image_ref=CONF.compute.image_ref,
+            key_name=self.keypair['name'], name=name,
+            networks=[{'port': port['id']}])['server']
+        waiters.wait_for_server_status(self.os_primary.servers_client,
+                                       server['id'],
+                                       constants.SERVER_STATUS_ACTIVE)
+        fip = self.create_and_associate_floatingip(port['id'])
+        return {'port': port, 'fip': fip, 'server': server}
+
+    def _verify_dns_records(self, address, name):
+        forward = name + '.' + self.zone['name']
+        reverse = ipaddress.ip_address(address).reverse_pointer
+        dns_waiters.wait_for_query(self.query_client, forward, 'A')
+        dns_waiters.wait_for_query(self.query_client, reverse, 'PTR')
+        fwd_response = self.query_client.query(forward, 'A')
+        rev_response = self.query_client.query(reverse, 'PTR')
+        for r in fwd_response:
+            for rr in r.answer:
+                self.assertIn(address, rr.to_text())
+        for r in rev_response:
+            for rr in r.answer:
+                self.assertIn(forward, rr.to_text())
+
+    @decorators.idempotent_id('850ee378-4b5a-4f71-960e-0e7b12e03a34')
+    def test_server_with_fip(self):
+        name = data_utils.rand_name('server-test')
+        server = self._create_server(name=name)
+        server_ip = server['fip']['floating_ip_address']
+        self._verify_dns_records(server_ip, name)
+
+    @decorators.idempotent_id('a8f2fade-8d5c-40f9-80f0-3de4b8d91985')
+    def test_fip(self):
+        name = data_utils.rand_name('fip-test')
+        fip = self._create_floatingip_with_dns(name)
+        self._verify_dns_records(fip['floating_ip_address'], name)
diff --git a/neutron_tempest_plugin/scenario/test_floatingip.py b/neutron_tempest_plugin/scenario/test_floatingip.py
index 5fcbdc0..0febce2 100644
--- a/neutron_tempest_plugin/scenario/test_floatingip.py
+++ b/neutron_tempest_plugin/scenario/test_floatingip.py
@@ -22,6 +22,7 @@
 from testscenarios.scenarios import multiply_scenarios
 
 from neutron_tempest_plugin.common import ssh
+from neutron_tempest_plugin.common import utils as common_utils
 from neutron_tempest_plugin import config
 from neutron_tempest_plugin.scenario import base
 from neutron_tempest_plugin.scenario import constants
@@ -134,6 +135,7 @@
 
     same_network = True
 
+    @common_utils.unstable_test("bug 1717302")
     @decorators.idempotent_id('05c4e3b3-7319-4052-90ad-e8916436c23b')
     def test_east_west(self):
         self._test_east_west()
@@ -151,6 +153,7 @@
 
     same_network = False
 
+    @common_utils.unstable_test("bug 1717302")
     @decorators.idempotent_id('f18f0090-3289-4783-b956-a0f8ac511e8b')
     def test_east_west(self):
         self._test_east_west()
diff --git a/neutron_tempest_plugin/scenario/test_security_groups.py b/neutron_tempest_plugin/scenario/test_security_groups.py
index faaeb84..9503fe3 100644
--- a/neutron_tempest_plugin/scenario/test_security_groups.py
+++ b/neutron_tempest_plugin/scenario/test_security_groups.py
@@ -26,13 +26,13 @@
 CONF = config.CONF
 
 
-class NetworkDefaultSecGroupTest(base.BaseTempestTestCase):
+class NetworkSecGroupTest(base.BaseTempestTestCase):
     credentials = ['primary', 'admin']
     required_extensions = ['router', 'security-group']
 
     @classmethod
     def resource_setup(cls):
-        super(NetworkDefaultSecGroupTest, cls).resource_setup()
+        super(NetworkSecGroupTest, cls).resource_setup()
         # setup basic topology for servers we can log into it
         cls.network = cls.create_network()
         cls.subnet = cls.create_subnet(cls.network)
@@ -40,15 +40,26 @@
         cls.create_router_interface(router['id'], cls.subnet['id'])
         cls.keypair = cls.create_keypair()
 
-    def create_vm_testing_sec_grp(self, num_servers=2, security_groups=None):
+    def create_vm_testing_sec_grp(self, num_servers=2, security_groups=None,
+                                  ports=None):
+        """Create instance for security group testing
+        :param num_servers (int): number of servers to spawn
+        :param security_groups (list): list of security groups
+        :param ports* (list): list of ports
+        *Needs to be the same length as num_servers
+        """
         servers, fips, server_ssh_clients = ([], [], [])
         for i in range(num_servers):
-            servers.append(self.create_server(
-                flavor_ref=CONF.compute.flavor_ref,
-                image_ref=CONF.compute.image_ref,
-                key_name=self.keypair['name'],
-                networks=[{'uuid': self.network['id']}],
-                security_groups=security_groups))
+            server_args = {
+                'flavor_ref': CONF.compute.flavor_ref,
+                'image_ref': CONF.compute.image_ref,
+                'key_name': self.keypair['name'],
+                'networks': [{'uuid': self.network['id']}],
+                'security_groups': security_groups
+            }
+            if ports is not None:
+                server_args['networks'][0].update({'port': ports[i]['id']})
+            servers.append(self.create_server(**server_args))
         for i, server in enumerate(servers):
             waiters.wait_for_server_status(
                 self.os_primary.servers_client, server['server']['id'],
@@ -62,6 +73,34 @@
                 pkey=self.keypair['private_key']))
         return server_ssh_clients, fips, servers
 
+    def _test_ip_prefix(self, rule_list, should_succeed):
+        # Add specific remote prefix to VMs and check connectivity
+        ssh_secgrp_name = data_utils.rand_name('ssh_secgrp')
+        icmp_secgrp_name = data_utils.rand_name('icmp_secgrp_with_cidr')
+        ssh_secgrp = self.os_primary.network_client.create_security_group(
+            name=ssh_secgrp_name)
+        self.create_loginable_secgroup_rule(
+            secgroup_id=ssh_secgrp['security_group']['id'])
+        icmp_secgrp = self.os_primary.network_client.create_security_group(
+            name=icmp_secgrp_name)
+        self.create_secgroup_rules(
+            rule_list, secgroup_id=icmp_secgrp['security_group']['id'])
+        for sec_grp in (ssh_secgrp, icmp_secgrp):
+            self.security_groups.append(sec_grp['security_group'])
+        security_groups_list = [{'name': ssh_secgrp_name},
+                                {'name': icmp_secgrp_name}]
+        server_ssh_clients, fips, servers = self.create_vm_testing_sec_grp(
+            security_groups=security_groups_list)
+
+        # make sure ssh connectivity works
+        self.check_connectivity(fips[0]['floating_ip_address'],
+                                CONF.validation.image_ssh_user,
+                                self.keypair['private_key'])
+
+        # make sure ICMP connectivity works
+        self.check_remote_connectivity(server_ssh_clients[0], fips[1][
+            'fixed_ip_address'], should_succeed=should_succeed)
+
     @decorators.idempotent_id('3d73ec1a-2ec6-45a9-b0f8-04a283d9d764')
     def test_default_sec_grp_scenarios(self):
         server_ssh_clients, fips, _ = self.create_vm_testing_sec_grp()
@@ -167,34 +206,85 @@
 
     @decorators.idempotent_id('3d73ec1a-2ec6-45a9-b0f8-04a283d9d664')
     def test_ip_prefix(self):
-        # Add specific remote prefix to VMs and check connectivity
-        ssh_secgrp_name = data_utils.rand_name('ssh_secgrp')
-        icmp_secgrp_name = data_utils.rand_name('icmp_secgrp_with_cidr')
         cidr = self.subnet['cidr']
-        ssh_secgrp = self.os_primary.network_client.create_security_group(
-            name=ssh_secgrp_name)
-        self.create_loginable_secgroup_rule(
-            secgroup_id=ssh_secgrp['security_group']['id'])
-
         rule_list = [{'protocol': constants.PROTO_NUM_ICMP,
                       'direction': constants.INGRESS_DIRECTION,
                       'remote_ip_prefix': cidr}]
-        icmp_secgrp = self.os_primary.network_client.create_security_group(
-            name=icmp_secgrp_name)
-        self.create_secgroup_rules(
-            rule_list, secgroup_id=icmp_secgrp['security_group']['id'])
-        for sec_grp in (ssh_secgrp, icmp_secgrp):
-            self.security_groups.append(sec_grp['security_group'])
-        security_groups_list = [{'name': ssh_secgrp_name},
-                                {'name': icmp_secgrp_name}]
+        self._test_ip_prefix(rule_list, should_succeed=True)
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('a01cd2ef-3cfc-4614-8aac-9d1333ea21dd')
+    def test_ip_prefix_negative(self):
+        # define bad CIDR
+        cidr = '10.100.0.254/32'
+        rule_list = [{'protocol': constants.PROTO_NUM_ICMP,
+                      'direction': constants.INGRESS_DIRECTION,
+                      'remote_ip_prefix': cidr}]
+        self._test_ip_prefix(rule_list, should_succeed=False)
+
+    @decorators.idempotent_id('7ed39b86-006d-40fb-887a-ae46693dabc9')
+    def test_remote_group(self):
+        # create a new sec group
+        ssh_secgrp_name = data_utils.rand_name('ssh_secgrp')
+        ssh_secgrp = self.os_primary.network_client.create_security_group(
+            name=ssh_secgrp_name)
+        # add cleanup
+        self.security_groups.append(ssh_secgrp['security_group'])
+        # configure sec group to support SSH connectivity
+        self.create_loginable_secgroup_rule(
+            secgroup_id=ssh_secgrp['security_group']['id'])
+        # spawn two instances with the sec group created
         server_ssh_clients, fips, servers = self.create_vm_testing_sec_grp(
-            security_groups=security_groups_list)
+            security_groups=[{'name': ssh_secgrp_name}])
+        # verify SSH functionality
+        for i in range(2):
+            self.check_connectivity(fips[i]['floating_ip_address'],
+                                    CONF.validation.image_ssh_user,
+                                    self.keypair['private_key'])
+        # try to ping instances without ICMP permissions
+        self.check_remote_connectivity(
+            server_ssh_clients[0], fips[1]['fixed_ip_address'],
+            should_succeed=False)
+        # add ICMP support to the remote group
+        rule_list = [{'protocol': constants.PROTO_NUM_ICMP,
+                      'direction': constants.INGRESS_DIRECTION,
+                      'remote_group_id': ssh_secgrp['security_group']['id']}]
+        self.create_secgroup_rules(
+            rule_list, secgroup_id=ssh_secgrp['security_group']['id'])
+        # verify ICMP connectivity between instances works
+        self.check_remote_connectivity(
+            server_ssh_clients[0], fips[1]['fixed_ip_address'])
+        # make sure ICMP connectivity doesn't work from framework
+        self.ping_ip_address(fips[0]['floating_ip_address'],
+                             should_succeed=False)
 
-        # make sure ssh connectivity works
-        self.check_connectivity(fips[0]['floating_ip_address'],
-                                CONF.validation.image_ssh_user,
-                                self.keypair['private_key'])
-
-        # make sure ICMP connectivity works
-        self.check_remote_connectivity(server_ssh_clients[0], fips[1][
-            'fixed_ip_address'])
+    @decorators.idempotent_id('f07d0159-8f9e-4faa-87f5-a869ab0ad488')
+    def test_multiple_ports_secgroup_inheritance(self):
+        """This test creates two ports with security groups, then
+        boots two instances and verify that the security group was
+        inherited properly and enforced in these instances.
+        """
+        # create a security group and make it loginable and pingable
+        secgrp = self.os_primary.network_client.create_security_group(
+            name=data_utils.rand_name('secgrp'))
+        self.create_loginable_secgroup_rule(
+            secgroup_id=secgrp['security_group']['id'])
+        self.create_pingable_secgroup_rule(
+            secgroup_id=secgrp['security_group']['id'])
+        # add security group to cleanup
+        self.security_groups.append(secgrp['security_group'])
+        # create two ports with fixed IPs and the security group created
+        ports = []
+        for i in range(2):
+            ports.append(self.create_port(
+                self.network, fixed_ips=[{'subnet_id': self.subnets[0]['id']}],
+                security_groups=[secgrp['security_group']['id']]))
+        # spawn instances with the ports created
+        server_ssh_clients, fips, servers = self.create_vm_testing_sec_grp(
+            ports=ports)
+        # verify ICMP reachability and ssh connectivity
+        for fip in fips:
+            self.ping_ip_address(fip['floating_ip_address'])
+            self.check_connectivity(fip['floating_ip_address'],
+                                    CONF.validation.image_ssh_user,
+                                    self.keypair['private_key'])
diff --git a/neutron_tempest_plugin/scenario/test_trunk.py b/neutron_tempest_plugin/scenario/test_trunk.py
index 0008b0a..bb4266c 100644
--- a/neutron_tempest_plugin/scenario/test_trunk.py
+++ b/neutron_tempest_plugin/scenario/test_trunk.py
@@ -145,6 +145,7 @@
                                 CONF.validation.image_ssh_user,
                                 self.keypair['private_key'])
 
+    @utils.unstable_test("bug 1740885")
     @decorators.idempotent_id('bb13fe28-f152-4000-8131-37890a40c79e')
     def test_trunk_subport_lifecycle(self):
         """Test trunk creation and subport transition to ACTIVE status.
@@ -173,9 +174,12 @@
             exception=RuntimeError("Timed out waiting for trunk %s to "
                                    "transition to ACTIVE." % trunk2_id))
         # create a few more networks and ports for subports
+        # check limit of networks per project
+        max_vlan = 3 + CONF.neutron_plugin_options.max_networks_per_project
+        allowed_vlans = range(3, max_vlan)
         subports = [{'port_id': self.create_port(self.create_network())['id'],
                      'segmentation_type': 'vlan', 'segmentation_id': seg_id}
-                    for seg_id in range(3, 7)]
+                    for seg_id in allowed_vlans]
         # add all subports to server1
         self.client.add_subports(trunk1_id, subports)
         # ensure trunk transitions to ACTIVE
@@ -225,6 +229,7 @@
     @testtools.skipUnless(
           CONF.neutron_plugin_options.image_is_advanced,
           "Advanced image is required to run this test.")
+    @utils.unstable_test("bug 1740885")
     @decorators.idempotent_id('a8a02c9b-b453-49b5-89a2-cce7da66aafb')
     def test_subport_connectivity(self):
         vlan_tag = 10
diff --git a/playbooks/neutron-tempest-plugin-api/post.yaml b/playbooks/neutron-tempest-plugin-api/post.yaml
new file mode 100644
index 0000000..dac8753
--- /dev/null
+++ b/playbooks/neutron-tempest-plugin-api/post.yaml
@@ -0,0 +1,80 @@
+- hosts: primary
+  tasks:
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=**/*nose_results.html
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=**/*testr_results.html.gz
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=/.testrepository/tmp*
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=**/*testrepository.subunit.gz
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}/tox'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=/.tox/*/log/*
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=/logs/**
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
diff --git a/playbooks/neutron-tempest-plugin-api/run.yaml b/playbooks/neutron-tempest-plugin-api/run.yaml
new file mode 100644
index 0000000..12638cc
--- /dev/null
+++ b/playbooks/neutron-tempest-plugin-api/run.yaml
@@ -0,0 +1,58 @@
+- hosts: all
+  name: neutron-tempest-plugin-api
+  tasks:
+
+    - name: Ensure legacy workspace directory
+      file:
+        path: '{{ ansible_user_dir }}/workspace'
+        state: directory
+
+    - shell:
+        cmd: |
+          set -e
+          set -x
+          cat > clonemap.yaml << EOF
+          clonemap:
+            - name: openstack-infra/devstack-gate
+              dest: devstack-gate
+          EOF
+          /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \
+              git://git.openstack.org \
+              openstack-infra/devstack-gate
+        executable: /bin/bash
+        chdir: '{{ ansible_user_dir }}/workspace'
+      environment: '{{ zuul | zuul_legacy_vars }}'
+
+    - shell:
+        cmd: |
+          set -e
+          set -x
+          export PYTHONUNBUFFERED=true
+          export DEVSTACK_GATE_TEMPEST=1
+          export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
+          export DEVSTACK_GATE_NEUTRON=1
+          export DEVSTACK_GATE_EXERCISES=0
+          export DEVSTACK_GATE_TEMPEST_REGEX="neutron_tempest_plugin.api"
+          export DEVSTACK_LOCAL_CONFIG="enable_plugin neutron-tempest-plugin git://git.openstack.org/openstack/neutron-tempest-plugin"
+          export BRANCH_OVERRIDE=default
+          if [ "$BRANCH_OVERRIDE" != "default" ] ; then
+              export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE
+          fi
+
+          export PROJECTS="openstack/neutron-tempest-plugin $PROJECTS"
+
+          function gate_hook {
+              bash -xe $BASE/new/neutron/neutron/tests/contrib/gate_hook.sh api
+          }
+          export -f gate_hook
+
+          function post_test_hook {
+              bash -xe $BASE/new/neutron/neutron/tests/contrib/post_test_hook.sh api
+          }
+          export -f post_test_hook
+
+          cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh
+          ./safe-devstack-vm-gate-wrap.sh
+        executable: /bin/bash
+        chdir: '{{ ansible_user_dir }}/workspace'
+      environment: '{{ zuul | zuul_legacy_vars }}'
diff --git a/playbooks/neutron-tempest-plugin-dvr-multinode-scenario/post.yaml b/playbooks/neutron-tempest-plugin-dvr-multinode-scenario/post.yaml
new file mode 100644
index 0000000..e07f551
--- /dev/null
+++ b/playbooks/neutron-tempest-plugin-dvr-multinode-scenario/post.yaml
@@ -0,0 +1,15 @@
+- hosts: primary
+  tasks:
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=/logs/**
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
diff --git a/playbooks/neutron-tempest-plugin-dvr-multinode-scenario/run.yaml b/playbooks/neutron-tempest-plugin-dvr-multinode-scenario/run.yaml
new file mode 100644
index 0000000..2102bb5
--- /dev/null
+++ b/playbooks/neutron-tempest-plugin-dvr-multinode-scenario/run.yaml
@@ -0,0 +1,61 @@
+- hosts: primary
+  name: neutron-tempest-plugin-dvr-multinode-scenario
+  tasks:
+
+    - name: Ensure legacy workspace directory
+      file:
+        path: '{{ ansible_user_dir }}/workspace'
+        state: directory
+
+    - shell:
+        cmd: |
+          set -e
+          set -x
+          cat > clonemap.yaml << EOF
+          clonemap:
+            - name: openstack-infra/devstack-gate
+              dest: devstack-gate
+          EOF
+          /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \
+              git://git.openstack.org \
+              openstack-infra/devstack-gate
+        executable: /bin/bash
+        chdir: '{{ ansible_user_dir }}/workspace'
+      environment: '{{ zuul | zuul_legacy_vars }}'
+
+    - shell:
+        cmd: |
+          set -e
+          set -x
+          export PYTHONUNBUFFERED=true
+          export DEVSTACK_GATE_TEMPEST=1
+          export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
+          export DEVSTACK_GATE_NEUTRON=1
+          export DEVSTACK_GATE_CONFIGDRIVE=0
+          export DEVSTACK_GATE_TEMPEST_REGEX="(neutron_tempest_plugin.scenario)"
+          export DEVSTACK_LOCAL_CONFIG="enable_plugin neutron-tempest-plugin git://git.openstack.org/openstack/neutron-tempest-plugin"
+          export TEMPEST_CONCURRENCY=2
+          # Test DVR works multinode
+          export DEVSTACK_GATE_NEUTRON_DVR=1
+          export BRANCH_OVERRIDE=default
+          if [ "$BRANCH_OVERRIDE" != "default" ] ; then
+              export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE
+          fi
+          export DEVSTACK_GATE_TOPOLOGY="multinode"
+          export PROJECTS="openstack/neutron-tempest-plugin $PROJECTS"
+
+          function gate_hook {
+              bash -xe $BASE/new/neutron/neutron/tests/contrib/gate_hook.sh dsvm-scenario-ovs
+          }
+          export -f gate_hook
+
+          function post_test_hook {
+              bash -xe $BASE/new/neutron/neutron/tests/contrib/post_test_hook.sh dsvm-scenario-ovs
+          }
+          export -f post_test_hook
+
+          cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh
+          ./safe-devstack-vm-gate-wrap.sh
+        executable: /bin/bash
+        chdir: '{{ ansible_user_dir }}/workspace'
+      environment: '{{ zuul | zuul_legacy_vars }}'
diff --git a/playbooks/neutron-tempest-plugin-scenario-linuxbridge/post.yaml b/playbooks/neutron-tempest-plugin-scenario-linuxbridge/post.yaml
new file mode 100644
index 0000000..dac8753
--- /dev/null
+++ b/playbooks/neutron-tempest-plugin-scenario-linuxbridge/post.yaml
@@ -0,0 +1,80 @@
+- hosts: primary
+  tasks:
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=**/*nose_results.html
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=**/*testr_results.html.gz
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=/.testrepository/tmp*
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=**/*testrepository.subunit.gz
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}/tox'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=/.tox/*/log/*
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
+
+    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
+      synchronize:
+        src: '{{ ansible_user_dir }}/workspace/'
+        dest: '{{ zuul.executor.log_root }}'
+        mode: pull
+        copy_links: true
+        verify_host: true
+        rsync_opts:
+          - --include=/logs/**
+          - --include=*/
+          - --exclude=*
+          - --prune-empty-dirs
diff --git a/playbooks/neutron-tempest-plugin-scenario-linuxbridge/run.yaml b/playbooks/neutron-tempest-plugin-scenario-linuxbridge/run.yaml
new file mode 100644
index 0000000..65e8b12
--- /dev/null
+++ b/playbooks/neutron-tempest-plugin-scenario-linuxbridge/run.yaml
@@ -0,0 +1,72 @@
+- hosts: all
+  name: neutron-tempest-plugin-scenario-linuxbridge
+  tasks:
+
+    - name: Ensure legacy workspace directory
+      file:
+        path: '{{ ansible_user_dir }}/workspace'
+        state: directory
+
+    - shell:
+        cmd: |
+          set -e
+          set -x
+          cat > clonemap.yaml << EOF
+          clonemap:
+            - name: openstack-infra/devstack-gate
+              dest: devstack-gate
+          EOF
+          /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \
+              git://git.openstack.org \
+              openstack-infra/devstack-gate
+        executable: /bin/bash
+        chdir: '{{ ansible_user_dir }}/workspace'
+      environment: '{{ zuul | zuul_legacy_vars }}'
+
+    - shell:
+        cmd: |
+          set -e
+          set -x
+          cat << 'EOF' >>"/tmp/dg-local.conf"
+          [[local|localrc]]
+          Q_AGENT=linuxbridge
+          PHYSICAL_NETWORK=default
+
+          EOF
+        executable: /bin/bash
+        chdir: '{{ ansible_user_dir }}/workspace'
+      environment: '{{ zuul | zuul_legacy_vars }}'
+
+    - shell:
+        cmd: |
+          set -e
+          set -x
+          export PYTHONUNBUFFERED=true
+          export DEVSTACK_GATE_TEMPEST=1
+          export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
+          export DEVSTACK_GATE_NEUTRON=1
+          export DEVSTACK_GATE_EXERCISES=0
+          export DEVSTACK_GATE_TEMPEST_REGEX="(neutron_tempest_plugin.scenario)"
+          export DEVSTACK_LOCAL_CONFIG="enable_plugin neutron-tempest-plugin git://git.openstack.org/openstack/neutron-tempest-plugin"
+          export BRANCH_OVERRIDE=default
+          if [ "$BRANCH_OVERRIDE" != "default" ] ; then
+              export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE
+          fi
+
+          export TEMPEST_CONCURRENCY=2
+          export PROJECTS="openstack/neutron-tempest-plugin $PROJECTS"
+          function gate_hook {
+              bash -xe $BASE/new/neutron/neutron/tests/contrib/gate_hook.sh dsvm-scenario-linuxbridge dvrskip
+          }
+          export -f gate_hook
+
+          function post_test_hook {
+              bash -xe $BASE/new/neutron/neutron/tests/contrib/post_test_hook.sh dsvm-scenario-linuxbridge
+          }
+          export -f post_test_hook
+
+          cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh
+          ./safe-devstack-vm-gate-wrap.sh
+        executable: /bin/bash
+        chdir: '{{ ansible_user_dir }}/workspace'
+      environment: '{{ zuul | zuul_legacy_vars }}'
diff --git a/requirements.txt b/requirements.txt
index 84ee391..e546885 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,6 +5,7 @@
 pbr>=2.0 # Apache-2.0
 neutron-lib>=1.9.0 # Apache-2.0
 oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0
+ipaddress>=1.0.16;python_version<'3.3' # PSF
 netaddr!=0.7.16,>=0.7.13 # BSD
 oslo.log>=3.22.0 # Apache-2.0
 oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0