Merge remote-tracking branch 'inspector/master'
diff --git a/ironic_tempest_plugin/README.rst b/ironic_tempest_plugin/README.rst
index a46e206..6de7741 100644
--- a/ironic_tempest_plugin/README.rst
+++ b/ironic_tempest_plugin/README.rst
@@ -2,8 +2,8 @@
Ironic tempest plugin
=====================
-This directory contains Tempest tests to cover the Ironic project,
-as well as a plugin to automatically load these tests into tempest.
+This directory contains Tempest tests to cover the ironic and ironic-inspector
+projects, as well as a plugin to automatically load these tests into tempest.
See the tempest plugin documentation for information about creating
a plugin, stable API interface, TempestPlugin class interface, plugin
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index 661b89c..aa416b8 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -18,11 +18,15 @@
from tempest import config # noqa
-service_option = cfg.BoolOpt('ironic',
- default=False,
- help='Whether or not Ironic is expected to be '
- 'available')
+ironic_service_option = cfg.BoolOpt('ironic',
+ default=False,
+ help='Whether or not ironic is expected '
+ 'to be available')
+inspector_service_option = cfg.BoolOpt("ironic-inspector",
+ default=True,
+ help="Whether or not ironic-inspector "
+ "is expected to be available")
baremetal_group = cfg.OptGroup(name='baremetal',
title='Baremetal provisioning service options',
@@ -34,6 +38,12 @@
'live_migration, pause, rescue, resize, '
'shelve, snapshot, and suspend')
+baremetal_introspection_group = cfg.OptGroup(
+ name="baremetal_introspection",
+ title="Baremetal introspection service options",
+ help="When enabling baremetal introspection tests,"
+ "Ironic must be configured.")
+
baremetal_features_group = cfg.OptGroup(
name='baremetal_feature_enabled',
title="Enabled Baremetal Service Features")
@@ -114,3 +124,45 @@
default=True,
help="Defines if IPXE is enabled"),
]
+
+BaremetalIntrospectionGroup = [
+ cfg.StrOpt('catalog_type',
+ default='baremetal-introspection',
+ help="Catalog type of the baremetal provisioning service"),
+ cfg.StrOpt('endpoint_type',
+ default='publicURL',
+ choices=['public', 'admin', 'internal',
+ 'publicURL', 'adminURL', 'internalURL'],
+ help="The endpoint type to use for the baremetal introspection"
+ " service"),
+ cfg.IntOpt('introspection_sleep',
+ default=30,
+ help="Introspection sleep before check status"),
+ cfg.IntOpt('introspection_timeout',
+ default=600,
+ help="Introspection time out"),
+ cfg.IntOpt('hypervisor_update_sleep',
+ default=60,
+ help="Time to wait until nova becomes aware of "
+ "bare metal instances"),
+ cfg.IntOpt('hypervisor_update_timeout',
+ default=300,
+ help="Time out for wait until nova becomes aware of "
+ "bare metal instances"),
+ # NOTE(aarefiev): status_check_period default is 60s, but checking
+ # node state takes some time(API call), so races appear here,
+ # 80s would be enough to make one more check.
+ cfg.IntOpt('ironic_sync_timeout',
+ default=80,
+ help="Time it might take for Ironic--Inspector "
+ "sync to happen"),
+ cfg.IntOpt('discovery_timeout',
+ default=300,
+ help="Time to wait until new node would enrolled in "
+ "ironic"),
+ cfg.BoolOpt('auto_discovery_feature',
+ default=False,
+ help="Is the auto-discovery feature enabled. Enroll hook "
+ "should be specified in node_not_found_hook - processing "
+ "section of inspector.conf"),
+]
diff --git a/ironic_tempest_plugin/exceptions.py b/ironic_tempest_plugin/exceptions.py
new file mode 100644
index 0000000..ac08d54
--- /dev/null
+++ b/ironic_tempest_plugin/exceptions.py
@@ -0,0 +1,25 @@
+# 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.
+
+from tempest.lib import exceptions
+
+
+class IntrospectionFailed(exceptions.TempestException):
+ message = "Introspection failed"
+
+
+class IntrospectionTimeout(exceptions.TempestException):
+ message = "Introspection time out"
+
+
+class HypervisorUpdateTimeout(exceptions.TempestException):
+ message = "Hypervisor stats update time out"
diff --git a/ironic_tempest_plugin/plugin.py b/ironic_tempest_plugin/plugin.py
index 9e9c175..e20386b 100644
--- a/ironic_tempest_plugin/plugin.py
+++ b/ironic_tempest_plugin/plugin.py
@@ -25,6 +25,8 @@
(project_config.baremetal_group, project_config.BaremetalGroup),
(project_config.baremetal_features_group,
project_config.BaremetalFeaturesGroup)
+ (project_config.baremetal_introspection_group,
+ project_config.BaremetalIntrospectionGroup),
]
@@ -37,7 +39,9 @@
return full_test_dir, base_path
def register_opts(self, conf):
- conf.register_opt(project_config.service_option,
+ conf.register_opt(project_config.ironic_service_option,
+ group='service_available')
+ conf.register_opt(project_config.inspector_service_option,
group='service_available')
for group, option in _opts:
config.register_opt_group(conf, group, option)
diff --git a/ironic_tempest_plugin/rules/basic_ops_rule.json b/ironic_tempest_plugin/rules/basic_ops_rule.json
new file mode 100644
index 0000000..f1cfb0b
--- /dev/null
+++ b/ironic_tempest_plugin/rules/basic_ops_rule.json
@@ -0,0 +1,25 @@
+[
+ {
+ "description": "Successful Rule",
+ "conditions": [
+ {"op": "ge", "field": "memory_mb", "value": 256},
+ {"op": "ge", "field": "local_gb", "value": 1}
+ ],
+ "actions": [
+ {"action": "set-attribute", "path": "/extra/rule_success",
+ "value": "yes"}
+ ]
+ },
+ {
+ "description": "Failing Rule",
+ "conditions": [
+ {"op": "lt", "field": "memory_mb", "value": 42},
+ {"op": "eq", "field": "local_gb", "value": 0}
+ ],
+ "actions": [
+ {"action": "set-attribute", "path": "/extra/rule_success",
+ "value": "no"},
+ {"action": "fail", "message": "This rule should not have run"}
+ ]
+ }
+]
diff --git a/ironic_tempest_plugin/services/introspection_client.py b/ironic_tempest_plugin/services/introspection_client.py
new file mode 100644
index 0000000..3b1a75b
--- /dev/null
+++ b/ironic_tempest_plugin/services/introspection_client.py
@@ -0,0 +1,83 @@
+# 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.
+
+from ironic_tempest_plugin.services.baremetal import base
+from tempest import clients
+from tempest.common import credentials_factory as common_creds
+from tempest import config
+
+
+CONF = config.CONF
+ADMIN_CREDS = common_creds.get_configured_admin_credentials()
+
+
+class Manager(clients.Manager):
+ def __init__(self,
+ credentials=ADMIN_CREDS,
+ api_microversions=None):
+ super(Manager, self).__init__(credentials)
+ self.introspection_client = BaremetalIntrospectionClient(
+ self.auth_provider,
+ CONF.baremetal_introspection.catalog_type,
+ CONF.identity.region,
+ endpoint_type=CONF.baremetal_introspection.endpoint_type)
+
+
+class BaremetalIntrospectionClient(base.BaremetalClient):
+ """Base Tempest REST client for Ironic Inspector API v1."""
+ version = '1'
+ uri_prefix = 'v1'
+
+ @base.handle_errors
+ def purge_rules(self):
+ """Purge all existing rules."""
+ return self._delete_request('rules', uuid=None)
+
+ @base.handle_errors
+ def create_rules(self, rules):
+ """Create introspection rules."""
+ if not isinstance(rules, list):
+ rules = [rules]
+ for rule in rules:
+ self._create_request('rules', rule)
+
+ @base.handle_errors
+ def get_status(self, uuid):
+ """Get introspection status for a node."""
+ return self._show_request('introspection', uuid=uuid)
+
+ @base.handle_errors
+ def get_data(self, uuid):
+ """Get introspection data for a node."""
+ return self._show_request('introspection', uuid=uuid,
+ uri='/%s/introspection/%s/data' %
+ (self.uri_prefix, uuid))
+
+ @base.handle_errors
+ def start_introspection(self, uuid):
+ """Start introspection for a node."""
+ resp, _body = self.post(url=('/%s/introspection/%s' %
+ (self.uri_prefix, uuid)),
+ body=None)
+ self.expected_success(202, resp.status)
+
+ return resp
+
+ @base.handle_errors
+ def abort_introspection(self, uuid):
+ """Abort introspection for a node."""
+ resp, _body = self.post(url=('/%s/introspection/%s/abort' %
+ (self.uri_prefix, uuid)),
+ body=None)
+ self.expected_success(202, resp.status)
+
+ return resp
diff --git a/ironic_tempest_plugin/tests/manager.py b/ironic_tempest_plugin/tests/manager.py
new file mode 100644
index 0000000..e6eb389
--- /dev/null
+++ b/ironic_tempest_plugin/tests/manager.py
@@ -0,0 +1,243 @@
+# 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.
+
+import json
+import os
+import time
+
+from ironic_tempest_plugin.tests.api.admin.api_microversion_fixture import \
+ APIMicroversionFixture as IronicMicroversionFixture
+from ironic_tempest_plugin.tests.scenario.baremetal_manager import \
+ BaremetalProvisionStates
+from ironic_tempest_plugin.tests.scenario.baremetal_manager import \
+ BaremetalScenarioTest
+import six
+import tempest
+from tempest import config
+from tempest.lib.common.api_version_utils import LATEST_MICROVERSION
+from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions as lib_exc
+
+from ironic_inspector.test.inspector_tempest_plugin import exceptions
+from ironic_inspector.test.inspector_tempest_plugin.services import \
+ introspection_client
+
+CONF = config.CONF
+
+
+class InspectorScenarioTest(BaremetalScenarioTest):
+ """Provide harness to do Inspector scenario tests."""
+
+ wait_provisioning_state_interval = 15
+
+ credentials = ['primary', 'admin']
+
+ ironic_api_version = LATEST_MICROVERSION
+
+ @classmethod
+ def setup_clients(cls):
+ super(InspectorScenarioTest, cls).setup_clients()
+ inspector_manager = introspection_client.Manager()
+ cls.introspection_client = inspector_manager.introspection_client
+
+ def setUp(self):
+ super(InspectorScenarioTest, self).setUp()
+ # we rely on the 'available' provision_state; using latest
+ # microversion
+ self.useFixture(IronicMicroversionFixture(self.ironic_api_version))
+ self.flavor = self.baremetal_flavor()
+ self.node_ids = {node['uuid'] for node in
+ self.node_filter(filter=lambda node:
+ node['provision_state'] ==
+ BaremetalProvisionStates.AVAILABLE)}
+ self.rule_purge()
+
+ def item_filter(self, list_method, show_method,
+ filter=lambda item: True, items=None):
+ if items is None:
+ items = [show_method(item['uuid']) for item in
+ list_method()]
+ return [item for item in items if filter(item)]
+
+ def node_list(self):
+ return self.baremetal_client.list_nodes()[1]['nodes']
+
+ def node_port_list(self, node_uuid):
+ return self.baremetal_client.list_node_ports(node_uuid)[1]['ports']
+
+ def node_update(self, uuid, patch):
+ return self.baremetal_client.update_node(uuid, **patch)
+
+ def node_show(self, uuid):
+ return self.baremetal_client.show_node(uuid)[1]
+
+ def node_delete(self, uuid):
+ return self.baremetal_client.delete_node(uuid)
+
+ def node_filter(self, filter=lambda node: True, nodes=None):
+ return self.item_filter(self.node_list, self.node_show,
+ filter=filter, items=nodes)
+
+ def node_set_power_state(self, uuid, state):
+ self.baremetal_client.set_node_power_state(uuid, state)
+
+ def node_set_provision_state(self, uuid, state):
+ self.baremetal_client.set_node_provision_state(self, uuid, state)
+
+ def hypervisor_stats(self):
+ return (self.os_admin.hypervisor_client.
+ show_hypervisor_statistics())
+
+ def server_show(self, uuid):
+ self.servers_client.show_server(uuid)
+
+ def rule_purge(self):
+ self.introspection_client.purge_rules()
+
+ def rule_import(self, rule_path):
+ with open(rule_path, 'r') as fp:
+ rules = json.load(fp)
+ self.introspection_client.create_rules(rules)
+
+ def rule_import_from_dict(self, rules):
+ self.introspection_client.create_rules(rules)
+
+ def introspection_status(self, uuid):
+ return self.introspection_client.get_status(uuid)[1]
+
+ def introspection_data(self, uuid):
+ return self.introspection_client.get_data(uuid)[1]
+
+ def introspection_start(self, uuid):
+ return self.introspection_client.start_introspection(uuid)
+
+ def introspection_abort(self, uuid):
+ return self.introspection_client.abort_introspection(uuid)
+
+ def baremetal_flavor(self):
+ flavor_id = CONF.compute.flavor_ref
+ flavor = self.flavors_client.show_flavor(flavor_id)['flavor']
+ flavor['properties'] = self.flavors_client.list_flavor_extra_specs(
+ flavor_id)['extra_specs']
+ return flavor
+
+ def get_rule_path(self, rule_file):
+ base_path = os.path.split(
+ os.path.dirname(os.path.abspath(__file__)))[0]
+ base_path = os.path.split(base_path)[0]
+ return os.path.join(base_path, "inspector_tempest_plugin",
+ "rules", rule_file)
+
+ def boot_instance(self):
+ return super(InspectorScenarioTest, self).boot_instance()
+
+ def terminate_instance(self, instance):
+ return super(InspectorScenarioTest, self).terminate_instance(instance)
+
+ def wait_for_node(self, node_name):
+ def check_node():
+ try:
+ self.node_show(node_name)
+ except lib_exc.NotFound:
+ return False
+ return True
+
+ if not test_utils.call_until_true(
+ check_node,
+ duration=CONF.baremetal_introspection.discovery_timeout,
+ sleep_for=20):
+ msg = ("Timed out waiting for node %s " % node_name)
+ raise lib_exc.TimeoutException(msg)
+
+ inspected_node = self.node_show(self.node_info['name'])
+ self.wait_for_introspection_finished(inspected_node['uuid'])
+
+ # TODO(aarefiev): switch to call_until_true
+ def wait_for_introspection_finished(self, node_ids):
+ """Waits for introspection of baremetal nodes to finish.
+
+ """
+ if isinstance(node_ids, six.text_type):
+ node_ids = [node_ids]
+ start = int(time.time())
+ not_introspected = {node_id for node_id in node_ids}
+
+ while not_introspected:
+ time.sleep(CONF.baremetal_introspection.introspection_sleep)
+ for node_id in node_ids:
+ status = self.introspection_status(node_id)
+ if status['finished']:
+ if status['error']:
+ message = ('Node %(node_id)s introspection failed '
+ 'with %(error)s.' %
+ {'node_id': node_id,
+ 'error': status['error']})
+ raise exceptions.IntrospectionFailed(message)
+ not_introspected = not_introspected - {node_id}
+
+ if (int(time.time()) - start >=
+ CONF.baremetal_introspection.introspection_timeout):
+ message = ('Introspection timed out for nodes: %s' %
+ not_introspected)
+ raise exceptions.IntrospectionTimeout(message)
+
+ def wait_for_nova_aware_of_bvms(self):
+ start = int(time.time())
+ while True:
+ time.sleep(CONF.baremetal_introspection.hypervisor_update_sleep)
+ stats = self.hypervisor_stats()
+ expected_cpus = self.baremetal_flavor()['vcpus']
+ if int(stats['hypervisor_statistics']['vcpus']) >= expected_cpus:
+ break
+
+ timeout = CONF.baremetal_introspection.hypervisor_update_timeout
+ if (int(time.time()) - start >= timeout):
+ message = (
+ 'Timeout while waiting for nova hypervisor-stats: '
+ '%(stats)s required time (%(timeout)s s).' %
+ {'stats': stats,
+ 'timeout': timeout})
+ raise exceptions.HypervisorUpdateTimeout(message)
+
+ def node_cleanup(self, node_id):
+ if (self.node_show(node_id)['provision_state'] ==
+ BaremetalProvisionStates.AVAILABLE):
+ return
+ # in case when introspection failed we need set provision state
+ # to 'manage' to make it possible transit into 'provide' state
+ if self.node_show(node_id)['provision_state'] == 'inspect failed':
+ self.baremetal_client.set_node_provision_state(node_id, 'manage')
+
+ try:
+ self.baremetal_client.set_node_provision_state(node_id, 'provide')
+ except tempest.lib.exceptions.RestClientException:
+ # maybe node already cleaning or available
+ pass
+
+ self.wait_provisioning_state(
+ node_id, [BaremetalProvisionStates.AVAILABLE,
+ BaremetalProvisionStates.NOSTATE],
+ timeout=CONF.baremetal.unprovision_timeout,
+ interval=self.wait_provisioning_state_interval)
+
+ def introspect_node(self, node_id, remove_props=True):
+ if remove_props:
+ # in case there are properties remove those
+ patch = {('properties/%s' % key): None for key in
+ self.node_show(node_id)['properties']}
+ # reset any previous rule result
+ patch['extra/rule_success'] = None
+ self.node_update(node_id, patch)
+
+ self.baremetal_client.set_node_provision_state(node_id, 'manage')
+ self.baremetal_client.set_node_provision_state(node_id, 'inspect')
+ self.addCleanup(self.node_cleanup, node_id)
diff --git a/ironic_tempest_plugin/tests/test_basic.py b/ironic_tempest_plugin/tests/test_basic.py
new file mode 100644
index 0000000..a6087e3
--- /dev/null
+++ b/ironic_tempest_plugin/tests/test_basic.py
@@ -0,0 +1,176 @@
+# 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.
+
+from ironic_tempest_plugin.tests.scenario import baremetal_manager
+from tempest.common import utils
+from tempest.config import CONF
+from tempest.lib import decorators
+
+from ironic_inspector.test.inspector_tempest_plugin.tests import manager
+
+
+class InspectorBasicTest(manager.InspectorScenarioTest):
+
+ def verify_node_introspection_data(self, node):
+ data = self.introspection_data(node['uuid'])
+ self.assertEqual(data['cpu_arch'],
+ self.flavor['properties']['cpu_arch'])
+ self.assertEqual(int(data['memory_mb']),
+ int(self.flavor['ram']))
+ self.assertEqual(int(data['cpus']), int(self.flavor['vcpus']))
+
+ def verify_node_flavor(self, node):
+ expected_cpus = self.flavor['vcpus']
+ expected_memory_mb = self.flavor['ram']
+ expected_cpu_arch = self.flavor['properties']['cpu_arch']
+ disk_size = self.flavor['disk']
+ ephemeral_size = self.flavor['OS-FLV-EXT-DATA:ephemeral']
+ expected_local_gb = disk_size + ephemeral_size
+
+ self.assertEqual(expected_cpus,
+ int(node['properties']['cpus']))
+ self.assertEqual(expected_memory_mb,
+ int(node['properties']['memory_mb']))
+ self.assertEqual(expected_local_gb,
+ int(node['properties']['local_gb']))
+ self.assertEqual(expected_cpu_arch,
+ node['properties']['cpu_arch'])
+
+ def verify_introspection_aborted(self, uuid):
+ status = self.introspection_status(uuid)
+
+ self.assertEqual('Canceled by operator', status['error'])
+ self.assertTrue(status['finished'])
+
+ self.wait_provisioning_state(
+ uuid, 'inspect failed',
+ timeout=CONF.baremetal.active_timeout,
+ interval=self.wait_provisioning_state_interval)
+
+ @decorators.idempotent_id('03bf7990-bee0-4dd7-bf74-b97ad7b52a4b')
+ @utils.services('compute', 'image', 'network')
+ def test_baremetal_introspection(self):
+ """This smoke test case follows this set of operations:
+
+ * Fetches expected properties from baremetal flavor
+ * Removes all properties from nodes
+ * Sets nodes to manageable state
+ * Imports introspection rule basic_ops_rule.json
+ * Inspects nodes
+ * 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
+ rule_path = self.get_rule_path("basic_ops_rule.json")
+ self.rule_import(rule_path)
+ self.addCleanup(self.rule_purge)
+
+ for node_id in self.node_ids:
+ self.introspect_node(node_id)
+
+ # settle down introspection
+ self.wait_for_introspection_finished(self.node_ids)
+ for node_id in self.node_ids:
+ self.wait_provisioning_state(
+ node_id, 'manageable',
+ timeout=CONF.baremetal_introspection.ironic_sync_timeout,
+ interval=self.wait_provisioning_state_interval)
+
+ for node_id in self.node_ids:
+ node = self.node_show(node_id)
+ self.assertEqual('yes', node['extra']['rule_success'])
+ if CONF.service_available.swift:
+ self.verify_node_introspection_data(node)
+ self.verify_node_flavor(node)
+
+ for node_id in self.node_ids:
+ self.baremetal_client.set_node_provision_state(node_id, 'provide')
+
+ for node_id in self.node_ids:
+ self.wait_provisioning_state(
+ node_id, baremetal_manager.BaremetalProvisionStates.AVAILABLE,
+ 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:
+
+ * Start nodes introspection
+ * Wait until nodes power on
+ * Abort introspection
+ * Verifies nodes status and power state
+
+ """
+ # start nodes introspection
+ for node_id in self.node_ids:
+ self.introspect_node(node_id, remove_props=False)
+
+ # wait for nodes power on
+ for node_id in self.node_ids:
+ self.wait_power_state(
+ node_id,
+ baremetal_manager.BaremetalPowerStates.POWER_ON)
+
+ # abort introspection
+ for node_id in self.node_ids:
+ self.introspection_abort(node_id)
+
+ # wait for nodes power off
+ for node_id in self.node_ids:
+ self.wait_power_state(
+ node_id,
+ baremetal_manager.BaremetalPowerStates.POWER_OFF)
+
+ # verify nodes status and provision state
+ for node_id in self.node_ids:
+ self.verify_introspection_aborted(node_id)
+
+
+class InspectorSmokeTest(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/ironic_tempest_plugin/tests/test_discovery.py b/ironic_tempest_plugin/tests/test_discovery.py
new file mode 100644
index 0000000..f222810
--- /dev/null
+++ b/ironic_tempest_plugin/tests/test_discovery.py
@@ -0,0 +1,150 @@
+# 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.
+
+import six
+
+from ironic_tempest_plugin.tests.scenario import baremetal_manager
+from tempest import config
+from tempest.lib import decorators
+from tempest import test # noqa
+
+from ironic_inspector.test.inspector_tempest_plugin.tests import manager
+
+CONF = config.CONF
+
+ProvisionStates = baremetal_manager.BaremetalProvisionStates
+
+
+class InspectorDiscoveryTest(manager.InspectorScenarioTest):
+ @classmethod
+ def skip_checks(cls):
+ super(InspectorDiscoveryTest, cls).skip_checks()
+ if not CONF.baremetal_introspection.auto_discovery_feature:
+ msg = ("Please, provide a value for node_not_found_hook in "
+ "processing section of inspector.conf for enable "
+ "auto-discovery feature.")
+ raise cls.skipException(msg)
+
+ def setUp(self):
+ super(InspectorDiscoveryTest, self).setUp()
+
+ discovered_node = self._get_discovery_node()
+ self.node_info = self._get_node_info(discovered_node)
+
+ rule = self._generate_discovery_rule(self.node_info)
+
+ self.rule_import_from_dict(rule)
+ self.addCleanup(self.rule_purge)
+
+ def _get_node_info(self, node_uuid):
+ node = self.node_show(node_uuid)
+ ports = self.node_port_list(node_uuid)
+ node['port_macs'] = [port['address'] for port in ports]
+ return node
+
+ def _get_discovery_node(self):
+ nodes = self.node_list()
+
+ discovered_node = None
+ for node in nodes:
+ if (node['provision_state'] == ProvisionStates.AVAILABLE or
+ node['provision_state'] == ProvisionStates.ENROLL or
+ node['provision_state'] is ProvisionStates.NOSTATE):
+ discovered_node = node['uuid']
+ break
+
+ self.assertIsNotNone(discovered_node)
+ return discovered_node
+
+ def _generate_discovery_rule(self, node):
+ rule = dict()
+ rule["description"] = "Node %s discovery rule" % node['name']
+ rule["actions"] = [
+ {"action": "set-attribute", "path": "/name",
+ "value": "%s" % node['name']},
+ {"action": "set-attribute", "path": "/driver",
+ "value": "%s" % node['driver']},
+ ]
+
+ for key, value in node['driver_info'].items():
+ rule["actions"].append(
+ {"action": "set-attribute", "path": "/driver_info/%s" % key,
+ "value": "%s" % value})
+ rule["conditions"] = [
+ {"op": "eq", "field": "data://auto_discovered", "value": True}
+ ]
+ return rule
+
+ def verify_node_introspection_data(self, node):
+ data = self.introspection_data(node['uuid'])
+ self.assertEqual(data['cpu_arch'],
+ self.flavor['properties']['cpu_arch'])
+ self.assertEqual(int(data['memory_mb']),
+ int(self.flavor['ram']))
+ self.assertEqual(int(data['cpus']), int(self.flavor['vcpus']))
+
+ def verify_node_flavor(self, node):
+ expected_cpus = self.flavor['vcpus']
+ expected_memory_mb = self.flavor['ram']
+ expected_cpu_arch = self.flavor['properties']['cpu_arch']
+ disk_size = self.flavor['disk']
+ ephemeral_size = self.flavor['OS-FLV-EXT-DATA:ephemeral']
+ expected_local_gb = disk_size + ephemeral_size
+
+ self.assertEqual(expected_cpus,
+ int(node['properties']['cpus']))
+ self.assertEqual(expected_memory_mb,
+ int(node['properties']['memory_mb']))
+ self.assertEqual(expected_local_gb,
+ int(node['properties']['local_gb']))
+ self.assertEqual(expected_cpu_arch,
+ node['properties']['cpu_arch'])
+
+ def verify_node_driver_info(self, node_info, inspected_node):
+ for key in node_info['driver_info']:
+ self.assertEqual(six.text_type(node_info['driver_info'][key]),
+ inspected_node['driver_info'].get(key))
+
+ @decorators.idempotent_id('dd3abe5e-0d23-488d-bb4e-344cdeff7dcb')
+ def test_bearmetal_auto_discovery(self):
+ """This test case follows this set of operations:
+
+ * Choose appropriate node, based on provision state;
+ * Get node info;
+ * Generate discovery rule;
+ * Delete discovered node from ironic;
+ * Start baremetal vm via virsh;
+ * Wating for node introspection;
+ * 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
+ # to whitelist discovered node first.
+ self.baremetal_client.set_node_provision_state(
+ self.node_info['uuid'], 'manage')
+ self.introspection_start(self.node_info['uuid'])
+ self.wait_power_state(
+ self.node_info['uuid'],
+ baremetal_manager.BaremetalPowerStates.POWER_ON)
+ self.node_delete(self.node_info['uuid'])
+
+ self.wait_for_node(self.node_info['name'])
+
+ inspected_node = self.node_show(self.node_info['name'])
+ self.verify_node_flavor(inspected_node)
+ if CONF.service_available.swift:
+ self.verify_node_introspection_data(inspected_node)
+ self.verify_node_driver_info(self.node_info, inspected_node)
+ self.assertEqual(ProvisionStates.ENROLL,
+ inspected_node['provision_state'])