Merge remote-tracking branch 'inspector/master'
This commit is contained in:
commit
b609cea4be
@ -2,8 +2,8 @@
|
||||
Ironic tempest plugin
|
||||
=====================
|
||||
|
||||
This directory contains Tempest tests to cover the Ironic project,
|
||||
as well as a plugin to automatically load these tests into tempest.
|
||||
This directory contains Tempest tests to cover the ironic and ironic-inspector
|
||||
projects, as well as a plugin to automatically load these tests into tempest.
|
||||
|
||||
See the tempest plugin documentation for information about creating
|
||||
a plugin, stable API interface, TempestPlugin class interface, plugin
|
||||
|
@ -18,11 +18,15 @@ from oslo_config import cfg
|
||||
from tempest import config # noqa
|
||||
|
||||
|
||||
service_option = cfg.BoolOpt('ironic',
|
||||
default=False,
|
||||
help='Whether or not Ironic is expected to be '
|
||||
'available')
|
||||
ironic_service_option = cfg.BoolOpt('ironic',
|
||||
default=False,
|
||||
help='Whether or not ironic is expected '
|
||||
'to be available')
|
||||
|
||||
inspector_service_option = cfg.BoolOpt("ironic-inspector",
|
||||
default=True,
|
||||
help="Whether or not ironic-inspector "
|
||||
"is expected to be available")
|
||||
|
||||
baremetal_group = cfg.OptGroup(name='baremetal',
|
||||
title='Baremetal provisioning service options',
|
||||
@ -34,6 +38,12 @@ baremetal_group = cfg.OptGroup(name='baremetal',
|
||||
'live_migration, pause, rescue, resize, '
|
||||
'shelve, snapshot, and suspend')
|
||||
|
||||
baremetal_introspection_group = cfg.OptGroup(
|
||||
name="baremetal_introspection",
|
||||
title="Baremetal introspection service options",
|
||||
help="When enabling baremetal introspection tests,"
|
||||
"Ironic must be configured.")
|
||||
|
||||
baremetal_features_group = cfg.OptGroup(
|
||||
name='baremetal_feature_enabled',
|
||||
title="Enabled Baremetal Service Features")
|
||||
@ -114,3 +124,45 @@ BaremetalFeaturesGroup = [
|
||||
default=True,
|
||||
help="Defines if IPXE is enabled"),
|
||||
]
|
||||
|
||||
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"),
|
||||
# NOTE(aarefiev): status_check_period default is 60s, but checking
|
||||
# node state takes some time(API call), so races appear here,
|
||||
# 80s would be enough to make one more check.
|
||||
cfg.IntOpt('ironic_sync_timeout',
|
||||
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"),
|
||||
]
|
||||
|
25
ironic_tempest_plugin/exceptions.py
Normal file
25
ironic_tempest_plugin/exceptions.py
Normal 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.lib 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"
|
@ -25,6 +25,8 @@ _opts = [
|
||||
(project_config.baremetal_group, project_config.BaremetalGroup),
|
||||
(project_config.baremetal_features_group,
|
||||
project_config.BaremetalFeaturesGroup)
|
||||
(project_config.baremetal_introspection_group,
|
||||
project_config.BaremetalIntrospectionGroup),
|
||||
]
|
||||
|
||||
|
||||
@ -37,7 +39,9 @@ class IronicTempestPlugin(plugins.TempestPlugin):
|
||||
return full_test_dir, base_path
|
||||
|
||||
def register_opts(self, conf):
|
||||
conf.register_opt(project_config.service_option,
|
||||
conf.register_opt(project_config.ironic_service_option,
|
||||
group='service_available')
|
||||
conf.register_opt(project_config.inspector_service_option,
|
||||
group='service_available')
|
||||
for group, option in _opts:
|
||||
config.register_opt_group(conf, group, option)
|
||||
|
25
ironic_tempest_plugin/rules/basic_ops_rule.json
Normal file
25
ironic_tempest_plugin/rules/basic_ops_rule.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
]
|
83
ironic_tempest_plugin/services/introspection_client.py
Normal file
83
ironic_tempest_plugin/services/introspection_client.py
Normal file
@ -0,0 +1,83 @@
|
||||
# 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 ironic_tempest_plugin.services.baremetal import base
|
||||
from tempest import clients
|
||||
from tempest.common import credentials_factory as common_creds
|
||||
from tempest import config
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
ADMIN_CREDS = common_creds.get_configured_admin_credentials()
|
||||
|
||||
|
||||
class Manager(clients.Manager):
|
||||
def __init__(self,
|
||||
credentials=ADMIN_CREDS,
|
||||
api_microversions=None):
|
||||
super(Manager, self).__init__(credentials)
|
||||
self.introspection_client = BaremetalIntrospectionClient(
|
||||
self.auth_provider,
|
||||
CONF.baremetal_introspection.catalog_type,
|
||||
CONF.identity.region,
|
||||
endpoint_type=CONF.baremetal_introspection.endpoint_type)
|
||||
|
||||
|
||||
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 create_rules(self, rules):
|
||||
"""Create introspection rules."""
|
||||
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))
|
||||
|
||||
@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
|
||||
|
||||
@base.handle_errors
|
||||
def abort_introspection(self, uuid):
|
||||
"""Abort introspection for a node."""
|
||||
resp, _body = self.post(url=('/%s/introspection/%s/abort' %
|
||||
(self.uri_prefix, uuid)),
|
||||
body=None)
|
||||
self.expected_success(202, resp.status)
|
||||
|
||||
return resp
|
243
ironic_tempest_plugin/tests/manager.py
Normal file
243
ironic_tempest_plugin/tests/manager.py
Normal file
@ -0,0 +1,243 @@
|
||||
# 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
|
||||
import os
|
||||
import time
|
||||
|
||||
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 ironic_tempest_plugin.tests.scenario.baremetal_manager import \
|
||||
BaremetalScenarioTest
|
||||
import six
|
||||
import tempest
|
||||
from tempest import config
|
||||
from tempest.lib.common.api_version_utils import LATEST_MICROVERSION
|
||||
from tempest.lib.common.utils import test_utils
|
||||
from tempest.lib import exceptions as lib_exc
|
||||
|
||||
from ironic_inspector.test.inspector_tempest_plugin import exceptions
|
||||
from ironic_inspector.test.inspector_tempest_plugin.services import \
|
||||
introspection_client
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class InspectorScenarioTest(BaremetalScenarioTest):
|
||||
"""Provide harness to do Inspector scenario tests."""
|
||||
|
||||
wait_provisioning_state_interval = 15
|
||||
|
||||
credentials = ['primary', 'admin']
|
||||
|
||||
ironic_api_version = LATEST_MICROVERSION
|
||||
|
||||
@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()
|
||||
# we rely on the 'available' provision_state; using latest
|
||||
# microversion
|
||||
self.useFixture(IronicMicroversionFixture(self.ironic_api_version))
|
||||
self.flavor = self.baremetal_flavor()
|
||||
self.node_ids = {node['uuid'] for node in
|
||||
self.node_filter(filter=lambda node:
|
||||
node['provision_state'] ==
|
||||
BaremetalProvisionStates.AVAILABLE)}
|
||||
self.rule_purge()
|
||||
|
||||
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_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.os_admin.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):
|
||||
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]
|
||||
|
||||
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 introspection_abort(self, uuid):
|
||||
return self.introspection_client.abort_introspection(uuid)
|
||||
|
||||
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)
|
||||
|
||||
def boot_instance(self):
|
||||
return super(InspectorScenarioTest, self).boot_instance()
|
||||
|
||||
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_utils.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}
|
||||
|
||||
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)
|
||||
|
||||
def node_cleanup(self, node_id):
|
||||
if (self.node_show(node_id)['provision_state'] ==
|
||||
BaremetalProvisionStates.AVAILABLE):
|
||||
return
|
||||
# in case when introspection failed we need set provision state
|
||||
# to 'manage' to make it possible transit into 'provide' state
|
||||
if self.node_show(node_id)['provision_state'] == 'inspect failed':
|
||||
self.baremetal_client.set_node_provision_state(node_id, 'manage')
|
||||
|
||||
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, remove_props=True):
|
||||
if remove_props:
|
||||
# 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)
|
176
ironic_tempest_plugin/tests/test_basic.py
Normal file
176
ironic_tempest_plugin/tests/test_basic.py
Normal file
@ -0,0 +1,176 @@
|
||||
# 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 ironic_tempest_plugin.tests.scenario import baremetal_manager
|
||||
from tempest.common import utils
|
||||
from tempest.config import CONF
|
||||
from tempest.lib import decorators
|
||||
|
||||
from ironic_inspector.test.inspector_tempest_plugin.tests import manager
|
||||
|
||||
|
||||
class InspectorBasicTest(manager.InspectorScenarioTest):
|
||||
|
||||
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_introspection_aborted(self, uuid):
|
||||
status = self.introspection_status(uuid)
|
||||
|
||||
self.assertEqual('Canceled by operator', status['error'])
|
||||
self.assertTrue(status['finished'])
|
||||
|
||||
self.wait_provisioning_state(
|
||||
uuid, 'inspect failed',
|
||||
timeout=CONF.baremetal.active_timeout,
|
||||
interval=self.wait_provisioning_state_interval)
|
||||
|
||||
@decorators.idempotent_id('03bf7990-bee0-4dd7-bf74-b97ad7b52a4b')
|
||||
@utils.services('compute', 'image', 'network')
|
||||
def test_baremetal_introspection(self):
|
||||
"""This smoke test case follows this 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.assertEqual('yes', node['extra']['rule_success'])
|
||||
if CONF.service_available.swift:
|
||||
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, baremetal_manager.BaremetalProvisionStates.AVAILABLE,
|
||||
timeout=CONF.baremetal.active_timeout,
|
||||
interval=self.wait_provisioning_state_interval)
|
||||
|
||||
self.wait_for_nova_aware_of_bvms()
|
||||
self.add_keypair()
|
||||
ins, _node = self.boot_instance()
|
||||
self.terminate_instance(ins)
|
||||
|
||||
@decorators.idempotent_id('70ca3070-184b-4b7d-8892-e977d2bc2870')
|
||||
def test_introspection_abort(self):
|
||||
"""This smoke test case follows this very basic set of operations:
|
||||
|
||||
* Start nodes introspection
|
||||
* Wait until nodes power on
|
||||
* Abort introspection
|
||||
* Verifies nodes status and power state
|
||||
|
||||
"""
|
||||
# start nodes introspection
|
||||
for node_id in self.node_ids:
|
||||
self.introspect_node(node_id, remove_props=False)
|
||||
|
||||
# wait for nodes power on
|
||||
for node_id in self.node_ids:
|
||||
self.wait_power_state(
|
||||
node_id,
|
||||
baremetal_manager.BaremetalPowerStates.POWER_ON)
|
||||
|
||||
# abort introspection
|
||||
for node_id in self.node_ids:
|
||||
self.introspection_abort(node_id)
|
||||
|
||||
# wait for nodes power off
|
||||
for node_id in self.node_ids:
|
||||
self.wait_power_state(
|
||||
node_id,
|
||||
baremetal_manager.BaremetalPowerStates.POWER_OFF)
|
||||
|
||||
# verify nodes status and provision state
|
||||
for node_id in self.node_ids:
|
||||
self.verify_introspection_aborted(node_id)
|
||||
|
||||
|
||||
class InspectorSmokeTest(manager.InspectorScenarioTest):
|
||||
|
||||
@decorators.idempotent_id('a702d1f1-88e4-42ce-88ef-cba2d9e3312e')
|
||||
@decorators.attr(type='smoke')
|
||||
@utils.services('object_storage')
|
||||
def test_baremetal_introspection(self):
|
||||
"""This smoke test case follows this very basic set of operations:
|
||||
|
||||
* Fetches expected properties from baremetal flavor
|
||||
* Removes all properties from one node
|
||||
* Sets the node to manageable state
|
||||
* Inspects the node
|
||||
* Sets the node to available state
|
||||
|
||||
"""
|
||||
# NOTE(dtantsur): we can't silently skip this test because it runs in
|
||||
# grenade with several other tests, and we won't have any indication
|
||||
# that it was not run.
|
||||
assert self.node_ids, "No available nodes"
|
||||
node_id = next(iter(self.node_ids))
|
||||
self.introspect_node(node_id)
|
||||
|
||||
# settle down introspection
|
||||
self.wait_for_introspection_finished([node_id])
|
||||
self.wait_provisioning_state(
|
||||
node_id, 'manageable',
|
||||
timeout=CONF.baremetal_introspection.ironic_sync_timeout,
|
||||
interval=self.wait_provisioning_state_interval)
|
150
ironic_tempest_plugin/tests/test_discovery.py
Normal file
150
ironic_tempest_plugin/tests/test_discovery.py
Normal file
@ -0,0 +1,150 @@
|
||||
# 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.lib import decorators
|
||||
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))
|
||||
|
||||
@decorators.idempotent_id('dd3abe5e-0d23-488d-bb4e-344cdeff7dcb')
|
||||
def test_bearmetal_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)
|
||||
if CONF.service_available.swift:
|
||||
self.verify_node_introspection_data(inspected_node)
|
||||
self.verify_node_driver_info(self.node_info, inspected_node)
|
||||
self.assertEqual(ProvisionStates.ENROLL,
|
||||
inspected_node['provision_state'])
|
Loading…
Reference in New Issue
Block a user