Merge "Fix cleanup NotImplemented error"
diff --git a/.zuul.yaml b/.zuul.yaml
index 462501e..5bec9f9 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -418,6 +418,7 @@
       - opendev.org/openstack/senlin-tempest-plugin
       - opendev.org/openstack/solum-tempest-plugin
       - opendev.org/x/tap-as-a-service
+      - opendev.org/x/tap-as-a-service-tempest-plugin
       - opendev.org/openstack/telemetry-tempest-plugin
       - opendev.org/openstack/tempest-horizon
       - opendev.org/x/tobiko
@@ -428,6 +429,7 @@
       - opendev.org/openstack/vitrage-tempest-plugin
       - opendev.org/x/vmware-nsx-tempest-plugin
       - opendev.org/openstack/watcher-tempest-plugin
+      - opendev.org/x/whitebox-tempest-plugin
       - opendev.org/openstack/zaqar-tempest-plugin
       - opendev.org/openstack/zun-tempest-plugin
 
diff --git a/doc/source/data/tempest-blacklisted-plugins-registry.header b/doc/source/data/tempest-blacklisted-plugins-registry.header
new file mode 100644
index 0000000..6b6af11
--- /dev/null
+++ b/doc/source/data/tempest-blacklisted-plugins-registry.header
@@ -0,0 +1,7 @@
+Blacklisted Plugins
+===================
+
+List of Tempest plugin projects that are stale or unmaintained for a long
+time (6 months or more). They can be moved out of blacklist state once one
+of the relevant patches gets merged:
+https://review.opendev.org/#/q/topic:tempest-sanity-gate+%28status:open%29
diff --git a/releasenotes/notes/bug-1647999-7aeda50a8d082d4c.yaml b/releasenotes/notes/bug-1647999-7aeda50a8d082d4c.yaml
new file mode 100644
index 0000000..384f916
--- /dev/null
+++ b/releasenotes/notes/bug-1647999-7aeda50a8d082d4c.yaml
@@ -0,0 +1,8 @@
+---
+features:
+  - |
+    A new parameter, compute/compute_volume_common_az is introduced to
+    specify availability zone where tempest creates instances and volumes
+    for scenario tests, to allow us to run scenario tests in the deployment
+    which has multiple availability zones and cinder/cross_az_attach in
+    nova.conf is set to False.
diff --git a/tempest/api/identity/admin/v3/test_endpoints.py b/tempest/api/identity/admin/v3/test_endpoints.py
index 2cd8906..366d6a0 100644
--- a/tempest/api/identity/admin/v3/test_endpoints.py
+++ b/tempest/api/identity/admin/v3/test_endpoints.py
@@ -44,11 +44,14 @@
             cls.addClassResourceCleanup(
                 cls.services_client.delete_service, service['id'])
 
-            region = data_utils.rand_name('region')
+            region_name = data_utils.rand_name('region')
             url = data_utils.rand_url()
             endpoint = cls.client.create_endpoint(
                 service_id=cls.service_ids[i], interface=interfaces[i],
-                url=url, region=region, enabled=True)['endpoint']
+                url=url, region=region_name, enabled=True)['endpoint']
+            region = cls.regions_client.show_region(region_name)['region']
+            cls.addClassResourceCleanup(
+                cls.regions_client.delete_region, region['id'])
             cls.addClassResourceCleanup(
                 cls.client.delete_endpoint, endpoint['id'])
             cls.setup_endpoint_ids.append(endpoint['id'])
@@ -108,17 +111,19 @@
 
     @decorators.idempotent_id('0e2446d2-c1fd-461b-a729-b9e73e3e3b37')
     def test_create_list_show_delete_endpoint(self):
-        region = data_utils.rand_name('region')
+        region_name = data_utils.rand_name('region')
         url = data_utils.rand_url()
         interface = 'public'
         endpoint = self.client.create_endpoint(service_id=self.service_ids[0],
                                                interface=interface,
-                                               url=url, region=region,
+                                               url=url, region=region_name,
                                                enabled=True)['endpoint']
+        region = self.regions_client.show_region(region_name)['region']
+        self.addCleanup(self.regions_client.delete_region, region['id'])
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
                         self.client.delete_endpoint, endpoint['id'])
         # Asserting Create Endpoint response body
-        self.assertEqual(region, endpoint['region'])
+        self.assertEqual(region_name, endpoint['region'])
         self.assertEqual(url, endpoint['url'])
 
         # Checking if created endpoint is present in the list of endpoints
@@ -133,7 +138,7 @@
         self.assertEqual(self.service_ids[0], fetched_endpoint['service_id'])
         self.assertEqual(interface, fetched_endpoint['interface'])
         self.assertEqual(url, fetched_endpoint['url'])
-        self.assertEqual(region, fetched_endpoint['region'])
+        self.assertEqual(region_name, fetched_endpoint['region'])
         self.assertEqual(True, fetched_endpoint['enabled'])
 
         # Deleting the endpoint created in this method
@@ -161,28 +166,33 @@
         self.addCleanup(self.services_client.delete_service, service2['id'])
 
         # Creating an endpoint so as to check update endpoint with new values
-        region1 = data_utils.rand_name('region')
+        region1_name = data_utils.rand_name('region')
         url1 = data_utils.rand_url()
         interface1 = 'public'
         endpoint_for_update = (
             self.client.create_endpoint(service_id=self.service_ids[0],
                                         interface=interface1,
-                                        url=url1, region=region1,
+                                        url=url1, region=region1_name,
                                         enabled=True)['endpoint'])
-        self.addCleanup(self.client.delete_endpoint, endpoint_for_update['id'])
+        region1 = self.regions_client.show_region(region1_name)['region']
+        self.addCleanup(self.regions_client.delete_region, region1['id'])
 
         # Updating endpoint with new values
-        region2 = data_utils.rand_name('region')
+        region2_name = data_utils.rand_name('region')
         url2 = data_utils.rand_url()
         interface2 = 'internal'
         endpoint = self.client.update_endpoint(endpoint_for_update['id'],
                                                service_id=service2['id'],
                                                interface=interface2,
-                                               url=url2, region=region2,
+                                               url=url2, region=region2_name,
                                                enabled=False)['endpoint']
+        region2 = self.regions_client.show_region(region2_name)['region']
+        self.addCleanup(self.regions_client.delete_region, region2['id'])
+        self.addCleanup(self.client.delete_endpoint, endpoint_for_update['id'])
+
         # Asserting if the attributes of endpoint are updated
         self.assertEqual(service2['id'], endpoint['service_id'])
         self.assertEqual(interface2, endpoint['interface'])
         self.assertEqual(url2, endpoint['url'])
-        self.assertEqual(region2, endpoint['region'])
+        self.assertEqual(region2_name, endpoint['region'])
         self.assertEqual(False, endpoint['enabled'])
diff --git a/tempest/api/identity/admin/v3/test_endpoints_negative.py b/tempest/api/identity/admin/v3/test_endpoints_negative.py
index 4c3eb1c..164b577 100644
--- a/tempest/api/identity/admin/v3/test_endpoints_negative.py
+++ b/tempest/api/identity/admin/v3/test_endpoints_negative.py
@@ -70,14 +70,16 @@
     def _assert_update_raises_bad_request(self, enabled):
 
         # Create an endpoint
-        region1 = data_utils.rand_name('region')
+        region1_name = data_utils.rand_name('region')
         url1 = data_utils.rand_url()
         interface1 = 'public'
         endpoint_for_update = (
             self.client.create_endpoint(service_id=self.service_id,
                                         interface=interface1,
-                                        url=url1, region=region1,
+                                        url=url1, region=region1_name,
                                         enabled=True)['endpoint'])
+        region1 = self.regions_client.show_region(region1_name)['region']
+        self.addCleanup(self.regions_client.delete_region, region1['id'])
         self.addCleanup(self.client.delete_endpoint, endpoint_for_update['id'])
 
         self.assertRaises(lib_exc.BadRequest, self.client.update_endpoint,
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
old mode 100755
new mode 100644
index e1b8cf5..11f3bf9
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -121,7 +121,9 @@
                      '/'.join((server_status, str(task_state))),
                      time.time() - start_time)
         if server_status == 'ERROR' and not ignore_error:
-            raise lib_exc.DeleteErrorException(resource_id=server_id)
+            raise lib_exc.DeleteErrorException(
+                "Server %s failed to delete and is in ERROR status" %
+                server_id)
 
         if int(time.time()) - start_time >= client.build_timeout:
             raise lib_exc.TimeoutException
diff --git a/tempest/config.py b/tempest/config.py
index 9e4718b..82cbe09 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -352,6 +352,19 @@
                     "If both values are not specified, Tempest avoids tests "
                     "which require a microversion. Valid values are string "
                     "with format 'X.Y' or string 'latest'"),
+    cfg.StrOpt('compute_volume_common_az',
+               default=None,
+               help='AZ to be used for Cinder and Nova. Set this parameter '
+                    'when the cloud has nova.conf: cinder.cross_az_attach '
+                    'set to false. Which means volumes attached to an '
+                    'instance must be in the same availability zone in Cinder '
+                    'as the instance availability zone in Nova. Set the '
+                    'common availability zone in this config which will be '
+                    'used to boot an instance as well as creating a volume. '
+                    'NOTE: If that AZ is not in Cinder (or '
+                    'allow_availability_zone_fallback=False in cinder.conf), '
+                    'the volume create request will fail and the instance '
+                    'will fail the build request.'),
 ]
 
 placement_group = cfg.OptGroup(name='placement',
diff --git a/tempest/lib/services/volume/v3/volumes_client.py b/tempest/lib/services/volume/v3/volumes_client.py
index 2dbdd11..a93c76e 100644
--- a/tempest/lib/services/volume/v3/volumes_client.py
+++ b/tempest/lib/services/volume/v3/volumes_client.py
@@ -212,7 +212,9 @@
         except lib_exc.NotFound:
             return True
         if volume["volume"]["status"] == "error_deleting":
-            raise lib_exc.DeleteErrorException(resource_id=id)
+            raise lib_exc.DeleteErrorException(
+                "Volume %s failed to delete and is in error_deleting status" %
+                volume['id'])
         return False
 
     @property
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 87d7e76..6ce5b78 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -274,6 +274,10 @@
 
         tenant_network = self.get_tenant_network()
 
+        if CONF.compute.compute_volume_common_az:
+            kwargs.setdefault('availability_zone',
+                              CONF.compute.compute_volume_common_az)
+
         body, _ = compute.create_test_server(
             clients,
             tenant_network=tenant_network,
@@ -307,6 +311,11 @@
                   'imageRef': imageRef,
                   'volume_type': volume_type,
                   'size': size}
+
+        if CONF.compute.compute_volume_common_az:
+            kwargs.setdefault('availability_zone',
+                              CONF.compute.compute_volume_common_az)
+
         volume = self.volumes_client.create_volume(**kwargs)['volume']
 
         self.addCleanup(self.volumes_client.wait_for_resource_deletion,
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
index 746cb34..35b1144 100644
--- a/tools/generate-tempest-plugins-list.py
+++ b/tools/generate-tempest-plugins-list.py
@@ -25,6 +25,7 @@
 
 import json
 import re
+import sys
 
 try:
     # For Python 3.0 and later
@@ -35,6 +36,25 @@
     import urllib2 as urllib
     from urllib2 import HTTPError
 
+# List of projects having tempest plugin stale or unmaintained for a long time
+# (6 months or more)
+# TODO(masayukig): Some of these can be removed from BLACKLIST in the future
+# when the patches are merged.
+BLACKLIST = [
+    'barbican-tempest-plugin',  # https://review.opendev.org/#/c/634631/
+    'cyborg-tempest-plugin',  # https://review.opendev.org/659687
+    'intel-nfv-ci-tests',  # https://review.opendev.org/#/c/634640/
+    'networking-ansible',  # https://review.opendev.org/#/c/634647/
+    'networking-generic-switch',  # https://review.opendev.org/#/c/634846/
+    'networking-l2gw-tempest-plugin',  # https://review.opendev.org/#/c/635093/
+    'networking-midonet',  # https://review.opendev.org/#/c/635096/
+    'networking-plumgrid',  # https://review.opendev.org/#/c/635096/
+    'networking-spp',  # https://review.opendev.org/#/c/635098/
+    'neutron-dynamic-routing',  # https://review.opendev.org/#/c/637718/
+    'neutron-vpnaas',  # https://review.opendev.org/#/c/637719/
+    'nova-lxd',  # https://review.opendev.org/#/c/638334/
+    'valet',  # https://review.opendev.org/#/c/638339/
+]
 
 url = 'https://review.opendev.org/projects/'
 
@@ -47,15 +67,10 @@
 '''
 
 
-def is_in_openstack_namespace(proj):
-    return proj.startswith('openstack/')
-
 # Rather than returning a 404 for a nonexistent file, cgit delivers a
 # 0-byte response to a GET request.  It also does not provide a
 # Content-Length in a HEAD response, so the way we tell if a file exists
 # is to check the length of the entire GET response body.
-
-
 def has_tempest_plugin(proj):
     try:
         r = urllib.urlopen(
@@ -71,24 +86,45 @@
         False
 
 
+if len(sys.argv) > 1 and sys.argv[1] == 'blacklist':
+    for black_plugin in BLACKLIST:
+        print(black_plugin)
+    # We just need BLACKLIST when we use this `blacklist` option.
+    # So, this exits here.
+    sys.exit()
+
 r = urllib.urlopen(url)
 # Gerrit prepends 4 garbage octets to the JSON, in order to counter
 # cross-site scripting attacks.  Therefore we must discard it so the
 # json library won't choke.
 content = r.read().decode('utf-8')[4:]
-projects = sorted(filter(is_in_openstack_namespace, json.loads(content)))
+projects = sorted(json.loads(content))
 
-# Retrieve projects having no deb, puppet, ui or spec namespace as those
+# Retrieve projects having no deployment tool repo (such as deb,
+# puppet, ansible, etc.), infra repos, ui or spec namespace as those
 # namespaces do not contains tempest plugins.
 projects_list = [i for i in projects if not (
+    i.startswith('openstack-dev/') or
+    i.startswith('openstack-infra/') or
+    i.startswith('openstack/ansible-') or
+    i.startswith('openstack/charm-') or
+    i.startswith('openstack/cookbook-openstack-') or
+    i.startswith('openstack/devstack-') or
+    i.startswith('openstack/fuel-') or
     i.startswith('openstack/deb-') or
     i.startswith('openstack/puppet-') or
+    i.startswith('openstack/openstack-ansible-') or
+    i.startswith('x/deb-') or
+    i.startswith('x/fuel-') or
+    i.startswith('x/python-') or
+    i.startswith('zuul/') or
     i.endswith('-ui') or
     i.endswith('-specs'))]
 
 found_plugins = list(filter(has_tempest_plugin, projects_list))
 
-# Every element of the found_plugins list begins with "openstack/".
-# We drop those initial 10 octets when printing the list.
+# We have tempest plugins not only in 'openstack/' namespace but also the
+# other name spaces such as 'airship/', 'x/', etc.
+# So, we print all of them here.
 for project in found_plugins:
-    print(project[10:])
+    print(project)
diff --git a/tools/generate-tempest-plugins-list.sh b/tools/generate-tempest-plugins-list.sh
index b4e5430..6e473b7 100755
--- a/tools/generate-tempest-plugins-list.sh
+++ b/tools/generate-tempest-plugins-list.sh
@@ -61,20 +61,37 @@
     printf " ===\n"
 }
 
+function print_plugin_table() {
+    title_underline ${name_col_len}
+    printf "%-3s %-${name_col_len}s %s\n" "SR" "Plugin Name" "URL"
+    title_underline ${name_col_len}
+
+    i=0
+    for plugin in $1; do
+        i=$((i+1))
+        giturl="https://opendev.org/openstack/${plugin}"
+        printf "%-3s %-${name_col_len}s %s\n" "$i" "${plugin}" "${giturl}"
+    done
+
+    title_underline ${name_col_len}
+}
+
 printf "\n\n"
-title_underline ${name_col_len}
-printf "%-3s %-${name_col_len}s %s\n" "SR" "Plugin Name" "URL"
-title_underline ${name_col_len}
+print_plugin_table "${sorted_plugins}"
 
-i=0
-for plugin in ${sorted_plugins}; do
-    i=$((i+1))
-    giturl="https://opendev.org/openstack/${plugin}"
-    gitlink="https://opendev.org/openstack/${plugin}"
-    printf "%-3s %-${name_col_len}s %s\n" "$i" "${plugin}" "\`${giturl} <${gitlink}>\`__"
-done
+printf "\n\n"
 
-title_underline ${name_col_len}
+# Print BLACKLIST
+if [[ -r doc/source/data/tempest-blacklisted-plugins-registry.header ]]; then
+    cat doc/source/data/tempest-blacklisted-plugins-registry.header
+fi
+
+blacklist=$(python tools/generate-tempest-plugins-list.py blacklist)
+name_col_len=$(echo "${blacklist}" | wc -L)
+name_col_len=$(( name_col_len + 20 ))
+
+printf "\n\n"
+print_plugin_table "${blacklist}"
 
 printf "\n\n"
 
diff --git a/tools/tempest-plugin-sanity.sh b/tools/tempest-plugin-sanity.sh
index 47a9ac9..b652369 100644
--- a/tools/tempest-plugin-sanity.sh
+++ b/tools/tempest-plugin-sanity.sh
@@ -43,49 +43,19 @@
 
 # retrieve a list of projects having tempest plugins
 PROJECT_LIST="$(python tools/generate-tempest-plugins-list.py)"
-# List of projects having tempest plugin stale or unmaintained for a long time
-# (6 months or more)
-# TODO(masayukig): Some of these can be removed from BLACKLIST in the future.
-# barbican-tempest-plugin: https://review.opendev.org/#/c/634631/
-# cyborg-tempest-plugin: https://review.opendev.org/659687
-# intel-nfv-ci-tests: https://review.opendev.org/#/c/634640/
-# networking-ansible: https://review.opendev.org/#/c/634647/
-# networking-generic-switch: https://review.opendev.org/#/c/634846/
-# networking-l2gw-tempest-plugin: https://review.opendev.org/#/c/635093/
-# networking-midonet: https://review.opendev.org/#/c/635096/
-# networking-plumgrid: https://review.opendev.org/#/c/635096/
-# networking-spp: https://review.opendev.org/#/c/635098/
-# neutron-dynamic-routing: https://review.opendev.org/#/c/637718/
-# neutron-vpnaas: https://review.opendev.org/#/c/637719/
-# nova-lxd: https://review.opendev.org/#/c/638334/
-# valet: https://review.opendev.org/#/c/638339/
 
-BLACKLIST="
-barbican-tempest-plugin
-cyborg-tempest-plugin
-intel-nfv-ci-tests
-networking-ansible
-networking-generic-switch
-networking-l2gw-tempest-plugin
-networking-midonet
-networking-plumgrid
-networking-spp
-neutron-dynamic-routing
-neutron-vpnaas
-nova-lxd
-valet
-"
+BLACKLIST="$(python tools/generate-tempest-plugins-list.py blacklist)"
 
 # Function to clone project using zuul-cloner or from git
 function clone_project() {
     if [ -e /usr/zuul-env/bin/zuul-cloner ]; then
         /usr/zuul-env/bin/zuul-cloner --cache-dir /opt/git \
         https://opendev.org \
-        openstack/"$1"
+        "$1"
 
     elif [ -e /usr/bin/git ]; then
-        /usr/bin/git clone https://opendev.org/openstack/"$1" \
-        openstack/"$1"
+        /usr/bin/git clone https://opendev.org/"$1" \
+        "$1"
 
     fi
 }
@@ -103,10 +73,10 @@
 
 # Function to install project
 function install_project() {
-    "$TVENV" pip install "$SANITY_DIR"/openstack/"$1"
+    "$TVENV" pip install "$SANITY_DIR"/"$1"
     # Check for test-requirements.txt file in a project then install it.
-    if [ -e "$SANITY_DIR"/openstack/"$1"/test-requirements.txt ]; then
-        "$TVENV" pip install -r "$SANITY_DIR"/openstack/"$1"/test-requirements.txt
+    if [ -e "$SANITY_DIR"/"$1"/test-requirements.txt ]; then
+        "$TVENV" pip install -r "$SANITY_DIR"/"$1"/test-requirements.txt
     fi
 }
 
@@ -124,7 +94,7 @@
     # Remove the sanity workspace in case of remaining
     rm -fr "$SANITY_DIR"/tempest_sanity
     # Remove the project directory after sanity run
-    rm -fr "$SANITY_DIR"/openstack/"$1"
+    rm -fr "$SANITY_DIR"/"$1"
 
     return $retval
 }