Merge "Add tags API tests"
diff --git a/octavia_tempest_plugin/config.py b/octavia_tempest_plugin/config.py
index 8573d89..fc04c33 100644
--- a/octavia_tempest_plugin/config.py
+++ b/octavia_tempest_plugin/config.py
@@ -195,6 +195,12 @@
     cfg.StrOpt('availability_zone',
                default=None,
                help='Availability zone to use for creating servers.'),
+    cfg.StrOpt('availability_zone2',
+               default=None,
+               help='A second availability zone to use for creating servers.'),
+    cfg.StrOpt('availability_zone3',
+               default=None,
+               help='A third availability zone to use for creating servers.'),
     cfg.BoolOpt('test_reuse_connection', default=True,
                 help='Reuse TCP connections while testing LB with '
                      'HTTP members (keep-alive).'),
diff --git a/octavia_tempest_plugin/contrib/test_server/README.rst b/octavia_tempest_plugin/contrib/test_server/README.rst
index da719b7..ba959f9 100644
--- a/octavia_tempest_plugin/contrib/test_server/README.rst
+++ b/octavia_tempest_plugin/contrib/test_server/README.rst
@@ -2,8 +2,8 @@
 Amphorae test server
 ====================
 
-test_server is a static application that simulates an HTTP and a UDP server.
-
+test_server.bin is a static application that simulates HTTP, HTTPS, and UDP
+servers. This server can properly handle concurrent requests.
 
 Building
 --------
@@ -12,15 +12,55 @@
 
 Install dependencies for Ubuntu/Debian:
 
+::
+
     sudo apt-get install -y golang
 
 Install dependencies for Centos (use golang 1.10 from go-toolset-7) and launch
 a shell into the new environment:
 
+::
+
     sudo yum install -y centos-release-scl
     sudo yum install -y go-toolset-7-golang-bin glibc-static openssl-static zlib-static
     scl enable go-toolset-7 bash
 
 Build the binary:
 
+::
+
     CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-s -w -extldflags -static' -o test_server.bin test_server.go
+
+
+Usage
+-----
+
+The usage string can be output from the command by running:
+
+::
+
+    ./test_server.bin --help
+
+Example output:
+
+::
+
+  Usage of ./test_server.bin:
+    -cert string
+          Server side PEM format certificate.
+    -client_ca string
+          Client side PEM format CA certificate.
+    -https_port int
+          HTTPS port to listen on, -1 is disabled. (default -1)
+    -id string
+          Server ID (default "1")
+    -key string
+          Server side PEM format key.
+    -port int
+          Port to listen on (default 8080)
+
+If -https_port is not specified, the server will not accept HTTPS requests.
+When --https_port is specified, -cert and -key are required parameters.
+If -https_port is specified, the -client_ca parameter is optional. When
+-client_ca is specified, it will configure the HTTPS port to require a valid
+client certificate to connect.
diff --git a/octavia_tempest_plugin/contrib/test_server/test_server.bin b/octavia_tempest_plugin/contrib/test_server/test_server.bin
index e3cc7ba..75ec2f2 100755
--- a/octavia_tempest_plugin/contrib/test_server/test_server.bin
+++ b/octavia_tempest_plugin/contrib/test_server/test_server.bin
Binary files differ
diff --git a/octavia_tempest_plugin/contrib/test_server/test_server.go b/octavia_tempest_plugin/contrib/test_server/test_server.go
index 8139580..f8bc1e0 100644
--- a/octavia_tempest_plugin/contrib/test_server/test_server.go
+++ b/octavia_tempest_plugin/contrib/test_server/test_server.go
@@ -1,11 +1,17 @@
 package main
 
 import (
+	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
 	"flag"
 	"fmt"
 	"io"
+	"io/ioutil"
+	"log"
 	"net"
 	"net/http"
+	"os"
 	"sync"
 	"time"
 )
@@ -83,13 +89,23 @@
 	fmt.Fprintf(w, "max_conn=%d\ntotal_conn=%d\n", max_conn, total_conn)
 }
 
+func https_wrapper(base_handler func(http.ResponseWriter,
+	*http.Request)) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+		w.Header().Add("Strict-Transport-Security",
+			"max-age=66012000; includeSubDomains")
+		base_handler(w, r)
+	})
+}
+
 func reset_handler(w http.ResponseWriter, r *http.Request) {
 	http.SetCookie(w, &sess_cookie)
 	scoreboard.reset()
 	fmt.Fprintf(w, "reset\n")
 }
 
-func http_serve(port int, id string) {
+func http_setup(id string) {
 	sess_cookie.Name = "JSESSIONID"
 	sess_cookie.Value = id
 
@@ -97,8 +113,65 @@
 	http.HandleFunc("/slow", slow_handler)
 	http.HandleFunc("/stats", stats_handler)
 	http.HandleFunc("/reset", reset_handler)
+}
+
+func http_serve(port int, id string) {
 	portStr := fmt.Sprintf(":%d", port)
-	http.ListenAndServe(portStr, nil)
+	log.Fatal(http.ListenAndServe(portStr, nil))
+}
+
+func https_serve(port int, id string, cert tls.Certificate,
+	certpool *x509.CertPool, server_cert_pem string,
+	server_key_pem string) {
+	mux := http.NewServeMux()
+	mux.Handle("/", https_wrapper(root_handler))
+	mux.Handle("/slow", https_wrapper(slow_handler))
+	mux.Handle("/stats", https_wrapper(stats_handler))
+	mux.Handle("/reset", https_wrapper(reset_handler))
+
+	var tls_config *tls.Config
+	if certpool != nil {
+		tls_config = &tls.Config{
+			Certificates: []tls.Certificate{cert},
+			ClientAuth:   tls.RequireAndVerifyClientCert,
+			ClientCAs:    certpool,
+			MinVersion:   tls.VersionTLS12,
+			CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384,
+				tls.CurveP256},
+			PreferServerCipherSuites: true,
+			CipherSuites: []uint16{
+				tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+				tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+				tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+				tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+			},
+		}
+	} else {
+		tls_config = &tls.Config{
+			Certificates: []tls.Certificate{cert},
+			ClientAuth:   tls.NoClientCert,
+			MinVersion:   tls.VersionTLS12,
+			CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384,
+				tls.CurveP256},
+			PreferServerCipherSuites: true,
+			CipherSuites: []uint16{
+				tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+				tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+				tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+				tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+			},
+		}
+	}
+	tls_config.Rand = rand.Reader
+	portStr := fmt.Sprintf(":%d", port)
+	srv := &http.Server{
+		Addr:      portStr,
+		Handler:   mux,
+		TLSConfig: tls_config,
+		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn,
+			http.Handler), 0),
+	}
+	log.Fatal(srv.ListenAndServeTLS(server_cert_pem, server_key_pem))
 }
 
 func udp_serve(port int, id string) {
@@ -129,11 +202,44 @@
 func main() {
 	portPtr := flag.Int("port", 8080, "Port to listen on")
 	idPtr := flag.String("id", "1", "Server ID")
+	https_portPtr := flag.Int("https_port", -1,
+		"HTTPS port to listen on, -1 is disabled.")
+	server_cert_pem := flag.String("cert", "",
+		"Server side PEM format certificate.")
+	server_key := flag.String("key", "", "Server side PEM format key.")
+	client_ca_cert_pem := flag.String("client_ca", "",
+		"Client side PEM format CA certificate.")
 
 	flag.Parse()
 
 	resp = fmt.Sprintf("%s", *idPtr)
 
+	http_setup(*idPtr)
+
+	if *https_portPtr > -1 {
+		cert, err := tls.LoadX509KeyPair(*server_cert_pem, *server_key)
+		if err != nil {
+			fmt.Println("Error load server certificate and key.\n")
+			os.Exit(1)
+		}
+		certpool := x509.NewCertPool()
+		if *client_ca_cert_pem != "" {
+			ca_pem, err := ioutil.ReadFile(*client_ca_cert_pem)
+			if err != nil {
+				fmt.Println("Error load client side CA cert.\n")
+				os.Exit(1)
+			}
+			if !certpool.AppendCertsFromPEM(ca_pem) {
+				fmt.Println("Can't parse client side certificate authority")
+				os.Exit(1)
+			}
+		} else {
+			certpool = nil
+		}
+		go https_serve(*https_portPtr, *idPtr, cert, certpool,
+			*server_cert_pem, *server_key)
+	}
+
 	go http_serve(*portPtr, *idPtr)
 	udp_serve(*portPtr, *idPtr)
 }
diff --git a/octavia_tempest_plugin/hacking/__init__.py b/octavia_tempest_plugin/hacking/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/octavia_tempest_plugin/hacking/__init__.py
diff --git a/octavia_tempest_plugin/hacking/checks.py b/octavia_tempest_plugin/hacking/checks.py
new file mode 100644
index 0000000..eec7476
--- /dev/null
+++ b/octavia_tempest_plugin/hacking/checks.py
@@ -0,0 +1,277 @@
+# Copyright (c) 2014 OpenStack Foundation.
+#
+# 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.
+
+
+"""
+Guidelines for writing new hacking checks
+
+ - Use only for Octavia specific tests. OpenStack general tests
+   should be submitted to the common 'hacking' module.
+ - Pick numbers in the range O3xx. Find the current test with
+   the highest allocated number and then pick the next value.
+ - Keep the test method code in the source file ordered based
+   on the O3xx value.
+ - List the new rule in the top level HACKING.rst file
+ - Add test cases for each new rule to
+   octavia/tests/unit/test_hacking.py
+
+"""
+
+import re
+
+from hacking import core
+
+
+_all_log_levels = {'critical', 'error', 'exception', 'info', 'warning'}
+_all_hints = {'_LC', '_LE', '_LI', '_', '_LW'}
+
+_log_translation_hint = re.compile(
+    r".*LOG\.(%(levels)s)\(\s*(%(hints)s)\(" % {
+        'levels': '|'.join(_all_log_levels),
+        'hints': '|'.join(_all_hints),
+    })
+
+assert_trueinst_re = re.compile(
+    r"(.)*assertTrue\(isinstance\((\w|\.|\'|\"|\[|\])+, "
+    r"(\w|\.|\'|\"|\[|\])+\)\)")
+assert_equal_in_end_with_true_or_false_re = re.compile(
+    r"assertEqual\((\w|[][.'\"])+ in (\w|[][.'\", ])+, (True|False)\)")
+assert_equal_in_start_with_true_or_false_re = re.compile(
+    r"assertEqual\((True|False), (\w|[][.'\"])+ in (\w|[][.'\", ])+\)")
+assert_equal_with_true_re = re.compile(
+    r"assertEqual\(True,")
+assert_equal_with_false_re = re.compile(
+    r"assertEqual\(False,")
+mutable_default_args = re.compile(r"^\s*def .+\((.+=\{\}|.+=\[\])")
+assert_equal_end_with_none_re = re.compile(r"(.)*assertEqual\(.+, None\)")
+assert_equal_start_with_none_re = re.compile(r".*assertEqual\(None, .+\)")
+assert_not_equal_end_with_none_re = re.compile(
+    r"(.)*assertNotEqual\(.+, None\)")
+assert_not_equal_start_with_none_re = re.compile(
+    r"(.)*assertNotEqual\(None, .+\)")
+revert_must_have_kwargs_re = re.compile(
+    r'[ ]*def revert\(.+,[ ](?!\*\*kwargs)\w+\):')
+untranslated_exception_re = re.compile(r"raise (?:\w*)\((.*)\)")
+no_eventlet_re = re.compile(r'(import|from)\s+[(]?eventlet')
+no_line_continuation_backslash_re = re.compile(r'.*(\\)\n')
+no_logging_re = re.compile(r'(import|from)\s+[(]?logging')
+import_mock_re = re.compile(r"\bimport[\s]+mock\b")
+import_from_mock_re = re.compile(r"\bfrom[\s]+mock[\s]+import\b")
+
+
+def _translation_checks_not_enforced(filename):
+    # Do not do these validations on tests
+    return any(pat in filename for pat in ["/tests/", "rally-jobs/plugins/"])
+
+
+@core.flake8ext
+def assert_true_instance(logical_line):
+    """Check for assertTrue(isinstance(a, b)) sentences
+
+    O316
+    """
+    if assert_trueinst_re.match(logical_line):
+        yield (0, "O316: assertTrue(isinstance(a, b)) sentences not allowed. "
+               "Use assertIsInstance instead.")
+
+
+@core.flake8ext
+def assert_equal_or_not_none(logical_line):
+    """Check for assertEqual(A, None) or assertEqual(None, A) sentences,
+
+    assertNotEqual(A, None) or assertNotEqual(None, A) sentences
+
+    O318
+    """
+    msg = ("O318: assertEqual/assertNotEqual(A, None) or "
+           "assertEqual/assertNotEqual(None, A) sentences not allowed")
+    res = (assert_equal_start_with_none_re.match(logical_line) or
+           assert_equal_end_with_none_re.match(logical_line) or
+           assert_not_equal_start_with_none_re.match(logical_line) or
+           assert_not_equal_end_with_none_re.match(logical_line))
+    if res:
+        yield (0, msg)
+
+
+@core.flake8ext
+def assert_equal_true_or_false(logical_line):
+    """Check for assertEqual(True, A) or assertEqual(False, A) sentences
+
+    O323
+    """
+    res = (assert_equal_with_true_re.search(logical_line) or
+           assert_equal_with_false_re.search(logical_line))
+    if res:
+        yield (0, "O323: assertEqual(True, A) or assertEqual(False, A) "
+               "sentences not allowed")
+
+
+@core.flake8ext
+def no_mutable_default_args(logical_line):
+    msg = "O324: Method's default argument shouldn't be mutable!"
+    if mutable_default_args.match(logical_line):
+        yield (0, msg)
+
+
+@core.flake8ext
+def assert_equal_in(logical_line):
+    """Check for assertEqual(A in B, True), assertEqual(True, A in B),
+
+    assertEqual(A in B, False) or assertEqual(False, A in B) sentences
+
+    O338
+    """
+    res = (assert_equal_in_start_with_true_or_false_re.search(logical_line) or
+           assert_equal_in_end_with_true_or_false_re.search(logical_line))
+    if res:
+        yield (0, "O338: Use assertIn/NotIn(A, B) rather than "
+               "assertEqual(A in B, True/False) when checking collection "
+               "contents.")
+
+
+@core.flake8ext
+def no_log_warn(logical_line):
+    """Disallow 'LOG.warn('
+
+    O339
+    """
+    if logical_line.startswith('LOG.warn('):
+        yield(0, "O339:Use LOG.warning() rather than LOG.warn()")
+
+
+@core.flake8ext
+def no_translate_logs(logical_line, filename):
+    """O341 - Don't translate logs.
+
+    Check for 'LOG.*(_(' and 'LOG.*(_Lx('
+
+    Translators don't provide translations for log messages, and operators
+    asked not to translate them.
+
+    * This check assumes that 'LOG' is a logger.
+
+    :param logical_line: The logical line to check.
+    :param filename: The file name where the logical line exists.
+    :returns: None if the logical line passes the check, otherwise a tuple
+              is yielded that contains the offending index in logical line
+              and a message describe the check validation failure.
+    """
+    if _translation_checks_not_enforced(filename):
+        return
+
+    msg = "O341: Log messages should not be translated!"
+    match = _log_translation_hint.match(logical_line)
+    if match:
+        yield (logical_line.index(match.group()), msg)
+
+
+@core.flake8ext
+def check_raised_localized_exceptions(logical_line, filename):
+    """O342 - Untranslated exception message.
+
+    :param logical_line: The logical line to check.
+    :param filename: The file name where the logical line exists.
+    :returns: None if the logical line passes the check, otherwise a tuple
+              is yielded that contains the offending index in logical line
+              and a message describe the check validation failure.
+    """
+    if _translation_checks_not_enforced(filename):
+        return
+
+    logical_line = logical_line.strip()
+    raised_search = untranslated_exception_re.match(logical_line)
+    if raised_search:
+        exception_msg = raised_search.groups()[0]
+        if exception_msg.startswith("\"") or exception_msg.startswith("\'"):
+            msg = "O342: Untranslated exception message."
+            yield (logical_line.index(exception_msg), msg)
+
+
+@core.flake8ext
+def check_no_eventlet_imports(logical_line):
+    """O345 - Usage of Python eventlet module not allowed.
+
+    :param logical_line: The logical line to check.
+    :returns: None if the logical line passes the check, otherwise a tuple
+              is yielded that contains the offending index in logical line
+              and a message describe the check validation failure.
+    """
+    if no_eventlet_re.match(logical_line):
+        msg = 'O345 Usage of Python eventlet module not allowed'
+        yield logical_line.index('eventlet'), msg
+
+
+@core.flake8ext
+def check_line_continuation_no_backslash(logical_line, tokens):
+    """O346 - Don't use backslashes for line continuation.
+
+    :param logical_line: The logical line to check. Not actually used.
+    :param tokens: List of tokens to check.
+    :returns: None if the tokens don't contain any issues, otherwise a tuple
+              is yielded that contains the offending index in the logical
+              line and a message describe the check validation failure.
+    """
+    backslash = None
+    for token_type, text, start, end, orig_line in tokens:
+        m = no_line_continuation_backslash_re.match(orig_line)
+        if m:
+            backslash = (start[0], m.start(1))
+            break
+
+    if backslash is not None:
+        msg = 'O346 Backslash line continuations not allowed'
+        yield backslash, msg
+
+
+@core.flake8ext
+def revert_must_have_kwargs(logical_line):
+    """O347 - Taskflow revert methods must have \\*\\*kwargs.
+
+    :param logical_line: The logical line to check.
+    :returns: None if the logical line passes the check, otherwise a tuple
+              is yielded that contains the offending index in logical line
+              and a message describe the check validation failure.
+    """
+    if revert_must_have_kwargs_re.match(logical_line):
+        msg = 'O347 Taskflow revert methods must have **kwargs'
+        yield 0, msg
+
+
+@core.flake8ext
+def check_no_logging_imports(logical_line):
+    """O348 - Usage of Python logging module not allowed.
+
+    :param logical_line: The logical line to check.
+    :returns: None if the logical line passes the check, otherwise a tuple
+              is yielded that contains the offending index in logical line
+              and a message describe the check validation failure.
+    """
+    if no_logging_re.match(logical_line):
+        msg = 'O348 Usage of Python logging module not allowed, use oslo_log'
+        yield logical_line.index('logging'), msg
+
+
+@core.flake8ext
+def check_no_import_mock(logical_line):
+    """O349 - Test code must not import mock library.
+
+    :param logical_line: The logical line to check.
+    :returns: None if the logical line passes the check, otherwise a tuple
+              is yielded that contains the offending index in logical line
+              and a message describe the check validation failure.
+    """
+    if (import_mock_re.match(logical_line) or
+            import_from_mock_re.match(logical_line)):
+        msg = 'O349 Test code must not import mock library, use unittest.mock'
+        yield 0, msg
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/__init__.py b/octavia_tempest_plugin/services/load_balancer/v2/__init__.py
index d31d6cf..04cb473 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/__init__.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/__init__.py
@@ -12,7 +12,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from octavia_tempest_plugin.services.load_balancer.v2.loadbalancer_client \
-    import LoadbalancerClient
+from .loadbalancer_client import LoadbalancerClient
 
 __all__ = ['LoadbalancerClient']
diff --git a/octavia_tempest_plugin/tests/api/v2/test_availability_zone.py b/octavia_tempest_plugin/tests/api/v2/test_availability_zone.py
index e9671fd..29426c2 100644
--- a/octavia_tempest_plugin/tests/api/v2/test_availability_zone.py
+++ b/octavia_tempest_plugin/tests/api/v2/test_availability_zone.py
@@ -32,6 +32,15 @@
     """Test the availability zone object API."""
 
     @classmethod
+    def skip_checks(cls):
+        super(AvailabilityZoneAPITest, cls).skip_checks()
+        if (CONF.load_balancer.availability_zone is None and
+                not CONF.load_balancer.test_with_noop):
+            raise cls.skipException(
+                'Availability Zone API tests require an availability zone '
+                'configured in the [load_balancer] availability_zone setting.')
+
+    @classmethod
     def resource_setup(cls):
         """Setup resources needed by the tests."""
         super(AvailabilityZoneAPITest, cls).resource_setup()
@@ -46,7 +55,7 @@
         availability_zone_profile_name = data_utils.rand_name(
             "lb_admin_availabilityzoneprofile-setup")
         availability_zone_data = {
-            const.COMPUTE_ZONE: 'my_compute_zone',
+            const.COMPUTE_ZONE: CONF.load_balancer.availability_zone,
             const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(),
         }
         availability_zone_data_json = jsonutils.dumps(availability_zone_data)
diff --git a/octavia_tempest_plugin/tests/api/v2/test_availability_zone_profile.py b/octavia_tempest_plugin/tests/api/v2/test_availability_zone_profile.py
index 1a5501a..f5e4b7e 100644
--- a/octavia_tempest_plugin/tests/api/v2/test_availability_zone_profile.py
+++ b/octavia_tempest_plugin/tests/api/v2/test_availability_zone_profile.py
@@ -32,6 +32,16 @@
 class AvailabilityZoneProfileAPITest(test_base.LoadBalancerBaseTest):
     """Test the availability zone profile object API."""
 
+    @classmethod
+    def skip_checks(cls):
+        super(AvailabilityZoneProfileAPITest, cls).skip_checks()
+        if (CONF.load_balancer.availability_zone is None and
+                not CONF.load_balancer.test_with_noop):
+            raise cls.skipException(
+                'Availability zone profile API tests require an availability '
+                'zone configured in the [load_balancer] availability_zone '
+                'setting in the tempest configuration file.')
+
     @decorators.idempotent_id('e512b580-ef32-44c3-bbd2-efdc27ba2ea6')
     def test_availability_zone_profile_create(self):
         """Tests availability zone profile create and basic show APIs.
@@ -53,7 +63,7 @@
         availability_zone_profile_name = data_utils.rand_name(
             "lb_admin_availabilityzoneprofile1-create")
         availability_zone_data = {
-            const.COMPUTE_ZONE: 'my_compute_zone',
+            const.COMPUTE_ZONE: CONF.load_balancer.availability_zone,
             const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(),
         }
         availability_zone_data_json = jsonutils.dumps(availability_zone_data)
@@ -127,12 +137,20 @@
             raise self.skipException(
                 'Availability zone profiles are only available on '
                 'Octavia API version 2.14 or newer.')
+        if ((CONF.load_balancer.availability_zone2 is None or
+             CONF.load_balancer.availability_zone3 is None) and
+                not CONF.load_balancer.test_with_noop):
+            raise self.skipException(
+                'Availability zone profile list API test requires the '
+                '[load_balancer] availability_zone, availability_zone2, and '
+                'availability_zone3 settings be defined in the tempest '
+                'configuration file.')
 
         # Create availability zone profile 1
         availability_zone_profile1_name = data_utils.rand_name(
             "lb_admin_availabilityzoneprofile-list-1")
         availability_zone_data1 = {
-            const.COMPUTE_ZONE: 'my_compute_zone1',
+            const.COMPUTE_ZONE: CONF.load_balancer.availability_zone,
             const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(),
         }
         availability_zone_data1_json = jsonutils.dumps(availability_zone_data1)
@@ -155,7 +173,7 @@
         availability_zone_profile2_name = data_utils.rand_name(
             "lb_admin_availabilityzoneprofile-list-2")
         availability_zone_data2 = {
-            const.COMPUTE_ZONE: 'my_compute_zone2',
+            const.COMPUTE_ZONE: CONF.load_balancer.availability_zone2,
             const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(),
         }
         availability_zone_data2_json = jsonutils.dumps(availability_zone_data2)
@@ -178,7 +196,7 @@
         availability_zone_profile3_name = data_utils.rand_name(
             "lb_admin_availabilityzoneprofile-list-3")
         availability_zone_data3 = {
-            const.COMPUTE_ZONE: 'my_compute_zone3',
+            const.COMPUTE_ZONE: CONF.load_balancer.availability_zone3,
             const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(),
         }
         availability_zone_data3_json = jsonutils.dumps(availability_zone_data3)
@@ -340,7 +358,7 @@
         availability_zone_profile_name = data_utils.rand_name(
             "lb_admin_availabilityzoneprofile1-show")
         availability_zone_data = {
-            const.COMPUTE_ZONE: 'my_compute_zone',
+            const.COMPUTE_ZONE: CONF.load_balancer.availability_zone,
             const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(),
         }
         availability_zone_data_json = jsonutils.dumps(availability_zone_data)
@@ -401,11 +419,17 @@
             raise self.skipException(
                 'Availability zone profiles are only available on '
                 'Octavia API version 2.14 or newer.')
+        if (CONF.load_balancer.availability_zone2 is None and
+                not CONF.load_balancer.test_with_noop):
+            raise self.skipException(
+                'Availability zone profile update API tests requires '
+                '[load_balancer] availability_zone2 to be defined in the '
+                'tempest configuration file.')
 
         availability_zone_profile_name = data_utils.rand_name(
             "lb_admin_availabilityzoneprofile1-update")
         availability_zone_data = {
-            const.COMPUTE_ZONE: 'my_compute_zone1',
+            const.COMPUTE_ZONE: CONF.load_balancer.availability_zone,
             const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(),
         }
         availability_zone_data_json = jsonutils.dumps(availability_zone_data)
@@ -438,7 +462,7 @@
         availability_zone_profile_name2 = data_utils.rand_name(
             "lb_admin_availabilityzoneprofile1-update2")
         availability_zone_data2 = {
-            const.COMPUTE_ZONE: 'my_compute_zone2',
+            const.COMPUTE_ZONE:  CONF.load_balancer.availability_zone2,
             const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(),
         }
         availability_zone_data2_json = jsonutils.dumps(availability_zone_data2)
@@ -506,7 +530,7 @@
         availability_zone_profile_name = data_utils.rand_name(
             "lb_admin_availabilityzoneprofile1-delete")
         availability_zone_data = {
-            const.COMPUTE_ZONE: 'my_compute_zone',
+            const.COMPUTE_ZONE: CONF.load_balancer.availability_zone,
             const.MANAGEMENT_NETWORK: uuidutils.generate_uuid(),
         }
         availability_zone_data_json = jsonutils.dumps(availability_zone_data)
diff --git a/octavia_tempest_plugin/tests/api/v2/test_l7rule.py b/octavia_tempest_plugin/tests/api/v2/test_l7rule.py
index 70eff54..e954b84 100644
--- a/octavia_tempest_plugin/tests/api/v2/test_l7rule.py
+++ b/octavia_tempest_plugin/tests/api/v2/test_l7rule.py
@@ -638,7 +638,7 @@
         l7rule_check = self.mem_l7rule_client.show_l7rule(
             l7rule[const.ID], l7policy_id=self.l7policy_id)
         self.assertEqual(const.ACTIVE, l7rule_check[const.PROVISIONING_STATUS])
-        self.assertEqual(False, l7rule_check[const.ADMIN_STATE_UP])
+        self.assertFalse(l7rule_check[const.ADMIN_STATE_UP])
 
         # Test that a user, without the load balancer member role, cannot
         # update this l7rule
@@ -653,7 +653,7 @@
         l7rule_check = self.mem_l7rule_client.show_l7rule(
             l7rule[const.ID], l7policy_id=self.l7policy_id)
         self.assertEqual(const.ACTIVE, l7rule_check[const.PROVISIONING_STATUS])
-        self.assertEqual(False, l7rule_check[const.ADMIN_STATE_UP])
+        self.assertFalse(l7rule_check[const.ADMIN_STATE_UP])
 
         l7rule_update_kwargs = {
             const.L7POLICY_ID: self.l7policy_id,
diff --git a/octavia_tempest_plugin/tests/scenario/v2/test_pool.py b/octavia_tempest_plugin/tests/scenario/v2/test_pool.py
index 5e0622c..720e80a 100644
--- a/octavia_tempest_plugin/tests/scenario/v2/test_pool.py
+++ b/octavia_tempest_plugin/tests/scenario/v2/test_pool.py
@@ -167,8 +167,8 @@
         }
 
         if self.lb_feature_enabled.pool_algorithms_enabled:
-            pool_update_kwargs[const.LB_ALGORITHM] = \
-                const.LB_ALGORITHM_LEAST_CONNECTIONS
+            pool_update_kwargs[const.LB_ALGORITHM] = (
+                const.LB_ALGORITHM_LEAST_CONNECTIONS)
 
         if self.protocol == const.HTTP and (
                 self.lb_feature_enabled.session_persistence_enabled):
diff --git a/octavia_tempest_plugin/tests/scenario/v2/test_traffic_ops.py b/octavia_tempest_plugin/tests/scenario/v2/test_traffic_ops.py
index 0d49d67..83049c7 100644
--- a/octavia_tempest_plugin/tests/scenario/v2/test_traffic_ops.py
+++ b/octavia_tempest_plugin/tests/scenario/v2/test_traffic_ops.py
@@ -198,6 +198,7 @@
                                     protocol_port=protocol_port,
                                     protocol=protocol)
 
+    @decorators.attr(type=['smoke', 'slow'])
     @testtools.skipIf(CONF.load_balancer.test_with_noop,
                       'Traffic tests will not work in noop mode.')
     @decorators.idempotent_id('6751135d-e15a-4e22-89f4-bfcc3408d424')
diff --git a/releasenotes/notes/skip-az-api-tests-if-azs-not-configured-c5d06cdcf29beeb5.yaml b/releasenotes/notes/skip-az-api-tests-if-azs-not-configured-c5d06cdcf29beeb5.yaml
new file mode 100644
index 0000000..84b1480
--- /dev/null
+++ b/releasenotes/notes/skip-az-api-tests-if-azs-not-configured-c5d06cdcf29beeb5.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+  - |
+    Fixed availability zone API tests to skip if the required availability
+    zones are not defined in the tempest configuration file and the test run
+    is not using no-op drivers.
diff --git a/setup.cfg b/setup.cfg
index 3e5d216..d7d3196 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -19,6 +19,7 @@
     Programming Language :: Python :: 3
     Programming Language :: Python :: 3.6
     Programming Language :: Python :: 3.7
+    Programming Language :: Python :: 3.8
 
 [global]
 setup-hooks =
diff --git a/test-requirements.txt b/test-requirements.txt
index 354a020..2125ea0 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -2,7 +2,7 @@
 # of appearance. Changing the order has an impact on the overall integration
 # process, which may cause wedges in the gate later.
 
-hacking>=3.0,<3.1.0;python_version>='3.5' # Apache-2.0
+hacking>=3.0.1,<3.1.0;python_version>='3.5' # Apache-2.0
 
 coverage!=4.4,>=4.0 # Apache-2.0
 python-subunit>=1.0.0 # Apache-2.0/BSD
diff --git a/tox.ini b/tox.ini
index 4a61222..a419c62 100644
--- a/tox.ini
+++ b/tox.ini
@@ -43,6 +43,7 @@
 deps =
     -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
     -r{toxinidir}/requirements.txt
+    -r{toxinidir}/test-requirements.txt
     -r{toxinidir}/doc/requirements.txt
 whitelist_externals = rm
 commands =
@@ -85,6 +86,24 @@
 # [H904]: Delay string interpolations at logging calls
 enable-extensions=H106,H203,H204,H205,H904
 
+[flake8:local-plugins]
+extension =
+  O316 = checks:assert_true_instance
+  O318 = checks:assert_equal_or_not_none
+  O323 = checks:assert_equal_true_or_false
+  O324 = checks:no_mutable_default_args
+  O338 = checks:assert_equal_in
+  O339 = checks:no_log_warn
+  O341 = checks:no_translate_logs
+  O342 = checks:check_raised_localized_exceptions
+  O345 = checks:check_no_eventlet_imports
+  O346 = checks:check_line_continuation_no_backslash
+  O347 = checks:revert_must_have_kwargs
+  O348 = checks:check_no_logging_imports
+  O349 = checks:check_no_import_mock
+paths =
+  ./octavia_tempest_plugin/hacking
+
 [testenv:genconfig]
 basepython = python3
 whitelist_externals = mkdir
diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml
index 38b2d04..2f94ada 100644
--- a/zuul.d/jobs.yaml
+++ b/zuul.d/jobs.yaml
@@ -1,10 +1,40 @@
 - nodeset:
+    name: octavia-single-node-ubuntu-bionic
+    nodes:
+      - name: controller
+        label: nested-virt-ubuntu-bionic
+    groups:
+      - name: tempest
+        nodes:
+          - controller
+
+- nodeset:
+    name: octavia-single-node-centos-7
+    nodes:
+      - name: controller
+        label: nested-virt-centos-7
+    groups:
+      - name: tempest
+        nodes:
+          - controller
+
+- nodeset:
+    name: octavia-single-node-centos-8
+    nodes:
+      - name: controller
+        label: nested-virt-centos-8
+    groups:
+      - name: tempest
+        nodes:
+          - controller
+
+- nodeset:
     name: octavia-two-node
     nodes:
       - name: controller
-        label: ubuntu-bionic
+        label: nested-virt-ubuntu-bionic
       - name: controller2
-        label: ubuntu-bionic
+        label: nested-virt-ubuntu-bionic
     groups:
       - name: controller
         nodes:
@@ -142,6 +172,7 @@
 - job:
     name: octavia-dsvm-live-base
     parent: octavia-dsvm-base
+    nodeset: octavia-single-node-ubuntu-bionic
     timeout: 9000
     required-projects:
       - openstack/diskimage-builder
@@ -176,6 +207,7 @@
 - job:
     name: octavia-dsvm-live-base-ipv6-only
     parent: octavia-dsvm-base-ipv6-only
+    nodeset: octavia-single-node-ubuntu-bionic
     timeout: 9000
     required-projects:
       - openstack/diskimage-builder
@@ -309,6 +341,7 @@
           g-api: true
           g-reg: true
           key: true
+          memory_tracker: false
           mysql: true
           n-api: true
           n-api-meta: true
@@ -324,7 +357,6 @@
           o-cw: true
           o-hm: true
           o-hk: true
-          peakmem_tracker: true
           placement-api: true
           q-agt: true
           q-dhcp: true
@@ -365,12 +397,18 @@
           "$TEMPEST_CONFIG":
             load_balancer:
               test_with_noop: True
+              # AZ API tests with no-op need AZs configured but they do not
+              # need to actually exist in Nova due to the no-op driver.
+              availability_zone: bogus-az-1
+              availability_zone2: bogus-az-2
+              availability_zone3: bogus-az-3
         post-config:
           $OCTAVIA_CONF:
             controller_worker:
               amphora_driver: amphora_noop_driver
               compute_driver: compute_noop_driver
               network_driver: network_noop_driver
+              image_driver: image_noop_driver
             certificates:
               cert_manager: local_cert_manager
       devstack_services:
@@ -408,6 +446,11 @@
         USE_PYTHON3: False
 
 - job:
+    name: octavia-v2-dsvm-noop-api-stable-ussuri
+    parent: octavia-v2-dsvm-noop-api
+    override-checkout: stable/ussuri
+
+- job:
     name: octavia-v2-dsvm-noop-api-stable-train
     parent: octavia-v2-dsvm-noop-api
     override-checkout: stable/train
@@ -468,6 +511,11 @@
         override-checkout: 2.30.0
 
 - job:
+    name: octavia-v2-dsvm-scenario-stable-ussuri
+    parent: octavia-v2-dsvm-scenario
+    override-checkout: stable/ussuri
+
+- job:
     name: octavia-v2-dsvm-scenario-stable-train
     parent: octavia-v2-dsvm-scenario
     override-checkout: stable/train
@@ -530,7 +578,7 @@
 - job:
     name: octavia-v2-dsvm-py2-scenario-centos-7
     parent: octavia-v2-dsvm-py2-scenario
-    nodeset: devstack-single-node-centos-7
+    nodeset: octavia-single-node-centos-7
     vars:
       devstack_localrc:
         OCTAVIA_AMP_BASE_OS: centos
@@ -540,6 +588,7 @@
 - job:
     name: octavia-v2-dsvm-scenario-centos-8
     parent: octavia-v2-dsvm-scenario
+    nodeset: octavia-single-node-centos-8
     vars:
       devstack_localrc:
         OCTAVIA_AMP_BASE_OS: centos
@@ -590,6 +639,11 @@
       - ^octavia_tempest_plugin/tests/(?!barbican_scenario/|\w+\.py).*
 
 - job:
+    name: octavia-v2-dsvm-tls-barbican-stable-ussuri
+    parent: octavia-v2-dsvm-tls-barbican
+    override-checkout: stable/ussuri
+
+- job:
     name: octavia-v2-dsvm-tls-barbican-stable-train
     parent: octavia-v2-dsvm-tls-barbican
     override-checkout: stable/train
@@ -605,11 +659,13 @@
 - job:
     name: octavia-v2-dsvm-tls-barbican-stable-rocky
     parent: octavia-v2-dsvm-tls-barbican
+    nodeset: openstack-single-node-xenial
     override-checkout: stable/rocky
 
 - job:
     name: octavia-v2-dsvm-tls-barbican-stable-queens
     parent: octavia-v2-dsvm-tls-barbican
+    nodeset: openstack-single-node-xenial
     override-checkout: stable/queens
 
 - job:
@@ -638,6 +694,11 @@
         override-checkout: 2.30.0
 
 - job:
+    name: octavia-v2-dsvm-spare-pool-stable-ussuri
+    parent: octavia-v2-dsvm-spare-pool
+    override-checkout: stable/ussuri
+
+- job:
     name: octavia-v2-dsvm-spare-pool-stable-train
     parent: octavia-v2-dsvm-spare-pool
     override-checkout: stable/train
@@ -707,6 +768,7 @@
 - job:
     name: octavia-v2-dsvm-scenario-centos-7
     parent: octavia-v2-dsvm-py2-scenario-centos-7
+    nodeset: octavia-single-node-centos-7
 
 - job:
     name: octavia-v2-act-stdby-iptables-dsvm-scenario
@@ -748,7 +810,7 @@
 - job:
     name: octavia-v2-act-stdby-iptables-dsvm-py2-scenario-centos-7
     parent: octavia-v2-act-stdby-iptables-dsvm-py2-scenario
-    nodeset: devstack-single-node-centos-7
+    nodeset: octavia-single-node-centos-7
     vars:
       devstack_localrc:
         USE_PYTHON3: False
@@ -779,6 +841,11 @@
       tox_envlist: all
 
 - job:
+    name: octavia-v2-act-stdby-dsvm-scenario-stable-ussuri
+    parent: octavia-v2-act-stdby-dsvm-scenario
+    override-checkout: stable/ussuri
+
+- job:
     name: octavia-v2-act-stdby-dsvm-scenario-stable-train
     parent: octavia-v2-act-stdby-dsvm-scenario
     override-checkout: stable/train
diff --git a/zuul.d/projects.yaml b/zuul.d/projects.yaml
index 6b1fcc0..ec11f85 100644
--- a/zuul.d/projects.yaml
+++ b/zuul.d/projects.yaml
@@ -9,11 +9,17 @@
     check:
       jobs:
         - octavia-v2-dsvm-noop-api
+        - octavia-v2-dsvm-noop-api-stable-ussuri
         - octavia-v2-dsvm-noop-api-stable-train
         - octavia-v2-dsvm-noop-api-stable-stein
         - octavia-v2-dsvm-scenario
+        - octavia-v2-dsvm-scenario-stable-ussuri
         - octavia-v2-dsvm-scenario-stable-train
         - octavia-v2-dsvm-scenario-stable-stein
+        - octavia-v2-dsvm-tls-barbican
+        - octavia-v2-dsvm-tls-barbican-stable-ussuri
+        - octavia-v2-dsvm-tls-barbican-stable-train
+        - octavia-v2-dsvm-tls-barbican-stable-stein
         - octavia-v2-dsvm-scenario-ipv6-only:
             voting: false
         - octavia-v2-dsvm-scenario-centos-8:
@@ -22,18 +28,16 @@
             voting: false
         - octavia-v2-act-stdby-dsvm-scenario:
             voting: false
+        - octavia-v2-act-stdby-dsvm-scenario-stable-ussuri:
+            voting: false
         - octavia-v2-act-stdby-dsvm-scenario-stable-train:
             voting: false
         - octavia-v2-act-stdby-dsvm-scenario-stable-stein:
             voting: false
-        - octavia-v2-dsvm-tls-barbican:
-            voting: false
-        - octavia-v2-dsvm-tls-barbican-stable-train:
-            voting: false
-        - octavia-v2-dsvm-tls-barbican-stable-stein:
-            voting: false
         - octavia-v2-dsvm-spare-pool:
             voting: false
+        - octavia-v2-dsvm-spare-pool-stable-ussuri:
+            voting: false
         - octavia-v2-dsvm-spare-pool-stable-train:
             voting: false
         - octavia-v2-dsvm-spare-pool-stable-stein:
@@ -45,8 +49,14 @@
       queue: octavia
       jobs:
         - octavia-v2-dsvm-noop-api
+        - octavia-v2-dsvm-noop-api-stable-ussuri
         - octavia-v2-dsvm-noop-api-stable-train
         - octavia-v2-dsvm-noop-api-stable-stein
         - octavia-v2-dsvm-scenario
+        - octavia-v2-dsvm-scenario-stable-ussuri
         - octavia-v2-dsvm-scenario-stable-train
         - octavia-v2-dsvm-scenario-stable-stein
+        - octavia-v2-dsvm-tls-barbican
+        - octavia-v2-dsvm-tls-barbican-stable-ussuri
+        - octavia-v2-dsvm-tls-barbican-stable-train
+        - octavia-v2-dsvm-tls-barbican-stable-stein