Migrate Tempest tests into Ironic tree

By using Tempest External Plugin, Tempest tests no longer
need to live in Tempest tree.
This patch set migrates Tempest tests from Tempest tree to Ironic.

Change-Id: Ic52806987dae9f9df561ebd662f12c3445d0e2af
This commit is contained in:
Yuiko Takada 2015-12-07 11:49:12 +09:00
parent 691de301ce
commit 57e3ba1af8
20 changed files with 2171 additions and 4 deletions

View File

@ -1,6 +1,22 @@
===============================================
Tempest Integration of Ironic
===============================================
=====================
Ironic tempest plugin
=====================
This directory contains Tempest tests to cover the Ironic project.
This directory contains Tempest tests to cover the Ironic project,
as well as a plugin to automatically load these tests into tempest.
See the tempest plugin docs for information on using it:
http://docs.openstack.org/developer/tempest/plugin.html#using-plugins
To run all tests from this plugin, install ironic into your environment
and run::
$ tox -e all-plugin -- ironic
To run a single test case, run with the test case name, for example::
$ tox -e all-plugin -- ironic_tempest_plugin.tests.scenario.test_baremetal_basic_ops.BaremetalBasicOps.test_baremetal_server_ops
To run all tempest tests including this plugin, run::
$ tox -e all-plugin

View File

@ -0,0 +1,39 @@
# All Rights Reserved.
#
# 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 clients
from tempest.common import credentials_factory as common_creds
from tempest import config
from ironic_tempest_plugin.services.baremetal.v1.json.baremetal_client import \
BaremetalClient
CONF = config.CONF
ADMIN_CREDS = common_creds.get_configured_credentials('identity_admin')
class Manager(clients.Manager):
def __init__(self,
credentials=ADMIN_CREDS,
service=None,
api_microversions=None):
super(Manager, self).__init__(credentials, service)
self.baremetal_client = BaremetalClient(
self.auth_provider,
CONF.baremetal.catalog_type,
CONF.identity.region,
endpoint_type=CONF.baremetal.endpoint_type,
**self.default_params_with_timeout_values)

View File

View File

@ -0,0 +1,48 @@
# All Rights Reserved.
#
# 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 time
from tempest_lib.common.utils import misc as misc_utils
from tempest_lib import exceptions as lib_exc
def wait_for_bm_node_status(client, node_id, attr, status):
"""Waits for a baremetal node attribute to reach given status.
The client should have a show_node(node_uuid) method to get the node.
"""
_, node = client.show_node(node_id)
start = int(time.time())
while node[attr] != status:
time.sleep(client.build_interval)
_, node = client.show_node(node_id)
status_curr = node[attr]
if status_curr == status:
return
if int(time.time()) - start >= client.build_timeout:
message = ('Node %(node_id)s failed to reach %(attr)s=%(status)s '
'within the required time (%(timeout)s s).' %
{'node_id': node_id,
'attr': attr,
'status': status,
'timeout': client.build_timeout})
message += ' Current state of %s: %s.' % (attr, status_curr)
caller = misc_utils.find_test_caller()
if caller:
message = '(%s) %s' % (caller, message)
raise lib_exc.TimeoutException(message)

View File

@ -0,0 +1,204 @@
# 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 functools
from oslo_serialization import jsonutils as json
import six
from six.moves.urllib import parse as urllib
from tempest_lib.common import rest_client
def handle_errors(f):
"""A decorator that allows to ignore certain types of errors."""
@functools.wraps(f)
def wrapper(*args, **kwargs):
param_name = 'ignore_errors'
ignored_errors = kwargs.get(param_name, tuple())
if param_name in kwargs:
del kwargs[param_name]
try:
return f(*args, **kwargs)
except ignored_errors:
# Silently ignore errors
pass
return wrapper
class BaremetalClient(rest_client.RestClient):
"""Base Tempest REST client for Ironic API."""
uri_prefix = ''
def serialize(self, object_dict):
"""Serialize an Ironic object."""
return json.dumps(object_dict)
def deserialize(self, object_str):
"""Deserialize an Ironic object."""
return json.loads(object_str)
def _get_uri(self, resource_name, uuid=None, permanent=False):
"""Get URI for a specific resource or object.
:param resource_name: The name of the REST resource, e.g., 'nodes'.
:param uuid: The unique identifier of an object in UUID format.
:returns: Relative URI for the resource or object.
"""
prefix = self.uri_prefix if not permanent else ''
return '{pref}/{res}{uuid}'.format(pref=prefix,
res=resource_name,
uuid='/%s' % uuid if uuid else '')
def _make_patch(self, allowed_attributes, **kwargs):
"""Create a JSON patch according to RFC 6902.
:param allowed_attributes: An iterable object that contains a set of
allowed attributes for an object.
:param **kwargs: Attributes and new values for them.
:returns: A JSON path that sets values of the specified attributes to
the new ones.
"""
def get_change(kwargs, path='/'):
for name, value in six.iteritems(kwargs):
if isinstance(value, dict):
for ch in get_change(value, path + '%s/' % name):
yield ch
else:
if value is None:
yield {'path': path + name,
'op': 'remove'}
else:
yield {'path': path + name,
'value': value,
'op': 'replace'}
patch = [ch for ch in get_change(kwargs)
if ch['path'].lstrip('/') in allowed_attributes]
return patch
def _list_request(self, resource, permanent=False, **kwargs):
"""Get the list of objects of the specified type.
:param resource: The name of the REST resource, e.g., 'nodes'.
:param **kwargs: Parameters for the request.
:returns: A tuple with the server response and deserialized JSON list
of objects
"""
uri = self._get_uri(resource, permanent=permanent)
if kwargs:
uri += "?%s" % urllib.urlencode(kwargs)
resp, body = self.get(uri)
self.expected_success(200, resp['status'])
return resp, self.deserialize(body)
def _show_request(self, resource, uuid, permanent=False, **kwargs):
"""Gets a specific object of the specified type.
:param uuid: Unique identifier of the object in UUID format.
:returns: Serialized object as a dictionary.
"""
if 'uri' in kwargs:
uri = kwargs['uri']
else:
uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
resp, body = self.get(uri)
self.expected_success(200, resp['status'])
return resp, self.deserialize(body)
def _create_request(self, resource, object_dict):
"""Create an object of the specified type.
:param resource: The name of the REST resource, e.g., 'nodes'.
:param object_dict: A Python dict that represents an object of the
specified type.
:returns: A tuple with the server response and the deserialized created
object.
"""
body = self.serialize(object_dict)
uri = self._get_uri(resource)
resp, body = self.post(uri, body=body)
self.expected_success(201, resp['status'])
return resp, self.deserialize(body)
def _delete_request(self, resource, uuid):
"""Delete specified object.
:param resource: The name of the REST resource, e.g., 'nodes'.
:param uuid: The unique identifier of an object in UUID format.
:returns: A tuple with the server response and the response body.
"""
uri = self._get_uri(resource, uuid)
resp, body = self.delete(uri)
self.expected_success(204, resp['status'])
return resp, body
def _patch_request(self, resource, uuid, patch_object):
"""Update specified object with JSON-patch.
:param resource: The name of the REST resource, e.g., 'nodes'.
:param uuid: The unique identifier of an object in UUID format.
:returns: A tuple with the server response and the serialized patched
object.
"""
uri = self._get_uri(resource, uuid)
patch_body = json.dumps(patch_object)
resp, body = self.patch(uri, body=patch_body)
self.expected_success(200, resp['status'])
return resp, self.deserialize(body)
@handle_errors
def get_api_description(self):
"""Retrieves all versions of the Ironic API."""
return self._list_request('', permanent=True)
@handle_errors
def get_version_description(self, version='v1'):
"""Retrieves the desctription of the API.
:param version: The version of the API. Default: 'v1'.
:returns: Serialized description of API resources.
"""
return self._list_request(version, permanent=True)
def _put_request(self, resource, put_object):
"""Update specified object with JSON-patch."""
uri = self._get_uri(resource)
put_body = json.dumps(put_object)
resp, body = self.put(uri, body=put_body)
self.expected_success(202, resp['status'])
return resp, body

View File

@ -0,0 +1,349 @@
# 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
class BaremetalClient(base.BaremetalClient):
"""Base Tempest REST client for Ironic API v1."""
version = '1'
uri_prefix = 'v1'
@base.handle_errors
def list_nodes(self, **kwargs):
"""List all existing nodes."""
return self._list_request('nodes', **kwargs)
@base.handle_errors
def list_chassis(self):
"""List all existing chassis."""
return self._list_request('chassis')
@base.handle_errors
def list_chassis_nodes(self, chassis_uuid):
"""List all nodes associated with a chassis."""
return self._list_request('/chassis/%s/nodes' % chassis_uuid)
@base.handle_errors
def list_ports(self, **kwargs):
"""List all existing ports."""
return self._list_request('ports', **kwargs)
@base.handle_errors
def list_node_ports(self, uuid):
"""List all ports associated with the node."""
return self._list_request('/nodes/%s/ports' % uuid)
@base.handle_errors
def list_nodestates(self, uuid):
"""List all existing states."""
return self._list_request('/nodes/%s/states' % uuid)
@base.handle_errors
def list_ports_detail(self, **kwargs):
"""Details list all existing ports."""
return self._list_request('/ports/detail', **kwargs)
@base.handle_errors
def list_drivers(self):
"""List all existing drivers."""
return self._list_request('drivers')
@base.handle_errors
def show_node(self, uuid):
"""Gets a specific node.
:param uuid: Unique identifier of the node in UUID format.
:return: Serialized node as a dictionary.
"""
return self._show_request('nodes', uuid)
@base.handle_errors
def show_node_by_instance_uuid(self, instance_uuid):
"""Gets a node associated with given instance uuid.
:param uuid: Unique identifier of the node in UUID format.
:return: Serialized node as a dictionary.
"""
uri = '/nodes/detail?instance_uuid=%s' % instance_uuid
return self._show_request('nodes',
uuid=None,
uri=uri)
@base.handle_errors
def show_chassis(self, uuid):
"""Gets a specific chassis.
:param uuid: Unique identifier of the chassis in UUID format.
:return: Serialized chassis as a dictionary.
"""
return self._show_request('chassis', uuid)
@base.handle_errors
def show_port(self, uuid):
"""Gets a specific port.
:param uuid: Unique identifier of the port in UUID format.
:return: Serialized port as a dictionary.
"""
return self._show_request('ports', uuid)
@base.handle_errors
def show_port_by_address(self, address):
"""Gets a specific port by address.
:param address: MAC address of the port.
:return: Serialized port as a dictionary.
"""
uri = '/ports/detail?address=%s' % address
return self._show_request('ports', uuid=None, uri=uri)
def show_driver(self, driver_name):
"""Gets a specific driver.
:param driver_name: Name of driver.
:return: Serialized driver as a dictionary.
"""
return self._show_request('drivers', driver_name)
@base.handle_errors
def create_node(self, chassis_id=None, **kwargs):
"""Create a baremetal node with the specified parameters.
:param cpu_arch: CPU architecture of the node. Default: x86_64.
:param cpus: Number of CPUs. Default: 8.
:param local_gb: Disk size. Default: 1024.
:param memory_mb: Available RAM. Default: 4096.
:param driver: Driver name. Default: "fake"
:return: A tuple with the server response and the created node.
"""
node = {'chassis_uuid': chassis_id,
'properties': {'cpu_arch': kwargs.get('cpu_arch', 'x86_64'),
'cpus': kwargs.get('cpus', 8),
'local_gb': kwargs.get('local_gb', 1024),
'memory_mb': kwargs.get('memory_mb', 4096)},
'driver': kwargs.get('driver', 'fake')}
return self._create_request('nodes', node)
@base.handle_errors
def create_chassis(self, **kwargs):
"""Create a chassis with the specified parameters.
:param description: The description of the chassis.
Default: test-chassis
:return: A tuple with the server response and the created chassis.
"""
chassis = {'description': kwargs.get('description', 'test-chassis')}
return self._create_request('chassis', chassis)
@base.handle_errors
def create_port(self, node_id, **kwargs):
"""Create a port with the specified parameters.
:param node_id: The ID of the node which owns the port.
:param address: MAC address of the port.
:param extra: Meta data of the port. Default: {'foo': 'bar'}.
:param uuid: UUID of the port.
:return: A tuple with the server response and the created port.
"""
port = {'extra': kwargs.get('extra', {'foo': 'bar'}),
'uuid': kwargs['uuid']}
if node_id is not None:
port['node_uuid'] = node_id
if kwargs['address'] is not None:
port['address'] = kwargs['address']
return self._create_request('ports', port)
@base.handle_errors
def delete_node(self, uuid):
"""Deletes a node having the specified UUID.
:param uuid: The unique identifier of the node.
:return: A tuple with the server response and the response body.
"""
return self._delete_request('nodes', uuid)
@base.handle_errors
def delete_chassis(self, uuid):
"""Deletes a chassis having the specified UUID.
:param uuid: The unique identifier of the chassis.
:return: A tuple with the server response and the response body.
"""
return self._delete_request('chassis', uuid)
@base.handle_errors
def delete_port(self, uuid):
"""Deletes a port having the specified UUID.
:param uuid: The unique identifier of the port.
:return: A tuple with the server response and the response body.
"""
return self._delete_request('ports', uuid)
@base.handle_errors
def update_node(self, uuid, **kwargs):
"""Update the specified node.
:param uuid: The unique identifier of the node.
:return: A tuple with the server response and the updated node.
"""
node_attributes = ('properties/cpu_arch',
'properties/cpus',
'properties/local_gb',
'properties/memory_mb',
'driver',
'instance_uuid')
patch = self._make_patch(node_attributes, **kwargs)
return self._patch_request('nodes', uuid, patch)
@base.handle_errors
def update_chassis(self, uuid, **kwargs):
"""Update the specified chassis.
:param uuid: The unique identifier of the chassis.
:return: A tuple with the server response and the updated chassis.
"""
chassis_attributes = ('description',)
patch = self._make_patch(chassis_attributes, **kwargs)
return self._patch_request('chassis', uuid, patch)
@base.handle_errors
def update_port(self, uuid, patch):
"""Update the specified port.
:param uuid: The unique identifier of the port.
:param patch: List of dicts representing json patches.
:return: A tuple with the server response and the updated port.
"""
return self._patch_request('ports', uuid, patch)
@base.handle_errors
def set_node_power_state(self, node_uuid, state):
"""Set power state of the specified node.
:param node_uuid: The unique identifier of the node.
:state: desired state to set (on/off/reboot).
"""
target = {'target': state}
return self._put_request('nodes/%s/states/power' % node_uuid,
target)
@base.handle_errors
def validate_driver_interface(self, node_uuid):
"""Get all driver interfaces of a specific node.
:param uuid: Unique identifier of the node in UUID format.
"""
uri = '{pref}/{res}/{uuid}/{postf}'.format(pref=self.uri_prefix,
res='nodes',
uuid=node_uuid,
postf='validate')
return self._show_request('nodes', node_uuid, uri=uri)
@base.handle_errors
def set_node_boot_device(self, node_uuid, boot_device, persistent=False):
"""Set the boot device of the specified node.
:param node_uuid: The unique identifier of the node.
:param boot_device: The boot device name.
:param persistent: Boolean value. True if the boot device will
persist to all future boots, False if not.
Default: False.
"""
request = {'boot_device': boot_device, 'persistent': persistent}
resp, body = self._put_request('nodes/%s/management/boot_device' %
node_uuid, request)
self.expected_success(204, resp.status)
return body
@base.handle_errors
def get_node_boot_device(self, node_uuid):
"""Get the current boot device of the specified node.
:param node_uuid: The unique identifier of the node.
"""
path = 'nodes/%s/management/boot_device' % node_uuid
resp, body = self._list_request(path)
self.expected_success(200, resp.status)
return body
@base.handle_errors
def get_node_supported_boot_devices(self, node_uuid):
"""Get the supported boot devices of the specified node.
:param node_uuid: The unique identifier of the node.
"""
path = 'nodes/%s/management/boot_device/supported' % node_uuid
resp, body = self._list_request(path)
self.expected_success(200, resp.status)
return body
@base.handle_errors
def get_console(self, node_uuid):
"""Get connection information about the console.
:param node_uuid: Unique identifier of the node in UUID format.
"""
resp, body = self._show_request('nodes/states/console', node_uuid)
self.expected_success(200, resp.status)
return resp, body
@base.handle_errors
def set_console_mode(self, node_uuid, enabled):
"""Start and stop the node console.
:param node_uuid: Unique identifier of the node in UUID format.
:param enabled: Boolean value; whether to enable or disable the
console.
"""
enabled = {'enabled': enabled}
resp, body = self._put_request('nodes/%s/states/console' % node_uuid,
enabled)
self.expected_success(202, resp.status)
return resp, body

View File

@ -0,0 +1,202 @@
# 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 functools
from tempest import config
from tempest import test
from tempest_lib.common.utils import data_utils
from tempest_lib import exceptions as lib_exc
from ironic_tempest_plugin import clients
CONF = config.CONF
# NOTE(adam_g): The baremetal API tests exercise operations such as enroll
# node, power on, power off, etc. Testing against real drivers (ie, IPMI)
# will require passing driver-specific data to Tempest (addresses,
# credentials, etc). Until then, only support testing against the fake driver,
# which has no external dependencies.
SUPPORTED_DRIVERS = ['fake']
# NOTE(jroll): resources must be deleted in a specific order, this list
# defines the resource types to clean up, and the correct order.
RESOURCE_TYPES = ['port', 'node', 'chassis']
def creates(resource):
"""Decorator that adds resources to the appropriate cleanup list."""
def decorator(f):
@functools.wraps(f)
def wrapper(cls, *args, **kwargs):
resp, body = f(cls, *args, **kwargs)
if 'uuid' in body:
cls.created_objects[resource].add(body['uuid'])
return resp, body
return wrapper
return decorator
class BaseBaremetalTest(test.BaseTestCase):
"""Base class for Baremetal API tests."""
credentials = ['admin']
@classmethod
def skip_checks(cls):
super(BaseBaremetalTest, cls).skip_checks()
if CONF.baremetal.driver not in SUPPORTED_DRIVERS:
skip_msg = ('%s skipped as Ironic driver %s is not supported for '
'testing.' %
(cls.__name__, CONF.baremetal.driver))
raise cls.skipException(skip_msg)
@classmethod
def setup_clients(cls):
super(BaseBaremetalTest, cls).setup_clients()
cls.client = clients.Manager().baremetal_client
@classmethod
def resource_setup(cls):
super(BaseBaremetalTest, cls).resource_setup()
cls.driver = CONF.baremetal.driver
cls.power_timeout = CONF.baremetal.power_timeout
cls.created_objects = {}
for resource in RESOURCE_TYPES:
cls.created_objects[resource] = set()
@classmethod
def resource_cleanup(cls):
"""Ensure that all created objects get destroyed."""
try:
for resource in RESOURCE_TYPES:
uuids = cls.created_objects[resource]
delete_method = getattr(cls.client, 'delete_%s' % resource)
for u in uuids:
delete_method(u, ignore_errors=lib_exc.NotFound)
finally:
super(BaseBaremetalTest, cls).resource_cleanup()
@classmethod
@creates('chassis')
def create_chassis(cls, description=None, expect_errors=False):
"""Wrapper utility for creating test chassis.
:param description: A description of the chassis. if not supplied,
a random value will be generated.
:return: Created chassis.
"""
description = description or data_utils.rand_name('test-chassis')
resp, body = cls.client.create_chassis(description=description)
return resp, body
@classmethod
@creates('node')
def create_node(cls, chassis_id, cpu_arch='x86', cpus=8, local_gb=10,
memory_mb=4096):
"""Wrapper utility for creating test baremetal nodes.
:param cpu_arch: CPU architecture of the node. Default: x86.
:param cpus: Number of CPUs. Default: 8.
:param local_gb: Disk size. Default: 10.
:param memory_mb: Available RAM. Default: 4096.
:return: Created node.
"""
resp, body = cls.client.create_node(chassis_id, cpu_arch=cpu_arch,
cpus=cpus, local_gb=local_gb,
memory_mb=memory_mb,
driver=cls.driver)
return resp, body
@classmethod
@creates('port')
def create_port(cls, node_id, address, extra=None, uuid=None):
"""Wrapper utility for creating test ports.
:param address: MAC address of the port.
:param extra: Meta data of the port. If not supplied, an empty
dictionary will be created.
:param uuid: UUID of the port.
:return: Created port.
"""
extra = extra or {}
resp, body = cls.client.create_port(address=address, node_id=node_id,
extra=extra, uuid=uuid)
return resp, body
@classmethod
def delete_chassis(cls, chassis_id):
"""Deletes a chassis having the specified UUID.
:param uuid: The unique identifier of the chassis.
:return: Server response.
"""
resp, body = cls.client.delete_chassis(chassis_id)
if chassis_id in cls.created_objects['chassis']:
cls.created_objects['chassis'].remove(chassis_id)
return resp
@classmethod
def delete_node(cls, node_id):
"""Deletes a node having the specified UUID.
:param uuid: The unique identifier of the node.
:return: Server response.
"""
resp, body = cls.client.delete_node(node_id)
if node_id in cls.created_objects['node']:
cls.created_objects['node'].remove(node_id)
return resp
@classmethod
def delete_port(cls, port_id):
"""Deletes a port having the specified UUID.
:param uuid: The unique identifier of the port.
:return: Server response.
"""
resp, body = cls.client.delete_port(port_id)
if port_id in cls.created_objects['port']:
cls.created_objects['port'].remove(port_id)
return resp
def validate_self_link(self, resource, uuid, link):
"""Check whether the given self link formatted correctly."""
expected_link = "{base}/{pref}/{res}/{uuid}".format(
base=self.client.base_url,
pref=self.client.uri_prefix,
res=resource,
uuid=uuid)
self.assertEqual(expected_link, link)

View File

@ -0,0 +1,43 @@
# 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
from ironic_tempest_plugin.tests.api.admin import base
class TestApiDiscovery(base.BaseBaremetalTest):
"""Tests for API discovery features."""
@test.idempotent_id('a3c27e94-f56c-42c4-8600-d6790650b9c5')
def test_api_versions(self):
_, descr = self.client.get_api_description()
expected_versions = ('v1',)
versions = [version['id'] for version in descr['versions']]
for v in expected_versions:
self.assertIn(v, versions)
@test.idempotent_id('896283a6-488e-4f31-af78-6614286cbe0d')
def test_default_version(self):
_, descr = self.client.get_api_description()
default_version = descr['default_version']
self.assertEqual(default_version['id'], 'v1')
@test.idempotent_id('abc0b34d-e684-4546-9728-ab7a9ad9f174')
def test_version_1_resources(self):
_, descr = self.client.get_version_description(version='v1')
expected_resources = ('nodes', 'chassis',
'ports', 'links', 'media_types')
for res in expected_resources:
self.assertIn(res, descr)

View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# 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 tempest import test
from tempest_lib.common.utils import data_utils
from tempest_lib import exceptions as lib_exc
from ironic_tempest_plugin.tests.api.admin import base
class TestChassis(base.BaseBaremetalTest):
"""Tests for chassis."""
@classmethod
def resource_setup(cls):
super(TestChassis, cls).resource_setup()
_, cls.chassis = cls.create_chassis()
def _assertExpected(self, expected, actual):
# Check if not expected keys/values exists in actual response body
for key, value in six.iteritems(expected):
if key not in ('created_at', 'updated_at'):
self.assertIn(key, actual)
self.assertEqual(value, actual[key])
@test.idempotent_id('7c5a2e09-699c-44be-89ed-2bc189992d42')
def test_create_chassis(self):
descr = data_utils.rand_name('test-chassis')
_, chassis = self.create_chassis(description=descr)
self.assertEqual(chassis['description'], descr)
@test.idempotent_id('cabe9c6f-dc16-41a7-b6b9-0a90c212edd5')
def test_create_chassis_unicode_description(self):
# Use a unicode string for testing:
# 'We ♡ OpenStack in Ukraine'
descr = u'В Україні ♡ OpenStack!'
_, chassis = self.create_chassis(description=descr)
self.assertEqual(chassis['description'], descr)
@test.idempotent_id('c84644df-31c4-49db-a307-8942881f41c0')
def test_show_chassis(self):
_, chassis = self.client.show_chassis(self.chassis['uuid'])
self._assertExpected(self.chassis, chassis)
@test.idempotent_id('29c9cd3f-19b5-417b-9864-99512c3b33b3')
def test_list_chassis(self):
_, body = self.client.list_chassis()
self.assertIn(self.chassis['uuid'],
[i['uuid'] for i in body['chassis']])
@test.idempotent_id('5ae649ad-22d1-4fe1-bbc6-97227d199fb3')
def test_delete_chassis(self):
_, body = self.create_chassis()
uuid = body['uuid']
self.delete_chassis(uuid)
self.assertRaises(lib_exc.NotFound, self.client.show_chassis, uuid)
@test.idempotent_id('cda8a41f-6be2-4cbf-840c-994b00a89b44')
def test_update_chassis(self):
_, body = self.create_chassis()
uuid = body['uuid']
new_description = data_utils.rand_name('new-description')
_, body = (self.client.update_chassis(uuid,
description=new_description))
_, chassis = self.client.show_chassis(uuid)
self.assertEqual(chassis['description'], new_description)
@test.idempotent_id('76305e22-a4e2-4ab3-855c-f4e2368b9335')
def test_chassis_node_list(self):
_, node = self.create_node(self.chassis['uuid'])
_, body = self.client.list_chassis_nodes(self.chassis['uuid'])
self.assertIn(node['uuid'], [n['uuid'] for n in body['nodes']])

View File

@ -0,0 +1,39 @@
# Copyright 2014 NEC Corporation. All rights reserved.
#
# 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 config
from tempest import test
from ironic_tempest_plugin.tests.api.admin import base
CONF = config.CONF
class TestDrivers(base.BaseBaremetalTest):
"""Tests for drivers."""
@classmethod
def resource_setup(cls):
super(TestDrivers, cls).resource_setup()
cls.driver_name = CONF.baremetal.driver
@test.idempotent_id('5aed2790-7592-4655-9b16-99abcc2e6ec5')
def test_list_drivers(self):
_, drivers = self.client.list_drivers()
self.assertIn(self.driver_name,
[d['name'] for d in drivers['drivers']])
@test.idempotent_id('fb3287a3-c4d7-44bf-ae9d-1eef906d78ce')
def test_show_driver(self):
_, driver = self.client.show_driver(self.driver_name)
self.assertEqual(self.driver_name, driver['name'])

View File

@ -0,0 +1,169 @@
# 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 tempest import test
from tempest_lib.common.utils import data_utils
from tempest_lib import exceptions as lib_exc
from ironic_tempest_plugin.common import waiters
from ironic_tempest_plugin.tests.api.admin import base
class TestNodes(base.BaseBaremetalTest):
"""Tests for baremetal nodes."""
def setUp(self):
super(TestNodes, self).setUp()
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
def _assertExpected(self, expected, actual):
# Check if not expected keys/values exists in actual response body
for key, value in six.iteritems(expected):
if key not in ('created_at', 'updated_at'):
self.assertIn(key, actual)
self.assertEqual(value, actual[key])
def _associate_node_with_instance(self):
self.client.set_node_power_state(self.node['uuid'], 'power off')
waiters.wait_for_bm_node_status(self.client, self.node['uuid'],
'power_state', 'power off')
instance_uuid = data_utils.rand_uuid()
self.client.update_node(self.node['uuid'],
instance_uuid=instance_uuid)
self.addCleanup(self.client.update_node,
uuid=self.node['uuid'], instance_uuid=None)
return instance_uuid
@test.idempotent_id('4e939eb2-8a69-4e84-8652-6fffcbc9db8f')
def test_create_node(self):
params = {'cpu_arch': 'x86_64',
'cpus': '12',
'local_gb': '10',
'memory_mb': '1024'}
_, body = self.create_node(self.chassis['uuid'], **params)
self._assertExpected(params, body['properties'])
@test.idempotent_id('9ade60a4-505e-4259-9ec4-71352cbbaf47')
def test_delete_node(self):
_, node = self.create_node(self.chassis['uuid'])
self.delete_node(node['uuid'])
self.assertRaises(lib_exc.NotFound, self.client.show_node,
node['uuid'])
@test.idempotent_id('55451300-057c-4ecf-8255-ba42a83d3a03')
def test_show_node(self):
_, loaded_node = self.client.show_node(self.node['uuid'])
self._assertExpected(self.node, loaded_node)
@test.idempotent_id('4ca123c4-160d-4d8d-a3f7-15feda812263')
def test_list_nodes(self):
_, body = self.client.list_nodes()
self.assertIn(self.node['uuid'],
[i['uuid'] for i in body['nodes']])
@test.idempotent_id('85b1f6e0-57fd-424c-aeff-c3422920556f')
def test_list_nodes_association(self):
_, body = self.client.list_nodes(associated=True)
self.assertNotIn(self.node['uuid'],
[n['uuid'] for n in body['nodes']])
self._associate_node_with_instance()
_, body = self.client.list_nodes(associated=True)
self.assertIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])
_, body = self.client.list_nodes(associated=False)
self.assertNotIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])
@test.idempotent_id('18c4ebd8-f83a-4df7-9653-9fb33a329730')
def test_node_port_list(self):
_, port = self.create_port(self.node['uuid'],
data_utils.rand_mac_address())
_, body = self.client.list_node_ports(self.node['uuid'])
self.assertIn(port['uuid'],
[p['uuid'] for p in body['ports']])
@test.idempotent_id('72591acb-f215-49db-8395-710d14eb86ab')
def test_node_port_list_no_ports(self):
_, node = self.create_node(self.chassis['uuid'])
_, body = self.client.list_node_ports(node['uuid'])
self.assertEmpty(body['ports'])
@test.idempotent_id('4fed270a-677a-4d19-be87-fd38ae490320')
def test_update_node(self):
props = {'cpu_arch': 'x86_64',
'cpus': '12',
'local_gb': '10',
'memory_mb': '128'}
_, node = self.create_node(self.chassis['uuid'], **props)
new_p = {'cpu_arch': 'x86',
'cpus': '1',
'local_gb': '10000',
'memory_mb': '12300'}
_, body = self.client.update_node(node['uuid'], properties=new_p)
_, node = self.client.show_node(node['uuid'])
self._assertExpected(new_p, node['properties'])
@test.idempotent_id('cbf1f515-5f4b-4e49-945c-86bcaccfeb1d')
def test_validate_driver_interface(self):
_, body = self.client.validate_driver_interface(self.node['uuid'])
core_interfaces = ['power', 'deploy']
for interface in core_interfaces:
self.assertIn(interface, body)
@test.idempotent_id('5519371c-26a2-46e9-aa1a-f74226e9d71f')
def test_set_node_boot_device(self):
self.client.set_node_boot_device(self.node['uuid'], 'pxe')
@test.idempotent_id('9ea73775-f578-40b9-bc34-efc639c4f21f')
def test_get_node_boot_device(self):
body = self.client.get_node_boot_device(self.node['uuid'])
self.assertIn('boot_device', body)
self.assertIn('persistent', body)
self.assertTrue(isinstance(body['boot_device'], six.string_types))
self.assertTrue(isinstance(body['persistent'], bool))
@test.idempotent_id('3622bc6f-3589-4bc2-89f3-50419c66b133')
def test_get_node_supported_boot_devices(self):
body = self.client.get_node_supported_boot_devices(self.node['uuid'])
self.assertIn('supported_boot_devices', body)
self.assertTrue(isinstance(body['supported_boot_devices'], list))
@test.idempotent_id('f63b6288-1137-4426-8cfe-0d5b7eb87c06')
def test_get_console(self):
_, body = self.client.get_console(self.node['uuid'])
con_info = ['console_enabled', 'console_info']
for key in con_info:
self.assertIn(key, body)
@test.idempotent_id('80504575-9b21-4670-92d1-143b948f9437')
def test_set_console_mode(self):
self.client.set_console_mode(self.node['uuid'], True)
_, body = self.client.get_console(self.node['uuid'])
self.assertEqual(True, body['console_enabled'])
@test.idempotent_id('b02a4f38-5e8b-44b2-aed2-a69a36ecfd69')
def test_get_node_by_instance_uuid(self):
instance_uuid = self._associate_node_with_instance()
_, body = self.client.show_node_by_instance_uuid(instance_uuid)
self.assertEqual(len(body['nodes']), 1)
self.assertIn(self.node['uuid'], [n['uuid'] for n in body['nodes']])

View File

@ -0,0 +1,59 @@
# Copyright 2014 NEC Corporation. All rights reserved.
#
# 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 oslo_utils import timeutils
from tempest import test
from tempest_lib import exceptions
from ironic_tempest_plugin.tests.api.admin import base
class TestNodeStates(base.BaseBaremetalTest):
"""Tests for baremetal NodeStates."""
@classmethod
def resource_setup(cls):
super(TestNodeStates, cls).resource_setup()
_, cls.chassis = cls.create_chassis()
_, cls.node = cls.create_node(cls.chassis['uuid'])
def _validate_power_state(self, node_uuid, power_state):
# Validate that power state is set within timeout
if power_state == 'rebooting':
power_state = 'power on'
start = timeutils.utcnow()
while timeutils.delta_seconds(
start, timeutils.utcnow()) < self.power_timeout:
_, node = self.client.show_node(node_uuid)
if node['power_state'] == power_state:
return
message = ('Failed to set power state within '
'the required time: %s sec.' % self.power_timeout)
raise exceptions.TimeoutException(message)
@test.idempotent_id('cd8afa5e-3f57-4e43-8185-beb83d3c9015')
def test_list_nodestates(self):
_, nodestates = self.client.list_nodestates(self.node['uuid'])
for key in nodestates:
self.assertEqual(nodestates[key], self.node[key])
@test.idempotent_id('fc5b9320-0c98-4e5a-8848-877fe5a0322c')
def test_set_node_power_state(self):
_, node = self.create_node(self.chassis['uuid'])
states = ["power on", "rebooting", "power off"]
for state in states:
# Set power state
self.client.set_node_power_state(node['uuid'], state)
# Check power state after state is set
self._validate_power_state(node['uuid'], state)

View File

@ -0,0 +1,266 @@
# 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 tempest import test
from tempest_lib.common.utils import data_utils
from tempest_lib import exceptions as lib_exc
from ironic_tempest_plugin.tests.api.admin import base
class TestPorts(base.BaseBaremetalTest):
"""Tests for ports."""
def setUp(self):
super(TestPorts, self).setUp()
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
_, self.port = self.create_port(self.node['uuid'],
data_utils.rand_mac_address())
def _assertExpected(self, expected, actual):
# Check if not expected keys/values exists in actual response body
for key, value in six.iteritems(expected):
if key not in ('created_at', 'updated_at'):
self.assertIn(key, actual)
self.assertEqual(value, actual[key])
@test.idempotent_id('83975898-2e50-42ed-b5f0-e510e36a0b56')
def test_create_port(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
_, body = self.client.show_port(port['uuid'])
self._assertExpected(port, body)
@test.idempotent_id('d1f6b249-4cf6-4fe6-9ed6-a6e84b1bf67b')
def test_create_port_specifying_uuid(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
uuid = data_utils.rand_uuid()
_, port = self.create_port(node_id=node_id,
address=address, uuid=uuid)
_, body = self.client.show_port(uuid)
self._assertExpected(port, body)
@test.idempotent_id('4a02c4b0-6573-42a4-a513-2e36ad485b62')
def test_create_port_with_extra(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'str': 'value', 'int': 123, 'float': 0.123,
'bool': True, 'list': [1, 2, 3], 'dict': {'foo': 'bar'}}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
_, body = self.client.show_port(port['uuid'])
self._assertExpected(port, body)
@test.idempotent_id('1bf257a9-aea3-494e-89c0-63f657ab4fdd')
def test_delete_port(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
self.delete_port(port['uuid'])
self.assertRaises(lib_exc.NotFound, self.client.show_port,
port['uuid'])
@test.idempotent_id('9fa77ab5-ce59-4f05-baac-148904ba1597')
def test_show_port(self):
_, port = self.client.show_port(self.port['uuid'])
self._assertExpected(self.port, port)
@test.idempotent_id('7c1114ff-fc3f-47bb-bc2f-68f61620ba8b')
def test_show_port_by_address(self):
_, port = self.client.show_port_by_address(self.port['address'])
self._assertExpected(self.port, port['ports'][0])
@test.idempotent_id('bd773405-aea5-465d-b576-0ab1780069e5')
def test_show_port_with_links(self):
_, port = self.client.show_port(self.port['uuid'])
self.assertIn('links', port.keys())
self.assertEqual(2, len(port['links']))
self.assertIn(port['uuid'], port['links'][0]['href'])
@test.idempotent_id('b5e91854-5cd7-4a8e-bb35-3e0a1314606d')
def test_list_ports(self):
_, body = self.client.list_ports()
self.assertIn(self.port['uuid'],
[i['uuid'] for i in body['ports']])
# Verify self links.
for port in body['ports']:
self.validate_self_link('ports', port['uuid'],
port['links'][0]['href'])
@test.idempotent_id('324a910e-2f80-4258-9087-062b5ae06240')
def test_list_with_limit(self):
_, body = self.client.list_ports(limit=3)
next_marker = body['ports'][-1]['uuid']
self.assertIn(next_marker, body['next'])
@test.idempotent_id('8a94b50f-9895-4a63-a574-7ecff86e5875')
def test_list_ports_details(self):
node_id = self.node['uuid']
uuids = [
self.create_port(node_id=node_id,
address=data_utils.rand_mac_address())
[1]['uuid'] for i in range(0, 5)]
_, body = self.client.list_ports_detail()
ports_dict = dict((port['uuid'], port) for port in body['ports']
if port['uuid'] in uuids)
for uuid in uuids:
self.assertIn(uuid, ports_dict)
port = ports_dict[uuid]
self.assertIn('extra', port)
self.assertIn('node_uuid', port)
# never expose the node_id
self.assertNotIn('node_id', port)
# Verify self link.
self.validate_self_link('ports', port['uuid'],
port['links'][0]['href'])
@test.idempotent_id('8a03f688-7d75-4ecd-8cbc-e06b8f346738')
def test_list_ports_details_with_address(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
self.create_port(node_id=node_id, address=address)
for i in range(0, 5):
self.create_port(node_id=node_id,
address=data_utils.rand_mac_address())
_, body = self.client.list_ports_detail(address=address)
self.assertEqual(1, len(body['ports']))
self.assertEqual(address, body['ports'][0]['address'])
@test.idempotent_id('9c26298b-1bcb-47b7-9b9e-8bdd6e3c4aba')
def test_update_port_replace(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
new_address = data_utils.rand_mac_address()
new_extra = {'key1': 'new-value1', 'key2': 'new-value2',
'key3': 'new-value3'}
patch = [{'path': '/address',
'op': 'replace',
'value': new_address},
{'path': '/extra/key1',
'op': 'replace',
'value': new_extra['key1']},
{'path': '/extra/key2',
'op': 'replace',
'value': new_extra['key2']},
{'path': '/extra/key3',
'op': 'replace',
'value': new_extra['key3']}]
self.client.update_port(port['uuid'], patch)
_, body = self.client.show_port(port['uuid'])
self.assertEqual(new_address, body['address'])
self.assertEqual(new_extra, body['extra'])
@test.idempotent_id('d7e7fece-6ed9-460a-9ebe-9267217e8580')
def test_update_port_remove(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
# Removing one item from the collection
self.client.update_port(port['uuid'],
[{'path': '/extra/key2',
'op': 'remove'}])
extra.pop('key2')
_, body = self.client.show_port(port['uuid'])
self.assertEqual(extra, body['extra'])
# Removing the collection
self.client.update_port(port['uuid'], [{'path': '/extra',
'op': 'remove'}])
_, body = self.client.show_port(port['uuid'])
self.assertEqual({}, body['extra'])
# Assert nothing else was changed
self.assertEqual(node_id, body['node_uuid'])
self.assertEqual(address, body['address'])
@test.idempotent_id('241288b3-e98a-400f-a4d7-d1f716146361')
def test_update_port_add(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
extra = {'key1': 'value1', 'key2': 'value2'}
patch = [{'path': '/extra/key1',
'op': 'add',
'value': extra['key1']},
{'path': '/extra/key2',
'op': 'add',
'value': extra['key2']}]
self.client.update_port(port['uuid'], patch)
_, body = self.client.show_port(port['uuid'])
self.assertEqual(extra, body['extra'])
@test.idempotent_id('5309e897-0799-4649-a982-0179b04c3876')
def test_update_port_mixed_ops(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key1': 'value1', 'key2': 'value2'}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
new_address = data_utils.rand_mac_address()
new_extra = {'key1': 0.123, 'key3': {'cat': 'meow'}}
patch = [{'path': '/address',
'op': 'replace',
'value': new_address},
{'path': '/extra/key1',
'op': 'replace',
'value': new_extra['key1']},
{'path': '/extra/key2',
'op': 'remove'},
{'path': '/extra/key3',
'op': 'add',
'value': new_extra['key3']}]
self.client.update_port(port['uuid'], patch)
_, body = self.client.show_port(port['uuid'])
self.assertEqual(new_address, body['address'])
self.assertEqual(new_extra, body['extra'])

View File

@ -0,0 +1,339 @@
# 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
from tempest_lib.common.utils import data_utils
from tempest_lib import exceptions as lib_exc
from ironic_tempest_plugin.tests.api.admin import base
class TestPortsNegative(base.BaseBaremetalTest):
"""Negative tests for ports."""
def setUp(self):
super(TestPortsNegative, self).setUp()
_, self.chassis = self.create_chassis()
_, self.node = self.create_node(self.chassis['uuid'])
@test.attr(type=['negative'])
@test.idempotent_id('0a6ee1f7-d0d9-4069-8778-37f3aa07303a')
def test_create_port_malformed_mac(self):
node_id = self.node['uuid']
address = 'malformed:mac'
self.assertRaises(lib_exc.BadRequest,
self.create_port, node_id=node_id, address=address)
@test.attr(type=['negative'])
@test.idempotent_id('30277ee8-0c60-4f1d-b125-0e51c2f43369')
def test_create_port_nonexsistent_node_id(self):
node_id = str(data_utils.rand_uuid())
address = data_utils.rand_mac_address()
self.assertRaises(lib_exc.BadRequest, self.create_port,
node_id=node_id, address=address)
@test.attr(type=['negative'])
@test.idempotent_id('029190f6-43e1-40a3-b64a-65173ba653a3')
def test_show_port_malformed_uuid(self):
self.assertRaises(lib_exc.BadRequest, self.client.show_port,
'malformed:uuid')
@test.attr(type=['negative'])
@test.idempotent_id('0d00e13d-e2e0-45b1-bcbc-55a6d90ca793')
def test_show_port_nonexistent_uuid(self):
self.assertRaises(lib_exc.NotFound, self.client.show_port,
data_utils.rand_uuid())
@test.attr(type=['negative'])
@test.idempotent_id('4ad85266-31e9-4942-99ac-751897dc9e23')
def test_show_port_by_mac_not_allowed(self):
self.assertRaises(lib_exc.BadRequest, self.client.show_port,
data_utils.rand_mac_address())
@test.attr(type=['negative'])
@test.idempotent_id('89a34380-3c61-4c32-955c-2cd9ce94da21')
def test_create_port_duplicated_port_uuid(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
uuid = data_utils.rand_uuid()
self.create_port(node_id=node_id, address=address, uuid=uuid)
self.assertRaises(lib_exc.Conflict, self.create_port, node_id=node_id,
address=address, uuid=uuid)
@test.attr(type=['negative'])
@test.idempotent_id('65e84917-733c-40ae-ae4b-96a4adff931c')
def test_create_port_no_mandatory_field_node_id(self):
address = data_utils.rand_mac_address()
self.assertRaises(lib_exc.BadRequest, self.create_port, node_id=None,
address=address)
@test.attr(type=['negative'])
@test.idempotent_id('bcea3476-7033-4183-acfe-e56a30809b46')
def test_create_port_no_mandatory_field_mac(self):
node_id = self.node['uuid']
self.assertRaises(lib_exc.BadRequest, self.create_port,
node_id=node_id, address=None)
@test.attr(type=['negative'])
@test.idempotent_id('2b51cd18-fb95-458b-9780-e6257787b649')
def test_create_port_malformed_port_uuid(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
uuid = 'malformed:uuid'
self.assertRaises(lib_exc.BadRequest, self.create_port,
node_id=node_id, address=address, uuid=uuid)
@test.attr(type=['negative'])
@test.idempotent_id('583a6856-6a30-4ac4-889f-14e2adff8105')
def test_create_port_malformed_node_id(self):
address = data_utils.rand_mac_address()
self.assertRaises(lib_exc.BadRequest, self.create_port,
node_id='malformed:nodeid', address=address)
@test.attr(type=['negative'])
@test.idempotent_id('e27f8b2e-42c6-4a43-a3cd-accff716bc5c')
def test_create_port_duplicated_mac(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
self.create_port(node_id=node_id, address=address)
self.assertRaises(lib_exc.Conflict,
self.create_port, node_id=node_id,
address=address)
@test.attr(type=['negative'])
@test.idempotent_id('8907082d-ac5e-4be3-b05f-d072ede82020')
def test_update_port_by_mac_not_allowed(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key': 'value'}
self.create_port(node_id=node_id, address=address, extra=extra)
patch = [{'path': '/extra/key',
'op': 'replace',
'value': 'new-value'}]
self.assertRaises(lib_exc.BadRequest,
self.client.update_port, address,
patch)
@test.attr(type=['negative'])
@test.idempotent_id('df1ac70c-db9f-41d9-90f1-78cd6b905718')
def test_update_port_nonexistent(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key': 'value'}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
port_id = port['uuid']
_, body = self.client.delete_port(port_id)
patch = [{'path': '/extra/key',
'op': 'replace',
'value': 'new-value'}]
self.assertRaises(lib_exc.NotFound,
self.client.update_port, port_id, patch)
@test.attr(type=['negative'])
@test.idempotent_id('c701e315-aa52-41ea-817c-65c5ca8ca2a8')
def test_update_port_malformed_port_uuid(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
self.create_port(node_id=node_id, address=address)
new_address = data_utils.rand_mac_address()
self.assertRaises(lib_exc.BadRequest, self.client.update_port,
uuid='malformed:uuid',
patch=[{'path': '/address', 'op': 'replace',
'value': new_address}])
@test.attr(type=['negative'])
@test.idempotent_id('f8f15803-34d6-45dc-b06f-e5e04bf1b38b')
def test_update_port_add_nonexistent_property(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
[{'path': '/nonexistent', ' op': 'add',
'value': 'value'}])
@test.attr(type=['negative'])
@test.idempotent_id('898ec904-38b1-4fcb-9584-1187d4263a2a')
def test_update_port_replace_node_id_with_malformed(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
patch = [{'path': '/node_uuid',
'op': 'replace',
'value': 'malformed:node_uuid'}]
self.assertRaises(lib_exc.BadRequest,
self.client.update_port, port_id, patch)
@test.attr(type=['negative'])
@test.idempotent_id('2949f30f-5f59-43fa-a6d9-4eac578afab4')
def test_update_port_replace_mac_with_duplicated(self):
node_id = self.node['uuid']
address1 = data_utils.rand_mac_address()
address2 = data_utils.rand_mac_address()
_, port1 = self.create_port(node_id=node_id, address=address1)
_, port2 = self.create_port(node_id=node_id, address=address2)
port_id = port2['uuid']
patch = [{'path': '/address',
'op': 'replace',
'value': address1}]
self.assertRaises(lib_exc.Conflict,
self.client.update_port, port_id, patch)
@test.attr(type=['negative'])
@test.idempotent_id('97f6e048-6e4f-4eba-a09d-fbbc78b77a77')
def test_update_port_replace_node_id_with_nonexistent(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
patch = [{'path': '/node_uuid',
'op': 'replace',
'value': data_utils.rand_uuid()}]
self.assertRaises(lib_exc.BadRequest,
self.client.update_port, port_id, patch)
@test.attr(type=['negative'])
@test.idempotent_id('375022c5-9e9e-4b11-9ca4-656729c0c9b2')
def test_update_port_replace_mac_with_malformed(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
patch = [{'path': '/address',
'op': 'replace',
'value': 'malformed:mac'}]
self.assertRaises(lib_exc.BadRequest,
self.client.update_port, port_id, patch)
@test.attr(type=['negative'])
@test.idempotent_id('5722b853-03fc-4854-8308-2036a1b67d85')
def test_update_port_replace_nonexistent_property(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
patch = [{'path': '/nonexistent', ' op': 'replace', 'value': 'value'}]
self.assertRaises(lib_exc.BadRequest,
self.client.update_port, port_id, patch)
@test.attr(type=['negative'])
@test.idempotent_id('ae2696ca-930a-4a7f-918f-30ae97c60f56')
def test_update_port_remove_mandatory_field_mac(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
[{'path': '/address', 'op': 'remove'}])
@test.attr(type=['negative'])
@test.idempotent_id('5392c1f0-2071-4697-9064-ec2d63019018')
def test_update_port_remove_mandatory_field_port_uuid(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
[{'path': '/uuid', 'op': 'remove'}])
@test.attr(type=['negative'])
@test.idempotent_id('06b50d82-802a-47ef-b079-0a3311cf85a2')
def test_update_port_remove_nonexistent_property(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
_, port = self.create_port(node_id=node_id, address=address)
port_id = port['uuid']
self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
[{'path': '/nonexistent', 'op': 'remove'}])
@test.attr(type=['negative'])
@test.idempotent_id('03d42391-2145-4a6c-95bf-63fe55eb64fd')
def test_delete_port_by_mac_not_allowed(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
self.create_port(node_id=node_id, address=address)
self.assertRaises(lib_exc.BadRequest, self.client.delete_port, address)
@test.attr(type=['negative'])
@test.idempotent_id('0629e002-818e-4763-b25b-ae5e07b1cb23')
def test_update_port_mixed_ops_integrity(self):
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
extra = {'key1': 'value1', 'key2': 'value2'}
_, port = self.create_port(node_id=node_id, address=address,
extra=extra)
port_id = port['uuid']
new_address = data_utils.rand_mac_address()
new_extra = {'key1': 'new-value1', 'key3': 'new-value3'}
patch = [{'path': '/address',
'op': 'replace',
'value': new_address},
{'path': '/extra/key1',
'op': 'replace',
'value': new_extra['key1']},
{'path': '/extra/key2',
'op': 'remove'},
{'path': '/extra/key3',
'op': 'add',
'value': new_extra['key3']},
{'path': '/nonexistent',
'op': 'replace',
'value': 'value'}]
self.assertRaises(lib_exc.BadRequest, self.client.update_port, port_id,
patch)
# patch should not be applied
_, body = self.client.show_port(port_id)
self.assertEqual(address, body['address'])
self.assertEqual(extra, body['extra'])

View File

@ -0,0 +1,178 @@
# Copyright 2012 OpenStack Foundation
# Copyright 2013 IBM Corp.
# All Rights Reserved.
#
# 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.common import waiters
from tempest import config
from tempest.scenario import manager # noqa
import tempest.test
from tempest_lib import exceptions as lib_exc
from ironic_tempest_plugin import clients
CONF = config.CONF
# power/provision states as of icehouse
class BaremetalPowerStates(object):
"""Possible power states of an Ironic node."""
POWER_ON = 'power on'
POWER_OFF = 'power off'
REBOOT = 'rebooting'
SUSPEND = 'suspended'
class BaremetalProvisionStates(object):
"""Possible provision states of an Ironic node."""
NOSTATE = None
INIT = 'initializing'
ACTIVE = 'active'
BUILDING = 'building'
DEPLOYWAIT = 'wait call-back'
DEPLOYING = 'deploying'
DEPLOYFAIL = 'deploy failed'
DEPLOYDONE = 'deploy complete'
DELETING = 'deleting'
DELETED = 'deleted'
ERROR = 'error'
class BaremetalScenarioTest(manager.ScenarioTest):
credentials = ['primary', 'admin']
@classmethod
def skip_checks(cls):
super(BaremetalScenarioTest, cls).skip_checks()
if not CONF.baremetal.driver_enabled:
msg = 'Ironic not available or Ironic compute driver not enabled'
raise cls.skipException(msg)
@classmethod
def setup_clients(cls):
super(BaremetalScenarioTest, cls).setup_clients()
cls.baremetal_client = clients.Manager().baremetal_client
@classmethod
def resource_setup(cls):
super(BaremetalScenarioTest, cls).resource_setup()
# allow any issues obtaining the node list to raise early
cls.baremetal_client.list_nodes()
def _node_state_timeout(self, node_id, state_attr,
target_states, timeout=10, interval=1):
if not isinstance(target_states, list):
target_states = [target_states]
def check_state():
node = self.get_node(node_id=node_id)
if node.get(state_attr) in target_states:
return True
return False
if not tempest.test.call_until_true(check_state, timeout, interval):
msg = ("Timed out waiting for node %s to reach %s state(s) %s" %
(node_id, state_attr, target_states))
raise lib_exc.TimeoutException(msg)
def wait_provisioning_state(self, node_id, state, timeout):
self._node_state_timeout(
node_id=node_id, state_attr='provision_state',
target_states=state, timeout=timeout)
def wait_power_state(self, node_id, state):
self._node_state_timeout(
node_id=node_id, state_attr='power_state',
target_states=state, timeout=CONF.baremetal.power_timeout)
def wait_node(self, instance_id):
"""Waits for a node to be associated with instance_id."""
def _get_node():
node = None
try:
node = self.get_node(instance_id=instance_id)
except lib_exc.NotFound:
pass
return node is not None
if (not tempest.test.call_until_true(
_get_node, CONF.baremetal.association_timeout, 1)):
msg = ('Timed out waiting to get Ironic node by instance id %s'
% instance_id)
raise lib_exc.TimeoutException(msg)
def get_node(self, node_id=None, instance_id=None):
if node_id:
_, body = self.baremetal_client.show_node(node_id)
return body
elif instance_id:
_, body = self.baremetal_client.show_node_by_instance_uuid(
instance_id)
if body['nodes']:
return body['nodes'][0]
def get_ports(self, node_uuid):
ports = []
_, body = self.baremetal_client.list_node_ports(node_uuid)
for port in body['ports']:
_, p = self.baremetal_client.show_port(port['uuid'])
ports.append(p)
return ports
def add_keypair(self):
self.keypair = self.create_keypair()
def verify_connectivity(self, ip=None):
if ip:
dest = self.get_remote_client(ip)
else:
dest = self.get_remote_client(self.instance)
dest.validate_authentication()
def boot_instance(self):
self.instance = self.create_server(
key_name=self.keypair['name'])
self.wait_node(self.instance['id'])
self.node = self.get_node(instance_id=self.instance['id'])
self.wait_power_state(self.node['uuid'], BaremetalPowerStates.POWER_ON)
self.wait_provisioning_state(
self.node['uuid'],
[BaremetalProvisionStates.DEPLOYWAIT,
BaremetalProvisionStates.ACTIVE],
timeout=15)
self.wait_provisioning_state(self.node['uuid'],
BaremetalProvisionStates.ACTIVE,
timeout=CONF.baremetal.active_timeout)
waiters.wait_for_server_status(self.servers_client,
self.instance['id'], 'ACTIVE')
self.node = self.get_node(instance_id=self.instance['id'])
self.instance = (self.servers_client.show_server(self.instance['id'])
['server'])
def terminate_instance(self):
self.servers_client.delete_server(self.instance['id'])
self.wait_power_state(self.node['uuid'],
BaremetalPowerStates.POWER_OFF)
self.wait_provisioning_state(
self.node['uuid'],
BaremetalProvisionStates.NOSTATE,
timeout=CONF.baremetal.unprovision_timeout)

View File

@ -0,0 +1,131 @@
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# 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 oslo_log import log as logging
from tempest.common import waiters
from tempest import config
from tempest import test
from ironic_tempest_plugin.tests.scenario import baremetal_manager
CONF = config.CONF
LOG = logging.getLogger(__name__)
class BaremetalBasicOps(baremetal_manager.BaremetalScenarioTest):
"""This smoke test tests the pxe_ssh Ironic driver.
It follows this basic set of operations:
* Creates a keypair
* Boots an instance using the keypair
* Monitors the associated Ironic node for power and
expected state transitions
* Validates Ironic node's port data has been properly updated
* Verifies SSH connectivity using created keypair via fixed IP
* Associates a floating ip
* Verifies SSH connectivity using created keypair via floating IP
* Verifies instance rebuild with ephemeral partition preservation
* Deletes instance
* Monitors the associated Ironic node for power and
expected state transitions
"""
def rebuild_instance(self, preserve_ephemeral=False):
self.rebuild_server(server_id=self.instance['id'],
preserve_ephemeral=preserve_ephemeral,
wait=False)
node = self.get_node(instance_id=self.instance['id'])
# We should remain on the same node
self.assertEqual(self.node['uuid'], node['uuid'])
self.node = node
waiters.wait_for_server_status(
self.servers_client,
server_id=self.instance['id'],
status='REBUILD',
ready_wait=False)
waiters.wait_for_server_status(
self.servers_client,
server_id=self.instance['id'],
status='ACTIVE')
def verify_partition(self, client, label, mount, gib_size):
"""Verify a labeled partition's mount point and size."""
LOG.info("Looking for partition %s mounted on %s" % (label, mount))
# Validate we have a device with the given partition label
cmd = "/sbin/blkid | grep '%s' | cut -d':' -f1" % label
device = client.exec_command(cmd).rstrip('\n')
LOG.debug("Partition device is %s" % device)
self.assertNotEqual('', device)
# Validate the mount point for the device
cmd = "mount | grep '%s' | cut -d' ' -f3" % device
actual_mount = client.exec_command(cmd).rstrip('\n')
LOG.debug("Partition mount point is %s" % actual_mount)
self.assertEqual(actual_mount, mount)
# Validate the partition size matches what we expect
numbers = '0123456789'
devnum = device.replace('/dev/', '')
cmd = "cat /sys/block/%s/%s/size" % (devnum.rstrip(numbers), devnum)
num_bytes = client.exec_command(cmd).rstrip('\n')
num_bytes = int(num_bytes) * 512
actual_gib_size = num_bytes / (1024 * 1024 * 1024)
LOG.debug("Partition size is %d GiB" % actual_gib_size)
self.assertEqual(actual_gib_size, gib_size)
def get_flavor_ephemeral_size(self):
"""Returns size of the ephemeral partition in GiB."""
f_id = self.instance['flavor']['id']
flavor = self.flavors_client.show_flavor(f_id)['flavor']
ephemeral = flavor.get('OS-FLV-EXT-DATA:ephemeral')
if not ephemeral or ephemeral == 'N/A':
return None
return int(ephemeral)
def validate_ports(self):
for port in self.get_ports(self.node['uuid']):
n_port_id = port['extra']['vif_port_id']
body = self.ports_client.show_port(n_port_id)
n_port = body['port']
self.assertEqual(n_port['device_id'], self.instance['id'])
self.assertEqual(n_port['mac_address'], port['address'])
@test.idempotent_id('549173a5-38ec-42bb-b0e2-c8b9f4a08943')
@test.services('baremetal', 'compute', 'image', 'network')
def test_baremetal_server_ops(self):
self.add_keypair()
self.boot_instance()
self.validate_ports()
self.verify_connectivity()
if CONF.validation.connect_method == 'floating':
floating_ip = self.create_floating_ip(self.instance)['ip']
self.verify_connectivity(ip=floating_ip)
vm_client = self.get_remote_client(self.instance)
# We expect the ephemeral partition to be mounted on /mnt and to have
# the same size as our flavor definition.
eph_size = self.get_flavor_ephemeral_size()
if eph_size:
self.verify_partition(vm_client, 'ephemeral0', '/mnt', eph_size)
# Create the test file
self.create_timestamp(
floating_ip, private_key=self.keypair['private_key'])
self.terminate_instance()