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
diff --git a/ironic_tempest_plugin/config.py b/ironic_tempest_plugin/config.py
index 7b09e90..00540cf 100644
--- a/ironic_tempest_plugin/config.py
+++ b/ironic_tempest_plugin/config.py
@@ -61,4 +61,13 @@
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/services/introspection_client.py b/ironic_tempest_plugin/services/introspection_client.py
index 3f43bf5..cce5213 100644
--- a/ironic_tempest_plugin/services/introspection_client.py
+++ b/ironic_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 @@
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 @@
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_tempest_plugin/tests/manager.py b/ironic_tempest_plugin/tests/manager.py
index 6d0f7d2..445a15e 100644
--- a/ironic_tempest_plugin/tests/manager.py
+++ b/ironic_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 @@
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 @@
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 @@
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 @@
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_tempest_plugin/tests/test_discovery.py b/ironic_tempest_plugin/tests/test_discovery.py
new file mode 100644
index 0000000..592fa81
--- /dev/null
+++ b/ironic_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)