Merge "Enable boot_option to be specified"
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 114d4b0..87b00c8 100755
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -40,7 +40,6 @@
 master_doc = 'index'
 
 # General information about the project.
-project = u'ironic-tempest-plugin'
 copyright = u'2016, OpenStack Foundation'
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
@@ -62,15 +61,15 @@
 html_theme = 'openstackdocs'
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = '%sdoc' % project
+htmlhelp_basename = 'ironic-tempest-plugindoc'
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass
 # [howto/manual]).
 latex_documents = [
     ('index',
-     '%s.tex' % project,
-     u'%s Documentation' % project,
+     'doc-ironic-tempest-plugin.tex',
+     u'Ironic Tempest Plugin Documentation',
      u'OpenStack Foundation', 'manual'),
 ]
 
diff --git a/ironic_tempest_plugin/common/waiters.py b/ironic_tempest_plugin/common/waiters.py
index 08e0941..554e5f8 100644
--- a/ironic_tempest_plugin/common/waiters.py
+++ b/ironic_tempest_plugin/common/waiters.py
@@ -45,7 +45,7 @@
 
 
 def wait_for_bm_node_status(client, node_id, attr, status, timeout=None,
-                            interval=None):
+                            interval=None, abort_on_error_state=False):
     """Waits for a baremetal node attribute to reach given status.
 
     :param client: an instance of tempest plugin BaremetalClient.
@@ -56,6 +56,8 @@
         Defaults to client.build_timeout.
     :param interval: an interval between show_node calls for status check.
         Defaults to client.build_interval.
+    :param abort_on_error_state: whether to abort waiting if the node reaches
+        an error state.
 
     The client should have a show_node(node_id) method to get the node.
     """
@@ -69,6 +71,14 @@
         node = utils.get_node(client, node_id=node_id)
         if node[attr] in status:
             return True
+        elif (abort_on_error_state
+              and node['provision_state'].endswith(' failed')):
+            raise lib_exc.TempestException(
+                'Node %(node)s reached failure state %(state)s while waiting '
+                'for %(attr)s=%(expected)s. Error: %(error)s' %
+                {'node': node_id, 'state': node['provision_state'],
+                 'attr': attr, 'expected': status,
+                 'error': node.get('last_error')})
         return False
 
     if not test_utils.call_until_true(is_attr_in_status, timeout,
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index d232790..5fc9333 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -185,6 +185,9 @@
     cfg.IntOpt('introspection_timeout',
                default=600,
                help="Introspection time out"),
+    cfg.IntOpt('introspection_start_timeout',
+               default=90,
+               help="Timeout to start introspection"),
     cfg.IntOpt('hypervisor_update_sleep',
                default=60,
                help="Time to wait until nova becomes aware of "
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
index 6d7d55b..1788463 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_manager.py
@@ -104,10 +104,12 @@
         cls.baremetal_client.list_nodes()
 
     @classmethod
-    def wait_provisioning_state(cls, node_id, state, timeout=10, interval=1):
+    def wait_provisioning_state(cls, node_id, state, timeout=10, interval=1,
+                                abort_on_error_state=True):
         ironic_waiters.wait_for_bm_node_status(
             cls.baremetal_client, node_id=node_id, attr='provision_state',
-            status=state, timeout=timeout, interval=interval)
+            status=state, timeout=timeout, interval=interval,
+            abort_on_error_state=abort_on_error_state)
 
     @classmethod
     def wait_power_state(cls, node_id, state):
diff --git a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
index 4cf6a81..eac82f3 100644
--- a/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/baremetal_standalone_manager.py
@@ -206,6 +206,8 @@
 
         def _try_to_associate_instance():
             n = node or cls.get_random_available_node()
+            if n is None:
+                return False
             try:
                 cls._associate_instance_with_node(n['uuid'], instance_uuid)
                 nodes.append(n)
diff --git a/ironic_tempest_plugin/tests/scenario/introspection_manager.py b/ironic_tempest_plugin/tests/scenario/introspection_manager.py
index c45f285..0131089 100644
--- a/ironic_tempest_plugin/tests/scenario/introspection_manager.py
+++ b/ironic_tempest_plugin/tests/scenario/introspection_manager.py
@@ -177,11 +177,24 @@
             node_ids = [node_ids]
         start = int(time.time())
         not_introspected = {node_id for node_id in node_ids}
+        introspection_start_timeout = (
+            CONF.baremetal_introspection.introspection_start_timeout)
 
         while not_introspected:
             time.sleep(CONF.baremetal_introspection.introspection_sleep)
             for node_id in node_ids:
-                status = self.introspection_status(node_id)
+                try:
+                    status = self.introspection_status(node_id)
+                except lib_exc.NotFound as exc:
+                    if int(time.time()) - start >= introspection_start_timeout:
+                        message = ('Node %(node_id)s did not appear in the '
+                                   'baremetal introspection API after '
+                                   '%(timeout)d seconds: %(error)s' %
+                                   {'node_id': node_id, 'error': exc,
+                                    'timeout': introspection_start_timeout})
+                        raise exceptions.IntrospectionFailed(message)
+                    else:
+                        continue
                 if status['finished']:
                     if status['error']:
                         message = ('Node %(node_id)s introspection failed '
diff --git a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_cleaning.py b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_cleaning.py
index 7df98ce..eb0d4d2 100644
--- a/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_cleaning.py
+++ b/ironic_tempest_plugin/tests/scenario/ironic_standalone/test_cleaning.py
@@ -65,6 +65,12 @@
     deploy_interface = 'iscsi'
     api_microversion = '1.31'
 
+    @classmethod
+    def skip_checks(cls):
+        super(BaremetalCleaningIpmiWholedisk, cls).skip_checks()
+        if CONF.baremetal_feature_enabled.software_raid:
+            raise cls.skipException("Cleaning is covered in the RAID test")
+
     @decorators.idempotent_id('065238db-1b6d-4d75-a9da-c240f8cbd956')
     @utils.services('image', 'network')
     def test_manual_cleaning(self):
diff --git a/test-requirements.txt b/test-requirements.txt
index b5667d2..fb97fc6 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -6,6 +6,6 @@
 
 sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD
 sphinx!=1.6.6,!=1.6.7,>=1.6.2;python_version>='3.4' # BSD
-openstackdocstheme>=1.18.1 # Apache-2.0
+openstackdocstheme>=1.20.0 # Apache-2.0
 
 reno>=2.5.0 # Apache-2.0
diff --git a/tox.ini b/tox.ini
index 0cc29c7..048d70b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -29,6 +29,12 @@
 basepython = python3
 commands = python setup.py build_sphinx
 
+[testenv:pdf-docs]
+basepython = python3
+whitelist_externals = make
+commands = sphinx-build -b latex doc/source doc/build/pdf
+           make -C doc/build/pdf
+
 [testenv:debug]
 basepython = python3
 commands = oslo_debug_helper {posargs}
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index bcedbc3..ffb57a5 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -7,22 +7,30 @@
       jobs:
         # NOTE(dtantsur): keep N-3 and older non-voting for these jobs.
         - ironic-standalone
+        - ironic-standalone-train
         - ironic-standalone-stein
-        - ironic-dsvm-standalone-rocky
+        - ironic-dsvm-standalone-rocky:
+            voting: false
         - ironic-dsvm-standalone-queens:
             voting: false
         - ironic-tempest-functional-python3
+        - ironic-tempest-functional-python3-train
         - ironic-tempest-functional-python3-stein
-        - ironic-tempest-dsvm-functional-python3-rocky
+        - ironic-tempest-dsvm-functional-python3-rocky:
+            voting: false
         - ironic-inspector-tempest
+        - ironic-inspector-tempest-train
         - ironic-inspector-tempest-stein
-        - ironic-tempest-dsvm-ironic-inspector-rocky
+        - ironic-tempest-dsvm-ironic-inspector-rocky:
+            voting: false
         - ironic-tempest-dsvm-ironic-inspector-queens:
             voting: false
         # NOTE(dtantsur): these jobs cover rarely changed tests and are quite
-        # unstable, so keep them non-voting on stable branches.
-        # NOTE(iurygregory): debug rocky and queens since they used to pass
-        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
+        # unstable, so keep them non-voting.
+        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode:
+            voting: false
+        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-train:
+            voting: false
         - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-stein:
             voting: false
         - ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode-rocky:
@@ -30,6 +38,8 @@
         - ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode-queens:
             voting: false
         - ironic-inspector-tempest-discovery
+        - ironic-inspector-tempest-discovery-train:
+            voting: false
         - ironic-inspector-tempest-discovery-stein:
             voting: false
         - ironic-inspector-tempest-dsvm-discovery-rocky:
@@ -40,13 +50,12 @@
       queue: ironic
       jobs:
         - ironic-standalone
+        - ironic-standalone-train
         - ironic-standalone-stein
-        - ironic-dsvm-standalone-rocky
         - ironic-tempest-functional-python3
+        - ironic-tempest-functional-python3-train
         - ironic-tempest-functional-python3-stein
-        - ironic-tempest-dsvm-functional-python3-rocky
         - ironic-inspector-tempest
+        - ironic-inspector-tempest-train
         - ironic-inspector-tempest-stein
-        - ironic-tempest-dsvm-ironic-inspector-rocky
-        - ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
         - ironic-inspector-tempest-discovery
diff --git a/zuul.d/stable-jobs.yaml b/zuul.d/stable-jobs.yaml
index f553787..bfb6706 100644
--- a/zuul.d/stable-jobs.yaml
+++ b/zuul.d/stable-jobs.yaml
@@ -1,13 +1,27 @@
 - job:
+    name: ironic-standalone-train
+    parent: ironic-standalone
+    override-branch: stable/train
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: False
+
+- job:
     name: ironic-standalone-stein
     parent: ironic-standalone
     override-branch: stable/stein
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: False
 
 - job:
     name: ironic-dsvm-standalone-rocky
     parent: ironic-standalone
     override-branch: stable/rocky
     nodeset: openstack-single-node-xenial
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: False
 
 - job:
     name: ironic-dsvm-standalone-queens
@@ -19,6 +33,12 @@
         FIXED_NETWORK_SIZE: 4096
         EBTABLES_RACE_FIX: True
         IRONIC_USE_MOD_WSGI: True
+        USE_PYTHON3: False
+
+- job:
+    name: ironic-tempest-functional-python3-train
+    parent: ironic-tempest-functional-python3
+    override-branch: stable/train
 
 - job:
     name: ironic-tempest-functional-python3-stein
@@ -37,9 +57,20 @@
         rabbit: True
 
 - job:
+    name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-train
+    parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
+    override-branch: stable/train
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: False
+
+- job:
     name: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode-stein
     parent: ironic-tempest-ipa-wholedisk-direct-tinyipa-multinode
     override-branch: stable/stein
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: False
 
 - job:
     name: ironic-tempest-dsvm-ipa-wholedisk-agent_ipmitool-tinyipa-multinode-rocky
@@ -54,6 +85,7 @@
         EBTABLES_RACE_FIX: True
         PUBLIC_BRIDGE: br_ironic_vxlan
         OVS_BRIDGE_MAPPINGS: 'mynetwork:brbm,public:br_ironic_vxlan'
+        USE_PYTHON3: False
     group-vars:
       subnode:
         devstack_localrc:
@@ -72,12 +104,23 @@
         EBTABLES_RACE_FIX: True
         PUBLIC_BRIDGE: br_ironic_vxlan
         OVS_BRIDGE_MAPPINGS: 'mynetwork:brbm,public:br_ironic_vxlan'
+        USE_PYTHON3: False
     group-vars:
       subnode:
         devstack_localrc:
           OVS_BRIDGE_MAPPINGS: 'mynetwork:sub1brbm,public:br_ironic_vxlan'
 
 - job:
+    name: ironic-inspector-tempest-train
+    parent: ironic-inspector-tempest
+    override-branch: stable/train
+    vars:
+      devstack_localrc:
+        FIXED_NETWORK_SIZE: 4096
+        EBTABLES_RACE_FIX: True
+        USE_PYTHON3: False
+
+- job:
     name: ironic-inspector-tempest-stein
     parent: ironic-inspector-tempest
     override-branch: stable/stein
@@ -85,6 +128,7 @@
       devstack_localrc:
         FIXED_NETWORK_SIZE: 4096
         EBTABLES_RACE_FIX: True
+        USE_PYTHON3: False
 
 - job:
     name: ironic-tempest-dsvm-ironic-inspector-rocky
@@ -95,6 +139,7 @@
       devstack_localrc:
         FIXED_NETWORK_SIZE: 4096
         EBTABLES_RACE_FIX: True
+        USE_PYTHON3: False
 
 - job:
     name: ironic-tempest-dsvm-ironic-inspector-queens
@@ -105,20 +150,38 @@
       devstack_localrc:
         FIXED_NETWORK_SIZE: 4096
         EBTABLES_RACE_FIX: True
+        USE_PYTHON3: False
+
+- job:
+    name: ironic-inspector-tempest-discovery-train
+    parent: ironic-inspector-tempest-discovery
+    override-branch: stable/train
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: False
 
 - job:
     name: ironic-inspector-tempest-discovery-stein
     parent: ironic-inspector-tempest-discovery
     override-branch: stable/stein
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: False
 
 - job:
     name: ironic-inspector-tempest-dsvm-discovery-rocky
     parent: ironic-inspector-tempest-discovery
     override-branch: stable/rocky
     nodeset: openstack-single-node-xenial
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: False
 
 - job:
     name: ironic-inspector-tempest-dsvm-discovery-queens
     parent: ironic-inspector-tempest-discovery
     override-branch: stable/queens
     nodeset: openstack-single-node-xenial
+    vars:
+      devstack_localrc:
+        USE_PYTHON3: False