Validate scheduling fields in basic ops scenario

Currently there is no validation of node scheduling fields - resource
class and traits - in the scenario tests. This change adds validation of
these fields to the bare metal basic ops test.

We query the flavor used to boot the instance, and extract all requested
resources and traits from extra_specs. These are matched against the
resource class and traits set on the bare metal node that was scheduled.

Change-Id: I9ddc895ead61cf02c6967ead094d061cb7f558d8
Depends-On: https://review.openstack.org/545370
Related-Bug: #1722194
This commit is contained in:
Mark Goddard 2018-02-16 13:37:25 +00:00
parent ca346cb1c9
commit 56399ccba1
5 changed files with 107 additions and 14 deletions

View File

@ -11,7 +11,7 @@
# under the License.
def get_node(client, node_id=None, instance_uuid=None):
def get_node(client, node_id=None, instance_uuid=None, api_version=None):
"""Get a node by its identifier or instance UUID.
If both node_id and instance_uuid specified, node_id will be used.
@ -19,15 +19,17 @@ def get_node(client, node_id=None, instance_uuid=None):
:param client: an instance of tempest plugin BaremetalClient.
:param node_id: identifier (UUID or name) of the node.
:param instance_uuid: UUID of the instance.
:param api_version: Ironic API version to use.
:returns: the requested node.
:raises: AssertionError, if neither node_id nor instance_uuid was provided
"""
assert node_id or instance_uuid, ('Either node or instance identifier '
'has to be provided.')
if node_id:
_, body = client.show_node(node_id)
_, body = client.show_node(node_id, api_version=api_version)
return body
elif instance_uuid:
_, body = client.show_node_by_instance_uuid(instance_uuid)
_, body = client.show_node_by_instance_uuid(instance_uuid,
api_version=api_version)
if body['nodes']:
return body['nodes'][0]

View File

@ -154,10 +154,14 @@ class BaremetalClient(rest_client.RestClient):
resource,
uuid=None,
permanent=False,
headers=None,
extra_headers=False,
**kwargs):
"""Gets a specific object of the specified type.
:param uuid: Unique identifier of the object in UUID format.
:param headers: List of headers to use in request.
:param extra_headers: Specify whether to use headers.
:returns: Serialized object as a dictionary.
"""
@ -165,7 +169,8 @@ class BaremetalClient(rest_client.RestClient):
uri = kwargs['uri']
else:
uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
resp, body = self.get(uri)
resp, body = self.get(uri, headers=headers,
extra_headers=extra_headers)
self.expected_success(http_client.OK, resp.status)
return resp, self.deserialize(body)

View File

@ -20,6 +20,24 @@ class BaremetalClient(base.BaremetalClient):
version = '1'
uri_prefix = 'v1'
@staticmethod
def _get_headers(api_version):
"""Return headers for a request.
Currently supports a header specifying the API version to use.
:param api_version: Ironic API version to use.
:return: a 2-tuple of (extra_headers, headers), where 'extra_headers'
is whether to use headers, and 'headers' is a list of headers to
use in the request.
"""
extra_headers = False
headers = None
if api_version is not None:
extra_headers = True
headers = {'x-openstack-ironic-api-version': api_version}
return extra_headers, headers
@base.handle_errors
def list_nodes(self, **kwargs):
"""List all existing nodes."""
@ -81,28 +99,33 @@ class BaremetalClient(base.BaremetalClient):
return self._list_request('drivers')
@base.handle_errors
def show_node(self, uuid):
def show_node(self, uuid, api_version=None):
"""Gets a specific node.
:param uuid: Unique identifier of the node in UUID format.
:param api_version: Ironic API version to use.
:return: Serialized node as a dictionary.
"""
return self._show_request('nodes', uuid)
extra_headers, headers = self._get_headers(api_version)
return self._show_request('nodes', uuid, headers=headers,
extra_headers=extra_headers)
@base.handle_errors
def show_node_by_instance_uuid(self, instance_uuid):
def show_node_by_instance_uuid(self, instance_uuid, api_version=None):
"""Gets a node associated with given instance uuid.
:param instance_uuid: Unique identifier of the instance in UUID format.
:param api_version: Ironic API version to use.
:return: Serialized node as a dictionary.
"""
uri = '/nodes/detail?instance_uuid=%s' % instance_uuid
extra_headers, headers = self._get_headers(api_version)
return self._show_request('nodes',
uuid=None,
uri=uri)
uri=uri, headers=headers,
extra_headers=extra_headers)
@base.handle_errors
def show_chassis(self, uuid):

View File

@ -119,8 +119,9 @@ class BaremetalScenarioTest(manager.ScenarioTest):
instance_id)
@classmethod
def get_node(cls, node_id=None, instance_id=None):
return utils.get_node(cls.baremetal_client, node_id, instance_id)
def get_node(cls, node_id=None, instance_id=None, api_version=None):
return utils.get_node(cls.baremetal_client, node_id, instance_id,
api_version)
def get_ports(self, node_uuid):
ports = []

View File

@ -35,6 +35,7 @@ class BaremetalBasicOps(baremetal_manager.BaremetalScenarioTest):
* Monitors the associated Ironic node for power and
expected state transitions
* Validates Ironic node's port data has been properly updated
* Validates Ironic node's resource class and traits have been honoured
* Verifies SSH connectivity using created keypair via fixed IP
* Associates a floating ip
* Verifies SSH connectivity using created keypair via floating IP
@ -44,6 +45,16 @@ class BaremetalBasicOps(baremetal_manager.BaremetalScenarioTest):
expected state transitions
"""
@staticmethod
def _is_version_supported(version):
"""Return whether an API microversion is supported."""
min_version = api_version_request.APIVersionRequest(
CONF.baremetal.min_microversion)
max_version = api_version_request.APIVersionRequest(
CONF.baremetal.max_microversion)
version = api_version_request.APIVersionRequest(version)
return min_version <= version <= max_version
def rebuild_instance(self, preserve_ephemeral=False):
self.rebuild_server(server_id=self.instance['id'],
preserve_ephemeral=preserve_ephemeral,
@ -105,9 +116,7 @@ class BaremetalBasicOps(baremetal_manager.BaremetalScenarioTest):
vifs = []
# TODO(vsaienko) switch to get_node_vifs() when all stable releases
# supports Ironic API 1.28
if (api_version_request.APIVersionRequest(
CONF.baremetal.max_microversion) >=
api_version_request.APIVersionRequest('1.28')):
if self._is_version_supported('1.28'):
vifs = self.get_node_vifs(node_uuid)
else:
for port in self.get_ports(self.node['uuid']):
@ -124,12 +133,65 @@ class BaremetalBasicOps(baremetal_manager.BaremetalScenarioTest):
self.assertEqual(n_port['device_id'], self.instance['id'])
self.assertIn(n_port['mac_address'], ir_ports_addresses)
def validate_scheduling(self):
"""Validate scheduling attributes of the node against the flavor.
Validates the resource class and traits requested by the flavor against
those set on the node. Does not assume that resource classes and traits
are in use.
"""
# Try to get a node with resource class (1.21) and traits (1.37).
# TODO(mgoddard): Remove this when all stable releases support these
# API versions.
for version in ('1.37', '1.21'):
if self._is_version_supported(version):
node = self.get_node(instance_id=self.instance['id'],
api_version=version)
break
else:
# Neither API is supported - cannot test.
LOG.warning("Cannot validate resource class and trait based "
"scheduling as these require API version 1.21 and "
"1.37 respectively")
return
f_id = self.instance['flavor']['id']
extra_specs = self.flavors_client.list_flavor_extra_specs(f_id)
extra_specs = extra_specs['extra_specs']
# Pull the requested resource class and traits from the flavor.
resource_class = None
traits = set()
for key, value in extra_specs.items():
if key.startswith('resources:CUSTOM_') and value == '1':
resource_class = key.partition(':')[2]
if key.startswith('trait:') and value == 'required':
trait = key.partition(':')[2]
traits.add(trait)
# Validate requested resource class and traits against the node.
if resource_class is not None:
# The resource class in ironic may be lower case, and must omit the
# CUSTOM_ prefix. Normalise it.
node_resource_class = node['resource_class']
node_resource_class = node_resource_class.upper()
node_resource_class = 'CUSTOM_' + node_resource_class
self.assertEqual(resource_class, node_resource_class)
if 'traits' in node and traits:
self.assertIn('traits', node['instance_info'])
# All flavor traits should be added as instance traits.
self.assertEqual(traits, set(node['instance_info']['traits']))
# Flavor traits should be a subset of node traits.
self.assertTrue(traits.issubset(set(node['traits'])))
@decorators.idempotent_id('549173a5-38ec-42bb-b0e2-c8b9f4a08943')
@utils.services('compute', 'image', 'network')
def test_baremetal_server_ops(self):
self.add_keypair()
self.instance, self.node = self.boot_instance()
self.validate_ports()
self.validate_scheduling()
ip_address = self.get_server_ip(self.instance)
self.get_remote_client(ip_address).validate_authentication()
vm_client = self.get_remote_client(ip_address)