Merge "Tempest: add basic test"
This commit is contained in:
commit
d2caf1b7fa
@ -10,4 +10,43 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from tempest import config # noqa
|
||||
|
||||
|
||||
baremetal_introspection_group = cfg.OptGroup(
|
||||
name="baremetal_introspection",
|
||||
title="Baremetal introspection service options",
|
||||
help="When enabling baremetal introspection tests,"
|
||||
"Ironic must be configured.")
|
||||
|
||||
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"),
|
||||
cfg.IntOpt('ironic_sync_timeout',
|
||||
default=60,
|
||||
help="Time it might take for Ironic--Inspector "
|
||||
"sync to happen"),
|
||||
]
|
||||
|
@ -10,17 +10,16 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tempest.scenario import manager
|
||||
from tempest import exceptions
|
||||
|
||||
|
||||
class InspectorScenarioTest(manager.BaremetalScenarioTest):
|
||||
"""Provide harness to do Inspector scenario tests."""
|
||||
class IntrospectionFailed(exceptions.TempestException):
|
||||
message = "Introspection failed"
|
||||
|
||||
credentials = ['primary', 'admin']
|
||||
|
||||
@classmethod
|
||||
def setup_clients(cls):
|
||||
super(InspectorScenarioTest, cls).setup_clients()
|
||||
class IntrospectionTimeout(exceptions.TempestException):
|
||||
message = "Introspection time out"
|
||||
|
||||
def setUp(self):
|
||||
super(InspectorScenarioTest, self).setUp()
|
||||
|
||||
class HypervisorUpdateTimeout(exceptions.TempestException):
|
||||
message = "Hypervisor stats update time out"
|
@ -13,10 +13,10 @@
|
||||
|
||||
import os
|
||||
|
||||
from tempest import config # noqa
|
||||
from tempest import config as tempest_config
|
||||
from tempest.test_discover import plugins
|
||||
|
||||
from ironic_inspector.test.inspector_tempest_plugin import config # noqa
|
||||
from ironic_inspector.test.inspector_tempest_plugin import config
|
||||
|
||||
|
||||
class InspectorTempestPlugin(plugins.TempestPlugin):
|
||||
@ -28,7 +28,10 @@ class InspectorTempestPlugin(plugins.TempestPlugin):
|
||||
return full_test_dir, base_path
|
||||
|
||||
def register_opts(self, conf):
|
||||
pass
|
||||
tempest_config.register_opt_group(
|
||||
conf, config.baremetal_introspection_group,
|
||||
config.BaremetalIntrospectionGroup)
|
||||
|
||||
def get_opt_lists(self):
|
||||
pass
|
||||
return [(config.baremetal_introspection_group.name,
|
||||
config.BaremetalIntrospectionGroup)]
|
||||
|
@ -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"}
|
||||
]
|
||||
}
|
||||
]
|
@ -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))
|
140
ironic_inspector/test/inspector_tempest_plugin/tests/manager.py
Normal file
140
ironic_inspector/test/inspector_tempest_plugin/tests/manager.py
Normal 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)
|
@ -1,27 +0,0 @@
|
||||
# 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 test # noqa
|
||||
|
||||
from ironic_inspector.test.inspector_tempest_plugin.tests.scenario \
|
||||
import manager
|
||||
|
||||
|
||||
class InspectorBasicTest(manager.InspectorScenarioTest):
|
||||
@test.idempotent_id('03bf7990-bee0-4dd7-bf74-b97ad7b52a4b')
|
||||
@test.services('baremetal', 'compute', 'image',
|
||||
'network', 'object_storage')
|
||||
def test_berametal_introspection_ops(self):
|
||||
"""This smoke test case follows this basic set of operations:
|
||||
|
||||
"""
|
||||
pass
|
@ -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()
|
Loading…
Reference in New Issue
Block a user