Merge "Add py38 package metadata"
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 54fca18..2a94e8b 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -3,11 +3,11 @@
 # process, which may cause wedges in the gate later.
 
 sphinxcontrib-apidoc>=0.2.0 # BSD
-sphinx!=1.6.6,!=1.6.7,>=1.6.2,!=2.1.0;  # BSD
-openstackdocstheme>=1.18.1 # Apache-2.0
+sphinx>=2.0.0,!=2.1.0 # BSD
+openstackdocstheme>=2.2.1 # Apache-2.0
 
 # releasenotes
-reno>=2.5.0 # Apache-2.0
+reno>=3.1.0 # Apache-2.0
 
 # PDF Docs
 sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 5bbe295..ff6a826 100755
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -16,8 +16,6 @@
 import os
 import sys
 
-import openstackdocstheme
-
 sys.path.insert(0, os.path.abspath('../..'))
 sys.path.insert(0, os.path.abspath('.'))
 
@@ -49,16 +47,6 @@
 project = u'octavia-tempest-plugin'
 copyright = u'2017-2019, OpenStack Foundation'
 
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-# Version info
-from octavia_tempest_plugin.version import version_info as octavia_tempest_ver
-release = octavia_tempest_ver.release_string()
-# The short X.Y version.
-version = octavia_tempest_ver.version_string()
-
 # If true, '()' will be appended to :func: etc. cross-reference text.
 add_function_parentheses = True
 
@@ -67,14 +55,14 @@
 add_module_names = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = 'native'
 
 # A list of ignored prefixes for module index sorting.
 modindex_common_prefix = ['octavia_tempest_plugin.']
 
-repository_name = 'openstack/octavia-tempest-plugin'
-bug_project = '910'
-bug_tag = 'docs'
+openstackdocs_repo_name = 'openstack/octavia-tempest-plugin'
+openstackdocs_pdf_link = True
+openstackdocs_use_storyboard = True
 
 apidoc_output_dir = '_build/modules'
 apidoc_module_dir = '../../octavia_tempest_plugin'
@@ -90,7 +78,6 @@
 
 html_theme = 'openstackdocs'
 
-html_last_updated_fmt = '%Y-%m-%d %H:%M'
 
 # Output file base name for HTML help builder.
 htmlhelp_basename = '%sdoc' % project
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_l7rule.py b/octavia_tempest_plugin/tests/api/v2/test_l7rule.py
index aaad8a0..9a73034 100644
--- a/octavia_tempest_plugin/tests/api/v2/test_l7rule.py
+++ b/octavia_tempest_plugin/tests/api/v2/test_l7rule.py
@@ -573,7 +573,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
@@ -588,7 +588,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/barbican_scenario/v2/test_tls_barbican.py b/octavia_tempest_plugin/tests/barbican_scenario/v2/test_tls_barbican.py
index 84bfc20..a753a5c 100644
--- a/octavia_tempest_plugin/tests/barbican_scenario/v2/test_tls_barbican.py
+++ b/octavia_tempest_plugin/tests/barbican_scenario/v2/test_tls_barbican.py
@@ -749,8 +749,8 @@
                                 CONF.load_balancer.build_timeout)
 
         # Test that no client certificate fails to connect
-        self.assertRaisesRegex(
-            requests.exceptions.SSLError, ".*certificate required.*",
+        self.assertRaises(
+            requests.exceptions.SSLError,
             requests.get,
             'https://{0}:{1}'.format(self.lb_vip_address, LISTENER1_TCP_PORT),
             timeout=12, verify=False)
@@ -764,8 +764,8 @@
                     serialization.Encoding.PEM,
                     serialization.PrivateFormat.TraditionalOpenSSL,
                     serialization.NoEncryption()))
-                self.assertRaisesRegex(
-                    requests.exceptions.SSLError, ".*revoked.*", requests.get,
+                self.assertRaises(
+                    requests.exceptions.SSLError, requests.get,
                     'https://{0}:{1}'.format(self.lb_vip_address,
                                              LISTENER1_TCP_PORT),
                     timeout=12, verify=False, cert=(cert_file.name,
@@ -836,8 +836,8 @@
                     serialization.Encoding.PEM,
                     serialization.PrivateFormat.TraditionalOpenSSL,
                     serialization.NoEncryption()))
-                self.assertRaisesRegex(
-                    requests.exceptions.SSLError, ".*revoked.*", requests.get,
+                self.assertRaises(
+                    requests.exceptions.SSLError, requests.get,
                     'https://{0}:{1}'.format(self.lb_vip_address,
                                              LISTENER1_TCP_PORT),
                     timeout=12, verify=False, cert=(cert_file.name,
@@ -954,15 +954,15 @@
                                 CONF.load_balancer.build_timeout)
 
         # Test that no client certificate fails to connect to listener1
-        self.assertRaisesRegex(
-            requests.exceptions.SSLError, ".*certificate required.*",
+        self.assertRaises(
+            requests.exceptions.SSLError,
             requests.get,
             'https://{0}:{1}'.format(self.lb_vip_address, LISTENER1_TCP_PORT),
             timeout=12, verify=False)
 
         # Test that no client certificate fails to connect to listener2
-        self.assertRaisesRegex(
-            requests.exceptions.SSLError, ".*certificate required.*",
+        self.assertRaises(
+            requests.exceptions.SSLError,
             requests.get,
             'https://{0}:{1}'.format(self.lb_vip_address, LISTENER2_TCP_PORT),
             timeout=12, verify=False)
@@ -976,8 +976,8 @@
                     serialization.Encoding.PEM,
                     serialization.PrivateFormat.TraditionalOpenSSL,
                     serialization.NoEncryption()))
-                self.assertRaisesRegex(
-                    requests.exceptions.SSLError, ".*revoked.*", requests.get,
+                self.assertRaises(
+                    requests.exceptions.SSLError, requests.get,
                     'https://{0}:{1}'.format(self.lb_vip_address,
                                              LISTENER1_TCP_PORT),
                     timeout=12, verify=False, cert=(cert_file.name,
@@ -992,8 +992,8 @@
                     serialization.Encoding.PEM,
                     serialization.PrivateFormat.TraditionalOpenSSL,
                     serialization.NoEncryption()))
-                self.assertRaisesRegex(
-                    requests.exceptions.SSLError, ".*revoked.*", requests.get,
+                self.assertRaises(
+                    requests.exceptions.SSLError, requests.get,
                     'https://{0}:{1}'.format(self.lb_vip_address,
                                              LISTENER2_TCP_PORT),
                     timeout=12, verify=False, cert=(cert_file.name,
@@ -1040,8 +1040,8 @@
                     serialization.Encoding.PEM,
                     serialization.PrivateFormat.TraditionalOpenSSL,
                     serialization.NoEncryption()))
-                self.assertRaisesRegex(
-                    requests.exceptions.SSLError, ".*decrypt error.*",
+                self.assertRaises(
+                    requests.exceptions.SSLError,
                     requests.get, 'https://{0}:{1}'.format(self.lb_vip_address,
                                                            LISTENER2_TCP_PORT),
                     timeout=12, verify=False, cert=(cert_file.name,
@@ -1056,8 +1056,8 @@
                     serialization.Encoding.PEM,
                     serialization.PrivateFormat.TraditionalOpenSSL,
                     serialization.NoEncryption()))
-                self.assertRaisesRegex(
-                    requests.exceptions.SSLError, ".*decrypt error.*",
+                self.assertRaises(
+                    requests.exceptions.SSLError,
                     requests.get, 'https://{0}:{1}'.format(self.lb_vip_address,
                                                            LISTENER1_TCP_PORT),
                     timeout=12, verify=False, cert=(cert_file.name,
@@ -1072,8 +1072,8 @@
                     serialization.Encoding.PEM,
                     serialization.PrivateFormat.TraditionalOpenSSL,
                     serialization.NoEncryption()))
-                self.assertRaisesRegex(
-                    requests.exceptions.SSLError, ".*decrypt error.*",
+                self.assertRaises(
+                    requests.exceptions.SSLError,
                     requests.get, 'https://{0}:{1}'.format(self.lb_vip_address,
                                                            LISTENER2_TCP_PORT),
                     timeout=12, verify=False, cert=(cert_file.name,
@@ -1088,8 +1088,8 @@
                     serialization.Encoding.PEM,
                     serialization.PrivateFormat.TraditionalOpenSSL,
                     serialization.NoEncryption()))
-                self.assertRaisesRegex(
-                    requests.exceptions.SSLError, ".*decrypt error.*",
+                self.assertRaises(
+                    requests.exceptions.SSLError,
                     requests.get, 'https://{0}:{1}'.format(self.lb_vip_address,
                                                            LISTENER1_TCP_PORT),
                     timeout=12, verify=False, cert=(cert_file.name,
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/releasenotes/source/conf.py b/releasenotes/source/conf.py
index a346f01..8437073 100644
--- a/releasenotes/source/conf.py
+++ b/releasenotes/source/conf.py
@@ -43,8 +43,9 @@
 ]
 
 # openstackdocstheme options
-repository_name = 'openstack/octavia-tempest-plugin'
-use_storyboard = True
+openstackdocs_repo_name = 'openstack/octavia-tempest-plugin'
+openstackdocs_auto_name = False
+openstackdocs_use_storyboard = True
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
@@ -102,7 +103,7 @@
 # show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = 'native'
 
 # A list of ignored prefixes for module index sorting.
 # modindex_common_prefix = []
diff --git a/requirements.txt b/requirements.txt
index 8020630..b30b450 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,6 @@
 
 cryptography>=2.1 # BSD/Apache-2.0
 python-dateutil>=2.5.3 # BSD
-ipaddress>=1.0.17;python_version<'3.3' # PSF
 pbr!=2.1.0,>=2.0.0 # Apache-2.0
 oslo.config>=5.2.0 # Apache-2.0
 oslo.log>=3.36.0  # Apache-2.0
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 60d9150..38b2d04 100644
--- a/zuul.d/jobs.yaml
+++ b/zuul.d/jobs.yaml
@@ -463,6 +463,9 @@
     vars:
       devstack_localrc:
         USE_PYTHON3: False
+    required-projects:
+      - name: openstack/diskimage-builder
+        override-checkout: 2.30.0
 
 - job:
     name: octavia-v2-dsvm-scenario-stable-train
@@ -473,6 +476,9 @@
     name: octavia-v2-dsvm-scenario-stable-stein
     parent: octavia-v2-dsvm-scenario
     override-checkout: stable/stein
+    required-projects:
+      - name: openstack/diskimage-builder
+        override-checkout: 2.30.0
 
 # Legacy jobs for the transition to the act-stdby two node jobs
 - job:
@@ -504,6 +510,9 @@
           USE_PYTHON3: False
           LIBVIRT_TYPE: kvm
           LIBVIRT_CPU_MODE: host-passthrough
+    required-projects:
+      - name: openstack/diskimage-builder
+        override-checkout: 2.30.0
 
 - job:
     name: octavia-v2-act-stdby-dsvm-scenario-two-node
@@ -589,6 +598,9 @@
     name: octavia-v2-dsvm-tls-barbican-stable-stein
     parent: octavia-v2-dsvm-tls-barbican
     override-checkout: stable/stein
+    required-projects:
+      - name: openstack/diskimage-builder
+        override-checkout: 2.30.0
 
 - job:
     name: octavia-v2-dsvm-tls-barbican-stable-rocky
@@ -621,6 +633,9 @@
     vars:
       devstack_localrc:
         USE_PYTHON3: False
+    required-projects:
+      - name: openstack/diskimage-builder
+        override-checkout: 2.30.0
 
 - job:
     name: octavia-v2-dsvm-spare-pool-stable-train
@@ -631,6 +646,9 @@
     name: octavia-v2-dsvm-spare-pool-stable-stein
     parent: octavia-v2-dsvm-spare-pool
     override-checkout: stable/stein
+    required-projects:
+      - name: openstack/diskimage-builder
+        override-checkout: 2.30.0
 
 - job:
     name: octavia-v2-dsvm-cinder-amphora
@@ -723,6 +741,9 @@
     vars:
       devstack_localrc:
         USE_PYTHON3: False
+    required-projects:
+      - name: openstack/diskimage-builder
+        override-checkout: 2.30.0
 
 - job:
     name: octavia-v2-act-stdby-iptables-dsvm-py2-scenario-centos-7
@@ -766,3 +787,6 @@
     name: octavia-v2-act-stdby-dsvm-scenario-stable-stein
     parent: octavia-v2-act-stdby-dsvm-scenario
     override-checkout: stable/stein
+    required-projects:
+      - name: openstack/diskimage-builder
+        override-checkout: 2.30.0