Tempest: add basic test

Change-Id: I7155e797fecf18b867eeb7c63ebbcb35d3cbb9c3
Co-Authored-By: dparalen <vetrisko@gmail.com>
Depends-On: Ibf0c73aa6795aaa52e945fd6baa821de20a599e7
Depends-On: I067504e49f68929298c91e61819aa9a61169fe52
This commit is contained in:
Anton Arefiev 2016-03-17 12:11:30 +02:00
parent d22378da76
commit 44f678c7a6
5 changed files with 409 additions and 0 deletions

View File

@ -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 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"

View File

@ -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"}
]
}
]

View File

@ -0,0 +1,70 @@
# 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
from tempest import clients
from tempest.common import credentials_factory as common_creds
from tempest import config
from tempest.services.baremetal import base
CONF = config.CONF
ADMIN_CREDS = common_creds.get_configured_admin_credentials()
class Manager(clients.Manager):
def __init__(self,
credentials=ADMIN_CREDS,
service=None,
api_microversions=None):
super(Manager, self).__init__(credentials, service)
self.introspection_client = BaremetalIntrospectionClient(
self.auth_provider,
CONF.baremetal_introspection.catalog_type,
CONF.identity.region,
endpoint_type=CONF.baremetal_introspection.endpoint_type,
**self.default_params_with_timeout_values)
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 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]
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))

View File

@ -0,0 +1,140 @@
# 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 os
import time
from tempest import config
from ironic_inspector.test.inspector_tempest_plugin import exceptions
from ironic_inspector.test.inspector_tempest_plugin.services import \
introspection_client
from ironic_tempest_plugin.tests.scenario.baremetal_manager import \
BaremetalScenarioTest
CONF = config.CONF
class InspectorScenarioTest(BaremetalScenarioTest):
"""Provide harness to do Inspector scenario tests."""
credentials = ['primary', 'admin']
@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()
self.flavor = self.baremetal_flavor()
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_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_filter(self, filter=lambda node: True, nodes=None):
return self.item_filter(self.node_list, self.node_show,
filter=filter, items=nodes)
def hypervisor_stats(self):
return (self.admin_manager.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):
self.introspection_client.import_rule(rule_path)
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 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)
# TODO(aarefiev): switch to call_until_true
def wait_for_introspection_finished(self, node_ids):
"""Waits for introspection of baremetal nodes to finish.
"""
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)

View File

@ -0,0 +1,149 @@
# 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 tempest
from tempest.config import CONF
from tempest import test # noqa
from ironic_inspector.test.inspector_tempest_plugin.tests import manager
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 tempest.lib.common.api_version_utils import LATEST_MICROVERSION
class InspectorBasicTest(manager.InspectorScenarioTest):
wait_provisioning_state_interval = 15
def node_cleanup(self, node_id):
if (self.node_show(node_id)['provision_state'] ==
BaremetalProvisionStates.AVAILABLE):
return
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):
# 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)
def setUp(self):
super(InspectorBasicTest, self).setUp()
# we rely on the 'available' provision_state; using latest
# microversion
self.useFixture(IronicMicroversionFixture(LATEST_MICROVERSION))
# avoid testing nodes that aren't available
self.node_ids = {node['uuid'] for node in
self.node_filter(filter=lambda node:
node['provision_state'] ==
BaremetalProvisionStates.AVAILABLE)}
if not self.node_ids:
self.skipTest('no available nodes detected')
self.rule_purge()
def verify_node_introspection_data(self, node):
self.assertEqual('yes', node['extra']['rule_success'])
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'])
@test.idempotent_id('03bf7990-bee0-4dd7-bf74-b97ad7b52a4b')
@test.services('baremetal', 'compute', 'image',
'network', 'object_storage')
def test_baremetal_introspection(self):
"""This smoke test case follows this basic 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.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, BaremetalProvisionStates.AVAILABLE,
timeout=CONF.baremetal.active_timeout,
interval=self.wait_provisioning_state_interval)
self.wait_for_nova_aware_of_bvms()
self.add_keypair()
self.boot_instance()
self.terminate_instance()