Merge "Add a scenario test for internal dns_name"
diff --git a/.zuul.yaml b/.zuul.yaml
index adcb433..4862822 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,9 +1,9 @@
 - job:
-    name: neutron-tempest-plugin-scenario
+    name: neutron-tempest-plugin
     parent: devstack-tempest
     abstract: true
     description: |
-        Perform setup common to all tempest scenario test jobs.
+        Perform setup common to all Neutron tempest tests
     roles:
       - zuul: openstack-dev/devstack
     required-projects:
@@ -12,22 +12,16 @@
       - openstack/neutron-tempest-plugin
       - openstack/tempest
     vars:
-      tempest_test_regex: ^neutron_tempest_plugin\.scenario
       tempest_concurrency: 4
       tox_envlist: all
       devstack_localrc:
-          TEMPEST_PLUGINS: /opt/stack/neutron-tempest-plugin
-          PHYSICAL_NETWORK: default
-          DOWNLOAD_DEFAULT_IMAGES: false
-          IMAGE_URLS: "http://cloud-images.ubuntu.com/releases/16.04/release-20170113/ubuntu-16.04-server-cloudimg-amd64-disk1.img,"
-          DEFAULT_INSTANCE_TYPE: ds512M
-          DEFAULT_INSTANCE_USER: ubuntu
-          BUILD_TIMEOUT: 784
+        TEMPEST_PLUGINS: /opt/stack/neutron-tempest-plugin
+        NETWORK_API_EXTENSIONS: "address-scope,agent,allowed-address-pairs,auto-allocated-topology,availability_zone,binding,default-subnetpools,dhcp_agent_scheduler,dns-domain-ports,dns-integration,dvr,empty-string-filtering,ext-gw-mode,external-net,extra_dhcp_opt,extraroute,fip-port-details,flavors,ip-substring-filtering,l3-flavors,l3-ha,l3_agent_scheduler,logging,metering,multi-provider,net-mtu,net-mtu-writable,network-ip-availability,network_availability_zone,pagination,port-security,project-id,provider,qos,qos-fip,quotas,quota_details,rbac-policies,router,router_availability_zone,security-group,port-mac-address-regenerate,port-security-groups-filtering,segment,service-type,sorting,standard-attr-description,standard-attr-revisions,standard-attr-segment,standard-attr-timestamp,standard-attr-tag,subnet_allocation,trunk,trunk-details"
       devstack_plugins:
         neutron: git://git.openstack.org/openstack/neutron.git
         neutron-tempest-plugin: git://git.openstack.org/openstack/neutron-tempest-plugin.git
       devstack_services:
-        cinder: true
+        tls-proxy: false
         tempest: true
         neutron-dns: true
         neutron-qos: true
@@ -46,11 +40,13 @@
           # lib/neutron-legacy
           "/$NEUTRON_CORE_PLUGIN_CONF":
             ml2:
-              type_drivers: flat,vlan,local,vxlan
+              type_drivers: flat,geneve,vlan,gre,local,vxlan
             ml2_type_vlan:
               network_vlan_ranges: foo:1:10
             ml2_type_vxlan:
               vni_ranges: 1:2000
+            ml2_type_gre:
+              tunnel_id_ranges: 1:1000
           $NEUTRON_L3_CONF:
             agent:
               availability_zone: nova
@@ -68,7 +64,7 @@
               provider_vlans: foo,
               agent_availability_zone: nova
               image_is_advanced: true
-              available_type_drivers: flat,vlan,local,vxlan
+              available_type_drivers: flat,geneve,vlan,gre,local,vxlan
     irrelevant-files:
       - ^(test-|)requirements.txt$
       - ^releasenotes/.*$
@@ -82,25 +78,21 @@
 
 - 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$
-      - ^.*\.rst$
-      - ^neutron/locale/.*$
-      - ^neutron/tests/unit/.*$
-      - ^tools/.*$
-      - ^tox.ini$
+    parent: neutron-tempest-plugin
+    vars:
+      tempest_test_regex: ^neutron_tempest_plugin\.api
+      devstack_services:
+        neutron-log: true
+      devstack_local_conf:
+        post-config:
+          # NOTE(slaweq): We can get rid of this hardcoded absolute path when
+          # devstack-tempest job will be switched to use lib/neutron instead of
+          # lib/neutron-legacy
+          "/$NEUTRON_CORE_PLUGIN_CONF":
+            AGENT:
+              tunnel_types: gre,vxlan
+            network_log:
+              local_output_log_base: /tmp/test_log.log
 
 - job:
     name: neutron-tempest-plugin-api-queens
@@ -108,6 +100,29 @@
     override-checkout: stable/queens
     vars:
       branch_override: stable/queens
+      devstack_localrc:
+        # TODO(slaweq): find a way to put this list of extensions in
+        # neutron repository and keep it different per branch,
+        # then it could be removed from here
+        NETWORK_API_EXTENSIONS: "address-scope,agent,allowed-address-pairs,auto-allocated-topology,availability_zone,binding,default-subnetpools,dhcp_agent_scheduler,dns-domain-ports,dns-integration,dvr,ext-gw-mode,external-net,extra_dhcp_opt,extraroute,flavors,ip-substring-filtering,l3-flavors,l3-ha,l3_agent_scheduler,logging,metering,multi-provider,net-mtu,net-mtu-writable,network-ip-availability,network_availability_zone,pagination,port-security,project-id,provider,qos,quotas,quota_details,rbac-policies,router,router_availability_zone,security-group,segment,service-type,sorting,standard-attr-description,standard-attr-revisions,standard-attr-timestamp,standard-attr-tag,subnet_allocation,tag,tag-ext,trunk,trunk-details"
+
+- job:
+    name: neutron-tempest-plugin-scenario
+    parent: neutron-tempest-plugin
+    abstract: true
+    description: |
+        Perform setup common to all tempest scenario test jobs.
+    vars:
+      tempest_test_regex: ^neutron_tempest_plugin\.scenario
+      devstack_localrc:
+          PHYSICAL_NETWORK: default
+          DOWNLOAD_DEFAULT_IMAGES: false
+          IMAGE_URLS: "http://cloud-images.ubuntu.com/releases/16.04/release-20170113/ubuntu-16.04-server-cloudimg-amd64-disk1.img,"
+          DEFAULT_INSTANCE_TYPE: ds512M
+          DEFAULT_INSTANCE_USER: ubuntu
+          BUILD_TIMEOUT: 784
+      devstack_services:
+        cinder: true
 
 - job:
     name: neutron-tempest-plugin-scenario-linuxbridge
@@ -115,8 +130,8 @@
     timeout: 10000
     vars:
       devstack_localrc:
-          NETWORK_API_EXTENSIONS: "address-scope,agent,allowed-address-pairs,auto-allocated-topology,availability_zone,binding,default-subnetpools,dhcp_agent_scheduler,dns-integration,ext-gw-mode,external-net,extra_dhcp_opt,extraroute,flavors,ip-substring-filtering,l3-flavors,l3-ha,l3_agent_scheduler,logging,metering,multi-provider,net-mtu,net-mtu-writable,network-ip-availability,network_availability_zone,pagination,port-security,project-id,provider,qos,qos-fip,quotas,quota_details,rbac-policies,router,router_availability_zone,security-group,port-security-groups-filtering,segment,service-type,sorting,standard-attr-description,standard-attr-revisions,standard-attr-timestamp,standard-attr-tag,subnet_allocation,tag,tag-ext,trunk,trunk-details"
           Q_AGENT: linuxbridge
+          NETWORK_API_EXTENSIONS: "address-scope,agent,allowed-address-pairs,auto-allocated-topology,availability_zone,binding,default-subnetpools,dhcp_agent_scheduler,dns-domain-ports,dns-integration,ext-gw-mode,external-net,extra_dhcp_opt,extraroute,flavors,ip-substring-filtering,l3-flavors,l3-ha,l3_agent_scheduler,logging,metering,multi-provider,net-mtu,net-mtu-writable,network-ip-availability,network_availability_zone,pagination,port-security,project-id,provider,qos,qos-fip,quotas,quota_details,rbac-policies,router,router_availability_zone,security-group,port-security-groups-filtering,segment,service-type,sorting,standard-attr-description,standard-attr-revisions,standard-attr-timestamp,standard-attr-tag,subnet_allocation,tag,tag-ext,trunk,trunk-details"
       devstack_local_conf:
         post-config:
           $NEUTRON_CONF:
@@ -124,6 +139,16 @@
               enable_dvr: false
             AGENT:
               debug_iptables_rules: true
+          # NOTE(slaweq): We can get rid of this hardcoded absolute path when
+          # devstack-tempest job will be switched to use lib/neutron instead of
+          # lib/neutron-legacy
+          "/$NEUTRON_CORE_PLUGIN_CONF":
+            ml2:
+              type_drivers: flat,vlan,local,vxlan
+        test-config:
+          $TEMPEST_CONFIG:
+            neutron_plugin_options:
+              available_type_drivers: flat,vlan,local,vxlan
 
 - job:
     name: neutron-tempest-plugin-scenario-linuxbridge-queens
@@ -211,7 +236,7 @@
         - build-openstack-sphinx-docs
 
 - project-template:
-    name: neutron-tempest-plugin-jobs-stable
+    name: neutron-tempest-plugin-jobs-queens
     check:
       jobs:
         - neutron-tempest-plugin-api-queens
@@ -225,4 +250,4 @@
 - project:
     templates:
       - neutron-tempest-plugin-jobs
-      - neutron-tempest-plugin-jobs-stable
+      - neutron-tempest-plugin-jobs-queens
diff --git a/neutron_tempest_plugin/api/admin/test_ports.py b/neutron_tempest_plugin/api/admin/test_ports.py
new file mode 100644
index 0000000..cbcd933
--- /dev/null
+++ b/neutron_tempest_plugin/api/admin/test_ports.py
@@ -0,0 +1,60 @@
+# Copyright 2018 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.
+
+import netaddr
+
+from tempest.common import utils
+from tempest.lib import decorators
+
+from neutron_tempest_plugin.api import base
+
+
+class PortTestCasesAdmin(base.BaseAdminNetworkTest):
+
+    @classmethod
+    def resource_setup(cls):
+        super(PortTestCasesAdmin, cls).resource_setup()
+        cls.network = cls.create_network()
+        cls.create_subnet(cls.network)
+
+    @decorators.idempotent_id('dfe8cc79-18d9-4ae8-acef-3ec6bb719bb1')
+    def test_update_mac_address(self):
+        body = self.create_port(self.network)
+        current_mac = body['mac_address']
+
+        # Verify mac_address can be successfully updated.
+        body = self.admin_client.update_port(body['id'],
+                                             mac_address='12:34:56:78:be:6d')
+        new_mac = body['port']['mac_address']
+        self.assertNotEqual(current_mac, new_mac)
+        self.assertEqual('12:34:56:78:be:6d', new_mac)
+
+        # Verify that port update without specifying mac_address does not
+        # change the mac address.
+        body = self.admin_client.update_port(body['port']['id'],
+                                             description='Port Description')
+        self.assertEqual(new_mac, body['port']['mac_address'])
+
+    @decorators.idempotent_id('dfe8cc79-18d9-4ae8-acef-3ec6bb719cc2')
+    @utils.requires_ext(extension="port-mac-address-regenerate",
+                        service="network")
+    def test_regenerate_mac_address(self):
+        body = self.create_port(self.network)
+        current_mac = body['mac_address']
+        body = self.admin_client.update_port(body['id'],
+                                             mac_address=None)
+        new_mac = body['port']['mac_address']
+        self.assertNotEqual(current_mac, new_mac)
+        self.assertTrue(netaddr.valid_mac(new_mac))
diff --git a/neutron_tempest_plugin/api/base.py b/neutron_tempest_plugin/api/base.py
index 6246eb7..2f5446c 100644
--- a/neutron_tempest_plugin/api/base.py
+++ b/neutron_tempest_plugin/api/base.py
@@ -124,6 +124,7 @@
         cls.projects = []
         cls.log_objects = []
         cls.reserved_subnet_cidrs = set()
+        cls.keypairs = []
 
     @classmethod
     def resource_cleanup(cls):
@@ -221,6 +222,9 @@
                 cls._try_delete_resource(cls.admin_client.delete_log,
                                          log_object['id'])
 
+            for keypair in cls.keypairs:
+                cls._try_delete_resource(cls.delete_keypair, keypair)
+
         super(BaseNetworkTest, cls).resource_cleanup()
 
     @classmethod
@@ -593,6 +597,23 @@
         cls.security_groups.append(body['security_group'])
         return body['security_group']
 
+    @classmethod
+    def create_keypair(cls, client=None, name=None, **kwargs):
+        client = client or cls.os_primary.keypairs_client
+        name = name or data_utils.rand_name('keypair-test')
+        keypair = client.create_keypair(name=name, **kwargs)['keypair']
+
+        # save client for later cleanup
+        keypair['client'] = client
+        cls.keypairs.append(keypair)
+        return keypair
+
+    @classmethod
+    def delete_keypair(cls, keypair, client=None):
+        client = (client or keypair.get('client') or
+                  cls.os_primary.keypairs_client)
+        client.delete_keypair(keypair_name=keypair['name'])
+
 
 class BaseAdminNetworkTest(BaseNetworkTest):
 
diff --git a/neutron_tempest_plugin/api/test_auto_allocated_topology.py b/neutron_tempest_plugin/api/test_auto_allocated_topology.py
index 37f9ad1..0baa2a8 100644
--- a/neutron_tempest_plugin/api/test_auto_allocated_topology.py
+++ b/neutron_tempest_plugin/api/test_auto_allocated_topology.py
@@ -63,7 +63,7 @@
 
         up = {'admin_state_up': True}
         networks = _count(self.client.list_networks(**up)['networks'])
-        subnets = _count(self.client.list_subnets(**up)['subnets'])
+        subnets = _count(self.client.list_subnets()['subnets'])
         routers = _count(self.client.list_routers(**up)['routers'])
         return networks, subnets, routers
 
diff --git a/neutron_tempest_plugin/api/test_ports.py b/neutron_tempest_plugin/api/test_ports.py
index 5a01798..3b877c2 100644
--- a/neutron_tempest_plugin/api/test_ports.py
+++ b/neutron_tempest_plugin/api/test_ports.py
@@ -108,7 +108,7 @@
         body = self.client.update_port(body['id'],
                                        dns_name='d2', dns_domain='d.org.')
         self.assertEqual('d2', body['port']['dns_name'])
-        self.assertEqual('d.org.', body['dns_domain'])
+        self.assertEqual('d.org.', body['port']['dns_domain'])
         self._confirm_dns_assignment(body['port'])
         body = self.client.show_port(body['port']['id'])['port']
         self.assertEqual('d2', body['dns_name'])
diff --git a/neutron_tempest_plugin/scenario/base.py b/neutron_tempest_plugin/scenario/base.py
index 8815945..10cdaf1 100644
--- a/neutron_tempest_plugin/scenario/base.py
+++ b/neutron_tempest_plugin/scenario/base.py
@@ -35,20 +35,6 @@
 
 
 class BaseTempestTestCase(base_api.BaseNetworkTest):
-    @classmethod
-    def resource_setup(cls):
-        super(BaseTempestTestCase, cls).resource_setup()
-
-        cls.keypairs = []
-
-    @classmethod
-    def resource_cleanup(cls):
-        for keypair in cls.keypairs:
-            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,
                       **kwargs):
@@ -107,17 +93,6 @@
         return server
 
     @classmethod
-    def create_keypair(cls, client=None):
-        client = client or cls.os_primary.keypairs_client
-        name = data_utils.rand_name('keypair-test')
-        body = client.create_keypair(name=name)
-        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=None):
         client = client or cls.os_primary.network_client
diff --git a/playbooks/neutron-tempest-plugin-api/post.yaml b/playbooks/neutron-tempest-plugin-api/post.yaml
deleted file mode 100644
index dac8753..0000000
--- a/playbooks/neutron-tempest-plugin-api/post.yaml
+++ /dev/null
@@ -1,80 +0,0 @@
-- 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
deleted file mode 100644
index 230ac10..0000000
--- a/playbooks/neutron-tempest-plugin-api/run.yaml
+++ /dev/null
@@ -1,58 +0,0 @@
-- 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="{{ branch_override | default('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/tools/customize_ubuntu_image b/tools/customize_ubuntu_image
new file mode 100755
index 0000000..9c3fd07
--- /dev/null
+++ b/tools/customize_ubuntu_image
@@ -0,0 +1,172 @@
+#!/bin/bash
+
+# IMPLEMENTATION NOTE: It was not possible to implement this script using
+# virt-customize because of below ubuntu bugs:
+#  - https://bugs.launchpad.net/ubuntu/+source/libguestfs/+bug/1632405
+#  - https://bugs.launchpad.net/ubuntu/+source/isc-dhcp/+bug/1650740
+#
+# It has therefore been adopted a more low level strategy performing below
+# steps:
+#  - mount guest image to a temporary folder
+#  - set up an environment suitable for executing chroot
+#  - execute customize_image function inside chroot environment
+#  - cleanup chroot environment
+
+# Array of packages to be installed of guest image
+INSTALL_GUEST_PACKAGES=(
+   socat  # used to replace nc for testing advanced network features like
+          # multicast
+)
+
+# Function to be executed once after chroot on guest image
+# Add more customization steps here
+function customize_image {
+    # dhclient-script requires to read /etc/fstab for setting up network
+    touch /etc/fstab
+    chmod ugo+r /etc/fstab
+
+    # Ubuntu guest image _apt user could require access to below folders
+    local apt_user_folders=( /var/lib/apt/lists/partial )
+    mkdir -p "${apt_user_folders[@]}"
+    chown _apt.root -fR "${apt_user_folders[@]}"
+
+    # Install desired packages to Ubuntu guest image
+    apt-get update -y
+    apt-get install -y "${INSTALL_GUEST_PACKAGES[@]}"
+}
+
+function main {
+    set -eux
+    trap cleanup EXIT
+    "${ENTRY_POINT:-chroot_image}" "$@"
+}
+
+# Chroot to guest image then executes customize_image function inside it
+function chroot_image {
+    local image_file=$1
+    local temp_dir=${TEMP_DIR:-$(make_temp -d)}
+
+    # Mount guest image into a temporary directory
+    local mount_dir=${temp_dir}/mount
+    mkdir -p "${mount_dir}"
+    mount_image "${mount_dir}" "${temp_dir}/pid"
+
+    # Mount system directories
+    bind_dir "/dev" "${mount_dir}/dev"
+    bind_dir "/dev/pts" "${mount_dir}/dev/pts"
+    bind_dir "/proc" "${mount_dir}/proc"
+    bind_dir "/sys" "${mount_dir}/sys"
+
+    # Mount to keep temporary files out of guest image
+    mkdir -p "${temp_dir}/apt" "${temp_dir}/cache" "${temp_dir}/tmp"
+    bind_dir "${temp_dir}/cache" "${mount_dir}/var/cache"
+    bind_dir "${temp_dir}/tmp" "${mount_dir}/tmp"
+    bind_dir "${temp_dir}/tmp" "${mount_dir}/var/tmp"
+    bind_dir "${temp_dir}/apt" "${mount_dir}/var/lib/apt"
+
+    # Replace /etc/resolv.conf symlink to use the same DNS as this host
+    sudo rm -f "${mount_dir}/etc/resolv.conf"
+    sudo cp /etc/resolv.conf "${mount_dir}/etc/resolv.conf"
+
+    # Makesure /etc/fstab exists and it is readable because it is required by
+    # /sbin/dhclient-script
+    sudo touch /etc/fstab
+    sudo chmod 644 /etc/fstab
+
+    # Copy this script to mount dir
+    local script_name=$(basename "$0")
+    local script_file=${mount_dir}/${script_name}
+    sudo cp "$0" "${script_file}"
+    sudo chmod 500 "${script_file}"
+    add_cleanup sudo rm -f "'${script_file}'"
+
+    # Execute customize_image inside chroot environment
+    local command_line=( ${CHROOT_COMMAND:-customize_image} )
+    local entry_point=${command_line[0]}
+    unset command_line[0]
+    sudo -E "ENTRY_POINT=${entry_point}" \
+        chroot "${mount_dir}" "/${script_name}" "${command_line[@]:-}"
+}
+
+# Mounts guest image to $1 directory writing pid to $1 pid file
+# Then registers umount of such directory for final cleanup
+function mount_image {
+    local mount_dir=$1
+    local pid_file=$2
+
+    # export libguest settings
+    export LIBGUESTFS_BACKEND=${LIBGUESTFS_BACKEND:-direct}
+    export LIBGUESTFS_BACKEND_SETTINGS=${LIBGUESTFS_BACKEND_SETTINGS:-force_tcg}
+
+    # Mount guest image
+    sudo -E guestmount -i \
+        --add "${image_file}" \
+        --pid-file "${pid_file}" \
+        "${mount_dir}"
+
+    add_cleanup \
+        'ENTRY_POINT=umount_image' \
+        "'$0'" "'${mount_dir}'" "'${pid_file}'"
+}
+
+# Unmounts guest image directory
+function umount_image {
+    local mount_dir=$1
+    local pid_file=$2
+    local timeout=10
+
+    # Take PID just before unmounting
+    local pid=$(cat ${pid_file} || true)
+    sudo -E guestunmount "${mount_dir}"
+
+    if [ "${pid:-}" != "" ]; then
+        # Make sure guestmount process is not running before using image
+        # file again
+        local count=${timeout}
+        while sudo kill -0 "${pid}" 2> /dev/null && (( count-- > 0 )); do
+            sleep 1
+        done
+        if [ ${count} == 0 ]; then
+            # It is not safe to use image file at this point
+            echo "Wait for guestmount to exit failed after ${timeout} seconds"
+        fi
+    fi
+}
+
+# Creates a temporary file or directory and register removal for final cleanup
+function make_temp {
+    local temporary=$(mktemp "$@")
+    add_cleanup sudo rm -fR "'${temporary}'"
+    echo "${temporary}"
+}
+
+# Bind directory $1 to directory $2 and register umount for final cleanup
+function bind_dir {
+    local source_dir=$1
+    local target_dir=$2
+    sudo mount --bind "${source_dir}" "${target_dir}"
+    add_cleanup sudo umount "'${target_dir}'"
+}
+
+# Registers a command line to be executed for final cleanup
+function add_cleanup {
+    CLEANUP_FILE=${CLEANUP_FILE:-$(mktemp)}
+
+    echo -e "$*" >> ${CLEANUP_FILE}
+}
+
+# Execute command lines for final cleanup in reversed order
+function cleanup {
+    error=$?
+
+    local cleanup_file=${CLEANUP_FILE:-}
+    if [ -r "${cleanup_file}" ]; then
+        tac "${cleanup_file}" | bash +e -x
+        CLEANUP_FILE=
+        rm -fR "${cleanup_file}"
+    fi
+
+    exit ${error}
+}
+
+main "$@"
diff --git a/tox.ini b/tox.ini
index bba0a64..5eb8b10 100644
--- a/tox.ini
+++ b/tox.ini
@@ -16,6 +16,7 @@
 commands = stestr run --slowest {posargs}
 
 [testenv:pep8]
+basepython = python3
 commands =
   sh ./tools/misc-sanity-checks.sh
   flake8
@@ -23,9 +24,11 @@
   sh
 
 [testenv:venv]
+basepython = python3
 commands = {posargs}
 
 [testenv:cover]
+basepython = python3
 setenv =
     {[testenv]setenv}
     PYTHON=coverage run --source neutron_tempest_plugin --parallel-mode
@@ -36,13 +39,16 @@
     coverage xml -o cover/coverage.xml
 
 [testenv:docs]
+basepython = python3
 commands = python setup.py build_sphinx
 
 [testenv:releasenotes]
+basepython = python3
 commands =
   sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
 
 [testenv:debug]
+basepython = python3
 commands = oslo_debug_helper -t neutron_tempest_plugin/ {posargs}
 
 [flake8]