diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 0e5dcecbd..d1d2a080e 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -327,6 +327,13 @@ elif [[ "$1" == "stack" && "$2" == "extra" ]]; then start_inspector_dhcp fi start_inspector +elif [[ "$1" == "stack" && "$2" == "test-config" ]]; then + if is_service_enabled tempest; then + echo_summary "Configuring Tempest for Ironic Inspector" + if [ -n "$IRONIC_INSPECTOR_NODE_NOT_FOUND_HOOK" ]; then + iniset $TEMPEST_CONFIG baremetal_introspection auto_discovery_feature True + fi + fi fi if [[ "$1" == "unstack" ]]; then diff --git a/ironic_inspector/test/inspector_tempest_plugin/config.py b/ironic_inspector/test/inspector_tempest_plugin/config.py index 7b09e9037..00540cf08 100644 --- a/ironic_inspector/test/inspector_tempest_plugin/config.py +++ b/ironic_inspector/test/inspector_tempest_plugin/config.py @@ -61,4 +61,13 @@ BaremetalIntrospectionGroup = [ 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_inspector/test/inspector_tempest_plugin/services/introspection_client.py b/ironic_inspector/test/inspector_tempest_plugin/services/introspection_client.py index 3f43bf543..cce521306 100644 --- a/ironic_inspector/test/inspector_tempest_plugin/services/introspection_client.py +++ b/ironic_inspector/test/inspector_tempest_plugin/services/introspection_client.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import json - from ironic_tempest_plugin.services.baremetal import base from tempest import clients from tempest.common import credentials_factory as common_creds @@ -47,13 +45,10 @@ class BaremetalIntrospectionClient(base.BaremetalClient): return self._delete_request('rules', uuid=None) @base.handle_errors - def import_rule(self, rule_path): - """Import introspection rules from a json file.""" - with open(rule_path, 'r') as fp: - rules = json.load(fp) - if not isinstance(rules, list): - rules = [rules] - + def create_rules(self, rules): + """Create introspection rules.""" + if not isinstance(rules, list): + rules = [rules] for rule in rules: self._create_request('rules', rule) @@ -68,3 +63,13 @@ class BaremetalIntrospectionClient(base.BaremetalClient): 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 diff --git a/ironic_inspector/test/inspector_tempest_plugin/tests/manager.py b/ironic_inspector/test/inspector_tempest_plugin/tests/manager.py index 6d0f7d2bb..445a15e0a 100644 --- a/ironic_inspector/test/inspector_tempest_plugin/tests/manager.py +++ b/ironic_inspector/test/inspector_tempest_plugin/tests/manager.py @@ -10,13 +10,16 @@ # License for the specific language governing permissions and limitations # under the License. - +import json import os +import six import time import tempest from tempest import config from tempest.lib.common.api_version_utils import LATEST_MICROVERSION +from tempest.lib import exceptions as lib_exc +from tempest import test from ironic_inspector.test.inspector_tempest_plugin import exceptions from ironic_inspector.test.inspector_tempest_plugin.services import \ @@ -69,16 +72,28 @@ class InspectorScenarioTest(BaremetalScenarioTest): 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.admin_manager.hypervisor_client. show_hypervisor_statistics()) @@ -90,7 +105,12 @@ class InspectorScenarioTest(BaremetalScenarioTest): self.introspection_client.purge_rules() def rule_import(self, rule_path): - self.introspection_client.import_rule(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] @@ -98,6 +118,9 @@ class InspectorScenarioTest(BaremetalScenarioTest): 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 baremetal_flavor(self): flavor_id = CONF.compute.flavor_ref flavor = self.flavors_client.show_flavor(flavor_id)['flavor'] @@ -118,11 +141,31 @@ class InspectorScenarioTest(BaremetalScenarioTest): 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.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} diff --git a/ironic_inspector/test/inspector_tempest_plugin/tests/test_discovery.py b/ironic_inspector/test/inspector_tempest_plugin/tests/test_discovery.py new file mode 100644 index 000000000..592fa8163 --- /dev/null +++ b/ironic_inspector/test/inspector_tempest_plugin/tests/test_discovery.py @@ -0,0 +1,147 @@ +# 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 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)) + + @test.idempotent_id('dd3abe5e-0d23-488d-bb4e-344cdeff7dcb') + @test.services('baremetal', 'compute') + def test_berametal_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) + self.verify_node_introspection_data(inspected_node) + self.verify_node_driver_info(self.node_info, inspected_node)