diff --git a/ironic_tempest_plugin/README.rst b/ironic_tempest_plugin/README.rst index a46e2067..6de7741c 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 661b89c2..aa416b84 100644 --- a/ironic_tempest_plugin/config.py +++ b/ironic_tempest_plugin/config.py @@ -18,11 +18,15 @@ from oslo_config import cfg 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 @@ baremetal_group = cfg.OptGroup(name='baremetal', '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 @@ BaremetalFeaturesGroup = [ 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 00000000..ac08d54a --- /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 9e9c175c..e20386b5 100644 --- a/ironic_tempest_plugin/plugin.py +++ b/ironic_tempest_plugin/plugin.py @@ -25,6 +25,8 @@ _opts = [ (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 @@ class IronicTempestPlugin(plugins.TempestPlugin): 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 00000000..f1cfb0b2 --- /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 00000000..3b1a75bd --- /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 00000000..e6eb3892 --- /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 00000000..a6087e3d --- /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 00000000..f222810d --- /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'])