API tests for Ironic
Ironic is a baremetal provisioning service that is intended to replace nova-baremetal-driver. Recently it was integrated to devstack so now it's reasonable to start testing it with tempest. This patch adds a client for baremetal provisioning service and some tests for the Ironic API. Change-Id: Ifd65d6a60179e72dbfa81825f234f0ff76ebb055
This commit is contained in:
@@ -95,6 +95,17 @@
|
||||
#syslog_log_facility=LOG_USER
|
||||
|
||||
|
||||
[baremetal]
|
||||
|
||||
#
|
||||
# Options defined in tempest.config
|
||||
#
|
||||
|
||||
# Catalog type of the baremetal provisioning service. (string
|
||||
# value)
|
||||
#catalog_type=baremetal
|
||||
|
||||
|
||||
[boto]
|
||||
|
||||
#
|
||||
@@ -672,6 +683,10 @@
|
||||
# value)
|
||||
#savanna=false
|
||||
|
||||
# Whether or not Ironic is expected to be available (boolean
|
||||
# value)
|
||||
#ironic=false
|
||||
|
||||
|
||||
[stress]
|
||||
|
||||
|
||||
0
tempest/api/baremetal/__init__.py
Normal file
0
tempest/api/baremetal/__init__.py
Normal file
171
tempest/api/baremetal/base.py
Normal file
171
tempest/api/baremetal/base.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 clients
|
||||
from tempest.common.utils import data_utils
|
||||
from tempest import exceptions as exc
|
||||
from tempest import test
|
||||
|
||||
|
||||
def creates(resource):
|
||||
"""Decorator that adds resources to the appropriate cleanup list."""
|
||||
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(cls, *args, **kwargs):
|
||||
result = f(cls, *args, **kwargs)
|
||||
body = result[resource]
|
||||
|
||||
if 'uuid' in body:
|
||||
cls.created_objects[resource].add(body['uuid'])
|
||||
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class BaseBaremetalTest(test.BaseTestCase):
|
||||
"""Base class for Baremetal API tests."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseBaremetalTest, cls).setUpClass()
|
||||
|
||||
if not cls.config.service_available.ironic:
|
||||
skip_msg = ('%s skipped as Ironic is not available' % cls.__name__)
|
||||
raise cls.skipException(skip_msg)
|
||||
|
||||
mgr = clients.AdminManager()
|
||||
cls.client = mgr.baremetal_client
|
||||
|
||||
cls.created_objects = {'chassis': set(),
|
||||
'port': set(),
|
||||
'node': set()}
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Ensure that all created objects get destroyed."""
|
||||
|
||||
try:
|
||||
for resource, uuids in cls.created_objects.iteritems():
|
||||
delete_method = getattr(cls.client, 'delete_%s' % resource)
|
||||
for u in uuids:
|
||||
delete_method(u, ignore_errors=exc.NotFound)
|
||||
finally:
|
||||
super(BaseBaremetalTest, cls).tearDownClass()
|
||||
|
||||
@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 {'chassis': body, 'response': resp}
|
||||
|
||||
@classmethod
|
||||
@creates('node')
|
||||
def create_node(cls, chassis_id, cpu_arch='x86', cpu_num=8, storage=1024,
|
||||
memory=4096, driver='fake'):
|
||||
"""
|
||||
Wrapper utility for creating test baremetal nodes.
|
||||
|
||||
:param cpu_arch: CPU architecture of the node. Default: x86.
|
||||
:param cpu_num: Number of CPUs. Default: 8.
|
||||
:param storage: Disk size. Default: 1024.
|
||||
:param memory: Available RAM. Default: 4096.
|
||||
:return: Created node.
|
||||
|
||||
"""
|
||||
resp, body = cls.client.create_node(chassis_id, cpu_arch=cpu_arch,
|
||||
cpu_num=cpu_num, storage=storage,
|
||||
memory=memory, driver=driver)
|
||||
|
||||
return {'node': body, 'response': resp}
|
||||
|
||||
@classmethod
|
||||
@creates('port')
|
||||
def create_port(cls, node_id, address=None):
|
||||
"""
|
||||
Wrapper utility for creating test ports.
|
||||
|
||||
:param address: MAC address of the port. If not supplied, a random
|
||||
value will be generated.
|
||||
:return: Created port.
|
||||
|
||||
"""
|
||||
address = address or data_utils.rand_mac_address()
|
||||
resp, body = cls.client.create_port(address=address, node_id=node_id)
|
||||
|
||||
return {'port': body, 'response': resp}
|
||||
|
||||
@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
|
||||
46
tempest/api/baremetal/test_api_discovery.py
Normal file
46
tempest/api/baremetal/test_api_discovery.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.api.baremetal import base
|
||||
from tempest import test
|
||||
|
||||
|
||||
class TestApiDiscovery(base.BaseBaremetalTest):
|
||||
"""Tests for API discovery features."""
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_api_versions(self):
|
||||
resp, 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.attr(type='smoke')
|
||||
def test_default_version(self):
|
||||
resp, descr = self.client.get_api_description()
|
||||
default_version = descr['default_version']
|
||||
|
||||
self.assertEqual(default_version['id'], 'v1')
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_version_1_resources(self):
|
||||
resp, 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)
|
||||
78
tempest/api/baremetal/test_chassis.py
Normal file
78
tempest/api/baremetal/test_chassis.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.api.baremetal import base
|
||||
from tempest.common.utils import data_utils
|
||||
from tempest import exceptions as exc
|
||||
from tempest import test
|
||||
|
||||
|
||||
class TestChassis(base.BaseBaremetalTest):
|
||||
"""Tests for chassis."""
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_create_chassis(self):
|
||||
descr = data_utils.rand_name('test-chassis-')
|
||||
ch = self.create_chassis(description=descr)['chassis']
|
||||
|
||||
self.assertEqual(ch['description'], descr)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_create_chassis_unicode_description(self):
|
||||
# Use a unicode string for testing:
|
||||
# 'We ♡ OpenStack in Ukraine'
|
||||
descr = u'В Україні ♡ OpenStack!'
|
||||
ch = self.create_chassis(description=descr)['chassis']
|
||||
|
||||
self.assertEqual(ch['description'], descr)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_show_chassis(self):
|
||||
descr = data_utils.rand_name('test-chassis-')
|
||||
uuid = self.create_chassis(description=descr)['chassis']['uuid']
|
||||
|
||||
resp, chassis = self.client.show_chassis(uuid)
|
||||
|
||||
self.assertEqual(chassis['uuid'], uuid)
|
||||
self.assertEqual(chassis['description'], descr)
|
||||
|
||||
@test.attr(type="smoke")
|
||||
def test_list_chassis(self):
|
||||
created_ids = [self.create_chassis()['chassis']['uuid']
|
||||
for i in range(0, 5)]
|
||||
|
||||
resp, body = self.client.list_chassis()
|
||||
loaded_ids = [ch['uuid'] for ch in body['chassis']]
|
||||
|
||||
for i in created_ids:
|
||||
self.assertIn(i, loaded_ids)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_delete_chassis(self):
|
||||
uuid = self.create_chassis()['chassis']['uuid']
|
||||
|
||||
self.delete_chassis(uuid)
|
||||
|
||||
self.assertRaises(exc.NotFound, self.client.show_chassis, uuid)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_update_chassis(self):
|
||||
chassis_id = self.create_chassis()['chassis']['uuid']
|
||||
|
||||
new_description = data_utils.rand_name('new-description-')
|
||||
self.client.update_chassis(chassis_id, description=new_description)
|
||||
|
||||
resp, chassis = self.client.show_chassis(chassis_id)
|
||||
self.assertEqual(chassis['description'], new_description)
|
||||
97
tempest/api/baremetal/test_nodes.py
Normal file
97
tempest/api/baremetal/test_nodes.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.api.baremetal import base
|
||||
from tempest import exceptions as exc
|
||||
from tempest import test
|
||||
|
||||
|
||||
class TestNodes(base.BaseBaremetalTest):
|
||||
'''Tests for baremetal nodes.'''
|
||||
|
||||
def setUp(self):
|
||||
super(TestNodes, self).setUp()
|
||||
|
||||
self.chassis = self.create_chassis()['chassis']
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_create_node(self):
|
||||
params = {'cpu_arch': 'x86_64',
|
||||
'cpu_num': '12',
|
||||
'storage': '10240',
|
||||
'memory': '1024'}
|
||||
|
||||
node = self.create_node(self.chassis['uuid'], **params)['node']
|
||||
|
||||
for key in params:
|
||||
self.assertEqual(node['properties'][key], params[key])
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_delete_node(self):
|
||||
node = self.create_node(self.chassis['uuid'])['node']
|
||||
node_id = node['uuid']
|
||||
|
||||
resp = self.delete_node(node_id)
|
||||
|
||||
self.assertEqual(resp['status'], '204')
|
||||
self.assertRaises(exc.NotFound, self.client.show_node, node_id)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_show_node(self):
|
||||
params = {'cpu_arch': 'x86_64',
|
||||
'cpu_num': '4',
|
||||
'storage': '100',
|
||||
'memory': '512'}
|
||||
|
||||
created_node = self.create_node(self.chassis['uuid'], **params)['node']
|
||||
resp, loaded_node = self.client.show_node(created_node['uuid'])
|
||||
|
||||
for key, val in created_node.iteritems():
|
||||
if key not in ('created_at', 'updated_at'):
|
||||
self.assertEqual(loaded_node[key], val)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_list_nodes(self):
|
||||
uuids = [self.create_node(self.chassis['uuid'])['node']['uuid']
|
||||
for i in range(0, 5)]
|
||||
|
||||
resp, body = self.client.list_nodes()
|
||||
loaded_uuids = [n['uuid'] for n in body['nodes']]
|
||||
|
||||
for u in uuids:
|
||||
self.assertIn(u, loaded_uuids)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_update_node(self):
|
||||
props = {'cpu_arch': 'x86_64',
|
||||
'cpu_num': '12',
|
||||
'storage': '10',
|
||||
'memory': '128'}
|
||||
|
||||
node = self.create_node(self.chassis['uuid'], **props)['node']
|
||||
node_id = node['uuid']
|
||||
|
||||
new_props = {'cpu_arch': 'x86',
|
||||
'cpu_num': '1',
|
||||
'storage': '10000',
|
||||
'memory': '12300'}
|
||||
|
||||
self.client.update_node(node_id, properties=new_props)
|
||||
resp, node = self.client.show_node(node_id)
|
||||
|
||||
for name, value in six.iteritems(new_props):
|
||||
if name not in ('created_at', 'updated_at'):
|
||||
self.assertEqual(node['properties'][name], value)
|
||||
85
tempest/api/baremetal/test_ports.py
Normal file
85
tempest/api/baremetal/test_ports.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.api.baremetal import base
|
||||
from tempest.common.utils import data_utils
|
||||
from tempest import exceptions as exc
|
||||
from tempest import test
|
||||
|
||||
|
||||
class TestPorts(base.BaseBaremetalTest):
|
||||
"""Tests for ports."""
|
||||
|
||||
def setUp(self):
|
||||
super(TestPorts, self).setUp()
|
||||
|
||||
chassis = self.create_chassis()['chassis']
|
||||
self.node = self.create_node(chassis['uuid'])['node']
|
||||
|
||||
@test.attr(type='smoke')
|
||||
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)['port']
|
||||
|
||||
self.assertEqual(port['address'], address)
|
||||
self.assertEqual(port['node_uuid'], node_id)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_delete_port(self):
|
||||
node_id = self.node['uuid']
|
||||
port_id = self.create_port(node_id=node_id)['port']['uuid']
|
||||
|
||||
resp = self.delete_port(port_id)
|
||||
|
||||
self.assertEqual(resp['status'], '204')
|
||||
self.assertRaises(exc.NotFound, self.client.show_port, port_id)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_show_port(self):
|
||||
node_id = self.node['uuid']
|
||||
address = data_utils.rand_mac_address()
|
||||
|
||||
port_id = self.create_port(node_id=node_id,
|
||||
address=address)['port']['uuid']
|
||||
|
||||
resp, port = self.client.show_port(port_id)
|
||||
|
||||
self.assertEqual(port['uuid'], port_id)
|
||||
self.assertEqual(port['address'], address)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_list_ports(self):
|
||||
node_id = self.node['uuid']
|
||||
|
||||
uuids = [self.create_port(node_id=node_id)['port']['uuid']
|
||||
for i in range(0, 5)]
|
||||
|
||||
resp, body = self.client.list_ports()
|
||||
loaded_uuids = [p['uuid'] for p in body['ports']]
|
||||
|
||||
for u in uuids:
|
||||
self.assertIn(u, loaded_uuids)
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_update_port(self):
|
||||
node_id = self.node['uuid']
|
||||
port_id = self.create_port(node_id=node_id)['port']['uuid']
|
||||
|
||||
new_address = data_utils.rand_mac_address()
|
||||
self.client.update_port(port_id, address=new_address)
|
||||
|
||||
resp, body = self.client.show_port(port_id)
|
||||
self.assertEqual(body['address'], new_address)
|
||||
42
tempest/api/baremetal/test_ports_negative.py
Normal file
42
tempest/api/baremetal/test_ports_negative.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.api.baremetal import base
|
||||
from tempest.common.utils import data_utils
|
||||
from tempest import exceptions as exc
|
||||
from tempest import test
|
||||
|
||||
|
||||
class TestPortsNegative(base.BaseBaremetalTest):
|
||||
"""Negative tests for ports."""
|
||||
|
||||
def setUp(self):
|
||||
super(TestPortsNegative, self).setUp()
|
||||
|
||||
chassis = self.create_chassis()['chassis']
|
||||
self.node = self.create_node(chassis['uuid'])['node']
|
||||
|
||||
@test.attr(type='negative')
|
||||
def test_create_port_invalid_mac(self):
|
||||
node_id = self.node['uuid']
|
||||
address = 'not an uuid'
|
||||
|
||||
self.assertRaises(exc.BadRequest,
|
||||
self.create_port, node_id=node_id, address=address)
|
||||
|
||||
@test.attr(type='negative')
|
||||
def test_create_port_wrong_node_id(self):
|
||||
node_id = str(data_utils.rand_uuid())
|
||||
|
||||
self.assertRaises(exc.BadRequest, self.create_port, node_id=node_id)
|
||||
@@ -18,6 +18,8 @@
|
||||
from tempest import config
|
||||
from tempest import exceptions
|
||||
from tempest.openstack.common import log as logging
|
||||
from tempest.services.baremetal.v1.client_json import BaremetalClientJSON
|
||||
from tempest.services.baremetal.v1.client_xml import BaremetalClientXML
|
||||
from tempest.services import botoclients
|
||||
from tempest.services.compute.json.aggregates_client import \
|
||||
AggregatesClientJSON
|
||||
@@ -232,6 +234,7 @@ class Manager(object):
|
||||
if interface == 'xml':
|
||||
self.certificates_client = CertificatesClientXML(*client_args)
|
||||
self.certificates_v3_client = CertificatesV3ClientXML(*client_args)
|
||||
self.baremetal_client = BaremetalClientXML(*client_args)
|
||||
self.servers_client = ServersClientXML(*client_args)
|
||||
self.servers_v3_client = ServersV3ClientXML(*client_args)
|
||||
self.limits_client = LimitsClientXML(*client_args)
|
||||
@@ -294,6 +297,7 @@ class Manager(object):
|
||||
self.certificates_client = CertificatesClientJSON(*client_args)
|
||||
self.certificates_v3_client = CertificatesV3ClientJSON(
|
||||
*client_args)
|
||||
self.baremetal_client = BaremetalClientJSON(*client_args)
|
||||
self.servers_client = ServersClientJSON(*client_args)
|
||||
self.servers_v3_client = ServersV3ClientJSON(*client_args)
|
||||
self.limits_client = LimitsClientJSON(*client_args)
|
||||
|
||||
@@ -40,6 +40,21 @@ def rand_int_id(start=0, end=0x7fffffff):
|
||||
return random.randint(start, end)
|
||||
|
||||
|
||||
def rand_mac_address():
|
||||
"""Generate an Ethernet MAC address."""
|
||||
# NOTE(vish): We would prefer to use 0xfe here to ensure that linux
|
||||
# bridge mac addresses don't change, but it appears to
|
||||
# conflict with libvirt, so we use the next highest octet
|
||||
# that has the unicast and locally administered bits set
|
||||
# properly: 0xfa.
|
||||
# Discussion: https://bugs.launchpad.net/nova/+bug/921838
|
||||
mac = [0xfa, 0x16, 0x3e,
|
||||
random.randint(0x00, 0xff),
|
||||
random.randint(0x00, 0xff),
|
||||
random.randint(0x00, 0xff)]
|
||||
return ':'.join(["%02x" % x for x in mac])
|
||||
|
||||
|
||||
def build_url(host, port, api_version=None, path=None,
|
||||
params=None, use_ssl=False):
|
||||
"""Build the request URL from given host, port, path and parameters."""
|
||||
|
||||
@@ -644,6 +644,9 @@ ServiceAvailableGroup = [
|
||||
cfg.BoolOpt('savanna',
|
||||
default=False,
|
||||
help="Whether or not Savanna is expected to be available"),
|
||||
cfg.BoolOpt('ironic',
|
||||
default=False,
|
||||
help="Whether or not Ironic is expected to be available"),
|
||||
]
|
||||
|
||||
debug_group = cfg.OptGroup(name="debug",
|
||||
@@ -656,6 +659,16 @@ DebugGroup = [
|
||||
]
|
||||
|
||||
|
||||
baremetal_group = cfg.OptGroup(name='baremetal',
|
||||
title='Baremetal provisioning service options')
|
||||
|
||||
BaremetalGroup = [
|
||||
cfg.StrOpt('catalog_type',
|
||||
default='baremetal',
|
||||
help="Catalog type of the baremetal provisioning service."),
|
||||
]
|
||||
|
||||
|
||||
# this should never be called outside of this class
|
||||
class TempestConfigPrivate(object):
|
||||
"""Provides OpenStack configuration information."""
|
||||
@@ -721,6 +734,8 @@ class TempestConfigPrivate(object):
|
||||
register_opt_group(cfg.CONF, service_available_group,
|
||||
ServiceAvailableGroup)
|
||||
register_opt_group(cfg.CONF, debug_group, DebugGroup)
|
||||
register_opt_group(cfg.CONF, baremetal_group, BaremetalGroup)
|
||||
|
||||
self.compute = cfg.CONF.compute
|
||||
self.compute_feature_enabled = cfg.CONF['compute-feature-enabled']
|
||||
self.identity = cfg.CONF.identity
|
||||
@@ -743,6 +758,8 @@ class TempestConfigPrivate(object):
|
||||
self.scenario = cfg.CONF.scenario
|
||||
self.service_available = cfg.CONF.service_available
|
||||
self.debug = cfg.CONF.debug
|
||||
self.baremetal = cfg.CONF.baremetal
|
||||
|
||||
if not self.compute_admin.username:
|
||||
self.compute_admin.username = self.identity.admin_username
|
||||
self.compute_admin.password = self.identity.admin_password
|
||||
|
||||
0
tempest/services/baremetal/__init__.py
Normal file
0
tempest/services/baremetal/__init__.py
Normal file
197
tempest/services/baremetal/base.py
Normal file
197
tempest/services/baremetal/base.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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
|
||||
import json
|
||||
|
||||
import six
|
||||
|
||||
from tempest.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.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config, username, password, auth_url, tenant_name=None):
|
||||
super(BaremetalClient, self).__init__(config, username, password,
|
||||
auth_url, tenant_name)
|
||||
self.service = self.config.baremetal.catalog_type
|
||||
self.uri_prefix = ''
|
||||
|
||||
def serialize(self, object_type, object_dict):
|
||||
"""Serialize an Ironic object."""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def deserialize(self, object_str):
|
||||
"""Deserialize an Ironic object."""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
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.
|
||||
:return: 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, **kw):
|
||||
"""
|
||||
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 **kw: Attributes and new values for them.
|
||||
:return: A JSON path that sets values of the specified attributes to
|
||||
the new ones.
|
||||
|
||||
"""
|
||||
def get_change(kw, path='/'):
|
||||
for name, value in six.iteritems(kw):
|
||||
if isinstance(value, dict):
|
||||
for ch in get_change(value, path + '%s/' % name):
|
||||
yield ch
|
||||
else:
|
||||
yield {'path': path + name,
|
||||
'value': value,
|
||||
'op': 'replace'}
|
||||
|
||||
patch = [ch for ch in get_change(kw)
|
||||
if ch['path'].lstrip('/') in allowed_attributes]
|
||||
|
||||
return patch
|
||||
|
||||
def _list_request(self, resource, permanent=False):
|
||||
"""
|
||||
Get the list of objects of the specified type.
|
||||
|
||||
:param resource: The name of the REST resource, e.g., 'nodes'.
|
||||
:return: A tuple with the server response and deserialized JSON list
|
||||
of objects
|
||||
|
||||
"""
|
||||
uri = self._get_uri(resource, permanent=permanent)
|
||||
|
||||
resp, body = self.get(uri, self.headers)
|
||||
|
||||
return resp, self.deserialize(body)
|
||||
|
||||
def _show_request(self, resource, uuid, permanent=False):
|
||||
"""
|
||||
Gets a specific object of the specified type.
|
||||
|
||||
:param uuid: Unique identifier of the object in UUID format.
|
||||
:return: Serialized object as a dictionary.
|
||||
|
||||
"""
|
||||
uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
|
||||
resp, body = self.get(uri, self.headers)
|
||||
|
||||
return resp, self.deserialize(body)
|
||||
|
||||
def _create_request(self, resource, object_type, 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.
|
||||
:return: A tuple with the server response and the deserialized created
|
||||
object.
|
||||
|
||||
"""
|
||||
body = self.serialize(object_type, object_dict)
|
||||
uri = self._get_uri(resource)
|
||||
|
||||
resp, body = self.post(uri, headers=self.headers, body=body)
|
||||
|
||||
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.
|
||||
:return: A tuple with the server response and the response body.
|
||||
|
||||
"""
|
||||
uri = self._get_uri(resource, uuid)
|
||||
|
||||
resp, body = self.delete(uri, self.headers)
|
||||
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.
|
||||
:return: 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, headers=self.headers, body=patch_body)
|
||||
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'.
|
||||
:return: Serialized description of API resources.
|
||||
|
||||
"""
|
||||
return self._list_request(version, permanent=True)
|
||||
0
tempest/services/baremetal/v1/__init__.py
Normal file
0
tempest/services/baremetal/v1/__init__.py
Normal file
209
tempest/services/baremetal/v1/base_v1.py
Normal file
209
tempest/services/baremetal/v1/base_v1.py
Normal file
@@ -0,0 +1,209 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.services.baremetal import base
|
||||
|
||||
|
||||
class BaremetalClientV1(base.BaremetalClient):
|
||||
"""
|
||||
Base Tempest REST client for Ironic API v1.
|
||||
|
||||
Specific implementations must implement serialize and deserialize
|
||||
methods in order to send requests to Ironic.
|
||||
|
||||
"""
|
||||
def __init__(self, config, username, password, auth_url, tenant_name=None):
|
||||
super(BaremetalClientV1, self).__init__(config, username, password,
|
||||
auth_url, tenant_name)
|
||||
self.version = '1'
|
||||
self.uri_prefix = 'v%s' % self.version
|
||||
|
||||
@base.handle_errors
|
||||
def list_nodes(self):
|
||||
"""List all existing nodes."""
|
||||
return self._list_request('nodes')
|
||||
|
||||
@base.handle_errors
|
||||
def list_chassis(self):
|
||||
"""List all existing chassis."""
|
||||
return self._list_request('chassis')
|
||||
|
||||
@base.handle_errors
|
||||
def list_ports(self):
|
||||
"""List all existing ports."""
|
||||
return self._list_request('ports')
|
||||
|
||||
@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_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 create_node(self, chassis_id, **kwargs):
|
||||
"""
|
||||
Create a baremetal node with the specified parameters.
|
||||
|
||||
:param cpu_arch: CPU architecture of the node. Default: x86_64.
|
||||
:param cpu_num: Number of CPUs. Default: 8.
|
||||
:param storage: Disk size. Default: 1024.
|
||||
:param memory: 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'),
|
||||
'cpu_num': kwargs.get('cpu_num', 8),
|
||||
'storage': kwargs.get('storage', 1024),
|
||||
'memory': kwargs.get('memory', 4096)},
|
||||
'driver': kwargs.get('driver', 'fake')}
|
||||
|
||||
return self._create_request('nodes', 'node', 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', 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. Default: 01:23:45:67:89:0A.
|
||||
:return: A tuple with the server response and the created port.
|
||||
|
||||
"""
|
||||
port = {'address': kwargs.get('address', '01:23:45:67:89:0A'),
|
||||
'node_uuid': node_id}
|
||||
|
||||
return self._create_request('ports', 'port', 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/cpu_num',
|
||||
'properties/storage',
|
||||
'properties/memory',
|
||||
'driver')
|
||||
|
||||
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, **kwargs):
|
||||
"""
|
||||
Update the specified port.
|
||||
|
||||
:param uuid: The unique identifier of the port.
|
||||
:return: A tuple with the server response and the updated port.
|
||||
|
||||
"""
|
||||
port_attributes = ('address',)
|
||||
patch = self._make_patch(port_attributes, **kwargs)
|
||||
|
||||
return self._patch_request('ports', uuid, patch)
|
||||
28
tempest/services/baremetal/v1/client_json.py
Normal file
28
tempest/services/baremetal/v1/client_json.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
|
||||
from tempest.services.baremetal.v1 import base_v1
|
||||
|
||||
|
||||
class BaremetalClientJSON(base_v1.BaremetalClientV1):
|
||||
"""Tempest REST client for Ironic JSON API v1."""
|
||||
|
||||
def __init__(self, config, username, password, auth_url, tenant_name=None):
|
||||
super(BaremetalClientJSON, self).__init__(config, username, password,
|
||||
auth_url, tenant_name)
|
||||
|
||||
self.serialize = lambda obj_type, obj_body: json.dumps(obj_body)
|
||||
self.deserialize = json.loads
|
||||
57
tempest/services/baremetal/v1/client_xml.py
Normal file
57
tempest/services/baremetal/v1/client_xml.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 rest_client
|
||||
from tempest.services.baremetal.v1 import base_v1 as base
|
||||
from tempest.services.compute.xml import common as xml
|
||||
|
||||
|
||||
class BaremetalClientXML(rest_client.RestClientXML, base.BaremetalClientV1):
|
||||
"""Tempest REST client for Ironic XML API v1."""
|
||||
|
||||
def __init__(self, config, username, password, auth_url, tenant_name=None):
|
||||
super(BaremetalClientXML, self).__init__(config, username, password,
|
||||
auth_url, tenant_name)
|
||||
|
||||
self.serialize = self.json_to_xml
|
||||
self.deserialize = xml.xml_to_json
|
||||
|
||||
def json_to_xml(self, object_type, object_dict):
|
||||
"""
|
||||
Brainlessly converts a specification of an object to XML string.
|
||||
|
||||
:param object_type: Kind of the object.
|
||||
:param object_dict: Specification of the object attributes as a dict.
|
||||
:return: An XML string that corresponds to the specification.
|
||||
|
||||
"""
|
||||
root = xml.Element(object_type)
|
||||
|
||||
for attr_name, value in object_dict:
|
||||
# Handle nested dictionaries
|
||||
if isinstance(value, dict):
|
||||
value = self.json_to_xml(attr_name, value)
|
||||
|
||||
root.append(xml.Element(attr_name, value))
|
||||
|
||||
return str(xml.Document(root))
|
||||
|
||||
def _patch_request(self, resource_name, uuid, patch_object):
|
||||
"""Changes Content-Type header to application/json for jsonpatch."""
|
||||
|
||||
self.headers['Content-Type'] = 'application/json'
|
||||
try:
|
||||
super(self)._patch_request(self, resource_name, uuid, patch_object)
|
||||
finally:
|
||||
self.headers['Content-Type'] = 'application/xml'
|
||||
Reference in New Issue
Block a user