From 31906bfec79cbd8c01257c8eb396bf8339ba7192 Mon Sep 17 00:00:00 2001 From: Anton Arefiev Date: Tue, 13 Sep 2016 12:17:29 +0300 Subject: [PATCH] Tempest: add auto-discovery test Add test, which delete pre-created baremetal vms, and discovers it via 'enroll' not_found_hook with default configuration. Note, test contains workaround for working on infra, as infra 'tempest' user doesn't have access to virsh, for running node and whitelisting firewall rules on existing node, inspector's inspect api is used. Change-Id: Ib0ec63295a496229b27552cd1bcf7e763c0c3e03 --- devstack/plugin.sh | 7 + .../test/inspector_tempest_plugin/config.py | 9 ++ .../services/introspection_client.py | 23 +-- .../inspector_tempest_plugin/tests/manager.py | 47 +++++- .../tests/test_discovery.py | 147 ++++++++++++++++++ 5 files changed, 222 insertions(+), 11 deletions(-) create mode 100644 ironic_inspector/test/inspector_tempest_plugin/tests/test_discovery.py diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 0e5dcec..d1d2a08 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 7b09e90..00540cf 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 3f43bf5..cce5213 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 6d0f7d2..445a15e 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 0000000..592fa81 --- /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)