Merge "Testing bad microversions on v1/nodes/{uuid}/firmware"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..62b24c9
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,66 @@
+---
+default_language_version:
+  # force all unspecified python hooks to run python3
+  python: python3
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.5.0
+    hooks:
+      - id: trailing-whitespace
+        # NOTE(JayF): We shouldn't modify release notes after their
+        #  associated release. Instead, ignore these minor lint issues.
+      - id: mixed-line-ending
+        args: ['--fix', 'lf']
+        exclude: |
+          (?x)(
+          .*.svg$|
+          )
+      - id: fix-byte-order-marker
+      - id: check-merge-conflict
+      - id: debug-statements
+      - id: check-json
+        files: .*\.json$
+      - id: check-yaml
+        files: .*\.(yaml|yml)$
+        exclude: releasenotes/.*$
+  - repo: https://github.com/Lucas-C/pre-commit-hooks
+    rev: v1.5.4
+    hooks:
+      - id: remove-tabs
+        exclude: '.*\.(svg)$'
+  - repo: https://opendev.org/openstack/hacking
+    rev: 6.1.0
+    hooks:
+      - id: hacking
+        additional_dependencies: []
+        exclude: '^(doc|releasenotes|tools)/.*$'
+  - repo: https://github.com/codespell-project/codespell
+    rev: v2.4.1
+    hooks:
+    - id: codespell
+      args: [--write-changes]
+  - repo: https://github.com/sphinx-contrib/sphinx-lint
+    rev: v1.0.0
+    hooks:
+      - id: sphinx-lint
+        args: [--enable=default-role]
+        files: ^doc/|releasenotes|api-ref
+  - repo: https://opendev.org/openstack/bashate
+    rev: 2.1.0
+    hooks:
+      - id: bashate
+        args: ["-iE006,E044", "-eE005,E042"]
+        name: bashate
+        description: This hook runs bashate for linting shell scripts
+        entry: bashate
+        language: python
+        types: [shell]
+  - repo: https://github.com/PyCQA/doc8
+    rev: v1.1.2
+    hooks:
+      - id: doc8
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.7.3
+    hooks:
+      - id: ruff
+        args: ['--fix', '--unsafe-fixes']
diff --git a/ironic_tempest_plugin/common/waiters.py b/ironic_tempest_plugin/common/waiters.py
index e538cd8..cd13fe7 100644
--- a/ironic_tempest_plugin/common/waiters.py
+++ b/ironic_tempest_plugin/common/waiters.py
@@ -180,7 +180,7 @@
         field_value = node[field]
         if raise_if_insufficent_access and '** Redacted' in field_value:
             msg = ('Unable to see contents of redacted field '
-                   'indicating insufficent access to execute this test.')
+                   'indicating insufficient access to execute this test.')
             raise lib_exc.InsufficientAPIAccess(msg)
         return value in field_value
 
diff --git a/ironic_tempest_plugin/exceptions.py b/ironic_tempest_plugin/exceptions.py
index 865ab08..a1a9873 100644
--- a/ironic_tempest_plugin/exceptions.py
+++ b/ironic_tempest_plugin/exceptions.py
@@ -30,5 +30,5 @@
 
 
 class InsufficientAPIAccess(exceptions.TempestException):
-    message = ("Insufficent Access to the API exists. Please use a user "
+    message = ("Insufficient Access to the API exists. Please use a user "
                "with an elevated level of access to execute this test.")
diff --git a/ironic_tempest_plugin/tests/api/base.py b/ironic_tempest_plugin/tests/api/base.py
index 11ce859..c07137b 100644
--- a/ironic_tempest_plugin/tests/api/base.py
+++ b/ironic_tempest_plugin/tests/api/base.py
@@ -523,8 +523,8 @@
 
 class BaseBaremetalRBACTest(BaseBaremetalTest):
 
-    # Unless otherwise superceeded by a version, RBAC tests generally start at
-    # version 1.70 as that is when System scope and the delineation occured.
+    # Unless otherwise superseded by a version, RBAC tests generally start at
+    # version 1.70 as that is when System scope and the delineation occurred.
     min_microversion = '1.70'
 
     @classmethod
diff --git a/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py b/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py
index cd16fe2..2af624a 100644
--- a/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py
+++ b/ironic_tempest_plugin/tests/api/rbac_defaults/test_nodes.py
@@ -675,7 +675,7 @@
     All tests here must always expect *multiple* nodes visible, since
     this is a global reader role.
 
-    https://opendev.org/openstack/ironic/src/branch/master/ironic/common/policy.py#L60  # noqa
+    https://opendev.org/openstack/ironic/src/branch/master/ironic/common/policy.py#L60
     """
 
     credentials = ['system_admin', 'system_reader']
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
index e9090cb..ad9e4c9 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
@@ -16,10 +16,12 @@
 
 import time
 
+from oslo_log import log as logging
 from tempest.common import waiters
 from tempest import config
 from tempest.lib.common import api_version_utils
 from tempest.lib.common.utils.linux import remote_client
+from tempest.lib.common.utils import test_utils
 from tempest.lib import exceptions as lib_exc
 
 from ironic_tempest_plugin.common import utils
@@ -27,6 +29,7 @@
 from ironic_tempest_plugin import manager
 
 CONF = config.CONF
+LOG = logging.getLogger(__name__)
 
 
 def retry_on_conflict(func):
@@ -307,3 +310,44 @@
                                        instance['id'], 'ACTIVE')
         # Verify server connection
         self.get_remote_client(server_ip, server=instance)
+
+    def wait_for_ssh(self, ip_address,
+                     username=None,
+                     private_key=None,
+                     server=None,
+                     timeout=60,
+                     delay=10):
+        def _wait_ssh():
+            try:
+                self.get_remote_client(ip_address, username, private_key,
+                                       server=server)
+            except Exception:
+                LOG.debug("Failed to get ssh client for %s", ip_address,
+                          exc_info=True)
+                return False
+            return True
+
+        res = test_utils.call_until_true(_wait_ssh, timeout, delay)
+        self.assertTrue(res, f"Failed to wait for ssh on {ip_address}")
+
+    def check_vm_connectivity(self,
+                              ip_address,
+                              username=None,
+                              private_key=None,
+                              should_connect=True,
+                              extra_msg="",
+                              server=None,
+                              mtu=None):
+        # NOTE(vsaienko): it may take some time to boot VM and initialize
+        # ssh by cloud init. Wait for SSH can pass authentication before
+        # checking connectivity.
+        if should_connect:
+            self.wait_for_ssh(ip_address=ip_address, username=username,
+                              private_key=private_key, server=server)
+        super().check_vm_connectivity(ip_address=ip_address,
+                                      username=username,
+                                      private_key=private_key,
+                                      should_connect=should_connect,
+                                      extra_msg=extra_msg,
+                                      server=server,
+                                      mtu=mtu)
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
index ddb550b..7fa8cb5 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
@@ -343,7 +343,7 @@
         :param image_ref: Reference to user image to boot node with.
         :param image_checksum: md5sum of image specified in image_ref.
                                Needed only when direct HTTP link is provided.
-        :param boot_option: The defaut boot option to utilize. If not
+        :param boot_option: The default boot option to utilize. If not
                             specified, the ironic deployment default shall
                             be utilized.
         :param config_drive_networking: If we should load configuration drive
@@ -583,7 +583,7 @@
     # If we don't require an explicit driver, then what drivers *can* we
     # operate with. In essence, this exists to prevent the test from failing
     # on 3rd party drivers, and vendor specific driers which do not support
-    # the sort of itnerfaces we may be trying to test by default.
+    # the sort of interfaces we may be trying to test by default.
     valid_driver_list = []
 
     # The bios interface to use by the HW type. The bios interface of the
@@ -719,7 +719,7 @@
         if (cls.use_available_driver
                 and not cls.driver
                 and cls.node['driver'] in cls.valid_driver_list):
-            # If we're attempting to re-use the existing driver, then
+            # If we're attempting to reuse the existing driver, then
             # lets save a value for update_node_driver to work with.
             cls.driver = cls.node['driver']
         cls.update_node_driver(cls.node['uuid'], cls.driver, **boot_kwargs)
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
index 5901d20..e16bfad 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_basic_ops.py
@@ -638,7 +638,7 @@
     use_available_driver = True
 
     # List of valid drivers which these tests *can* attempt to utilize.
-    # Generally these should be the most commom, stock, upstream drivers.
+    # Generally these should be the most common, stock, upstream drivers.
     valid_driver_list = ['ipmi', 'redfish']
 
     # Bypass secondary attribute presence check as these tests don't require
diff --git a/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py b/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
index 2a36c6a..dcfc023 100644
--- a/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
+++ b/ironic_tempest_plugin/tests/scenario/test_baremetal_basic_ops.py
@@ -237,20 +237,6 @@
             self.rescue_instance(self.instance, self.node, ip_address)
             self.unrescue_instance(self.instance, self.node, ip_address)
 
-        # Reboot node
-        self.reboot_node(self.instance)
-
-        # Ensure we have some sort of connectivity
-        # Attempt to ping, if all else fails fall back to an ssh connection
-        # which worked previously.
-        pinging = self.ping_ip_address(ip_address)
-        if not pinging:
-            self.get_remote_client(ip_address, server=self.instance)
-        else:
-            # If we're here, this is successful. If ssh fails above,
-            # the job will ultimately fail.
-            self.assertTrue(pinging)
-
         self.terminate_instance(self.instance)
 
     @decorators.idempotent_id('549173a5-38ec-42bb-b0e2-c8b9f4a08943')
diff --git a/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py b/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
index f30b5e0..44b8f98 100644
--- a/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
+++ b/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
@@ -61,9 +61,6 @@
             * Verifies all properties are inspected
             * Verifies introspection data
             * Sets node to available state
-            * Creates a keypair
-            * Boots an instance using the keypair
-            * Deletes the instance
 
         """
         # prepare introspection rule
@@ -103,11 +100,6 @@
                 timeout=CONF.baremetal.active_timeout,
                 interval=self.wait_provisioning_state_interval)
 
-        self.wait_for_nova_aware_of_bvms()
-        self.add_keypair()
-        ins, _node = self.boot_instance()
-        self.terminate_instance(ins)
-
     @decorators.idempotent_id('70ca3070-184b-4b7d-8892-e977d2bc2870')
     def test_introspection_abort(self):
         """This smoke test case follows this very basic set of operations:
diff --git a/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py b/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
index 6ebfcc6..9950453 100644
--- a/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
+++ b/ironic_tempest_plugin/tests/scenario/test_introspection_discovery.py
@@ -136,13 +136,13 @@
            * Generate discovery rule;
            * Start introspection via ironic-inspector API;
            * Delete the node from ironic;
-           * Wating for node discovery;
+           * Waiting for node discovery;
            * Verify introspected node.
         """
         # NOTE(aarefiev): workaround for infra, 'tempest' user doesn't
         # have virsh privileges, so lets power on the node via ironic
         # and then delete it. Because of node is blacklisted in inspector
-        # we can't just power on it, therefor start introspection is used
+        # we can't just power on it, therefore start introspection is used
         # to whitelist discovered node first.
         self.baremetal_client.set_node_provision_state(
             self.node_info['uuid'], 'manage')
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..4fa5ffd
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,18 @@
+[build-system]
+requires = ["pbr>=6.0.0", "setuptools>=64.0.0"]
+build-backend = "pbr.build"
+
+[tool.doc8]
+ignore = ["D001"]
+
+[tool.ruff]
+line-length = 79
+target-version = "py37"
+
+[tool.ruff.lint]
+select = [
+    "E",        # pycodestyle (error)
+    "F",        # pyflakes
+    "G",        # flake8-logging-format
+    "LOG",      # flake8-logging
+]
diff --git a/requirements.txt b/requirements.txt
index bfafa58..ff7b704 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-pbr>=2.0.0 # Apache-2.0
+pbr>=6.0.0 # Apache-2.0
 oslo.config>=5.2.0 # Apache-2.0
 oslo.log>=3.36.0 # Apache-2.0
 oslo.serialization>=2.18.0 # Apache-2.0
diff --git a/setup.py b/setup.py
index cd35c3c..b997e51 100644
--- a/setup.py
+++ b/setup.py
@@ -16,5 +16,5 @@
 import setuptools
 
 setuptools.setup(
-    setup_requires=['pbr>=2.0.0'],
+    setup_requires=['pbr>=6.0.0'],
     pbr=True)
diff --git a/tox.ini b/tox.ini
index 3a67abc..bf8061c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-minversion = 3.18.0
+minversion = 4.4.0
 envlist = pep8
 ignore_basepython_conflict=true
 
@@ -14,11 +14,9 @@
 commands = stestr run --slowest {posargs}
 
 [testenv:pep8]
-deps =
-    hacking~=6.0.0 # Apache-2.0
-    flake8-import-order>=0.17.1 # LGPLv3
-    pycodestyle>=2.0.0,<3.0.0 # MIT
-commands = flake8 {posargs}
+deps = pre-commit
+allowlist_externals = pre-commit
+commands = pre-commit run --all-files --show-diff-on-failure {posargs}
 
 [testenv:venv]
 commands = {posargs}