Merge "Add tests for external network"
diff --git a/.zuul.yaml b/.zuul.yaml
index 7e8e532..45ef47a 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,9 +1,69 @@
-- 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
+
+- 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-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/clients.py b/neutron_tempest_plugin/api/clients.py
index 272f5be..875992e 100644
--- a/neutron_tempest_plugin/api/clients.py
+++ b/neutron_tempest_plugin/api/clients.py
@@ -13,6 +13,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from tempest.lib.services.compute import availability_zone_client
+from tempest.lib.services.compute import hypervisor_client
 from tempest.lib.services.compute import keypairs_client
 from tempest.lib.services.compute import servers_client
 from tempest.lib.services.identity.v2 import tenants_client
@@ -74,6 +76,10 @@
             **params)
         self.keypairs_client = keypairs_client.KeyPairsClient(
             self.auth_provider, **params)
+        self.hv_client = hypervisor_client.HypervisorClient(
+            self.auth_provider, **params)
+        self.az_client = availability_zone_client.AvailabilityZoneClient(
+            self.auth_provider, **params)
 
     def _set_identity_clients(self):
         params = {
diff --git a/neutron_tempest_plugin/scenario/admin/__init__.py b/neutron_tempest_plugin/scenario/admin/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/neutron_tempest_plugin/scenario/admin/__init__.py
diff --git a/neutron_tempest_plugin/scenario/admin/test_floatingip.py b/neutron_tempest_plugin/scenario/admin/test_floatingip.py
new file mode 100644
index 0000000..e58be83
--- /dev/null
+++ b/neutron_tempest_plugin/scenario/admin/test_floatingip.py
@@ -0,0 +1,109 @@
+# Copyright 2017 Red Hat, Inc.
+# 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 tempest.common import utils
+from tempest.common import waiters
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+from neutron_tempest_plugin.common import ssh
+from neutron_tempest_plugin import config
+from neutron_tempest_plugin.scenario import base
+from neutron_tempest_plugin.scenario import constants as const
+
+CONF = config.CONF
+
+
+class FloatingIpTestCasesAdmin(base.BaseTempestTestCase):
+    credentials = ['primary', 'admin']
+
+    @classmethod
+    @utils.requires_ext(extension="router", service="network")
+    def resource_setup(cls):
+        super(FloatingIpTestCasesAdmin, cls).resource_setup()
+        cls.network = cls.create_network()
+        cls.create_subnet(cls.network)
+        router = cls.create_router_by_client()
+        cls.create_router_interface(router['id'], cls.subnets[0]['id'])
+        # Create keypair with admin privileges
+        cls.keypair = cls.create_keypair(client=cls.os_admin.keypairs_client)
+        # Create security group with admin privileges
+        cls.secgroup = cls.os_admin.network_client.create_security_group(
+            name=data_utils.rand_name('secgroup'))['security_group']
+        # Execute funcs to achieve ssh and ICMP capabilities
+        funcs = [cls.create_loginable_secgroup_rule,
+                 cls.create_pingable_secgroup_rule]
+        for func in funcs:
+            func(secgroup_id=cls.secgroup['id'],
+                 client=cls.os_admin.network_client)
+
+    @classmethod
+    def resource_cleanup(cls):
+        # Cleanup for security group
+        cls.os_admin.network_client.delete_security_group(
+            security_group_id=cls.secgroup['id'])
+        super(FloatingIpTestCasesAdmin, cls).resource_cleanup()
+
+    def _list_hypervisors(self):
+        # List of hypervisors
+        return self.os_admin.hv_client.list_hypervisors()['hypervisors']
+
+    def _list_availability_zones(self):
+        # List of availability zones
+        func = self.os_admin.az_client.list_availability_zones
+        return func()['availabilityZoneInfo']
+
+    def _create_vms(self, hyper, avail_zone, num_servers=2):
+        servers, fips, server_ssh_clients = ([], [], [])
+        # Create the availability zone with default zone and
+        # a specific mentioned hypervisor.
+        az = avail_zone + ':' + hyper
+        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=[{'name': self.secgroup['name']}],
+                availability_zone=az))
+        for i, server in enumerate(servers):
+            waiters.wait_for_server_status(
+                self.os_admin.servers_client, server['server']['id'],
+                const.SERVER_STATUS_ACTIVE)
+            port = self.client.list_ports(
+                network_id=self.network['id'],
+                device_id=server['server']['id']
+            )['ports'][0]
+            fips.append(self.create_and_associate_floatingip(
+                port['id'], client=self.os_admin.network_client))
+            server_ssh_clients.append(ssh.Client(
+                fips[i]['floating_ip_address'], CONF.validation.image_ssh_user,
+                pkey=self.keypair['private_key']))
+            self.addCleanup(self.os_admin.network_client.delete_floatingip,
+                            fips[i]['id'])
+        return server_ssh_clients, fips
+
+    @decorators.idempotent_id('6bba729b-3fb6-494b-9e1e-82bbd89a1045')
+    def test_two_vms_fips(self):
+        """This test verifies the ability of two instances
+        that were created in the same compute node and same availability zone
+        to reach each other.
+        """
+        # Get hypervisor list to pass it for vm creation
+        hyper = self._list_hypervisors()[0]['hypervisor_hostname']
+        # Get availability zone list to pass it for vm creation
+        avail_zone = self._list_availability_zones()[0]['zoneName']
+        server_ssh_clients, fips = self._create_vms(hyper, avail_zone)
+        self.check_remote_connectivity(
+            server_ssh_clients[0], fips[1]['floating_ip_address'])
diff --git a/neutron_tempest_plugin/scenario/base.py b/neutron_tempest_plugin/scenario/base.py
index 5cc085f..2bb6344 100644
--- a/neutron_tempest_plugin/scenario/base.py
+++ b/neutron_tempest_plugin/scenario/base.py
@@ -42,15 +42,17 @@
     @classmethod
     def resource_cleanup(cls):
         for keypair in cls.keypairs:
-            cls.os_primary.keypairs_client.delete_keypair(
-                keypair_name=keypair['name'])
+            client = keypair['client']
+            client.delete_keypair(
+                keypair_name=keypair['keypair']['name'])
 
         super(BaseTempestTestCase, cls).resource_cleanup()
 
     def create_server(self, flavor_ref, image_ref, key_name, networks,
-                      name=None, security_groups=None):
+                      **kwargs):
         """Create a server using tempest lib
         All the parameters are the ones used in Compute API
+        * - Kwargs that require admin privileges
 
         Args:
            flavor_ref(str): The flavor of the server to be provisioned.
@@ -61,30 +63,44 @@
                an interface to be attached to the server. For network
                it should be {'uuid': network_uuid} and for port it should
                be {'port': port_uuid}
+        kwargs:
            name(str): Name of the server to be provisioned.
            security_groups(list): List of dictionaries where
                 the keys is 'name' and the value is the name of
                 the security group. If it's not passed the default
                 security group will be used.
+           availability_zone(str)*: The availability zone that
+                the instance will be in.
+                You can request a specific az without actually creating one,
+                Just pass 'X:Y' where X is the default availability
+                zone, and Y is the compute host name.
         """
 
-        name = name or data_utils.rand_name('server-test')
-        if not security_groups:
-            security_groups = [{'name': 'default'}]
+        kwargs.setdefault('name', data_utils.rand_name('server-test'))
 
-        server = self.os_primary.servers_client.create_server(
-            name=name,
+        # 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'}]
+
+        client = self.os_primary.servers_client
+        if kwargs.get('availability_zone'):
+            client = self.os_admin.servers_client
+
+        server = client.create_server(
             flavorRef=flavor_ref,
             imageRef=image_ref,
             key_name=key_name,
             networks=networks,
-            security_groups=security_groups)
+            **kwargs)
 
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
-            waiters.wait_for_server_termination,
-            self.os_primary.servers_client, server['server']['id'])
+                        waiters.wait_for_server_termination,
+                        client,
+                        server['server']['id'])
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
-                        self.os_primary.servers_client.delete_server,
+                        client.delete_server,
                         server['server']['id'])
         return server
 
@@ -93,12 +109,16 @@
         client = client or cls.os_primary.keypairs_client
         name = data_utils.rand_name('keypair-test')
         body = client.create_keypair(name=name)
-        cls.keypairs.append(body['keypair'])
+        body.update(client=client)
+        if client is cls.os_primary.keypairs_client:
+            cls.keypairs.append(body)
+
         return body['keypair']
 
     @classmethod
-    def create_secgroup_rules(cls, rule_list, secgroup_id=None):
-        client = cls.os_primary.network_client
+    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']
             for sg in sgs:
@@ -114,7 +134,8 @@
                 **rule)
 
     @classmethod
-    def create_loginable_secgroup_rule(cls, secgroup_id=None):
+    def create_loginable_secgroup_rule(cls, secgroup_id=None,
+                                       client=None):
         """This rule is intended to permit inbound ssh
 
         Allowing ssh traffic traffic from all sources, so no group_id is
@@ -128,10 +149,13 @@
                       'port_range_min': 22,
                       'port_range_max': 22,
                       'remote_ip_prefix': '0.0.0.0/0'}]
-        cls.create_secgroup_rules(rule_list, secgroup_id=secgroup_id)
+        client = client or cls.os_primary.network_client
+        cls.create_secgroup_rules(rule_list, client=client,
+                                  secgroup_id=secgroup_id)
 
     @classmethod
-    def create_pingable_secgroup_rule(cls, secgroup_id=None):
+    def create_pingable_secgroup_rule(cls, secgroup_id=None,
+                                      client=None):
         """This rule is intended to permit inbound ping
         """
 
@@ -140,7 +164,9 @@
                       'port_range_min': 8,  # type
                       'port_range_max': 0,  # code
                       'remote_ip_prefix': '0.0.0.0/0'}]
-        cls.create_secgroup_rules(rule_list, secgroup_id=secgroup_id)
+        client = client or cls.os_primary.network_client
+        cls.create_secgroup_rules(rule_list, client=client,
+                                  secgroup_id=secgroup_id)
 
     @classmethod
     def create_router_by_client(cls, is_admin=False, **kwargs):
@@ -155,11 +181,13 @@
         cls.routers.append(router)
         return router
 
-    def create_and_associate_floatingip(self, port_id):
-        fip = self.os_primary.network_client.create_floatingip(
+    def create_and_associate_floatingip(self, port_id, client=None):
+        client = client or self.os_primary.network_client
+        fip = client.create_floatingip(
             CONF.network.public_network_id,
             port_id=port_id)['floatingip']
-        self.floating_ips.append(fip)
+        if client is self.os_primary.network_client:
+            self.floating_ips.append(fip)
         return fip
 
     def setup_network_and_server(self, router=None, **kwargs):
diff --git a/neutron_tempest_plugin/scenario/test_security_groups.py b/neutron_tempest_plugin/scenario/test_security_groups.py
index faaeb84..1244535 100644
--- a/neutron_tempest_plugin/scenario/test_security_groups.py
+++ b/neutron_tempest_plugin/scenario/test_security_groups.py
@@ -62,6 +62,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 +195,18 @@
 
     @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}]
-        server_ssh_clients, fips, servers = self.create_vm_testing_sec_grp(
-            security_groups=security_groups_list)
+        self._test_ip_prefix(rule_list, should_succeed=True)
 
-        # 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.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)
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..c2d33a9
--- /dev/null
+++ b/playbooks/neutron-tempest-plugin-dvr-multinode-scenario/run.yaml
@@ -0,0 +1,61 @@
+- hosts: all
+  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 }}'