Merge "Validate scheduling fields in basic ops scenario"
This commit is contained in:
commit
d8de5ac182
|
@ -11,7 +11,7 @@
|
||||||
# under the License.
|
# 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.
|
"""Get a node by its identifier or instance UUID.
|
||||||
|
|
||||||
If both node_id and instance_uuid specified, node_id will be used.
|
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 client: an instance of tempest plugin BaremetalClient.
|
||||||
:param node_id: identifier (UUID or name) of the node.
|
:param node_id: identifier (UUID or name) of the node.
|
||||||
:param instance_uuid: UUID of the instance.
|
:param instance_uuid: UUID of the instance.
|
||||||
|
:param api_version: Ironic API version to use.
|
||||||
:returns: the requested node.
|
:returns: the requested node.
|
||||||
:raises: AssertionError, if neither node_id nor instance_uuid was provided
|
:raises: AssertionError, if neither node_id nor instance_uuid was provided
|
||||||
"""
|
"""
|
||||||
assert node_id or instance_uuid, ('Either node or instance identifier '
|
assert node_id or instance_uuid, ('Either node or instance identifier '
|
||||||
'has to be provided.')
|
'has to be provided.')
|
||||||
if node_id:
|
if node_id:
|
||||||
_, body = client.show_node(node_id)
|
_, body = client.show_node(node_id, api_version=api_version)
|
||||||
return body
|
return body
|
||||||
elif instance_uuid:
|
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']:
|
if body['nodes']:
|
||||||
return body['nodes'][0]
|
return body['nodes'][0]
|
||||||
|
|
|
@ -154,10 +154,14 @@ class BaremetalClient(rest_client.RestClient):
|
||||||
resource,
|
resource,
|
||||||
uuid=None,
|
uuid=None,
|
||||||
permanent=False,
|
permanent=False,
|
||||||
|
headers=None,
|
||||||
|
extra_headers=False,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""Gets a specific object of the specified type.
|
"""Gets a specific object of the specified type.
|
||||||
|
|
||||||
:param uuid: Unique identifier of the object in UUID format.
|
: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.
|
:returns: Serialized object as a dictionary.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -165,7 +169,8 @@ class BaremetalClient(rest_client.RestClient):
|
||||||
uri = kwargs['uri']
|
uri = kwargs['uri']
|
||||||
else:
|
else:
|
||||||
uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
|
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)
|
self.expected_success(http_client.OK, resp.status)
|
||||||
|
|
||||||
return resp, self.deserialize(body)
|
return resp, self.deserialize(body)
|
||||||
|
|
|
@ -20,6 +20,24 @@ class BaremetalClient(base.BaremetalClient):
|
||||||
version = '1'
|
version = '1'
|
||||||
uri_prefix = 'v1'
|
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
|
@base.handle_errors
|
||||||
def list_nodes(self, **kwargs):
|
def list_nodes(self, **kwargs):
|
||||||
"""List all existing nodes."""
|
"""List all existing nodes."""
|
||||||
|
@ -81,28 +99,33 @@ class BaremetalClient(base.BaremetalClient):
|
||||||
return self._list_request('drivers')
|
return self._list_request('drivers')
|
||||||
|
|
||||||
@base.handle_errors
|
@base.handle_errors
|
||||||
def show_node(self, uuid):
|
def show_node(self, uuid, api_version=None):
|
||||||
"""Gets a specific node.
|
"""Gets a specific node.
|
||||||
|
|
||||||
:param uuid: Unique identifier of the node in UUID format.
|
: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: 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
|
@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.
|
"""Gets a node associated with given instance uuid.
|
||||||
|
|
||||||
:param instance_uuid: Unique identifier of the instance in UUID format.
|
: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.
|
:return: Serialized node as a dictionary.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
uri = '/nodes/detail?instance_uuid=%s' % instance_uuid
|
uri = '/nodes/detail?instance_uuid=%s' % instance_uuid
|
||||||
|
extra_headers, headers = self._get_headers(api_version)
|
||||||
return self._show_request('nodes',
|
return self._show_request('nodes',
|
||||||
uuid=None,
|
uuid=None,
|
||||||
uri=uri)
|
uri=uri, headers=headers,
|
||||||
|
extra_headers=extra_headers)
|
||||||
|
|
||||||
@base.handle_errors
|
@base.handle_errors
|
||||||
def show_chassis(self, uuid):
|
def show_chassis(self, uuid):
|
||||||
|
|
|
@ -120,8 +120,9 @@ class BaremetalScenarioTest(manager.ScenarioTest):
|
||||||
instance_id)
|
instance_id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_node(cls, node_id=None, instance_id=None):
|
def get_node(cls, node_id=None, instance_id=None, api_version=None):
|
||||||
return utils.get_node(cls.baremetal_client, node_id, instance_id)
|
return utils.get_node(cls.baremetal_client, node_id, instance_id,
|
||||||
|
api_version)
|
||||||
|
|
||||||
def get_ports(self, node_uuid):
|
def get_ports(self, node_uuid):
|
||||||
ports = []
|
ports = []
|
||||||
|
|
|
@ -35,6 +35,7 @@ class BaremetalBasicOps(baremetal_manager.BaremetalScenarioTest):
|
||||||
* Monitors the associated Ironic node for power and
|
* Monitors the associated Ironic node for power and
|
||||||
expected state transitions
|
expected state transitions
|
||||||
* Validates Ironic node's port data has been properly updated
|
* 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
|
* Verifies SSH connectivity using created keypair via fixed IP
|
||||||
* Associates a floating ip
|
* Associates a floating ip
|
||||||
* Verifies SSH connectivity using created keypair via floating IP
|
* Verifies SSH connectivity using created keypair via floating IP
|
||||||
|
@ -44,6 +45,16 @@ class BaremetalBasicOps(baremetal_manager.BaremetalScenarioTest):
|
||||||
expected state transitions
|
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):
|
def rebuild_instance(self, preserve_ephemeral=False):
|
||||||
self.rebuild_server(server_id=self.instance['id'],
|
self.rebuild_server(server_id=self.instance['id'],
|
||||||
preserve_ephemeral=preserve_ephemeral,
|
preserve_ephemeral=preserve_ephemeral,
|
||||||
|
@ -105,9 +116,7 @@ class BaremetalBasicOps(baremetal_manager.BaremetalScenarioTest):
|
||||||
vifs = []
|
vifs = []
|
||||||
# TODO(vsaienko) switch to get_node_vifs() when all stable releases
|
# TODO(vsaienko) switch to get_node_vifs() when all stable releases
|
||||||
# supports Ironic API 1.28
|
# supports Ironic API 1.28
|
||||||
if (api_version_request.APIVersionRequest(
|
if self._is_version_supported('1.28'):
|
||||||
CONF.baremetal.max_microversion) >=
|
|
||||||
api_version_request.APIVersionRequest('1.28')):
|
|
||||||
vifs = self.get_node_vifs(node_uuid)
|
vifs = self.get_node_vifs(node_uuid)
|
||||||
else:
|
else:
|
||||||
for port in self.get_ports(self.node['uuid']):
|
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.assertEqual(n_port['device_id'], self.instance['id'])
|
||||||
self.assertIn(n_port['mac_address'], ir_ports_addresses)
|
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')
|
@decorators.idempotent_id('549173a5-38ec-42bb-b0e2-c8b9f4a08943')
|
||||||
@utils.services('compute', 'image', 'network')
|
@utils.services('compute', 'image', 'network')
|
||||||
def test_baremetal_server_ops(self):
|
def test_baremetal_server_ops(self):
|
||||||
self.add_keypair()
|
self.add_keypair()
|
||||||
self.instance, self.node = self.boot_instance()
|
self.instance, self.node = self.boot_instance()
|
||||||
self.validate_ports()
|
self.validate_ports()
|
||||||
|
self.validate_scheduling()
|
||||||
ip_address = self.get_server_ip(self.instance)
|
ip_address = self.get_server_ip(self.instance)
|
||||||
self.get_remote_client(ip_address).validate_authentication()
|
self.get_remote_client(ip_address).validate_authentication()
|
||||||
vm_client = self.get_remote_client(ip_address)
|
vm_client = self.get_remote_client(ip_address)
|
||||||
|
|
Loading…
Reference in New Issue