Merge "Validate scheduling fields in basic ops scenario"
diff --git a/doc/source/config/with-nova.rst b/doc/source/config/with-nova.rst
new file mode 100644
index 0000000..3537588
--- /dev/null
+++ b/doc/source/config/with-nova.rst
@@ -0,0 +1,76 @@
+Full cloud with the Compute service and flat networking
+=======================================================
+
+This section documents running tempest on a full OpenStack cloud with the
+Compute, Image and Networking services enabled. The Bare Metal Introspection
+service (ironic-inspector) is not enabled. Flat networking is used.
+
+Prerequisite
+------------
+
+* `Create a bare metal flavor`_ in the Compute service in advance
+  and record its ID (``<flavor uuid>`` below).
+
+* `Create an image`_ to use for instances and record its ID (``<image uuid>``).
+  It can be either a whole disk or a partition image.
+
+* Create and record the name or UUID of a flat network to use for bare metal
+  instances (``<network name>``).
+
+* Get the minimum and maximum API versions that you want to test against.
+  Check the `API version history`_ to find the appropriate versions for
+  your deployment.
+
+  .. note:: The minimum version can usually be set to ``1.1``.
+
+* Enroll_ at least one node and make it ``available``.
+
+.. _Create a bare metal flavor: https://docs.openstack.org/ironic/latest/install/configure-nova-flavors.html
+.. _Create an image: https://docs.openstack.org/ironic/latest/install/configure-glance-images.html
+.. _API version history: https://docs.openstack.org/ironic/latest/contributor/webapi-version-history.html
+.. _Enroll: https://docs.openstack.org/ironic/latest/install/enrollment.html
+
+Configuration
+-------------
+
+.. code-block:: ini
+
+    [service_available]
+    # Enable ironic tests.
+    ironic = True
+
+    # Disable ironic-inspector tests.
+    ironic-inspector = False
+
+    [baremetal]
+    # Minimum and maximum API versions to test against.
+    min_microversion = <min API version as X.Y>
+    max_microversion = <max API version as X.Y>
+
+    [compute]
+    # Configure the bare metal flavor so that the Compute services provisions
+    # bare metal instances during the tests.
+    flavor_ref = <flavor uuid>
+    flavor_ref_alt = <flavor uuid>
+
+    # Configure the image to use.
+    image_ref = <image uuid>
+    image_ref_alt = <image uuid>
+
+    # Configure the network to use.
+    fixed_network_name = <network name>
+
+    [compute-feature-enabled]
+    # Ironic does not support this feature.
+    disk_config = False
+
+    # Not supported with flat networking.
+    interface_attach = False
+
+    [auth]
+    # Not supported with flat networking.
+    create_isolated_networks = False
+
+    [network]
+    # Required for flat networking.
+    shared_physical_network = True
diff --git a/doc/source/usage.rst b/doc/source/usage.rst
index 8ab88eb..3e3c56e 100644
--- a/doc/source/usage.rst
+++ b/doc/source/usage.rst
@@ -19,7 +19,14 @@
     [service_enabled]
     ironic_inspector = True
 
-.. TODO(dtantsur): I'm pretty sure more configuration is required, fill it in
+See the following example configurations for more details:
+
+.. toctree::
+   :maxdepth: 1
+
+   config/with-nova
+
+.. TODO(dtantsur): cover standalone tests
 
 .. _Tempest configuration: https://docs.openstack.org/tempest/latest/configuration.html
 
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index f9fefc4..3e8bc52 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -80,6 +80,12 @@
                help="Timeout for unprovisioning an Ironic node. "
                     "Takes longer since Kilo as Ironic performs an extra "
                     "step in Node cleaning."),
+    cfg.IntOpt('rescue_timeout',
+               default=300,
+               help="Timeout for rescuing an Ironic node."),
+    cfg.IntOpt('unrescue_timeout',
+               default=300,
+               help="Timeout for unrescuing an Ironic node."),
     cfg.StrOpt('min_microversion',
                help="Lower version of the test target microversion range. "
                     "The format is 'X.Y', where 'X' and 'Y' are int values. "
@@ -116,6 +122,9 @@
     cfg.ListOpt('enabled_deploy_interfaces',
                 default=['iscsi', 'direct'],
                 help="List of Ironic enabled deploy interfaces."),
+    cfg.ListOpt('enabled_rescue_interfaces',
+                default=['no-rescue'],
+                help="List of Ironic enabled rescue interfaces."),
     cfg.IntOpt('adjusted_root_disk_size_gb',
                min=0,
                help="Ironic adjusted disk size to use in the standalone tests "
diff --git a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
index c9f52ae..11fdebf 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -411,6 +411,7 @@
                            'properties/memory_mb',
                            'driver',
                            'deploy_interface',
+                           'rescue_interface',
                            'instance_uuid',
                            'resource_class')
         if not patch:
@@ -483,7 +484,7 @@
 
     @base.handle_errors
     def set_node_provision_state(self, node_uuid, state, configdrive=None,
-                                 clean_steps=None):
+                                 clean_steps=None, rescue_password=None):
         """Set provision state of the specified node.
 
         :param node_uuid: The unique identifier of the node.
@@ -492,6 +493,7 @@
         :param configdrive: A gzipped, base64-encoded
             configuration drive string.
         :param clean_steps: A list with clean steps to execute.
+        :param rescue_password: user password used to rescue.
         """
         data = {'target': state}
         # NOTE (vsaienk0): Add both here if specified, do not check anything.
@@ -500,6 +502,8 @@
             data['configdrive'] = configdrive
         if clean_steps is not None:
             data['clean_steps'] = clean_steps
+        if rescue_password is not None:
+            data['rescue_password'] = rescue_password
         return self._put_request('nodes/%s/states/provision' % node_uuid,
                                  data)
 
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
index 763496a..c220619 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
@@ -69,6 +69,7 @@
     DELETED = 'deleted'
     ERROR = 'error'
     MANAGEABLE = 'manageable'
+    RESCUE = 'rescue'
 
 
 class BaremetalScenarioTest(manager.ScenarioTest):
@@ -154,9 +155,10 @@
     @classmethod
     @retry_on_conflict
     def set_node_provision_state(cls, node_id, state, configdrive=None,
-                                 clean_steps=None):
+                                 clean_steps=None, rescue_password=None):
         cls.baremetal_client.set_node_provision_state(
-            node_id, state, configdrive=configdrive, clean_steps=clean_steps)
+            node_id, state, configdrive=configdrive,
+            clean_steps=clean_steps, rescue_password=rescue_password)
 
     def verify_connectivity(self, ip=None):
         if ip:
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
index 0137db8..5356c5d 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
@@ -272,6 +272,27 @@
             timeout=CONF.baremetal.unprovision_timeout,
             interval=1)
 
+    @classmethod
+    def rescue_node(cls, node_id, rescue_password):
+        """Rescue the node."""
+        cls.set_node_provision_state(node_id, 'rescue',
+                                     rescue_password=rescue_password)
+        cls.wait_provisioning_state(
+            node_id,
+            bm.BaremetalProvisionStates.RESCUE,
+            timeout=CONF.baremetal.rescue_timeout,
+            interval=1)
+
+    @classmethod
+    def unrescue_node(cls, node_id):
+        """Unrescue the node."""
+        cls.set_node_provision_state(node_id, 'unrescue')
+        cls.wait_provisioning_state(
+            node_id,
+            bm.BaremetalProvisionStates.ACTIVE,
+            timeout=CONF.baremetal.unrescue_timeout,
+            interval=1)
+
 
 class BaremetalStandaloneScenarioTest(BaremetalStandaloneManager):
 
@@ -284,6 +305,9 @@
     # The deploy interface to use by the HW type
     deploy_interface = None
 
+    # The rescue interface to use by the HW type
+    rescue_interface = None
+
     # User image ref to boot node with.
     image_ref = None
 
@@ -318,6 +342,13 @@
                 "in the list of enabled deploy interfaces %(enabled)s" % {
                     'iface': cls.deploy_interface,
                     'enabled': CONF.baremetal.enabled_deploy_interfaces})
+        if (cls.rescue_interface and cls.rescue_interface not in
+                CONF.baremetal.enabled_rescue_interfaces):
+            raise cls.skipException(
+                "Rescue interface %(iface)s required by test is not "
+                "in the list of enabled rescue interfaces %(enabled)s" % {
+                    'iface': cls.rescue_interface,
+                    'enabled': CONF.baremetal.enabled_rescue_interfaces})
         if not cls.wholedisk_image and CONF.baremetal.use_provision_network:
             raise cls.skipException(
                 'Partitioned images are not supported with multitenancy.')
@@ -336,6 +367,8 @@
         boot_kwargs = {'image_checksum': image_checksum}
         if cls.deploy_interface:
             boot_kwargs['deploy_interface'] = cls.deploy_interface
+        if cls.rescue_interface:
+            boot_kwargs['rescue_interface'] = cls.rescue_interface
         cls.node = cls.boot_node(cls.driver, cls.image_ref, **boot_kwargs)
         cls.node_ip = cls.add_floatingip_to_node(cls.node['uuid'])
 
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 62a0605..de0a17c 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
@@ -119,7 +119,22 @@
                                              should_succeed=True))
 
 
-class BaremetalIpmiWholedisk(bsm.BaremetalStandaloneScenarioTest):
+class BaremetalIpmiIscsiWholedisk(bsm.BaremetalStandaloneScenarioTest):
+
+    api_microversion = '1.31'  # to set the deploy_interface
+    driver = 'ipmi'
+    deploy_interface = 'iscsi'
+    image_ref = CONF.baremetal.whole_disk_image_ref
+    wholedisk_image = True
+
+    @decorators.idempotent_id('f25b71df-2150-45d7-a780-7f5b07124808')
+    @utils.services('image', 'network')
+    def test_ip_access_to_server(self):
+        self.assertTrue(self.ping_ip_address(self.node_ip,
+                                             should_succeed=True))
+
+
+class BaremetalIpmiDirectWholedisk(bsm.BaremetalStandaloneScenarioTest):
 
     api_microversion = '1.31'  # to set the deploy_interface
     driver = 'ipmi'
@@ -134,7 +149,7 @@
                                              should_succeed=True))
 
 
-class BaremetalIpmiPartitioned(bsm.BaremetalStandaloneScenarioTest):
+class BaremetalIpmiIscsiPartitioned(bsm.BaremetalStandaloneScenarioTest):
 
     api_microversion = '1.31'  # to set the deploy_interface
     driver = 'ipmi'
@@ -149,6 +164,21 @@
                                              should_succeed=True))
 
 
+class BaremetalIpmiDirectPartitioned(bsm.BaremetalStandaloneScenarioTest):
+
+    api_microversion = '1.31'  # to set the deploy_interface
+    driver = 'ipmi'
+    deploy_interface = 'direct'
+    image_ref = CONF.baremetal.partition_image_ref
+    wholedisk_image = False
+
+    @decorators.idempotent_id('7b4b2dcd-2bbb-44f5-991f-0964300af6b7')
+    @utils.services('image', 'network')
+    def test_ip_access_to_server(self):
+        self.assertTrue(self.ping_ip_address(self.node_ip,
+                                             should_succeed=True))
+
+
 class BaremetalIpmiAnsibleWholedisk(bsm.BaremetalStandaloneScenarioTest):
 
     api_microversion = '1.31'  # to set the deploy_interface
@@ -162,3 +192,51 @@
     def test_ip_access_to_server(self):
         self.assertTrue(self.ping_ip_address(self.node_ip,
                                              should_succeed=True))
+
+
+class BaremetalIpmiRescueWholedisk(bsm.BaremetalStandaloneScenarioTest):
+
+    api_microversion = '1.38'
+    min_microversion = '1.38'
+    driver = 'ipmi'
+    rescue_interface = 'agent'
+    image_ref = CONF.baremetal.whole_disk_image_ref
+    wholedisk_image = True
+
+    # NOTE(tiendc) Using direct deploy interface and a whole disk
+    # image may lead to the bug:
+    # https://bugs.launchpad.net/ironic/+bug/1750958
+    # This is a workaround by using iscsi deploy interface.
+    deploy_interface = 'iscsi'
+
+    @decorators.idempotent_id('d6a1780f-c4bb-4136-8144-29e822e14d66')
+    @utils.services('image', 'network')
+    def test_rescue_mode(self):
+        self.rescue_node(self.node['uuid'], 'abc123')
+        self.assertTrue(self.ping_ip_address(self.node_ip,
+                                             should_succeed=True))
+
+        self.unrescue_node(self.node['uuid'])
+        self.assertTrue(self.ping_ip_address(self.node_ip,
+                                             should_succeed=True))
+
+
+class BaremetalIpmiRescuePartitioned(bsm.BaremetalStandaloneScenarioTest):
+
+    api_microversion = '1.38'
+    min_microversion = '1.38'
+    driver = 'ipmi'
+    rescue_interface = 'agent'
+    image_ref = CONF.baremetal.partition_image_ref
+    wholedisk_image = False
+
+    @decorators.idempotent_id('113acd0a-9872-4631-b3ee-54da7e3bb262')
+    @utils.services('image', 'network')
+    def test_rescue_mode(self):
+        self.rescue_node(self.node['uuid'], 'abc123')
+        self.assertTrue(self.ping_ip_address(self.node_ip,
+                                             should_succeed=True))
+
+        self.unrescue_node(self.node['uuid'])
+        self.assertTrue(self.ping_ip_address(self.node_ip,
+                                             should_succeed=True))
diff --git a/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py b/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
index 8ae92e9..f97316d 100644
--- a/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
+++ b/ironic_tempest_plugin/tests/scenario/test_introspection_basic.py
@@ -57,6 +57,7 @@
             interval=self.wait_provisioning_state_interval)
 
     @decorators.idempotent_id('03bf7990-bee0-4dd7-bf74-b97ad7b52a4b')
+    @decorators.attr(type='smoke')
     @utils.services('compute', 'image', 'network')
     def test_baremetal_introspection(self):
         """This smoke test case follows this set of operations:
@@ -144,33 +145,3 @@
         # verify nodes status and provision state
         for node_id in self.node_ids:
             self.verify_introspection_aborted(node_id)
-
-
-class InspectorSmokeTest(introspection_manager.InspectorScenarioTest):
-
-    @decorators.idempotent_id('a702d1f1-88e4-42ce-88ef-cba2d9e3312e')
-    @decorators.attr(type='smoke')
-    @utils.services('object_storage')
-    def test_baremetal_introspection(self):
-        """This smoke test case follows this very basic set of operations:
-
-            * Fetches expected properties from baremetal flavor
-            * Removes all properties from one node
-            * Sets the node to manageable state
-            * Inspects the node
-            * Sets the node to available state
-
-        """
-        # NOTE(dtantsur): we can't silently skip this test because it runs in
-        # grenade with several other tests, and we won't have any indication
-        # that it was not run.
-        assert self.node_ids, "No available nodes"
-        node_id = next(iter(self.node_ids))
-        self.introspect_node(node_id)
-
-        # settle down introspection
-        self.wait_for_introspection_finished([node_id])
-        self.wait_provisioning_state(
-            node_id, 'manageable',
-            timeout=CONF.baremetal_introspection.ironic_sync_timeout,
-            interval=self.wait_provisioning_state_interval)
diff --git a/test-requirements.txt b/test-requirements.txt
index fbdfb19..1b68194 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -4,7 +4,7 @@
 
 hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
 
-sphinx!=1.6.6,>=1.6.2 # BSD
+sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
 oslosphinx>=4.7.0 # Apache-2.0
 
 # releasenotes
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 7b84ec7..a1f312c 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -12,9 +12,6 @@
         - ironic-tempest-dsvm-ironic-inspector-queens
         - ironic-tempest-dsvm-ironic-inspector-pike
         - ironic-tempest-dsvm-ironic-inspector-ocata
-        - ironic-inspector-grenade-dsvm
-        - ironic-inspector-grenade-dsvm-queens
-        - ironic-inspector-grenade-dsvm-pike
     gate:
       jobs:
         - ironic-dsvm-standalone
@@ -28,6 +25,3 @@
         - ironic-tempest-dsvm-ironic-inspector-queens
         - ironic-tempest-dsvm-ironic-inspector-pike
         - ironic-tempest-dsvm-ironic-inspector-ocata
-        - ironic-inspector-grenade-dsvm
-        - ironic-inspector-grenade-dsvm-queens
-        - ironic-inspector-grenade-dsvm-pike
diff --git a/zuul.d/stable-jobs.yaml b/zuul.d/stable-jobs.yaml
index 9f3d1db..7a9150a 100644
--- a/zuul.d/stable-jobs.yaml
+++ b/zuul.d/stable-jobs.yaml
@@ -38,13 +38,3 @@
     name: ironic-tempest-dsvm-ironic-inspector-ocata
     parent: ironic-tempest-dsvm-ironic-inspector
     override-checkout: stable/ocata
-
-- job:
-    name: ironic-inspector-grenade-dsvm-queens
-    parent: ironic-inspector-grenade-dsvm
-    override-checkout: stable/queens
-
-- job:
-    name: ironic-inspector-grenade-dsvm-pike
-    parent: ironic-inspector-grenade-dsvm
-    override-checkout: stable/pike