Merge "Verify more information for member in lbaas api tests"
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..c9b6467
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,4 @@
+[run]
+branch = True
+source = tempest
+omit = tempest/tests/*,tempest/openstack/*
diff --git a/.gitignore b/.gitignore
index 28a9b9c..1777cb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,5 @@
build
.testrepository
.coverage*
+!.coveragerc
cover/
diff --git a/.testr.conf b/.testr.conf
index abaf14a..4f6e0b3 100644
--- a/.testr.conf
+++ b/.testr.conf
@@ -2,6 +2,7 @@
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-500} \
+ OS_TEST_LOCK_PATH=${OS_TEST_LOCK_PATH:-${TMPDIR:-'/tmp'}} \
${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./tempest/test_discover} $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list
diff --git a/HACKING.rst b/HACKING.rst
index c0df0fb..8652971 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -120,13 +120,14 @@
- A json schema: defines properties for a request.
After that a test class must be added to automatically generate test scenarios
-out of the given interface description:
+out of the given interface description::
+
+ load_tests = test.NegativeAutoTest.load_tests
class SampeTestNegativeTestJSON(<your base class>, test.NegativeAutoTest):
_interface = 'json'
_service = 'compute'
- _schema_file = 'compute/servers/get_console_output.json'
- scenarios = test.NegativeAutoTest.generate_scenario(_schema_file)
+ _schema_file = <your Schema file>
Negative tests must be marked with a negative attribute::
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 761a077..70c791b 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -101,14 +101,32 @@
# Options defined in tempest.config
#
-# Catalog type of the baremetal provisioning service. (string
+# Catalog type of the baremetal provisioning service (string
# value)
#catalog_type=baremetal
+# Whether the Ironic nova-compute driver is enabled (boolean
+# value)
+#driver_enabled=false
+
# The endpoint type to use for the baremetal provisioning
-# service. (string value)
+# service (string value)
#endpoint_type=publicURL
+# Timeout for Ironic node to completely provision (integer
+# value)
+#active_timeout=300
+
+# Timeout for association of Nova instance and Ironic node
+# (integer value)
+#association_timeout=10
+
+# Timeout for Ironic power transitions. (integer value)
+#power_timeout=20
+
+# Timeout for unprovisioning an Ironic node. (integer value)
+#unprovision_timeout=20
+
[boto]
@@ -193,7 +211,7 @@
# admin credentials are known. (boolean value)
#allow_tenant_isolation=false
-# Valid secondary image reference to be used in tests. (string
+# Valid primary image reference to be used in tests. (string
# value)
#image_ref={$IMAGE_ID}
@@ -261,7 +279,7 @@
# IP version used for SSH connections. (integer value)
#ip_version_for_ssh=4
-# Dose the SSH uses Floating IP? (boolean value)
+# Does SSH use Floating IPs? (boolean value)
#use_floatingip_for_ssh=true
# Catalog type of the Compute service. (string value)
@@ -421,6 +439,22 @@
# Enable diagnostic commands (boolean value)
#enable=true
+# A regex to determine which requests should be traced. This
+# is a regex to match the caller for rest client requests to
+# be able to selectively trace calls out of specific classes
+# and methods. It largely exists for test development, and is
+# not expected to be used in a real deploy of tempest. This
+# will be matched against the discovered ClassName:method in
+# the test environment. Expected values for this field are:
+# * ClassName:test_method_name - traces one test_method *
+# ClassName:setUp(Class) - traces specific setup functions *
+# ClassName:tearDown(Class) - traces specific teardown
+# functions * ClassName:_run_cleanups - traces the cleanup
+# functions If nothing is specified, this feature is not
+# enabled. To trace everything specify .* as the regex.
+# (string value)
+#trace_requests=
+
[identity]
@@ -718,7 +752,7 @@
# Timeout in seconds to wait for a stack to build. (integer
# value)
-#build_timeout=600
+#build_timeout=1200
# Instance type for tests. Needs to be big enough for a full
# OS plus the test workload (string value)
@@ -736,6 +770,10 @@
# (integer value)
#max_template_size=524288
+# Value must match heat configuration of the same name.
+# (integer value)
+#max_resources_per_stack=1000
+
[queuing]
@@ -940,6 +978,10 @@
# value)
#disk_format=raw
+# Default size in GB for volumes created by volumes tests
+# (integer value)
+#volume_size=1
+
[volume-feature-enabled]
@@ -954,6 +996,9 @@
# Runs Cinder volumes backup test (boolean value)
#backup=true
+# Runs Cinder volume snapshot test (boolean value)
+#snapshot=true
+
# A list of enabled volume extensions with a special entry all
# which indicates every extension is enabled (list value)
#api_extensions=all
diff --git a/requirements.txt b/requirements.txt
index a18b092..e97eece 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-pbr>=0.6,<1.0
+pbr>=0.6,!=0.7,<1.0
anyjson>=0.3.3
httplib2>=0.7.5
jsonschema>=2.0.0,<3.0.0
@@ -8,15 +8,16 @@
paramiko>=1.9.0
netaddr>=0.7.6
python-glanceclient>=0.9.0
-python-keystoneclient>=0.6.0
+python-keystoneclient>=0.7.0
python-novaclient>=2.17.0
python-neutronclient>=2.3.4,<3
python-cinderclient>=1.0.6
python-heatclient>=0.2.3
+python-ironicclient
python-saharaclient>=0.6.0
python-swiftclient>=1.6
testresources>=0.2.4
-keyring>=1.6.1,<2.0,>=2.1
+keyring>=2.1
testrepository>=0.0.18
oslo.config>=1.2.0
six>=1.5.2
diff --git a/tempest/api/baremetal/base.py b/tempest/api/baremetal/base.py
index 2e745f8..021adaf 100644
--- a/tempest/api/baremetal/base.py
+++ b/tempest/api/baremetal/base.py
@@ -106,17 +106,20 @@
@classmethod
@creates('port')
- def create_port(cls, node_id, address=None):
+ def create_port(cls, node_id, address, extra=None, uuid=None):
"""
Wrapper utility for creating test ports.
- :param address: MAC address of the port. If not supplied, a random
- value will be generated.
+ :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.
"""
- address = address or data_utils.rand_mac_address()
- resp, body = cls.client.create_port(address=address, node_id=node_id)
+ extra = extra or {}
+ resp, body = cls.client.create_port(address=address, node_id=node_id,
+ extra=extra, uuid=uuid)
return {'port': body, 'response': resp}
@@ -170,3 +173,12 @@
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)
diff --git a/tempest/api/baremetal/test_drivers.py b/tempest/api/baremetal/test_drivers.py
new file mode 100644
index 0000000..445ca60
--- /dev/null
+++ b/tempest/api/baremetal/test_drivers.py
@@ -0,0 +1,26 @@
+# 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.api.baremetal import base
+from tempest import test
+
+
+class TestDrivers(base.BaseBaremetalTest):
+ """Tests for drivers."""
+
+ @test.attr(type="smoke")
+ def test_list_drivers(self):
+ resp, drivers = self.client.list_drivers()
+ self.assertEqual('200', resp['status'])
+ self.assertIn('fake', [d['name'] for d in drivers['drivers']])
diff --git a/tempest/api/baremetal/test_nodestates.py b/tempest/api/baremetal/test_nodestates.py
new file mode 100644
index 0000000..c658d7f
--- /dev/null
+++ b/tempest/api/baremetal/test_nodestates.py
@@ -0,0 +1,33 @@
+# 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.api.baremetal import base
+from tempest import test
+
+
+class TestNodeStates(base.BaseBaremetalTest):
+ """Tests for baremetal NodeStates."""
+
+ @classmethod
+ def setUpClass(self):
+ super(TestNodeStates, self).setUpClass()
+ chassis = self.create_chassis()['chassis']
+ self.node = self.create_node(chassis['uuid'])['node']
+
+ @test.attr(type='smoke')
+ def test_list_nodestates(self):
+ resp, nodestates = self.client.list_nodestates(self.node['uuid'])
+ self.assertEqual('200', resp['status'])
+ for key in nodestates:
+ self.assertEqual(nodestates[key], self.node[key])
diff --git a/tempest/api/baremetal/test_ports.py b/tempest/api/baremetal/test_ports.py
index fb2acc7..8b76811 100644
--- a/tempest/api/baremetal/test_ports.py
+++ b/tempest/api/baremetal/test_ports.py
@@ -30,54 +30,268 @@
node_id = self.node['uuid']
address = data_utils.rand_mac_address()
- port = self.create_port(node_id=node_id, address=address)['port']
+ result = self.create_port(node_id=node_id, address=address)
- self.assertEqual(port['address'], address)
- self.assertEqual(port['node_uuid'], node_id)
+ port = result['port']
+
+ resp, body = self.client.show_port(port['uuid'])
+
+ self.assertEqual(200, resp.status)
+ self.assertEqual(port['uuid'], body['uuid'])
+ self.assertEqual(address, body['address'])
+ self.assertEqual({}, body['extra'])
+ self.assertEqual(node_id, body['node_uuid'])
+
+ @test.attr(type='smoke')
+ def test_create_port_specifying_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)
+
+ resp, body = self.client.show_port(uuid)
+
+ self.assertEqual(200, resp.status)
+ self.assertEqual(uuid, body['uuid'])
+ self.assertEqual(address, body['address'])
+ self.assertEqual({}, body['extra'])
+ self.assertEqual(node_id, body['node_uuid'])
+
+ @test.attr(type='smoke')
+ def test_create_port_with_extra(self):
+ node_id = self.node['uuid']
+ address = data_utils.rand_mac_address()
+ extra = {'key': 'value'}
+
+ result = self.create_port(node_id=node_id, address=address,
+ extra=extra)
+ port = result['port']
+
+ resp, body = self.client.show_port(port['uuid'])
+
+ self.assertEqual(200, resp.status)
+ self.assertEqual(port['uuid'], body['uuid'])
+ self.assertEqual(address, body['address'])
+ self.assertEqual(extra, body['extra'])
+ self.assertEqual(node_id, body['node_uuid'])
@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']
+ address = data_utils.rand_mac_address()
+ port_id = self.create_port(node_id=node_id, address=address)['port'][
+ 'uuid']
resp = self.delete_port(port_id)
- self.assertEqual(resp['status'], '204')
+ self.assertEqual(204, resp.status)
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()
+ extra = {'key': 'value'}
- port_id = self.create_port(node_id=node_id,
- address=address)['port']['uuid']
+ port_id = self.create_port(node_id=node_id, address=address,
+ extra=extra)['port']['uuid']
resp, port = self.client.show_port(port_id)
- self.assertEqual(port['uuid'], port_id)
- self.assertEqual(port['address'], address)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(port_id, port['uuid'])
+ self.assertEqual(address, port['address'])
+ self.assertEqual(extra, port['extra'])
+
+ @test.attr(type='smoke')
+ def test_show_port_with_links(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, body = self.client.show_port(port_id)
+
+ self.assertEqual(200, resp.status)
+ self.assertIn('links', body.keys())
+ self.assertEqual(2, len(body['links']))
+ self.assertIn(port_id, body['links'][0]['href'])
@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)]
+ uuids = [self.create_port(node_id=node_id,
+ address=data_utils.rand_mac_address())
+ ['port']['uuid'] for i in xrange(5)]
resp, body = self.client.list_ports()
+ self.assertEqual(200, resp.status)
loaded_uuids = [p['uuid'] for p in body['ports']]
- for u in uuids:
- self.assertIn(u, loaded_uuids)
+ for uuid in uuids:
+ self.assertIn(uuid, loaded_uuids)
+
+ # Verify self links.
+ for port in body['ports']:
+ self.validate_self_link('ports', port['uuid'],
+ port['links'][0]['href'])
@test.attr(type='smoke')
- def test_update_port(self):
+ def test_list_with_limit(self):
node_id = self.node['uuid']
- port_id = self.create_port(node_id=node_id)['port']['uuid']
+
+ for i in xrange(5):
+ self.create_port(node_id=node_id,
+ address=data_utils.rand_mac_address())
+
+ resp, body = self.client.list_ports(limit=3)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(3, len(body['ports']))
+
+ next_marker = body['ports'][-1]['uuid']
+ self.assertIn(next_marker, body['next'])
+
+ 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())
+ ['port']['uuid'] for i in range(0, 5)]
+
+ resp, body = self.client.list_ports_detail()
+ self.assertEqual(200, resp.status)
+
+ ports_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.attr(type='smoke')
+ 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_id = self.create_port(node_id=node_id, address=address,
+ extra=extra)['port']['uuid']
new_address = data_utils.rand_mac_address()
- self.client.update_port(port_id, address=new_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_id, patch)
resp, body = self.client.show_port(port_id)
- self.assertEqual(body['address'], new_address)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(new_address, body['address'])
+ self.assertEqual(new_extra, body['extra'])
+
+ @test.attr(type='smoke')
+ 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_id = self.create_port(node_id=node_id, address=address,
+ extra=extra)['port']['uuid']
+
+ # Removing one item from the collection
+ resp, _ = self.client.update_port(port_id, [{'path': '/extra/key2',
+ 'op': 'remove'}])
+ self.assertEqual(200, resp.status)
+ extra.pop('key2')
+ resp, body = self.client.show_port(port_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(extra, body['extra'])
+
+ # Removing the collection
+ resp, _ = self.client.update_port(port_id, [{'path': '/extra',
+ 'op': 'remove'}])
+ self.assertEqual(200, resp.status)
+ resp, body = self.client.show_port(port_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual({}, body['extra'])
+
+ # Assert nothing else was changed
+ self.assertEqual(node_id, body['node_uuid'])
+ self.assertEqual(address, body['address'])
+
+ @test.attr(type='smoke')
+ def test_update_port_add(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']
+
+ 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_id, patch)
+
+ resp, body = self.client.show_port(port_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(extra, body['extra'])
+
+ @test.attr(type='smoke')
+ def test_update_port_mixed_ops(self):
+ node_id = self.node['uuid']
+ address = data_utils.rand_mac_address()
+ extra = {'key1': 'value1', 'key2': 'value2'}
+
+ port_id = self.create_port(node_id=node_id, address=address,
+ extra=extra)['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']}]
+
+ self.client.update_port(port_id, patch)
+
+ resp, body = self.client.show_port(port_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(new_address, body['address'])
+ self.assertEqual(new_extra, body['extra'])
diff --git a/tempest/api/baremetal/test_ports_negative.py b/tempest/api/baremetal/test_ports_negative.py
index 6cb8812..4cbe00e 100644
--- a/tempest/api/baremetal/test_ports_negative.py
+++ b/tempest/api/baremetal/test_ports_negative.py
@@ -25,16 +25,346 @@
chassis = self.create_chassis()['chassis']
self.node = self.create_node(chassis['uuid'])['node']
- @test.attr(type='negative')
- def test_create_port_invalid_mac(self):
+ @test.attr(type=['negative', 'smoke'])
+ def test_create_port_malformed_mac(self):
node_id = self.node['uuid']
- address = 'not an uuid'
+ address = 'malformed:mac'
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())
+ @test.attr(type=['negative', 'smoke'])
+ def test_create_port_malformed_extra(self):
+ node_id = self.node['uuid']
+ address = data_utils.rand_mac_address()
+ extra = {'key': 0.123}
+ self.assertRaises(exc.BadRequest,
+ self.create_port, node_id=node_id,
+ address=address, extra=extra)
- self.assertRaises(exc.BadRequest, self.create_port, node_id=node_id)
+ @test.attr(type=['negative', 'smoke'])
+ def test_create_port_nonexsistent_node_id(self):
+ node_id = str(data_utils.rand_uuid())
+ address = data_utils.rand_mac_address()
+ self.assertRaises(exc.BadRequest, self.create_port, node_id=node_id,
+ address=address)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_show_port_malformed_uuid(self):
+ self.assertRaises(exc.BadRequest, self.client.show_port,
+ 'malformed:uuid')
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_show_port_nonexistent_uuid(self):
+ self.assertRaises(exc.NotFound, self.client.show_port,
+ data_utils.rand_uuid())
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_show_port_by_mac_not_allowed(self):
+ self.assertRaises(exc.BadRequest, self.client.show_port,
+ data_utils.rand_mac_address())
+
+ @test.attr(type=['negative', 'smoke'])
+ 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(exc.Conflict, self.create_port, node_id=node_id,
+ address=address, uuid=uuid)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_create_port_no_mandatory_field_node_id(self):
+ address = data_utils.rand_mac_address()
+
+ self.assertRaises(exc.BadRequest, self.create_port, node_id=None,
+ address=address)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_create_port_no_mandatory_field_mac(self):
+ node_id = self.node['uuid']
+
+ self.assertRaises(exc.BadRequest, self.create_port, node_id=node_id,
+ address=None)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_create_port_malformed_port_uuid(self):
+ node_id = self.node['uuid']
+ address = data_utils.rand_mac_address()
+ uuid = 'malformed:uuid'
+
+ self.assertRaises(exc.BadRequest, self.create_port, node_id=node_id,
+ address=address, uuid=uuid)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_create_port_malformed_node_id(self):
+ address = data_utils.rand_mac_address()
+ self.assertRaises(exc.BadRequest, self.create_port,
+ node_id='malformed:nodeid', address=address)
+
+ @test.attr(type=['negative', 'smoke'])
+ 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(exc.Conflict,
+ self.create_port, node_id=node_id,
+ address=address)
+
+ @test.attr(type=['negative', 'smoke'])
+ 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(exc.BadRequest,
+ self.client.update_port, address,
+ patch)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_nonexistent(self):
+ node_id = self.node['uuid']
+ address = data_utils.rand_mac_address()
+ extra = {'key': 'value'}
+
+ port_id = self.create_port(node_id=node_id, address=address,
+ extra=extra)['port']['uuid']
+ self.client.delete_port(port_id)
+
+ patch = [{'path': '/extra/key',
+ 'op': 'replace',
+ 'value': 'new-value'}]
+ self.assertRaises(exc.NotFound,
+ self.client.update_port, port_id, patch)
+
+ @test.attr(type=['negative', 'smoke'])
+ 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(exc.BadRequest, self.client.update_port,
+ uuid='malformed:uuid',
+ patch=[{'path': '/address', 'op': 'replace',
+ 'value': new_address}])
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_add_malformed_extra(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']
+
+ self.assertRaises(exc.BadRequest, self.client.update_port, port_id,
+ [{'path': '/extra/key', ' op': 'add',
+ 'value': 0.123}])
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_add_whole_malformed_extra(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']
+
+ self.assertRaises(exc.BadRequest, self.client.update_port, port_id,
+ [{'path': '/extra',
+ 'op': 'add',
+ 'value': [1, 2, 3, 4, 'a']}])
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_add_nonexistent_property(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']
+ self.assertRaises(exc.BadRequest, self.client.update_port, port_id,
+ [{'path': '/nonexistent', ' op': 'add',
+ 'value': 'value'}])
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_replace_node_id_with_malformed(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']
+
+ patch = [{'path': '/node_uuid',
+ 'op': 'replace',
+ 'value': 'malformed:node_uuid'}]
+ self.assertRaises(exc.BadRequest,
+ self.client.update_port, port_id, patch)
+
+ @test.attr(type=['negative', 'smoke'])
+ 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()
+
+ self.create_port(node_id=node_id, address=address1)
+ port_id = self.create_port(node_id=node_id,
+ address=address2)['port']['uuid']
+ patch = [{'path': '/address',
+ 'op': 'replace',
+ 'value': address1}]
+ self.assertRaises(exc.Conflict,
+ self.client.update_port, port_id, patch)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_replace_node_id_with_nonexistent(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']
+
+ patch = [{'path': '/node_uuid',
+ 'op': 'replace',
+ 'value': data_utils.rand_uuid()}]
+ self.assertRaises(exc.BadRequest,
+ self.client.update_port, port_id, patch)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_replace_mac_with_malformed(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']
+ patch = [{'path': '/address',
+ 'op': 'replace',
+ 'value': 'malformed:mac'}]
+
+ self.assertRaises(exc.BadRequest,
+ self.client.update_port, port_id, patch)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_replace_extra_item_with_malformed(self):
+ node_id = self.node['uuid']
+ address = data_utils.rand_mac_address()
+ extra = {'key': 'value'}
+
+ port_id = self.create_port(node_id=node_id,
+ address=address,
+ extra=extra)['port']['uuid']
+ patch = [{'path': '/extra/key',
+ 'op': 'replace',
+ 'value': 0.123}]
+
+ self.assertRaises(exc.BadRequest,
+ self.client.update_port, port_id, patch)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_replace_whole_extra_with_malformed(self):
+ node_id = self.node['uuid']
+ address = data_utils.rand_mac_address()
+ extra = {'key': 'value'}
+
+ port_id = self.create_port(node_id=node_id,
+ address=address,
+ extra=extra)['port']['uuid']
+ patch = [{'path': '/extra',
+ 'op': 'replace',
+ 'value': [1, 2, 3, 4, 'a']}]
+
+ self.assertRaises(exc.BadRequest,
+ self.client.update_port, port_id, patch)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_replace_nonexistent_property(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']
+
+ patch = [{'path': '/nonexistent', ' op': 'replace', 'value': 'value'}]
+
+ self.assertRaises(exc.BadRequest,
+ self.client.update_port, port_id, patch)
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_remove_mandatory_field_mac(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']
+ self.assertRaises(exc.BadRequest, self.client.update_port, port_id,
+ [{'path': '/address', 'op': 'remove'}])
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_remove_mandatory_field_port_uuid(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']
+ self.assertRaises(exc.BadRequest, self.client.update_port, port_id,
+ [{'path': '/uuid', 'op': 'remove'}])
+
+ @test.attr(type=['negative', 'smoke'])
+ def test_update_port_remove_nonexistent_property(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']
+ self.assertRaises(exc.BadRequest, self.client.update_port, port_id,
+ [{'path': '/nonexistent', 'op': 'remove'}])
+
+ @test.attr(type=['negative', 'smoke'])
+ 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(exc.BadRequest, self.client.delete_port, address)
+
+ @test.attr(type=['negative', 'smoke'])
+ 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_id = self.create_port(node_id=node_id, address=address,
+ extra=extra)['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(exc.BadRequest, self.client.update_port, port_id,
+ patch)
+
+ # patch should not be applied
+ resp, body = self.client.show_port(port_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(address, body['address'])
+ self.assertEqual(extra, body['extra'])
diff --git a/tempest/api/compute/admin/test_agents.py b/tempest/api/compute/admin/test_agents.py
new file mode 100644
index 0000000..4808601
--- /dev/null
+++ b/tempest/api/compute/admin/test_agents.py
@@ -0,0 +1,123 @@
+# 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.api.compute import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.openstack.common import log
+from tempest import test
+
+LOG = log.getLogger(__name__)
+
+
+class AgentsAdminTestJSON(base.BaseV2ComputeAdminTest):
+ """
+ Tests Agents API
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super(AgentsAdminTestJSON, cls).setUpClass()
+ cls.client = cls.os_adm.agents_client
+
+ def setUp(self):
+ super(AgentsAdminTestJSON, self).setUp()
+ params = self._param_helper(
+ hypervisor='common', os='linux', architecture='x86_64',
+ version='7.0', url='xxx://xxxx/xxx/xxx',
+ md5hash='add6bb58e139be103324d04d82d8f545')
+ resp, body = self.client.create_agent(**params)
+ self.assertEqual(200, resp.status)
+ self.agent_id = body['agent_id']
+
+ def tearDown(self):
+ try:
+ self.client.delete_agent(self.agent_id)
+ except exceptions.NotFound:
+ pass
+ except Exception:
+ LOG.exception('Exception raised deleting agent %s', self.agent_id)
+ super(AgentsAdminTestJSON, self).tearDown()
+
+ def _param_helper(self, **kwargs):
+ rand_key = 'architecture'
+ if rand_key in kwargs:
+ # NOTE: The rand_name is for avoiding agent conflicts.
+ # If you try to create an agent with the same hypervisor,
+ # os and architecture as an exising agent, Nova will return
+ # an HTTPConflict or HTTPServerError.
+ kwargs[rand_key] = data_utils.rand_name(kwargs[rand_key])
+ return kwargs
+
+ @test.attr(type='gate')
+ def test_create_agent(self):
+ # Create an agent.
+ params = self._param_helper(
+ hypervisor='kvm', os='win', architecture='x86',
+ version='7.0', url='xxx://xxxx/xxx/xxx',
+ md5hash='add6bb58e139be103324d04d82d8f545')
+ resp, body = self.client.create_agent(**params)
+ self.assertEqual(200, resp.status)
+ self.addCleanup(self.client.delete_agent, body['agent_id'])
+ for expected_item, value in params.items():
+ self.assertEqual(value, body[expected_item])
+
+ @test.attr(type='gate')
+ def test_update_agent(self):
+ # Update an agent.
+ params = self._param_helper(
+ version='8.0', url='xxx://xxxx/xxx/xxx2',
+ md5hash='add6bb58e139be103324d04d82d8f547')
+ resp, body = self.client.update_agent(self.agent_id, **params)
+ self.assertEqual(200, resp.status)
+ for expected_item, value in params.items():
+ self.assertEqual(value, body[expected_item])
+
+ @test.attr(type='gate')
+ def test_delete_agent(self):
+ # Delete an agent.
+ resp, _ = self.client.delete_agent(self.agent_id)
+ self.assertEqual(200, resp.status)
+
+ # Verify the list doesn't contain the deleted agent.
+ resp, agents = self.client.list_agents()
+ self.assertEqual(200, resp.status)
+ self.assertNotIn(self.agent_id, map(lambda x: x['agent_id'], agents))
+
+ @test.attr(type='gate')
+ def test_list_agents(self):
+ # List all agents.
+ resp, agents = self.client.list_agents()
+ self.assertEqual(200, resp.status)
+ self.assertTrue(len(agents) > 0, 'Cannot get any agents.(%s)' % agents)
+ self.assertIn(self.agent_id, map(lambda x: x['agent_id'], agents))
+
+ @test.attr(type='gate')
+ def test_list_agents_with_filter(self):
+ # List the agent builds by the filter.
+ params = self._param_helper(
+ hypervisor='xen', os='linux', architecture='x86',
+ version='7.0', url='xxx://xxxx/xxx/xxx1',
+ md5hash='add6bb58e139be103324d04d82d8f546')
+ resp, agent_xen = self.client.create_agent(**params)
+ self.assertEqual(200, resp.status)
+ self.addCleanup(self.client.delete_agent, agent_xen['agent_id'])
+
+ agent_id_xen = agent_xen['agent_id']
+ params_filter = {'hypervisor': agent_xen['hypervisor']}
+ resp, agents = self.client.list_agents(params_filter)
+ self.assertEqual(200, resp.status)
+ self.assertTrue(len(agents) > 0, 'Cannot get any agents.(%s)' % agents)
+ self.assertIn(agent_id_xen, map(lambda x: x['agent_id'], agents))
+ self.assertNotIn(self.agent_id, map(lambda x: x['agent_id'], agents))
diff --git a/tempest/api/compute/admin/test_aggregates.py b/tempest/api/compute/admin/test_aggregates.py
index fb249e5..c2376c9 100644
--- a/tempest/api/compute/admin/test_aggregates.py
+++ b/tempest/api/compute/admin/test_aggregates.py
@@ -16,6 +16,7 @@
from tempest.api.compute import base
from tempest.common import tempest_fixtures as fixtures
from tempest.common.utils import data_utils
+from tempest import exceptions
from tempest import test
@@ -39,11 +40,20 @@
filter(lambda y: y['service'] == 'compute', hosts_all))
cls.host = hosts[0]
+ def _try_delete_aggregate(self, aggregate_id):
+ # delete aggregate, if it exists
+ try:
+ self.client.delete_aggregate(aggregate_id)
+ # if aggregate not found, it depict it was deleted in the test
+ except exceptions.NotFound:
+ pass
+
@test.attr(type='gate')
def test_aggregate_create_delete(self):
# Create and delete an aggregate.
aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
resp, aggregate = self.client.create_aggregate(name=aggregate_name)
+ self.addCleanup(self._try_delete_aggregate, aggregate['id'])
self.assertEqual(200, resp.status)
self.assertEqual(aggregate_name, aggregate['name'])
self.assertIsNone(aggregate['availability_zone'])
@@ -59,6 +69,7 @@
az_name = data_utils.rand_name(self.az_name_prefix)
resp, aggregate = self.client.create_aggregate(
name=aggregate_name, availability_zone=az_name)
+ self.addCleanup(self._try_delete_aggregate, aggregate['id'])
self.assertEqual(200, resp.status)
self.assertEqual(aggregate_name, aggregate['name'])
self.assertEqual(az_name, aggregate['availability_zone'])
diff --git a/tempest/api/compute/admin/test_flavors.py b/tempest/api/compute/admin/test_flavors.py
index 759585e..111ac9c 100644
--- a/tempest/api/compute/admin/test_flavors.py
+++ b/tempest/api/compute/admin/test_flavors.py
@@ -170,7 +170,6 @@
flag = True
self.assertTrue(flag)
- @test.skip_because(bug='1286297')
@test.attr(type='gate')
def test_list_non_public_flavor(self):
# Create a flavor with os-flavor-access:is_public false.
diff --git a/tempest/api/compute/admin/test_flavors_access.py b/tempest/api/compute/admin/test_flavors_access.py
index aa0138f..193d415 100644
--- a/tempest/api/compute/admin/test_flavors_access.py
+++ b/tempest/api/compute/admin/test_flavors_access.py
@@ -46,7 +46,6 @@
cls.vcpus = 1
cls.disk = 10
- @test.skip_because(bug='1286297')
@test.attr(type='gate')
def test_flavor_access_list_with_private_flavor(self):
# Test to make sure that list flavor access on a newly created
diff --git a/tempest/api/compute/admin/test_flavors_negative.py b/tempest/api/compute/admin/test_flavors_negative.py
index 162e419..b37d32c 100644
--- a/tempest/api/compute/admin/test_flavors_negative.py
+++ b/tempest/api/compute/admin/test_flavors_negative.py
@@ -101,13 +101,9 @@
self.flavor_ref_alt)
+@test.SimpleNegativeAutoTest
class FlavorCreateNegativeTestJSON(base.BaseV2ComputeAdminTest,
test.NegativeAutoTest):
_interface = 'json'
_service = 'compute'
_schema_file = 'compute/admin/flavor_create.json'
-
- @test.attr(type=['negative', 'gate'])
- def test_create_flavor(self):
- # flavor details are not returned for non-existent flavors
- self.execute(self._schema_file)
diff --git a/tempest/api/compute/admin/test_migrations.py b/tempest/api/compute/admin/test_migrations.py
new file mode 100644
index 0000000..514f1fa
--- /dev/null
+++ b/tempest/api/compute/admin/test_migrations.py
@@ -0,0 +1,55 @@
+# 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.
+
+import testtools
+
+from tempest.api.compute import base
+from tempest import config
+from tempest import test
+
+CONF = config.CONF
+
+
+class MigrationsAdminTest(base.BaseV2ComputeAdminTest):
+
+ @classmethod
+ def setUpClass(cls):
+ super(MigrationsAdminTest, cls).setUpClass()
+ cls.client = cls.os_adm.migrations_client
+
+ @test.attr(type='gate')
+ def test_list_migrations(self):
+ # Admin can get the migrations list
+ resp, _ = self.client.list_migrations()
+ self.assertEqual(200, resp.status)
+
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
+ @test.attr(type='gate')
+ def test_list_migrations_in_flavor_resize_situation(self):
+ # Admin can get the migrations list which contains the resized server
+ resp, server = self.create_test_server(wait_until="ACTIVE")
+ server_id = server['id']
+
+ resp, _ = self.servers_client.resize(server_id, self.flavor_ref_alt)
+ self.assertEqual(202, resp.status)
+ self.servers_client.wait_for_server_status(server_id, 'VERIFY_RESIZE')
+ self.servers_client.confirm_resize(server_id)
+ self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
+
+ resp, body = self.client.list_migrations()
+ self.assertEqual(200, resp.status)
+
+ instance_uuids = [x['instance_uuid'] for x in body]
+ self.assertIn(server_id, instance_uuids)
diff --git a/tempest/api/compute/admin/test_quotas.py b/tempest/api/compute/admin/test_quotas.py
index 09c7274..32e0478 100644
--- a/tempest/api/compute/admin/test_quotas.py
+++ b/tempest/api/compute/admin/test_quotas.py
@@ -76,21 +76,38 @@
# TODO(afazekas): merge these test cases
@test.attr(type='gate')
def test_get_updated_quotas(self):
- # Verify that GET shows the updated quota set
+ # Verify that GET shows the updated quota set of tenant
tenant_name = data_utils.rand_name('cpu_quota_tenant_')
tenant_desc = tenant_name + '-desc'
identity_client = self.os_adm.identity_client
_, tenant = identity_client.create_tenant(name=tenant_name,
description=tenant_desc)
tenant_id = tenant['id']
- self.addCleanup(identity_client.delete_tenant,
- tenant_id)
+ self.addCleanup(identity_client.delete_tenant, tenant_id)
- self.adm_client.update_quota_set(tenant_id,
- ram='5120')
+ self.adm_client.update_quota_set(tenant_id, ram='5120')
resp, quota_set = self.adm_client.get_quota_set(tenant_id)
self.assertEqual(200, resp.status)
- self.assertEqual(quota_set['ram'], 5120)
+ self.assertEqual(5120, quota_set['ram'])
+
+ # Verify that GET shows the updated quota set of user
+ user_name = data_utils.rand_name('cpu_quota_user_')
+ password = data_utils.rand_name('password-')
+ email = user_name + '@testmail.tm'
+ _, user = identity_client.create_user(name=user_name,
+ password=password,
+ tenant_id=tenant_id,
+ email=email)
+ user_id = user['id']
+ self.addCleanup(identity_client.delete_user, user_id)
+
+ self.adm_client.update_quota_set(tenant_id,
+ user_id=user_id,
+ ram='2048')
+ resp, quota_set = self.adm_client.get_quota_set(tenant_id,
+ user_id=user_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(2048, quota_set['ram'])
@test.attr(type='gate')
def test_delete_quota(self):
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
index 1f2ddf4..49af645 100644
--- a/tempest/api/compute/admin/test_servers.py
+++ b/tempest/api/compute/admin/test_servers.py
@@ -14,7 +14,6 @@
from tempest.api.compute import base
from tempest.common.utils import data_utils
-from tempest import exceptions
from tempest import test
@@ -44,16 +43,6 @@
wait_until='ACTIVE')
cls.s2_id = server['id']
- def _get_unused_flavor_id(self):
- flavor_id = data_utils.rand_int_id(start=1000)
- while True:
- try:
- resp, body = self.flavors_client.get_flavor_details(flavor_id)
- except exceptions.NotFound:
- break
- flavor_id = data_utils.rand_int_id(start=1000)
- return flavor_id
-
@test.attr(type='gate')
def test_list_servers_by_admin(self):
# Listing servers by admin user returns empty list by default
diff --git a/tempest/api/compute/admin/test_servers_negative.py b/tempest/api/compute/admin/test_servers_negative.py
index 797b780..9fa07f6 100644
--- a/tempest/api/compute/admin/test_servers_negative.py
+++ b/tempest/api/compute/admin/test_servers_negative.py
@@ -14,11 +14,16 @@
import uuid
+import testtools
+
from tempest.api.compute import base
from tempest.common.utils import data_utils
+from tempest import config
from tempest import exceptions
from tempest import test
+CONF = config.CONF
+
class ServersAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
@@ -119,6 +124,8 @@
self.client.migrate_server,
str(uuid.uuid4()))
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.attr(type=['negative', 'gate'])
def test_migrate_server_invalid_state(self):
# create server.
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index abd36a6..d1bda83 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -227,12 +227,14 @@
cls.interfaces_client = cls.os.interfaces_client
cls.fixed_ips_client = cls.os.fixed_ips_client
cls.availability_zone_client = cls.os.availability_zone_client
+ cls.agents_client = cls.os.agents_client
cls.aggregates_client = cls.os.aggregates_client
cls.services_client = cls.os.services_client
cls.instance_usages_audit_log_client = \
cls.os.instance_usages_audit_log_client
cls.hypervisor_client = cls.os.hypervisor_client
cls.certificates_client = cls.os.certificates_client
+ cls.migrations_client = cls.os.migrations_client
@classmethod
def create_image_from_server(cls, server_id, **kwargs):
@@ -339,6 +341,7 @@
cls.hosts_client = cls.os.hosts_v3_client
cls.quotas_client = cls.os.quotas_v3_client
cls.version_client = cls.os.version_v3_client
+ cls.migrations_client = cls.os.migrations_v3_client
@classmethod
def create_image_from_server(cls, server_id, **kwargs):
@@ -412,3 +415,4 @@
cls.hosts_admin_client = cls.os_adm.hosts_v3_client
cls.quotas_admin_client = cls.os_adm.quotas_v3_client
cls.agents_admin_client = cls.os_adm.agents_v3_client
+ cls.migrations_admin_client = cls.os_adm.migrations_v3_client
diff --git a/tempest/api/compute/certificates/test_certificates.py b/tempest/api/compute/certificates/test_certificates.py
index 5299d13..f6cadf7 100644
--- a/tempest/api/compute/certificates/test_certificates.py
+++ b/tempest/api/compute/certificates/test_certificates.py
@@ -20,12 +20,15 @@
class CertificatesTestJSON(base.BaseV2ComputeTest):
@test.attr(type='gate')
- def test_create_and_get_root_certificate(self):
+ def test_create_root_certificate(self):
# create certificates
- resp, create_body = self.certificates_client.create_certificate()
+ resp, body = self.certificates_client.create_certificate()
self.assertEqual(200, resp.status)
- self.assertIn('data', create_body)
- self.assertIn('private_key', create_body)
+ self.assertIn('data', body)
+ self.assertIn('private_key', body)
+
+ @test.attr(type='gate')
+ def test_get_root_certificate(self):
# get the root certificate
resp, body = self.certificates_client.get_certificate('root')
self.assertEqual(200, resp.status)
diff --git a/tempest/api/compute/flavors/test_flavors_negative.py b/tempest/api/compute/flavors/test_flavors_negative.py
index a81b7d9..1638f2d 100644
--- a/tempest/api/compute/flavors/test_flavors_negative.py
+++ b/tempest/api/compute/flavors/test_flavors_negative.py
@@ -21,16 +21,14 @@
load_tests = test.NegativeAutoTest.load_tests
-class FlavorsListNegativeTestJSON(base.BaseV2ComputeTest,
- test.NegativeAutoTest):
+@test.SimpleNegativeAutoTest
+class FlavorsListWithDetailsNegativeTestJSON(base.BaseV2ComputeTest,
+ test.NegativeAutoTest):
_service = 'compute'
_schema_file = 'compute/flavors/flavors_list.json'
- @test.attr(type=['negative', 'gate'])
- def test_list_flavors_with_detail(self):
- self.execute(self._schema_file)
-
+@test.SimpleNegativeAutoTest
class FlavorDetailsNegativeTestJSON(base.BaseV2ComputeTest,
test.NegativeAutoTest):
_service = 'compute'
@@ -40,8 +38,3 @@
def setUpClass(cls):
super(FlavorDetailsNegativeTestJSON, cls).setUpClass()
cls.set_resource("flavor", cls.flavor_ref)
-
- @test.attr(type=['negative', 'gate'])
- def test_get_flavor_details(self):
- # flavor details are not returned for non-existent flavors
- self.execute(self._schema_file)
diff --git a/tempest/api/compute/floating_ips/test_floating_ips_actions.py b/tempest/api/compute/floating_ips/test_floating_ips_actions.py
index abd8a4c..b3789f8 100644
--- a/tempest/api/compute/floating_ips/test_floating_ips_actions.py
+++ b/tempest/api/compute/floating_ips/test_floating_ips_actions.py
@@ -45,6 +45,14 @@
resp, body = cls.client.delete_floating_ip(cls.floating_ip_id)
super(FloatingIPsTestJSON, cls).tearDownClass()
+ def _try_delete_floating_ip(self, floating_ip_id):
+ # delete floating ip, if it exists
+ try:
+ self.client.delete_floating_ip(floating_ip_id)
+ # if not found, it depicts it was deleted in the test
+ except exceptions.NotFound:
+ pass
+
@test.attr(type='gate')
def test_allocate_floating_ip(self):
# Positive test:Allocation of a new floating IP to a project
@@ -66,6 +74,7 @@
# should be successful
# Creating the floating IP that is to be deleted in this method
resp, floating_ip_body = self.client.create_floating_ip()
+ self.addCleanup(self._try_delete_floating_ip, floating_ip_body['id'])
# Storing the details of floating IP before deleting it
cli_resp = self.client.get_floating_ip_details(floating_ip_body['id'])
resp, floating_ip_details = cli_resp
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index f6eed00..297b300 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -127,7 +127,7 @@
_ifs = self._test_delete_interface(server, ifs)
self.assertEqual(len(ifs) - 1, len(_ifs))
- @test.attr(type='gate')
+ @test.attr(type='smoke')
def test_add_remove_fixed_ip(self):
# Add and Remove the fixed IP to server.
server, ifs = self._create_server_get_interfaces()
diff --git a/tempest/api/compute/servers/test_create_server.py b/tempest/api/compute/servers/test_create_server.py
index 778294e..f0a8c8d 100644
--- a/tempest/api/compute/servers/test_create_server.py
+++ b/tempest/api/compute/servers/test_create_server.py
@@ -28,7 +28,6 @@
class ServersTestJSON(base.BaseV2ComputeTest):
- run_ssh = CONF.compute.run_ssh
disk_config = 'AUTO'
@classmethod
@@ -89,7 +88,8 @@
found = any([i for i in servers if i['id'] == self.server['id']])
self.assertTrue(found)
- @testtools.skipIf(not run_ssh, 'Instance validation tests are disabled.')
+ @testtools.skipUnless(CONF.compute.run_ssh,
+ 'Instance validation tests are disabled.')
@test.attr(type='gate')
def test_verify_created_server_vcpus(self):
# Verify that the number of vcpus reported by the instance matches
@@ -99,7 +99,8 @@
self.password)
self.assertEqual(flavor['vcpus'], linux_client.get_number_of_vcpus())
- @testtools.skipIf(not run_ssh, 'Instance validation tests are disabled.')
+ @testtools.skipUnless(CONF.compute.run_ssh,
+ 'Instance validation tests are disabled.')
@test.attr(type='gate')
def test_host_name_is_same_as_server_name(self):
# Verify the instance host name is the same as the server name
@@ -109,7 +110,6 @@
class ServersWithSpecificFlavorTestJSON(base.BaseV2ComputeAdminTest):
- run_ssh = CONF.compute.run_ssh
disk_config = 'AUTO'
@classmethod
@@ -135,7 +135,8 @@
cls.client.wait_for_server_status(cls.server_initial['id'], 'ACTIVE')
resp, cls.server = cls.client.get_server(cls.server_initial['id'])
- @testtools.skipIf(not run_ssh, 'Instance validation tests are disabled.')
+ @testtools.skipUnless(CONF.compute.run_ssh,
+ 'Instance validation tests are disabled.')
@test.attr(type='gate')
def test_verify_created_server_ephemeral_disk(self):
# Verify that the ephemeral disk is created when creating server
diff --git a/tempest/api/compute/servers/test_delete_server.py b/tempest/api/compute/servers/test_delete_server.py
index 7e34213..9e34922 100644
--- a/tempest/api/compute/servers/test_delete_server.py
+++ b/tempest/api/compute/servers/test_delete_server.py
@@ -23,7 +23,6 @@
class DeleteServersTestJSON(base.BaseV2ComputeTest):
- pause_available = CONF.compute_feature_enabled.pause
# NOTE: Server creations of each test class should be under 10
# for preventing "Quota exceeded for instances"
@@ -59,7 +58,8 @@
self.assertEqual('204', resp['status'])
self.client.wait_for_server_termination(server['id'])
- @testtools.skipIf(not pause_available, 'Pause is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type='gate')
def test_delete_server_while_in_pause_state(self):
# Delete a server while it's VM state is Pause
@@ -102,6 +102,26 @@
self.assertEqual('204', resp['status'])
self.client.wait_for_server_termination(server['id'])
+ @test.attr(type='gate')
+ def test_delete_server_while_in_attached_volume(self):
+ # Delete a server while a volume is attached to it
+ volumes_client = self.volumes_extensions_client
+ device = '/dev/%s' % CONF.compute.volume_device_name
+ resp, server = self.create_test_server(wait_until='ACTIVE')
+
+ resp, volume = volumes_client.create_volume(1)
+ self.addCleanup(volumes_client.delete_volume, volume['id'])
+ volumes_client.wait_for_volume_status(volume['id'], 'available')
+ resp, body = self.client.attach_volume(server['id'],
+ volume['id'],
+ device=device)
+ volumes_client.wait_for_volume_status(volume['id'], 'in-use')
+
+ resp, _ = self.client.delete_server(server['id'])
+ self.assertEqual('204', resp['status'])
+ self.client.wait_for_server_termination(server['id'])
+ volumes_client.wait_for_volume_status(volume['id'], 'available')
+
class DeleteServersAdminTestJSON(base.BaseV2ComputeAdminTest):
# NOTE: Server creations of each test class should be under 10
diff --git a/tempest/api/compute/servers/test_list_server_filters.py b/tempest/api/compute/servers/test_list_server_filters.py
index f0913f1..f66020c 100644
--- a/tempest/api/compute/servers/test_list_server_filters.py
+++ b/tempest/api/compute/servers/test_list_server_filters.py
@@ -28,6 +28,7 @@
@classmethod
@test.safe_setup
def setUpClass(cls):
+ cls.set_network_resources(network=True, subnet=True, dhcp=True)
super(ListServerFiltersTestJSON, cls).setUpClass()
cls.client = cls.servers_client
@@ -69,8 +70,12 @@
resp, cls.s3 = cls.create_test_server(name=cls.s3_name,
flavor=cls.flavor_ref_alt,
wait_until='ACTIVE')
-
- cls.fixed_network_name = CONF.compute.fixed_network_name
+ if (CONF.service_available.neutron and
+ CONF.compute.allow_tenant_isolation):
+ network = cls.isolated_creds.get_primary_network()
+ cls.fixed_network_name = network['name']
+ else:
+ cls.fixed_network_name = CONF.compute.fixed_network_name
@utils.skip_unless_attr('multiple_images', 'Only one image found')
@test.attr(type='gate')
@@ -226,7 +231,6 @@
self.assertNotIn(self.s2_name, map(lambda x: x['name'], servers))
self.assertNotIn(self.s3_name, map(lambda x: x['name'], servers))
- @test.skip_because(bug="1170718")
@test.attr(type='gate')
def test_list_servers_filtered_by_ip(self):
# Filter servers by ip
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 72ccc71..cc2d1ee 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -29,9 +29,6 @@
class ServerActionsTestJSON(base.BaseV2ComputeTest):
- resize_available = CONF.compute_feature_enabled.resize
- pause_available = CONF.compute_feature_enabled.pause
- suspend_available = CONF.compute_feature_enabled.suspend
run_ssh = CONF.compute.run_ssh
def setUp(self):
@@ -186,7 +183,8 @@
if current_flavor == self.flavor_ref else self.flavor_ref
return current_flavor, new_flavor_ref
- @testtools.skipIf(not resize_available, 'Resize not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
@test.attr(type='smoke')
def test_resize_server_confirm(self):
# The server's RAM and disk space should be modified to that of
@@ -205,7 +203,8 @@
resp, server = self.client.get_server(self.server_id)
self.assertEqual(new_flavor_ref, server['flavor']['id'])
- @testtools.skipIf(not resize_available, 'Resize not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
@test.attr(type='gate')
def test_resize_server_revert(self):
# The server's RAM and disk space should return to its original
@@ -342,7 +341,8 @@
self.wait_for(self._get_output)
- @testtools.skipIf(not pause_available, 'Pause is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type='gate')
def test_pause_unpause_server(self):
resp, server = self.client.pause_server(self.server_id)
@@ -352,7 +352,8 @@
self.assertEqual(202, resp.status)
self.client.wait_for_server_status(self.server_id, 'ACTIVE')
- @testtools.skipIf(not suspend_available, 'Suspend is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.attr(type='gate')
def test_suspend_resume_server(self):
resp, server = self.client.suspend_server(self.server_id)
diff --git a/tempest/api/compute/servers/test_server_rescue_negative.py b/tempest/api/compute/servers/test_server_rescue_negative.py
index ef45585..dae4709 100644
--- a/tempest/api/compute/servers/test_server_rescue_negative.py
+++ b/tempest/api/compute/servers/test_server_rescue_negative.py
@@ -12,12 +12,16 @@
# 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 testtools
from tempest.api.compute import base
from tempest.common.utils import data_utils
+from tempest import config
from tempest import exceptions
from tempest import test
+CONF = config.CONF
+
class ServerRescueNegativeTestJSON(base.BaseV2ComputeTest):
@@ -45,6 +49,7 @@
cls.servers_client.rescue_server(
cls.rescue_id, adminPass=rescue_password)
cls.servers_client.wait_for_server_status(cls.rescue_id, 'RESCUE')
+ cls.servers_client.wait_for_server_status(cls.server_id, 'ACTIVE')
@classmethod
def tearDownClass(cls):
@@ -66,6 +71,8 @@
self.assertEqual(202, resp.status)
self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type=['negative', 'gate'])
def test_rescue_paused_instance(self):
# Rescue a paused server
diff --git a/tempest/api/compute/servers/test_servers_negative.py b/tempest/api/compute/servers/test_servers_negative.py
index cbfec5c..cc801b5 100644
--- a/tempest/api/compute/servers/test_servers_negative.py
+++ b/tempest/api/compute/servers/test_servers_negative.py
@@ -29,8 +29,6 @@
class ServersNegativeTestJSON(base.BaseV2ComputeTest):
- pause_available = CONF.compute_feature_enabled.pause
- suspend_available = CONF.compute_feature_enabled.suspend
def setUp(self):
super(ServersNegativeTestJSON, self).setUp()
@@ -129,7 +127,8 @@
self.assertRaises(exceptions.NotFound, self.client.reboot,
nonexistent_server, 'SOFT')
- @testtools.skipIf(not pause_available, 'Pause is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type=['negative', 'gate'])
def test_pause_paused_server(self):
# Pause a paused server.
@@ -309,7 +308,8 @@
self.assertRaises(exceptions.NotFound, self.servers_client.stop,
nonexistent_server)
- @testtools.skipIf(not pause_available, 'Pause is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type=['negative', 'gate'])
def test_pause_non_existent_server(self):
# pause a non existent server
@@ -317,7 +317,8 @@
self.assertRaises(exceptions.NotFound, self.client.pause_server,
nonexistent_server)
- @testtools.skipIf(not pause_available, 'Pause is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type=['negative', 'gate'])
def test_unpause_non_existent_server(self):
# unpause a non existent server
@@ -325,7 +326,8 @@
self.assertRaises(exceptions.NotFound, self.client.unpause_server,
nonexistent_server)
- @testtools.skipIf(not pause_available, 'Pause is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type=['negative', 'gate'])
def test_unpause_server_invalid_state(self):
# unpause an active server.
@@ -333,7 +335,8 @@
self.client.unpause_server,
self.server_id)
- @testtools.skipIf(not suspend_available, 'Suspend is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.attr(type=['negative', 'gate'])
def test_suspend_non_existent_server(self):
# suspend a non existent server
@@ -341,7 +344,8 @@
self.assertRaises(exceptions.NotFound, self.client.suspend_server,
nonexistent_server)
- @testtools.skipIf(not suspend_available, 'Suspend is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.attr(type=['negative', 'gate'])
def test_suspend_server_invalid_state(self):
# suspend a suspended server.
@@ -354,7 +358,8 @@
self.client.suspend_server,
self.server_id)
- @testtools.skipIf(not suspend_available, 'Suspend is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.attr(type=['negative', 'gate'])
def test_resume_non_existent_server(self):
# resume a non existent server
@@ -362,7 +367,8 @@
self.assertRaises(exceptions.NotFound, self.client.resume_server,
nonexistent_server)
- @testtools.skipIf(not suspend_available, 'Suspend is not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.attr(type=['negative', 'gate'])
def test_resume_server_invalid_state(self):
# resume an active server.
diff --git a/tempest/api/compute/servers/test_servers_negative_new.py b/tempest/api/compute/servers/test_servers_negative_new.py
index f860ff9..43ddb3a 100644
--- a/tempest/api/compute/servers/test_servers_negative_new.py
+++ b/tempest/api/compute/servers/test_servers_negative_new.py
@@ -21,6 +21,7 @@
load_tests = test.NegativeAutoTest.load_tests
+@test.SimpleNegativeAutoTest
class GetConsoleOutputNegativeTestJSON(base.BaseV2ComputeTest,
test.NegativeAutoTest):
_service = 'compute'
@@ -31,7 +32,3 @@
super(GetConsoleOutputNegativeTestJSON, cls).setUpClass()
_resp, server = cls.create_test_server()
cls.set_resource("server", server['id'])
-
- @test.attr(type=['negative', 'gate'])
- def test_get_console_output(self):
- self.execute(self._schema_file)
diff --git a/tempest/api/compute/test_authorization.py b/tempest/api/compute/test_authorization.py
index 7f909d7..c87f24e 100644
--- a/tempest/api/compute/test_authorization.py
+++ b/tempest/api/compute/test_authorization.py
@@ -60,7 +60,7 @@
resp, cls.server = cls.client.get_server(server['id'])
name = data_utils.rand_name('image')
- resp, body = cls.client.create_image(server['id'], name)
+ resp, body = cls.images_client.create_image(server['id'], name)
image_id = data_utils.parse_image_id(resp['location'])
cls.images_client.wait_for_image_status(image_id, 'ACTIVE')
resp, cls.image = cls.images_client.get_image(image_id)
diff --git a/tempest/api/compute/v3/admin/test_flavors.py b/tempest/api/compute/v3/admin/test_flavors.py
index 401eb85..2a4fc02 100644
--- a/tempest/api/compute/v3/admin/test_flavors.py
+++ b/tempest/api/compute/v3/admin/test_flavors.py
@@ -169,7 +169,6 @@
flag = True
self.assertTrue(flag)
- @test.skip_because(bug="1209101")
@test.attr(type='gate')
def test_list_non_public_flavor(self):
# Create a flavor with os-flavor-access:is_public false should
diff --git a/tempest/api/compute/v3/admin/test_migrations.py b/tempest/api/compute/v3/admin/test_migrations.py
new file mode 100644
index 0000000..e8bd473
--- /dev/null
+++ b/tempest/api/compute/v3/admin/test_migrations.py
@@ -0,0 +1,50 @@
+# 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.
+
+import testtools
+
+from tempest.api.compute import base
+from tempest import config
+from tempest import test
+
+CONF = config.CONF
+
+
+class MigrationsAdminV3Test(base.BaseV3ComputeAdminTest):
+
+ @test.attr(type='gate')
+ def test_list_migrations(self):
+ # Admin can get the migrations list
+ resp, _ = self.migrations_admin_client.list_migrations()
+ self.assertEqual(200, resp.status)
+
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
+ @test.attr(type='gate')
+ def test_list_migrations_in_flavor_resize_situation(self):
+ # Admin can get the migrations list which contains the resized server
+ resp, server = self.create_test_server(wait_until="ACTIVE")
+ server_id = server['id']
+
+ resp, _ = self.servers_client.resize(server_id, self.flavor_ref_alt)
+ self.assertEqual(202, resp.status)
+ self.servers_client.wait_for_server_status(server_id, 'VERIFY_RESIZE')
+ self.servers_client.confirm_resize(server_id)
+ self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
+
+ resp, body = self.migrations_admin_client.list_migrations()
+ self.assertEqual(200, resp.status)
+
+ instance_uuids = [x['instance_uuid'] for x in body]
+ self.assertIn(server_id, instance_uuids)
diff --git a/tempest/api/compute/v3/admin/test_quotas.py b/tempest/api/compute/v3/admin/test_quotas.py
index 917c115..b70e254 100644
--- a/tempest/api/compute/v3/admin/test_quotas.py
+++ b/tempest/api/compute/v3/admin/test_quotas.py
@@ -94,21 +94,38 @@
# TODO(afazekas): merge these test cases
@test.attr(type='gate')
def test_get_updated_quotas(self):
- # Verify that GET shows the updated quota set
+ # Verify that GET shows the updated quota set of tenant
tenant_name = data_utils.rand_name('cpu_quota_tenant_')
tenant_desc = tenant_name + '-desc'
identity_client = self.os_adm.identity_client
_, tenant = identity_client.create_tenant(name=tenant_name,
description=tenant_desc)
tenant_id = tenant['id']
- self.addCleanup(identity_client.delete_tenant,
- tenant_id)
+ self.addCleanup(identity_client.delete_tenant, tenant_id)
- self.adm_client.update_quota_set(tenant_id,
- ram='5120')
+ self.adm_client.update_quota_set(tenant_id, ram='5120')
resp, quota_set = self.adm_client.get_quota_set(tenant_id)
self.assertEqual(200, resp.status)
- self.assertEqual(quota_set['ram'], 5120)
+ self.assertEqual(5120, quota_set['ram'])
+
+ # Verify that GET shows the updated quota set of user
+ user_name = data_utils.rand_name('cpu_quota_user_')
+ password = data_utils.rand_name('password-')
+ email = user_name + '@testmail.tm'
+ _, user = identity_client.create_user(name=user_name,
+ password=password,
+ tenant_id=tenant_id,
+ email=email)
+ user_id = user['id']
+ self.addCleanup(identity_client.delete_user, user_id)
+
+ self.adm_client.update_quota_set(tenant_id,
+ user_id=user_id,
+ ram='2048')
+ resp, quota_set = self.adm_client.get_quota_set(tenant_id,
+ user_id=user_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(2048, quota_set['ram'])
@test.attr(type='gate')
def test_delete_quota(self):
diff --git a/tempest/api/compute/v3/admin/test_servers.py b/tempest/api/compute/v3/admin/test_servers.py
index 579a535..366cfc6 100644
--- a/tempest/api/compute/v3/admin/test_servers.py
+++ b/tempest/api/compute/v3/admin/test_servers.py
@@ -14,7 +14,6 @@
from tempest.api.compute import base
from tempest.common.utils import data_utils
-from tempest import exceptions
from tempest import test
@@ -44,16 +43,6 @@
wait_until='ACTIVE')
cls.s2_id = server['id']
- def _get_unused_flavor_id(self):
- flavor_id = data_utils.rand_int_id(start=1000)
- while True:
- try:
- resp, body = self.flavors_client.get_flavor_details(flavor_id)
- except exceptions.NotFound:
- break
- flavor_id = data_utils.rand_int_id(start=1000)
- return flavor_id
-
@test.attr(type='gate')
def test_list_servers_by_admin(self):
# Listing servers by admin user returns empty list by default
diff --git a/tempest/api/compute/v3/admin/test_servers_negative.py b/tempest/api/compute/v3/admin/test_servers_negative.py
index cc1be4e..fba4cd1 100644
--- a/tempest/api/compute/v3/admin/test_servers_negative.py
+++ b/tempest/api/compute/v3/admin/test_servers_negative.py
@@ -14,11 +14,16 @@
import uuid
+import testtools
+
from tempest.api.compute import base
from tempest.common.utils import data_utils
+from tempest import config
from tempest import exceptions
from tempest.test import attr
+CONF = config.CONF
+
class ServersAdminNegativeV3Test(base.BaseV3ComputeAdminTest):
@@ -119,6 +124,8 @@
self.client.migrate_server,
str(uuid.uuid4()))
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@attr(type=['negative', 'gate'])
def test_migrate_server_invalid_state(self):
# create server.
diff --git a/tempest/api/compute/v3/certificates/test_certificates.py b/tempest/api/compute/v3/certificates/test_certificates.py
index ce025fc..0ba44cb 100644
--- a/tempest/api/compute/v3/certificates/test_certificates.py
+++ b/tempest/api/compute/v3/certificates/test_certificates.py
@@ -20,12 +20,15 @@
class CertificatesV3Test(base.BaseV3ComputeTest):
@attr(type='gate')
- def test_create_and_get_root_certificate(self):
+ def test_create_root_certificate(self):
# create certificates
- resp, create_body = self.certificates_client.create_certificate()
+ resp, body = self.certificates_client.create_certificate()
self.assertEqual(201, resp.status)
- self.assertIn('data', create_body)
- self.assertIn('private_key', create_body)
+ self.assertIn('data', body)
+ self.assertIn('private_key', body)
+
+ @attr(type='gate')
+ def test_get_root_certificate(self):
# get the root certificate
resp, body = self.certificates_client.get_certificate('root')
self.assertEqual(200, resp.status)
diff --git a/tempest/api/compute/v3/flavors/test_flavors_negative.py b/tempest/api/compute/v3/flavors/test_flavors_negative.py
index 1c0e4fb..657e2cd 100644
--- a/tempest/api/compute/v3/flavors/test_flavors_negative.py
+++ b/tempest/api/compute/v3/flavors/test_flavors_negative.py
@@ -20,16 +20,14 @@
load_tests = test.NegativeAutoTest.load_tests
+@test.SimpleNegativeAutoTest
class FlavorsListNegativeV3Test(base.BaseV3ComputeTest,
test.NegativeAutoTest):
_service = 'computev3'
_schema_file = 'compute/flavors/flavors_list_v3.json'
- @test.attr(type=['negative', 'gate'])
- def test_list_flavors_with_detail(self):
- self.execute(self._schema_file)
-
+@test.SimpleNegativeAutoTest
class FlavorDetailsNegativeV3Test(base.BaseV3ComputeTest,
test.NegativeAutoTest):
_service = 'computev3'
@@ -39,8 +37,3 @@
def setUpClass(cls):
super(FlavorDetailsNegativeV3Test, cls).setUpClass()
cls.set_resource("flavor", cls.flavor_ref)
-
- @test.attr(type=['negative', 'gate'])
- def test_get_flavor_details(self):
- # flavor details are not returned for non-existent flavors
- self.execute(self._schema_file)
diff --git a/tempest/api/compute/v3/servers/test_attach_interfaces.py b/tempest/api/compute/v3/servers/test_attach_interfaces.py
index e1c69d9..c848f8c 100644
--- a/tempest/api/compute/v3/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/v3/servers/test_attach_interfaces.py
@@ -127,7 +127,7 @@
_ifs = self._test_delete_interface(server, ifs)
self.assertEqual(len(ifs) - 1, len(_ifs))
- @attr(type='gate')
+ @attr(type='smoke')
def test_add_remove_fixed_ip(self):
# Add and Remove the fixed IP to server.
server, ifs = self._create_server_get_interfaces()
diff --git a/tempest/api/compute/v3/servers/test_attach_volume.py b/tempest/api/compute/v3/servers/test_attach_volume.py
index 8577aab..28d8517 100644
--- a/tempest/api/compute/v3/servers/test_attach_volume.py
+++ b/tempest/api/compute/v3/servers/test_attach_volume.py
@@ -24,7 +24,6 @@
class AttachVolumeV3Test(base.BaseV3ComputeTest):
- run_ssh = CONF.compute.run_ssh
def __init__(self, *args, **kwargs):
super(AttachVolumeV3Test, self).__init__(*args, **kwargs)
@@ -76,7 +75,7 @@
self.attached = True
self.addCleanup(self._detach, server['id'], volume['id'])
- @testtools.skipIf(not run_ssh, 'SSH required for this test')
+ @testtools.skipUnless(CONF.compute.run_ssh, 'SSH required for this test')
@test.attr(type='gate')
def test_attach_detach_volume(self):
# Stop and Start a server with an attached volume, ensuring that
diff --git a/tempest/api/compute/v3/servers/test_create_server.py b/tempest/api/compute/v3/servers/test_create_server.py
index 14a4338..80c40a2 100644
--- a/tempest/api/compute/v3/servers/test_create_server.py
+++ b/tempest/api/compute/v3/servers/test_create_server.py
@@ -28,7 +28,6 @@
class ServersV3Test(base.BaseV3ComputeTest):
- run_ssh = CONF.compute.run_ssh
disk_config = 'AUTO'
@classmethod
@@ -90,7 +89,8 @@
found = any([i for i in servers if i['id'] == self.server['id']])
self.assertTrue(found)
- @testtools.skipIf(not run_ssh, 'Instance validation tests are disabled.')
+ @testtools.skipUnless(CONF.compute.run_ssh,
+ 'Instance validation tests are disabled.')
@test.attr(type='gate')
def test_verify_created_server_vcpus(self):
# Verify that the number of vcpus reported by the instance matches
@@ -100,7 +100,8 @@
self.ssh_user, self.password)
self.assertEqual(flavor['vcpus'], linux_client.get_number_of_vcpus())
- @testtools.skipIf(not run_ssh, 'Instance validation tests are disabled.')
+ @testtools.skipUnless(CONF.compute.run_ssh,
+ 'Instance validation tests are disabled.')
@test.attr(type='gate')
def test_host_name_is_same_as_server_name(self):
# Verify the instance host name is the same as the server name
@@ -110,7 +111,6 @@
class ServersWithSpecificFlavorV3Test(base.BaseV3ComputeAdminTest):
- run_ssh = CONF.compute.run_ssh
disk_config = 'AUTO'
@classmethod
@@ -136,7 +136,8 @@
cls.client.wait_for_server_status(cls.server_initial['id'], 'ACTIVE')
resp, cls.server = cls.client.get_server(cls.server_initial['id'])
- @testtools.skipIf(not run_ssh, 'Instance validation tests are disabled.')
+ @testtools.skipUnless(CONF.compute.run_ssh,
+ 'Instance validation tests are disabled.')
@test.attr(type='gate')
def test_verify_created_server_ephemeral_disk(self):
# Verify that the ephemeral disk is created when creating server
diff --git a/tempest/api/compute/v3/servers/test_delete_server.py b/tempest/api/compute/v3/servers/test_delete_server.py
index d694a33..add69ab 100644
--- a/tempest/api/compute/v3/servers/test_delete_server.py
+++ b/tempest/api/compute/v3/servers/test_delete_server.py
@@ -56,6 +56,8 @@
self.assertEqual('204', resp['status'])
self.client.wait_for_server_termination(server['id'])
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type='gate')
def test_delete_server_while_in_pause_state(self):
# Delete a server while it's VM state is Pause
@@ -99,6 +101,25 @@
self.assertEqual('204', resp['status'])
self.client.wait_for_server_termination(server['id'])
+ @test.attr(type='gate')
+ def test_delete_server_while_in_attached_volume(self):
+ # Delete a server while a volume is attached to it
+ device = '/dev/%s' % CONF.compute.volume_device_name
+ resp, server = self.create_test_server(wait_until='ACTIVE')
+
+ resp, volume = self.volumes_client.create_volume(1)
+ self.addCleanup(self.volumes_client.delete_volume, volume['id'])
+ self.volumes_client.wait_for_volume_status(volume['id'], 'available')
+ resp, body = self.client.attach_volume(server['id'],
+ volume['id'],
+ device=device)
+ self.volumes_client.wait_for_volume_status(volume['id'], 'in-use')
+
+ resp, _ = self.client.delete_server(server['id'])
+ self.assertEqual('204', resp['status'])
+ self.client.wait_for_server_termination(server['id'])
+ self.volumes_client.wait_for_volume_status(volume['id'], 'available')
+
class DeleteServersAdminV3Test(base.BaseV3ComputeAdminTest):
# NOTE: Server creations of each test class should be under 10
diff --git a/tempest/api/compute/v3/servers/test_instance_actions.py b/tempest/api/compute/v3/servers/test_instance_actions.py
index 7d25100..399541b 100644
--- a/tempest/api/compute/v3/servers/test_instance_actions.py
+++ b/tempest/api/compute/v3/servers/test_instance_actions.py
@@ -27,25 +27,27 @@
cls.resp = resp
cls.server_id = server['id']
+ @test.skip_because(bug="1206032")
@test.attr(type='gate')
- def test_list_instance_actions(self):
+ def test_list_server_actions(self):
# List actions of the provided server
resp, body = self.client.reboot(self.server_id, 'HARD')
self.client.wait_for_server_status(self.server_id, 'ACTIVE')
- resp, body = self.client.list_instance_actions(self.server_id)
+ resp, body = self.client.list_server_actions(self.server_id)
self.assertEqual(200, resp.status)
self.assertTrue(len(body) == 2, str(body))
self.assertTrue(any([i for i in body if i['action'] == 'create']))
self.assertTrue(any([i for i in body if i['action'] == 'reboot']))
+ @test.skip_because(bug="1206032")
@test.attr(type='gate')
@test.skip_because(bug="1281915")
- def test_get_instance_action(self):
+ def test_get_server_action(self):
# Get the action details of the provided server
request_id = self.resp['x-compute-request-id']
- resp, body = self.client.get_instance_action(self.server_id,
- request_id)
+ resp, body = self.client.get_server_action(self.server_id,
+ request_id)
self.assertEqual(200, resp.status)
- self.assertEqual(self.server_id, body['instance_uuid'])
+ self.assertEqual(self.server_id, body['server_uuid'])
self.assertEqual('create', body['action'])
diff --git a/tempest/api/compute/v3/servers/test_instance_actions_negative.py b/tempest/api/compute/v3/servers/test_instance_actions_negative.py
index b0a7050..0b2c6f9 100644
--- a/tempest/api/compute/v3/servers/test_instance_actions_negative.py
+++ b/tempest/api/compute/v3/servers/test_instance_actions_negative.py
@@ -29,15 +29,15 @@
cls.server_id = server['id']
@test.attr(type=['negative', 'gate'])
- def test_list_instance_actions_invalid_server(self):
+ def test_list_server_actions_invalid_server(self):
# List actions of the invalid server id
invalid_server_id = data_utils.rand_uuid()
self.assertRaises(exceptions.NotFound,
- self.client.list_instance_actions, invalid_server_id)
+ self.client.list_server_actions, invalid_server_id)
@test.attr(type=['negative', 'gate'])
- def test_get_instance_action_invalid_request(self):
+ def test_get_server_action_invalid_request(self):
# Get the action details of the provided server with invalid request
invalid_request_id = 'req-' + data_utils.rand_uuid()
- self.assertRaises(exceptions.NotFound, self.client.get_instance_action,
+ self.assertRaises(exceptions.NotFound, self.client.get_server_action,
self.server_id, invalid_request_id)
diff --git a/tempest/api/compute/v3/servers/test_list_server_filters.py b/tempest/api/compute/v3/servers/test_list_server_filters.py
index 2cb176c..778b033 100644
--- a/tempest/api/compute/v3/servers/test_list_server_filters.py
+++ b/tempest/api/compute/v3/servers/test_list_server_filters.py
@@ -28,6 +28,7 @@
@classmethod
@test.safe_setup
def setUpClass(cls):
+ cls.set_network_resources(network=True, subnet=True, dhcp=True)
super(ListServerFiltersV3Test, cls).setUpClass()
cls.client = cls.servers_client
@@ -70,7 +71,12 @@
flavor=cls.flavor_ref_alt,
wait_until='ACTIVE')
- cls.fixed_network_name = CONF.compute.fixed_network_name
+ if (CONF.service_available.neutron and
+ CONF.compute.allow_tenant_isolation):
+ network = cls.isolated_creds.get_primary_network()
+ cls.fixed_network_name = network['name']
+ else:
+ cls.fixed_network_name = CONF.compute.fixed_network_name
@utils.skip_unless_attr('multiple_images', 'Only one image found')
@test.attr(type='gate')
@@ -226,7 +232,6 @@
self.assertNotIn(self.s2_name, map(lambda x: x['name'], servers))
self.assertNotIn(self.s3_name, map(lambda x: x['name'], servers))
- @test.skip_because(bug="1170718")
@test.attr(type='gate')
def test_list_servers_filtered_by_ip(self):
# Filter servers by ip
diff --git a/tempest/api/compute/v3/servers/test_server_actions.py b/tempest/api/compute/v3/servers/test_server_actions.py
index 2582fa8..ab1ccc4 100644
--- a/tempest/api/compute/v3/servers/test_server_actions.py
+++ b/tempest/api/compute/v3/servers/test_server_actions.py
@@ -26,7 +26,6 @@
class ServerActionsV3Test(base.BaseV3ComputeTest):
- resize_available = CONF.compute_feature_enabled.resize
run_ssh = CONF.compute.run_ssh
def setUp(self):
@@ -175,7 +174,8 @@
if current_flavor == self.flavor_ref else self.flavor_ref
return current_flavor, new_flavor_ref
- @testtools.skipIf(not resize_available, 'Resize not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
@test.attr(type='smoke')
def test_resize_server_confirm(self):
# The server's RAM and disk space should be modified to that of
@@ -194,7 +194,8 @@
resp, server = self.client.get_server(self.server_id)
self.assertEqual(new_flavor_ref, server['flavor']['id'])
- @testtools.skipIf(not resize_available, 'Resize not available.')
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
@test.attr(type='gate')
def test_resize_server_revert(self):
# The server's RAM and disk space should return to its original
@@ -328,6 +329,8 @@
self.wait_for(self._get_output)
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type='gate')
def test_pause_unpause_server(self):
resp, server = self.client.pause_server(self.server_id)
@@ -337,6 +340,8 @@
self.assertEqual(202, resp.status)
self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.attr(type='gate')
def test_suspend_resume_server(self):
resp, server = self.client.suspend_server(self.server_id)
diff --git a/tempest/api/compute/v3/servers/test_server_rescue_negative.py b/tempest/api/compute/v3/servers/test_server_rescue_negative.py
index 6bb441c..eb6bcdd 100644
--- a/tempest/api/compute/v3/servers/test_server_rescue_negative.py
+++ b/tempest/api/compute/v3/servers/test_server_rescue_negative.py
@@ -12,12 +12,16 @@
# 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 testtools
from tempest.api.compute import base
from tempest.common.utils import data_utils
+from tempest import config
from tempest import exceptions
from tempest import test
+CONF = config.CONF
+
class ServerRescueNegativeV3Test(base.BaseV3ComputeTest):
@@ -44,6 +48,7 @@
cls.servers_client.rescue_server(
cls.rescue_id, admin_password=cls.rescue_password)
cls.servers_client.wait_for_server_status(cls.rescue_id, 'RESCUE')
+ cls.servers_client.wait_for_server_status(cls.server_id, 'ACTIVE')
@classmethod
def tearDownClass(cls):
@@ -65,6 +70,8 @@
self.assertEqual(202, resp.status)
self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type=['negative', 'gate'])
def test_rescue_paused_instance(self):
# Rescue a paused server
diff --git a/tempest/api/compute/v3/servers/test_servers_negative.py b/tempest/api/compute/v3/servers/test_servers_negative.py
index cb5e93d..c1d1935 100644
--- a/tempest/api/compute/v3/servers/test_servers_negative.py
+++ b/tempest/api/compute/v3/servers/test_servers_negative.py
@@ -16,6 +16,8 @@
import base64
import sys
+import testtools
+
from tempest.api.compute import base
from tempest import clients
from tempest.common.utils import data_utils
@@ -113,6 +115,8 @@
self.assertRaises(exceptions.NotFound, self.client.reboot,
nonexistent_server, 'SOFT')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type=['negative', 'gate'])
def test_pause_paused_server(self):
# Pause a paused server.
@@ -311,6 +315,8 @@
self.client.unpause_server,
self.server_id)
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.attr(type=['negative', 'gate'])
def test_suspend_non_existent_server(self):
# suspend a non existent server
@@ -318,6 +324,8 @@
self.assertRaises(exceptions.NotFound, self.client.suspend_server,
nonexistent_server)
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.attr(type=['negative', 'gate'])
def test_suspend_server_invalid_state(self):
# suspend a suspended server.
@@ -337,6 +345,8 @@
self.assertRaises(exceptions.NotFound, self.client.resume_server,
nonexistent_server)
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.attr(type=['negative', 'gate'])
def test_resume_server_invalid_state(self):
# resume an active server.
diff --git a/tempest/api/compute/volumes/test_attach_volume.py b/tempest/api/compute/volumes/test_attach_volume.py
index 3c5feed..ab9d144 100644
--- a/tempest/api/compute/volumes/test_attach_volume.py
+++ b/tempest/api/compute/volumes/test_attach_volume.py
@@ -24,7 +24,6 @@
class AttachVolumeTestJSON(base.BaseV2ComputeTest):
- run_ssh = CONF.compute.run_ssh
def __init__(self, *args, **kwargs):
super(AttachVolumeTestJSON, self).__init__(*args, **kwargs)
@@ -76,7 +75,7 @@
self.attached = True
self.addCleanup(self._detach, server['id'], volume['id'])
- @testtools.skipIf(not run_ssh, 'SSH required for this test')
+ @testtools.skipUnless(CONF.compute.run_ssh, 'SSH required for this test')
@test.attr(type='gate')
def test_attach_detach_volume(self):
# Stop and Start a server with an attached volume, ensuring that
diff --git a/tempest/api/data_processing/base.py b/tempest/api/data_processing/base.py
index 73ad22b..84d5be6 100644
--- a/tempest/api/data_processing/base.py
+++ b/tempest/api/data_processing/base.py
@@ -26,9 +26,10 @@
@classmethod
def setUpClass(cls):
super(BaseDataProcessingTest, cls).setUpClass()
- os = cls.get_client_manager()
if not CONF.service_available.sahara:
raise cls.skipException("Sahara support is required")
+
+ os = cls.get_client_manager()
cls.client = os.data_processing_client
# set some constants
@@ -57,7 +58,7 @@
@classmethod
def tearDownClass(cls):
# cleanup node group templates
- for ngt_id in cls._node_group_templates:
+ for ngt_id in getattr(cls, '_node_group_templates', []):
try:
cls.client.delete_node_group_template(ngt_id)
except Exception:
diff --git a/tempest/api/identity/admin/test_roles.py b/tempest/api/identity/admin/test_roles.py
index 81bc5de..a29f27e 100644
--- a/tempest/api/identity/admin/test_roles.py
+++ b/tempest/api/identity/admin/test_roles.py
@@ -60,8 +60,7 @@
# Role should be created, verified, and deleted
role_name = data_utils.rand_name(name='role-test-')
resp, body = self.client.create_role(role_name)
- self.assertIn('status', resp)
- self.assertTrue(resp['status'].startswith('2'))
+ self.assertEqual(200, resp.status)
self.assertEqual(role_name, body['name'])
resp, body = self.client.list_roles()
@@ -69,14 +68,25 @@
self.assertTrue(any(found))
resp, body = self.client.delete_role(found[0]['id'])
- self.assertIn('status', resp)
- self.assertTrue(resp['status'].startswith('2'))
+ self.assertEqual(204, resp.status)
resp, body = self.client.list_roles()
found = [role for role in body if role['name'] == role_name]
self.assertFalse(any(found))
@test.attr(type='gate')
+ def test_get_role_by_id(self):
+ # Get a role by its id
+ self.data.setup_test_role()
+ role_id = self.data.role['id']
+ role_name = self.data.role['name']
+ resp, body = self.client.get_role(role_id)
+ self.assertIn('status', resp)
+ self.assertTrue('200', resp['status'])
+ self.assertEqual(role_id, body['id'])
+ self.assertEqual(role_name, body['name'])
+
+ @test.attr(type='gate')
def test_assign_user_role(self):
# Assign a role to a user on a tenant
(user, tenant, role) = self._get_role_params()
@@ -92,7 +102,7 @@
user['id'], role['id'])
resp, body = self.client.remove_user_role(tenant['id'], user['id'],
user_role['id'])
- self.assertEqual(resp['status'], '204')
+ self.assertEqual(204, resp.status)
@test.attr(type='gate')
def test_list_user_roles(self):
diff --git a/tempest/api/identity/admin/test_roles_negative.py b/tempest/api/identity/admin/test_roles_negative.py
index 7a0bdea..d311143 100644
--- a/tempest/api/identity/admin/test_roles_negative.py
+++ b/tempest/api/identity/admin/test_roles_negative.py
@@ -74,8 +74,7 @@
role_name = data_utils.rand_name(name='role-dup-')
resp, body = self.client.create_role(role_name)
role1_id = body.get('id')
- self.assertIn('status', resp)
- self.assertTrue(resp['status'].startswith('2'))
+ self.assertEqual(200, resp.status)
self.addCleanup(self.client.delete_role, role1_id)
self.assertRaises(exceptions.Conflict, self.client.create_role,
role_name)
diff --git a/tempest/api/identity/admin/test_services.py b/tempest/api/identity/admin/test_services.py
index 459c44c..e5cb348 100644
--- a/tempest/api/identity/admin/test_services.py
+++ b/tempest/api/identity/admin/test_services.py
@@ -27,7 +27,7 @@
def _del_service(self, service_id):
# Deleting the service created in this method
resp, _ = self.client.delete_service(service_id)
- self.assertEqual(resp['status'], '204')
+ self.assertEqual(204, resp.status)
# Checking whether service is deleted successfully
self.assertRaises(exceptions.NotFound, self.client.get_service,
service_id)
@@ -43,7 +43,7 @@
name, type, description=description)
self.assertFalse(service_data['id'] is None)
self.addCleanup(self._del_service, service_data['id'])
- self.assertTrue(resp['status'].startswith('2'))
+ self.assertEqual(200, resp.status)
# Verifying response body of create service
self.assertIn('id', service_data)
self.assertIn('name', service_data)
@@ -54,7 +54,7 @@
self.assertEqual(description, service_data['description'])
# Get service
resp, fetched_service = self.client.get_service(service_data['id'])
- self.assertTrue(resp['status'].startswith('2'))
+ self.assertEqual(200, resp.status)
# verifying the existence of service created
self.assertIn('id', fetched_service)
self.assertEqual(fetched_service['id'], service_data['id'])
@@ -66,6 +66,20 @@
self.assertEqual(fetched_service['description'],
service_data['description'])
+ @attr(type='gate')
+ def test_create_service_without_description(self):
+ # Create a service only with name and type
+ name = data_utils.rand_name('service-')
+ type = data_utils.rand_name('type--')
+ resp, service = self.client.create_service(name, type)
+ self.assertIn('id', service)
+ self.assertTrue('200', resp['status'])
+ self.addCleanup(self._del_service, service['id'])
+ self.assertIn('name', service)
+ self.assertEqual(name, service['name'])
+ self.assertIn('type', service)
+ self.assertEqual(type, service['type'])
+
@attr(type='smoke')
def test_list_services(self):
# Create, List, Verify and Delete Services
@@ -86,7 +100,7 @@
self.addCleanup(delete_services)
# List and Verify Services
resp, body = self.client.list_services()
- self.assertTrue(resp['status'].startswith('2'))
+ self.assertEqual(200, resp.status)
found = [service for service in body if service['id'] in service_ids]
self.assertEqual(len(found), len(services), 'Services not found')
diff --git a/tempest/api/identity/admin/test_tenants.py b/tempest/api/identity/admin/test_tenants.py
index 257a6d7..7ba46bb 100644
--- a/tempest/api/identity/admin/test_tenants.py
+++ b/tempest/api/identity/admin/test_tenants.py
@@ -35,13 +35,13 @@
tenants.append(tenant)
tenant_ids = map(lambda x: x['id'], tenants)
resp, body = self.client.list_tenants()
- self.assertTrue(resp['status'].startswith('2'))
+ self.assertEqual(200, resp.status)
found = [tenant for tenant in body if tenant['id'] in tenant_ids]
self.assertEqual(len(found), len(tenants), 'Tenants not created')
for tenant in tenants:
resp, body = self.client.delete_tenant(tenant['id'])
- self.assertTrue(resp['status'].startswith('2'))
+ self.assertEqual(204, resp.status)
self.data.tenants.remove(tenant)
resp, body = self.client.list_tenants()
@@ -57,10 +57,9 @@
description=tenant_desc)
tenant = body
self.data.tenants.append(tenant)
- st1 = resp['status']
tenant_id = body['id']
desc1 = body['description']
- self.assertTrue(st1.startswith('2'))
+ self.assertEqual(200, resp.status)
self.assertEqual(desc1, tenant_desc, 'Description should have '
'been sent in response for create')
resp, body = self.client.get_tenant(tenant_id)
@@ -78,9 +77,8 @@
tenant = body
self.data.tenants.append(tenant)
tenant_id = body['id']
- st1 = resp['status']
en1 = body['enabled']
- self.assertTrue(st1.startswith('2'))
+ self.assertEqual(200, resp.status)
self.assertTrue(en1, 'Enable should be True in response')
resp, body = self.client.get_tenant(tenant_id)
en2 = body['enabled']
@@ -96,9 +94,8 @@
tenant = body
self.data.tenants.append(tenant)
tenant_id = body['id']
- st1 = resp['status']
en1 = body['enabled']
- self.assertTrue(st1.startswith('2'))
+ self.assertEqual(200, resp.status)
self.assertEqual('false', str(en1).lower(),
'Enable should be False in response')
resp, body = self.client.get_tenant(tenant_id)
@@ -122,9 +119,8 @@
t_name2 = data_utils.rand_name(name='tenant2-')
resp, body = self.client.update_tenant(t_id, name=t_name2)
- st2 = resp['status']
resp2_name = body['name']
- self.assertTrue(st2.startswith('2'))
+ self.assertEqual(200, resp.status)
self.assertNotEqual(resp1_name, resp2_name)
resp, body = self.client.get_tenant(t_id)
@@ -152,9 +148,8 @@
t_desc2 = data_utils.rand_name(name='desc2-')
resp, body = self.client.update_tenant(t_id, description=t_desc2)
- st2 = resp['status']
resp2_desc = body['description']
- self.assertTrue(st2.startswith('2'))
+ self.assertEqual(200, resp.status)
self.assertNotEqual(resp1_desc, resp2_desc)
resp, body = self.client.get_tenant(t_id)
@@ -182,9 +177,8 @@
t_en2 = True
resp, body = self.client.update_tenant(t_id, enabled=t_en2)
- st2 = resp['status']
resp2_en = body['enabled']
- self.assertTrue(st2.startswith('2'))
+ self.assertEqual(200, resp.status)
self.assertNotEqual(resp1_en, resp2_en)
resp, body = self.client.get_tenant(t_id)
diff --git a/tempest/api/identity/admin/test_tokens.py b/tempest/api/identity/admin/test_tokens.py
index c931bcf..7fec28d 100644
--- a/tempest/api/identity/admin/test_tokens.py
+++ b/tempest/api/identity/admin/test_tokens.py
@@ -72,11 +72,16 @@
self.assertEqual(200, resp.status)
self.data.users.append(user)
- # Create a tenant.
- tenant_name = data_utils.rand_name(name='tenant-')
- resp, tenant = self.client.create_tenant(tenant_name)
+ # Create a couple tenants.
+ tenant1_name = data_utils.rand_name(name='tenant-')
+ resp, tenant1 = self.client.create_tenant(tenant1_name)
self.assertEqual(200, resp.status)
- self.data.tenants.append(tenant)
+ self.data.tenants.append(tenant1)
+
+ tenant2_name = data_utils.rand_name(name='tenant-')
+ resp, tenant2 = self.client.create_tenant(tenant2_name)
+ self.assertEqual(200, resp.status)
+ self.data.tenants.append(tenant2)
# Create a role
role_name = data_utils.rand_name(name='role-')
@@ -84,8 +89,12 @@
self.assertEqual(200, resp.status)
self.data.roles.append(role)
- # Grant the user the role on the tenant.
- resp, _ = self.client.assign_user_role(tenant['id'], user['id'],
+ # Grant the user the role on the tenants.
+ resp, _ = self.client.assign_user_role(tenant1['id'], user['id'],
+ role['id'])
+ self.assertEqual(200, resp.status)
+
+ resp, _ = self.client.assign_user_role(tenant2['id'], user['id'],
role['id'])
self.assertEqual(200, resp.status)
@@ -95,10 +104,20 @@
token_id = body['token']['id']
- # Use the unscoped token to get a scoped token.
- rsp, body = self.token_client.auth_token(token_id, tenant=tenant_name)
+ # Use the unscoped token to get a token scoped to tenant1
+ rsp, body = self.token_client.auth_token(token_id, tenant=tenant1_name)
self.assertEqual(200, resp.status)
+ scoped_token_id = body['token']['id']
+
+ # Revoke the scoped token
+ resp, body = self.client.delete_token(scoped_token_id)
+ self.assertEqual(204, resp.status)
+
+ # Use the unscoped token to get a token scoped to tenant2
+ rsp, body = self.token_client.auth_token(token_id, tenant=tenant2_name)
+ self.assertEqual(204, resp.status)
+
class TokensTestXML(TokensTestJSON):
_interface = 'xml'
diff --git a/tempest/api/identity/admin/v3/test_roles.py b/tempest/api/identity/admin/v3/test_roles.py
index 24c7b83..90dccca 100644
--- a/tempest/api/identity/admin/v3/test_roles.py
+++ b/tempest/api/identity/admin/v3/test_roles.py
@@ -73,7 +73,7 @@
self.assertIn(role_id, fetched_role_ids)
@test.attr(type='smoke')
- def test_role_create_update_get(self):
+ def test_role_create_update_get_list(self):
r_name = data_utils.rand_name('Role-')
resp, role = self.client.create_role(r_name)
self.addCleanup(self.client.delete_role, role['id'])
@@ -94,6 +94,10 @@
self.assertEqual(new_name, new_role['name'])
self.assertEqual(updated_role['id'], new_role['id'])
+ resp, roles = self.client.list_roles()
+ self.assertEqual(resp['status'], '200')
+ self.assertIn(role['id'], [r['id'] for r in roles])
+
@test.attr(type='smoke')
def test_grant_list_revoke_role_to_user_on_project(self):
resp, _ = self.client.assign_user_role_on_project(
diff --git a/tempest/api/identity/admin/v3/test_tokens.py b/tempest/api/identity/admin/v3/test_tokens.py
index 9629213..ebc1cac 100644
--- a/tempest/api/identity/admin/v3/test_tokens.py
+++ b/tempest/api/identity/admin/v3/test_tokens.py
@@ -33,15 +33,15 @@
resp, user = self.client.create_user(
u_name, description=u_desc, password=u_password,
email=u_email)
- self.assertTrue(resp['status'].startswith('2'))
+ self.assertEqual(201, resp.status)
self.addCleanup(self.client.delete_user, user['id'])
# Perform Authentication
resp, body = self.token.auth(user['id'], u_password)
- self.assertEqual(resp['status'], '201')
+ self.assertEqual(201, resp.status)
subject_token = resp['x-subject-token']
# Perform GET Token
resp, token_details = self.client.get_token(subject_token)
- self.assertEqual(resp['status'], '200')
+ self.assertEqual(200, resp.status)
self.assertEqual(resp['x-subject-token'], subject_token)
self.assertEqual(token_details['user']['id'], user['id'])
self.assertEqual(token_details['user']['name'], u_name)
@@ -50,6 +50,115 @@
self.assertRaises(exceptions.NotFound, self.client.get_token,
subject_token)
+ @attr(type='gate')
+ def test_rescope_token(self):
+ """Rescope a token.
+
+ An unscoped token can be requested, that token can be used to request a
+ scoped token. The scoped token can be revoked, and the original token
+ used to get a token in a different project.
+
+ """
+
+ # Create a user.
+ user_name = data_utils.rand_name(name='user-')
+ user_password = data_utils.rand_name(name='pass-')
+ resp, user = self.client.create_user(user_name, password=user_password)
+ self.assertEqual(201, resp.status)
+ self.addCleanup(self.client.delete_user, user['id'])
+
+ # Create a couple projects
+ project1_name = data_utils.rand_name(name='project-')
+ resp, project1 = self.client.create_project(project1_name)
+ self.assertEqual(201, resp.status)
+ self.addCleanup(self.client.delete_project, project1['id'])
+
+ project2_name = data_utils.rand_name(name='project-')
+ resp, project2 = self.client.create_project(project2_name)
+ self.assertEqual(201, resp.status)
+ self.addCleanup(self.client.delete_project, project2['id'])
+
+ # Create a role
+ role_name = data_utils.rand_name(name='role-')
+ resp, role = self.client.create_role(role_name)
+ self.assertEqual(201, resp.status)
+ self.addCleanup(self.client.delete_role, role['id'])
+
+ # Grant the user the role on both projects.
+ resp, _ = self.client.assign_user_role(project1['id'], user['id'],
+ role['id'])
+ self.assertEqual(204, resp.status)
+
+ resp, _ = self.client.assign_user_role(project2['id'], user['id'],
+ role['id'])
+ self.assertEqual(204, resp.status)
+
+ # Get an unscoped token.
+ resp, token_auth = self.token.auth(user=user['id'],
+ password=user_password)
+ self.assertEqual(201, resp.status)
+
+ token_id = resp['x-subject-token']
+ orig_expires_at = token_auth['token']['expires_at']
+ orig_issued_at = token_auth['token']['issued_at']
+ orig_user = token_auth['token']['user']
+
+ self.assertIsInstance(token_auth['token']['expires_at'], unicode)
+ self.assertIsInstance(token_auth['token']['issued_at'], unicode)
+ self.assertEqual(['password'], token_auth['token']['methods'])
+ self.assertEqual(user['id'], token_auth['token']['user']['id'])
+ self.assertEqual(user['name'], token_auth['token']['user']['name'])
+ self.assertEqual('default',
+ token_auth['token']['user']['domain']['id'])
+ self.assertEqual('Default',
+ token_auth['token']['user']['domain']['name'])
+ self.assertNotIn('catalog', token_auth['token'])
+ self.assertNotIn('project', token_auth['token'])
+ self.assertNotIn('roles', token_auth['token'])
+
+ # Use the unscoped token to get a scoped token.
+ resp, token_auth = self.token.auth(token=token_id,
+ tenant=project1_name,
+ domain='Default')
+ token1_id = resp['x-subject-token']
+ self.assertEqual(201, resp.status)
+
+ self.assertEqual(orig_expires_at, token_auth['token']['expires_at'],
+ 'Expiration time should match original token')
+ self.assertIsInstance(token_auth['token']['issued_at'], unicode)
+ self.assertNotEqual(orig_issued_at, token_auth['token']['issued_at'])
+ self.assertEqual(set(['password', 'token']),
+ set(token_auth['token']['methods']))
+ self.assertEqual(orig_user, token_auth['token']['user'],
+ 'User should match original token')
+ self.assertIsInstance(token_auth['token']['catalog'], list)
+ self.assertEqual(project1['id'],
+ token_auth['token']['project']['id'])
+ self.assertEqual(project1['name'],
+ token_auth['token']['project']['name'])
+ self.assertEqual('default',
+ token_auth['token']['project']['domain']['id'])
+ self.assertEqual('Default',
+ token_auth['token']['project']['domain']['name'])
+ self.assertEqual(1, len(token_auth['token']['roles']))
+ self.assertEqual(role['id'], token_auth['token']['roles'][0]['id'])
+ self.assertEqual(role['name'], token_auth['token']['roles'][0]['name'])
+
+ # Revoke the unscoped token.
+ resp, _ = self.client.delete_token(token1_id)
+ self.assertEqual(204, resp.status)
+
+ # Now get another scoped token using the unscoped token.
+ resp, token_auth = self.token.auth(token=token_id,
+ tenant=project2_name,
+ domain='Default')
+ self.assertEqual(201, resp.status)
+
+ self.assertEqual(project2['id'],
+ token_auth['token']['project']['id'])
+ self.assertEqual(project2['name'],
+ token_auth['token']['project']['name'])
+
class TokensV3TestXML(TokensV3TestJSON):
_interface = 'xml'
diff --git a/tempest/api/image/v1/test_images.py b/tempest/api/image/v1/test_images.py
index 517123d..8466c7b 100644
--- a/tempest/api/image/v1/test_images.py
+++ b/tempest/api/image/v1/test_images.py
@@ -109,6 +109,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(ListImagesTest, cls).setUpClass()
# We add a few images here to test the listing functionality of
@@ -244,6 +245,7 @@
class ListSnapshotImagesTest(base.BaseV1ImageTest):
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(ListSnapshotImagesTest, cls).setUpClass()
if not CONF.compute_feature_enabled.api_v3:
@@ -268,7 +270,7 @@
@classmethod
def tearDownClass(cls):
- for server in cls.servers:
+ for server in getattr(cls, "servers", []):
cls.servers_client.delete_server(server['id'])
super(ListSnapshotImagesTest, cls).tearDownClass()
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index abde8f7..2592409 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -135,6 +135,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(ListImagesTest, cls).setUpClass()
# We add a few images here to test the listing functionality of
diff --git a/tempest/api/network/admin/test_agent_management.py b/tempest/api/network/admin/test_agent_management.py
index 342bc6a..b848994 100644
--- a/tempest/api/network/admin/test_agent_management.py
+++ b/tempest/api/network/admin/test_agent_management.py
@@ -37,8 +37,10 @@
agents = body['agents']
# Hearthbeats must be excluded from comparison
self.agent.pop('heartbeat_timestamp', None)
+ self.agent.pop('configurations', None)
for agent in agents:
agent.pop('heartbeat_timestamp', None)
+ agent.pop('configurations', None)
self.assertIn(self.agent, agents)
@test.attr(type=['smoke'])
diff --git a/tempest/api/network/admin/test_dhcp_agent_scheduler.py b/tempest/api/network/admin/test_dhcp_agent_scheduler.py
index 0e601d1..25e1cc0 100644
--- a/tempest/api/network/admin/test_dhcp_agent_scheduler.py
+++ b/tempest/api/network/admin/test_dhcp_agent_scheduler.py
@@ -20,6 +20,7 @@
_interface = 'json'
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(DHCPAgentSchedulersTestJSON, cls).setUpClass()
if not test.is_extension_enabled('dhcp_agent_scheduler', 'network'):
@@ -59,17 +60,31 @@
return network_id in network_ids
@test.attr(type='smoke')
- def test_remove_network_from_dhcp_agent(self):
+ def test_add_remove_network_from_dhcp_agent(self):
# The agent is now bound to the network, we can free the port
self.client.delete_port(self.port['id'])
self.ports.remove(self.port)
- resp, body = self.admin_client.list_dhcp_agent_hosting_network(
- self.network['id'])
+ agent = dict()
+ agent['agent_type'] = None
+ resp, body = self.admin_client.list_agents()
agents = body['agents']
- self.assertIsNotNone(agents)
- # Get an agent.
- agent = agents[0]
- network_id = self.network['id']
+ for a in agents:
+ if a['agent_type'] == 'DHCP agent':
+ agent = a
+ break
+ self.assertEqual(agent['agent_type'], 'DHCP agent', 'Could not find '
+ 'DHCP agent in agent list though dhcp_agent_scheduler'
+ ' is enabled.')
+ network = self.create_network()
+ network_id = network['id']
+ if self._check_network_in_dhcp_agent(network_id, agent):
+ self._remove_network_from_dhcp_agent(network_id, agent)
+ self._add_dhcp_agent_to_network(network_id, agent)
+ else:
+ self._add_dhcp_agent_to_network(network_id, agent)
+ self._remove_network_from_dhcp_agent(network_id, agent)
+
+ def _remove_network_from_dhcp_agent(self, network_id, agent):
resp, body = self.admin_client.remove_network_from_dhcp_agent(
agent_id=agent['id'],
network_id=network_id)
@@ -77,6 +92,13 @@
self.assertFalse(self._check_network_in_dhcp_agent(
network_id, agent))
+ def _add_dhcp_agent_to_network(self, network_id, agent):
+ resp, body = self.admin_client.add_dhcp_agent_to_network(
+ agent['id'], network_id)
+ self.assertEqual(resp['status'], '201')
+ self.assertTrue(self._check_network_in_dhcp_agent(
+ network_id, agent))
+
class DHCPAgentSchedulersTestXML(DHCPAgentSchedulersTestJSON):
_interface = 'xml'
diff --git a/tempest/api/network/admin/test_lbaas_agent_scheduler.py b/tempest/api/network/admin/test_lbaas_agent_scheduler.py
index a5ba90f..675c62d 100644
--- a/tempest/api/network/admin/test_lbaas_agent_scheduler.py
+++ b/tempest/api/network/admin/test_lbaas_agent_scheduler.py
@@ -35,6 +35,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(LBaaSAgentSchedulerTestJSON, cls).setUpClass()
if not test.is_extension_enabled('lbaas_agent_scheduler', 'network'):
diff --git a/tempest/api/network/admin/test_load_balancer_admin_actions.py b/tempest/api/network/admin/test_load_balancer_admin_actions.py
index df1e0d5..bc7f1d6 100644
--- a/tempest/api/network/admin/test_load_balancer_admin_actions.py
+++ b/tempest/api/network/admin/test_load_balancer_admin_actions.py
@@ -29,6 +29,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(LoadBalancerAdminTestJSON, cls).setUpClass()
if not test.is_extension_enabled('lbaas', 'network'):
diff --git a/tempest/api/network/base.py b/tempest/api/network/base.py
index f92ad68..696a1c3 100644
--- a/tempest/api/network/base.py
+++ b/tempest/api/network/base.py
@@ -79,9 +79,17 @@
cls.floating_ips = []
cls.metering_labels = []
cls.metering_label_rules = []
+ cls.fw_rules = []
+ cls.fw_policies = []
@classmethod
def tearDownClass(cls):
+ # Clean up firewall policies
+ for fw_policy in cls.fw_policies:
+ cls.client.delete_firewall_policy(fw_policy['id'])
+ # Clean up firewall rules
+ for fw_rule in cls.fw_rules:
+ cls.client.delete_firewall_rule(fw_rule['id'])
# Clean up ike policies
for ikepolicy in cls.ikepolicies:
cls.client.delete_ikepolicy(ikepolicy['id'])
@@ -93,12 +101,8 @@
cls.client.delete_floatingip(floating_ip['id'])
# Clean up routers
for router in cls.routers:
- resp, body = cls.client.list_router_interfaces(router['id'])
- interfaces = body['ports']
- for i in interfaces:
- cls.client.remove_router_interface_with_subnet_id(
- router['id'], i['fixed_ips'][0]['subnet_id'])
- cls.client.delete_router(router['id'])
+ cls.delete_router(router)
+
# Clean up health monitors
for health_monitor in cls.health_monitors:
cls.client.delete_health_monitor(health_monitor['id'])
@@ -296,6 +300,35 @@
cls.ikepolicies.append(ikepolicy)
return ikepolicy
+ @classmethod
+ def create_firewall_rule(cls, action, protocol):
+ """Wrapper utility that returns a test firewall rule."""
+ resp, body = cls.client.create_firewall_rule(
+ name=data_utils.rand_name("fw-rule"),
+ action=action,
+ protocol=protocol)
+ fw_rule = body['firewall_rule']
+ cls.fw_rules.append(fw_rule)
+ return fw_rule
+
+ @classmethod
+ def create_firewall_policy(cls):
+ """Wrapper utility that returns a test firewall policy."""
+ resp, body = cls.client.create_firewall_policy(
+ name=data_utils.rand_name("fw-policy"))
+ fw_policy = body['firewall_policy']
+ cls.fw_policies.append(fw_policy)
+ return fw_policy
+
+ @classmethod
+ def delete_router(cls, router):
+ resp, body = cls.client.list_router_interfaces(router['id'])
+ interfaces = body['ports']
+ for i in interfaces:
+ cls.client.remove_router_interface_with_subnet_id(
+ router['id'], i['fixed_ips'][0]['subnet_id'])
+ cls.client.delete_router(router['id'])
+
class BaseAdminNetworkTest(BaseNetworkTest):
diff --git a/tempest/api/network/test_extra_dhcp_options.py b/tempest/api/network/test_extra_dhcp_options.py
index ed86d75..371c651 100644
--- a/tempest/api/network/test_extra_dhcp_options.py
+++ b/tempest/api/network/test_extra_dhcp_options.py
@@ -36,6 +36,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(ExtraDHCPOptionsTestJSON, cls).setUpClass()
if not test.is_extension_enabled('extra_dhcp_opt', 'network'):
diff --git a/tempest/api/network/test_floating_ips.py b/tempest/api/network/test_floating_ips.py
index 06871ad..7191940 100644
--- a/tempest/api/network/test_floating_ips.py
+++ b/tempest/api/network/test_floating_ips.py
@@ -44,6 +44,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(FloatingIPTestJSON, cls).setUpClass()
if not test.is_extension_enabled('router', 'network'):
diff --git a/tempest/api/network/test_fwaas_extensions.py b/tempest/api/network/test_fwaas_extensions.py
new file mode 100644
index 0000000..0647069
--- /dev/null
+++ b/tempest/api/network/test_fwaas_extensions.py
@@ -0,0 +1,207 @@
+# 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.api.network import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest import test
+
+
+class FWaaSExtensionTestJSON(base.BaseNetworkTest):
+ _interface = 'json'
+
+ """
+ Tests the following operations in the Neutron API using the REST client for
+ Neutron:
+
+ List firewall rules
+ Create firewall rule
+ Update firewall rule
+ Delete firewall rule
+ Show firewall rule
+ List firewall policies
+ Create firewall policy
+ Update firewall policy
+ Delete firewall policy
+ Show firewall policy
+ List firewall
+ Create firewall
+ Update firewall
+ Delete firewall
+ Show firewall
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super(FWaaSExtensionTestJSON, cls).setUpClass()
+ if not test.is_extension_enabled('fwaas', 'network'):
+ msg = "FWaaS Extension not enabled."
+ raise cls.skipException(msg)
+ cls.fw_rule = cls.create_firewall_rule("allow", "tcp")
+ cls.fw_policy = cls.create_firewall_policy()
+
+ def _try_delete_policy(self, policy_id):
+ # delete policy, if it exists
+ try:
+ self.client.delete_firewall_policy(policy_id)
+ # if policy is not found, this means it was deleted in the test
+ except exceptions.NotFound:
+ pass
+
+ def _try_delete_firewall(self, fw_id):
+ # delete firewall, if it exists
+ try:
+ self.client.delete_firewall(fw_id)
+ # if firewall is not found, this means it was deleted in the test
+ except exceptions.NotFound:
+ pass
+
+ @test.attr(type='smoke')
+ def test_list_firewall_rules(self):
+ # List firewall rules
+ resp, fw_rules = self.client.list_firewall_rules()
+ self.assertEqual('200', resp['status'])
+ fw_rules = fw_rules['firewall_rules']
+ self.assertIn((self.fw_rule['id'],
+ self.fw_rule['name'],
+ self.fw_rule['action'],
+ self.fw_rule['protocol'],
+ self.fw_rule['ip_version'],
+ self.fw_rule['enabled']),
+ [(m['id'],
+ m['name'],
+ m['action'],
+ m['protocol'],
+ m['ip_version'],
+ m['enabled']) for m in fw_rules])
+
+ @test.attr(type='smoke')
+ def test_create_update_delete_firewall_rule(self):
+ # Create firewall rule
+ resp, body = self.client.create_firewall_rule(
+ name=data_utils.rand_name("fw-rule"),
+ action="allow",
+ protocol="tcp")
+ self.assertEqual('201', resp['status'])
+ fw_rule_id = body['firewall_rule']['id']
+
+ # Update firewall rule
+ resp, body = self.client.update_firewall_rule(fw_rule_id,
+ shared=True)
+ self.assertEqual('200', resp['status'])
+ self.assertTrue(body["firewall_rule"]['shared'])
+
+ # Delete firewall rule
+ resp, _ = self.client.delete_firewall_rule(fw_rule_id)
+ self.assertEqual('204', resp['status'])
+ # Confirm deletion
+ resp, fw_rules = self.client.list_firewall_rules()
+ self.assertNotIn(fw_rule_id,
+ [m['id'] for m in fw_rules['firewall_rules']])
+
+ @test.attr(type='smoke')
+ def test_show_firewall_rule(self):
+ # show a created firewall rule
+ resp, fw_rule = self.client.show_firewall_rule(self.fw_rule['id'])
+ self.assertEqual('200', resp['status'])
+ for key, value in fw_rule['firewall_rule'].iteritems():
+ self.assertEqual(self.fw_rule[key], value)
+
+ @test.attr(type='smoke')
+ def test_list_firewall_policies(self):
+ resp, fw_policies = self.client.list_firewall_policies()
+ self.assertEqual('200', resp['status'])
+ fw_policies = fw_policies['firewall_policies']
+ self.assertIn((self.fw_policy['id'],
+ self.fw_policy['name'],
+ self.fw_policy['firewall_rules']),
+ [(m['id'],
+ m['name'],
+ m['firewall_rules']) for m in fw_policies])
+
+ @test.attr(type='smoke')
+ def test_create_update_delete_firewall_policy(self):
+ # Create firewall policy
+ resp, body = self.client.create_firewall_policy(
+ name=data_utils.rand_name("fw-policy"))
+ self.assertEqual('201', resp['status'])
+ fw_policy_id = body['firewall_policy']['id']
+ self.addCleanup(self._try_delete_policy, fw_policy_id)
+
+ # Update firewall policy
+ resp, body = self.client.update_firewall_policy(fw_policy_id,
+ shared=True,
+ name="updated_policy")
+ self.assertEqual('200', resp['status'])
+ updated_fw_policy = body["firewall_policy"]
+ self.assertTrue(updated_fw_policy['shared'])
+ self.assertEqual("updated_policy", updated_fw_policy['name'])
+
+ # Delete firewall policy
+ resp, _ = self.client.delete_firewall_policy(fw_policy_id)
+ self.assertEqual('204', resp['status'])
+ # Confirm deletion
+ resp, fw_policies = self.client.list_firewall_policies()
+ fw_policies = fw_policies['firewall_policies']
+ self.assertNotIn(fw_policy_id, [m['id'] for m in fw_policies])
+
+ @test.attr(type='smoke')
+ def test_show_firewall_policy(self):
+ # show a created firewall policy
+ resp, fw_policy = self.client.show_firewall_policy(
+ self.fw_policy['id'])
+ self.assertEqual('200', resp['status'])
+ fw_policy = fw_policy['firewall_policy']
+ for key, value in fw_policy.iteritems():
+ self.assertEqual(self.fw_policy[key], value)
+
+ @test.attr(type='smoke')
+ def test_create_show_delete_firewall(self):
+ # Create firewall
+ resp, body = self.client.create_firewall(
+ name=data_utils.rand_name("firewall"),
+ firewall_policy_id=self.fw_policy['id'])
+ self.assertEqual('201', resp['status'])
+ created_firewall = body['firewall']
+ firewall_id = created_firewall['id']
+ self.addCleanup(self._try_delete_firewall, firewall_id)
+
+ # show a created firewall
+ resp, firewall = self.client.show_firewall(firewall_id)
+ self.assertEqual('200', resp['status'])
+ firewall = firewall['firewall']
+ for key, value in firewall.iteritems():
+ self.assertEqual(created_firewall[key], value)
+
+ # list firewall
+ resp, firewalls = self.client.list_firewalls()
+ self.assertEqual('200', resp['status'])
+ firewalls = firewalls['firewalls']
+ self.assertIn((created_firewall['id'],
+ created_firewall['name'],
+ created_firewall['firewall_policy_id']),
+ [(m['id'],
+ m['name'],
+ m['firewall_policy_id']) for m in firewalls])
+
+ # Delete firewall
+ resp, _ = self.client.delete_firewall(firewall_id)
+ self.assertEqual('204', resp['status'])
+ # Confirm deletion
+ # TODO(raies): Confirm deletion can be done only when,
+ # deleted firewall status is not "PENDING_DELETE".
+
+
+class FWaaSExtensionTestXML(FWaaSExtensionTestJSON):
+ _interface = 'xml'
diff --git a/tempest/api/network/test_load_balancer.py b/tempest/api/network/test_load_balancer.py
index e2deecc..673fc47 100644
--- a/tempest/api/network/test_load_balancer.py
+++ b/tempest/api/network/test_load_balancer.py
@@ -38,6 +38,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(LoadBalancerTestJSON, cls).setUpClass()
if not test.is_extension_enabled('lbaas', 'network'):
diff --git a/tempest/api/network/test_networks.py b/tempest/api/network/test_networks.py
index b9041ee..0175de7 100644
--- a/tempest/api/network/test_networks.py
+++ b/tempest/api/network/test_networks.py
@@ -19,7 +19,7 @@
from tempest.common.utils import data_utils
from tempest import config
from tempest import exceptions
-from tempest.test import attr
+from tempest import test
CONF = config.CONF
@@ -64,6 +64,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(NetworksTestJSON, cls).setUpClass()
cls.network = cls.create_network()
@@ -71,7 +72,7 @@
cls.subnet = cls.create_subnet(cls.network)
cls.cidr = cls.subnet['cidr']
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_create_update_delete_network_subnet(self):
# Create a network
name = data_utils.rand_name('network-')
@@ -102,7 +103,7 @@
resp, body = self.client.delete_network(net_id)
self.assertEqual('204', resp['status'])
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_show_network(self):
# Verify the details of a network
resp, body = self.client.show_network(self.network['id'])
@@ -111,7 +112,7 @@
for key in ['id', 'name']:
self.assertEqual(network[key], self.network[key])
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_show_network_fields(self):
# Verify specific fields of a network
field_list = [('fields', 'id'), ('fields', 'name'), ]
@@ -123,7 +124,7 @@
for label, field_name in field_list:
self.assertEqual(network[field_name], self.network[field_name])
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_list_networks(self):
# Verify the network exists in the list of all networks
resp, body = self.client.list_networks()
@@ -132,7 +133,7 @@
if network['id'] == self.network['id']]
self.assertNotEmpty(networks, "Created network not found in the list")
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_list_networks_fields(self):
# Verify specific fields of the networks
resp, body = self.client.list_networks(fields='id')
@@ -143,7 +144,7 @@
self.assertEqual(len(network), 1)
self.assertIn('id', network)
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_show_subnet(self):
# Verify the details of a subnet
resp, body = self.client.show_subnet(self.subnet['id'])
@@ -154,7 +155,7 @@
self.assertIn(key, subnet)
self.assertEqual(subnet[key], self.subnet[key])
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_show_subnet_fields(self):
# Verify specific fields of a subnet
field_list = [('fields', 'id'), ('fields', 'cidr'), ]
@@ -166,7 +167,7 @@
for label, field_name in field_list:
self.assertEqual(subnet[field_name], self.subnet[field_name])
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_list_subnets(self):
# Verify the subnet exists in the list of all subnets
resp, body = self.client.list_subnets()
@@ -175,7 +176,7 @@
if subnet['id'] == self.subnet['id']]
self.assertNotEmpty(subnets, "Created subnet not found in the list")
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_list_subnets_fields(self):
# Verify specific fields of subnets
resp, body = self.client.list_subnets(fields='id')
@@ -194,7 +195,7 @@
except exceptions.NotFound:
pass
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_delete_network_with_subnet(self):
# Creates a network
name = data_utils.rand_name('network-')
@@ -221,7 +222,7 @@
# it from the list.
self.subnets.pop()
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_create_port_with_no_ip(self):
# For this test create a new network - do not use any previously
# created networks.
@@ -275,6 +276,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(BulkNetworkOpsTestJSON, cls).setUpClass()
cls.network1 = cls.create_network()
@@ -310,7 +312,7 @@
for n in created_ports:
self.assertNotIn(n['id'], ports_list)
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_bulk_create_delete_network(self):
# Creates 2 networks in one request
network_names = [data_utils.rand_name('network-'),
@@ -326,7 +328,7 @@
self.assertIsNotNone(n['id'])
self.assertIn(n['id'], networks_list)
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_bulk_create_delete_subnet(self):
# Creates 2 subnets in one request
cidr = netaddr.IPNetwork(CONF.network.tenant_network_cidr)
@@ -358,7 +360,7 @@
self.assertIsNotNone(n['id'])
self.assertIn(n['id'], subnets_list)
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_bulk_create_delete_port(self):
# Creates 2 ports in one request
networks = [self.network1['id'], self.network2['id']]
diff --git a/tempest/api/network/test_ports.py b/tempest/api/network/test_ports.py
index fbb25a8..bda5b2b 100644
--- a/tempest/api/network/test_ports.py
+++ b/tempest/api/network/test_ports.py
@@ -27,6 +27,7 @@
_interface = 'json'
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(PortsTestJSON, cls).setUpClass()
cls.network = cls.create_network()
@@ -143,6 +144,7 @@
_interface = 'json'
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(PortsAdminExtendedAttrsTestJSON, cls).setUpClass()
cls.identity_client = cls._get_identity_admin_client()
@@ -180,15 +182,31 @@
@test.attr(type='smoke')
def test_list_ports_binding_ext_attr(self):
- resp, body = self.admin_client.list_ports(
- **{'tenant_id': self.tenant['id']})
+ # Create a new port
+ post_body = {"network_id": self.network['id']}
+ resp, body = self.admin_client.create_port(**post_body)
+ self.assertEqual('201', resp['status'])
+ port = body['port']
+ self.addCleanup(self.admin_client.delete_port, port['id'])
+
+ # Update the port's binding attributes so that is now 'bound'
+ # to a host
+ update_body = {"binding:host_id": self.host_id}
+ resp, _ = self.admin_client.update_port(port['id'], **update_body)
+ self.assertEqual('200', resp['status'])
+
+ # List all ports, ensure new port is part of list and its binding
+ # attributes are set and accurate
+ resp, body = self.admin_client.list_ports()
self.assertEqual('200', resp['status'])
ports_list = body['ports']
- for port in ports_list:
- vif_type = port['binding:vif_type']
- self.assertIsNotNone(vif_type)
- vif_details = port['binding:vif_details']['port_filter']
- self.assertIsNotNone(vif_details)
+ pids_list = [p['id'] for p in ports_list]
+ self.assertIn(port['id'], pids_list)
+ listed_port = [p for p in ports_list if p['id'] == port['id']]
+ self.assertEqual(1, len(listed_port),
+ 'Multiple ports listed with id %s in ports listing: '
+ '%s' % (port['id'], ports_list))
+ self.assertEqual(self.host_id, listed_port[0]['binding:host_id'])
@test.attr(type='smoke')
def test_show_port_binding_ext_attr(self):
diff --git a/tempest/api/network/test_routers.py b/tempest/api/network/test_routers.py
index 2657031..4cc0338 100644
--- a/tempest/api/network/test_routers.py
+++ b/tempest/api/network/test_routers.py
@@ -36,6 +36,18 @@
admin_manager = clients.AdminManager()
cls.identity_admin_client = admin_manager.identity_client
+ def _cleanup_router(self, router):
+ self.delete_router(router)
+ self.routers.remove(router)
+
+ def _create_router(self, name, admin_state_up=False,
+ external_network_id=None, enable_snat=None):
+ # associate a cleanup with created routers to avoid quota limits
+ router = self.create_router(name, admin_state_up,
+ external_network_id, enable_snat)
+ self.addCleanup(self._cleanup_router, router)
+ return router
+
@test.attr(type='smoke')
def test_create_show_list_update_delete_router(self):
# Create a router
@@ -102,7 +114,7 @@
def test_add_remove_router_interface_with_subnet_id(self):
network = self.create_network()
subnet = self.create_subnet(network)
- router = self.create_router(data_utils.rand_name('router-'))
+ router = self._create_router(data_utils.rand_name('router-'))
# Add router interface with subnet id
resp, interface = self.client.add_router_interface_with_subnet_id(
router['id'], subnet['id'])
@@ -121,7 +133,7 @@
def test_add_remove_router_interface_with_port_id(self):
network = self.create_network()
self.create_subnet(network)
- router = self.create_router(data_utils.rand_name('router-'))
+ router = self._create_router(data_utils.rand_name('router-'))
resp, port_body = self.client.create_port(
network_id=network['id'])
# add router interface to port created above
@@ -164,7 +176,7 @@
@test.attr(type='smoke')
def test_update_router_set_gateway(self):
- router = self.create_router(data_utils.rand_name('router-'))
+ router = self._create_router(data_utils.rand_name('router-'))
self.client.update_router(
router['id'],
external_gateway_info={
@@ -180,7 +192,7 @@
@test.requires_ext(extension='ext-gw-mode', service='network')
@test.attr(type='smoke')
def test_update_router_set_gateway_with_snat_explicit(self):
- router = self.create_router(data_utils.rand_name('router-'))
+ router = self._create_router(data_utils.rand_name('router-'))
self.admin_client.update_router_with_snat_gw_info(
router['id'],
external_gateway_info={
@@ -195,7 +207,7 @@
@test.requires_ext(extension='ext-gw-mode', service='network')
@test.attr(type='smoke')
def test_update_router_set_gateway_without_snat(self):
- router = self.create_router(data_utils.rand_name('router-'))
+ router = self._create_router(data_utils.rand_name('router-'))
self.admin_client.update_router_with_snat_gw_info(
router['id'],
external_gateway_info={
@@ -209,7 +221,7 @@
@test.attr(type='smoke')
def test_update_router_unset_gateway(self):
- router = self.create_router(
+ router = self._create_router(
data_utils.rand_name('router-'),
external_network_id=CONF.network.public_network_id)
self.client.update_router(router['id'], external_gateway_info={})
@@ -223,7 +235,7 @@
@test.requires_ext(extension='ext-gw-mode', service='network')
@test.attr(type='smoke')
def test_update_router_reset_gateway_without_snat(self):
- router = self.create_router(
+ router = self._create_router(
data_utils.rand_name('router-'),
external_network_id=CONF.network.public_network_id)
self.admin_client.update_router_with_snat_gw_info(
@@ -244,7 +256,8 @@
self.name = self.network['name']
self.subnet = self.create_subnet(self.network)
# Add router interface with subnet id
- self.router = self.create_router(data_utils.rand_name('router-'), True)
+ self.router = self._create_router(
+ data_utils.rand_name('router-'), True)
self.create_router_interface(self.router['id'], self.subnet['id'])
self.addCleanup(
self._delete_extra_routes,
@@ -259,7 +272,7 @@
@test.attr(type='smoke')
def test_update_router_admin_state(self):
- self.router = self.create_router(data_utils.rand_name('router-'))
+ self.router = self._create_router(data_utils.rand_name('router-'))
self.assertFalse(self.router['admin_state_up'])
# Update router admin state
resp, update_body = self.client.update_router(self.router['id'],
@@ -275,7 +288,7 @@
network = self.create_network()
subnet01 = self.create_subnet(network)
subnet02 = self.create_subnet(network)
- router = self.create_router(data_utils.rand_name('router-'))
+ router = self._create_router(data_utils.rand_name('router-'))
interface01 = self._add_router_interface_with_subnet_id(router['id'],
subnet01['id'])
self._verify_router_interface(router['id'], subnet01['id'],
diff --git a/tempest/api/network/test_routers_negative.py b/tempest/api/network/test_routers_negative.py
index e6ad4de..91ab9d6 100644
--- a/tempest/api/network/test_routers_negative.py
+++ b/tempest/api/network/test_routers_negative.py
@@ -23,6 +23,7 @@
_interface = 'json'
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(RoutersNegativeTest, cls).setUpClass()
if not test.is_extension_enabled('router', 'network'):
diff --git a/tempest/api/network/test_vpnaas_extensions.py b/tempest/api/network/test_vpnaas_extensions.py
index 78bc80a..7edaaf8 100644
--- a/tempest/api/network/test_vpnaas_extensions.py
+++ b/tempest/api/network/test_vpnaas_extensions.py
@@ -21,7 +21,7 @@
CONF = config.CONF
-class VPNaaSJSON(base.BaseNetworkTest):
+class VPNaaSTestJSON(base.BaseNetworkTest):
_interface = 'json'
"""
@@ -37,11 +37,12 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
if not test.is_extension_enabled('vpnaas', 'network'):
msg = "vpnaas extension not enabled."
raise cls.skipException(msg)
- super(VPNaaSJSON, cls).setUpClass()
+ super(VPNaaSTestJSON, cls).setUpClass()
cls.network = cls.create_network()
cls.subnet = cls.create_subnet(cls.network)
cls.router = cls.create_router(
@@ -175,3 +176,7 @@
ikepolicy['phase1_negotiation_mode'])
self.assertEqual(self.ikepolicy['ike_version'],
ikepolicy['ike_version'])
+
+
+class VPNaaSTestXML(VPNaaSTestJSON):
+ _interface = 'xml'
diff --git a/tempest/api/object_storage/test_account_quotas.py b/tempest/api/object_storage/test_account_quotas.py
index a3098a5..d919245 100644
--- a/tempest/api/object_storage/test_account_quotas.py
+++ b/tempest/api/object_storage/test_account_quotas.py
@@ -82,7 +82,8 @@
# Set a quota of 20 bytes on the user's account before each test
headers = {"X-Account-Meta-Quota-Bytes": "20"}
- self.os.custom_account_client.request("POST", "", headers, "")
+ self.os.custom_account_client.request("POST", url="", headers=headers,
+ body="")
def tearDown(self):
# Set the reselleradmin auth in headers for next custom_account_client
@@ -94,7 +95,8 @@
# remove the quota from the container
headers = {"X-Remove-Account-Meta-Quota-Bytes": "x"}
- self.os.custom_account_client.request("POST", "", headers, "")
+ self.os.custom_account_client.request("POST", url="", headers=headers,
+ body="")
super(AccountQuotasTest, self).tearDown()
@classmethod
@@ -135,8 +137,9 @@
)
headers = {"X-Account-Meta-Quota-Bytes": quota}
- resp, _ = self.os.custom_account_client.request("POST", "",
- headers, "")
+ resp, _ = self.os.custom_account_client.request("POST", url="",
+ headers=headers,
+ body="")
self.assertEqual(resp["status"], "204")
self.assertHeaders(resp, 'Account', 'POST')
diff --git a/tempest/api/object_storage/test_account_quotas_negative.py b/tempest/api/object_storage/test_account_quotas_negative.py
index 7648ea1..29afebc 100644
--- a/tempest/api/object_storage/test_account_quotas_negative.py
+++ b/tempest/api/object_storage/test_account_quotas_negative.py
@@ -81,7 +81,8 @@
# Set a quota of 20 bytes on the user's account before each test
headers = {"X-Account-Meta-Quota-Bytes": "20"}
- self.os.custom_account_client.request("POST", "", headers, "")
+ self.os.custom_account_client.request("POST", url="", headers=headers,
+ body="")
def tearDown(self):
# Set the reselleradmin auth in headers for next custom_account_client
@@ -93,7 +94,8 @@
# remove the quota from the container
headers = {"X-Remove-Account-Meta-Quota-Bytes": "x"}
- self.os.custom_account_client.request("POST", "", headers, "")
+ self.os.custom_account_client.request("POST", url="", headers=headers,
+ body="")
super(AccountQuotasNegativeTest, self).tearDown()
@classmethod
diff --git a/tempest/api/object_storage/test_object_formpost.py b/tempest/api/object_storage/test_object_formpost.py
index e0d15ac..81db252 100644
--- a/tempest/api/object_storage/test_object_formpost.py
+++ b/tempest/api/object_storage/test_object_formpost.py
@@ -39,6 +39,18 @@
cls.metadata = {'Temp-URL-Key': cls.key}
cls.account_client.create_account_metadata(metadata=cls.metadata)
+ def setUp(self):
+ super(ObjectFormPostTest, self).setUp()
+
+ # make sure the metadata has been set
+ account_client_metadata, _ = \
+ self.account_client.list_account_metadata()
+ self.assertIn('x-account-meta-temp-url-key',
+ account_client_metadata)
+ self.assertEqual(
+ account_client_metadata['x-account-meta-temp-url-key'],
+ self.key)
+
@classmethod
def tearDownClass(cls):
cls.account_client.delete_account_metadata(metadata=cls.metadata)
@@ -100,13 +112,9 @@
headers = {'Content-Type': content_type,
'Content-Length': str(len(body))}
- url = "%s/%s/%s" % (self.container_client.base_url,
- self.container_name,
- self.object_name)
+ url = "%s/%s" % (self.container_name, self.object_name)
- # Use a raw request, otherwise authentication headers are used
- resp, body = self.object_client.http_obj.request(url, "POST",
- body, headers=headers)
+ resp, body = self.object_client.post(url, body, headers=headers)
self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
self.assertHeaders(resp, "Object", "POST")
diff --git a/tempest/api/object_storage/test_object_formpost_negative.py b/tempest/api/object_storage/test_object_formpost_negative.py
index a52c248..fe0c994 100644
--- a/tempest/api/object_storage/test_object_formpost_negative.py
+++ b/tempest/api/object_storage/test_object_formpost_negative.py
@@ -20,6 +20,7 @@
from tempest.api.object_storage import base
from tempest.common.utils import data_utils
+from tempest import exceptions
from tempest import test
@@ -38,6 +39,18 @@
cls.metadata = {'Temp-URL-Key': cls.key}
cls.account_client.create_account_metadata(metadata=cls.metadata)
+ def setUp(self):
+ super(ObjectFormPostNegativeTest, self).setUp()
+
+ # make sure the metadata has been set
+ account_client_metadata, _ = \
+ self.account_client.list_account_metadata()
+ self.assertIn('x-account-meta-temp-url-key',
+ account_client_metadata)
+ self.assertEqual(
+ account_client_metadata['x-account-meta-temp-url-key'],
+ self.key)
+
@classmethod
def tearDownClass(cls):
cls.account_client.delete_account_metadata(metadata=cls.metadata)
@@ -100,12 +113,25 @@
headers = {'Content-Type': content_type,
'Content-Length': str(len(body))}
- url = "%s/%s/%s" % (self.container_client.base_url,
- self.container_name,
- self.object_name)
+ url = "%s/%s" % (self.container_name, self.object_name)
+ exc = self.assertRaises(
+ exceptions.Unauthorized,
+ self.object_client.post,
+ url, body, headers=headers)
+ self.assertIn('FormPost: Form Expired', str(exc))
- # Use a raw request, otherwise authentication headers are used
- resp, body = self.object_client.http_obj.request(url, "POST",
- body, headers=headers)
- self.assertEqual(int(resp['status']), 401)
- self.assertIn('FormPost: Form Expired', body)
+ @test.requires_ext(extension='formpost', service='object')
+ @test.attr(type='gate')
+ def test_post_object_using_form_invalid_signature(self):
+ self.key = "Wrong"
+ body, content_type = self.get_multipart_form()
+
+ headers = {'Content-Type': content_type,
+ 'Content-Length': str(len(body))}
+
+ url = "%s/%s" % (self.container_name, self.object_name)
+ exc = self.assertRaises(
+ exceptions.Unauthorized,
+ self.object_client.post,
+ url, body, headers=headers)
+ self.assertIn('FormPost: Invalid Signature', str(exc))
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index 91df292..06e63a4 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -14,7 +14,10 @@
# under the License.
import hashlib
+import random
+import re
from six import moves
+import time
from tempest.api.object_storage import base
from tempest.common import custom_matchers
@@ -35,6 +38,29 @@
cls.delete_containers(cls.containers)
super(ObjectTest, cls).tearDownClass()
+ def _create_object(self, metadata=None):
+ # setup object
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ self.object_client.create_object(self.container_name,
+ object_name, data, metadata=metadata)
+
+ return object_name, data
+
+ def _upload_segments(self):
+ # create object
+ object_name = data_utils.rand_name(name='LObject')
+ data = data_utils.arbitrary_string()
+ segments = 10
+ data_segments = [data + str(i) for i in moves.xrange(segments)]
+ # uploading segments
+ for i in moves.xrange(segments):
+ resp, _ = self.object_client.create_object_segments(
+ self.container_name, object_name, i, data_segments[i])
+ self.assertEqual(resp['status'], '201')
+
+ return object_name, data_segments
+
@test.attr(type='smoke')
def test_create_object(self):
# create object
@@ -64,42 +90,227 @@
self.assertHeaders(resp, 'Object', 'DELETE')
@test.attr(type='smoke')
- def test_object_metadata(self):
- # add metadata to storage object, test if metadata is retrievable
+ def test_update_object_metadata(self):
+ # update object metadata
+ object_name, data = self._create_object()
- # create Object
- object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
- resp, _ = self.object_client.create_object(self.container_name,
- object_name, data)
- # set object metadata
- meta_key = data_utils.rand_name(name='test-')
- meta_value = data_utils.rand_name(name='MetaValue-')
- orig_metadata = {meta_key: meta_value}
+ metadata = {'X-Object-Meta-test-meta': 'Meta'}
resp, _ = self.object_client.update_object_metadata(
- self.container_name, object_name, orig_metadata)
+ self.container_name,
+ object_name,
+ metadata,
+ metadata_prefix='')
self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
self.assertHeaders(resp, 'Object', 'POST')
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertIn('x-object-meta-test-meta', resp)
+ self.assertEqual(resp['x-object-meta-test-meta'], 'Meta')
+
+ def test_update_object_metadata_with_remove_metadata(self):
+ # update object metadata with remove metadata
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ create_metadata = {'X-Object-Meta-test-meta1': 'Meta1'}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=create_metadata)
+
+ update_metadata = {'X-Remove-Object-Meta-test-meta1': 'Meta1'}
+ resp, _ = self.object_client.update_object_metadata(
+ self.container_name,
+ object_name,
+ update_metadata,
+ metadata_prefix='')
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'POST')
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertNotIn('x-object-meta-test-meta1', resp)
+
+ @test.attr(type='smoke')
+ def test_update_object_metadata_with_create_and_remove_metadata(self):
+ # creation and deletion of metadata with one request
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ create_metadata = {'X-Object-Meta-test-meta1': 'Meta1'}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=create_metadata)
+
+ update_metadata = {'X-Object-Meta-test-meta2': 'Meta2',
+ 'X-Remove-Object-Meta-test-meta1': 'Meta1'}
+ resp, _ = self.object_client.update_object_metadata(
+ self.container_name,
+ object_name,
+ update_metadata,
+ metadata_prefix='')
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'POST')
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertNotIn('x-object-meta-test-meta1', resp)
+ self.assertIn('x-object-meta-test-meta2', resp)
+ self.assertEqual(resp['x-object-meta-test-meta2'], 'Meta2')
+
+ @test.attr(type='smoke')
+ def test_update_object_metadata_with_x_object_manifest(self):
+ # update object metadata with x_object_manifest
+
+ # uploading segments
+ object_name, data_segments = self._upload_segments()
+ # creating a manifest file
+ data_empty = ''
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data_empty,
+ metadata=None)
+ object_prefix = '%s/%s' % (self.container_name, object_name)
+ update_metadata = {'X-Object-Manifest': object_prefix}
+ resp, _ = self.object_client.update_object_metadata(
+ self.container_name,
+ object_name,
+ update_metadata,
+ metadata_prefix='')
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'POST')
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertIn('x-object-manifest', resp)
+ self.assertNotEqual(len(resp['x-object-manifest']), 0)
+
+ def test_update_object_metadata_with_x_object_metakey(self):
+ # update object metadata with a blenk value of metadata
+ object_name, data = self._create_object()
+
+ update_metadata = {'X-Object-Meta-test-meta': ''}
+ resp, _ = self.object_client.update_object_metadata(
+ self.container_name,
+ object_name,
+ update_metadata,
+ metadata_prefix='')
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'POST')
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertIn('x-object-meta-test-meta', resp)
+ self.assertEqual(resp['x-object-meta-test-meta'], '')
+
+ @test.attr(type='smoke')
+ def test_update_object_metadata_with_x_remove_object_metakey(self):
+ # update object metadata with a blank value of remove metadata
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ create_metadata = {'X-Object-Meta-test-meta': 'Meta'}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=create_metadata)
+
+ update_metadata = {'X-Remove-Object-Meta-test-meta': ''}
+ resp, _ = self.object_client.update_object_metadata(
+ self.container_name,
+ object_name,
+ update_metadata,
+ metadata_prefix='')
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'POST')
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertNotIn('x-object-meta-test-meta', resp)
+
+ @test.attr(type='smoke')
+ def test_list_object_metadata(self):
# get object metadata
- resp, resp_metadata = self.object_client.list_object_metadata(
- self.container_name, object_name)
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ metadata = {'X-Object-Meta-test-meta': 'Meta'}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=metadata)
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
self.assertHeaders(resp, 'Object', 'HEAD')
+ self.assertIn('x-object-meta-test-meta', resp)
+ self.assertEqual(resp['x-object-meta-test-meta'], 'Meta')
- actual_meta_key = 'x-object-meta-' + meta_key
- self.assertIn(actual_meta_key, resp)
- self.assertEqual(resp[actual_meta_key], meta_value)
+ @test.attr(type='smoke')
+ def test_list_no_object_metadata(self):
+ # get empty list of object metadata
+ object_name, data = self._create_object()
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'HEAD')
+ self.assertNotIn('x-object-meta-', str(resp))
+
+ @test.attr(type='smoke')
+ def test_list_object_metadata_with_x_object_manifest(self):
+ # get object metadata with x_object_manifest
+
+ # uploading segments
+ object_name, data_segments = self._upload_segments()
+ # creating a manifest file
+ object_prefix = '%s/%s' % (self.container_name, object_name)
+ metadata = {'X-Object-Manifest': object_prefix}
+ data_empty = ''
+ resp, _ = self.object_client.create_object(
+ self.container_name,
+ object_name,
+ data_empty,
+ metadata=metadata)
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+
+ # Check only the existence of common headers with custom matcher
+ self.assertThat(resp, custom_matchers.ExistsAllResponseHeaders(
+ 'Object', 'HEAD'))
+ self.assertIn('x-object-manifest', resp)
+
+ # Etag value of a large object is enclosed in double-quotations.
+ # This is a special case, therefore the formats of response headers
+ # are checked without a custom matcher.
+ self.assertTrue(resp['etag'].startswith('\"'))
+ self.assertTrue(resp['etag'].endswith('\"'))
+ self.assertTrue(resp['etag'].strip('\"').isalnum())
+ self.assertTrue(re.match("^\d+\.?\d*\Z", resp['x-timestamp']))
+ self.assertNotEqual(len(resp['content-type']), 0)
+ self.assertTrue(re.match("^tx[0-9a-f]*-[0-9a-f]*$",
+ resp['x-trans-id']))
+ self.assertNotEqual(len(resp['date']), 0)
+ self.assertEqual(resp['accept-ranges'], 'bytes')
+ self.assertEqual(resp['x-object-manifest'],
+ '%s/%s' % (self.container_name, object_name))
@test.attr(type='smoke')
def test_get_object(self):
# retrieve object's data (in response body)
# create object
- object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
- resp, _ = self.object_client.create_object(self.container_name,
- object_name, data)
+ object_name, data = self._create_object()
# get object
resp, body = self.object_client.get_object(self.container_name,
object_name)
@@ -109,6 +320,183 @@
self.assertEqual(body, data)
@test.attr(type='smoke')
+ def test_get_object_with_metadata(self):
+ # get object with metadata
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ metadata = {'X-Object-Meta-test-meta': 'Meta'}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=metadata)
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=None)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertIn('x-object-meta-test-meta', resp)
+ self.assertEqual(resp['x-object-meta-test-meta'], 'Meta')
+ self.assertEqual(body, data)
+
+ @test.attr(type='smoke')
+ def test_get_object_with_range(self):
+ # get object with range
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string(100)
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=None)
+ rand_num = random.randint(3, len(data) - 1)
+ metadata = {'Range': 'bytes=%s-%s' % (rand_num - 3, rand_num - 1)}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data[rand_num - 3: rand_num])
+
+ @test.attr(type='smoke')
+ def test_get_object_with_x_object_manifest(self):
+ # get object with x_object_manifest
+
+ # uploading segments
+ object_name, data_segments = self._upload_segments()
+ # creating a manifest file
+ object_prefix = '%s/%s' % (self.container_name, object_name)
+ metadata = {'X-Object-Manifest': object_prefix}
+ data_empty = ''
+ resp, body = self.object_client.create_object(
+ self.container_name,
+ object_name,
+ data_empty,
+ metadata=metadata)
+
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=None)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+
+ # Check only the existence of common headers with custom matcher
+ self.assertThat(resp, custom_matchers.ExistsAllResponseHeaders(
+ 'Object', 'GET'))
+ self.assertIn('x-object-manifest', resp)
+
+ # Etag value of a large object is enclosed in double-quotations.
+ # This is a special case, therefore the formats of response headers
+ # are checked without a custom matcher.
+ self.assertTrue(resp['etag'].startswith('\"'))
+ self.assertTrue(resp['etag'].endswith('\"'))
+ self.assertTrue(resp['etag'].strip('\"').isalnum())
+ self.assertTrue(re.match("^\d+\.?\d*\Z", resp['x-timestamp']))
+ self.assertNotEqual(len(resp['content-type']), 0)
+ self.assertTrue(re.match("^tx[0-9a-f]*-[0-9a-f]*$",
+ resp['x-trans-id']))
+ self.assertNotEqual(len(resp['date']), 0)
+ self.assertEqual(resp['accept-ranges'], 'bytes')
+ self.assertEqual(resp['x-object-manifest'],
+ '%s/%s' % (self.container_name, object_name))
+
+ self.assertEqual(''.join(data_segments), body)
+
+ @test.attr(type='smoke')
+ def test_get_object_with_if_match(self):
+ # get object with if_match
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string(10)
+ create_md5 = hashlib.md5(data).hexdigest()
+ create_metadata = {'Etag': create_md5}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=create_metadata)
+
+ list_metadata = {'If-Match': create_md5}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=list_metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data)
+
+ @test.attr(type='smoke')
+ def test_get_object_with_if_modified_since(self):
+ # get object with if_modified_since
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ time_now = time.time()
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=None)
+
+ http_date = time.ctime(time_now - 86400)
+ list_metadata = {'If-Modified-Since': http_date}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=list_metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data)
+
+ def test_get_object_with_if_none_match(self):
+ # get object with if_none_match
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string(10)
+ create_md5 = hashlib.md5(data).hexdigest()
+ create_metadata = {'Etag': create_md5}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=create_metadata)
+
+ list_data = data_utils.arbitrary_string(15)
+ list_md5 = hashlib.md5(list_data).hexdigest()
+ list_metadata = {'If-None-Match': list_md5}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=list_metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data)
+
+ @test.attr(type='smoke')
+ def test_get_object_with_if_unmodified_since(self):
+ # get object with if_unmodified_since
+ object_name, data = self._create_object()
+
+ time_now = time.time()
+ http_date = time.ctime(time_now + 86400)
+ list_metadata = {'If-Unmodified-Since': http_date}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=list_metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data)
+
+ @test.attr(type='smoke')
+ def test_get_object_with_x_newest(self):
+ # get object with x_newest
+ object_name, data = self._create_object()
+
+ list_metadata = {'X-Newest': 'true'}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=list_metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data)
+
+ @test.attr(type='smoke')
def test_copy_object_in_same_container(self):
# create source object
src_object_name = data_utils.rand_name(name='SrcObject')
@@ -286,10 +674,7 @@
# Make a conditional request for an object using the If-None-Match
# header, it should get downloaded only if the local file is different,
# otherwise the response code should be 304 Not Modified
- object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
- self.object_client.create_object(self.container_name,
- object_name, data)
+ object_name, data = self._create_object()
# local copy is identical, no download
md5 = hashlib.md5(data).hexdigest()
headers = {'If-None-Match': md5}
diff --git a/tempest/api/orchestration/base.py b/tempest/api/orchestration/base.py
index 9bf9568..c27bedf 100644
--- a/tempest/api/orchestration/base.py
+++ b/tempest/api/orchestration/base.py
@@ -10,6 +10,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import os.path
+
from tempest import clients
from tempest.common.utils import data_utils
from tempest import config
@@ -28,18 +30,17 @@
@classmethod
def setUpClass(cls):
super(BaseOrchestrationTest, cls).setUpClass()
- os = clients.OrchestrationManager()
+ cls.os = clients.OrchestrationManager()
if not CONF.service_available.heat:
raise cls.skipException("Heat support is required")
cls.build_timeout = CONF.orchestration.build_timeout
cls.build_interval = CONF.orchestration.build_interval
- cls.os = os
- cls.orchestration_client = os.orchestration_client
- cls.client = os.orchestration_client
- cls.servers_client = os.servers_client
- cls.keypairs_client = os.keypairs_client
- cls.network_client = os.network_client
+ cls.orchestration_client = cls.os.orchestration_client
+ cls.client = cls.orchestration_client
+ cls.servers_client = cls.os.servers_client
+ cls.keypairs_client = cls.os.keypairs_client
+ cls.network_client = cls.os.network_client
cls.stacks = []
cls.keypairs = []
@@ -52,11 +53,9 @@
@classmethod
def _get_identity_admin_client(cls):
- """
- Returns an instance of the Identity Admin API client
- """
- os = clients.AdminManager(interface=cls._interface)
- admin_client = os.identity_client
+ """Returns an instance of the Identity Admin API client."""
+ manager = clients.AdminManager(interface=cls._interface)
+ admin_client = manager.identity_client
return admin_client
@classmethod
@@ -71,7 +70,7 @@
return stack_identifier
@classmethod
- def clear_stacks(cls):
+ def _clear_stacks(cls):
for stack_identifier in cls.stacks:
try:
cls.client.delete_stack(stack_identifier)
@@ -93,7 +92,7 @@
return body
@classmethod
- def clear_keypairs(cls):
+ def _clear_keypairs(cls):
for kp_name in cls.keypairs:
try:
cls.keypairs_client.delete_keypair(kp_name)
@@ -101,13 +100,39 @@
pass
@classmethod
+ def load_template(cls, name, ext='yaml'):
+ loc = ["stacks", "templates", "%s.%s" % (name, ext)]
+ fullpath = os.path.join(os.path.dirname(__file__), *loc)
+
+ with open(fullpath, "r") as f:
+ content = f.read()
+ return content
+
+ @classmethod
def tearDownClass(cls):
- cls.clear_stacks()
- cls.clear_keypairs()
+ cls._clear_stacks()
+ cls._clear_keypairs()
super(BaseOrchestrationTest, cls).tearDownClass()
@staticmethod
def stack_output(stack, output_key):
- """Return a stack output value for a give key."""
+ """Return a stack output value for a given key."""
return next((o['output_value'] for o in stack['outputs']
if o['output_key'] == output_key), None)
+
+ def assert_fields_in_dict(self, obj, *fields):
+ for field in fields:
+ self.assertIn(field, obj)
+
+ def list_resources(self, stack_identifier):
+ """Get a dict mapping of resource names to types."""
+ resp, resources = self.client.list_resources(stack_identifier)
+ self.assertEqual('200', resp['status'])
+ self.assertIsInstance(resources, list)
+ for res in resources:
+ self.assert_fields_in_dict(res, 'logical_resource_id',
+ 'resource_type', 'resource_status',
+ 'updated_time')
+
+ return dict((r['resource_name'], r['resource_type'])
+ for r in resources)
diff --git a/tempest/api/orchestration/stacks/templates/cfn_init_signal.yaml b/tempest/api/orchestration/stacks/templates/cfn_init_signal.yaml
new file mode 100644
index 0000000..fa5345e
--- /dev/null
+++ b/tempest/api/orchestration/stacks/templates/cfn_init_signal.yaml
@@ -0,0 +1,82 @@
+HeatTemplateFormatVersion: '2012-12-12'
+Description: |
+ Template which uses a wait condition to confirm that a minimal
+ cfn-init and cfn-signal has worked
+Parameters:
+ key_name:
+ Type: String
+ flavor:
+ Type: String
+ image:
+ Type: String
+ network:
+ Type: String
+ timeout:
+ Type: Number
+Resources:
+ CfnUser:
+ Type: AWS::IAM::User
+ SmokeSecurityGroup:
+ Type: AWS::EC2::SecurityGroup
+ Properties:
+ GroupDescription: Enable only ping and SSH access
+ SecurityGroupIngress:
+ - {CidrIp: 0.0.0.0/0, FromPort: '-1', IpProtocol: icmp, ToPort: '-1'}
+ - {CidrIp: 0.0.0.0/0, FromPort: '22', IpProtocol: tcp, ToPort: '22'}
+ SmokeKeys:
+ Type: AWS::IAM::AccessKey
+ Properties:
+ UserName: {Ref: CfnUser}
+ SmokeServer:
+ Type: OS::Nova::Server
+ Metadata:
+ AWS::CloudFormation::Init:
+ config:
+ files:
+ /tmp/smoke-status:
+ content: smoke test complete
+ /etc/cfn/cfn-credentials:
+ content:
+ Fn::Replace:
+ - SmokeKeys: {Ref: SmokeKeys}
+ SecretAccessKey:
+ 'Fn::GetAtt': [SmokeKeys, SecretAccessKey]
+ - |
+ AWSAccessKeyId=SmokeKeys
+ AWSSecretKey=SecretAccessKey
+ mode: '000400'
+ owner: root
+ group: root
+ Properties:
+ image: {Ref: image}
+ flavor: {Ref: flavor}
+ key_name: {Ref: key_name}
+ security_groups:
+ - {Ref: SmokeSecurityGroup}
+ networks:
+ - uuid: {Ref: network}
+ user_data:
+ Fn::Replace:
+ - WaitHandle: {Ref: WaitHandle}
+ - |
+ #!/bin/bash -v
+ /opt/aws/bin/cfn-init
+ /opt/aws/bin/cfn-signal -e 0 --data "`cat /tmp/smoke-status`" \
+ "WaitHandle"
+ WaitHandle:
+ Type: AWS::CloudFormation::WaitConditionHandle
+ WaitCondition:
+ Type: AWS::CloudFormation::WaitCondition
+ DependsOn: SmokeServer
+ Properties:
+ Handle: {Ref: WaitHandle}
+ Timeout: {Ref: timeout}
+Outputs:
+ WaitConditionStatus:
+ Description: Contents of /tmp/smoke-status on SmokeServer
+ Value:
+ Fn::GetAtt: [WaitCondition, Data]
+ SmokeServerIp:
+ Description: IP address of server
+ Value:
+ Fn::GetAtt: [SmokeServer, first_address]
diff --git a/tempest/api/orchestration/stacks/templates/neutron_basic.yaml b/tempest/api/orchestration/stacks/templates/neutron_basic.yaml
new file mode 100644
index 0000000..275d040
--- /dev/null
+++ b/tempest/api/orchestration/stacks/templates/neutron_basic.yaml
@@ -0,0 +1,71 @@
+heat_template_version: '2013-05-23'
+description: |
+ Template which creates single EC2 instance
+parameters:
+ KeyName:
+ type: string
+ InstanceType:
+ type: string
+ ImageId:
+ type: string
+ ExternalRouterId:
+ type: string
+ ExternalNetworkId:
+ type: string
+ timeout:
+ type: number
+resources:
+ Network:
+ type: OS::Neutron::Net
+ properties:
+ name: NewNetwork
+ Subnet:
+ type: OS::Neutron::Subnet
+ properties:
+ network_id: {Ref: Network}
+ name: NewSubnet
+ ip_version: 4
+ cidr: 10.0.3.0/24
+ dns_nameservers: ["8.8.8.8"]
+ allocation_pools:
+ - {end: 10.0.3.150, start: 10.0.3.20}
+ Router:
+ type: OS::Neutron::Router
+ properties:
+ name: NewRouter
+ admin_state_up: false
+ external_gateway_info:
+ network: {get_param: ExternalNetworkId}
+ enable_snat: false
+ RouterInterface:
+ type: OS::Neutron::RouterInterface
+ properties:
+ router_id: {get_param: ExternalRouterId}
+ subnet_id: {get_resource: Subnet}
+ Server:
+ type: OS::Nova::Server
+ metadata:
+ Name: SmokeServerNeutron
+ properties:
+ image: {get_param: ImageId}
+ flavor: {get_param: InstanceType}
+ key_name: {get_param: KeyName}
+ networks:
+ - network: {get_resource: Network}
+ user_data:
+ str_replace:
+ template: |
+ #!/bin/bash -v
+
+ /opt/aws/bin/cfn-signal -e 0 -r "SmokeServerNeutron created" \
+ 'wait_handle'
+ params:
+ wait_handle: {get_resource: WaitHandleNeutron}
+ WaitHandleNeutron:
+ type: AWS::CloudFormation::WaitConditionHandle
+ WaitCondition:
+ type: AWS::CloudFormation::WaitCondition
+ depends_on: Server
+ properties:
+ Handle: {get_resource: WaitHandleNeutron}
+ Timeout: {get_param: timeout}
diff --git a/tempest/api/orchestration/stacks/templates/non_empty_stack.yaml b/tempest/api/orchestration/stacks/templates/non_empty_stack.yaml
new file mode 100644
index 0000000..58a934e
--- /dev/null
+++ b/tempest/api/orchestration/stacks/templates/non_empty_stack.yaml
@@ -0,0 +1,32 @@
+HeatTemplateFormatVersion: '2012-12-12'
+Description: |
+ Template which creates some simple resources
+Parameters:
+ trigger:
+ Type: String
+ Default: not_yet
+Resources:
+ fluffy:
+ Type: AWS::AutoScaling::LaunchConfiguration
+ Metadata:
+ kittens:
+ - Tom
+ - Stinky
+ Properties:
+ ImageId: not_used
+ InstanceType: not_used
+ UserData:
+ Fn::Replace:
+ - variable_a: {Ref: trigger}
+ variable_b: bee
+ - |
+ A == variable_a
+ B == variable_b
+Outputs:
+ fluffy:
+ Description: "fluffies irc nick"
+ Value:
+ Fn::Replace:
+ - nick: {Ref: fluffy}
+ - |
+ #nick
diff --git a/tempest/api/orchestration/stacks/templates/nova_keypair.json b/tempest/api/orchestration/stacks/templates/nova_keypair.json
new file mode 100644
index 0000000..63d3817
--- /dev/null
+++ b/tempest/api/orchestration/stacks/templates/nova_keypair.json
@@ -0,0 +1,48 @@
+{
+ "AWSTemplateFormatVersion" : "2010-09-09",
+ "Description" : "Template which create two key pairs.",
+ "Parameters" : {
+ "KeyPairName1": {
+ "Type": "String",
+ "Default": "testkey1"
+ },
+ "KeyPairName2": {
+ "Type": "String",
+ "Default": "testkey2"
+ }
+ },
+ "Resources" : {
+ "KeyPairSavePrivate": {
+ "Type": "OS::Nova::KeyPair",
+ "Properties": {
+ "name" : { "Ref" : "KeyPairName1" },
+ "save_private_key": true
+ }
+ },
+ "KeyPairDontSavePrivate": {
+ "Type": "OS::Nova::KeyPair",
+ "Properties": {
+ "name" : { "Ref" : "KeyPairName2" },
+ "save_private_key": false
+ }
+ }
+ },
+ "Outputs": {
+ "KeyPair_PublicKey": {
+ "Description": "Public Key of generated keypair.",
+ "Value": { "Fn::GetAtt" : ["KeyPairSavePrivate", "public_key"] }
+ },
+ "KeyPair_PrivateKey": {
+ "Description": "Private Key of generated keypair.",
+ "Value": { "Fn::GetAtt" : ["KeyPairSavePrivate", "private_key"] }
+ },
+ "KeyPairDontSavePrivate_PublicKey": {
+ "Description": "Public Key of generated keypair.",
+ "Value": { "Fn::GetAtt" : ["KeyPairDontSavePrivate", "public_key"] }
+ },
+ "KeyPairDontSavePrivate_PrivateKey": {
+ "Description": "Private Key of generated keypair.",
+ "Value": { "Fn::GetAtt" : ["KeyPairDontSavePrivate", "private_key"] }
+ }
+ }
+}
diff --git a/tempest/api/orchestration/stacks/templates/nova_keypair.yaml b/tempest/api/orchestration/stacks/templates/nova_keypair.yaml
new file mode 100644
index 0000000..81ad99c
--- /dev/null
+++ b/tempest/api/orchestration/stacks/templates/nova_keypair.yaml
@@ -0,0 +1,43 @@
+heat_template_version: 2013-05-23
+
+description: >
+ Template which creates two key pairs.
+
+parameters:
+ KeyPairName1:
+ type: string
+ default: testkey
+
+ KeyPairName2:
+ type: string
+ default: testkey2
+
+resources:
+ KeyPairSavePrivate:
+ type: OS::Nova::KeyPair
+ properties:
+ name: { get_param: KeyPairName1 }
+ save_private_key: true
+
+ KeyPairDontSavePrivate:
+ type: OS::Nova::KeyPair
+ properties:
+ name: { get_param: KeyPairName2 }
+ save_private_key: false
+
+outputs:
+ KeyPair_PublicKey:
+ description: Public Key of generated keypair
+ value: { get_attr: [KeyPairSavePrivate, public_key] }
+
+ KeyPair_PrivateKey:
+ description: Private Key of generated keypair
+ value: { get_attr: [KeyPairSavePrivate, private_key] }
+
+ KeyPairDontSavePrivate_PublicKey:
+ description: Public Key of generated keypair
+ value: { get_attr: [KeyPairDontSavePrivate, public_key] }
+
+ KeyPairDontSavePrivate_PrivateKey:
+ description: Private Key of generated keypair
+ value: { get_attr: [KeyPairDontSavePrivate, private_key] }
diff --git a/tempest/api/orchestration/stacks/templates/swift_basic.yaml b/tempest/api/orchestration/stacks/templates/swift_basic.yaml
new file mode 100644
index 0000000..713f8bc
--- /dev/null
+++ b/tempest/api/orchestration/stacks/templates/swift_basic.yaml
@@ -0,0 +1,23 @@
+heat_template_version: 2013-05-23
+description: Template which creates a Swift container resource
+
+resources:
+ SwiftContainerWebsite:
+ deletion_policy: "Delete"
+ type: OS::Swift::Container
+ properties:
+ X-Container-Read: ".r:*"
+ X-Container-Meta:
+ web-index: "index.html"
+ web-error: "error.html"
+
+ SwiftContainer:
+ type: OS::Swift::Container
+
+outputs:
+ WebsiteURL:
+ description: "URL for website hosted on S3"
+ value: { get_attr: [SwiftContainer, WebsiteURL] }
+ DomainName:
+ description: "Domain of Swift host"
+ value: { get_attr: [SwiftContainer, DomainName] }
diff --git a/tempest/api/orchestration/stacks/test_limits.py b/tempest/api/orchestration/stacks/test_limits.py
index 893dcc4..283ab2b 100644
--- a/tempest/api/orchestration/stacks/test_limits.py
+++ b/tempest/api/orchestration/stacks/test_limits.py
@@ -37,3 +37,17 @@
ex = self.assertRaises(exceptions.BadRequest, self.create_stack,
stack_name, template)
self.assertIn('Template exceeds maximum allowed size', str(ex))
+
+ @attr(type='gate')
+ def test_exceed_max_resources_per_stack(self):
+ stack_name = data_utils.rand_name('heat')
+ # Create a big template, one resource more than the limit
+ template = 'heat_template_version: \'2013-05-23\'\nresources:\n'
+ rsrc_snippet = ' random%s:\n type: \'OS::Heat::RandomString\'\n'
+ num_resources = CONF.orchestration.max_resources_per_stack + 1
+ for i in range(num_resources):
+ template += rsrc_snippet % i
+
+ ex = self.assertRaises(exceptions.BadRequest, self.create_stack,
+ stack_name, template)
+ self.assertIn('Maximum resources per stack exceeded', str(ex))
diff --git a/tempest/api/orchestration/stacks/test_neutron_resources.py b/tempest/api/orchestration/stacks/test_neutron_resources.py
index dee26b1..b96f6ce 100644
--- a/tempest/api/orchestration/stacks/test_neutron_resources.py
+++ b/tempest/api/orchestration/stacks/test_neutron_resources.py
@@ -26,78 +26,6 @@
class NeutronResourcesTestJSON(base.BaseOrchestrationTest):
- _interface = 'json'
-
- template = """
-heat_template_version: '2013-05-23'
-description: |
- Template which creates single EC2 instance
-parameters:
- KeyName:
- type: string
- InstanceType:
- type: string
- ImageId:
- type: string
- ExternalRouterId:
- type: string
- ExternalNetworkId:
- type: string
-resources:
- Network:
- type: OS::Neutron::Net
- properties:
- name: NewNetwork
- Subnet:
- type: OS::Neutron::Subnet
- properties:
- network_id: {Ref: Network}
- name: NewSubnet
- ip_version: 4
- cidr: 10.0.3.0/24
- dns_nameservers: ["8.8.8.8"]
- allocation_pools:
- - {end: 10.0.3.150, start: 10.0.3.20}
- Router:
- type: OS::Neutron::Router
- properties:
- name: NewRouter
- admin_state_up: false
- external_gateway_info:
- network: {get_param: ExternalNetworkId}
- enable_snat: false
- RouterInterface:
- type: OS::Neutron::RouterInterface
- properties:
- router_id: {get_param: ExternalRouterId}
- subnet_id: {get_resource: Subnet}
- Server:
- type: AWS::EC2::Instance
- metadata:
- Name: SmokeServerNeutron
- properties:
- ImageId: {get_param: ImageId}
- InstanceType: {get_param: InstanceType}
- KeyName: {get_param: KeyName}
- SubnetId: {get_resource: Subnet}
- UserData:
- str_replace:
- template: |
- #!/bin/bash -v
-
- /opt/aws/bin/cfn-signal -e 0 -r "SmokeServerNeutron created" \
- 'wait_handle'
- params:
- wait_handle: {get_resource: WaitHandleNeutron}
- WaitHandleNeutron:
- type: AWS::CloudFormation::WaitConditionHandle
- WaitCondition:
- type: AWS::CloudFormation::WaitCondition
- depends_on: Server
- properties:
- Handle: {get_resource: WaitHandleNeutron}
- Timeout: '600'
-"""
@classmethod
@test.safe_setup
@@ -105,12 +33,12 @@
super(NeutronResourcesTestJSON, cls).setUpClass()
if not CONF.orchestration.image_ref:
raise cls.skipException("No image available to test")
- cls.client = cls.orchestration_client
os = clients.Manager()
if not CONF.service_available.neutron:
raise cls.skipException("Neutron support is required")
cls.network_client = os.network_client
cls.stack_name = data_utils.rand_name('heat')
+ template = cls.load_template('neutron_basic')
cls.keypair_name = (CONF.orchestration.keypair_name or
cls._create_keypair()['name'])
cls.external_router_id = cls._get_external_router_id()
@@ -119,13 +47,14 @@
# create the stack
cls.stack_identifier = cls.create_stack(
cls.stack_name,
- cls.template,
+ template,
parameters={
'KeyName': cls.keypair_name,
'InstanceType': CONF.orchestration.instance_type,
'ImageId': CONF.orchestration.image_ref,
'ExternalRouterId': cls.external_router_id,
- 'ExternalNetworkId': cls.external_network_id
+ 'ExternalNetworkId': cls.external_network_id,
+ 'timeout': CONF.orchestration.build_timeout
})
cls.stack_id = cls.stack_identifier.split('/')[1]
try:
@@ -162,7 +91,7 @@
resources = [('Network', 'OS::Neutron::Net'),
('Subnet', 'OS::Neutron::Subnet'),
('RouterInterface', 'OS::Neutron::RouterInterface'),
- ('Server', 'AWS::EC2::Instance')]
+ ('Server', 'OS::Nova::Server')]
for resource_name, resource_type in resources:
resource = self.test_resources.get(resource_name, None)
self.assertIsInstance(resource, dict)
diff --git a/tempest/api/orchestration/stacks/test_non_empty_stack.py b/tempest/api/orchestration/stacks/test_non_empty_stack.py
index 11d01f7..9ef95a1 100644
--- a/tempest/api/orchestration/stacks/test_non_empty_stack.py
+++ b/tempest/api/orchestration/stacks/test_non_empty_stack.py
@@ -21,53 +21,17 @@
class StacksTestJSON(base.BaseOrchestrationTest):
- _interface = 'json'
-
- template = """
-HeatTemplateFormatVersion: '2012-12-12'
-Description: |
- Template which creates some simple resources
-Parameters:
- trigger:
- Type: String
- Default: not_yet
-Resources:
- fluffy:
- Type: AWS::AutoScaling::LaunchConfiguration
- Metadata:
- kittens:
- - Tom
- - Stinky
- Properties:
- ImageId: not_used
- InstanceType: not_used
- UserData:
- Fn::Replace:
- - variable_a: {Ref: trigger}
- variable_b: bee
- - |
- A == variable_a
- B == variable_b
-Outputs:
- fluffy:
- Description: "fluffies irc nick"
- Value:
- Fn::Replace:
- - nick: {Ref: fluffy}
- - |
- #nick
-"""
@classmethod
def setUpClass(cls):
super(StacksTestJSON, cls).setUpClass()
- cls.client = cls.orchestration_client
cls.stack_name = data_utils.rand_name('heat')
+ template = cls.load_template('non_empty_stack')
# create the stack
cls.stack_identifier = cls.create_stack(
cls.stack_name,
- cls.template,
+ template,
parameters={
'trigger': 'start'
})
@@ -76,16 +40,18 @@
cls.resource_type = 'AWS::AutoScaling::LaunchConfiguration'
cls.client.wait_for_stack_status(cls.stack_id, 'CREATE_COMPLETE')
- def assert_fields_in_dict(self, obj, *fields):
- for field in fields:
- self.assertIn(field, obj)
+ def _list_stacks(self, expected_num=None, **filter_kwargs):
+ resp, stacks = self.client.list_stacks(params=filter_kwargs)
+ self.assertEqual('200', resp['status'])
+ self.assertIsInstance(stacks, list)
+ if expected_num is not None:
+ self.assertEqual(expected_num, len(stacks))
+ return stacks
@attr(type='gate')
def test_stack_list(self):
- """Created stack should be on the list of existing stacks."""
- resp, stacks = self.client.list_stacks()
- self.assertEqual('200', resp['status'])
- self.assertIsInstance(stacks, list)
+ """Created stack should be in the list of existing stacks."""
+ stacks = self._list_stacks()
stacks_names = map(lambda stack: stack['stack_name'], stacks)
self.assertIn(self.stack_name, stacks_names)
@@ -111,7 +77,7 @@
@attr(type='gate')
def test_suspend_resume_stack(self):
- """suspend and resume a stack."""
+ """Suspend and resume a stack."""
resp, suspend_stack = self.client.suspend_stack(self.stack_identifier)
self.assertEqual('200', resp['status'])
self.client.wait_for_stack_status(self.stack_identifier,
@@ -125,20 +91,8 @@
def test_list_resources(self):
"""Getting list of created resources for the stack should be possible.
"""
- resp, resources = self.client.list_resources(self.stack_identifier)
- self.assertEqual('200', resp['status'])
- self.assertIsInstance(resources, list)
- for res in resources:
- self.assert_fields_in_dict(res, 'logical_resource_id',
- 'resource_type', 'resource_status',
- 'updated_time')
-
- resources_names = map(lambda resource: resource['logical_resource_id'],
- resources)
- self.assertIn(self.resource_name, resources_names)
- resources_types = map(lambda resource: resource['resource_type'],
- resources)
- self.assertIn(self.resource_type, resources_types)
+ resources = self.list_resources(self.stack_identifier)
+ self.assertEqual({self.resource_name: self.resource_type}, resources)
@attr(type='gate')
def test_show_resource(self):
@@ -156,7 +110,7 @@
@attr(type='gate')
def test_resource_metadata(self):
- """Getting metadata for created resource should be possible."""
+ """Getting metadata for created resources should be possible."""
resp, metadata = self.client.show_resource_metadata(
self.stack_identifier,
self.resource_name)
@@ -182,7 +136,7 @@
@attr(type='gate')
def test_show_event(self):
- """Getting details about existing event should be possible."""
+ """Getting details about an event should be possible."""
resp, events = self.client.list_resource_events(self.stack_identifier,
self.resource_name)
self.assertNotEqual([], events)
diff --git a/tempest/api/orchestration/stacks/test_nova_keypair_resources.py b/tempest/api/orchestration/stacks/test_nova_keypair_resources.py
index 9d3bf13..60b8dc1 100644
--- a/tempest/api/orchestration/stacks/test_nova_keypair_resources.py
+++ b/tempest/api/orchestration/stacks/test_nova_keypair_resources.py
@@ -22,63 +22,18 @@
class NovaKeyPairResourcesYAMLTest(base.BaseOrchestrationTest):
- _interface = 'json'
- template = """
-heat_template_version: 2013-05-23
-
-description: >
- Template which creates two key pairs.
-
-parameters:
- KeyPairName1:
- type: string
- default: testkey
-
- KeyPairName2:
- type: string
- default: testkey2
-
-resources:
- KeyPairSavePrivate:
- type: OS::Nova::KeyPair
- properties:
- name: { get_param: KeyPairName1 }
- save_private_key: true
-
- KeyPairDontSavePrivate:
- type: OS::Nova::KeyPair
- properties:
- name: { get_param: KeyPairName2 }
- save_private_key: false
-
-outputs:
- KeyPair_PublicKey:
- description: Public Key of generated keypair
- value: { get_attr: [KeyPairSavePrivate, public_key] }
-
- KeyPair_PrivateKey:
- description: Private Key of generated keypair
- value: { get_attr: [KeyPairSavePrivate, private_key] }
-
- KeyPairDontSavePrivate_PublicKey:
- description: Public Key of generated keypair
- value: { get_attr: [KeyPairDontSavePrivate, public_key] }
-
- KeyPairDontSavePrivate_PrivateKey:
- description: Private Key of generated keypair
- value: { get_attr: [KeyPairDontSavePrivate, private_key] }
-"""
+ _tpl_type = 'yaml'
@classmethod
def setUpClass(cls):
super(NovaKeyPairResourcesYAMLTest, cls).setUpClass()
- cls.client = cls.orchestration_client
cls.stack_name = data_utils.rand_name('heat')
+ template = cls.load_template('nova_keypair', ext=cls._tpl_type)
# create the stack, avoid any duplicated key.
cls.stack_identifier = cls.create_stack(
cls.stack_name,
- cls.template,
+ template,
parameters={
'KeyPairName1': cls.stack_name + '_1',
'KeyPairName2': cls.stack_name + '_2'
@@ -129,53 +84,4 @@
class NovaKeyPairResourcesAWSTest(NovaKeyPairResourcesYAMLTest):
- template = """
-{
- "AWSTemplateFormatVersion" : "2010-09-09",
- "Description" : "Template which create two key pairs.",
- "Parameters" : {
- "KeyPairName1": {
- "Type": "String",
- "Default": "testkey1"
- },
- "KeyPairName2": {
- "Type": "String",
- "Default": "testkey2"
- }
- },
- "Resources" : {
- "KeyPairSavePrivate": {
- "Type": "OS::Nova::KeyPair",
- "Properties": {
- "name" : { "Ref" : "KeyPairName1" },
- "save_private_key": true
- }
- },
- "KeyPairDontSavePrivate": {
- "Type": "OS::Nova::KeyPair",
- "Properties": {
- "name" : { "Ref" : "KeyPairName2" },
- "save_private_key": false
- }
- }
- },
- "Outputs": {
- "KeyPair_PublicKey": {
- "Description": "Public Key of generated keypair.",
- "Value": { "Fn::GetAtt" : ["KeyPairSavePrivate", "public_key"] }
- },
- "KeyPair_PrivateKey": {
- "Description": "Private Key of generated keypair.",
- "Value": { "Fn::GetAtt" : ["KeyPairSavePrivate", "private_key"] }
- },
- "KeyPairDontSavePrivate_PublicKey": {
- "Description": "Public Key of generated keypair.",
- "Value": { "Fn::GetAtt" : ["KeyPairDontSavePrivate", "public_key"] }
- },
- "KeyPairDontSavePrivate_PrivateKey": {
- "Description": "Private Key of generated keypair.",
- "Value": { "Fn::GetAtt" : ["KeyPairDontSavePrivate", "private_key"] }
- }
- }
-}
-"""
+ _tpl_type = 'json'
diff --git a/tempest/api/orchestration/stacks/test_server_cfn_init.py b/tempest/api/orchestration/stacks/test_server_cfn_init.py
index a6f74b6..cb5d941 100644
--- a/tempest/api/orchestration/stacks/test_server_cfn_init.py
+++ b/tempest/api/orchestration/stacks/test_server_cfn_init.py
@@ -26,100 +26,15 @@
class ServerCfnInitTestJSON(base.BaseOrchestrationTest):
- _interface = 'json'
existing_keypair = CONF.orchestration.keypair_name is not None
- template = """
-HeatTemplateFormatVersion: '2012-12-12'
-Description: |
- Template which uses a wait condition to confirm that a minimal
- cfn-init and cfn-signal has worked
-Parameters:
- key_name:
- Type: String
- flavor:
- Type: String
- image:
- Type: String
- network:
- Type: String
-Resources:
- CfnUser:
- Type: AWS::IAM::User
- SmokeSecurityGroup:
- Type: AWS::EC2::SecurityGroup
- Properties:
- GroupDescription: Enable only ping and SSH access
- SecurityGroupIngress:
- - {CidrIp: 0.0.0.0/0, FromPort: '-1', IpProtocol: icmp, ToPort: '-1'}
- - {CidrIp: 0.0.0.0/0, FromPort: '22', IpProtocol: tcp, ToPort: '22'}
- SmokeKeys:
- Type: AWS::IAM::AccessKey
- Properties:
- UserName: {Ref: CfnUser}
- SmokeServer:
- Type: OS::Nova::Server
- Metadata:
- AWS::CloudFormation::Init:
- config:
- files:
- /tmp/smoke-status:
- content: smoke test complete
- /etc/cfn/cfn-credentials:
- content:
- Fn::Replace:
- - SmokeKeys: {Ref: SmokeKeys}
- SecretAccessKey:
- 'Fn::GetAtt': [SmokeKeys, SecretAccessKey]
- - |
- AWSAccessKeyId=SmokeKeys
- AWSSecretKey=SecretAccessKey
- mode: '000400'
- owner: root
- group: root
- Properties:
- image: {Ref: image}
- flavor: {Ref: flavor}
- key_name: {Ref: key_name}
- security_groups:
- - {Ref: SmokeSecurityGroup}
- networks:
- - uuid: {Ref: network}
- user_data:
- Fn::Replace:
- - WaitHandle: {Ref: WaitHandle}
- - |
- #!/bin/bash -v
- /opt/aws/bin/cfn-init
- /opt/aws/bin/cfn-signal -e 0 --data "`cat /tmp/smoke-status`" \
- "WaitHandle"
- WaitHandle:
- Type: AWS::CloudFormation::WaitConditionHandle
- WaitCondition:
- Type: AWS::CloudFormation::WaitCondition
- DependsOn: SmokeServer
- Properties:
- Handle: {Ref: WaitHandle}
- Timeout: '600'
-Outputs:
- WaitConditionStatus:
- Description: Contents of /tmp/smoke-status on SmokeServer
- Value:
- Fn::GetAtt: [WaitCondition, Data]
- SmokeServerIp:
- Description: IP address of server
- Value:
- Fn::GetAtt: [SmokeServer, first_address]
-"""
-
@classmethod
@test.safe_setup
def setUpClass(cls):
super(ServerCfnInitTestJSON, cls).setUpClass()
if not CONF.orchestration.image_ref:
raise cls.skipException("No image available to test")
- cls.client = cls.orchestration_client
-
+ template = cls.load_template('cfn_init_signal')
stack_name = data_utils.rand_name('heat')
if CONF.orchestration.keypair_name:
keypair_name = CONF.orchestration.keypair_name
@@ -130,12 +45,13 @@
# create the stack
cls.stack_identifier = cls.create_stack(
stack_name,
- cls.template,
+ template,
parameters={
'key_name': keypair_name,
'flavor': CONF.orchestration.instance_type,
'image': CONF.orchestration.image_ref,
- 'network': cls._get_default_network()['id']
+ 'network': cls._get_default_network()['id'],
+ 'timeout': CONF.orchestration.build_timeout
})
@test.attr(type='slow')
@@ -178,7 +94,8 @@
try:
self.client.wait_for_resource_status(
sid, 'WaitCondition', 'CREATE_COMPLETE')
- except exceptions.TimeoutException as e:
+ except (exceptions.StackResourceBuildErrorException,
+ exceptions.TimeoutException) as e:
# attempt to log the server console to help with debugging
# the cause of the server not signalling the waitcondition
# to heat.
diff --git a/tempest/api/orchestration/stacks/test_stacks.py b/tempest/api/orchestration/stacks/test_stacks.py
index fc2dda8..867995c 100644
--- a/tempest/api/orchestration/stacks/test_stacks.py
+++ b/tempest/api/orchestration/stacks/test_stacks.py
@@ -20,14 +20,11 @@
class StacksTestJSON(base.BaseOrchestrationTest):
- _interface = 'json'
-
empty_template = "HeatTemplateFormatVersion: '2012-12-12'\n"
@classmethod
def setUpClass(cls):
super(StacksTestJSON, cls).setUpClass()
- cls.client = cls.orchestration_client
@attr(type='smoke')
def test_stack_list_responds(self):
diff --git a/tempest/api/orchestration/stacks/test_swift_resources.py b/tempest/api/orchestration/stacks/test_swift_resources.py
index fcf357a..6d53fb2 100644
--- a/tempest/api/orchestration/stacks/test_swift_resources.py
+++ b/tempest/api/orchestration/stacks/test_swift_resources.py
@@ -25,40 +25,12 @@
class SwiftResourcesTestJSON(base.BaseOrchestrationTest):
- _interface = 'json'
- template = """
-heat_template_version: 2013-05-23
-description: Template which creates a Swift container resource
-
-resources:
- SwiftContainerWebsite:
- deletion_policy: "Delete"
- type: OS::Swift::Container
- properties:
- X-Container-Read: ".r:*"
- X-Container-Meta:
- web-index: "index.html"
- web-error: "error.html"
-
- SwiftContainer:
- type: OS::Swift::Container
-
-outputs:
- WebsiteURL:
- description: "URL for website hosted on S3"
- value: { get_attr: [SwiftContainer, WebsiteURL] }
- DomainName:
- description: "Domain of Swift host"
- value: { get_attr: [SwiftContainer, DomainName] }
-
-"""
-
@classmethod
@test.safe_setup
def setUpClass(cls):
super(SwiftResourcesTestJSON, cls).setUpClass()
- cls.client = cls.orchestration_client
cls.stack_name = data_utils.rand_name('heat')
+ template = cls.load_template('swift_basic')
os = clients.Manager()
if not CONF.service_available.swift:
raise cls.skipException("Swift support is required")
@@ -67,7 +39,7 @@
# create the stack
cls.stack_identifier = cls.create_stack(
cls.stack_name,
- cls.template)
+ template)
cls.stack_id = cls.stack_identifier.split('/')[1]
cls.client.wait_for_stack_status(cls.stack_id, 'CREATE_COMPLETE')
cls.test_resources = {}
@@ -76,7 +48,7 @@
cls.test_resources[resource['logical_resource_id']] = resource
def test_created_resources(self):
- """Created stack should be on the list of existing stacks."""
+ """Created stack should be in the list of existing stacks."""
resources = [('SwiftContainer', 'OS::Swift::Container'),
('SwiftContainerWebsite', 'OS::Swift::Container')]
for resource_name, resource_type in resources:
diff --git a/tempest/api/orchestration/stacks/test_templates.py b/tempest/api/orchestration/stacks/test_templates.py
index 2dc29fc..74950a9 100644
--- a/tempest/api/orchestration/stacks/test_templates.py
+++ b/tempest/api/orchestration/stacks/test_templates.py
@@ -16,8 +16,6 @@
class TemplateYAMLTestJSON(base.BaseOrchestrationTest):
- _interface = 'json'
-
template = """
HeatTemplateFormatVersion: '2012-12-12'
Description: |
@@ -27,13 +25,10 @@
Type: AWS::IAM::User
"""
- invalid_template_url = 'http://www.example.com/template.yaml'
-
@classmethod
@test.safe_setup
def setUpClass(cls):
super(TemplateYAMLTestJSON, cls).setUpClass()
- cls.client = cls.orchestration_client
cls.stack_name = data_utils.rand_name('heat')
cls.stack_identifier = cls.create_stack(cls.stack_name, cls.template)
cls.client.wait_for_stack_status(cls.stack_identifier,
@@ -67,5 +62,3 @@
}
}
"""
-
- invalid_template_url = 'http://www.example.com/template.template'
diff --git a/tempest/api/orchestration/stacks/test_templates_negative.py b/tempest/api/orchestration/stacks/test_templates_negative.py
index c55f6ee..b325104 100644
--- a/tempest/api/orchestration/stacks/test_templates_negative.py
+++ b/tempest/api/orchestration/stacks/test_templates_negative.py
@@ -18,8 +18,6 @@
class TemplateYAMLNegativeTestJSON(base.BaseOrchestrationTest):
- _interface = 'json'
-
template = """
HeatTemplateFormatVersion: '2012-12-12'
Description: |
@@ -34,7 +32,6 @@
@classmethod
def setUpClass(cls):
super(TemplateYAMLNegativeTestJSON, cls).setUpClass()
- cls.client = cls.orchestration_client
cls.parameters = {}
@test.attr(type=['gate', 'negative'])
diff --git a/tempest/api/orchestration/stacks/test_update.py b/tempest/api/orchestration/stacks/test_update.py
new file mode 100644
index 0000000..a9a43b6
--- /dev/null
+++ b/tempest/api/orchestration/stacks/test_update.py
@@ -0,0 +1,84 @@
+# 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 logging
+
+from tempest.api.orchestration import base
+from tempest.common.utils import data_utils
+from tempest import test
+
+
+LOG = logging.getLogger(__name__)
+
+
+class UpdateStackTestJSON(base.BaseOrchestrationTest):
+ _interface = 'json'
+
+ template = '''
+heat_template_version: 2013-05-23
+resources:
+ random1:
+ type: OS::Heat::RandomString
+'''
+ update_template = '''
+heat_template_version: 2013-05-23
+resources:
+ random1:
+ type: OS::Heat::RandomString
+ random2:
+ type: OS::Heat::RandomString
+'''
+
+ def update_stack(self, stack_identifier, template):
+ stack_name = stack_identifier.split('/')[0]
+ resp = self.client.update_stack(
+ stack_identifier=stack_identifier,
+ name=stack_name,
+ template=template)
+ self.assertEqual('202', resp[0]['status'])
+ self.client.wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
+
+ @test.attr(type='gate')
+ def test_stack_update_nochange(self):
+ stack_name = data_utils.rand_name('heat')
+ stack_identifier = self.create_stack(stack_name, self.template)
+ self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
+ expected_resources = {'random1': 'OS::Heat::RandomString'}
+ self.assertEqual(expected_resources,
+ self.list_resources(stack_identifier))
+
+ # Update with no changes, resources should be unchanged
+ self.update_stack(stack_identifier, self.template)
+ self.assertEqual(expected_resources,
+ self.list_resources(stack_identifier))
+
+ @test.attr(type='gate')
+ @test.skip_because(bug='1308682')
+ def test_stack_update_add_remove(self):
+ stack_name = data_utils.rand_name('heat')
+ stack_identifier = self.create_stack(stack_name, self.template)
+ self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
+ initial_resources = {'random1': 'OS::Heat::RandomString'}
+ self.assertEqual(initial_resources,
+ self.list_resources(stack_identifier))
+
+ # Add one resource via a stack update
+ self.update_stack(stack_identifier, self.update_template)
+ updated_resources = {'random1': 'OS::Heat::RandomString',
+ 'random2': 'OS::Heat::RandomString'}
+ self.assertEqual(updated_resources,
+ self.list_resources(stack_identifier))
+
+ # Then remove it by updating with the original template
+ self.update_stack(stack_identifier, self.template)
+ self.assertEqual(initial_resources,
+ self.list_resources(stack_identifier))
diff --git a/tempest/api/volume/admin/test_volume_quotas_negative.py b/tempest/api/volume/admin/test_volume_quotas_negative.py
new file mode 100644
index 0000000..292f8ed
--- /dev/null
+++ b/tempest/api/volume/admin/test_volume_quotas_negative.py
@@ -0,0 +1,83 @@
+# Copyright 2014 OpenStack Foundation
+# 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.api.volume import base
+from tempest import exceptions
+from tempest import test
+
+
+class VolumeQuotasNegativeTestJSON(base.BaseVolumeV1AdminTest):
+ _interface = "json"
+ force_tenant_isolation = True
+
+ @classmethod
+ @test.safe_setup
+ def setUpClass(cls):
+ super(VolumeQuotasNegativeTestJSON, cls).setUpClass()
+ demo_user = cls.isolated_creds.get_primary_user()
+ cls.demo_tenant_id = demo_user.get('tenantId')
+ cls.shared_quota_set = {'gigabytes': 3, 'volumes': 1, 'snapshots': 1}
+
+ # NOTE(gfidente): no need to restore original quota set
+ # after the tests as they only work with tenant isolation.
+ resp, quota_set = cls.quotas_client.update_quota_set(
+ cls.demo_tenant_id,
+ **cls.shared_quota_set)
+
+ # NOTE(gfidente): no need to delete in tearDown as
+ # they are created using utility wrapper methods.
+ cls.volume = cls.create_volume()
+ cls.snapshot = cls.create_snapshot(cls.volume['id'])
+
+ @test.attr(type='negative')
+ def test_quota_volumes(self):
+ self.assertRaises(exceptions.OverLimit,
+ self.volumes_client.create_volume,
+ size=1)
+
+ @test.attr(type='negative')
+ def test_quota_volume_snapshots(self):
+ self.assertRaises(exceptions.OverLimit,
+ self.snapshots_client.create_snapshot,
+ self.volume['id'])
+
+ @test.attr(type='negative')
+ def test_quota_volume_gigabytes(self):
+ # NOTE(gfidente): quota set needs to be changed for this test
+ # or we may be limited by the volumes or snaps quota number, not by
+ # actual gigs usage; next line ensures shared set is restored.
+ self.addCleanup(self.quotas_client.update_quota_set,
+ self.demo_tenant_id,
+ **self.shared_quota_set)
+
+ new_quota_set = {'gigabytes': 2, 'volumes': 2, 'snapshots': 1}
+ resp, quota_set = self.quotas_client.update_quota_set(
+ self.demo_tenant_id,
+ **new_quota_set)
+ self.assertRaises(exceptions.OverLimit,
+ self.volumes_client.create_volume,
+ size=1)
+
+ new_quota_set = {'gigabytes': 2, 'volumes': 1, 'snapshots': 2}
+ resp, quota_set = self.quotas_client.update_quota_set(
+ self.demo_tenant_id,
+ **self.shared_quota_set)
+ self.assertRaises(exceptions.OverLimit,
+ self.snapshots_client.create_snapshot,
+ self.volume['id'])
+
+
+class VolumeQuotasNegativeTestXML(VolumeQuotasNegativeTestJSON):
+ _interface = "xml"
diff --git a/tempest/api/volume/test_volumes_get.py b/tempest/api/volume/test_volumes_get.py
index be5d76b..58da440 100644
--- a/tempest/api/volume/test_volumes_get.py
+++ b/tempest/api/volume/test_volumes_get.py
@@ -51,8 +51,7 @@
v_name = data_utils.rand_name('Volume')
metadata = {'Type': 'Test'}
# Create a volume
- resp, volume = self.client.create_volume(size=1,
- display_name=v_name,
+ resp, volume = self.client.create_volume(display_name=v_name,
metadata=metadata,
**kwargs)
self.assertEqual(200, resp.status)
diff --git a/tempest/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index 2ce3a4f..6294cd9 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -29,6 +29,9 @@
super(VolumesSnapshotTest, cls).setUpClass()
cls.volume_origin = cls.create_volume()
+ if not CONF.volume_feature_enabled.snapshot:
+ raise cls.skipException("Cinder volume snapshots are disabled")
+
@classmethod
def tearDownClass(cls):
super(VolumesSnapshotTest, cls).tearDownClass()
diff --git a/tempest/api/volume/test_volumes_snapshots_negative.py b/tempest/api/volume/test_volumes_snapshots_negative.py
index 9e47c03..61aa307 100644
--- a/tempest/api/volume/test_volumes_snapshots_negative.py
+++ b/tempest/api/volume/test_volumes_snapshots_negative.py
@@ -14,13 +14,23 @@
from tempest.api.volume import base
from tempest.common.utils import data_utils
+from tempest import config
from tempest import exceptions
from tempest import test
+CONF = config.CONF
+
class VolumesSnapshotNegativeTest(base.BaseVolumeV1Test):
_interface = "json"
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesSnapshotNegativeTest, cls).setUpClass()
+
+ if not CONF.volume_feature_enabled.snapshot:
+ raise cls.skipException("Cinder volume snapshots are disabled")
+
@test.attr(type=['negative', 'gate'])
def test_create_snapshot_with_nonexistent_volume_id(self):
# Create a snapshot with nonexistent volume id
diff --git a/tempest/api_schema/compute/aggregates.py b/tempest/api_schema/compute/aggregates.py
index 49793fe..a3ab3c8 100644
--- a/tempest/api_schema/compute/aggregates.py
+++ b/tempest/api_schema/compute/aggregates.py
@@ -12,6 +12,26 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
+
+aggregate = {
+ 'type': 'object',
+ 'properties': {
+ 'availability_zone': {'type': ['string', 'null']},
+ 'created_at': {'type': 'string'},
+ 'deleted': {'type': 'boolean'},
+ 'deleted_at': {'type': ['string', 'null']},
+ 'hosts': {'type': 'array'},
+ 'id': {'type': 'integer'},
+ 'metadata': {'type': 'object'},
+ 'name': {'type': 'string'},
+ 'updated_at': {'type': ['string', 'null']}
+ },
+ 'required': ['availability_zone', 'created_at', 'deleted',
+ 'deleted_at', 'hosts', 'id', 'metadata',
+ 'name', 'updated_at']
+}
+
list_aggregates = {
'status_code': [200],
'response_body': {
@@ -19,25 +39,28 @@
'properties': {
'aggregates': {
'type': 'array',
- 'items': {
- 'type': 'object',
- 'properties': {
- 'availability_zone': {'type': ['string', 'null']},
- 'created_at': {'type': 'string'},
- 'deleted': {'type': 'boolean'},
- 'deleted_at': {'type': ['string', 'null']},
- 'hosts': {'type': 'array'},
- 'id': {'type': 'integer'},
- 'metadata': {'type': 'object'},
- 'name': {'type': 'string'},
- 'updated_at': {'type': ['string', 'null']}
- },
- 'required': ['availability_zone', 'created_at', 'deleted',
- 'deleted_at', 'hosts', 'id', 'metadata',
- 'name', 'updated_at']
- }
+ 'items': aggregate
}
},
'required': ['aggregates']
}
}
+
+get_aggregate = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'aggregate': aggregate
+ },
+ 'required': ['aggregate']
+ }
+}
+
+aggregate_set_metadata = get_aggregate
+# The 'updated_at' attribute of 'update_aggregate' can't be null.
+update_aggregate = copy.deepcopy(get_aggregate)
+update_aggregate['response_body']['properties']['aggregate']['properties'][
+ 'updated_at'] = {
+ 'type': 'string'
+ }
diff --git a/tempest/api_schema/compute/availability_zone.py b/tempest/api_schema/compute/availability_zone.py
new file mode 100644
index 0000000..c1abc64
--- /dev/null
+++ b/tempest/api_schema/compute/availability_zone.py
@@ -0,0 +1,37 @@
+# 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.
+
+# NOTE: This is the detail information for "get az detail" API.
+# The information is the same between v2 and v3 APIs.
+detail = {
+ 'type': 'object',
+ 'patternProperties': {
+ # NOTE: Here is for a hostname
+ '^[a-zA-Z0-9-_.]+$': {
+ 'type': 'object',
+ 'patternProperties': {
+ # NOTE: Here is for a service name
+ '^.*$': {
+ 'type': 'object',
+ 'properties': {
+ 'available': {'type': 'boolean'},
+ 'active': {'type': 'boolean'},
+ 'updated_at': {'type': 'string'}
+ },
+ 'required': ['available', 'active', 'updated_at']
+ }
+ }
+ }
+ }
+}
diff --git a/tempest/api_schema/compute/flavors.py b/tempest/api_schema/compute/flavors.py
new file mode 100644
index 0000000..fd02780
--- /dev/null
+++ b/tempest/api_schema/compute/flavors.py
@@ -0,0 +1,64 @@
+# 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.api_schema.compute import parameter_types
+
+list_flavors = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'flavors': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'name': {'type': 'string'},
+ 'links': parameter_types.links,
+ 'id': {'type': 'string'}
+ },
+ 'required': ['name', 'links', 'id']
+ }
+ }
+ },
+ 'required': ['flavors']
+ }
+}
+
+common_flavor_list_details = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'flavors': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'name': {'type': 'string'},
+ 'links': parameter_types.links,
+ 'ram': {'type': 'integer'},
+ 'vcpus': {'type': 'integer'},
+ 'swap': {'type': 'integer'},
+ 'disk': {'type': 'integer'},
+ 'id': {'type': 'string'}
+ },
+ 'required': ['name', 'links', 'ram', 'vcpus',
+ 'swap', 'disk', 'id']
+ }
+ }
+ },
+ 'required': ['flavors']
+ }
+}
diff --git a/tempest/api_schema/compute/flavors_access.py b/tempest/api_schema/compute/flavors_access.py
index 152e24c..cd31b0a 100644
--- a/tempest/api_schema/compute/flavors_access.py
+++ b/tempest/api_schema/compute/flavors_access.py
@@ -12,7 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-list_flavor_access = {
+add_remove_list_flavor_access = {
'status_code': [200],
'response_body': {
'type': 'object',
diff --git a/tempest/api_schema/compute/hosts.py b/tempest/api_schema/compute/hosts.py
index b9a3db9..a73e214 100644
--- a/tempest/api_schema/compute/hosts.py
+++ b/tempest/api_schema/compute/hosts.py
@@ -33,3 +33,34 @@
'required': ['hosts']
}
}
+
+show_host_detail = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'host': {
+ 'type': 'array',
+ 'item': {
+ 'type': 'object',
+ 'properties': {
+ 'resource': {
+ 'type': 'object',
+ 'properties': {
+ 'cpu': {'type': 'integer'},
+ 'disk_gb': {'type': 'integer'},
+ 'host': {'type': 'string'},
+ 'memory_mb': {'type': 'integer'},
+ 'project': {'type': 'string'}
+ },
+ 'required': ['cpu', 'disk_gb', 'host',
+ 'memory_mb', 'project']
+ }
+ },
+ 'required': ['resource']
+ }
+ }
+ },
+ 'required': ['host']
+ }
+}
diff --git a/tempest/api_schema/compute/hypervisors.py b/tempest/api_schema/compute/hypervisors.py
new file mode 100644
index 0000000..630901e
--- /dev/null
+++ b/tempest/api_schema/compute/hypervisors.py
@@ -0,0 +1,197 @@
+# 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.
+
+import copy
+
+hypervisor_statistics = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'hypervisor_statistics': {
+ 'type': 'object',
+ 'properties': {
+ 'count': {'type': 'integer'},
+ 'current_workload': {'type': 'integer'},
+ 'disk_available_least': {'type': 'integer'},
+ 'free_disk_gb': {'type': 'integer'},
+ 'free_ram_mb': {'type': 'integer'},
+ 'local_gb': {'type': 'integer'},
+ 'local_gb_used': {'type': 'integer'},
+ 'memory_mb': {'type': 'integer'},
+ 'memory_mb_used': {'type': 'integer'},
+ 'running_vms': {'type': 'integer'},
+ 'vcpus': {'type': 'integer'},
+ 'vcpus_used': {'type': 'integer'}
+ },
+ 'required': ['count', 'current_workload',
+ 'disk_available_least', 'free_disk_gb',
+ 'free_ram_mb', 'local_gb', 'local_gb_used',
+ 'memory_mb', 'memory_mb_used', 'running_vms',
+ 'vcpus', 'vcpus_used']
+ }
+ },
+ 'required': ['hypervisor_statistics']
+ }
+}
+
+common_list_hypervisors_detail = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'hypervisors': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'cpu_info': {'type': 'string'},
+ 'current_workload': {'type': 'integer'},
+ 'disk_available_least': {'type': ['integer', 'null']},
+ 'host_ip': {
+ 'type': 'string',
+ 'format': 'ip-address'
+ },
+ 'free_disk_gb': {'type': 'integer'},
+ 'free_ram_mb': {'type': 'integer'},
+ 'hypervisor_hostname': {'type': 'string'},
+ 'hypervisor_type': {'type': 'string'},
+ 'hypervisor_version': {'type': 'integer'},
+ 'id': {'type': ['integer', 'string']},
+ 'local_gb': {'type': 'integer'},
+ 'local_gb_used': {'type': 'integer'},
+ 'memory_mb': {'type': 'integer'},
+ 'memory_mb_used': {'type': 'integer'},
+ 'running_vms': {'type': 'integer'},
+ 'service': {
+ 'type': 'object',
+ 'properties': {
+ 'host': {'type': 'string'},
+ 'id': {'type': ['integer', 'string']}
+ },
+ 'required': ['host', 'id']
+ },
+ 'vcpus': {'type': 'integer'},
+ 'vcpus_used': {'type': 'integer'}
+ },
+ 'required': ['cpu_info', 'current_workload',
+ 'disk_available_least', 'host_ip',
+ 'free_disk_gb', 'free_ram_mb',
+ 'hypervisor_hostname', 'hypervisor_type',
+ 'hypervisor_version', 'id', 'local_gb',
+ 'local_gb_used', 'memory_mb',
+ 'memory_mb_used', 'running_vms', 'service',
+ 'vcpus', 'vcpus_used']
+ }
+ }
+ },
+ 'required': ['hypervisors']
+ }
+}
+
+common_show_hypervisor = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'hypervisor': {
+ 'type': 'object',
+ 'properties': {
+ 'cpu_info': {'type': 'string'},
+ 'current_workload': {'type': 'integer'},
+ 'disk_available_least': {'type': 'integer'},
+ 'host_ip': {
+ 'type': 'string',
+ 'format': 'ip-address'
+ },
+ 'free_disk_gb': {'type': 'integer'},
+ 'free_ram_mb': {'type': 'integer'},
+ 'hypervisor_hostname': {'type': 'string'},
+ 'hypervisor_type': {'type': 'string'},
+ 'hypervisor_version': {'type': 'integer'},
+ 'id': {'type': ['integer', 'string']},
+ 'local_gb': {'type': 'integer'},
+ 'local_gb_used': {'type': 'integer'},
+ 'memory_mb': {'type': 'integer'},
+ 'memory_mb_used': {'type': 'integer'},
+ 'running_vms': {'type': 'integer'},
+ 'service': {
+ 'type': 'object',
+ 'properties': {
+ 'host': {'type': 'string'},
+ 'id': {'type': ['integer', 'string']}
+ },
+ 'required': ['host', 'id']
+ },
+ 'vcpus': {'type': 'integer'},
+ 'vcpus_used': {'type': 'integer'}
+ },
+ 'required': ['cpu_info', 'current_workload',
+ 'disk_available_least', 'host_ip',
+ 'free_disk_gb', 'free_ram_mb',
+ 'hypervisor_hostname', 'hypervisor_type',
+ 'hypervisor_version', 'id', 'local_gb',
+ 'local_gb_used', 'memory_mb', 'memory_mb_used',
+ 'running_vms', 'service', 'vcpus', 'vcpus_used']
+ }
+ },
+ 'required': ['hypervisor']
+ }
+}
+
+common_hypervisors_detail = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'hypervisors': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': ['integer', 'string']},
+ 'hypervisor_hostname': {'type': 'string'}
+ },
+ 'required': ['id', 'hypervisor_hostname']
+ }
+ }
+ },
+ 'required': ['hypervisors']
+ }
+}
+
+common_hypervisors_info = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'hypervisor': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': ['integer', 'string']},
+ 'hypervisor_hostname': {'type': 'string'},
+ },
+ 'required': ['id', 'hypervisor_hostname']
+ }
+ },
+ 'required': ['hypervisor']
+ }
+}
+
+
+hypervisor_uptime = copy.deepcopy(common_hypervisors_info)
+hypervisor_uptime['response_body']['properties']['hypervisor'][
+ 'properties']['uptime'] = {'type': 'string'}
+hypervisor_uptime['response_body']['properties']['hypervisor'][
+ 'required'] = ['id', 'hypervisor_hostname', 'uptime']
diff --git a/tempest/api_schema/compute/interfaces.py b/tempest/api_schema/compute/interfaces.py
new file mode 100644
index 0000000..1e15c18
--- /dev/null
+++ b/tempest/api_schema/compute/interfaces.py
@@ -0,0 +1,17 @@
+# 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.
+
+delete_interface = {
+ 'status_code': [202]
+}
diff --git a/tempest/api_schema/compute/keypairs.py b/tempest/api_schema/compute/keypairs.py
index 8973c02..b8f905f 100644
--- a/tempest/api_schema/compute/keypairs.py
+++ b/tempest/api_schema/compute/keypairs.py
@@ -39,3 +39,27 @@
'required': ['keypairs']
}
}
+
+create_keypair = {
+ 'type': 'object',
+ 'properties': {
+ 'keypair': {
+ 'type': 'object',
+ 'properties': {
+ 'fingerprint': {'type': 'string'},
+ 'name': {'type': 'string'},
+ 'public_key': {'type': 'string'},
+ # NOTE: Now the type of 'user_id' is integer, but here
+ # allows 'string' also because we will be able to change
+ # it to 'uuid' in the future.
+ 'user_id': {'type': ['integer', 'string']},
+ 'private_key': {'type': 'string'}
+ },
+ # When create keypair API is being called with 'Public key'
+ # (Importing keypair) then, response body does not contain
+ # 'private_key' So it is not defined as 'required'
+ 'required': ['fingerprint', 'name', 'public_key', 'user_id']
+ }
+ },
+ 'required': ['keypair']
+}
diff --git a/tempest/api_schema/compute/parameter_types.py b/tempest/api_schema/compute/parameter_types.py
index 67c0c9b..95d5b92 100644
--- a/tempest/api_schema/compute/parameter_types.py
+++ b/tempest/api_schema/compute/parameter_types.py
@@ -26,3 +26,8 @@
'required': ['href', 'rel']
}
}
+
+mac_address = {
+ 'type': 'string',
+ 'pattern': '(?:[a-f0-9]{2}:){5}[a-f0-9]{2}'
+}
diff --git a/tempest/api_schema/compute/servers.py b/tempest/api_schema/compute/servers.py
new file mode 100644
index 0000000..a273abb
--- /dev/null
+++ b/tempest/api_schema/compute/servers.py
@@ -0,0 +1,49 @@
+# 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.
+
+get_password = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'password': {'type': 'string'}
+ },
+ 'required': ['password']
+ }
+}
+
+get_vnc_console = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'console': {
+ 'type': 'object',
+ 'properties': {
+ 'type': {'type': 'string'},
+ 'url': {
+ 'type': 'string',
+ 'format': 'uri'
+ }
+ },
+ 'required': ['type', 'url']
+ }
+ },
+ 'required': ['console']
+ }
+}
+
+delete_server = {
+ 'status_code': [204],
+}
diff --git a/tempest/api_schema/compute/services.py b/tempest/api_schema/compute/services.py
index 4793f5a..4c58013 100644
--- a/tempest/api_schema/compute/services.py
+++ b/tempest/api_schema/compute/services.py
@@ -42,3 +42,22 @@
'required': ['services']
}
}
+
+enable_service = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'service': {
+ 'type': 'object',
+ 'properties': {
+ 'status': {'type': 'string'},
+ 'binary': {'type': 'string'},
+ 'host': {'type': 'string'}
+ },
+ 'required': ['status', 'binary', 'host']
+ }
+ },
+ 'required': ['service']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/agents.py b/tempest/api_schema/compute/v2/agents.py
new file mode 100644
index 0000000..837731f
--- /dev/null
+++ b/tempest/api_schema/compute/v2/agents.py
@@ -0,0 +1,17 @@
+# 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.
+
+delete_agent = {
+ 'status_code': [200]
+}
diff --git a/tempest/api_schema/compute/v2/availability_zone.py b/tempest/api_schema/compute/v2/availability_zone.py
new file mode 100644
index 0000000..d3d2787
--- /dev/null
+++ b/tempest/api_schema/compute/v2/availability_zone.py
@@ -0,0 +1,54 @@
+# 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.
+
+import copy
+
+from tempest.api_schema.compute import availability_zone as common
+
+
+base = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'availabilityZoneInfo': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'zoneName': {'type': 'string'},
+ 'zoneState': {
+ 'type': 'object',
+ 'properties': {
+ 'available': {'type': 'boolean'}
+ },
+ 'required': ['available']
+ },
+ # NOTE: Here is the difference between detail and
+ # non-detail.
+ 'hosts': {'type': 'null'}
+ },
+ 'required': ['zoneName', 'zoneState', 'hosts']
+ }
+ }
+ },
+ 'required': ['availabilityZoneInfo']
+ }
+}
+
+get_availability_zone_list = copy.deepcopy(base)
+
+get_availability_zone_list_detail = copy.deepcopy(base)
+get_availability_zone_list_detail['response_body']['properties'][
+ 'availabilityZoneInfo']['items']['properties']['hosts'] = common.detail
diff --git a/tempest/api_schema/compute/v2/extensions.py b/tempest/api_schema/compute/v2/extensions.py
new file mode 100644
index 0000000..570cd03
--- /dev/null
+++ b/tempest/api_schema/compute/v2/extensions.py
@@ -0,0 +1,45 @@
+# 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.
+
+list_extensions = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'extensions': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'updated': {
+ 'type': 'string',
+ 'format': 'data-time'
+ },
+ 'name': {'type': 'string'},
+ 'links': {'type': 'array'},
+ 'namespace': {
+ 'type': 'string',
+ 'format': 'uri'
+ },
+ 'alias': {'type': 'string'},
+ 'description': {'type': 'string'}
+ },
+ 'required': ['updated', 'name', 'links', 'namespace',
+ 'alias', 'description']
+ }
+ }
+ },
+ 'required': ['extensions']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/fixed_ips.py b/tempest/api_schema/compute/v2/fixed_ips.py
index a6add04..446633f 100644
--- a/tempest/api_schema/compute/v2/fixed_ips.py
+++ b/tempest/api_schema/compute/v2/fixed_ips.py
@@ -34,3 +34,8 @@
'required': ['fixed_ip']
}
}
+
+fixed_ip_action = {
+ 'status_code': [202],
+ 'response_body': {'type': 'string'}
+}
diff --git a/tempest/api_schema/compute/v2/flavors.py b/tempest/api_schema/compute/v2/flavors.py
new file mode 100644
index 0000000..999ca19
--- /dev/null
+++ b/tempest/api_schema/compute/v2/flavors.py
@@ -0,0 +1,33 @@
+# 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.
+
+import copy
+
+from tempest.api_schema.compute import flavors
+
+list_flavors_details = copy.deepcopy(flavors.common_flavor_list_details)
+
+# 'swap' attributes comes as integre value but if it is empty it comes as "".
+# So defining type of as string and integer.
+list_flavors_details['response_body']['properties']['flavors']['items'][
+ 'properties']['swap'] = {'type': ['string', 'integer']}
+
+# Defining extra attributes for V2 flavor schema
+list_flavors_details['response_body']['properties']['flavors']['items'][
+ 'properties'].update({'OS-FLV-DISABLED:disabled': {'type': 'boolean'},
+ 'os-flavor-access:is_public': {'type': 'boolean'},
+ 'rxtx_factor': {'type': 'number'},
+ 'OS-FLV-EXT-DATA:ephemeral': {'type': 'integer'}})
+# 'OS-FLV-DISABLED', 'os-flavor-access', 'rxtx_factor' and 'OS-FLV-EXT-DATA'
+# are API extensions. So they are not 'required'.
diff --git a/tempest/api_schema/compute/v2/hosts.py b/tempest/api_schema/compute/v2/hosts.py
new file mode 100644
index 0000000..cd6bd7b
--- /dev/null
+++ b/tempest/api_schema/compute/v2/hosts.py
@@ -0,0 +1,43 @@
+# 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.
+
+import copy
+
+body = {
+ 'type': 'object',
+ 'properties': {
+ 'host': {'type': 'string'},
+ 'power_action': {'enum': ['startup']}
+ },
+ 'required': ['host', 'power_action']
+}
+
+startup_host = {
+ 'status_code': [200],
+ 'response_body': body
+}
+
+# The 'power_action' attribute of 'shutdown_host' API is 'shutdown'
+shutdown_host = copy.deepcopy(startup_host)
+
+shutdown_host['response_body']['properties']['power_action'] = {
+ 'enum': ['shutdown']
+}
+
+# The 'power_action' attribute of 'reboot_host' API is 'reboot'
+reboot_host = copy.deepcopy(startup_host)
+
+reboot_host['response_body']['properties']['power_action'] = {
+ 'enum': ['reboot']
+}
diff --git a/tempest/api_schema/compute/v2/hypervisors.py b/tempest/api_schema/compute/v2/hypervisors.py
new file mode 100644
index 0000000..6bb43a7
--- /dev/null
+++ b/tempest/api_schema/compute/v2/hypervisors.py
@@ -0,0 +1,37 @@
+# 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.
+
+import copy
+from tempest.api_schema.compute import hypervisors
+
+hypervisors_servers = copy.deepcopy(hypervisors.common_hypervisors_detail)
+
+# Defining extra attributes for V3 show hypervisor schema
+hypervisors_servers['response_body']['properties']['hypervisors']['items'][
+ 'properties']['servers'] = {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ # NOTE: Now the type of 'id' is integer,
+ # but here allows 'string' also because we
+ # will be able to change it to 'uuid' in
+ # the future.
+ 'id': {'type': ['integer', 'string']},
+ 'name': {'type': 'string'}
+ }
+ }
+ }
+# In V2 API, if there is no servers (VM) on the Hypervisor host then 'servers'
+# attribute will not be present in response body So it is not 'required'.
diff --git a/tempest/api_schema/compute/v2/images.py b/tempest/api_schema/compute/v2/images.py
index 41593c6..fad6b56 100644
--- a/tempest/api_schema/compute/v2/images.py
+++ b/tempest/api_schema/compute/v2/images.py
@@ -90,3 +90,33 @@
'required': ['images']
}
}
+
+create_image = {
+ 'status_code': [202]
+}
+
+delete = {
+ 'status_code': [204]
+}
+
+image_metadata = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'metadata': {'type': 'object'}
+ },
+ 'required': ['metadata']
+ }
+}
+
+image_meta_item = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'meta': {'type': 'object'}
+ },
+ 'required': ['meta']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/instance_usage_audit_logs.py b/tempest/api_schema/compute/v2/instance_usage_audit_logs.py
new file mode 100644
index 0000000..658f574
--- /dev/null
+++ b/tempest/api_schema/compute/v2/instance_usage_audit_logs.py
@@ -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.
+
+common_instance_usage_audit_log = {
+ 'type': 'object',
+ 'properties': {
+ 'hosts_not_run': {
+ 'type': 'array',
+ 'items': {'type': 'string'}
+ },
+ 'log': {'type': 'object'},
+ 'num_hosts': {'type': 'integer'},
+ 'num_hosts_done': {'type': 'integer'},
+ 'num_hosts_not_run': {'type': 'integer'},
+ 'num_hosts_running': {'type': 'integer'},
+ 'overall_status': {'type': 'string'},
+ 'period_beginning': {'type': 'string'},
+ 'period_ending': {'type': 'string'},
+ 'total_errors': {'type': 'integer'},
+ 'total_instances': {'type': 'integer'}
+ },
+ 'required': ['hosts_not_run', 'log', 'num_hosts', 'num_hosts_done',
+ 'num_hosts_not_run', 'num_hosts_running', 'overall_status',
+ 'period_beginning', 'period_ending', 'total_errors',
+ 'total_instances']
+}
+
+get_instance_usage_audit_log = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'instance_usage_audit_log': common_instance_usage_audit_log
+ },
+ 'required': ['instance_usage_audit_log']
+ }
+}
+
+list_instance_usage_audit_log = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'instance_usage_audit_logs': common_instance_usage_audit_log
+ },
+ 'required': ['instance_usage_audit_logs']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/keypairs.py b/tempest/api_schema/compute/v2/keypairs.py
index 3225b0d..9a025c3 100644
--- a/tempest/api_schema/compute/v2/keypairs.py
+++ b/tempest/api_schema/compute/v2/keypairs.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.api_schema.compute import keypairs
+
get_keypair = {
'status_code': [200],
'response_body': {
@@ -45,3 +47,12 @@
'required': ['keypair']
}
}
+
+create_keypair = {
+ 'status_code': [200],
+ 'response_body': keypairs.create_keypair
+}
+
+delete_keypair = {
+ 'status_code': [202],
+}
diff --git a/tempest/api_schema/compute/v2/quotas.py b/tempest/api_schema/compute/v2/quotas.py
index d69cbd7..17dc4dd 100644
--- a/tempest/api_schema/compute/v2/quotas.py
+++ b/tempest/api_schema/compute/v2/quotas.py
@@ -45,3 +45,7 @@
'required': ['quota_set']
}
}
+
+delete_quota = {
+ 'status_code': [202]
+}
diff --git a/tempest/api_schema/compute/v2/security_groups.py b/tempest/api_schema/compute/v2/security_groups.py
index 68b65b4..6dd44cd 100644
--- a/tempest/api_schema/compute/v2/security_groups.py
+++ b/tempest/api_schema/compute/v2/security_groups.py
@@ -36,3 +36,41 @@
'required': ['security_groups']
}
}
+
+create_security_group_rule = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'security_group_rule': {
+ 'type': 'object',
+ 'properties': {
+ 'from_port': {'type': 'integer'},
+ 'to_port': {'type': 'integer'},
+ 'group': {'type': 'object'},
+ 'ip_protocol': {'type': 'string'},
+ # 'parent_group_id' can be UUID so defining it
+ # as 'string' also.
+ 'parent_group_id': {'type': ['integer', 'string']},
+ 'id': {'type': ['integer', 'string']},
+ 'ip_range': {
+ 'type': 'object',
+ 'properties': {
+ 'cidr': {'type': 'string'}
+ }
+ # When optional argument is provided in request body
+ # like 'group_id' then, attribute 'cidr' does not
+ # comes in response body. So it is not 'required'.
+ }
+ },
+ 'required': ['from_port', 'to_port', 'group', 'ip_protocol',
+ 'parent_group_id', 'id', 'ip_range']
+ }
+ },
+ 'required': ['security_group_rule']
+ }
+}
+
+delete_security_group_rule = {
+ 'status_code': [202]
+}
diff --git a/tempest/api_schema/compute/v2/servers.py b/tempest/api_schema/compute/v2/servers.py
index b4e6e53..eed4589 100644
--- a/tempest/api_schema/compute/v2/servers.py
+++ b/tempest/api_schema/compute/v2/servers.py
@@ -34,9 +34,59 @@
# NOTE: OS-DCF:diskConfig is API extension, and some
# environments return a response without the attribute.
# So it is not 'required'.
- 'required': ['id', 'security_groups', 'links', 'adminPass']
+ # NOTE: adminPass is not required because it can be deactivated
+ # with nova API flag enable_instance_password=False
+ 'required': ['id', 'security_groups', 'links']
}
},
'required': ['server']
}
}
+
+list_virtual_interfaces = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'virtual_interfaces': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'mac_address': parameter_types.mac_address,
+ 'OS-EXT-VIF-NET:net_id': {'type': 'string'}
+ },
+ # 'OS-EXT-VIF-NET:net_id' is API extension So it is
+ # not defined as 'required'
+ 'required': ['id', 'mac_address']
+ }
+ }
+ },
+ 'required': ['virtual_interfaces']
+ }
+}
+
+attach_volume = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'volumeAttachment': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'device': {'type': 'string'},
+ 'volumeId': {'type': 'string'},
+ 'serverId': {'type': ['integer', 'string']}
+ },
+ 'required': ['id', 'device', 'volumeId', 'serverId']
+ }
+ },
+ 'required': ['volumeAttachment']
+ }
+}
+
+detach_volume = {
+ 'status_code': [202]
+}
diff --git a/tempest/api_schema/compute/v2/volumes.py b/tempest/api_schema/compute/v2/volumes.py
index 9cfd7e3..84a659c 100644
--- a/tempest/api_schema/compute/v2/volumes.py
+++ b/tempest/api_schema/compute/v2/volumes.py
@@ -12,7 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-get_volume = {
+create_get_volume = {
'status_code': [200],
'response_body': {
'type': 'object',
@@ -108,3 +108,7 @@
'required': ['volumes']
}
}
+
+delete_volume = {
+ 'status_code': [202]
+}
diff --git a/tempest/api_schema/compute/v3/agents.py b/tempest/api_schema/compute/v3/agents.py
new file mode 100644
index 0000000..63d1c46
--- /dev/null
+++ b/tempest/api_schema/compute/v3/agents.py
@@ -0,0 +1,17 @@
+# 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.
+
+delete_agent = {
+ 'status_code': [204]
+}
diff --git a/tempest/api_schema/compute/v3/availability_zone.py b/tempest/api_schema/compute/v3/availability_zone.py
new file mode 100644
index 0000000..5f36c33
--- /dev/null
+++ b/tempest/api_schema/compute/v3/availability_zone.py
@@ -0,0 +1,53 @@
+# 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.
+
+import copy
+
+from tempest.api_schema.compute import availability_zone as common
+
+
+base = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'availability_zone_info': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'zone_name': {'type': 'string'},
+ 'zone_state': {
+ 'type': 'object',
+ 'properties': {
+ 'available': {'type': 'boolean'}
+ },
+ 'required': ['available']
+ },
+ # NOTE: Here is the difference between detail and
+ # non-detail
+ 'hosts': {'type': 'null'}
+ },
+ 'required': ['zone_name', 'zone_state', 'hosts']
+ }
+ }
+ },
+ 'required': ['availability_zone_info']
+ }
+}
+
+get_availability_zone_list = copy.deepcopy(base)
+get_availability_zone_list_detail = copy.deepcopy(base)
+get_availability_zone_list_detail['response_body']['properties'][
+ 'availability_zone_info']['items']['properties']['hosts'] = common.detail
diff --git a/tempest/api_schema/compute/v3/extensions.py b/tempest/api_schema/compute/v3/extensions.py
new file mode 100644
index 0000000..ceb0ce2
--- /dev/null
+++ b/tempest/api_schema/compute/v3/extensions.py
@@ -0,0 +1,36 @@
+# 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.
+
+list_extensions = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'extensions': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'name': {'type': 'string'},
+ 'alias': {'type': 'string'},
+ 'description': {'type': 'string'},
+ 'version': {'type': 'integer'}
+ },
+ 'required': ['name', 'alias', 'description', 'version']
+ }
+ }
+ },
+ 'required': ['extensions']
+ }
+}
diff --git a/tempest/api_schema/compute/v3/flavors.py b/tempest/api_schema/compute/v3/flavors.py
new file mode 100644
index 0000000..542d2b1
--- /dev/null
+++ b/tempest/api_schema/compute/v3/flavors.py
@@ -0,0 +1,33 @@
+# 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.
+
+import copy
+
+from tempest.api_schema.compute import flavors
+
+list_flavors_details = copy.deepcopy(flavors.common_flavor_list_details)
+
+# NOTE- In v3 API, 'swap' comes as '0' not empty string '""'
+# (In V2 API, it comes as empty string) So leaving 'swap'as integer type only.
+
+# Defining extra attributes for V3 flavor schema
+list_flavors_details['response_body']['properties']['flavors']['items'][
+ 'properties'].update({'disabled': {'type': 'boolean'},
+ 'ephemeral': {'type': 'integer'},
+ 'flavor-access:is_public': {'type': 'boolean'},
+ 'os-flavor-rxtx:rxtx_factor': {'type': 'number'}})
+# 'flavor-access' and 'os-flavor-rxtx' are API extensions.
+# So they are not 'required'.
+list_flavors_details['response_body']['properties']['flavors']['items'][
+ 'required'].extend(['disabled', 'ephemeral'])
diff --git a/tempest/api_schema/compute/v3/hosts.py b/tempest/api_schema/compute/v3/hosts.py
new file mode 100644
index 0000000..2cf8f9b
--- /dev/null
+++ b/tempest/api_schema/compute/v3/hosts.py
@@ -0,0 +1,41 @@
+# 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.
+
+import copy
+from tempest.api_schema.compute.v2 import hosts
+
+startup_host = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'host': hosts.body
+ },
+ 'required': ['host']
+ }
+}
+
+# The 'power_action' attribute of 'shutdown_host' API is 'shutdown'
+shutdown_host = copy.deepcopy(startup_host)
+
+shutdown_host['response_body']['properties']['power_action'] = {
+ 'enum': ['shutdown']
+}
+
+# The 'power_action' attribute of 'reboot_host' API is 'reboot'
+reboot_host = copy.deepcopy(startup_host)
+
+reboot_host['response_body']['properties']['power_action'] = {
+ 'enum': ['reboot']
+}
diff --git a/tempest/api_schema/compute/v3/hypervisors.py b/tempest/api_schema/compute/v3/hypervisors.py
new file mode 100644
index 0000000..aa31827
--- /dev/null
+++ b/tempest/api_schema/compute/v3/hypervisors.py
@@ -0,0 +1,50 @@
+# 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.
+
+import copy
+from tempest.api_schema.compute import hypervisors
+
+list_hypervisors_detail = copy.deepcopy(
+ hypervisors.common_list_hypervisors_detail)
+# Defining extra attributes for V3 show hypervisor schema
+list_hypervisors_detail['response_body']['properties']['hypervisors'][
+ 'items']['properties']['os-pci:pci_stats'] = {'type': 'array'}
+
+show_hypervisor = copy.deepcopy(hypervisors.common_show_hypervisor)
+# Defining extra attributes for V3 show hypervisor schema
+show_hypervisor['response_body']['properties']['hypervisor']['properties'][
+ 'os-pci:pci_stats'] = {'type': 'array'}
+
+hypervisors_servers = copy.deepcopy(hypervisors.common_hypervisors_info)
+
+# Defining extra attributes for V3 show hypervisor schema
+hypervisors_servers['response_body']['properties']['hypervisor']['properties'][
+ 'servers'] = {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ # NOTE: Now the type of 'id' is integer,
+ # but here allows 'string' also because we
+ # will be able to change it to 'uuid' in
+ # the future.
+ 'id': {'type': ['integer', 'string']},
+ 'name': {'type': 'string'}
+ }
+ }
+ }
+# V3 API response body always contains the 'servers' attribute even there
+# is no server (VM) are present on Hypervisor host.
+hypervisors_servers['response_body']['properties']['hypervisor'][
+ 'required'] = ['id', 'hypervisor_hostname', 'servers']
diff --git a/tempest/api_schema/compute/v3/keypairs.py b/tempest/api_schema/compute/v3/keypairs.py
index 0197c84..de5f4ba 100644
--- a/tempest/api_schema/compute/v3/keypairs.py
+++ b/tempest/api_schema/compute/v3/keypairs.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.api_schema.compute import keypairs
+
get_keypair = {
'status_code': [200],
'response_body': {
@@ -30,3 +32,12 @@
'required': ['keypair']
}
}
+
+create_keypair = {
+ 'status_code': [201],
+ 'response_body': keypairs.create_keypair
+}
+
+delete_keypair = {
+ 'status_code': [204],
+}
diff --git a/tempest/api_schema/compute/v3/quotas.py b/tempest/api_schema/compute/v3/quotas.py
index 1b9989d..aec1e80 100644
--- a/tempest/api_schema/compute/v3/quotas.py
+++ b/tempest/api_schema/compute/v3/quotas.py
@@ -40,3 +40,46 @@
'required': ['quota_set']
}
}
+
+quota_common_info = {
+ 'type': 'object',
+ 'properties': {
+ 'reserved': {'type': 'integer'},
+ 'limit': {'type': 'integer'},
+ 'in_use': {'type': 'integer'}
+ },
+ 'required': ['reserved', 'limit', 'in_use']
+}
+
+quota_set_detail = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'quota_set': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'instances': quota_common_info,
+ 'cores': quota_common_info,
+ 'ram': quota_common_info,
+ 'floating_ips': quota_common_info,
+ 'fixed_ips': quota_common_info,
+ 'metadata_items': quota_common_info,
+ 'key_pairs': quota_common_info,
+ 'security_groups': quota_common_info,
+ 'security_group_rules': quota_common_info
+ },
+ 'required': ['id', 'instances', 'cores', 'ram',
+ 'floating_ips', 'fixed_ips',
+ 'metadata_items', 'key_pairs',
+ 'security_groups', 'security_group_rules']
+ }
+ },
+ 'required': ['quota_set']
+ }
+}
+
+delete_quota = {
+ 'status_code': [204]
+}
diff --git a/tempest/api_schema/compute/v3/servers.py b/tempest/api_schema/compute/v3/servers.py
index 390962e..f2a4b78 100644
--- a/tempest/api_schema/compute/v3/servers.py
+++ b/tempest/api_schema/compute/v3/servers.py
@@ -42,3 +42,7 @@
'required': ['server']
}
}
+
+attach_detach_volume = {
+ 'status_code': [202]
+}
diff --git a/tempest/api_schema/compute/version.py b/tempest/api_schema/compute/version.py
new file mode 100644
index 0000000..32c6d96
--- /dev/null
+++ b/tempest/api_schema/compute/version.py
@@ -0,0 +1,55 @@
+# 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.
+
+version = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'version': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'links': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'href': {'type': 'string', 'format': 'uri'},
+ 'rel': {'type': 'string'},
+ 'type': {'type': 'string'}
+ },
+ 'required': ['href', 'rel']
+ }
+ },
+ 'media-types': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'base': {'type': 'string'},
+ 'type': {'type': 'string'}
+ },
+ 'required': ['base', 'type']
+ }
+ },
+ 'status': {'type': 'string'},
+ 'updated': {'type': 'string', 'format': 'date-time'}
+ },
+ 'required': ['id', 'links', 'media-types', 'status', 'updated']
+ }
+ },
+ 'required': ['version']
+ }
+}
diff --git a/tempest/cli/simple_read_only/test_cinder.py b/tempest/cli/simple_read_only/test_cinder.py
index afbd732..723333b 100644
--- a/tempest/cli/simple_read_only/test_cinder.py
+++ b/tempest/cli/simple_read_only/test_cinder.py
@@ -16,6 +16,7 @@
import logging
import re
import subprocess
+import testtools
import tempest.cli
from tempest import config
@@ -86,6 +87,8 @@
def test_cinder_rate_limits(self):
self.cinder('rate-limits')
+ @testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
+ 'Volume snapshot not available.')
def test_cinder_snapshot_list(self):
self.cinder('snapshot-list')
diff --git a/tempest/cli/simple_read_only/test_sahara.py b/tempest/cli/simple_read_only/test_sahara.py
index cd819a4..36cc324 100644
--- a/tempest/cli/simple_read_only/test_sahara.py
+++ b/tempest/cli/simple_read_only/test_sahara.py
@@ -48,23 +48,93 @@
def test_sahara_plugins_list(self):
plugins = self.parser.listing(self.sahara('plugin-list'))
- self.assertTableStruct(plugins, ['name', 'versions', 'title'])
+ self.assertTableStruct(plugins, [
+ 'name',
+ 'versions',
+ 'title'
+ ])
def test_sahara_plugins_show(self):
- plugin = self.parser.listing(self.sahara('plugin-show',
- params='--name vanilla'))
- self.assertTableStruct(plugin, ['Property', 'Value'])
+ result = self.sahara('plugin-show', params='--name vanilla')
+ plugin = self.parser.listing(result)
+ self.assertTableStruct(plugin, [
+ 'Property',
+ 'Value'
+ ])
def test_sahara_node_group_template_list(self):
- plugins = self.parser.listing(self.sahara('node-group-template-list'))
- self.assertTableStruct(plugins, ['name', 'id', 'plugin_name',
- 'node_processes', 'description'])
+ result = self.sahara('node-group-template-list')
+ node_group_templates = self.parser.listing(result)
+ self.assertTableStruct(node_group_templates, [
+ 'name',
+ 'id',
+ 'plugin_name',
+ 'node_processes',
+ 'description'
+ ])
def test_sahara_cluster_template_list(self):
- plugins = self.parser.listing(self.sahara('cluster-template-list'))
- self.assertTableStruct(plugins, ['name', 'id', 'plugin_name',
- 'node_groups', 'description'])
+ result = self.sahara('cluster-template-list')
+ cluster_templates = self.parser.listing(result)
+ self.assertTableStruct(cluster_templates, [
+ 'name',
+ 'id',
+ 'plugin_name',
+ 'node_groups',
+ 'description'
+ ])
def test_sahara_cluster_list(self):
- plugins = self.parser.listing(self.sahara('cluster-list'))
- self.assertTableStruct(plugins, ['name', 'id', 'status', 'node_count'])
+ result = self.sahara('cluster-list')
+ clusters = self.parser.listing(result)
+ self.assertTableStruct(clusters, [
+ 'name',
+ 'id',
+ 'status',
+ 'node_count'
+ ])
+
+ def test_sahara_data_source_list(self):
+ result = self.sahara('data-source-list')
+ data_sources = self.parser.listing(result)
+ self.assertTableStruct(data_sources, [
+ 'name',
+ 'id',
+ 'type',
+ 'description'
+ ])
+
+ def test_sahara_job_binary_data_list(self):
+ result = self.sahara('job-binary-data-list')
+ job_binary_data_list = self.parser.listing(result)
+ self.assertTableStruct(job_binary_data_list, [
+ 'id',
+ 'name'
+ ])
+
+ def test_sahara_job_binary_list(self):
+ result = self.sahara('job-binary-list')
+ job_binaries = self.parser.listing(result)
+ self.assertTableStruct(job_binaries, [
+ 'id',
+ 'name',
+ 'description'
+ ])
+
+ def test_sahara_job_template_list(self):
+ result = self.sahara('job-template-list')
+ job_templates = self.parser.listing(result)
+ self.assertTableStruct(job_templates, [
+ 'id',
+ 'name',
+ 'description'
+ ])
+
+ def test_sahara_job_list(self):
+ result = self.sahara('job-list')
+ jobs = self.parser.listing(result)
+ self.assertTableStruct(jobs, [
+ 'id',
+ 'cluster_id',
+ 'status'
+ ])
diff --git a/tempest/clients.py b/tempest/clients.py
index 7ebd983..0ebbd7c 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -13,15 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
-# Default client libs
-import cinderclient.client
-import glanceclient
-import heatclient.client
import keystoneclient.exceptions
import keystoneclient.v2_0.client
-import neutronclient.v2_0.client
-import novaclient.client
-import swiftclient
from tempest.common.rest_client import NegativeRestClient
from tempest import config
@@ -30,6 +23,8 @@
from tempest.openstack.common import log as logging
from tempest.services.baremetal.v1.client_json import BaremetalClientJSON
from tempest.services import botoclients
+from tempest.services.compute.json.agents_client import \
+ AgentsClientJSON
from tempest.services.compute.json.aggregates_client import \
AggregatesClientJSON
from tempest.services.compute.json.availability_zone_client import \
@@ -52,6 +47,8 @@
InterfacesClientJSON
from tempest.services.compute.json.keypairs_client import KeyPairsClientJSON
from tempest.services.compute.json.limits_client import LimitsClientJSON
+from tempest.services.compute.json.migrations_client import \
+ MigrationsClientJSON
from tempest.services.compute.json.quotas_client import QuotasClientJSON
from tempest.services.compute.json.security_groups_client import \
SecurityGroupsClientJSON
@@ -78,6 +75,8 @@
InterfacesV3ClientJSON
from tempest.services.compute.v3.json.keypairs_client import \
KeyPairsV3ClientJSON
+from tempest.services.compute.v3.json.migration_client import \
+ MigrationsV3ClientJSON
from tempest.services.compute.v3.json.quotas_client import \
QuotasV3ClientJSON
from tempest.services.compute.v3.json.servers_client import \
@@ -325,6 +324,8 @@
self.tenant_usages_client = TenantUsagesClientJSON(
self.auth_provider)
self.version_v3_client = VersionV3ClientJSON(self.auth_provider)
+ self.migrations_v3_client = MigrationsV3ClientJSON(
+ self.auth_provider)
self.policy_client = PolicyClientJSON(self.auth_provider)
self.hosts_client = HostsClientJSON(self.auth_provider)
self.hypervisor_v3_client = HypervisorV3ClientJSON(
@@ -366,6 +367,7 @@
# common clients
self.account_client = AccountClient(self.auth_provider)
+ self.agents_client = AgentsClientJSON(self.auth_provider)
if CONF.service_available.glance:
self.image_client = ImageClientJSON(self.auth_provider)
self.image_client_v2 = ImageClientV2JSON(self.auth_provider)
@@ -381,6 +383,7 @@
AccountClientCustomizedHeader(self.auth_provider)
self.data_processing_client = DataProcessingClient(
self.auth_provider)
+ self.migrations_client = MigrationsClientJSON(self.auth_provider)
class AltManager(Manager):
@@ -456,6 +459,8 @@
NOVACLIENT_VERSION = '2'
CINDERCLIENT_VERSION = '1'
HEATCLIENT_VERSION = '1'
+ IRONICCLIENT_VERSION = '1'
+ SAHARACLIENT_VERSION = '1.1'
def __init__(self, username, password, tenant_name):
# FIXME(andreaf) Auth provider for client_type 'official' is
@@ -465,6 +470,7 @@
# super cares for credentials validation
super(OfficialClientManager, self).__init__(
username=username, password=password, tenant_name=tenant_name)
+ self.baremetal_client = self._get_baremetal_client()
self.compute_client = self._get_compute_client(username,
password,
tenant_name)
@@ -484,11 +490,34 @@
username,
password,
tenant_name)
+ self.data_processing_client = self._get_data_processing_client(
+ username,
+ password,
+ tenant_name)
+
+ def _get_roles(self):
+ keystone_admin = self._get_identity_client(
+ CONF.identity.admin_username,
+ CONF.identity.admin_password,
+ CONF.identity.admin_tenant_name)
+
+ username = self.credentials['username']
+ tenant_name = self.credentials['tenant_name']
+ user_id = keystone_admin.users.find(name=username).id
+ tenant_id = keystone_admin.tenants.find(name=tenant_name).id
+
+ roles = keystone_admin.roles.roles_for_user(
+ user=user_id, tenant=tenant_id)
+
+ return [r.name for r in roles]
def _get_compute_client(self, username, password, tenant_name):
# Novaclient will not execute operations for anyone but the
# identified user, so a new client needs to be created for
# each user that operations need to be performed for.
+ if not CONF.service_available.nova:
+ return None
+ import novaclient.client
self._validate_credentials(username, password, tenant_name)
auth_url = CONF.identity.uri
@@ -510,6 +539,9 @@
http_log_debug=True)
def _get_image_client(self):
+ if not CONF.service_available.glance:
+ return None
+ import glanceclient
token = self.identity_client.auth_token
region = CONF.identity.region
endpoint_type = CONF.image.endpoint_type
@@ -521,9 +553,13 @@
insecure=dscv)
def _get_volume_client(self, username, password, tenant_name):
+ if not CONF.service_available.cinder:
+ return None
+ import cinderclient.client
auth_url = CONF.identity.uri
region = CONF.identity.region
endpoint_type = CONF.volume.endpoint_type
+ dscv = CONF.identity.disable_ssl_certificate_validation
return cinderclient.client.Client(self.CINDERCLIENT_VERSION,
username,
password,
@@ -531,9 +567,13 @@
auth_url,
region_name=region,
endpoint_type=endpoint_type,
+ insecure=dscv,
http_log_debug=True)
def _get_object_storage_client(self, username, password, tenant_name):
+ if not CONF.service_available.swift:
+ return None
+ import swiftclient
auth_url = CONF.identity.uri
# add current tenant to swift operator role group.
keystone_admin = self._get_identity_client(
@@ -563,6 +603,9 @@
def _get_orchestration_client(self, username=None, password=None,
tenant_name=None):
+ if not CONF.service_available.heat:
+ return None
+ import heatclient.client
if not username:
username = CONF.identity.admin_username
if not password:
@@ -606,6 +649,37 @@
auth_url=auth_url,
insecure=dscv)
+ def _get_baremetal_client(self):
+ # ironic client is currently intended to by used by admin users
+ if not CONF.service_available.ironic:
+ return None
+ import ironicclient.client
+ roles = self._get_roles()
+ if CONF.identity.admin_role not in roles:
+ return None
+
+ auth_url = CONF.identity.uri
+ api_version = self.IRONICCLIENT_VERSION
+ insecure = CONF.identity.disable_ssl_certificate_validation
+ service_type = CONF.baremetal.catalog_type
+ endpoint_type = CONF.baremetal.endpoint_type
+ creds = {
+ 'os_username': self.credentials['username'],
+ 'os_password': self.credentials['password'],
+ 'os_tenant_name': self.credentials['tenant_name']
+ }
+
+ try:
+ return ironicclient.client.get_client(
+ api_version=api_version,
+ os_auth_url=auth_url,
+ insecure=insecure,
+ os_service_type=service_type,
+ os_endpoint_type=endpoint_type,
+ **creds)
+ except keystoneclient.exceptions.EndpointNotFound:
+ return None
+
def _get_network_client(self):
# The intended configuration is for the network client to have
# admin privileges and indicate for whom resources are being
@@ -613,6 +687,9 @@
# preferable to authenticating as a specific user because
# working with certain resources (public routers and networks)
# often requires admin privileges anyway.
+ if not CONF.service_available.neutron:
+ return None
+ import neutronclient.v2_0.client
username = CONF.identity.admin_username
password = CONF.identity.admin_password
tenant_name = CONF.identity.admin_tenant_name
@@ -629,3 +706,25 @@
endpoint_type=endpoint_type,
auth_url=auth_url,
insecure=dscv)
+
+ def _get_data_processing_client(self, username, password, tenant_name):
+ if not CONF.service_available.sahara:
+ # Sahara isn't available
+ return None
+
+ import saharaclient.client
+
+ self._validate_credentials(username, password, tenant_name)
+
+ endpoint_type = CONF.data_processing.endpoint_type
+ catalog_type = CONF.data_processing.catalog_type
+ auth_url = CONF.identity.uri
+
+ client = saharaclient.client.Client(self.SAHARACLIENT_VERSION,
+ username, password,
+ project_name=tenant_name,
+ endpoint_type=endpoint_type,
+ service_type=catalog_type,
+ auth_url=auth_url)
+
+ return client
diff --git a/tempest/common/debug.py b/tempest/common/debug.py
index 6a496c2..228be7a 100644
--- a/tempest/common/debug.py
+++ b/tempest/common/debug.py
@@ -20,7 +20,7 @@
CONF = config.CONF
LOG = logging.getLogger(__name__)
-tables = ['filter', 'nat', 'mangle']
+TABLES = ['filter', 'nat', 'mangle']
def log_ip_ns():
@@ -28,14 +28,14 @@
return
LOG.info("Host Addr:\n" + commands.ip_addr_raw())
LOG.info("Host Route:\n" + commands.ip_route_raw())
- for table in ['filter', 'nat', 'mangle']:
+ for table in TABLES:
LOG.info('Host %s table:\n%s', table, commands.iptables_raw(table))
ns_list = commands.ip_ns_list()
LOG.info("Host ns list" + str(ns_list))
for ns in ns_list:
LOG.info("ns(%s) Addr:\n%s", ns, commands.ip_ns_addr(ns))
LOG.info("ns(%s) Route:\n%s", ns, commands.ip_ns_route(ns))
- for table in ['filter', 'nat', 'mangle']:
+ for table in TABLES:
LOG.info('ns(%s) table(%s):\n%s', ns, table,
commands.iptables_ns(ns, table))
diff --git a/tempest/common/generator/base_generator.py b/tempest/common/generator/base_generator.py
index 95d50e2..57b98f7 100644
--- a/tempest/common/generator/base_generator.py
+++ b/tempest/common/generator/base_generator.py
@@ -87,12 +87,6 @@
"additionalProperties": False,
}
- def __new__(cls, *args, **kwargs):
- if not cls._instance:
- cls._instance = super(BasicGeneratorSet, cls).__new__(cls, *args,
- **kwargs)
- return cls._instance
-
def __init__(self):
self.types_dict = {}
for m in dir(self):
@@ -129,7 +123,7 @@
raise Exception("non-integer list types not supported")
result = []
if schema_type not in self.types_dict:
- raise Exception("generator (%s) doesn't support type: %s"
+ raise TypeError("generator (%s) doesn't support type: %s"
% (self.__class__.__name__, schema_type))
for generator in self.types_dict[schema_type]:
ret = generator(schema)
diff --git a/tempest/common/glance_http.py b/tempest/common/glance_http.py
index b4ba933..9358851 100644
--- a/tempest/common/glance_http.py
+++ b/tempest/common/glance_http.py
@@ -19,6 +19,7 @@
import hashlib
import httplib
import json
+import OpenSSL
import posixpath
import re
from six import moves
@@ -27,14 +28,6 @@
import struct
import urlparse
-
-# Python 2.5 compat fix
-if not hasattr(urlparse, 'parse_qsl'):
- import cgi
- urlparse.parse_qsl = cgi.parse_qsl
-
-import OpenSSL
-
from tempest import exceptions as exc
from tempest.openstack.common import log as logging
@@ -50,7 +43,7 @@
self.auth_provider = auth_provider
self.filters = filters
self.endpoint = auth_provider.base_url(filters)
- endpoint_parts = self.parse_endpoint(self.endpoint)
+ endpoint_parts = urlparse.urlparse(self.endpoint)
self.endpoint_scheme = endpoint_parts.scheme
self.endpoint_hostname = endpoint_parts.hostname
self.endpoint_port = endpoint_parts.port
@@ -61,10 +54,6 @@
self.endpoint_scheme, **kwargs)
@staticmethod
- def parse_endpoint(endpoint):
- return urlparse.urlparse(endpoint)
-
- @staticmethod
def get_connection_class(scheme):
if scheme == 'https':
return VerifiedHTTPSConnection
@@ -107,7 +96,7 @@
conn = self.get_connection()
try:
- url_parts = self.parse_endpoint(url)
+ url_parts = urlparse.urlparse(url)
conn_url = posixpath.normpath(url_parts.path)
LOG.debug('Actual Path: {path}'.format(path=conn_url))
if kwargs['headers'].get('Transfer-Encoding') == 'chunked':
@@ -134,7 +123,6 @@
raise exc.TimeoutException(message)
body_iter = ResponseBodyIterator(resp)
-
# Read body into string if it isn't obviously image data
if resp.getheader('content-type', None) != 'application/octet-stream':
body_str = ''.join([body_chunk for body_chunk in body_iter])
@@ -178,7 +166,7 @@
resp, body_iter = self._http_request(url, method, **kwargs)
- if 'application/json' in resp.getheader('content-type', None):
+ if 'application/json' in resp.getheader('content-type', ''):
body = ''.join([chunk for chunk in body_iter])
try:
body = json.loads(body)
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index 934b861..8c07d4f 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -24,10 +24,10 @@
import jsonschema
from tempest.common import http
+from tempest.common import xml_utils as common
from tempest import config
from tempest import exceptions
from tempest.openstack.common import log as logging
-from tempest.services.compute.xml import common
CONF = config.CONF
@@ -197,26 +197,26 @@
details = pattern.format(read_code, expected_code)
raise exceptions.InvalidHttpSuccessCode(details)
- def post(self, url, body, headers=None):
- return self.request('POST', url, headers, body)
+ def post(self, url, body, headers=None, extra_headers=False):
+ return self.request('POST', url, extra_headers, headers, body)
- def get(self, url, headers=None):
- return self.request('GET', url, headers)
+ def get(self, url, headers=None, extra_headers=False):
+ return self.request('GET', url, extra_headers, headers)
- def delete(self, url, headers=None, body=None):
- return self.request('DELETE', url, headers, body)
+ def delete(self, url, headers=None, body=None, extra_headers=False):
+ return self.request('DELETE', url, extra_headers, headers, body)
- def patch(self, url, body, headers=None):
- return self.request('PATCH', url, headers, body)
+ def patch(self, url, body, headers=None, extra_headers=False):
+ return self.request('PATCH', url, extra_headers, headers, body)
- def put(self, url, body, headers=None):
- return self.request('PUT', url, headers, body)
+ def put(self, url, body, headers=None, extra_headers=False):
+ return self.request('PUT', url, extra_headers, headers, body)
- def head(self, url, headers=None):
- return self.request('HEAD', url, headers)
+ def head(self, url, headers=None, extra_headers=False):
+ return self.request('HEAD', url, extra_headers, headers)
- def copy(self, url, headers=None):
- return self.request('COPY', url, headers)
+ def copy(self, url, headers=None, extra_headers=False):
+ return self.request('COPY', url, extra_headers, headers)
def get_versions(self):
resp, body = self.get('')
@@ -281,24 +281,50 @@
return resp[i]
return ""
- def _log_request(self, method, req_url, resp, secs=""):
+ def _log_request(self, method, req_url, resp,
+ secs="", req_headers=None,
+ req_body=None, resp_body=None):
# if we have the request id, put it in the right part of the log
extra = dict(request_id=self._get_request_id(resp))
# NOTE(sdague): while we still have 6 callers to this function
# we're going to just provide work around on who is actually
# providing timings by gracefully adding no content if they don't.
# Once we're down to 1 caller, clean this up.
+ caller_name = self._find_caller()
if secs:
secs = " %.3fs" % secs
self.LOG.info(
'Request (%s): %s %s %s%s' % (
- self._find_caller(),
+ caller_name,
resp['status'],
method,
req_url,
secs),
extra=extra)
+ # We intentionally duplicate the info content because in a parallel
+ # world this is important to match
+ trace_regex = CONF.debug.trace_requests
+ if trace_regex and re.search(trace_regex, caller_name):
+ log_fmt = """Request (%s): %s %s %s%s
+ Request - Headers: %s
+ Body: %s
+ Response - Headers: %s
+ Body: %s"""
+
+ self.LOG.debug(
+ log_fmt % (
+ caller_name,
+ resp['status'],
+ method,
+ req_url,
+ secs,
+ str(req_headers),
+ str(req_body)[:2048],
+ str(resp),
+ str(resp_body)[:2048]),
+ extra=extra)
+
def _parse_resp(self, body):
if self._get_type() is "json":
body = json.loads(body)
@@ -382,20 +408,32 @@
resp, resp_body = self.http_obj.request(
req_url, method, headers=req_headers, body=req_body)
end = time.time()
- self._log_request(method, req_url, resp, secs=(end - start))
+ self._log_request(method, req_url, resp, secs=(end - start),
+ req_headers=req_headers, req_body=req_body,
+ resp_body=resp_body)
+
# Verify HTTP response codes
self.response_checker(method, url, req_headers, req_body, resp,
resp_body)
return resp, resp_body
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
+ # if extra_headers is True
+ # default headers would be added to headers
retry = 0
if headers is None:
# NOTE(vponomaryov): if some client do not need headers,
# it should explicitly pass empty dict
headers = self.get_headers()
+ elif extra_headers:
+ try:
+ headers = headers.copy()
+ headers.update(self.get_headers())
+ except (ValueError, TypeError):
+ headers = self.get_headers()
resp, resp_body = self._request(method, url,
headers=headers, body=body)
@@ -452,7 +490,7 @@
raise exceptions.InvalidContentType(str(resp.status))
if resp.status == 401 or resp.status == 403:
- raise exceptions.Unauthorized()
+ raise exceptions.Unauthorized(resp_body)
if resp.status == 404:
raise exceptions.NotFound(resp_body)
@@ -549,7 +587,7 @@
# code if it exists is something that we expect. This is explicitly
# declared in the V3 API and so we should be able to export this in
# the response schema. For now we'll ignore it.
- if str(resp.status).startswith('2'):
+ if resp.status in HTTP_SUCCESS:
response_code = schema['status_code']
if resp.status not in response_code:
msg = ("The status code(%s) is different than the expected "
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index 00e5e0d..95b6833 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -112,3 +112,8 @@
def turn_nic_on(self, nic):
cmd = "sudo /bin/ip link set {nic} up".format(nic=nic)
return self.exec_command(cmd)
+
+ def get_pids(self, pr_name):
+ # Get pid(s) of a process/program
+ cmd = "ps -ef | grep %s | grep -v 'grep' | awk {'print $1'}" % pr_name
+ return self.exec_command(cmd).split('\n')
diff --git a/tempest/services/compute/xml/common.py b/tempest/common/xml_utils.py
similarity index 100%
rename from tempest/services/compute/xml/common.py
rename to tempest/common/xml_utils.py
diff --git a/tempest/config.py b/tempest/config.py
index b0945bb..b9fe572 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -126,7 +126,7 @@
"OpenStack Identity API admin credentials are known."),
cfg.StrOpt('image_ref',
default="{$IMAGE_ID}",
- help="Valid secondary image reference to be used in tests."),
+ help="Valid primary image reference to be used in tests."),
cfg.StrOpt('image_ref_alt',
default="{$IMAGE_ID_ALT}",
help="Valid secondary image reference to be used in tests."),
@@ -189,7 +189,7 @@
help="IP version used for SSH connections."),
cfg.BoolOpt('use_floatingip_for_ssh',
default=True,
- help="Dose the SSH uses Floating IP?"),
+ help="Does SSH use Floating IPs?"),
cfg.StrOpt('catalog_type',
default='compute',
help="Catalog type of the Compute service."),
@@ -441,6 +441,9 @@
cfg.StrOpt('disk_format',
default='raw',
help='Disk format to use when copying a volume to image'),
+ cfg.IntOpt('volume_size',
+ default=1,
+ help='Default size in GB for volumes created by volumes tests'),
]
volume_feature_group = cfg.OptGroup(name='volume-feature-enabled',
@@ -453,6 +456,9 @@
cfg.BoolOpt('backup',
default=True,
help='Runs Cinder volumes backup test'),
+ cfg.BoolOpt('snapshot',
+ default=True,
+ help='Runs Cinder volume snapshot test'),
cfg.ListOpt('api_extensions',
default=['all'],
help='A list of enabled volume extensions with a special '
@@ -553,7 +559,7 @@
default=1,
help="Time in seconds between build status checks."),
cfg.IntOpt('build_timeout',
- default=600,
+ default=1200,
help="Timeout in seconds to wait for a stack to build."),
cfg.StrOpt('instance_type',
default='m1.micro',
@@ -569,6 +575,9 @@
cfg.IntOpt('max_template_size',
default=524288,
help="Value must match heat configuration of the same name."),
+ cfg.IntOpt('max_resources_per_stack',
+ default=1000,
+ help="Value must match heat configuration of the same name."),
]
@@ -788,6 +797,26 @@
cfg.BoolOpt('enable',
default=True,
help="Enable diagnostic commands"),
+ cfg.StrOpt('trace_requests',
+ default='',
+ help="""A regex to determine which requests should be traced.
+
+This is a regex to match the caller for rest client requests to be able to
+selectively trace calls out of specific classes and methods. It largely
+exists for test development, and is not expected to be used in a real deploy
+of tempest. This will be matched against the discovered ClassName:method
+in the test environment.
+
+Expected values for this field are:
+
+ * ClassName:test_method_name - traces one test_method
+ * ClassName:setUp(Class) - traces specific setup functions
+ * ClassName:tearDown(Class) - traces specific teardown functions
+ * ClassName:_run_cleanups - traces the cleanup functions
+
+If nothing is specified, this feature is not enabled. To trace everything
+specify .* as the regex.
+""")
]
input_scenario_group = cfg.OptGroup(name="input-scenario",
@@ -818,13 +847,29 @@
BaremetalGroup = [
cfg.StrOpt('catalog_type',
default='baremetal',
- help="Catalog type of the baremetal provisioning service."),
+ help="Catalog type of the baremetal provisioning service"),
+ cfg.BoolOpt('driver_enabled',
+ default=False,
+ help="Whether the Ironic nova-compute driver is enabled"),
cfg.StrOpt('endpoint_type',
default='publicURL',
choices=['public', 'admin', 'internal',
'publicURL', 'adminURL', 'internalURL'],
help="The endpoint type to use for the baremetal provisioning "
- "service."),
+ "service"),
+ cfg.IntOpt('active_timeout',
+ default=300,
+ help="Timeout for Ironic node to completely provision"),
+ cfg.IntOpt('association_timeout',
+ default=10,
+ help="Timeout for association of Nova instance and Ironic "
+ "node"),
+ cfg.IntOpt('power_timeout',
+ default=20,
+ help="Timeout for Ironic power transitions."),
+ cfg.IntOpt('unprovision_timeout',
+ default=20,
+ help="Timeout for unprovisioning an Ironic node.")
]
cli_group = cfg.OptGroup(name='cli', title="cli Configuration Options")
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
new file mode 100644
index 0000000..857e1e8
--- /dev/null
+++ b/tempest/exceptions.py
@@ -0,0 +1,209 @@
+# Copyright 2012 OpenStack Foundation
+# 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 testtools
+
+
+class TempestException(Exception):
+ """
+ Base Tempest Exception
+
+ To correctly use this class, inherit from it and define
+ a 'message' property. That message will get printf'd
+ with the keyword arguments provided to the constructor.
+ """
+ message = "An unknown exception occurred"
+
+ def __init__(self, *args, **kwargs):
+ super(TempestException, self).__init__()
+ try:
+ self._error_string = self.message % kwargs
+ except Exception:
+ # at least get the core message out if something happened
+ self._error_string = self.message
+ if len(args) > 0:
+ # If there is a non-kwarg parameter, assume it's the error
+ # message or reason description and tack it on to the end
+ # of the exception message
+ # Convert all arguments into their string representations...
+ args = ["%s" % arg for arg in args]
+ self._error_string = (self._error_string +
+ "\nDetails: %s" % '\n'.join(args))
+
+ def __str__(self):
+ return self._error_string
+
+
+class RestClientException(TempestException,
+ testtools.TestCase.failureException):
+ pass
+
+
+class RFCViolation(RestClientException):
+ message = "RFC Violation"
+
+
+class InvalidConfiguration(TempestException):
+ message = "Invalid Configuration"
+
+
+class InvalidCredentials(TempestException):
+ message = "Invalid Credentials"
+
+
+class InvalidHttpSuccessCode(RestClientException):
+ message = "The success code is different than the expected one"
+
+
+class NotFound(RestClientException):
+ message = "Object not found"
+
+
+class Unauthorized(RestClientException):
+ message = 'Unauthorized'
+
+
+class InvalidServiceTag(RestClientException):
+ message = "Invalid service tag"
+
+
+class TimeoutException(TempestException):
+ message = "Request timed out"
+
+
+class BuildErrorException(TempestException):
+ message = "Server %(server_id)s failed to build and is in ERROR status"
+
+
+class ImageKilledException(TempestException):
+ message = "Image %(image_id)s 'killed' while waiting for '%(status)s'"
+
+
+class AddImageException(TempestException):
+ message = "Image %(image_id)s failed to become ACTIVE in the allotted time"
+
+
+class EC2RegisterImageException(TempestException):
+ message = ("Image %(image_id)s failed to become 'available' "
+ "in the allotted time")
+
+
+class VolumeBuildErrorException(TempestException):
+ message = "Volume %(volume_id)s failed to build and is in ERROR status"
+
+
+class SnapshotBuildErrorException(TempestException):
+ message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status"
+
+
+class VolumeBackupException(TempestException):
+ message = "Volume backup %(backup_id)s failed and is in ERROR status"
+
+
+class StackBuildErrorException(TempestException):
+ message = ("Stack %(stack_identifier)s is in %(stack_status)s status "
+ "due to '%(stack_status_reason)s'")
+
+
+class StackResourceBuildErrorException(TempestException):
+ message = ("Resource %(resource_name) in stack %(stack_identifier)s is "
+ "in %(resource_status)s status due to "
+ "'%(resource_status_reason)s'")
+
+
+class BadRequest(RestClientException):
+ message = "Bad request"
+
+
+class UnprocessableEntity(RestClientException):
+ message = "Unprocessable entity"
+
+
+class AuthenticationFailure(RestClientException):
+ message = ("Authentication with user %(user)s and password "
+ "%(password)s failed auth using tenant %(tenant)s.")
+
+
+class EndpointNotFound(TempestException):
+ message = "Endpoint not found"
+
+
+class RateLimitExceeded(TempestException):
+ message = "Rate limit exceeded"
+
+
+class OverLimit(TempestException):
+ message = "Quota exceeded"
+
+
+class ServerFault(TempestException):
+ message = "Got server fault"
+
+
+class ImageFault(TempestException):
+ message = "Got image fault"
+
+
+class IdentityError(TempestException):
+ message = "Got identity error"
+
+
+class Conflict(RestClientException):
+ message = "An object with that identifier already exists"
+
+
+class SSHTimeout(TempestException):
+ message = ("Connection to the %(host)s via SSH timed out.\n"
+ "User: %(user)s, Password: %(password)s")
+
+
+class SSHExecCommandFailed(TempestException):
+ """Raised when remotely executed command returns nonzero status."""
+ message = ("Command '%(command)s', exit status: %(exit_status)d, "
+ "Error:\n%(strerror)s")
+
+
+class ServerUnreachable(TempestException):
+ message = "The server is not reachable via the configured network"
+
+
+class TearDownException(TempestException):
+ message = "%(num)d cleanUp operation failed"
+
+
+class ResponseWithNonEmptyBody(RFCViolation):
+ message = ("RFC Violation! Response with %(status)d HTTP Status Code "
+ "MUST NOT have a body")
+
+
+class ResponseWithEntity(RFCViolation):
+ message = ("RFC Violation! Response with 205 HTTP Status Code "
+ "MUST NOT have an entity")
+
+
+class InvalidHTTPResponseBody(RestClientException):
+ message = "HTTP response body is invalid json or xml"
+
+
+class InvalidContentType(RestClientException):
+ message = "Invalid content type provided"
+
+
+class UnexpectedResponseCode(RestClientException):
+ message = "Unexpected response code received"
+
+
+class InvalidStructure(TempestException):
+ message = "Invalid structure of table with details"
diff --git a/tempest/exceptions/README.rst b/tempest/exceptions/README.rst
deleted file mode 100644
index dbe42b2..0000000
--- a/tempest/exceptions/README.rst
+++ /dev/null
@@ -1,27 +0,0 @@
-Tempest Field Guide to Exceptions
-=================================
-
-
-What are these exceptions?
---------------------------
-
-These exceptions are used by Tempest for covering OpenStack specific exceptional
-cases.
-
-How to add new exceptions?
---------------------------
-
-Each exception-template for inheritance purposes should be added into 'base'
-submodule.
-All other exceptions can be added in two ways:
-- in main module
-- in submodule
-But only in one of the ways. Need to make sure, that new exception is not
-present already.
-
-How to use exceptions?
-----------------------
-
-Any exceptions from this module or its submodules should be used in appropriate
-places to handle exceptional cases.
-Classes from 'base' module should be used only for inheritance.
diff --git a/tempest/exceptions/__init__.py b/tempest/exceptions/__init__.py
deleted file mode 100644
index d313def..0000000
--- a/tempest/exceptions/__init__.py
+++ /dev/null
@@ -1,164 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-# 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.exceptions import base
-
-
-class InvalidConfiguration(base.TempestException):
- message = "Invalid Configuration"
-
-
-class InvalidCredentials(base.TempestException):
- message = "Invalid Credentials"
-
-
-class InvalidHttpSuccessCode(base.RestClientException):
- message = "The success code is different than the expected one"
-
-
-class NotFound(base.RestClientException):
- message = "Object not found"
-
-
-class Unauthorized(base.RestClientException):
- message = 'Unauthorized'
-
-
-class InvalidServiceTag(base.RestClientException):
- message = "Invalid service tag"
-
-
-class TimeoutException(base.TempestException):
- message = "Request timed out"
-
-
-class BuildErrorException(base.TempestException):
- message = "Server %(server_id)s failed to build and is in ERROR status"
-
-
-class ImageKilledException(base.TempestException):
- message = "Image %(image_id)s 'killed' while waiting for '%(status)s'"
-
-
-class AddImageException(base.TempestException):
- message = "Image %(image_id)s failed to become ACTIVE in the allotted time"
-
-
-class EC2RegisterImageException(base.TempestException):
- message = ("Image %(image_id)s failed to become 'available' "
- "in the allotted time")
-
-
-class VolumeBuildErrorException(base.TempestException):
- message = "Volume %(volume_id)s failed to build and is in ERROR status"
-
-
-class SnapshotBuildErrorException(base.TempestException):
- message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status"
-
-
-class VolumeBackupException(base.TempestException):
- message = "Volume backup %(backup_id)s failed and is in ERROR status"
-
-
-class StackBuildErrorException(base.TempestException):
- message = ("Stack %(stack_identifier)s is in %(stack_status)s status "
- "due to '%(stack_status_reason)s'")
-
-
-class BadRequest(base.RestClientException):
- message = "Bad request"
-
-
-class UnprocessableEntity(base.RestClientException):
- message = "Unprocessable entity"
-
-
-class AuthenticationFailure(base.RestClientException):
- message = ("Authentication with user %(user)s and password "
- "%(password)s failed auth using tenant %(tenant)s.")
-
-
-class EndpointNotFound(base.TempestException):
- message = "Endpoint not found"
-
-
-class RateLimitExceeded(base.TempestException):
- message = "Rate limit exceeded"
-
-
-class OverLimit(base.TempestException):
- message = "Quota exceeded"
-
-
-class ServerFault(base.TempestException):
- message = "Got server fault"
-
-
-class ImageFault(base.TempestException):
- message = "Got image fault"
-
-
-class IdentityError(base.TempestException):
- message = "Got identity error"
-
-
-class Conflict(base.RestClientException):
- message = "An object with that identifier already exists"
-
-
-class SSHTimeout(base.TempestException):
- message = ("Connection to the %(host)s via SSH timed out.\n"
- "User: %(user)s, Password: %(password)s")
-
-
-class SSHExecCommandFailed(base.TempestException):
- """Raised when remotely executed command returns nonzero status."""
- message = ("Command '%(command)s', exit status: %(exit_status)d, "
- "Error:\n%(strerror)s")
-
-
-class ServerUnreachable(base.TempestException):
- message = "The server is not reachable via the configured network"
-
-
-class TearDownException(base.TempestException):
- message = "%(num)d cleanUp operation failed"
-
-
-class ResponseWithNonEmptyBody(base.RFCViolation):
- message = ("RFC Violation! Response with %(status)d HTTP Status Code "
- "MUST NOT have a body")
-
-
-class ResponseWithEntity(base.RFCViolation):
- message = ("RFC Violation! Response with 205 HTTP Status Code "
- "MUST NOT have an entity")
-
-
-class InvalidHTTPResponseBody(base.RestClientException):
- message = "HTTP response body is invalid json or xml"
-
-
-class InvalidContentType(base.RestClientException):
- message = "Invalid content type provided"
-
-
-class UnexpectedResponseCode(base.RestClientException):
- message = "Unexpected response code received"
-
-
-class InvalidStructure(base.TempestException):
- message = "Invalid structure of table with details"
diff --git a/tempest/exceptions/base.py b/tempest/exceptions/base.py
deleted file mode 100644
index b8e470e..0000000
--- a/tempest/exceptions/base.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-# 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 testtools
-
-
-class TempestException(Exception):
- """
- Base Tempest Exception
-
- To correctly use this class, inherit from it and define
- a 'message' property. That message will get printf'd
- with the keyword arguments provided to the constructor.
- """
- message = "An unknown exception occurred"
-
- def __init__(self, *args, **kwargs):
- super(TempestException, self).__init__()
- try:
- self._error_string = self.message % kwargs
- except Exception:
- # at least get the core message out if something happened
- self._error_string = self.message
- if len(args) > 0:
- # If there is a non-kwarg parameter, assume it's the error
- # message or reason description and tack it on to the end
- # of the exception message
- # Convert all arguments into their string representations...
- args = ["%s" % arg for arg in args]
- self._error_string = (self._error_string +
- "\nDetails: %s" % '\n'.join(args))
-
- def __str__(self):
- return self._error_string
-
-
-class RestClientException(TempestException,
- testtools.TestCase.failureException):
- pass
-
-
-class RFCViolation(RestClientException):
- message = "RFC Violation"
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index 7f39905..270851d 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -17,7 +17,7 @@
PYTHON_CLIENTS = ['cinder', 'glance', 'keystone', 'nova', 'swift', 'neutron',
'trove', 'ironic', 'savanna', 'heat', 'ceilometer',
- 'marconi']
+ 'marconi', 'sahara']
PYTHON_CLIENT_RE = re.compile('import (%s)client' % '|'.join(PYTHON_CLIENTS))
TEST_DEFINITION = re.compile(r'^\s*def test.*')
@@ -47,7 +47,7 @@
T104: Scenario tests require a services decorator
"""
- if 'tempest/scenario' in filename:
+ if 'tempest/scenario/test_' in filename:
if TEST_DEFINITION.match(physical_line):
if not SCENARIO_DECORATOR.match(previous_logical):
return (physical_line.find('def'),
diff --git a/tempest/openstack/common/__init__.py b/tempest/openstack/common/__init__.py
index e69de29..d1223ea 100644
--- a/tempest/openstack/common/__init__.py
+++ b/tempest/openstack/common/__init__.py
@@ -0,0 +1,17 @@
+#
+# 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
+
+
+six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox'))
diff --git a/tempest/openstack/common/config/generator.py b/tempest/openstack/common/config/generator.py
index eeb5a32..8156cc5 100644
--- a/tempest/openstack/common/config/generator.py
+++ b/tempest/openstack/common/config/generator.py
@@ -1,4 +1,5 @@
# Copyright 2012 SINA Corporation
+# Copyright 2014 Cisco Systems, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -18,6 +19,7 @@
from __future__ import print_function
+import argparse
import imp
import os
import re
@@ -27,6 +29,7 @@
from oslo.config import cfg
import six
+import stevedore.named
from tempest.openstack.common import gettextutils
from tempest.openstack.common import importutils
@@ -38,6 +41,7 @@
INTOPT = "IntOpt"
FLOATOPT = "FloatOpt"
LISTOPT = "ListOpt"
+DICTOPT = "DictOpt"
MULTISTROPT = "MultiStrOpt"
OPT_TYPES = {
@@ -46,11 +50,12 @@
INTOPT: 'integer value',
FLOATOPT: 'floating point value',
LISTOPT: 'list value',
+ DICTOPT: 'dict value',
MULTISTROPT: 'multi valued',
}
OPTION_REGEX = re.compile(r"(%s)" % "|".join([STROPT, BOOLOPT, INTOPT,
- FLOATOPT, LISTOPT,
+ FLOATOPT, LISTOPT, DICTOPT,
MULTISTROPT]))
PY_EXT = ".py"
@@ -59,34 +64,60 @@
WORDWRAP_WIDTH = 60
-def generate(srcfiles):
+def raise_extension_exception(extmanager, ep, err):
+ raise
+
+
+def generate(argv):
+ parser = argparse.ArgumentParser(
+ description='generate sample configuration file',
+ )
+ parser.add_argument('-m', dest='modules', action='append')
+ parser.add_argument('-l', dest='libraries', action='append')
+ parser.add_argument('srcfiles', nargs='*')
+ parsed_args = parser.parse_args(argv)
+
mods_by_pkg = dict()
- for filepath in srcfiles:
+ for filepath in parsed_args.srcfiles:
pkg_name = filepath.split(os.sep)[1]
mod_str = '.'.join(['.'.join(filepath.split(os.sep)[:-1]),
os.path.basename(filepath).split('.')[0]])
mods_by_pkg.setdefault(pkg_name, list()).append(mod_str)
# NOTE(lzyeval): place top level modules before packages
- pkg_names = filter(lambda x: x.endswith(PY_EXT), mods_by_pkg.keys())
- pkg_names.sort()
- ext_names = filter(lambda x: x not in pkg_names, mods_by_pkg.keys())
- ext_names.sort()
+ pkg_names = sorted(pkg for pkg in mods_by_pkg if pkg.endswith(PY_EXT))
+ ext_names = sorted(pkg for pkg in mods_by_pkg if pkg not in pkg_names)
pkg_names.extend(ext_names)
# opts_by_group is a mapping of group name to an options list
# The options list is a list of (module, options) tuples
opts_by_group = {'DEFAULT': []}
- extra_modules = os.getenv("TEMPEST_CONFIG_GENERATOR_EXTRA_MODULES", "")
- if extra_modules:
- for module_name in extra_modules.split(','):
- module_name = module_name.strip()
+ if parsed_args.modules:
+ for module_name in parsed_args.modules:
module = _import_module(module_name)
if module:
for group, opts in _list_opts(module):
opts_by_group.setdefault(group, []).append((module_name,
opts))
+ # Look for entry points defined in libraries (or applications) for
+ # option discovery, and include their return values in the output.
+ #
+ # Each entry point should be a function returning an iterable
+ # of pairs with the group name (or None for the default group)
+ # and the list of Opt instances for that group.
+ if parsed_args.libraries:
+ loader = stevedore.named.NamedExtensionManager(
+ 'oslo.config.opts',
+ names=list(set(parsed_args.libraries)),
+ invoke_on_load=False,
+ on_load_failure_callback=raise_extension_exception
+ )
+ for ext in loader:
+ for group, opts in ext.plugin():
+ opt_list = opts_by_group.setdefault(group or 'DEFAULT', [])
+ opt_list.append((ext.name, opts))
+
for pkg_name in pkg_names:
mods = mods_by_pkg.get(pkg_name)
mods.sort()
@@ -120,7 +151,7 @@
def _is_in_group(opt, group):
"Check if opt is in group."
- for key, value in group._opts.items():
+ for value in group._opts.values():
# NOTE(llu): Temporary workaround for bug #1262148, wait until
# newly released oslo.config support '==' operator.
if not(value['opt'] != opt):
@@ -134,7 +165,7 @@
return 'DEFAULT'
# what other groups is it in?
- for key, value in cfg.CONF.items():
+ for value in cfg.CONF.values():
if isinstance(value, cfg.CONF.GroupAttr):
if _is_in_group(opt, value._group):
return value._group.name
@@ -203,7 +234,7 @@
return value.replace(BASEDIR, '')
elif value == _get_my_ip():
return '10.0.0.1'
- elif value == socket.gethostname() and 'host' in name:
+ elif value in (socket.gethostname(), socket.getfqdn()) and 'host' in name:
return 'tempest'
elif value.strip() != value:
return '"%s"' % value
@@ -221,7 +252,8 @@
except (ValueError, AttributeError) as err:
sys.stderr.write("%s\n" % str(err))
sys.exit(1)
- opt_help += ' (' + OPT_TYPES[opt_type] + ')'
+ opt_help = u'%s (%s)' % (opt_help,
+ OPT_TYPES[opt_type])
print('#', "\n# ".join(textwrap.wrap(opt_help, WORDWRAP_WIDTH)))
if opt.deprecated_opts:
for deprecated_opt in opt.deprecated_opts:
@@ -251,6 +283,11 @@
elif opt_type == LISTOPT:
assert(isinstance(opt_default, list))
print('#%s=%s' % (opt_name, ','.join(opt_default)))
+ elif opt_type == DICTOPT:
+ assert(isinstance(opt_default, dict))
+ opt_default_strlist = [str(key) + ':' + str(value)
+ for (key, value) in opt_default.items()]
+ print('#%s=%s' % (opt_name, ','.join(opt_default_strlist)))
elif opt_type == MULTISTROPT:
assert(isinstance(opt_default, list))
if not opt_default:
diff --git a/tempest/openstack/common/gettextutils.py b/tempest/openstack/common/gettextutils.py
index 825c2e0..17f66f7 100644
--- a/tempest/openstack/common/gettextutils.py
+++ b/tempest/openstack/common/gettextutils.py
@@ -23,14 +23,11 @@
"""
import copy
+import functools
import gettext
-import logging
+import locale
+from logging import handlers
import os
-import re
-try:
- import UserString as _userString
-except ImportError:
- import collections as _userString
from babel import localedata
import six
@@ -38,6 +35,17 @@
_localedir = os.environ.get('tempest'.upper() + '_LOCALEDIR')
_t = gettext.translation('tempest', localedir=_localedir, fallback=True)
+# We use separate translation catalogs for each log level, so set up a
+# mapping between the log level name and the translator. The domain
+# for the log level is project_name + "-log-" + log_level so messages
+# for each level end up in their own catalog.
+_t_log_levels = dict(
+ (level, gettext.translation('tempest' + '-log-' + level,
+ localedir=_localedir,
+ fallback=True))
+ for level in ['info', 'warning', 'error', 'critical']
+)
+
_AVAILABLE_LANGUAGES = {}
USE_LAZY = False
@@ -56,13 +64,35 @@
def _(msg):
if USE_LAZY:
- return Message(msg, 'tempest')
+ return Message(msg, domain='tempest')
else:
if six.PY3:
return _t.gettext(msg)
return _t.ugettext(msg)
+def _log_translation(msg, level):
+ """Build a single translation of a log message
+ """
+ if USE_LAZY:
+ return Message(msg, domain='tempest' + '-log-' + level)
+ else:
+ translator = _t_log_levels[level]
+ if six.PY3:
+ return translator.gettext(msg)
+ return translator.ugettext(msg)
+
+# Translators for log levels.
+#
+# The abbreviated names are meant to reflect the usual use of a short
+# name like '_'. The "L" is for "log" and the other letter comes from
+# the level.
+_LI = functools.partial(_log_translation, level='info')
+_LW = functools.partial(_log_translation, level='warning')
+_LE = functools.partial(_log_translation, level='error')
+_LC = functools.partial(_log_translation, level='critical')
+
+
def install(domain, lazy=False):
"""Install a _() function using the given translation domain.
@@ -88,11 +118,6 @@
# messages in OpenStack. We override the standard _() function
# and % (format string) operation to build Message objects that can
# later be translated when we have more information.
- #
- # Also included below is an example LocaleHandler that translates
- # Messages to an associated locale, effectively allowing many logs,
- # each with their own locale.
-
def _lazy_gettext(msg):
"""Create and return a Message object.
@@ -103,7 +128,7 @@
Message encapsulates a string so that we can translate
it later when needed.
"""
- return Message(msg, domain)
+ return Message(msg, domain=domain)
from six import moves
moves.builtins.__dict__['_'] = _lazy_gettext
@@ -118,182 +143,144 @@
unicode=True)
-class Message(_userString.UserString, object):
- """Class used to encapsulate translatable messages."""
- def __init__(self, msg, domain):
- # _msg is the gettext msgid and should never change
- self._msg = msg
- self._left_extra_msg = ''
- self._right_extra_msg = ''
- self._locale = None
- self.params = None
- self.domain = domain
+class Message(six.text_type):
+ """A Message object is a unicode object that can be translated.
- @property
- def data(self):
- # NOTE(mrodden): this should always resolve to a unicode string
- # that best represents the state of the message currently
+ Translation of Message is done explicitly using the translate() method.
+ For all non-translation intents and purposes, a Message is simply unicode,
+ and can be treated as such.
+ """
- localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR')
- if self.locale:
- lang = gettext.translation(self.domain,
- localedir=localedir,
- languages=[self.locale],
- fallback=True)
- else:
- # use system locale for translations
- lang = gettext.translation(self.domain,
- localedir=localedir,
- fallback=True)
+ def __new__(cls, msgid, msgtext=None, params=None,
+ domain='tempest', *args):
+ """Create a new Message object.
+ In order for translation to work gettext requires a message ID, this
+ msgid will be used as the base unicode text. It is also possible
+ for the msgid and the base unicode text to be different by passing
+ the msgtext parameter.
+ """
+ # If the base msgtext is not given, we use the default translation
+ # of the msgid (which is in English) just in case the system locale is
+ # not English, so that the base text will be in that locale by default.
+ if not msgtext:
+ msgtext = Message._translate_msgid(msgid, domain)
+ # We want to initialize the parent unicode with the actual object that
+ # would have been plain unicode if 'Message' was not enabled.
+ msg = super(Message, cls).__new__(cls, msgtext)
+ msg.msgid = msgid
+ msg.domain = domain
+ msg.params = params
+ return msg
+
+ def translate(self, desired_locale=None):
+ """Translate this message to the desired locale.
+
+ :param desired_locale: The desired locale to translate the message to,
+ if no locale is provided the message will be
+ translated to the system's default locale.
+
+ :returns: the translated message in unicode
+ """
+
+ translated_message = Message._translate_msgid(self.msgid,
+ self.domain,
+ desired_locale)
+ if self.params is None:
+ # No need for more translation
+ return translated_message
+
+ # This Message object may have been formatted with one or more
+ # Message objects as substitution arguments, given either as a single
+ # argument, part of a tuple, or as one or more values in a dictionary.
+ # When translating this Message we need to translate those Messages too
+ translated_params = _translate_args(self.params, desired_locale)
+
+ translated_message = translated_message % translated_params
+
+ return translated_message
+
+ @staticmethod
+ def _translate_msgid(msgid, domain, desired_locale=None):
+ if not desired_locale:
+ system_locale = locale.getdefaultlocale()
+ # If the system locale is not available to the runtime use English
+ if not system_locale[0]:
+ desired_locale = 'en_US'
+ else:
+ desired_locale = system_locale[0]
+
+ locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
+ lang = gettext.translation(domain,
+ localedir=locale_dir,
+ languages=[desired_locale],
+ fallback=True)
if six.PY3:
- ugettext = lang.gettext
+ translator = lang.gettext
else:
- ugettext = lang.ugettext
+ translator = lang.ugettext
- full_msg = (self._left_extra_msg +
- ugettext(self._msg) +
- self._right_extra_msg)
-
- if self.params is not None:
- full_msg = full_msg % self.params
-
- return six.text_type(full_msg)
-
- @property
- def locale(self):
- return self._locale
-
- @locale.setter
- def locale(self, value):
- self._locale = value
- if not self.params:
- return
-
- # This Message object may have been constructed with one or more
- # Message objects as substitution parameters, given as a single
- # Message, or a tuple or Map containing some, so when setting the
- # locale for this Message we need to set it for those Messages too.
- if isinstance(self.params, Message):
- self.params.locale = value
- return
- if isinstance(self.params, tuple):
- for param in self.params:
- if isinstance(param, Message):
- param.locale = value
- return
- if isinstance(self.params, dict):
- for param in self.params.values():
- if isinstance(param, Message):
- param.locale = value
-
- def _save_dictionary_parameter(self, dict_param):
- full_msg = self.data
- # look for %(blah) fields in string;
- # ignore %% and deal with the
- # case where % is first character on the line
- keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg)
-
- # if we don't find any %(blah) blocks but have a %s
- if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg):
- # apparently the full dictionary is the parameter
- params = copy.deepcopy(dict_param)
- else:
- params = {}
- for key in keys:
- try:
- params[key] = copy.deepcopy(dict_param[key])
- except TypeError:
- # cast uncopyable thing to unicode string
- params[key] = six.text_type(dict_param[key])
-
- return params
-
- def _save_parameters(self, other):
- # we check for None later to see if
- # we actually have parameters to inject,
- # so encapsulate if our parameter is actually None
- if other is None:
- self.params = (other, )
- elif isinstance(other, dict):
- self.params = self._save_dictionary_parameter(other)
- else:
- # fallback to casting to unicode,
- # this will handle the problematic python code-like
- # objects that cannot be deep-copied
- try:
- self.params = copy.deepcopy(other)
- except TypeError:
- self.params = six.text_type(other)
-
- return self
-
- # overrides to be more string-like
- def __unicode__(self):
- return self.data
-
- def __str__(self):
- if six.PY3:
- return self.__unicode__()
- return self.data.encode('utf-8')
-
- def __getstate__(self):
- to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg',
- 'domain', 'params', '_locale']
- new_dict = self.__dict__.fromkeys(to_copy)
- for attr in to_copy:
- new_dict[attr] = copy.deepcopy(self.__dict__[attr])
-
- return new_dict
-
- def __setstate__(self, state):
- for (k, v) in state.items():
- setattr(self, k, v)
-
- # operator overloads
- def __add__(self, other):
- copied = copy.deepcopy(self)
- copied._right_extra_msg += other.__str__()
- return copied
-
- def __radd__(self, other):
- copied = copy.deepcopy(self)
- copied._left_extra_msg += other.__str__()
- return copied
+ translated_message = translator(msgid)
+ return translated_message
def __mod__(self, other):
- # do a format string to catch and raise
- # any possible KeyErrors from missing parameters
- self.data % other
- copied = copy.deepcopy(self)
- return copied._save_parameters(other)
+ # When we mod a Message we want the actual operation to be performed
+ # by the parent class (i.e. unicode()), the only thing we do here is
+ # save the original msgid and the parameters in case of a translation
+ params = self._sanitize_mod_params(other)
+ unicode_mod = super(Message, self).__mod__(params)
+ modded = Message(self.msgid,
+ msgtext=unicode_mod,
+ params=params,
+ domain=self.domain)
+ return modded
- def __mul__(self, other):
- return self.data * other
+ def _sanitize_mod_params(self, other):
+ """Sanitize the object being modded with this Message.
- def __rmul__(self, other):
- return other * self.data
-
- def __getitem__(self, key):
- return self.data[key]
-
- def __getslice__(self, start, end):
- return self.data.__getslice__(start, end)
-
- def __getattribute__(self, name):
- # NOTE(mrodden): handle lossy operations that we can't deal with yet
- # These override the UserString implementation, since UserString
- # uses our __class__ attribute to try and build a new message
- # after running the inner data string through the operation.
- # At that point, we have lost the gettext message id and can just
- # safely resolve to a string instead.
- ops = ['capitalize', 'center', 'decode', 'encode',
- 'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip',
- 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
- if name in ops:
- return getattr(self.data, name)
+ - Add support for modding 'None' so translation supports it
+ - Trim the modded object, which can be a large dictionary, to only
+ those keys that would actually be used in a translation
+ - Snapshot the object being modded, in case the message is
+ translated, it will be used as it was when the Message was created
+ """
+ if other is None:
+ params = (other,)
+ elif isinstance(other, dict):
+ # Merge the dictionaries
+ # Copy each item in case one does not support deep copy.
+ params = {}
+ if isinstance(self.params, dict):
+ for key, val in self.params.items():
+ params[key] = self._copy_param(val)
+ for key, val in other.items():
+ params[key] = self._copy_param(val)
else:
- return _userString.UserString.__getattribute__(self, name)
+ params = self._copy_param(other)
+ return params
+
+ def _copy_param(self, param):
+ try:
+ return copy.deepcopy(param)
+ except Exception:
+ # Fallback to casting to unicode this will handle the
+ # python code-like objects that can't be deep-copied
+ return six.text_type(param)
+
+ def __add__(self, other):
+ msg = _('Message objects do not support addition.')
+ raise TypeError(msg)
+
+ def __radd__(self, other):
+ return self.__add__(other)
+
+ def __str__(self):
+ # NOTE(luisg): Logging in python 2.6 tries to str() log records,
+ # and it expects specifically a UnicodeError in order to proceed.
+ msg = _('Message objects do not support str() because they may '
+ 'contain non-ascii characters. '
+ 'Please use unicode() or translate() instead.')
+ raise UnicodeError(msg)
def get_available_languages(domain):
@@ -319,53 +306,143 @@
list_identifiers = (getattr(localedata, 'list', None) or
getattr(localedata, 'locale_identifiers'))
locale_identifiers = list_identifiers()
+
for i in locale_identifiers:
if find(i) is not None:
language_list.append(i)
+
+ # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported
+ # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they
+ # are perfectly legitimate locales:
+ # https://github.com/mitsuhiko/babel/issues/37
+ # In Babel 1.3 they fixed the bug and they support these locales, but
+ # they are still not explicitly "listed" by locale_identifiers().
+ # That is why we add the locales here explicitly if necessary so that
+ # they are listed as supported.
+ aliases = {'zh': 'zh_CN',
+ 'zh_Hant_HK': 'zh_HK',
+ 'zh_Hant': 'zh_TW',
+ 'fil': 'tl_PH'}
+ for (locale, alias) in six.iteritems(aliases):
+ if locale in language_list and alias not in language_list:
+ language_list.append(alias)
+
_AVAILABLE_LANGUAGES[domain] = language_list
return copy.copy(language_list)
-def get_localized_message(message, user_locale):
- """Gets a localized version of the given message in the given locale.
+def translate(obj, desired_locale=None):
+ """Gets the translated unicode representation of the given object.
- If the message is not a Message object the message is returned as-is.
- If the locale is None the message is translated to the default locale.
+ If the object is not translatable it is returned as-is.
+ If the locale is None the object is translated to the system locale.
- :returns: the translated message in unicode, or the original message if
+ :param obj: the object to translate
+ :param desired_locale: the locale to translate the message to, if None the
+ default system locale will be used
+ :returns: the translated object in unicode, or the original object if
it could not be translated
"""
- translated = message
+ message = obj
+ if not isinstance(message, Message):
+ # If the object to translate is not already translatable,
+ # let's first get its unicode representation
+ message = six.text_type(obj)
if isinstance(message, Message):
- original_locale = message.locale
- message.locale = user_locale
- translated = six.text_type(message)
- message.locale = original_locale
- return translated
+ # Even after unicoding() we still need to check if we are
+ # running with translatable unicode before translating
+ return message.translate(desired_locale)
+ return obj
-class LocaleHandler(logging.Handler):
- """Handler that can have a locale associated to translate Messages.
+def _translate_args(args, desired_locale=None):
+ """Translates all the translatable elements of the given arguments object.
- A quick example of how to utilize the Message class above.
- LocaleHandler takes a locale and a target logging.Handler object
- to forward LogRecord objects to after translating the internal Message.
+ This method is used for translating the translatable values in method
+ arguments which include values of tuples or dictionaries.
+ If the object is not a tuple or a dictionary the object itself is
+ translated if it is translatable.
+
+ If the locale is None the object is translated to the system locale.
+
+ :param args: the args to translate
+ :param desired_locale: the locale to translate the args to, if None the
+ default system locale will be used
+ :returns: a new args object with the translated contents of the original
+ """
+ if isinstance(args, tuple):
+ return tuple(translate(v, desired_locale) for v in args)
+ if isinstance(args, dict):
+ translated_dict = {}
+ for (k, v) in six.iteritems(args):
+ translated_v = translate(v, desired_locale)
+ translated_dict[k] = translated_v
+ return translated_dict
+ return translate(args, desired_locale)
+
+
+class TranslationHandler(handlers.MemoryHandler):
+ """Handler that translates records before logging them.
+
+ The TranslationHandler takes a locale and a target logging.Handler object
+ to forward LogRecord objects to after translating them. This handler
+ depends on Message objects being logged, instead of regular strings.
+
+ The handler can be configured declaratively in the logging.conf as follows:
+
+ [handlers]
+ keys = translatedlog, translator
+
+ [handler_translatedlog]
+ class = handlers.WatchedFileHandler
+ args = ('/var/log/api-localized.log',)
+ formatter = context
+
+ [handler_translator]
+ class = openstack.common.log.TranslationHandler
+ target = translatedlog
+ args = ('zh_CN',)
+
+ If the specified locale is not available in the system, the handler will
+ log in the default locale.
"""
- def __init__(self, locale, target):
- """Initialize a LocaleHandler
+ def __init__(self, locale=None, target=None):
+ """Initialize a TranslationHandler
:param locale: locale to use for translating messages
:param target: logging.Handler object to forward
LogRecord objects to after translation
"""
- logging.Handler.__init__(self)
+ # NOTE(luisg): In order to allow this handler to be a wrapper for
+ # other handlers, such as a FileHandler, and still be able to
+ # configure it using logging.conf, this handler has to extend
+ # MemoryHandler because only the MemoryHandlers' logging.conf
+ # parsing is implemented such that it accepts a target handler.
+ handlers.MemoryHandler.__init__(self, capacity=0, target=target)
self.locale = locale
- self.target = target
+
+ def setFormatter(self, fmt):
+ self.target.setFormatter(fmt)
def emit(self, record):
- if isinstance(record.msg, Message):
- # set the locale and resolve to a string
- record.msg.locale = self.locale
+ # We save the message from the original record to restore it
+ # after translation, so other handlers are not affected by this
+ original_msg = record.msg
+ original_args = record.args
+
+ try:
+ self._translate_and_log_record(record)
+ finally:
+ record.msg = original_msg
+ record.args = original_args
+
+ def _translate_and_log_record(self, record):
+ record.msg = translate(record.msg, self.locale)
+
+ # In addition to translating the message, we also need to translate
+ # arguments that were passed to the log method that were not part
+ # of the main message e.g., log.info(_('Some message %s'), this_one))
+ record.args = _translate_args(record.args, self.locale)
self.target.emit(record)
diff --git a/tempest/openstack/common/importutils.py b/tempest/openstack/common/importutils.py
index 4fd9ae2..6c0d3b2 100644
--- a/tempest/openstack/common/importutils.py
+++ b/tempest/openstack/common/importutils.py
@@ -58,6 +58,13 @@
return sys.modules[import_str]
+def import_versioned_module(version, submodule=None):
+ module = 'tempest.v%s' % version
+ if submodule:
+ module = '.'.join((module, submodule))
+ return import_module(module)
+
+
def try_import(import_str, default=None):
"""Try to import a module and if it fails return default."""
try:
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index f06a850..1e7ddb1 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -71,11 +71,13 @@
username, password, tenant_name)
cls.compute_client = cls.manager.compute_client
cls.image_client = cls.manager.image_client
+ cls.baremetal_client = cls.manager.baremetal_client
cls.identity_client = cls.manager.identity_client
cls.network_client = cls.manager.network_client
cls.volume_client = cls.manager.volume_client
cls.object_storage_client = cls.manager.object_storage_client
cls.orchestration_client = cls.manager.orchestration_client
+ cls.data_processing_client = cls.manager.data_processing_client
cls.resource_keys = {}
cls.os_resources = []
@@ -283,7 +285,7 @@
return rules
def create_server(self, client=None, name=None, image=None, flavor=None,
- create_kwargs={}):
+ wait=True, create_kwargs={}):
if client is None:
client = self.compute_client
if name is None:
@@ -318,7 +320,8 @@
server = client.servers.create(name, image, flavor, **create_kwargs)
self.assertEqual(server.name, name)
self.set_resource(name, server)
- self.status_timeout(client.servers, server.id, 'ACTIVE')
+ if wait:
+ self.status_timeout(client.servers, server.id, 'ACTIVE')
# The instance retrieved on creation is missing network
# details, necessitating retrieval after it becomes active to
# ensure correct details.
@@ -439,6 +442,82 @@
LOG.debug("image:%s" % self.image)
+class BaremetalScenarioTest(OfficialClientTest):
+ @classmethod
+ def setUpClass(cls):
+ super(BaremetalScenarioTest, cls).setUpClass()
+
+ if (not CONF.service_available.ironic or
+ not CONF.baremetal.driver_enabled):
+ msg = 'Ironic not available or Ironic compute driver not enabled'
+ raise cls.skipException(msg)
+
+ # use an admin client manager for baremetal client
+ username, password, tenant = cls.admin_credentials()
+ manager = clients.OfficialClientManager(username, password, tenant)
+ cls.baremetal_client = manager.baremetal_client
+
+ # allow any issues obtaining the node list to raise early
+ cls.baremetal_client.node.list()
+
+ 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 getattr(node, 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 exceptions.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."""
+ from ironicclient import exc as ironic_exceptions
+
+ def _get_node():
+ node = None
+ try:
+ node = self.get_node(instance_id=instance_id)
+ except ironic_exceptions.HTTPNotFound:
+ 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 exceptions.TimeoutException(msg)
+
+ def get_node(self, node_id=None, instance_id=None):
+ if node_id:
+ return self.baremetal_client.node.get(node_id)
+ elif instance_id:
+ return self.baremetal_client.node.get_by_instance_uuid(instance_id)
+
+ def get_ports(self, node_id):
+ ports = []
+ for port in self.baremetal_client.node.list_ports(node_id):
+ ports.append(self.baremetal_client.port.get(port.uuid))
+ return ports
+
+
class NetworkScenarioTest(OfficialClientTest):
"""
Base class for network scenario tests
diff --git a/tempest/scenario/test_baremetal_basic_ops.py b/tempest/scenario/test_baremetal_basic_ops.py
new file mode 100644
index 0000000..c53aa83
--- /dev/null
+++ b/tempest/scenario/test_baremetal_basic_ops.py
@@ -0,0 +1,147 @@
+#
+# 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 tempest import config
+from tempest.openstack.common import log as logging
+from tempest.scenario import manager
+from tempest import test
+
+CONF = config.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+# power/provision states as of icehouse
+class PowerStates(object):
+ """Possible power states of an Ironic node."""
+ POWER_ON = 'power on'
+ POWER_OFF = 'power off'
+ REBOOT = 'rebooting'
+ SUSPEND = 'suspended'
+
+
+class ProvisionStates(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 BaremetalBasicOptsPXESSH(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 driver_info has been properly
+ updated
+ * 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
+ * Deletes instance
+ * Monitors the associated Ironic node for power and
+ expected state transitions
+ """
+ def add_keypair(self):
+ self.keypair = self.create_keypair()
+
+ def add_floating_ip(self):
+ floating_ip = self.compute_client.floating_ips.create()
+ self.instance.add_floating_ip(floating_ip)
+ return floating_ip.ip
+
+ 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 validate_driver_info(self):
+ f_id = self.instance.flavor['id']
+ flavor_extra = self.compute_client.flavors.get(f_id).get_keys()
+ driver_info = self.node.driver_info
+ self.assertEqual(driver_info['pxe_deploy_kernel'],
+ flavor_extra['baremetal:deploy_kernel_id'])
+ self.assertEqual(driver_info['pxe_deploy_ramdisk'],
+ flavor_extra['baremetal:deploy_ramdisk_id'])
+ self.assertEqual(driver_info['pxe_image_source'],
+ self.instance.image['id'])
+
+ def validate_ports(self):
+ for port in self.get_ports(self.node.uuid):
+ n_port_id = port.extra['vif_port_id']
+ n_port = self.network_client.show_port(n_port_id)['port']
+ self.assertEqual(n_port['device_id'], self.instance.id)
+ self.assertEqual(n_port['mac_address'], port.address)
+
+ def boot_instance(self):
+ create_kwargs = {
+ 'key_name': self.keypair.id
+ }
+ self.instance = self.create_server(
+ wait=False, create_kwargs=create_kwargs)
+
+ self.set_resource('instance', self.instance)
+
+ self.wait_node(self.instance.id)
+ self.node = self.get_node(instance_id=self.instance.id)
+
+ self.wait_power_state(self.node.uuid, PowerStates.POWER_ON)
+
+ self.wait_provisioning_state(
+ self.node.uuid,
+ [ProvisionStates.DEPLOYWAIT, ProvisionStates.ACTIVE],
+ timeout=15)
+
+ self.wait_provisioning_state(self.node.uuid, ProvisionStates.ACTIVE,
+ timeout=CONF.baremetal.active_timeout)
+
+ self.status_timeout(
+ self.compute_client.servers, self.instance.id, 'ACTIVE')
+
+ self.node = self.get_node(instance_id=self.instance.id)
+ self.instance = self.compute_client.servers.get(self.instance.id)
+
+ def terminate_instance(self):
+ self.instance.delete()
+ self.remove_resource('instance')
+ self.wait_power_state(self.node.uuid, PowerStates.POWER_OFF)
+ self.wait_provisioning_state(
+ self.node.uuid,
+ ProvisionStates.NOSTATE,
+ timeout=CONF.baremetal.unprovision_timeout)
+
+ @test.services('baremetal', 'compute', 'image', 'network')
+ def test_baremetal_server_ops(self):
+ self.add_keypair()
+ self.boot_instance()
+ self.validate_driver_info()
+ self.validate_ports()
+ self.verify_connectivity()
+ floating_ip = self.add_floating_ip()
+ self.verify_connectivity(ip=floating_ip)
+ self.terminate_instance()
diff --git a/tempest/scenario/test_load_balancer_basic.py b/tempest/scenario/test_load_balancer_basic.py
index f7a3d6f..5f71461 100644
--- a/tempest/scenario/test_load_balancer_basic.py
+++ b/tempest/scenario/test_load_balancer_basic.py
@@ -17,7 +17,6 @@
import urllib
from tempest.api.network import common as net_common
-from tempest.common.utils import data_utils
from tempest import config
from tempest import exceptions
from tempest.scenario import manager
@@ -59,24 +58,45 @@
def setUpClass(cls):
super(TestLoadBalancerBasic, cls).setUpClass()
cls.check_preconditions()
- cls.security_groups = {}
cls.servers_keypairs = {}
cls.members = []
cls.floating_ips = {}
- cls.server_ip = None
- cls.vip_ip = None
+ cls.server_ips = {}
cls.port1 = 80
cls.port2 = 88
- def _create_security_groups(self):
- self.security_groups[self.tenant_id] =\
- self._create_security_group_neutron(tenant_id=self.tenant_id)
+ def setUp(self):
+ super(TestLoadBalancerBasic, self).setUp()
+ self.server_ips = {}
+ self._create_security_group()
- def _create_server(self):
- tenant_id = self.tenant_id
- name = data_utils.rand_name("smoke_server-")
+ def cleanup_wrapper(self, resource):
+ self.cleanup_resource(resource, self.__class__.__name__)
+
+ def _create_security_group(self):
+ self.security_group = self._create_security_group_neutron(
+ tenant_id=self.tenant_id)
+ self._create_security_group_rules_for_port(self.port1)
+ self._create_security_group_rules_for_port(self.port2)
+ self.addCleanup(self.cleanup_wrapper, self.security_group)
+
+ def _create_security_group_rules_for_port(self, port):
+ rule = {
+ 'direction': 'ingress',
+ 'protocol': 'tcp',
+ 'port_range_min': port,
+ 'port_range_max': port,
+ }
+ self._create_security_group_rule(
+ client=self.network_client,
+ secgroup=self.security_group,
+ tenant_id=self.tenant_id,
+ **rule)
+
+ def _create_server(self, name):
keypair = self.create_keypair(name='keypair-%s' % name)
- security_groups = [self.security_groups[tenant_id].name]
+ self.addCleanup(self.cleanup_wrapper, keypair)
+ security_groups = [self.security_group.name]
net = self._list_networks(tenant_id=self.tenant_id)[0]
create_kwargs = {
'nics': [
@@ -87,51 +107,106 @@
}
server = self.create_server(name=name,
create_kwargs=create_kwargs)
- self.servers_keypairs[server] = keypair
+ self.addCleanup(self.cleanup_wrapper, server)
+ self.servers_keypairs[server.id] = keypair
if (config.network.public_network_id and not
config.network.tenant_networks_reachable):
public_network_id = config.network.public_network_id
floating_ip = self._create_floating_ip(
server, public_network_id)
+ self.addCleanup(self.cleanup_wrapper, floating_ip)
self.floating_ips[floating_ip] = server
- self.server_ip = floating_ip.floating_ip_address
+ self.server_ips[server.id] = floating_ip.floating_ip_address
else:
- self.server_ip = server.networks[net['name']][0]
+ self.server_ips[server.id] = server.networks[net.name][0]
self.assertTrue(self.servers_keypairs)
return server
- def _start_servers(self, server):
+ def _create_servers(self):
+ for count in range(2):
+ self._create_server(name=("server%s" % (count + 1)))
+ self.assertEqual(len(self.servers_keypairs), 2)
+
+ def _start_servers(self):
"""
+ Start two backends
+
1. SSH to the instance
2. Start two http backends listening on ports 80 and 88 respectively
+ In case there are two instances, each backend is created on a separate
+ instance.
+
+ The backends are the inetd services. To start them we need to edit
+ /etc/inetd.conf in the following way:
+ www stream tcp nowait root /bin/sh sh /home/cirros/script_name
+
+ Where /home/cirros/script_name is a path to a script which
+ echoes the responses:
+ echo -e 'HTTP/1.0 200 OK\r\n\r\nserver_name
+
+ If we want the server to listen on port 88, then we use
+ "kerberos" instead of "www".
"""
- private_key = self.servers_keypairs[server].private_key
- ssh_client = self.get_remote_client(
- server_or_ip=self.server_ip,
- private_key=private_key).ssh_client
- start_server = "while true; do echo -e 'HTTP/1.0 200 OK\r\n\r\n" \
- "%(server)s' | sudo nc -l -p %(port)s ; done &"
- cmd = start_server % {'server': 'server1',
- 'port': self.port1}
- ssh_client.exec_command(cmd)
- cmd = start_server % {'server': 'server2',
- 'port': self.port2}
- ssh_client.exec_command(cmd)
+ for server_id, ip in self.server_ips.iteritems():
+ private_key = self.servers_keypairs[server_id].private_key
+ server_name = self.compute_client.servers.get(server_id).name
+ ssh_client = self.get_remote_client(
+ server_or_ip=ip,
+ private_key=private_key)
+ ssh_client.validate_authentication()
+ # Create service for inetd
+ create_script = """sudo sh -c "echo -e \\"echo -e 'HTTP/1.0 """ \
+ """200 OK\\\\\\r\\\\\\n\\\\\\r\\\\\\n""" \
+ """%(server)s'\\" >>/home/cirros/%(script)s\""""
- def _check_connection(self, check_ip):
- def try_connect(ip):
+ cmd = create_script % {
+ 'server': server_name,
+ 'script': 'script1'}
+ ssh_client.exec_command(cmd)
+ # Configure inetd
+ configure_inetd = """sudo sh -c "echo -e \\"%(service)s """ \
+ """stream tcp nowait root /bin/sh sh """ \
+ """/home/cirros/%(script)s\\" >> """ \
+ """/etc/inetd.conf\""""
+ # "www" stands for port 80
+ cmd = configure_inetd % {'service': 'www',
+ 'script': 'script1'}
+ ssh_client.exec_command(cmd)
+
+ if len(self.server_ips) == 1:
+ cmd = create_script % {'server': 'server2',
+ 'script': 'script2'}
+ ssh_client.exec_command(cmd)
+ # "kerberos" stands for port 88
+ cmd = configure_inetd % {'service': 'kerberos',
+ 'script': 'script2'}
+ ssh_client.exec_command(cmd)
+
+ # Get PIDs of inetd
+ pids = ssh_client.get_pids('inetd')
+ if pids != ['']:
+ # If there are any inetd processes, reload them
+ kill_cmd = "sudo kill -HUP %s" % ' '.join(pids)
+ ssh_client.exec_command(kill_cmd)
+ else:
+ # In other case start inetd
+ start_inetd = "sudo /usr/sbin/inetd /etc/inetd.conf"
+ ssh_client.exec_command(start_inetd)
+
+ def _check_connection(self, check_ip, port=80):
+ def try_connect(ip, port):
try:
- urllib.urlopen("http://{0}/".format(ip))
- return True
+ resp = urllib.urlopen("http://{0}:{1}/".format(ip, port))
+ if resp.getcode() == 200:
+ return True
+ return False
except IOError:
return False
timeout = config.compute.ping_timeout
- timer = 0
- while not try_connect(check_ip):
- time.sleep(1)
- timer += 1
- if timer >= timeout:
+ start = time.time()
+ while not try_connect(check_ip, port):
+ if (time.time() - start) > timeout:
message = "Timed out trying to connect to %s" % check_ip
raise exceptions.TimeoutException(message)
@@ -142,30 +217,37 @@
self.subnet = net_common.DeletableSubnet(client=self.network_client,
**subnet)
self.pool = super(TestLoadBalancerBasic, self)._create_pool(
- 'ROUND_ROBIN',
- 'HTTP',
- self.subnet.id)
+ lb_method='ROUND_ROBIN',
+ protocol='HTTP',
+ subnet_id=self.subnet.id)
+ self.addCleanup(self.cleanup_wrapper, self.pool)
self.assertTrue(self.pool)
- def _create_members(self, server_ids):
+ def _create_members(self):
"""
Create two members.
In case there is only one server, create both members with the same ip
but with different ports to listen on.
"""
- servers = self.compute_client.servers.list()
- for server in servers:
- if server.id in server_ids:
- ip = self.server_ip
- pool_id = self.pool.id
- if len(set(server_ids)) == 1 or len(servers) == 1:
- member1 = self._create_member(ip, self.port1, pool_id)
- member2 = self._create_member(ip, self.port2, pool_id)
- self.members.extend([member1, member2])
- else:
- member = self._create_member(ip, self.port1, pool_id)
- self.members.append(member)
+
+ for server_id, ip in self.server_ips.iteritems():
+ if len(self.server_ips) == 1:
+ member1 = self._create_member(address=ip,
+ protocol_port=self.port1,
+ pool_id=self.pool.id)
+ self.addCleanup(self.cleanup_wrapper, member1)
+ member2 = self._create_member(address=ip,
+ protocol_port=self.port2,
+ pool_id=self.pool.id)
+ self.addCleanup(self.cleanup_wrapper, member2)
+ self.members.extend([member1, member2])
+ else:
+ member = self._create_member(address=ip,
+ protocol_port=self.port1,
+ pool_id=self.pool.id)
+ self.addCleanup(self.cleanup_wrapper, member)
+ self.members.append(member)
self.assertTrue(self.members)
def _assign_floating_ip_to_vip(self, vip):
@@ -173,22 +255,23 @@
port_id = vip.port_id
floating_ip = self._create_floating_ip(vip, public_network_id,
port_id=port_id)
+ self.addCleanup(self.cleanup_wrapper, floating_ip)
self.floating_ips.setdefault(vip.id, [])
self.floating_ips[vip.id].append(floating_ip)
def _create_load_balancer(self):
self._create_pool()
- self._create_members([self.servers_keypairs.keys()[0].id])
- subnet_id = self.subnet.id
- pool_id = self.pool.id
- self.vip = super(TestLoadBalancerBasic, self)._create_vip('HTTP', 80,
- subnet_id,
- pool_id)
- self._status_timeout(NeutronRetriever(self.network_client,
- self.network_client.vip_path,
- net_common.DeletableVip),
- self.vip.id,
- expected_status='ACTIVE')
+ self._create_members()
+ self.vip = self._create_vip(protocol='HTTP',
+ protocol_port=80,
+ subnet_id=self.subnet.id,
+ pool_id=self.pool.id)
+ self.addCleanup(self.cleanup_wrapper, self.vip)
+ self.status_timeout(NeutronRetriever(self.network_client,
+ self.network_client.vip_path,
+ net_common.DeletableVip),
+ self.vip.id,
+ expected_status='ACTIVE')
if (config.network.public_network_id and not
config.network.tenant_networks_reachable):
self._assign_floating_ip_to_vip(self.vip)
@@ -199,34 +282,49 @@
def _check_load_balancing(self):
"""
- 1. Send 10 requests on the floating ip associated with the VIP
+ 1. Send 100 requests on the floating ip associated with the VIP
2. Check that the requests are shared between
the two servers and that both of them get equal portions
of the requests
"""
self._check_connection(self.vip_ip)
+ resp = self._send_requests(self.vip_ip)
+ self.assertEqual(set(["server1\n", "server2\n"]), set(resp))
+ self.assertEqual(50, resp.count("server1\n"))
+ self.assertEqual(50, resp.count("server2\n"))
+
+ def _send_requests(self, vip_ip):
resp = []
- for count in range(10):
+ for count in range(100):
resp.append(
urllib.urlopen(
- "http://{0}/".format(self.vip_ip)).read())
- self.assertEqual(set(["server1\n", "server2\n"]), set(resp))
- self.assertEqual(5, resp.count("server1\n"))
- self.assertEqual(5, resp.count("server2\n"))
+ "http://{0}/".format(vip_ip)).read())
+ return resp
- @test.skip_because(bug='1295165')
@test.attr(type='smoke')
@test.services('compute', 'network')
def test_load_balancer_basic(self):
- self._create_security_groups()
- server = self._create_server()
- self._start_servers(server)
+ self._create_server('server1')
+ self._start_servers()
self._create_load_balancer()
self._check_load_balancing()
class NeutronRetriever(object):
+ """
+ Helper class to make possible handling neutron objects returned by GET
+ requests as attribute dicts.
+
+ Whet get() method is called, the returned dictionary is wrapped into
+ a corresponding DeletableResource class which provides attribute access
+ to dictionary values.
+
+ Usage:
+ This retriever is used to allow using status_timeout from
+ tempest.manager with Neutron objects.
+ """
+
def __init__(self, network_client, path, resource):
self.network_client = network_client
self.path = path
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index e7e97b5..0ba65cf 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
from tempest.common import debug
from tempest.common.utils import data_utils
from tempest import config
@@ -158,6 +160,8 @@
self.server.rebuild(image_ref_alt)
self._wait_server_status_and_check_network_connectivity()
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@services('compute', 'network')
def test_server_connectivity_pause_unpause(self):
self.server.pause()
@@ -167,6 +171,8 @@
self.server.unpause()
self._wait_server_status_and_check_network_connectivity()
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@services('compute', 'network')
def test_server_connectivity_suspend_resume(self):
self.server.suspend()
@@ -176,11 +182,10 @@
self.server.resume()
self._wait_server_status_and_check_network_connectivity()
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize is not available.')
@services('compute', 'network')
def test_server_connectivity_resize(self):
- if not CONF.compute_feature_enabled.resize:
- msg = "Skipping test - resize not available on this host"
- raise self.skipException(msg)
resize_flavor = CONF.compute.flavor_ref_alt
if resize_flavor == CONF.compute.flavor_ref:
msg = "Skipping test - flavor_ref and flavor_ref_alt are identical"
diff --git a/tempest/scenario/test_server_advanced_ops.py b/tempest/scenario/test_server_advanced_ops.py
index c0eb6e7..5a1dc04 100644
--- a/tempest/scenario/test_server_advanced_ops.py
+++ b/tempest/scenario/test_server_advanced_ops.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
from tempest import config
from tempest.openstack.common import log as logging
from tempest.scenario import manager
@@ -37,16 +39,12 @@
cls.set_network_resources()
super(TestServerAdvancedOps, cls).setUpClass()
- if not CONF.compute_feature_enabled.resize:
- msg = "Skipping test - resize not available on this host"
- raise cls.skipException(msg)
-
- resize_flavor = CONF.compute.flavor_ref_alt
-
- if resize_flavor == CONF.compute.flavor_ref:
+ if CONF.compute.flavor_ref_alt == CONF.compute.flavor_ref:
msg = "Skipping test - flavor_ref and flavor_ref_alt are identical"
raise cls.skipException(msg)
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize is not available.')
@test.services('compute')
def test_resize_server_confirm(self):
# We create an instance for use in this test
@@ -65,6 +63,8 @@
self.status_timeout(
self.compute_client.servers, instance_id, 'ACTIVE')
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.services('compute')
def test_server_sequence_suspend_resume(self):
# We create an instance for use in this test
diff --git a/tempest/scenario/test_server_basic_ops.py b/tempest/scenario/test_server_basic_ops.py
index d369f12..13e00a5 100644
--- a/tempest/scenario/test_server_basic_ops.py
+++ b/tempest/scenario/test_server_basic_ops.py
@@ -14,19 +14,17 @@
# under the License.
from tempest.common.utils import data_utils
-from tempest.common.utils import test_utils
from tempest import config
from tempest.openstack.common import log as logging
from tempest.scenario import manager
+from tempest.scenario import utils as test_utils
from tempest import test
-import testscenarios
-
CONF = config.CONF
LOG = logging.getLogger(__name__)
-load_tests = testscenarios.load_tests_apply_scenarios
+load_tests = test_utils.load_tests_input_scenario_utils
class TestServerBasicOps(manager.OfficialClientTest):
@@ -43,13 +41,6 @@
* Terminate the instance
"""
- scenario_utils = test_utils.InputScenarioUtils()
- scenario_flavor = scenario_utils.scenario_flavors
- scenario_image = scenario_utils.scenario_images
-
- scenarios = testscenarios.multiply_scenarios(scenario_image,
- scenario_flavor)
-
def setUp(self):
super(TestServerBasicOps, self).setUp()
# Setup image and flavor the test instance
@@ -99,42 +90,6 @@
create_kwargs=create_kwargs)
self.set_resource('instance', instance)
- def pause_server(self):
- instance = self.get_resource('instance')
- instance_id = instance.id
- LOG.debug("Pausing instance %s. Current status: %s",
- instance_id, instance.status)
- instance.pause()
- self.status_timeout(
- self.compute_client.servers, instance_id, 'PAUSED')
-
- def unpause_server(self):
- instance = self.get_resource('instance')
- instance_id = instance.id
- LOG.debug("Unpausing instance %s. Current status: %s",
- instance_id, instance.status)
- instance.unpause()
- self.status_timeout(
- self.compute_client.servers, instance_id, 'ACTIVE')
-
- def suspend_server(self):
- instance = self.get_resource('instance')
- instance_id = instance.id
- LOG.debug("Suspending instance %s. Current status: %s",
- instance_id, instance.status)
- instance.suspend()
- self.status_timeout(self.compute_client.servers,
- instance_id, 'SUSPENDED')
-
- def resume_server(self):
- instance = self.get_resource('instance')
- instance_id = instance.id
- LOG.debug("Resuming instance %s. Current status: %s",
- instance_id, instance.status)
- instance.resume()
- self.status_timeout(
- self.compute_client.servers, instance_id, 'ACTIVE')
-
def terminate_instance(self):
instance = self.get_resource('instance')
instance.delete()
@@ -149,10 +104,11 @@
instance.add_floating_ip(floating_ip)
# Check ssh
try:
- self.get_remote_client(
+ linux_client = self.get_remote_client(
server_or_ip=floating_ip.ip,
username=self.image_utils.ssh_user(self.image_ref),
- private_key=self.keypair.private)
+ private_key=self.keypair.private_key)
+ linux_client.validate_authentication()
except Exception:
LOG.exception('ssh to server failed')
self._log_console_output()
@@ -163,9 +119,5 @@
self.add_keypair()
self.create_security_group()
self.boot_instance()
- self.pause_server()
- self.unpause_server()
- self.suspend_server()
- self.resume_server()
self.verify_ssh()
self.terminate_instance()
diff --git a/tempest/scenario/test_stamp_pattern.py b/tempest/scenario/test_stamp_pattern.py
index 128ec17..5235871 100644
--- a/tempest/scenario/test_stamp_pattern.py
+++ b/tempest/scenario/test_stamp_pattern.py
@@ -50,6 +50,13 @@
14. Check the existence of a file which created at 6. in volume2
"""
+ @classmethod
+ def setUpClass(cls):
+ super(TestStampPattern, cls).setUpClass()
+
+ if not CONF.volume_feature_enabled.snapshot:
+ raise cls.skipException("Cinder volume snapshots are disabled")
+
def _wait_for_volume_snapshot_status(self, volume_snapshot, status):
self.status_timeout(self.volume_client.volume_snapshots,
volume_snapshot.id, status)
diff --git a/tempest/scenario/test_volume_boot_pattern.py b/tempest/scenario/test_volume_boot_pattern.py
index e89ea70..faca31f 100644
--- a/tempest/scenario/test_volume_boot_pattern.py
+++ b/tempest/scenario/test_volume_boot_pattern.py
@@ -35,6 +35,12 @@
* Boot an additional instance from the new snapshot based volume
* Check written content in the instance booted from snapshot
"""
+ @classmethod
+ def setUpClass(cls):
+ super(TestVolumeBootPattern, cls).setUpClass()
+
+ if not CONF.volume_feature_enabled.snapshot:
+ raise cls.skipException("Cinder volume snapshots are disabled")
def _create_volume_from_image(self):
img_uuid = CONF.compute.image_ref
diff --git a/tempest/common/utils/test_utils.py b/tempest/scenario/utils.py
similarity index 85%
rename from tempest/common/utils/test_utils.py
rename to tempest/scenario/utils.py
index cc0d831..4c7b6d7 100644
--- a/tempest/common/utils/test_utils.py
+++ b/tempest/scenario/utils.py
@@ -12,15 +12,19 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest import clients
-from tempest.common.utils import misc
-from tempest import config
import json
import re
import string
import unicodedata
+import testscenarios
+import testtools
+
+from tempest import clients
+from tempest.common.utils import misc
+from tempest import config
+
CONF = config.CONF
@@ -79,7 +83,7 @@
class TestInputScenario(manager.OfficialClientTest):
- scenario_utils = test_utils.InputScenarioUtils()
+ scenario_utils = utils.InputScenarioUtils()
scenario_flavor = scenario_utils.scenario_flavors
scenario_image = scenario_utils.scenario_images
scenarios = testscenarios.multiply_scenarios(scenario_image,
@@ -134,3 +138,22 @@
for f in flavors if re.search(self.flavor_pattern, str(f.name))
]
return self._scenario_flavors
+
+
+def load_tests_input_scenario_utils(*args):
+ """
+ Wrapper for testscenarios to set the scenarios to avoid running a getattr
+ on the CONF object at import.
+ """
+ if getattr(args[0], 'suiteClass', None) is not None:
+ loader, standard_tests, pattern = args
+ else:
+ standard_tests, module, loader = args
+ scenario_utils = InputScenarioUtils()
+ scenario_flavor = scenario_utils.scenario_flavors
+ scenario_image = scenario_utils.scenario_images
+ for test in testtools.iterate_tests(standard_tests):
+ setattr(test, 'scenarios', testscenarios.multiply_scenarios(
+ scenario_image,
+ scenario_flavor))
+ return testscenarios.load_tests_apply_scenarios(*args)
diff --git a/tempest/services/baremetal/base.py b/tempest/services/baremetal/base.py
index 5f6b513..2af287f 100644
--- a/tempest/services/baremetal/base.py
+++ b/tempest/services/baremetal/base.py
@@ -12,6 +12,7 @@
import functools
import json
+import urllib
import six
@@ -103,16 +104,19 @@
return patch
- def _list_request(self, resource, permanent=False):
+ 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 **kw: Parameters for the request.
:return: 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)
diff --git a/tempest/services/baremetal/v1/base_v1.py b/tempest/services/baremetal/v1/base_v1.py
index 3f4c509..296a199 100644
--- a/tempest/services/baremetal/v1/base_v1.py
+++ b/tempest/services/baremetal/v1/base_v1.py
@@ -37,9 +37,24 @@
return self._list_request('chassis')
@base.handle_errors
- def list_ports(self):
+ def list_ports(self, **kwargs):
"""List all existing ports."""
- return self._list_request('ports')
+ return self._list_request('ports', **kwargs)
+
+ @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):
+ """Details list all existing ports."""
+ return self._list_request('/ports/detail')
+
+ @base.handle_errors
+ def list_drivers(self):
+ """List all existing drivers."""
+ return self._list_request('drivers')
@base.handle_errors
def show_node(self, uuid):
@@ -116,12 +131,20 @@
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.
+ :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 = {'address': kwargs.get('address', '01:23:45:67:89:0A'),
- 'node_uuid': node_id}
+ 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', port)
@@ -192,15 +215,14 @@
return self._patch_request('chassis', uuid, patch)
@base.handle_errors
- def update_port(self, uuid, **kwargs):
+ 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.
"""
- port_attributes = ('address',)
- patch = self._make_patch(port_attributes, **kwargs)
return self._patch_request('ports', uuid, patch)
diff --git a/tempest/services/compute/json/agents_client.py b/tempest/services/compute/json/agents_client.py
new file mode 100644
index 0000000..19821e7
--- /dev/null
+++ b/tempest/services/compute/json/agents_client.py
@@ -0,0 +1,58 @@
+# 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.
+
+import json
+import urllib
+
+from tempest.api_schema.compute.v2 import agents as schema
+from tempest.common import rest_client
+from tempest import config
+
+CONF = config.CONF
+
+
+class AgentsClientJSON(rest_client.RestClient):
+ """
+ Tests Agents API
+ """
+
+ def __init__(self, auth_provider):
+ super(AgentsClientJSON, self).__init__(auth_provider)
+ self.service = CONF.compute.catalog_type
+
+ def list_agents(self, params=None):
+ """List all agent builds."""
+ url = 'os-agents'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ return resp, json.loads(body).get('agents')
+
+ def create_agent(self, **kwargs):
+ """Create an agent build."""
+ post_body = json.dumps({'agent': kwargs})
+ resp, body = self.post('os-agents', post_body)
+ return resp, self._parse_resp(body)
+
+ def delete_agent(self, agent_id):
+ """Delete an existing agent build."""
+ resp, body = self.delete("os-agents/%s" % str(agent_id))
+ self.validate_response(schema.delete_agent, resp, body)
+ return resp, body
+
+ def update_agent(self, agent_id, **kwargs):
+ """Update an agent build."""
+ put_body = json.dumps({'para': kwargs})
+ resp, body = self.put('os-agents/%s' % str(agent_id), put_body)
+ return resp, self._parse_resp(body)
diff --git a/tempest/services/compute/json/aggregates_client.py b/tempest/services/compute/json/aggregates_client.py
index ccb85c4..54d1252 100644
--- a/tempest/services/compute/json/aggregates_client.py
+++ b/tempest/services/compute/json/aggregates_client.py
@@ -40,6 +40,7 @@
"""Get details of the given aggregate."""
resp, body = self.get("os-aggregates/%s" % str(aggregate_id))
body = json.loads(body)
+ self.validate_response(schema.get_aggregate, resp, body)
return resp, body['aggregate']
def create_aggregate(self, **kwargs):
@@ -60,6 +61,7 @@
resp, body = self.put('os-aggregates/%s' % str(aggregate_id), put_body)
body = json.loads(body)
+ self.validate_response(schema.update_aggregate, resp, body)
return resp, body['aggregate']
def delete_aggregate(self, aggregate_id):
@@ -104,4 +106,5 @@
resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
post_body)
body = json.loads(body)
+ self.validate_response(schema.aggregate_set_metadata, resp, body)
return resp, body['aggregate']
diff --git a/tempest/services/compute/json/availability_zone_client.py b/tempest/services/compute/json/availability_zone_client.py
index 9278d5b..1c067e8 100644
--- a/tempest/services/compute/json/availability_zone_client.py
+++ b/tempest/services/compute/json/availability_zone_client.py
@@ -15,6 +15,7 @@
import json
+from tempest.api_schema.compute.v2 import availability_zone as schema
from tempest.common import rest_client
from tempest import config
@@ -31,9 +32,12 @@
def get_availability_zone_list(self):
resp, body = self.get('os-availability-zone')
body = json.loads(body)
+ self.validate_response(schema.get_availability_zone_list, resp, body)
return resp, body['availabilityZoneInfo']
def get_availability_zone_list_detail(self):
resp, body = self.get('os-availability-zone/detail')
body = json.loads(body)
+ self.validate_response(schema.get_availability_zone_list_detail, resp,
+ body)
return resp, body['availabilityZoneInfo']
diff --git a/tempest/services/compute/json/extensions_client.py b/tempest/services/compute/json/extensions_client.py
index 5ad8b98..ed2b14d 100644
--- a/tempest/services/compute/json/extensions_client.py
+++ b/tempest/services/compute/json/extensions_client.py
@@ -15,6 +15,7 @@
import json
+from tempest.api_schema.compute.v2 import extensions as schema
from tempest.common import rest_client
from tempest import config
@@ -31,6 +32,7 @@
url = 'extensions'
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(schema.list_extensions, resp, body)
return resp, body['extensions']
def is_enabled(self, extension):
diff --git a/tempest/services/compute/json/fixed_ips_client.py b/tempest/services/compute/json/fixed_ips_client.py
index 5fdd564..f2d5cbe 100644
--- a/tempest/services/compute/json/fixed_ips_client.py
+++ b/tempest/services/compute/json/fixed_ips_client.py
@@ -39,4 +39,5 @@
"""This reserves and unreserves fixed ips."""
url = "os-fixed-ips/%s/action" % (ip)
resp, body = self.post(url, json.dumps(body))
+ self.validate_response(schema.fixed_ip_action, resp, body)
return resp, body
diff --git a/tempest/services/compute/json/flavors_client.py b/tempest/services/compute/json/flavors_client.py
index bc64117..0206b82 100644
--- a/tempest/services/compute/json/flavors_client.py
+++ b/tempest/services/compute/json/flavors_client.py
@@ -16,7 +16,9 @@
import json
import urllib
+from tempest.api_schema.compute import flavors as common_schema
from tempest.api_schema.compute import flavors_access as schema_access
+from tempest.api_schema.compute.v2 import flavors as v2schema
from tempest.common import rest_client
from tempest import config
@@ -36,6 +38,7 @@
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(common_schema.list_flavors, resp, body)
return resp, body['flavors']
def list_flavors_with_detail(self, params=None):
@@ -45,6 +48,7 @@
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(v2schema.list_flavors_details, resp, body)
return resp, body['flavors']
def get_flavor_details(self, flavor_id):
@@ -126,7 +130,8 @@
"""Gets flavor access information given the flavor id."""
resp, body = self.get('flavors/%s/os-flavor-access' % flavor_id)
body = json.loads(body)
- self.validate_response(schema_access.list_flavor_access, resp, body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
def add_flavor_access(self, flavor_id, tenant_id):
@@ -139,6 +144,8 @@
post_body = json.dumps(post_body)
resp, body = self.post('flavors/%s/action' % flavor_id, post_body)
body = json.loads(body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
def remove_flavor_access(self, flavor_id, tenant_id):
@@ -151,4 +158,6 @@
post_body = json.dumps(post_body)
resp, body = self.post('flavors/%s/action' % flavor_id, post_body)
body = json.loads(body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
diff --git a/tempest/services/compute/json/hosts_client.py b/tempest/services/compute/json/hosts_client.py
index 0130f27..e148572 100644
--- a/tempest/services/compute/json/hosts_client.py
+++ b/tempest/services/compute/json/hosts_client.py
@@ -16,6 +16,7 @@
import urllib
from tempest.api_schema.compute import hosts as schema
+from tempest.api_schema.compute.v2 import hosts as v2_schema
from tempest.common import rest_client
from tempest import config
@@ -45,6 +46,7 @@
resp, body = self.get("os-hosts/%s" % str(hostname))
body = json.loads(body)
+ self.validate_response(schema.show_host_detail, resp, body)
return resp, body['host']
def update_host(self, hostname, **kwargs):
@@ -66,6 +68,7 @@
resp, body = self.get("os-hosts/%s/startup" % str(hostname))
body = json.loads(body)
+ self.validate_response(v2_schema.startup_host, resp, body)
return resp, body['host']
def shutdown_host(self, hostname):
@@ -73,6 +76,7 @@
resp, body = self.get("os-hosts/%s/shutdown" % str(hostname))
body = json.loads(body)
+ self.validate_response(v2_schema.shutdown_host, resp, body)
return resp, body['host']
def reboot_host(self, hostname):
@@ -80,4 +84,5 @@
resp, body = self.get("os-hosts/%s/reboot" % str(hostname))
body = json.loads(body)
+ self.validate_response(v2_schema.reboot_host, resp, body)
return resp, body['host']
diff --git a/tempest/services/compute/json/hypervisor_client.py b/tempest/services/compute/json/hypervisor_client.py
index c6b13b0..30228b3 100644
--- a/tempest/services/compute/json/hypervisor_client.py
+++ b/tempest/services/compute/json/hypervisor_client.py
@@ -15,6 +15,8 @@
import json
+from tempest.api_schema.compute import hypervisors as common_schema
+from tempest.api_schema.compute.v2 import hypervisors as v2schema
from tempest.common import rest_client
from tempest import config
@@ -31,40 +33,51 @@
"""List hypervisors information."""
resp, body = self.get('os-hypervisors')
body = json.loads(body)
+ self.validate_response(common_schema.common_hypervisors_detail,
+ resp, body)
return resp, body['hypervisors']
def get_hypervisor_list_details(self):
"""Show detailed hypervisors information."""
resp, body = self.get('os-hypervisors/detail')
body = json.loads(body)
+ self.validate_response(common_schema.common_list_hypervisors_detail,
+ resp, body)
return resp, body['hypervisors']
def get_hypervisor_show_details(self, hyper_id):
"""Display the details of the specified hypervisor."""
resp, body = self.get('os-hypervisors/%s' % hyper_id)
body = json.loads(body)
+ self.validate_response(common_schema.common_show_hypervisor,
+ resp, body)
return resp, body['hypervisor']
def get_hypervisor_servers(self, hyper_name):
"""List instances belonging to the specified hypervisor."""
resp, body = self.get('os-hypervisors/%s/servers' % hyper_name)
body = json.loads(body)
+ self.validate_response(v2schema.hypervisors_servers, resp, body)
return resp, body['hypervisors']
def get_hypervisor_stats(self):
"""Get hypervisor statistics over all compute nodes."""
resp, body = self.get('os-hypervisors/statistics')
body = json.loads(body)
+ self.validate_response(common_schema.hypervisor_statistics, resp, body)
return resp, body['hypervisor_statistics']
def get_hypervisor_uptime(self, hyper_id):
"""Display the uptime of the specified hypervisor."""
resp, body = self.get('os-hypervisors/%s/uptime' % hyper_id)
body = json.loads(body)
+ self.validate_response(common_schema.hypervisor_uptime, resp, body)
return resp, body['hypervisor']
def search_hypervisor(self, hyper_name):
"""Search specified hypervisor."""
resp, body = self.get('os-hypervisors/%s/search' % hyper_name)
body = json.loads(body)
+ self.validate_response(common_schema.common_hypervisors_detail,
+ resp, body)
return resp, body['hypervisors']
diff --git a/tempest/services/compute/json/images_client.py b/tempest/services/compute/json/images_client.py
index 2f128f2..bd39a04 100644
--- a/tempest/services/compute/json/images_client.py
+++ b/tempest/services/compute/json/images_client.py
@@ -48,6 +48,7 @@
post_body = json.dumps(post_body)
resp, body = self.post('servers/%s/action' % str(server_id),
post_body)
+ self.validate_response(schema.create_image, resp, body)
return resp, body
def list_images(self, params=None):
@@ -81,7 +82,9 @@
def delete_image(self, image_id):
"""Deletes the provided image."""
- return self.delete("images/%s" % str(image_id))
+ resp, body = self.delete("images/%s" % str(image_id))
+ self.validate_response(schema.delete, resp, body)
+ return resp, body
def wait_for_image_status(self, image_id, status):
"""Waits for an image to reach a given status."""
@@ -91,6 +94,7 @@
"""Lists all metadata items for an image."""
resp, body = self.get("images/%s/metadata" % str(image_id))
body = json.loads(body)
+ self.validate_response(schema.image_metadata, resp, body)
return resp, body['metadata']
def set_image_metadata(self, image_id, meta):
@@ -98,6 +102,7 @@
post_body = json.dumps({'metadata': meta})
resp, body = self.put('images/%s/metadata' % str(image_id), post_body)
body = json.loads(body)
+ self.validate_response(schema.image_metadata, resp, body)
return resp, body['metadata']
def update_image_metadata(self, image_id, meta):
@@ -105,12 +110,14 @@
post_body = json.dumps({'metadata': meta})
resp, body = self.post('images/%s/metadata' % str(image_id), post_body)
body = json.loads(body)
+ self.validate_response(schema.image_metadata, resp, body)
return resp, body['metadata']
def get_image_metadata_item(self, image_id, key):
"""Returns the value for a specific image metadata key."""
resp, body = self.get("images/%s/metadata/%s" % (str(image_id), key))
body = json.loads(body)
+ self.validate_response(schema.image_meta_item, resp, body)
return resp, body['meta']
def set_image_metadata_item(self, image_id, key, meta):
@@ -119,12 +126,14 @@
resp, body = self.put('images/%s/metadata/%s' % (str(image_id), key),
post_body)
body = json.loads(body)
+ self.validate_response(schema.image_meta_item, resp, body)
return resp, body['meta']
def delete_image_metadata_item(self, image_id, key):
"""Deletes a single image metadata key/value pair."""
resp, body = self.delete("images/%s/metadata/%s" %
(str(image_id), key))
+ self.validate_response(schema.delete, resp, body)
return resp, body
def is_resource_deleted(self, id):
diff --git a/tempest/services/compute/json/instance_usage_audit_log_client.py b/tempest/services/compute/json/instance_usage_audit_log_client.py
index 1f6e988..4700ca7 100644
--- a/tempest/services/compute/json/instance_usage_audit_log_client.py
+++ b/tempest/services/compute/json/instance_usage_audit_log_client.py
@@ -15,6 +15,8 @@
import json
+from tempest.api_schema.compute.v2 import instance_usage_audit_logs \
+ as schema
from tempest.common import rest_client
from tempest import config
@@ -32,10 +34,13 @@
url = 'os-instance_usage_audit_log'
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(schema.list_instance_usage_audit_log,
+ resp, body)
return resp, body["instance_usage_audit_logs"]
def get_instance_usage_audit_log(self, time_before):
url = 'os-instance_usage_audit_log/%s' % time_before
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(schema.get_instance_usage_audit_log, resp, body)
return resp, body["instance_usage_audit_log"]
diff --git a/tempest/services/compute/json/interfaces_client.py b/tempest/services/compute/json/interfaces_client.py
index 9928b94..2f165a2 100644
--- a/tempest/services/compute/json/interfaces_client.py
+++ b/tempest/services/compute/json/interfaces_client.py
@@ -16,6 +16,7 @@
import json
import time
+from tempest.api_schema.compute import interfaces as common_schema
from tempest.common import rest_client
from tempest import config
from tempest import exceptions
@@ -58,6 +59,7 @@
def delete_interface(self, server, port_id):
resp, body = self.delete('servers/%s/os-interface/%s' % (server,
port_id))
+ self.validate_response(common_schema.delete_interface, resp, body)
return resp, body
def wait_for_interface_status(self, server, port_id, status):
diff --git a/tempest/services/compute/json/keypairs_client.py b/tempest/services/compute/json/keypairs_client.py
index 71f235d..be93789 100644
--- a/tempest/services/compute/json/keypairs_client.py
+++ b/tempest/services/compute/json/keypairs_client.py
@@ -53,7 +53,10 @@
post_body = json.dumps(post_body)
resp, body = self.post("os-keypairs", body=post_body)
body = json.loads(body)
+ self.validate_response(schema.create_keypair, resp, body)
return resp, body['keypair']
def delete_keypair(self, key_name):
- return self.delete("os-keypairs/%s" % str(key_name))
+ resp, body = self.delete("os-keypairs/%s" % str(key_name))
+ self.validate_response(schema.delete_keypair, resp, body)
+ return resp, body
diff --git a/tempest/services/compute/json/migrations_client.py b/tempest/services/compute/json/migrations_client.py
new file mode 100644
index 0000000..a13349e
--- /dev/null
+++ b/tempest/services/compute/json/migrations_client.py
@@ -0,0 +1,39 @@
+# Copyright 2014 NEC Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+import urllib
+
+from tempest.common import rest_client
+from tempest import config
+
+CONF = config.CONF
+
+
+class MigrationsClientJSON(rest_client.RestClient):
+
+ def __init__(self, auth_provider):
+ super(MigrationsClientJSON, self).__init__(auth_provider)
+ self.service = CONF.compute.catalog_type
+
+ def list_migrations(self, params=None):
+ """Lists all migrations."""
+
+ url = 'os-migrations'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body['migrations']
diff --git a/tempest/services/compute/json/quotas_client.py b/tempest/services/compute/json/quotas_client.py
index 9346183..9bddf2c 100644
--- a/tempest/services/compute/json/quotas_client.py
+++ b/tempest/services/compute/json/quotas_client.py
@@ -48,8 +48,8 @@
self.validate_response(schema.quota_set, resp, body)
return resp, body['quota_set']
- def update_quota_set(self, tenant_id, force=None,
- injected_file_content_bytes=None,
+ def update_quota_set(self, tenant_id, user_id=None,
+ force=None, injected_file_content_bytes=None,
metadata_items=None, ram=None, floating_ips=None,
fixed_ips=None, key_pairs=None, instances=None,
security_group_rules=None, injected_files=None,
@@ -101,11 +101,19 @@
post_body['security_groups'] = security_groups
post_body = json.dumps({'quota_set': post_body})
- resp, body = self.put('os-quota-sets/%s' % str(tenant_id), post_body)
+
+ if user_id:
+ resp, body = self.put('os-quota-sets/%s?user_id=%s' %
+ (str(tenant_id), str(user_id)), post_body)
+ else:
+ resp, body = self.put('os-quota-sets/%s' % str(tenant_id),
+ post_body)
body = json.loads(body)
return resp, body['quota_set']
def delete_quota_set(self, tenant_id):
"""Delete the tenant's quota set."""
- return self.delete('os-quota-sets/%s' % str(tenant_id))
+ resp, body = self.delete('os-quota-sets/%s' % str(tenant_id))
+ self.validate_response(schema.delete_quota, resp, body)
+ return resp, body
diff --git a/tempest/services/compute/json/security_groups_client.py b/tempest/services/compute/json/security_groups_client.py
index 9267be7..7411fb7 100644
--- a/tempest/services/compute/json/security_groups_client.py
+++ b/tempest/services/compute/json/security_groups_client.py
@@ -111,11 +111,15 @@
url = 'os-security-group-rules'
resp, body = self.post(url, post_body)
body = json.loads(body)
+ self.validate_response(schema.create_security_group_rule, resp, body)
return resp, body['security_group_rule']
def delete_security_group_rule(self, group_rule_id):
"""Deletes the provided Security Group rule."""
- return self.delete('os-security-group-rules/%s' % str(group_rule_id))
+ resp, body = self.delete('os-security-group-rules/%s' %
+ str(group_rule_id))
+ self.validate_response(schema.delete_security_group_rule, resp, body)
+ return resp, body
def list_security_group_rules(self, security_group_id):
"""List all rules for a security group."""
diff --git a/tempest/services/compute/json/servers_client.py b/tempest/services/compute/json/servers_client.py
index d6705db..70a950a 100644
--- a/tempest/services/compute/json/servers_client.py
+++ b/tempest/services/compute/json/servers_client.py
@@ -18,6 +18,7 @@
import time
import urllib
+from tempest.api_schema.compute import servers as common_schema
from tempest.api_schema.compute.v2 import servers as schema
from tempest.common import rest_client
from tempest.common import waiters
@@ -135,7 +136,9 @@
def delete_server(self, server_id):
"""Deletes the given server."""
- return self.delete("servers/%s" % str(server_id))
+ resp, body = self.delete("servers/%s" % str(server_id))
+ self.validate_response(common_schema.delete_server, resp, body)
+ return resp, body
def list_servers(self, params=None):
"""Lists all servers for a user."""
@@ -197,12 +200,21 @@
body = json.loads(body)
return resp, body
- def action(self, server_id, action_name, response_key, **kwargs):
+ def action(self, server_id, action_name, response_key,
+ schema=None, **kwargs):
post_body = json.dumps({action_name: kwargs})
resp, body = self.post('servers/%s/action' % str(server_id),
post_body)
if response_key is not None:
- body = json.loads(body)[response_key]
+ body = json.loads(body)
+ # Check for Schema as 'None' because if we donot have any server
+ # action schema implemented yet then they can pass 'None' to skip
+ # the validation.Once all server action has their schema
+ # implemented then, this check can be removed if every actions are
+ # supposed to validate their response.
+ if schema is not None:
+ self.validate_response(schema, resp, body)
+ body = body[response_key]
return resp, body
def create_backup(self, server_id, backup_type, rotation, name):
@@ -221,6 +233,7 @@
resp, body = self.get("servers/%s/os-server-password" %
str(server_id))
body = json.loads(body)
+ self.validate_response(common_schema.get_password, resp, body)
return resp, body
def delete_password(self, server_id):
@@ -260,10 +273,6 @@
"""Reverts a server back to its original flavor."""
return self.action(server_id, 'revertResize', None, **kwargs)
- def create_image(self, server_id, name):
- """Creates an image of the given server."""
- return self.action(server_id, 'createImage', None, name=name)
-
def list_server_metadata(self, server_id):
resp, body = self.get("servers/%s/metadata" % str(server_id))
body = json.loads(body)
@@ -319,12 +328,15 @@
})
resp, body = self.post('servers/%s/os-volume_attachments' % server_id,
post_body)
+ body = json.loads(body)
+ self.validate_response(schema.attach_volume, resp, body)
return resp, body
def detach_volume(self, server_id, volume_id):
"""Detaches a volume from a server instance."""
resp, body = self.delete('servers/%s/os-volume_attachments/%s' %
(server_id, volume_id))
+ self.validate_response(schema.detach_volume, resp, body)
return resp, body
def add_security_group(self, server_id, name):
@@ -403,7 +415,9 @@
"""
resp, body = self.get('/'.join(['servers', server_id,
'os-virtual-interfaces']))
- return resp, json.loads(body)
+ body = json.loads(body)
+ self.validate_response(schema.list_virtual_interfaces, resp, body)
+ return resp, body
def rescue_server(self, server_id, **kwargs):
"""Rescue the provided server."""
@@ -451,4 +465,5 @@
def get_vnc_console(self, server_id, console_type):
"""Get URL of VNC console."""
return self.action(server_id, "os-getVNCConsole",
- "console", type=console_type)
+ "console", common_schema.get_vnc_console,
+ type=console_type)
diff --git a/tempest/services/compute/json/services_client.py b/tempest/services/compute/json/services_client.py
index 0f7d4cb..d58ca6f 100644
--- a/tempest/services/compute/json/services_client.py
+++ b/tempest/services/compute/json/services_client.py
@@ -49,6 +49,7 @@
post_body = json.dumps({'binary': binary, 'host': host_name})
resp, body = self.put('os-services/enable', post_body)
body = json.loads(body)
+ self.validate_response(schema.enable_service, resp, body)
return resp, body['service']
def disable_service(self, host_name, binary):
diff --git a/tempest/services/compute/json/volumes_extensions_client.py b/tempest/services/compute/json/volumes_extensions_client.py
index 17468eb..d1014af 100644
--- a/tempest/services/compute/json/volumes_extensions_client.py
+++ b/tempest/services/compute/json/volumes_extensions_client.py
@@ -61,7 +61,7 @@
url = "os-volumes/%s" % str(volume_id)
resp, body = self.get(url)
body = json.loads(body)
- self.validate_response(schema.get_volume, resp, body)
+ self.validate_response(schema.create_get_volume, resp, body)
return resp, body['volume']
def create_volume(self, size, **kwargs):
@@ -81,11 +81,14 @@
post_body = json.dumps({'volume': post_body})
resp, body = self.post('os-volumes', post_body)
body = json.loads(body)
+ self.validate_response(schema.create_get_volume, resp, body)
return resp, body['volume']
def delete_volume(self, volume_id):
"""Deletes the Specified Volume."""
- return self.delete("os-volumes/%s" % str(volume_id))
+ resp, body = self.delete("os-volumes/%s" % str(volume_id))
+ self.validate_response(schema.delete_volume, resp, body)
+ return resp, body
def wait_for_volume_status(self, volume_id, status):
"""Waits for a Volume to reach a given status."""
diff --git a/tempest/services/compute/v3/json/agents_client.py b/tempest/services/compute/v3/json/agents_client.py
index 6893af2..e1c286c 100644
--- a/tempest/services/compute/v3/json/agents_client.py
+++ b/tempest/services/compute/v3/json/agents_client.py
@@ -15,6 +15,7 @@
import json
import urllib
+from tempest.api_schema.compute.v3 import agents as schema
from tempest.common import rest_client
from tempest import config
@@ -43,7 +44,9 @@
def delete_agent(self, agent_id):
"""Delete an existing agent build."""
- return self.delete('os-agents/%s' % str(agent_id))
+ resp, body = self.delete("os-agents/%s" % str(agent_id))
+ self.validate_response(schema.delete_agent, resp, body)
+ return resp, body
def update_agent(self, agent_id, **kwargs):
"""Update an agent build."""
diff --git a/tempest/services/compute/v3/json/aggregates_client.py b/tempest/services/compute/v3/json/aggregates_client.py
index 7f73622..0fc6af9 100644
--- a/tempest/services/compute/v3/json/aggregates_client.py
+++ b/tempest/services/compute/v3/json/aggregates_client.py
@@ -40,6 +40,7 @@
"""Get details of the given aggregate."""
resp, body = self.get("os-aggregates/%s" % str(aggregate_id))
body = json.loads(body)
+ self.validate_response(schema.get_aggregate, resp, body)
return resp, body['aggregate']
def create_aggregate(self, **kwargs):
@@ -60,6 +61,7 @@
resp, body = self.put('os-aggregates/%s' % str(aggregate_id), put_body)
body = json.loads(body)
+ self.validate_response(schema.update_aggregate, resp, body)
return resp, body['aggregate']
def delete_aggregate(self, aggregate_id):
@@ -104,4 +106,5 @@
resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
post_body)
body = json.loads(body)
+ self.validate_response(schema.aggregate_set_metadata, resp, body)
return resp, body['aggregate']
diff --git a/tempest/services/compute/v3/json/availability_zone_client.py b/tempest/services/compute/v3/json/availability_zone_client.py
index bad2de9..bf74e68 100644
--- a/tempest/services/compute/v3/json/availability_zone_client.py
+++ b/tempest/services/compute/v3/json/availability_zone_client.py
@@ -15,6 +15,7 @@
import json
+from tempest.api_schema.compute.v3 import availability_zone as schema
from tempest.common import rest_client
from tempest import config
@@ -31,9 +32,12 @@
def get_availability_zone_list(self):
resp, body = self.get('os-availability-zone')
body = json.loads(body)
+ self.validate_response(schema.get_availability_zone_list, resp, body)
return resp, body['availability_zone_info']
def get_availability_zone_list_detail(self):
resp, body = self.get('os-availability-zone/detail')
body = json.loads(body)
+ self.validate_response(schema.get_availability_zone_list_detail, resp,
+ body)
return resp, body['availability_zone_info']
diff --git a/tempest/services/compute/v3/json/extensions_client.py b/tempest/services/compute/v3/json/extensions_client.py
index 46f17a4..13292db 100644
--- a/tempest/services/compute/v3/json/extensions_client.py
+++ b/tempest/services/compute/v3/json/extensions_client.py
@@ -15,6 +15,7 @@
import json
+from tempest.api_schema.compute.v3 import extensions as schema
from tempest.common import rest_client
from tempest import config
@@ -31,6 +32,7 @@
url = 'extensions'
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(schema.list_extensions, resp, body)
return resp, body['extensions']
def is_enabled(self, extension):
diff --git a/tempest/services/compute/v3/json/flavors_client.py b/tempest/services/compute/v3/json/flavors_client.py
index 655e279..189fe3f 100644
--- a/tempest/services/compute/v3/json/flavors_client.py
+++ b/tempest/services/compute/v3/json/flavors_client.py
@@ -16,7 +16,9 @@
import json
import urllib
+from tempest.api_schema.compute import flavors as common_schema
from tempest.api_schema.compute import flavors_access as schema_access
+from tempest.api_schema.compute.v3 import flavors as v3schema
from tempest.common import rest_client
from tempest import config
@@ -36,6 +38,7 @@
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(common_schema.list_flavors, resp, body)
return resp, body['flavors']
def list_flavors_with_detail(self, params=None):
@@ -45,6 +48,7 @@
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(v3schema.list_flavors_details, resp, body)
return resp, body['flavors']
def get_flavor_details(self, flavor_id):
@@ -126,7 +130,8 @@
"""Gets flavor access information given the flavor id."""
resp, body = self.get('flavors/%s/flavor-access' % flavor_id)
body = json.loads(body)
- self.validate_response(schema_access.list_flavor_access, resp, body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
def add_flavor_access(self, flavor_id, tenant_id):
@@ -139,6 +144,8 @@
post_body = json.dumps(post_body)
resp, body = self.post('flavors/%s/action' % flavor_id, post_body)
body = json.loads(body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
def remove_flavor_access(self, flavor_id, tenant_id):
@@ -151,4 +158,6 @@
post_body = json.dumps(post_body)
resp, body = self.post('flavors/%s/action' % flavor_id, post_body)
body = json.loads(body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
diff --git a/tempest/services/compute/v3/json/hosts_client.py b/tempest/services/compute/v3/json/hosts_client.py
index bcb9d36..24d43d0 100644
--- a/tempest/services/compute/v3/json/hosts_client.py
+++ b/tempest/services/compute/v3/json/hosts_client.py
@@ -16,6 +16,7 @@
import urllib
from tempest.api_schema.compute import hosts as schema
+from tempest.api_schema.compute.v3 import hosts as v3_schema
from tempest.common import rest_client
from tempest import config
@@ -45,6 +46,7 @@
resp, body = self.get("os-hosts/%s" % str(hostname))
body = json.loads(body)
+ self.validate_response(schema.show_host_detail, resp, body)
return resp, body['host']
def update_host(self, hostname, **kwargs):
@@ -66,6 +68,7 @@
resp, body = self.get("os-hosts/%s/startup" % str(hostname))
body = json.loads(body)
+ self.validate_response(v3_schema.startup_host, resp, body)
return resp, body['host']
def shutdown_host(self, hostname):
@@ -73,6 +76,7 @@
resp, body = self.get("os-hosts/%s/shutdown" % str(hostname))
body = json.loads(body)
+ self.validate_response(v3_schema.shutdown_host, resp, body)
return resp, body['host']
def reboot_host(self, hostname):
@@ -80,4 +84,5 @@
resp, body = self.get("os-hosts/%s/reboot" % str(hostname))
body = json.loads(body)
+ self.validate_response(v3_schema.reboot_host, resp, body)
return resp, body['host']
diff --git a/tempest/services/compute/v3/json/hypervisor_client.py b/tempest/services/compute/v3/json/hypervisor_client.py
index 30e391f..51468c9 100644
--- a/tempest/services/compute/v3/json/hypervisor_client.py
+++ b/tempest/services/compute/v3/json/hypervisor_client.py
@@ -15,6 +15,8 @@
import json
+from tempest.api_schema.compute import hypervisors as common_schema
+from tempest.api_schema.compute.v3 import hypervisors as v3schema
from tempest.common import rest_client
from tempest import config
@@ -31,40 +33,49 @@
"""List hypervisors information."""
resp, body = self.get('os-hypervisors')
body = json.loads(body)
+ self.validate_response(common_schema.common_hypervisors_detail,
+ resp, body)
return resp, body['hypervisors']
def get_hypervisor_list_details(self):
"""Show detailed hypervisors information."""
resp, body = self.get('os-hypervisors/detail')
body = json.loads(body)
+ self.validate_response(v3schema.list_hypervisors_detail, resp, body)
return resp, body['hypervisors']
def get_hypervisor_show_details(self, hyper_id):
"""Display the details of the specified hypervisor."""
resp, body = self.get('os-hypervisors/%s' % hyper_id)
body = json.loads(body)
+ self.validate_response(v3schema.show_hypervisor, resp, body)
return resp, body['hypervisor']
def get_hypervisor_servers(self, hyper_name):
"""List instances belonging to the specified hypervisor."""
resp, body = self.get('os-hypervisors/%s/servers' % hyper_name)
body = json.loads(body)
+ self.validate_response(v3schema.hypervisors_servers, resp, body)
return resp, body['hypervisor']
def get_hypervisor_stats(self):
"""Get hypervisor statistics over all compute nodes."""
resp, body = self.get('os-hypervisors/statistics')
body = json.loads(body)
+ self.validate_response(common_schema.hypervisor_statistics, resp, body)
return resp, body['hypervisor_statistics']
def get_hypervisor_uptime(self, hyper_id):
"""Display the uptime of the specified hypervisor."""
resp, body = self.get('os-hypervisors/%s/uptime' % hyper_id)
body = json.loads(body)
+ self.validate_response(common_schema.hypervisor_uptime, resp, body)
return resp, body['hypervisor']
def search_hypervisor(self, hyper_name):
"""Search specified hypervisor."""
resp, body = self.get('os-hypervisors/search?query=%s' % hyper_name)
body = json.loads(body)
+ self.validate_response(common_schema.common_hypervisors_detail,
+ resp, body)
return resp, body['hypervisors']
diff --git a/tempest/services/compute/v3/json/interfaces_client.py b/tempest/services/compute/v3/json/interfaces_client.py
index b45426c..25c8db7 100644
--- a/tempest/services/compute/v3/json/interfaces_client.py
+++ b/tempest/services/compute/v3/json/interfaces_client.py
@@ -16,6 +16,7 @@
import json
import time
+from tempest.api_schema.compute import interfaces as common_schema
from tempest.common import rest_client
from tempest import config
from tempest import exceptions
@@ -59,6 +60,7 @@
resp, body =\
self.delete('servers/%s/os-attach-interfaces/%s' % (server,
port_id))
+ self.validate_response(common_schema.delete_interface, resp, body)
return resp, body
def wait_for_interface_status(self, server, port_id, status):
diff --git a/tempest/services/compute/v3/json/keypairs_client.py b/tempest/services/compute/v3/json/keypairs_client.py
index d315bc4..f090d7d 100644
--- a/tempest/services/compute/v3/json/keypairs_client.py
+++ b/tempest/services/compute/v3/json/keypairs_client.py
@@ -53,7 +53,10 @@
post_body = json.dumps(post_body)
resp, body = self.post("keypairs", body=post_body)
body = json.loads(body)
+ self.validate_response(schema.create_keypair, resp, body)
return resp, body['keypair']
def delete_keypair(self, key_name):
- return self.delete("keypairs/%s" % str(key_name))
+ resp, body = self.delete("keypairs/%s" % str(key_name))
+ self.validate_response(schema.delete_keypair, resp, body)
+ return resp, body
diff --git a/tempest/services/compute/v3/json/migration_client.py b/tempest/services/compute/v3/json/migration_client.py
new file mode 100644
index 0000000..efd39b7
--- /dev/null
+++ b/tempest/services/compute/v3/json/migration_client.py
@@ -0,0 +1,37 @@
+# Copyright 2014 NEC Corporation.
+#
+# 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 urllib
+
+from tempest.common import rest_client
+from tempest import config
+
+CONF = config.CONF
+
+
+class MigrationsV3ClientJSON(rest_client.RestClient):
+
+ def __init__(self, auth_provider):
+ super(MigrationsV3ClientJSON, self).__init__(auth_provider)
+ self.service = CONF.compute.catalog_v3_type
+
+ def list_migrations(self, params=None):
+ """Lists all migrations."""
+
+ url = 'os-migrations'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ return resp, self._parse_resp(body)
diff --git a/tempest/services/compute/v3/json/quotas_client.py b/tempest/services/compute/v3/json/quotas_client.py
index a8507c4..37a8906 100644
--- a/tempest/services/compute/v3/json/quotas_client.py
+++ b/tempest/services/compute/v3/json/quotas_client.py
@@ -45,6 +45,7 @@
url = 'os-quota-sets/%s/detail' % str(tenant_id)
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(schema.quota_set_detail, resp, body)
return resp, body['quota_set']
def get_default_quota_set(self, tenant_id):
@@ -56,7 +57,7 @@
self.validate_response(schema.quota_set, resp, body)
return resp, body['quota_set']
- def update_quota_set(self, tenant_id, force=None,
+ def update_quota_set(self, tenant_id, user_id=None, force=None,
metadata_items=None, ram=None, floating_ips=None,
fixed_ips=None, key_pairs=None, instances=None,
security_group_rules=None, cores=None,
@@ -97,7 +98,13 @@
post_body['security_groups'] = security_groups
post_body = json.dumps({'quota_set': post_body})
- resp, body = self.put('os-quota-sets/%s' % str(tenant_id), post_body)
+
+ if user_id:
+ resp, body = self.put('os-quota-sets/%s?user_id=%s' %
+ (str(tenant_id), str(user_id)), post_body)
+ else:
+ resp, body = self.put('os-quota-sets/%s' % str(tenant_id),
+ post_body)
body = json.loads(body)
self.validate_response(schema.quota_set, resp, body)
@@ -105,4 +112,6 @@
def delete_quota_set(self, tenant_id):
"""Delete the tenant's quota set."""
- return self.delete('os-quota-sets/%s' % str(tenant_id))
+ resp, body = self.delete('os-quota-sets/%s' % str(tenant_id))
+ self.validate_response(schema.delete_quota, resp, body)
+ return resp, body
diff --git a/tempest/services/compute/v3/json/servers_client.py b/tempest/services/compute/v3/json/servers_client.py
index 6f492d0..bbffc13 100644
--- a/tempest/services/compute/v3/json/servers_client.py
+++ b/tempest/services/compute/v3/json/servers_client.py
@@ -19,6 +19,7 @@
import time
import urllib
+from tempest.api_schema.compute import servers as common_schema
from tempest.api_schema.compute.v3 import servers as schema
from tempest.common import rest_client
from tempest.common import waiters
@@ -135,7 +136,9 @@
def delete_server(self, server_id):
"""Deletes the given server."""
- return self.delete("servers/%s" % str(server_id))
+ resp, body = self.delete("servers/%s" % str(server_id))
+ self.validate_response(common_schema.delete_server, resp, body)
+ return resp, body
def list_servers(self, params=None):
"""Lists all servers for a user."""
@@ -221,6 +224,7 @@
resp, body = self.get("servers/%s/os-server-password" %
str(server_id))
body = json.loads(body)
+ self.validate_response(common_schema.get_password, resp, body)
return resp, body
def delete_password(self, server_id):
@@ -324,12 +328,17 @@
def attach_volume(self, server_id, volume_id, device='/dev/vdz'):
"""Attaches a volume to a server instance."""
- return self.action(server_id, 'attach', None, volume_id=volume_id,
- device=device)
+ resp, body = self.action(server_id, 'attach', None,
+ volume_id=volume_id, device=device)
+ self.validate_response(schema.attach_detach_volume, resp, body)
+ return resp, body
def detach_volume(self, server_id, volume_id):
"""Detaches a volume from a server instance."""
- return self.action(server_id, 'detach', None, volume_id=volume_id)
+ resp, body = self.action(server_id, 'detach', None,
+ volume_id=volume_id)
+ self.validate_response(schema.attach_detach_volume, resp, body)
+ return resp, body
def live_migrate_server(self, server_id, dest_host, use_block_migration):
"""This should be called with administrator privileges ."""
@@ -408,19 +417,19 @@
str(server_id))
return resp, json.loads(body)
- def list_instance_actions(self, server_id):
+ def list_server_actions(self, server_id):
"""List the provided server action."""
- resp, body = self.get("servers/%s/os-instance-actions" %
+ resp, body = self.get("servers/%s/os-server-actions" %
str(server_id))
body = json.loads(body)
- return resp, body['instance_actions']
+ return resp, body['server_actions']
- def get_instance_action(self, server_id, request_id):
+ def get_server_action(self, server_id, request_id):
"""Returns the action details of the provided server."""
- resp, body = self.get("servers/%s/os-instance-actions/%s" %
+ resp, body = self.get("servers/%s/os-server-actions/%s" %
(str(server_id), str(request_id)))
body = json.loads(body)
- return resp, body['instance_action']
+ return resp, body['server_action']
def force_delete_server(self, server_id, **kwargs):
"""Force delete a server."""
@@ -440,6 +449,7 @@
resp, body = self.post('servers/%s/action' % str(server_id),
post_body)
body = json.loads(body)
+ self.validate_response(common_schema.get_vnc_console, resp, body)
return resp, body['console']
def reset_network(self, server_id, **kwargs):
diff --git a/tempest/services/compute/v3/json/services_client.py b/tempest/services/compute/v3/json/services_client.py
index 88c4d16..96ff580 100644
--- a/tempest/services/compute/v3/json/services_client.py
+++ b/tempest/services/compute/v3/json/services_client.py
@@ -54,6 +54,7 @@
})
resp, body = self.put('os-services/enable', post_body)
body = json.loads(body)
+ self.validate_response(schema.enable_service, resp, body)
return resp, body['service']
def disable_service(self, host_name, binary):
diff --git a/tempest/services/compute/v3/json/version_client.py b/tempest/services/compute/v3/json/version_client.py
index b560c58..568678d 100644
--- a/tempest/services/compute/v3/json/version_client.py
+++ b/tempest/services/compute/v3/json/version_client.py
@@ -15,6 +15,7 @@
import json
+from tempest.api_schema.compute import version as schema
from tempest.common import rest_client
from tempest import config
@@ -30,4 +31,5 @@
def get_version(self):
resp, body = self.get('')
body = json.loads(body)
+ self.validate_response(schema.version, resp, body)
return resp, body['version']
diff --git a/tempest/services/compute/xml/aggregates_client.py b/tempest/services/compute/xml/aggregates_client.py
index 5b250ee..9c2d4aa 100644
--- a/tempest/services/compute/xml/aggregates_client.py
+++ b/tempest/services/compute/xml/aggregates_client.py
@@ -16,12 +16,9 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
from tempest import exceptions
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import Text
-from tempest.services.compute.xml.common import xml_to_json
CONF = config.CONF
@@ -34,7 +31,7 @@
self.service = CONF.compute.catalog_type
def _format_aggregate(self, g):
- agg = xml_to_json(g)
+ agg = xml_utils.xml_to_json(g)
aggregate = {}
for key, value in agg.items():
if key == 'hosts':
@@ -64,21 +61,25 @@
def create_aggregate(self, name, availability_zone=None):
"""Creates a new aggregate."""
- post_body = Element("aggregate",
- name=name,
- availability_zone=availability_zone)
+ if availability_zone is not None:
+ post_body = xml_utils.Element("aggregate", name=name,
+ availability_zone=availability_zone)
+ else:
+ post_body = xml_utils.Element("aggregate", name=name)
resp, body = self.post('os-aggregates',
- str(Document(post_body)))
+ str(xml_utils.Document(post_body)))
aggregate = self._format_aggregate(etree.fromstring(body))
return resp, aggregate
def update_aggregate(self, aggregate_id, name, availability_zone=None):
"""Update a aggregate."""
- put_body = Element("aggregate",
- name=name,
- availability_zone=availability_zone)
+ if availability_zone is not None:
+ put_body = xml_utils.Element("aggregate", name=name,
+ availability_zone=availability_zone)
+ else:
+ put_body = xml_utils.Element("aggregate", name=name)
resp, body = self.put('os-aggregates/%s' % str(aggregate_id),
- str(Document(put_body)))
+ str(xml_utils.Document(put_body)))
aggregate = self._format_aggregate(etree.fromstring(body))
return resp, aggregate
@@ -95,30 +96,30 @@
def add_host(self, aggregate_id, host):
"""Adds a host to the given aggregate."""
- post_body = Element("add_host", host=host)
+ post_body = xml_utils.Element("add_host", host=host)
resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
- str(Document(post_body)))
+ str(xml_utils.Document(post_body)))
aggregate = self._format_aggregate(etree.fromstring(body))
return resp, aggregate
def remove_host(self, aggregate_id, host):
"""Removes a host from the given aggregate."""
- post_body = Element("remove_host", host=host)
+ post_body = xml_utils.Element("remove_host", host=host)
resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
- str(Document(post_body)))
+ str(xml_utils.Document(post_body)))
aggregate = self._format_aggregate(etree.fromstring(body))
return resp, aggregate
def set_metadata(self, aggregate_id, meta):
"""Replaces the aggregate's existing metadata with new metadata."""
- post_body = Element("set_metadata")
- metadata = Element("metadata")
+ post_body = xml_utils.Element("set_metadata")
+ metadata = xml_utils.Element("metadata")
post_body.append(metadata)
for k, v in meta.items():
- meta = Element(k)
- meta.append(Text(v))
+ meta = xml_utils.Element(k)
+ meta.append(xml_utils.Text(v))
metadata.append(meta)
resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
- str(Document(post_body)))
+ str(xml_utils.Document(post_body)))
aggregate = self._format_aggregate(etree.fromstring(body))
return resp, aggregate
diff --git a/tempest/services/compute/xml/availability_zone_client.py b/tempest/services/compute/xml/availability_zone_client.py
index 4d71186..38446b8 100644
--- a/tempest/services/compute/xml/availability_zone_client.py
+++ b/tempest/services/compute/xml/availability_zone_client.py
@@ -16,8 +16,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
-from tempest.services.compute.xml.common import xml_to_json
CONF = config.CONF
@@ -31,7 +31,7 @@
self.service = CONF.compute.catalog_type
def _parse_array(self, node):
- return [xml_to_json(x) for x in node]
+ return [xml_utils.xml_to_json(x) for x in node]
def get_availability_zone_list(self):
resp, body = self.get('os-availability-zone')
diff --git a/tempest/services/compute/xml/extensions_client.py b/tempest/services/compute/xml/extensions_client.py
index 3e8254c..d924dff 100644
--- a/tempest/services/compute/xml/extensions_client.py
+++ b/tempest/services/compute/xml/extensions_client.py
@@ -16,8 +16,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
-from tempest.services.compute.xml.common import xml_to_json
CONF = config.CONF
@@ -32,7 +32,7 @@
def _parse_array(self, node):
array = []
for child in node:
- array.append(xml_to_json(child))
+ array.append(xml_utils.xml_to_json(child))
return array
def list_extensions(self):
@@ -48,5 +48,5 @@
def get_extension(self, extension_alias):
resp, body = self.get('extensions/%s' % extension_alias)
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
diff --git a/tempest/services/compute/xml/fixed_ips_client.py b/tempest/services/compute/xml/fixed_ips_client.py
index 0475530..e14ced6 100644
--- a/tempest/services/compute/xml/fixed_ips_client.py
+++ b/tempest/services/compute/xml/fixed_ips_client.py
@@ -15,10 +15,8 @@
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import Text
CONF = config.CONF
@@ -43,7 +41,7 @@
# accept any action key value here to permit tests to cover cases with
# invalid actions raising badrequest.
key, value = body.popitem()
- xml_body = Element(key)
- xml_body.append(Text(value))
- resp, body = self.post(url, str(Document(xml_body)))
+ xml_body = xml_utils.Element(key)
+ xml_body.append(xml_utils.Text(value))
+ resp, body = self.post(url, str(xml_utils.Document(xml_body)))
return resp, body
diff --git a/tempest/services/compute/xml/flavors_client.py b/tempest/services/compute/xml/flavors_client.py
index 68a27c9..68ef323 100644
--- a/tempest/services/compute/xml/flavors_client.py
+++ b/tempest/services/compute/xml/flavors_client.py
@@ -18,12 +18,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import Text
-from tempest.services.compute.xml.common import xml_to_json
-from tempest.services.compute.xml.common import XMLNS_11
CONF = config.CONF
@@ -76,7 +72,7 @@
return flavor
def _parse_array(self, node):
- return [self._format_flavor(xml_to_json(x)) for x in node]
+ return [self._format_flavor(xml_utils.xml_to_json(x)) for x in node]
def _list_flavors(self, url, params):
if params:
@@ -96,19 +92,19 @@
def get_flavor_details(self, flavor_id):
resp, body = self.get("flavors/%s" % str(flavor_id))
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
flavor = self._format_flavor(body)
return resp, flavor
def create_flavor(self, name, ram, vcpus, disk, flavor_id, **kwargs):
"""Creates a new flavor or instance type."""
- flavor = Element("flavor",
- xmlns=XMLNS_11,
- ram=ram,
- vcpus=vcpus,
- disk=disk,
- id=flavor_id,
- name=name)
+ flavor = xml_utils.Element("flavor",
+ xmlns=xml_utils.XMLNS_11,
+ ram=ram,
+ vcpus=vcpus,
+ disk=disk,
+ id=flavor_id,
+ name=name)
if kwargs.get('rxtx'):
flavor.add_attr('rxtx_factor', kwargs.get('rxtx'))
if kwargs.get('swap'):
@@ -121,8 +117,8 @@
kwargs.get('is_public'))
flavor.add_attr('xmlns:OS-FLV-EXT-DATA', XMLNS_OS_FLV_EXT_DATA)
flavor.add_attr('xmlns:os-flavor-access', XMLNS_OS_FLV_ACCESS)
- resp, body = self.post('flavors', str(Document(flavor)))
- body = xml_to_json(etree.fromstring(body))
+ resp, body = self.post('flavors', str(xml_utils.Document(flavor)))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
flavor = self._format_flavor(body)
return resp, flavor
@@ -142,18 +138,18 @@
def set_flavor_extra_spec(self, flavor_id, specs):
"""Sets extra Specs to the mentioned flavor."""
- extra_specs = Element("extra_specs")
+ extra_specs = xml_utils.Element("extra_specs")
for key in specs.keys():
extra_specs.add_attr(key, specs[key])
resp, body = self.post('flavors/%s/os-extra_specs' % flavor_id,
- str(Document(extra_specs)))
- body = xml_to_json(etree.fromstring(body))
+ str(xml_utils.Document(extra_specs)))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def get_flavor_extra_spec(self, flavor_id):
"""Gets extra Specs of the mentioned flavor."""
resp, body = self.get('flavors/%s/os-extra_specs' % flavor_id)
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def get_flavor_extra_spec_with_key(self, flavor_id, key):
@@ -163,21 +159,21 @@
body = {}
element = etree.fromstring(xml_body)
key = element.get('key')
- body[key] = xml_to_json(element)
+ body[key] = xml_utils.xml_to_json(element)
return resp, body
def update_flavor_extra_spec(self, flavor_id, key, **kwargs):
"""Update extra Specs details of the mentioned flavor and key."""
- doc = Document()
+ doc = xml_utils.Document()
for (k, v) in kwargs.items():
- element = Element(k)
+ element = xml_utils.Element(k)
doc.append(element)
- value = Text(v)
+ value = xml_utils.Text(v)
element.append(value)
resp, body = self.put('flavors/%s/os-extra_specs/%s' %
(flavor_id, key), str(doc))
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, {key: body}
def unset_flavor_extra_spec(self, flavor_id, key):
@@ -186,7 +182,7 @@
key))
def _parse_array_access(self, node):
- return [xml_to_json(x) for x in node]
+ return [xml_utils.xml_to_json(x) for x in node]
def list_flavor_access(self, flavor_id):
"""Gets flavor access information given the flavor id."""
@@ -196,8 +192,8 @@
def add_flavor_access(self, flavor_id, tenant_id):
"""Add flavor access for the specified tenant."""
- doc = Document()
- server = Element("addTenantAccess")
+ doc = xml_utils.Document()
+ server = xml_utils.Element("addTenantAccess")
doc.append(server)
server.add_attr("tenant", tenant_id)
resp, body = self.post('flavors/%s/action' % str(flavor_id), str(doc))
@@ -206,8 +202,8 @@
def remove_flavor_access(self, flavor_id, tenant_id):
"""Remove flavor access from the specified tenant."""
- doc = Document()
- server = Element("removeTenantAccess")
+ doc = xml_utils.Document()
+ server = xml_utils.Element("removeTenantAccess")
doc.append(server)
server.add_attr("tenant", tenant_id)
resp, body = self.post('flavors/%s/action' % str(flavor_id), str(doc))
diff --git a/tempest/services/compute/xml/floating_ips_client.py b/tempest/services/compute/xml/floating_ips_client.py
index be54753..fa4aa07 100644
--- a/tempest/services/compute/xml/floating_ips_client.py
+++ b/tempest/services/compute/xml/floating_ips_client.py
@@ -17,12 +17,9 @@
import urllib
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
from tempest import exceptions
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import Text
-from tempest.services.compute.xml.common import xml_to_json
CONF = config.CONF
@@ -37,11 +34,11 @@
def _parse_array(self, node):
array = []
for child in node.getchildren():
- array.append(xml_to_json(child))
+ array.append(xml_utils.xml_to_json(child))
return array
def _parse_floating_ip(self, body):
- json = xml_to_json(body)
+ json = xml_utils.xml_to_json(body)
return json
def list_floating_ips(self, params=None):
@@ -67,9 +64,9 @@
"""Allocate a floating IP to the project."""
url = 'os-floating-ips'
if pool_name:
- doc = Document()
- pool = Element("pool")
- pool.append(Text(pool_name))
+ doc = xml_utils.Document()
+ pool = xml_utils.Element("pool")
+ pool.append(xml_utils.Text(pool_name))
doc.append(pool)
resp, body = self.post(url, str(doc))
else:
@@ -86,8 +83,8 @@
def associate_floating_ip_to_server(self, floating_ip, server_id):
"""Associate the provided floating IP to a specific server."""
url = "servers/%s/action" % str(server_id)
- doc = Document()
- server = Element("addFloatingIp")
+ doc = xml_utils.Document()
+ server = xml_utils.Element("addFloatingIp")
doc.append(server)
server.add_attr("address", floating_ip)
resp, body = self.post(url, str(doc))
@@ -96,8 +93,8 @@
def disassociate_floating_ip_from_server(self, floating_ip, server_id):
"""Disassociate the provided floating IP from a specific server."""
url = "servers/%s/action" % str(server_id)
- doc = Document()
- server = Element("removeFloatingIp")
+ doc = xml_utils.Document()
+ server = xml_utils.Element("removeFloatingIp")
doc.append(server)
server.add_attr("address", floating_ip)
resp, body = self.post(url, str(doc))
diff --git a/tempest/services/compute/xml/hosts_client.py b/tempest/services/compute/xml/hosts_client.py
index b74cd04..23a7dd6 100644
--- a/tempest/services/compute/xml/hosts_client.py
+++ b/tempest/services/compute/xml/hosts_client.py
@@ -16,10 +16,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import xml_to_json
CONF = config.CONF
@@ -40,7 +38,7 @@
resp, body = self.get(url)
node = etree.fromstring(body)
- body = [xml_to_json(x) for x in node.getchildren()]
+ body = [xml_utils.xml_to_json(x) for x in node.getchildren()]
return resp, body
def show_host_detail(self, hostname):
@@ -48,20 +46,20 @@
resp, body = self.get("os-hosts/%s" % str(hostname))
node = etree.fromstring(body)
- body = [xml_to_json(node)]
+ body = [xml_utils.xml_to_json(node)]
return resp, body
def update_host(self, hostname, **kwargs):
"""Update a host."""
- request_body = Element("updates")
+ request_body = xml_utils.Element("updates")
if kwargs:
for k, v in kwargs.iteritems():
- request_body.append(Element(k, v))
+ request_body.append(xml_utils.Element(k, v))
resp, body = self.put("os-hosts/%s" % str(hostname),
- str(Document(request_body)))
+ str(xml_utils.Document(request_body)))
node = etree.fromstring(body)
- body = [xml_to_json(x) for x in node.getchildren()]
+ body = [xml_utils.xml_to_json(x) for x in node.getchildren()]
return resp, body
def startup_host(self, hostname):
@@ -69,7 +67,7 @@
resp, body = self.get("os-hosts/%s/startup" % str(hostname))
node = etree.fromstring(body)
- body = [xml_to_json(x) for x in node.getchildren()]
+ body = [xml_utils.xml_to_json(x) for x in node.getchildren()]
return resp, body
def shutdown_host(self, hostname):
@@ -77,7 +75,7 @@
resp, body = self.get("os-hosts/%s/shutdown" % str(hostname))
node = etree.fromstring(body)
- body = [xml_to_json(x) for x in node.getchildren()]
+ body = [xml_utils.xml_to_json(x) for x in node.getchildren()]
return resp, body
def reboot_host(self, hostname):
@@ -85,5 +83,5 @@
resp, body = self.get("os-hosts/%s/reboot" % str(hostname))
node = etree.fromstring(body)
- body = [xml_to_json(x) for x in node.getchildren()]
+ body = [xml_utils.xml_to_json(x) for x in node.getchildren()]
return resp, body
diff --git a/tempest/services/compute/xml/hypervisor_client.py b/tempest/services/compute/xml/hypervisor_client.py
index ecd7541..1452708 100644
--- a/tempest/services/compute/xml/hypervisor_client.py
+++ b/tempest/services/compute/xml/hypervisor_client.py
@@ -16,8 +16,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
-from tempest.services.compute.xml.common import xml_to_json
CONF = config.CONF
@@ -30,7 +30,7 @@
self.service = CONF.compute.catalog_type
def _parse_array(self, node):
- return [xml_to_json(x) for x in node]
+ return [xml_utils.xml_to_json(x) for x in node]
def get_hypervisor_list(self):
"""List hypervisors information."""
@@ -47,7 +47,7 @@
def get_hypervisor_show_details(self, hyper_id):
"""Display the details of the specified hypervisor."""
resp, body = self.get('os-hypervisors/%s' % hyper_id)
- hypervisor = xml_to_json(etree.fromstring(body))
+ hypervisor = xml_utils.xml_to_json(etree.fromstring(body))
return resp, hypervisor
def get_hypervisor_servers(self, hyper_name):
@@ -59,13 +59,13 @@
def get_hypervisor_stats(self):
"""Get hypervisor statistics over all compute nodes."""
resp, body = self.get('os-hypervisors/statistics')
- stats = xml_to_json(etree.fromstring(body))
+ stats = xml_utils.xml_to_json(etree.fromstring(body))
return resp, stats
def get_hypervisor_uptime(self, hyper_id):
"""Display the uptime of the specified hypervisor."""
resp, body = self.get('os-hypervisors/%s/uptime' % hyper_id)
- uptime = xml_to_json(etree.fromstring(body))
+ uptime = xml_utils.xml_to_json(etree.fromstring(body))
return resp, uptime
def search_hypervisor(self, hyper_name):
diff --git a/tempest/services/compute/xml/images_client.py b/tempest/services/compute/xml/images_client.py
index 9d529be..6b15404 100644
--- a/tempest/services/compute/xml/images_client.py
+++ b/tempest/services/compute/xml/images_client.py
@@ -19,13 +19,9 @@
from tempest.common import rest_client
from tempest.common import waiters
+from tempest.common import xml_utils
from tempest import config
from tempest import exceptions
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import Text
-from tempest.services.compute.xml.common import xml_to_json
-from tempest.services.compute.xml.common import XMLNS_11
CONF = config.CONF
@@ -40,24 +36,24 @@
self.build_timeout = CONF.compute.build_timeout
def _parse_server(self, node):
- data = xml_to_json(node)
+ data = xml_utils.xml_to_json(node)
return self._parse_links(node, data)
def _parse_image(self, node):
"""Parses detailed XML image information into dictionary."""
- data = xml_to_json(node)
+ data = xml_utils.xml_to_json(node)
self._parse_links(node, data)
# parse all metadata
if 'metadata' in data:
- tag = node.find('{%s}metadata' % XMLNS_11)
+ tag = node.find('{%s}metadata' % xml_utils.XMLNS_11)
data['metadata'] = dict((x.get('key'), x.text)
for x in tag.getchildren())
# parse server information
if 'server' in data:
- tag = node.find('{%s}server' % XMLNS_11)
+ tag = node.find('{%s}server' % xml_utils.XMLNS_11)
data['server'] = self._parse_server(tag)
return data
@@ -67,7 +63,7 @@
if 'link' in data:
# remove single link element
del data['link']
- data['links'] = [xml_to_json(x) for x in
+ data['links'] = [xml_utils.xml_to_json(x) for x in
node.findall('{http://www.w3.org/2005/Atom}link')]
return data
@@ -93,17 +89,17 @@
def create_image(self, server_id, name, meta=None):
"""Creates an image of the original server."""
- post_body = Element('createImage', name=name)
+ post_body = xml_utils.Element('createImage', name=name)
if meta:
- metadata = Element('metadata')
+ metadata = xml_utils.Element('metadata')
post_body.append(metadata)
for k, v in meta.items():
- data = Element('meta', key=k)
- data.append(Text(v))
+ data = xml_utils.Element('meta', key=k)
+ data.append(xml_utils.Text(v))
metadata.append(data)
resp, body = self.post('servers/%s/action' % str(server_id),
- str(Document(post_body)))
+ str(xml_utils.Document(post_body)))
return resp, body
def list_images(self, params=None):
@@ -144,10 +140,10 @@
waiters.wait_for_image_status(self, image_id, status)
def _metadata_body(self, meta):
- post_body = Element('metadata')
+ post_body = xml_utils.Element('metadata')
for k, v in meta.items():
- data = Element('meta', key=k)
- data.append(Text(v))
+ data = xml_utils.Element('meta', key=k)
+ data.append(xml_utils.Text(v))
post_body.append(data)
return post_body
@@ -161,7 +157,7 @@
"""Sets the metadata for an image."""
post_body = self._metadata_body(meta)
resp, body = self.put('images/%s/metadata' % image_id,
- str(Document(post_body)))
+ str(xml_utils.Document(post_body)))
body = self._parse_key_value(etree.fromstring(body))
return resp, body
@@ -169,7 +165,7 @@
"""Updates the metadata for an image."""
post_body = self._metadata_body(meta)
resp, body = self.post('images/%s/metadata' % str(image_id),
- str(Document(post_body)))
+ str(xml_utils.Document(post_body)))
body = self._parse_key_value(etree.fromstring(body))
return resp, body
@@ -183,19 +179,19 @@
def set_image_metadata_item(self, image_id, key, meta):
"""Sets the value for a specific image metadata key."""
for k, v in meta.items():
- post_body = Element('meta', key=key)
- post_body.append(Text(v))
+ post_body = xml_utils.Element('meta', key=key)
+ post_body.append(xml_utils.Text(v))
resp, body = self.put('images/%s/metadata/%s' % (str(image_id), key),
- str(Document(post_body)))
- body = xml_to_json(etree.fromstring(body))
+ str(xml_utils.Document(post_body)))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def update_image_metadata_item(self, image_id, key, meta):
"""Sets the value for a specific image metadata key."""
- post_body = Document('meta', Text(meta), key=key)
+ post_body = xml_utils.Document('meta', xml_utils.Text(meta), key=key)
resp, body = self.put('images/%s/metadata/%s' % (str(image_id), key),
post_body)
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body['meta']
def delete_image_metadata_item(self, image_id, key):
diff --git a/tempest/services/compute/xml/instance_usage_audit_log_client.py b/tempest/services/compute/xml/instance_usage_audit_log_client.py
index 1cd8c07..b139db1 100644
--- a/tempest/services/compute/xml/instance_usage_audit_log_client.py
+++ b/tempest/services/compute/xml/instance_usage_audit_log_client.py
@@ -16,8 +16,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
-from tempest.services.compute.xml.common import xml_to_json
CONF = config.CONF
@@ -33,11 +33,13 @@
def list_instance_usage_audit_logs(self):
url = 'os-instance_usage_audit_log'
resp, body = self.get(url)
- instance_usage_audit_logs = xml_to_json(etree.fromstring(body))
+ instance_usage_audit_logs = xml_utils.xml_to_json(
+ etree.fromstring(body))
return resp, instance_usage_audit_logs
def get_instance_usage_audit_log(self, time_before):
url = 'os-instance_usage_audit_log/%s' % time_before
resp, body = self.get(url)
- instance_usage_audit_log = xml_to_json(etree.fromstring(body))
+ instance_usage_audit_log = xml_utils.xml_to_json(
+ etree.fromstring(body))
return resp, instance_usage_audit_log
diff --git a/tempest/services/compute/xml/interfaces_client.py b/tempest/services/compute/xml/interfaces_client.py
index 8d4bfcc..e30a97c 100644
--- a/tempest/services/compute/xml/interfaces_client.py
+++ b/tempest/services/compute/xml/interfaces_client.py
@@ -18,13 +18,9 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
from tempest import exceptions
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import Text
-from tempest.services.compute.xml.common import xml_to_json
-from tempest.services.compute.xml.common import XMLNS_11
CONF = config.CONF
@@ -37,9 +33,9 @@
self.service = CONF.compute.catalog_type
def _process_xml_interface(self, node):
- iface = xml_to_json(node)
+ iface = xml_utils.xml_to_json(node)
# NOTE(danms): if multiple addresses per interface is ever required,
- # xml_to_json will need to be fixed or replaced in this case
+ # xml_utils.xml_to_json will need to be fixed or replaced in this case
iface['fixed_ips'] = [dict(iface['fixed_ips']['fixed_ip'].items())]
return iface
@@ -52,21 +48,21 @@
def create_interface(self, server, port_id=None, network_id=None,
fixed_ip=None):
- doc = Document()
- iface = Element('interfaceAttachment')
+ doc = xml_utils.Document()
+ iface = xml_utils.Element('interfaceAttachment')
if port_id:
- _port_id = Element('port_id')
- _port_id.append(Text(port_id))
+ _port_id = xml_utils.Element('port_id')
+ _port_id.append(xml_utils.Text(port_id))
iface.append(_port_id)
if network_id:
- _network_id = Element('net_id')
- _network_id.append(Text(network_id))
+ _network_id = xml_utils.Element('net_id')
+ _network_id.append(xml_utils.Text(network_id))
iface.append(_network_id)
if fixed_ip:
- _fixed_ips = Element('fixed_ips')
- _fixed_ip = Element('fixed_ip')
- _ip_address = Element('ip_address')
- _ip_address.append(Text(fixed_ip))
+ _fixed_ips = xml_utils.Element('fixed_ips')
+ _fixed_ip = xml_utils.Element('fixed_ip')
+ _ip_address = xml_utils.Element('ip_address')
+ _ip_address.append(xml_utils.Text(fixed_ip))
_fixed_ip.append(_ip_address)
_fixed_ips.append(_fixed_ip)
iface.append(_fixed_ips)
@@ -108,18 +104,18 @@
def add_fixed_ip(self, server_id, network_id):
"""Add a fixed IP to input server instance."""
- post_body = Element("addFixedIp",
- xmlns=XMLNS_11,
- networkId=network_id)
+ post_body = xml_utils.Element("addFixedIp",
+ xmlns=xml_utils.XMLNS_11,
+ networkId=network_id)
resp, body = self.post('servers/%s/action' % str(server_id),
- str(Document(post_body)))
+ str(xml_utils.Document(post_body)))
return resp, body
def remove_fixed_ip(self, server_id, ip_address):
"""Remove input fixed IP from input server instance."""
- post_body = Element("removeFixedIp",
- xmlns=XMLNS_11,
- address=ip_address)
+ post_body = xml_utils.Element("removeFixedIp",
+ xmlns=xml_utils.XMLNS_11,
+ address=ip_address)
resp, body = self.post('servers/%s/action' % str(server_id),
- str(Document(post_body)))
+ str(xml_utils.Document(post_body)))
return resp, body
diff --git a/tempest/services/compute/xml/keypairs_client.py b/tempest/services/compute/xml/keypairs_client.py
index fb498c0..8ff37ac 100644
--- a/tempest/services/compute/xml/keypairs_client.py
+++ b/tempest/services/compute/xml/keypairs_client.py
@@ -17,11 +17,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import Text
-from tempest.services.compute.xml.common import xml_to_json
CONF = config.CONF
@@ -36,34 +33,35 @@
def list_keypairs(self):
resp, body = self.get("os-keypairs")
node = etree.fromstring(body)
- body = [{'keypair': xml_to_json(x)} for x in node.getchildren()]
+ body = [{'keypair': xml_utils.xml_to_json(x)} for x in
+ node.getchildren()]
return resp, body
def get_keypair(self, key_name):
resp, body = self.get("os-keypairs/%s" % str(key_name))
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def create_keypair(self, name, pub_key=None):
- doc = Document()
+ doc = xml_utils.Document()
- keypair_element = Element("keypair")
+ keypair_element = xml_utils.Element("keypair")
if pub_key:
- public_key_element = Element("public_key")
- public_key_text = Text(pub_key)
+ public_key_element = xml_utils.Element("public_key")
+ public_key_text = xml_utils.Text(pub_key)
public_key_element.append(public_key_text)
keypair_element.append(public_key_element)
- name_element = Element("name")
- name_text = Text(name)
+ name_element = xml_utils.Element("name")
+ name_text = xml_utils.Text(name)
name_element.append(name_text)
keypair_element.append(name_element)
doc.append(keypair_element)
resp, body = self.post("os-keypairs", body=str(doc))
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def delete_keypair(self, key_name):
diff --git a/tempest/services/compute/xml/quotas_client.py b/tempest/services/compute/xml/quotas_client.py
index 911c476..5502fcc 100644
--- a/tempest/services/compute/xml/quotas_client.py
+++ b/tempest/services/compute/xml/quotas_client.py
@@ -16,11 +16,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import xml_to_json
-from tempest.services.compute.xml.common import XMLNS_11
CONF = config.CONF
@@ -51,7 +48,7 @@
if user_id:
url += '?user_id=%s' % str(user_id)
resp, body = self.get(url)
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
body = self._format_quota(body)
return resp, body
@@ -60,12 +57,12 @@
url = 'os-quota-sets/%s/defaults' % str(tenant_id)
resp, body = self.get(url)
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
body = self._format_quota(body)
return resp, body
- def update_quota_set(self, tenant_id, force=None,
- injected_file_content_bytes=None,
+ def update_quota_set(self, tenant_id, user_id=None,
+ force=None, injected_file_content_bytes=None,
metadata_items=None, ram=None, floating_ips=None,
fixed_ips=None, key_pairs=None, instances=None,
security_group_rules=None, injected_files=None,
@@ -74,8 +71,8 @@
"""
Updates the tenant's quota limits for one or more resources
"""
- post_body = Element("quota_set",
- xmlns=XMLNS_11)
+ post_body = xml_utils.Element("quota_set",
+ xmlns=xml_utils.XMLNS_11)
if force is not None:
post_body.add_attr('force', force)
@@ -118,9 +115,15 @@
if security_groups is not None:
post_body.add_attr('security_groups', security_groups)
- resp, body = self.put('os-quota-sets/%s' % str(tenant_id),
- str(Document(post_body)))
- body = xml_to_json(etree.fromstring(body))
+ if user_id:
+ resp, body = self.put('os-quota-sets/%s?user_id=%s' %
+ (str(tenant_id), str(user_id)),
+ str(xml_utils.Document(post_body)))
+ else:
+ resp, body = self.put('os-quota-sets/%s' % str(tenant_id),
+ str(xml_utils.Document(post_body)))
+
+ body = xml_utils.xml_to_json(etree.fromstring(body))
body = self._format_quota(body)
return resp, body
diff --git a/tempest/services/compute/xml/security_groups_client.py b/tempest/services/compute/xml/security_groups_client.py
index d53e8da..9eccb90 100644
--- a/tempest/services/compute/xml/security_groups_client.py
+++ b/tempest/services/compute/xml/security_groups_client.py
@@ -17,13 +17,9 @@
import urllib
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
from tempest import exceptions
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import Text
-from tempest.services.compute.xml.common import xml_to_json
-from tempest.services.compute.xml.common import XMLNS_11
CONF = config.CONF
@@ -38,11 +34,11 @@
def _parse_array(self, node):
array = []
for child in node.getchildren():
- array.append(xml_to_json(child))
+ array.append(xml_utils.xml_to_json(child))
return array
def _parse_body(self, body):
- json = xml_to_json(body)
+ json = xml_utils.xml_to_json(body)
return json
def list_security_groups(self, params=None):
@@ -69,12 +65,12 @@
name (Required): Name of security group.
description (Required): Description of security group.
"""
- security_group = Element("security_group", name=name)
- des = Element("description")
- des.append(Text(content=description))
+ security_group = xml_utils.Element("security_group", name=name)
+ des = xml_utils.Element("description")
+ des.append(xml_utils.Text(content=description))
security_group.append(des)
resp, body = self.post('os-security-groups',
- str(Document(security_group)))
+ str(xml_utils.Document(security_group)))
body = self._parse_body(etree.fromstring(body))
return resp, body
@@ -86,18 +82,18 @@
name: new name of security group
description: new description of security group
"""
- security_group = Element("security_group")
+ security_group = xml_utils.Element("security_group")
if name:
- sg_name = Element("name")
- sg_name.append(Text(content=name))
+ sg_name = xml_utils.Element("name")
+ sg_name.append(xml_utils.Text(content=name))
security_group.append(sg_name)
if description:
- des = Element("description")
- des.append(Text(content=description))
+ des = xml_utils.Element("description")
+ des.append(xml_utils.Text(content=description))
security_group.append(des)
resp, body = self.put('os-security-groups/%s' %
str(security_group_id),
- str(Document(security_group)))
+ str(xml_utils.Document(security_group)))
body = self._parse_body(etree.fromstring(body))
return resp, body
@@ -117,7 +113,7 @@
cidr : CIDR for address range.
group_id : ID of the Source group
"""
- group_rule = Element("security_group_rule")
+ group_rule = xml_utils.Element("security_group_rule")
elements = dict()
elements['cidr'] = kwargs.get('cidr')
@@ -129,12 +125,12 @@
for k, v in elements.items():
if v is not None:
- element = Element(k)
- element.append(Text(content=str(v)))
+ element = xml_utils.Element(k)
+ element.append(xml_utils.Text(content=str(v)))
group_rule.append(element)
url = 'os-security-group-rules'
- resp, body = self.post(url, str(Document(group_rule)))
+ resp, body = self.post(url, str(xml_utils.Document(group_rule)))
body = self._parse_body(etree.fromstring(body))
return resp, body
@@ -151,8 +147,8 @@
secgroups = body.getchildren()
for secgroup in secgroups:
if secgroup.get('id') == security_group_id:
- node = secgroup.find('{%s}rules' % XMLNS_11)
- rules = [xml_to_json(x) for x in node.getchildren()]
+ node = secgroup.find('{%s}rules' % xml_utils.XMLNS_11)
+ rules = [xml_utils.xml_to_json(x) for x in node.getchildren()]
return resp, rules
raise exceptions.NotFound('No such Security Group')
diff --git a/tempest/services/compute/xml/servers_client.py b/tempest/services/compute/xml/servers_client.py
index 7a2a071..c1105f9 100644
--- a/tempest/services/compute/xml/servers_client.py
+++ b/tempest/services/compute/xml/servers_client.py
@@ -21,14 +21,10 @@
from tempest.common import rest_client
from tempest.common import waiters
+from tempest.common import xml_utils
from tempest import config
from tempest import exceptions
from tempest.openstack.common import log as logging
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import Text
-from tempest.services.compute.xml.common import xml_to_json
-from tempest.services.compute.xml.common import XMLNS_11
CONF = config.CONF
@@ -60,12 +56,13 @@
def _translate_network_xml_to_json(network):
return [_translate_ip_xml_json(ip.attrib)
- for ip in network.findall('{%s}ip' % XMLNS_11)]
+ for ip in network.findall('{%s}ip' % xml_utils.XMLNS_11)]
def _translate_addresses_xml_to_json(xml_addresses):
return dict((network.attrib['id'], _translate_network_xml_to_json(network))
- for network in xml_addresses.findall('{%s}network' % XMLNS_11))
+ for network in xml_addresses.findall('{%s}network' %
+ xml_utils.XMLNS_11))
def _translate_server_xml_to_json(xml_dom):
@@ -97,16 +94,16 @@
'version': 6}],
'foo_novanetwork': [{'addr': '192.168.0.4', 'version': 4}]}}
"""
- nsmap = {'api': XMLNS_11}
+ nsmap = {'api': xml_utils.XMLNS_11}
addresses = xml_dom.xpath('/api:server/api:addresses', namespaces=nsmap)
if addresses:
if len(addresses) > 1:
raise ValueError('Expected only single `addresses` element.')
json_addresses = _translate_addresses_xml_to_json(addresses[0])
- json = xml_to_json(xml_dom)
+ json = xml_utils.xml_to_json(xml_dom)
json['addresses'] = json_addresses
else:
- json = xml_to_json(xml_dom)
+ json = xml_utils.xml_to_json(xml_dom)
diskConfig = ('{http://docs.openstack.org'
'/compute/ext/disk_config/api/v1.1}diskConfig')
terminated_at = ('{http://docs.openstack.org/'
@@ -122,6 +119,10 @@
'/compute/ext/extended_status/api/v1.1}vm_state')
task_state = ('{http://docs.openstack.org'
'/compute/ext/extended_status/api/v1.1}task_state')
+ if 'tenantId' in json:
+ json['tenant_id'] = json.pop('tenantId')
+ if 'userId' in json:
+ json['user_id'] = json.pop('userId')
if diskConfig in json:
json['OS-DCF:diskConfig'] = json.pop(diskConfig)
if terminated_at in json:
@@ -157,7 +158,7 @@
del json['link']
json['links'] = []
for linknode in node.findall('{http://www.w3.org/2005/Atom}link'):
- json['links'].append(xml_to_json(linknode))
+ json['links'].append(xml_utils.xml_to_json(linknode))
def _parse_server(self, body):
json = _translate_server_xml_to_json(body)
@@ -165,7 +166,7 @@
if 'metadata' in json and json['metadata']:
# NOTE(danms): if there was metadata, we need to re-parse
# that as a special type
- metadata_tag = body.find('{%s}metadata' % XMLNS_11)
+ metadata_tag = body.find('{%s}metadata' % xml_utils.XMLNS_11)
json["metadata"] = self._parse_key_value(metadata_tag)
if 'link' in json:
self._parse_links(body, json)
@@ -242,7 +243,13 @@
def _parse_array(self, node):
array = []
for child in node.getchildren():
- array.append(xml_to_json(child))
+ array.append(xml_utils.xml_to_json(child))
+ return array
+
+ def _parse_server_array(self, node):
+ array = []
+ for child in node.getchildren():
+ array.append(self._parse_server(child))
return array
def list_servers(self, params=None):
@@ -251,7 +258,7 @@
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
- servers = self._parse_array(etree.fromstring(body))
+ servers = self._parse_server_array(etree.fromstring(body))
return resp, {"servers": servers}
def list_servers_with_detail(self, params=None):
@@ -260,13 +267,13 @@
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
- servers = self._parse_array(etree.fromstring(body))
+ servers = self._parse_server_array(etree.fromstring(body))
return resp, {"servers": servers}
def update_server(self, server_id, name=None, meta=None, accessIPv4=None,
accessIPv6=None, disk_config=None):
- doc = Document()
- server = Element("server")
+ doc = xml_utils.Document()
+ server = xml_utils.Element("server")
doc.append(server)
if name is not None:
@@ -280,15 +287,15 @@
"compute/ext/disk_config/api/v1.1")
server.add_attr("OS-DCF:diskConfig", disk_config)
if meta is not None:
- metadata = Element("metadata")
+ metadata = xml_utils.Element("metadata")
server.append(metadata)
for k, v in meta:
- meta = Element("meta", key=k)
- meta.append(Text(v))
+ meta = xml_utils.Element("meta", key=k)
+ meta.append(xml_utils.Text(v))
metadata.append(meta)
resp, body = self.put('servers/%s' % str(server_id), str(doc))
- return resp, xml_to_json(etree.fromstring(body))
+ return resp, xml_utils.xml_to_json(etree.fromstring(body))
def create_server(self, name, image_ref, flavor_ref, **kwargs):
"""
@@ -312,11 +319,11 @@
max_count: Count of maximum number of instances to launch.
disk_config: Determines if user or admin controls disk configuration.
"""
- server = Element("server",
- xmlns=XMLNS_11,
- imageRef=image_ref,
- flavorRef=flavor_ref,
- name=name)
+ server = xml_utils.Element("server",
+ xmlns=xml_utils.XMLNS_11,
+ imageRef=image_ref,
+ flavorRef=flavor_ref,
+ name=name)
for attr in ["adminPass", "accessIPv4", "accessIPv6", "key_name",
"user_data", "availability_zone", "min_count",
@@ -330,46 +337,46 @@
server.add_attr('OS-DCF:diskConfig', kwargs['disk_config'])
if 'security_groups' in kwargs:
- secgroups = Element("security_groups")
+ secgroups = xml_utils.Element("security_groups")
server.append(secgroups)
for secgroup in kwargs['security_groups']:
- s = Element("security_group", name=secgroup['name'])
+ s = xml_utils.Element("security_group", name=secgroup['name'])
secgroups.append(s)
if 'networks' in kwargs:
- networks = Element("networks")
+ networks = xml_utils.Element("networks")
server.append(networks)
for network in kwargs['networks']:
- s = Element("network", uuid=network['uuid'],
- fixed_ip=network['fixed_ip'])
+ s = xml_utils.Element("network", uuid=network['uuid'],
+ fixed_ip=network['fixed_ip'])
networks.append(s)
if 'meta' in kwargs:
- metadata = Element("metadata")
+ metadata = xml_utils.Element("metadata")
server.append(metadata)
for k, v in kwargs['meta'].items():
- meta = Element("meta", key=k)
- meta.append(Text(v))
+ meta = xml_utils.Element("meta", key=k)
+ meta.append(xml_utils.Text(v))
metadata.append(meta)
if 'personality' in kwargs:
- personality = Element('personality')
+ personality = xml_utils.Element('personality')
server.append(personality)
for k in kwargs['personality']:
- temp = Element('file', path=k['path'])
- temp.append(Text(k['contents']))
+ temp = xml_utils.Element('file', path=k['path'])
+ temp.append(xml_utils.Text(k['contents']))
personality.append(temp)
if 'sched_hints' in kwargs:
sched_hints = kwargs.get('sched_hints')
- hints = Element("os:scheduler_hints")
- hints.add_attr('xmlns:os', XMLNS_11)
+ hints = xml_utils.Element("os:scheduler_hints")
+ hints.add_attr('xmlns:os', xml_utils.XMLNS_11)
for attr in sched_hints:
- p1 = Element(attr)
+ p1 = xml_utils.Element(attr)
p1.append(sched_hints[attr])
hints.append(p1)
server.append(hints)
- resp, body = self.post('servers', str(Document(server)))
+ resp, body = self.post('servers', str(xml_utils.Document(server)))
server = self._parse_server(etree.fromstring(body))
return resp, server
@@ -427,11 +434,11 @@
def action(self, server_id, action_name, response_key, **kwargs):
if 'xmlns' not in kwargs:
- kwargs['xmlns'] = XMLNS_11
- doc = Document((Element(action_name, **kwargs)))
+ kwargs['xmlns'] = xml_utils.XMLNS_11
+ doc = xml_utils.Document((xml_utils.Element(action_name, **kwargs)))
resp, body = self.post("servers/%s/action" % server_id, str(doc))
if response_key is not None:
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def create_backup(self, server_id, backup_type, rotation, name):
@@ -447,7 +454,7 @@
def get_password(self, server_id):
resp, body = self.get("servers/%s/os-server-password" % str(server_id))
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def delete_password(self, server_id):
@@ -470,24 +477,23 @@
"compute/ext/disk_config/api/v1.1"
kwargs['xmlns:atom'] = "http://www.w3.org/2005/Atom"
if 'xmlns' not in kwargs:
- kwargs['xmlns'] = XMLNS_11
+ kwargs['xmlns'] = xml_utils.XMLNS_11
attrs = kwargs.copy()
if 'metadata' in attrs:
del attrs['metadata']
- rebuild = Element("rebuild",
- **attrs)
+ rebuild = xml_utils.Element("rebuild", **attrs)
if 'metadata' in kwargs:
- metadata = Element("metadata")
+ metadata = xml_utils.Element("metadata")
rebuild.append(metadata)
for k, v in kwargs['metadata'].items():
- meta = Element("meta", key=k)
- meta.append(Text(v))
+ meta = xml_utils.Element("meta", key=k)
+ meta.append(xml_utils.Text(v))
metadata.append(meta)
resp, body = self.post('servers/%s/action' % server_id,
- str(Document(rebuild)))
+ str(xml_utils.Document(rebuild)))
server = self._parse_server(etree.fromstring(body))
return resp, server
@@ -525,14 +531,14 @@
def live_migrate_server(self, server_id, dest_host, use_block_migration):
"""This should be called with administrator privileges ."""
- req_body = Element("os-migrateLive",
- xmlns=XMLNS_11,
- disk_over_commit=False,
- block_migration=use_block_migration,
- host=dest_host)
+ req_body = xml_utils.Element("os-migrateLive",
+ xmlns=xml_utils.XMLNS_11,
+ disk_over_commit=False,
+ block_migration=use_block_migration,
+ host=dest_host)
resp, body = self.post("servers/%s/action" % str(server_id),
- str(Document(req_body)))
+ str(xml_utils.Document(req_body)))
return resp, body
def list_server_metadata(self, server_id):
@@ -541,44 +547,44 @@
return resp, body
def set_server_metadata(self, server_id, meta, no_metadata_field=False):
- doc = Document()
+ doc = xml_utils.Document()
if not no_metadata_field:
- metadata = Element("metadata")
+ metadata = xml_utils.Element("metadata")
doc.append(metadata)
for k, v in meta.items():
- meta_element = Element("meta", key=k)
- meta_element.append(Text(v))
+ meta_element = xml_utils.Element("meta", key=k)
+ meta_element.append(xml_utils.Text(v))
metadata.append(meta_element)
resp, body = self.put('servers/%s/metadata' % str(server_id), str(doc))
- return resp, xml_to_json(etree.fromstring(body))
+ return resp, xml_utils.xml_to_json(etree.fromstring(body))
def update_server_metadata(self, server_id, meta):
- doc = Document()
- metadata = Element("metadata")
+ doc = xml_utils.Document()
+ metadata = xml_utils.Element("metadata")
doc.append(metadata)
for k, v in meta.items():
- meta_element = Element("meta", key=k)
- meta_element.append(Text(v))
+ meta_element = xml_utils.Element("meta", key=k)
+ meta_element.append(xml_utils.Text(v))
metadata.append(meta_element)
resp, body = self.post("/servers/%s/metadata" % str(server_id),
str(doc))
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def get_server_metadata_item(self, server_id, key):
resp, body = self.get("servers/%s/metadata/%s" % (str(server_id), key))
return resp, dict([(etree.fromstring(body).attrib['key'],
- xml_to_json(etree.fromstring(body)))])
+ xml_utils.xml_to_json(etree.fromstring(body)))])
def set_server_metadata_item(self, server_id, key, meta):
- doc = Document()
+ doc = xml_utils.Document()
for k, v in meta.items():
- meta_element = Element("meta", key=k)
- meta_element.append(Text(v))
+ meta_element = xml_utils.Element("meta", key=k)
+ meta_element.append(xml_utils.Text(v))
doc.append(meta_element)
resp, body = self.put('servers/%s/metadata/%s' % (str(server_id), key),
str(doc))
- return resp, xml_to_json(etree.fromstring(body))
+ return resp, xml_utils.xml_to_json(etree.fromstring(body))
def delete_server_metadata_item(self, server_id, key):
resp, body = self.delete("servers/%s/metadata/%s" %
@@ -607,10 +613,10 @@
return self.action(server_id, 'unrescue', None)
def attach_volume(self, server_id, volume_id, device='/dev/vdz'):
- post_body = Element("volumeAttachment", volumeId=volume_id,
- device=device)
+ post_body = xml_utils.Element("volumeAttachment", volumeId=volume_id,
+ device=device)
resp, body = self.post('servers/%s/os-volume_attachments' % server_id,
- str(Document(post_body)))
+ str(xml_utils.Document(post_body)))
return resp, body
def detach_volume(self, server_id, volume_id):
@@ -623,7 +629,7 @@
def get_server_diagnostics(self, server_id):
"""Get the usage data for a server."""
resp, body = self.get("servers/%s/diagnostics" % server_id)
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def list_instance_actions(self, server_id):
@@ -636,7 +642,7 @@
"""Returns the action details of the provided server."""
resp, body = self.get("servers/%s/os-instance-actions/%s" %
(server_id, request_id))
- body = xml_to_json(etree.fromstring(body))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def force_delete_server(self, server_id, **kwargs):
diff --git a/tempest/services/compute/xml/services_client.py b/tempest/services/compute/xml/services_client.py
index d7b8a60..e1e78d0 100644
--- a/tempest/services/compute/xml/services_client.py
+++ b/tempest/services/compute/xml/services_client.py
@@ -19,10 +19,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import xml_to_json
CONF = config.CONF
@@ -41,7 +39,7 @@
resp, body = self.get(url)
node = etree.fromstring(body)
- body = [xml_to_json(x) for x in node.getchildren()]
+ body = [xml_utils.xml_to_json(x) for x in node.getchildren()]
return resp, body
def enable_service(self, host_name, binary):
@@ -50,12 +48,13 @@
host_name: Name of host
binary: Service binary
"""
- post_body = Element("service")
+ post_body = xml_utils.Element("service")
post_body.add_attr('binary', binary)
post_body.add_attr('host', host_name)
- resp, body = self.put('os-services/enable', str(Document(post_body)))
- body = xml_to_json(etree.fromstring(body))
+ resp, body = self.put('os-services/enable', str(
+ xml_utils.Document(post_body)))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def disable_service(self, host_name, binary):
@@ -64,10 +63,11 @@
host_name: Name of host
binary: Service binary
"""
- post_body = Element("service")
+ post_body = xml_utils.Element("service")
post_body.add_attr('binary', binary)
post_body.add_attr('host', host_name)
- resp, body = self.put('os-services/disable', str(Document(post_body)))
- body = xml_to_json(etree.fromstring(body))
+ resp, body = self.put('os-services/disable', str(
+ xml_utils.Document(post_body)))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
diff --git a/tempest/services/compute/xml/tenant_usages_client.py b/tempest/services/compute/xml/tenant_usages_client.py
index 79f0ac9..0b19f63 100644
--- a/tempest/services/compute/xml/tenant_usages_client.py
+++ b/tempest/services/compute/xml/tenant_usages_client.py
@@ -18,8 +18,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
-from tempest.services.compute.xml.common import xml_to_json
CONF = config.CONF
@@ -32,7 +32,7 @@
self.service = CONF.compute.catalog_type
def _parse_array(self, node):
- json = xml_to_json(node)
+ json = xml_utils.xml_to_json(node)
return json
def list_tenant_usages(self, params=None):
diff --git a/tempest/services/compute/xml/volumes_extensions_client.py b/tempest/services/compute/xml/volumes_extensions_client.py
index 570b715..e9c5035 100644
--- a/tempest/services/compute/xml/volumes_extensions_client.py
+++ b/tempest/services/compute/xml/volumes_extensions_client.py
@@ -19,13 +19,9 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils
from tempest import config
from tempest import exceptions
-from tempest.services.compute.xml.common import Document
-from tempest.services.compute.xml.common import Element
-from tempest.services.compute.xml.common import Text
-from tempest.services.compute.xml.common import xml_to_json
-from tempest.services.compute.xml.common import XMLNS_11
CONF = config.CONF
@@ -51,7 +47,7 @@
vol['metadata'] = dict((meta.get('key'),
meta.text) for meta in list(child))
else:
- vol[tag] = xml_to_json(child)
+ vol[tag] = xml_utils.xml_to_json(child)
return vol
def list_volumes(self, params=None):
@@ -96,23 +92,23 @@
:param display_name: Optional Volume Name.
:param metadata: An optional dictionary of values for metadata.
"""
- volume = Element("volume",
- xmlns=XMLNS_11,
- size=size)
+ volume = xml_utils.Element("volume",
+ xmlns=xml_utils.XMLNS_11,
+ size=size)
if display_name:
volume.add_attr('display_name', display_name)
if metadata:
- _metadata = Element('metadata')
+ _metadata = xml_utils.Element('metadata')
volume.append(_metadata)
for key, value in metadata.items():
- meta = Element('meta')
+ meta = xml_utils.Element('meta')
meta.add_attr('key', key)
- meta.append(Text(value))
+ meta.append(xml_utils.Text(value))
_metadata.append(meta)
- resp, body = self.post('os-volumes', str(Document(volume)))
- body = xml_to_json(etree.fromstring(body))
+ resp, body = self.post('os-volumes', str(xml_utils.Document(volume)))
+ body = xml_utils.xml_to_json(etree.fromstring(body))
return resp, body
def delete_volume(self, volume_id):
diff --git a/tempest/services/identity/json/identity_client.py b/tempest/services/identity/json/identity_client.py
index 58451fb..55239f7 100644
--- a/tempest/services/identity/json/identity_client.py
+++ b/tempest/services/identity/json/identity_client.py
@@ -49,6 +49,12 @@
resp, body = self.post('OS-KSADM/roles', post_body)
return resp, self._parse_resp(body)
+ def get_role(self, role_id):
+ """Get a role by its id."""
+ resp, body = self.get('OS-KSADM/roles/%s' % role_id)
+ body = json.loads(body)
+ return resp, body['role']
+
def create_tenant(self, name, **kwargs):
"""
Create a tenant
@@ -213,7 +219,7 @@
def list_services(self):
"""List Service - Returns Services."""
- resp, body = self.get('/OS-KSADM/services/')
+ resp, body = self.get('/OS-KSADM/services')
return resp, self._parse_resp(body)
def delete_service(self, service_id):
@@ -269,13 +275,20 @@
return resp, body['access']
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
"""A simple HTTP request interface."""
if headers is None:
# Always accept 'json', for TokenClientXML too.
# Because XML response is not easily
# converted to the corresponding JSON one
headers = self.get_headers(accept_type="json")
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers(accept_type="json"))
+ except (ValueError, TypeError):
+ headers = self.get_headers(accept_type="json")
+
resp, resp_body = self.http_obj.request(url, method,
headers=headers, body=body)
self._log_request(method, url, resp)
diff --git a/tempest/services/identity/v3/json/identity_client.py b/tempest/services/identity/v3/json/identity_client.py
index 35d8aa0..6829333 100644
--- a/tempest/services/identity/v3/json/identity_client.py
+++ b/tempest/services/identity/v3/json/identity_client.py
@@ -163,6 +163,12 @@
body = json.loads(body)
return resp, body['role']
+ def list_roles(self):
+ """Get the list of Roles."""
+ resp, body = self.get("roles")
+ body = json.loads(body)
+ return resp, body['roles']
+
def update_role(self, name, role_id):
"""Create a Role."""
post_body = {
@@ -459,16 +465,20 @@
self.auth_url = auth_url
- def auth(self, user, password, tenant=None, user_type='id', domain=None):
+ def auth(self, user=None, password=None, tenant=None, user_type='id',
+ domain=None, token=None):
"""
:param user: user id or name, as specified in user_type
:param domain: the user and tenant domain
+ :param token: a token to re-scope.
Accepts different combinations of credentials. Restrictions:
- tenant and domain are only name (no id)
- user domain and tenant domain are assumed identical
- domain scope is not supported here
Sample sample valid combinations:
+ - token
+ - token, tenant, domain
- user_id, password
- username, password, domain
- username, password, tenant, domain
@@ -477,23 +487,32 @@
creds = {
'auth': {
'identity': {
- 'methods': ['password'],
- 'password': {
- 'user': {
- 'password': password,
- }
- }
+ 'methods': [],
}
}
}
- if user_type == 'id':
- creds['auth']['identity']['password']['user']['id'] = user
- else:
- creds['auth']['identity']['password']['user']['name'] = user
- if domain is not None:
- _domain = dict(name=domain)
- creds['auth']['identity']['password']['user']['domain'] = _domain
+ id_obj = creds['auth']['identity']
+ if token:
+ id_obj['methods'].append('token')
+ id_obj['token'] = {
+ 'id': token
+ }
+ if user and password:
+ id_obj['methods'].append('password')
+ id_obj['password'] = {
+ 'user': {
+ 'password': password,
+ }
+ }
+ if user_type == 'id':
+ id_obj['password']['user']['id'] = user
+ else:
+ id_obj['password']['user']['name'] = user
+ if domain is not None:
+ _domain = dict(name=domain)
+ id_obj['password']['user']['domain'] = _domain
if tenant is not None:
+ _domain = dict(name=domain)
project = dict(name=tenant, domain=_domain)
scope = dict(project=project)
creds['auth']['scope'] = scope
@@ -502,13 +521,20 @@
resp, body = self.post(self.auth_url, body=body)
return resp, body
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
"""A simple HTTP request interface."""
if headers is None:
# Always accept 'json', for xml token client too.
# Because XML response is not easily
# converted to the corresponding JSON one
headers = self.get_headers(accept_type="json")
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers(accept_type="json"))
+ except (ValueError, TypeError):
+ headers = self.get_headers(accept_type="json")
+
resp, resp_body = self.http_obj.request(url, method,
headers=headers, body=body)
self._log_request(method, url, resp)
diff --git a/tempest/services/identity/v3/xml/credentials_client.py b/tempest/services/identity/v3/xml/credentials_client.py
index 70f85a1..3c44188 100644
--- a/tempest/services/identity/v3/xml/credentials_client.py
+++ b/tempest/services/identity/v3/xml/credentials_client.py
@@ -18,8 +18,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils as common
from tempest import config
-from tempest.services.compute.xml import common
CONF = config.CONF
diff --git a/tempest/services/identity/v3/xml/endpoints_client.py b/tempest/services/identity/v3/xml/endpoints_client.py
index a1f9811..6490e34 100644
--- a/tempest/services/identity/v3/xml/endpoints_client.py
+++ b/tempest/services/identity/v3/xml/endpoints_client.py
@@ -17,8 +17,8 @@
from tempest.common import http
from tempest.common import rest_client
+from tempest.common import xml_utils as common
from tempest import config
-from tempest.services.compute.xml import common
CONF = config.CONF
@@ -46,12 +46,19 @@
json = common.xml_to_json(body)
return json
- def request(self, method, url, headers=None, body=None, wait=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None, wait=None):
"""Overriding the existing HTTP request in super class RestClient."""
+ if extra_headers:
+ try:
+ headers.update(self.get_headers())
+ except (ValueError, TypeError):
+ headers = self.get_headers()
dscv = CONF.identity.disable_ssl_certificate_validation
self.http_obj = http.ClosingHttp(
disable_ssl_certificate_validation=dscv)
return super(EndPointClientXML, self).request(method, url,
+ extra_headers,
headers=headers,
body=body)
diff --git a/tempest/services/identity/v3/xml/identity_client.py b/tempest/services/identity/v3/xml/identity_client.py
index 8f42924..35295d7 100644
--- a/tempest/services/identity/v3/xml/identity_client.py
+++ b/tempest/services/identity/v3/xml/identity_client.py
@@ -18,9 +18,9 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils as common
from tempest import config
from tempest import exceptions
-from tempest.services.compute.xml import common
CONF = config.CONF
@@ -217,6 +217,12 @@
body = self._parse_body(etree.fromstring(body))
return resp, body
+ def list_roles(self):
+ """Get the list of Roles."""
+ resp, body = self.get("roles")
+ body = self._parse_roles(etree.fromstring(body))
+ return resp, body
+
def update_role(self, name, role_id):
"""Updates a Role."""
post_body = common.Element("role",
@@ -453,43 +459,61 @@
self.auth_url = auth_url
- def auth(self, user, password, tenant=None, user_type='id', domain=None):
+ def auth(self, user=None, password=None, tenant=None, user_type='id',
+ domain=None, token=None):
"""
:param user: user id or name, as specified in user_type
+ :param domain: the user and tenant domain
+ :param token: a token to re-scope.
Accepts different combinations of credentials. Restrictions:
- tenant and domain are only name (no id)
- user domain and tenant domain are assumed identical
+ - domain scope is not supported here
Sample sample valid combinations:
+ - token
+ - token, tenant, domain
- user_id, password
- username, password, domain
- username, password, tenant, domain
Validation is left to the server side.
"""
- if user_type == 'id':
- _user = common.Element('user', id=user, password=password)
- else:
- _user = common.Element('user', name=user, password=password)
- if domain is not None:
- _domain = common.Element('domain', name=domain)
- _user.append(_domain)
- password = common.Element('password')
- password.append(_user)
-
- method = common.Element('method')
- method.append(common.Text('password'))
methods = common.Element('methods')
- methods.append(method)
identity = common.Element('identity')
+
+ if token:
+ method = common.Element('method')
+ method.append(common.Text('token'))
+ methods.append(method)
+
+ token = common.Element('token', id=token)
+ identity.append(token)
+
+ if user and password:
+ if user_type == 'id':
+ _user = common.Element('user', id=user, password=password)
+ else:
+ _user = common.Element('user', name=user, password=password)
+ if domain is not None:
+ _domain = common.Element('domain', name=domain)
+ _user.append(_domain)
+
+ password = common.Element('password')
+ password.append(_user)
+ method = common.Element('method')
+ method.append(common.Text('password'))
+ methods.append(method)
+ identity.append(password)
+
identity.append(methods)
- identity.append(password)
auth = common.Element('auth')
auth.append(identity)
if tenant is not None:
project = common.Element('project', name=tenant)
+ _domain = common.Element('domain', name=domain)
project.append(_domain)
scope = common.Element('scope')
scope.append(project)
@@ -498,13 +522,19 @@
resp, body = self.post(self.auth_url, body=str(common.Document(auth)))
return resp, body
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
"""A simple HTTP request interface."""
if headers is None:
# Always accept 'json', for xml token client too.
# Because XML response is not easily
# converted to the corresponding JSON one
headers = self.get_headers(accept_type="json")
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers(accept_type="json"))
+ except (ValueError, TypeError):
+ headers = self.get_headers(accept_type="json")
resp, resp_body = self.http_obj.request(url, method,
headers=headers, body=body)
self._log_request(method, url, resp)
diff --git a/tempest/services/identity/v3/xml/policy_client.py b/tempest/services/identity/v3/xml/policy_client.py
index bf4cce7..73d831b 100644
--- a/tempest/services/identity/v3/xml/policy_client.py
+++ b/tempest/services/identity/v3/xml/policy_client.py
@@ -17,8 +17,8 @@
from tempest.common import http
from tempest.common import rest_client
+from tempest.common import xml_utils as common
from tempest import config
-from tempest.services.compute.xml import common
CONF = config.CONF
@@ -46,12 +46,19 @@
json = common.xml_to_json(body)
return json
- def request(self, method, url, headers=None, body=None, wait=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None, wait=None):
"""Overriding the existing HTTP request in super class RestClient."""
+ if extra_headers:
+ try:
+ headers.update(self.get_headers())
+ except (ValueError, TypeError):
+ headers = self.get_headers()
dscv = CONF.identity.disable_ssl_certificate_validation
self.http_obj = http.ClosingHttp(
disable_ssl_certificate_validation=dscv)
return super(PolicyClientXML, self).request(method, url,
+ extra_headers,
headers=headers,
body=body)
diff --git a/tempest/services/identity/v3/xml/service_client.py b/tempest/services/identity/v3/xml/service_client.py
index 966d7f7..37ed892 100644
--- a/tempest/services/identity/v3/xml/service_client.py
+++ b/tempest/services/identity/v3/xml/service_client.py
@@ -16,8 +16,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils as common
from tempest import config
-from tempest.services.compute.xml import common
CONF = config.CONF
diff --git a/tempest/services/identity/xml/identity_client.py b/tempest/services/identity/xml/identity_client.py
index c5bf310..c48bc90 100644
--- a/tempest/services/identity/xml/identity_client.py
+++ b/tempest/services/identity/xml/identity_client.py
@@ -12,8 +12,8 @@
# 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 xml_utils as xml
from tempest import config
-from tempest.services.compute.xml import common as xml
from tempest.services.identity.json import identity_client
CONF = config.CONF
@@ -31,6 +31,11 @@
str(xml.Document(create_role)))
return resp, self._parse_resp(body)
+ def get_role(self, role_id):
+ """Get a role by its id."""
+ resp, body = self.get('OS-KSADM/roles/%s' % role_id)
+ return resp, self._parse_resp(body)
+
def create_tenant(self, name, **kwargs):
"""
Create a tenant
diff --git a/tempest/services/network/json/network_client.py b/tempest/services/network/json/network_client.py
index 27f4655..f9dd8ef 100644
--- a/tempest/services/network/json/network_client.py
+++ b/tempest/services/network/json/network_client.py
@@ -180,18 +180,6 @@
body = json.loads(body)
return resp, body
- def update_vpnservice(self, uuid, description):
- put_body = {
- "vpnservice": {
- "description": description
- }
- }
- body = json.dumps(put_body)
- uri = '%s/vpn/vpnservices/%s' % (self.uri_prefix, uuid)
- resp, body = self.put(uri, body)
- body = json.loads(body)
- return resp, body
-
def list_router_interfaces(self, uuid):
uri = '%s/ports?device_id=%s' % (self.uri_prefix, uuid)
resp, body = self.get(uri)
@@ -281,14 +269,6 @@
body = json.loads(body)
return resp, body
- def update_ikepolicy(self, uuid, **kwargs):
- put_body = {'ikepolicy': kwargs}
- body = json.dumps(put_body)
- uri = '%s/vpn/ikepolicies/%s' % (self.uri_prefix, uuid)
- resp, body = self.put(uri, body)
- body = json.loads(body)
- return resp, body
-
def update_extra_routes(self, router_id, nexthop, destination):
uri = '%s/routers/%s' % (self.uri_prefix, router_id)
put_body = {
@@ -320,3 +300,11 @@
resp, body = self.get(uri)
body = json.loads(body)
return resp, body
+
+ def add_dhcp_agent_to_network(self, agent_id, network_id):
+ post_body = {'network_id': network_id}
+ body = json.dumps(post_body)
+ uri = '%s/agents/%s/dhcp-networks' % (self.uri_prefix, agent_id)
+ resp, body = self.post(uri, body)
+ body = json.loads(body)
+ return resp, body
diff --git a/tempest/services/network/network_client_base.py b/tempest/services/network/network_client_base.py
index e21abe1..34c61b0 100644
--- a/tempest/services/network/network_client_base.py
+++ b/tempest/services/network/network_client_base.py
@@ -31,12 +31,15 @@
'vpnservices': 'vpn',
'ikepolicies': 'vpn',
'metering_labels': 'metering',
- 'metering_label_rules': 'metering'
+ 'metering_label_rules': 'metering',
+ 'firewall_rules': 'fw',
+ 'firewall_policies': 'fw',
+ 'firewalls': 'fw'
}
# The following list represents resource names that do not require
# changing underscore to a hyphen
-hyphen_exceptions = ["health_monitors"]
+hyphen_exceptions = ["health_monitors", "firewall_rules", "firewall_policies"]
# map from resource name to a plural name
# needed only for those which can't be constructed as name + 's'
@@ -44,7 +47,8 @@
'security_groups': 'security_groups',
'security_group_rules': 'security_group_rules',
'ikepolicy': 'ikepolicies',
- 'quotas': 'quotas'
+ 'quotas': 'quotas',
+ 'firewall_policy': 'firewall_policies'
}
diff --git a/tempest/services/network/xml/network_client.py b/tempest/services/network/xml/network_client.py
index ab1d3a3..50a1954 100644
--- a/tempest/services/network/xml/network_client.py
+++ b/tempest/services/network/xml/network_client.py
@@ -14,7 +14,7 @@
import xml.etree.ElementTree as ET
from tempest.common import rest_client
-from tempest.services.compute.xml import common
+from tempest.common import xml_utils as common
from tempest.services.network import network_client_base as client_base
@@ -250,6 +250,45 @@
body = _root_tag_fetcher_and_xml_to_json_parse(body)
return resp, body
+ def add_dhcp_agent_to_network(self, agent_id, network_id):
+ uri = '%s/agents/%s/dhcp-networks' % (self.uri_prefix, agent_id)
+ network = common.Element("network_id", network_id)
+ resp, body = self.post(uri, str(common.Document(network)))
+ body = _root_tag_fetcher_and_xml_to_json_parse(body)
+ return resp, body
+
+ def create_vpnservice(self, subnet_id, router_id, **kwargs):
+ uri = '%s/vpn/vpnservices' % (self.uri_prefix)
+ vpnservice = common.Element("vpnservice")
+ p1 = common.Element("subnet_id", subnet_id)
+ p2 = common.Element("router_id", router_id)
+ vpnservice.append(p1)
+ vpnservice.append(p2)
+ common.deep_dict_to_xml(vpnservice, kwargs)
+ resp, body = self.post(uri, str(common.Document(vpnservice)))
+ body = _root_tag_fetcher_and_xml_to_json_parse(body)
+ return resp, body
+
+ def create_ikepolicy(self, name, **kwargs):
+ uri = '%s/vpn/ikepolicies' % (self.uri_prefix)
+ ikepolicy = common.Element("ikepolicy")
+ p1 = common.Element("name", name)
+ ikepolicy.append(p1)
+ common.deep_dict_to_xml(ikepolicy, kwargs)
+ resp, body = self.post(uri, str(common.Document(ikepolicy)))
+ body = _root_tag_fetcher_and_xml_to_json_parse(body)
+ return resp, body
+
+ def create_ipsecpolicy(self, name, **kwargs):
+ uri = '%s/vpn/ipsecpolicies' % (self.uri_prefix)
+ ipsecpolicy = common.Element("ipsecpolicy")
+ p1 = common.Element("name", name)
+ ipsecpolicy.append(p1)
+ common.deep_dict_to_xml(ipsecpolicy, kwargs)
+ resp, body = self.post(uri, str(common.Document(ipsecpolicy)))
+ body = _root_tag_fetcher_and_xml_to_json_parse(body)
+ return resp, body
+
def _root_tag_fetcher_and_xml_to_json_parse(xml_returned_body):
body = ET.fromstring(xml_returned_body)
diff --git a/tempest/services/object_storage/account_client.py b/tempest/services/object_storage/account_client.py
index 6e7910e..a0506f2 100644
--- a/tempest/services/object_storage/account_client.py
+++ b/tempest/services/object_storage/account_client.py
@@ -162,11 +162,17 @@
self.service = CONF.object_storage.catalog_type
self.format = 'json'
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
"""A simple HTTP request interface."""
self.http_obj = http.ClosingHttp()
if headers is None:
headers = {}
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers())
+ except (ValueError, TypeError):
+ headers = {}
# Authorize the request
req_url, req_headers, req_body = self.auth_provider.auth_request(
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
index 49f7f49..f3f4eb6 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/services/object_storage/object_client.py
@@ -29,12 +29,16 @@
self.service = CONF.object_storage.catalog_type
- def create_object(self, container, object_name, data, params=None):
+ def create_object(self, container, object_name, data,
+ params=None, metadata=None):
"""Create storage object."""
headers = self.get_headers()
if not data:
headers['content-length'] = '0'
+ if metadata:
+ for key in metadata:
+ headers[str(key)] = metadata[key]
url = "%s/%s" % (str(container), str(object_name))
if params:
url += '?%s' % urllib.urlencode(params)
@@ -73,11 +77,16 @@
resp, body = self.head(url)
return resp, body
- def get_object(self, container, object_name):
+ def get_object(self, container, object_name, metadata=None):
"""Retrieve object's data."""
+ headers = {}
+ if metadata:
+ for key in metadata:
+ headers[str(key)] = metadata[key]
+
url = "{0}/{1}".format(container, object_name)
- resp, body = self.get(url)
+ resp, body = self.get(url, headers=headers)
return resp, body
def copy_object_in_same_container(self, container, src_object_name,
@@ -146,13 +155,19 @@
self.service = CONF.object_storage.catalog_type
self.format = 'json'
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
"""A simple HTTP request interface."""
dscv = CONF.identity.disable_ssl_certificate_validation
self.http_obj = http.ClosingHttp(
disable_ssl_certificate_validation=dscv)
if headers is None:
headers = {}
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers())
+ except (ValueError, TypeError):
+ headers = {}
# Authorize the request
req_url, req_headers, req_body = self.auth_provider.auth_request(
diff --git a/tempest/services/orchestration/json/orchestration_client.py b/tempest/services/orchestration/json/orchestration_client.py
index 113003c..2311bdd 100644
--- a/tempest/services/orchestration/json/orchestration_client.py
+++ b/tempest/services/orchestration/json/orchestration_client.py
@@ -154,7 +154,8 @@
if resource_status == status:
return
if fail_regexp.search(resource_status):
- raise exceptions.StackBuildErrorException(
+ raise exceptions.StackResourceBuildErrorException(
+ resource_name=resource_name,
stack_identifier=stack_identifier,
resource_status=resource_status,
resource_status_reason=body['resource_status_reason'])
diff --git a/tempest/services/telemetry/xml/telemetry_client.py b/tempest/services/telemetry/xml/telemetry_client.py
index 673f98e..3bee8bf 100644
--- a/tempest/services/telemetry/xml/telemetry_client.py
+++ b/tempest/services/telemetry/xml/telemetry_client.py
@@ -16,7 +16,7 @@
from lxml import etree
from tempest.common import rest_client
-from tempest.services.compute.xml import common
+from tempest.common import xml_utils as common
import tempest.services.telemetry.telemetry_client_base as client
diff --git a/tempest/services/volume/json/volumes_client.py b/tempest/services/volume/json/volumes_client.py
index e4d2e8d..b55a037 100644
--- a/tempest/services/volume/json/volumes_client.py
+++ b/tempest/services/volume/json/volumes_client.py
@@ -67,10 +67,10 @@
body = json.loads(body)
return resp, body['volume']
- def create_volume(self, size, **kwargs):
+ def create_volume(self, size=None, **kwargs):
"""
Creates a new Volume.
- size(Required): Size of volume in GB.
+ size: Size of volume in GB.
Following optional keyword arguments are accepted:
display_name: Optional Volume Name.
metadata: A dictionary of values to be used as metadata.
@@ -78,6 +78,10 @@
snapshot_id: When specified the volume is created from this snapshot
imageRef: When specified the volume is created from this image
"""
+ # for bug #1293885:
+ # If no size specified, read volume size from CONF
+ if size is None:
+ size = CONF.volume.volume_size
post_body = {'size': size}
post_body.update(kwargs)
post_body = json.dumps({'volume': post_body})
diff --git a/tempest/services/volume/v2/json/volumes_client.py b/tempest/services/volume/v2/json/volumes_client.py
index 5bfa75f..df20a2a 100644
--- a/tempest/services/volume/v2/json/volumes_client.py
+++ b/tempest/services/volume/v2/json/volumes_client.py
@@ -68,10 +68,10 @@
body = json.loads(body)
return resp, body['volume']
- def create_volume(self, size, **kwargs):
+ def create_volume(self, size=None, **kwargs):
"""
Creates a new Volume.
- size(Required): Size of volume in GB.
+ size: Size of volume in GB.
Following optional keyword arguments are accepted:
name: Optional Volume Name.
metadata: A dictionary of values to be used as metadata.
@@ -79,6 +79,10 @@
snapshot_id: When specified the volume is created from this snapshot
imageRef: When specified the volume is created from this image
"""
+ # for bug #1293885:
+ # If no size specified, read volume size from CONF
+ if size is None:
+ size = CONF.volume.volume_size
post_body = {'size': size}
post_body.update(kwargs)
post_body = json.dumps({'volume': post_body})
diff --git a/tempest/services/volume/v2/xml/volumes_client.py b/tempest/services/volume/v2/xml/volumes_client.py
index 0b8f47c..1fdaf19 100644
--- a/tempest/services/volume/v2/xml/volumes_client.py
+++ b/tempest/services/volume/v2/xml/volumes_client.py
@@ -19,9 +19,9 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils as common
from tempest import config
from tempest import exceptions
-from tempest.services.compute.xml import common
CONF = config.CONF
@@ -117,10 +117,10 @@
body = self._check_if_bootable(body)
return resp, body
- def create_volume(self, size, **kwargs):
+ def create_volume(self, size=None, **kwargs):
"""Creates a new Volume.
- :param size: Size of volume in GB. (Required)
+ :param size: Size of volume in GB.
:param name: Optional Volume Name.
:param metadata: An optional dictionary of values for metadata.
:param volume_type: Optional Name of volume_type for the volume
@@ -129,6 +129,10 @@
:param imageRef: When specified the volume is created from this
image
"""
+ # for bug #1293885:
+ # If no size specified, read volume size from CONF
+ if size is None:
+ size = CONF.volume.volume_size
# NOTE(afazekas): it should use a volume namespace
volume = common.Element("volume", xmlns=common.XMLNS_11, size=size)
diff --git a/tempest/services/volume/xml/admin/volume_hosts_client.py b/tempest/services/volume/xml/admin/volume_hosts_client.py
index e34b9f0..967c7c2 100644
--- a/tempest/services/volume/xml/admin/volume_hosts_client.py
+++ b/tempest/services/volume/xml/admin/volume_hosts_client.py
@@ -18,8 +18,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils as common
from tempest import config
-from tempest.services.compute.xml import common
CONF = config.CONF
diff --git a/tempest/services/volume/xml/admin/volume_quotas_client.py b/tempest/services/volume/xml/admin/volume_quotas_client.py
index d2eac34..710fb3a 100644
--- a/tempest/services/volume/xml/admin/volume_quotas_client.py
+++ b/tempest/services/volume/xml/admin/volume_quotas_client.py
@@ -17,8 +17,8 @@
from ast import literal_eval
from lxml import etree
+from tempest.common import xml_utils as xml
from tempest import config
-from tempest.services.compute.xml import common as xml
from tempest.services.volume.json.admin import volume_quotas_client
CONF = config.CONF
diff --git a/tempest/services/volume/xml/admin/volume_types_client.py b/tempest/services/volume/xml/admin/volume_types_client.py
index 1fa3e73..90897ee 100644
--- a/tempest/services/volume/xml/admin/volume_types_client.py
+++ b/tempest/services/volume/xml/admin/volume_types_client.py
@@ -18,9 +18,9 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils as common
from tempest import config
from tempest import exceptions
-from tempest.services.compute.xml import common
CONF = config.CONF
diff --git a/tempest/services/volume/xml/extensions_client.py b/tempest/services/volume/xml/extensions_client.py
index 4861733..2986fcd 100644
--- a/tempest/services/volume/xml/extensions_client.py
+++ b/tempest/services/volume/xml/extensions_client.py
@@ -16,8 +16,8 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils as common
from tempest import config
-from tempest.services.compute.xml import common
CONF = config.CONF
diff --git a/tempest/services/volume/xml/snapshots_client.py b/tempest/services/volume/xml/snapshots_client.py
index 9ad86d2..4b1ba25 100644
--- a/tempest/services/volume/xml/snapshots_client.py
+++ b/tempest/services/volume/xml/snapshots_client.py
@@ -16,10 +16,10 @@
from lxml import etree
from tempest.common import rest_client
+from tempest.common import xml_utils as common
from tempest import config
from tempest import exceptions
from tempest.openstack.common import log as logging
-from tempest.services.compute.xml import common
CONF = config.CONF
diff --git a/tempest/services/volume/xml/volumes_client.py b/tempest/services/volume/xml/volumes_client.py
index 8e886ce..65bc321 100644
--- a/tempest/services/volume/xml/volumes_client.py
+++ b/tempest/services/volume/xml/volumes_client.py
@@ -20,9 +20,9 @@
from xml.sax import saxutils
from tempest.common import rest_client
+from tempest.common import xml_utils as common
from tempest import config
from tempest import exceptions
-from tempest.services.compute.xml import common
CONF = config.CONF
@@ -118,10 +118,10 @@
body = self._check_if_bootable(body)
return resp, body
- def create_volume(self, size, **kwargs):
+ def create_volume(self, size=None, **kwargs):
"""Creates a new Volume.
- :param size: Size of volume in GB. (Required)
+ :param size: Size of volume in GB.
:param display_name: Optional Volume Name.
:param metadata: An optional dictionary of values for metadata.
:param volume_type: Optional Name of volume_type for the volume
@@ -130,6 +130,10 @@
:param imageRef: When specified the volume is created from this
image
"""
+ # for bug #1293885:
+ # If no size specified, read volume size from CONF
+ if size is None:
+ size = CONF.volume.volume_size
# NOTE(afazekas): it should use a volume namespace
volume = common.Element("volume", xmlns=common.XMLNS_11, size=size)
diff --git a/tempest/test.py b/tempest/test.py
index abf42c0..8df405c 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -17,6 +17,7 @@
import functools
import json
import os
+import re
import sys
import time
import urllib
@@ -93,6 +94,7 @@
service_list = {
'compute': CONF.service_available.nova,
'image': CONF.service_available.glance,
+ 'baremetal': CONF.service_available.ironic,
'volume': CONF.service_available.cinder,
'orchestration': CONF.service_available.heat,
# NOTE(mtreinish) nova-network will provide networking functionality
@@ -581,6 +583,24 @@
return None
+def SimpleNegativeAutoTest(klass):
+ """
+ This decorator registers a test function on basis of the class name.
+ """
+ @attr(type=['negative', 'gate'])
+ def generic_test(self):
+ self.execute(self._schema_file)
+
+ cn = klass.__name__
+ cn = cn.replace('JSON', '')
+ cn = cn.replace('Test', '')
+ # NOTE(mkoderer): replaces uppercase chars inside the class name with '_'
+ lower_cn = re.sub('(?<!^)(?=[A-Z])', '_', cn).lower()
+ func_name = 'test_%s' % lower_cn
+ setattr(klass, func_name, generic_test)
+ return klass
+
+
def call_until_true(func, duration, sleep_for):
"""
Call the given function until it returns True (and return True) or
diff --git a/tempest/tests/cli/__init__.py b/tempest/tests/cli/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/tests/cli/__init__.py
diff --git a/tempest/tests/cli/test_output_parser.py b/tempest/tests/cli/test_output_parser.py
new file mode 100644
index 0000000..7ad270c
--- /dev/null
+++ b/tempest/tests/cli/test_output_parser.py
@@ -0,0 +1,177 @@
+# 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.cli import output_parser
+from tempest import exceptions
+from tempest.tests import base
+
+
+class TestOutputParser(base.TestCase):
+ OUTPUT_LINES = """
++----+------+---------+
+| ID | Name | Status |
++----+------+---------+
+| 11 | foo | BUILD |
+| 21 | bar | ERROR |
+| 31 | bee | None |
++----+------+---------+
+"""
+ OUTPUT_LINES2 = """
++----+-------+---------+
+| ID | Name2 | Status2 |
++----+-------+---------+
+| 41 | aaa | SSSSS |
+| 51 | bbb | TTTTT |
+| 61 | ccc | AAAAA |
++----+-------+---------+
+"""
+
+ EXPECTED_TABLE = {'headers': ['ID', 'Name', 'Status'],
+ 'values': [['11', 'foo', 'BUILD'],
+ ['21', 'bar', 'ERROR'],
+ ['31', 'bee', 'None']]}
+ EXPECTED_TABLE2 = {'headers': ['ID', 'Name2', 'Status2'],
+ 'values': [['41', 'aaa', 'SSSSS'],
+ ['51', 'bbb', 'TTTTT'],
+ ['61', 'ccc', 'AAAAA']]}
+
+ def test_table_with_normal_values(self):
+ actual = output_parser.table(self.OUTPUT_LINES)
+ self.assertIsInstance(actual, dict)
+ self.assertEqual(self.EXPECTED_TABLE, actual)
+
+ def test_table_with_list(self):
+ output_lines = self.OUTPUT_LINES.split('\n')
+ actual = output_parser.table(output_lines)
+ self.assertIsInstance(actual, dict)
+ self.assertEqual(self.EXPECTED_TABLE, actual)
+
+ def test_table_with_invalid_line(self):
+ output_lines = self.OUTPUT_LINES + "aaaa"
+ actual = output_parser.table(output_lines)
+ self.assertIsInstance(actual, dict)
+ self.assertEqual(self.EXPECTED_TABLE, actual)
+
+ def test_tables_with_normal_values(self):
+ output_lines = 'test' + self.OUTPUT_LINES +\
+ 'test2' + self.OUTPUT_LINES2
+ expected = [{'headers': self.EXPECTED_TABLE['headers'],
+ 'label': 'test',
+ 'values': self.EXPECTED_TABLE['values']},
+ {'headers': self.EXPECTED_TABLE2['headers'],
+ 'label': 'test2',
+ 'values': self.EXPECTED_TABLE2['values']}]
+ actual = output_parser.tables(output_lines)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
+
+ def test_tables_with_invalid_values(self):
+ output_lines = 'test' + self.OUTPUT_LINES +\
+ 'test2' + self.OUTPUT_LINES2 + '\n'
+ expected = [{'headers': self.EXPECTED_TABLE['headers'],
+ 'label': 'test',
+ 'values': self.EXPECTED_TABLE['values']},
+ {'headers': self.EXPECTED_TABLE2['headers'],
+ 'label': 'test2',
+ 'values': self.EXPECTED_TABLE2['values']}]
+ actual = output_parser.tables(output_lines)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
+
+ def test_tables_with_invalid_line(self):
+ output_lines = 'test' + self.OUTPUT_LINES +\
+ 'test2' + self.OUTPUT_LINES2 +\
+ '+----+-------+---------+'
+ expected = [{'headers': self.EXPECTED_TABLE['headers'],
+ 'label': 'test',
+ 'values': self.EXPECTED_TABLE['values']},
+ {'headers': self.EXPECTED_TABLE2['headers'],
+ 'label': 'test2',
+ 'values': self.EXPECTED_TABLE2['values']}]
+
+ actual = output_parser.tables(output_lines)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
+
+ LISTING_OUTPUT = """
++----+
+| ID |
++----+
+| 11 |
+| 21 |
+| 31 |
++----+
+"""
+
+ def test_listing(self):
+ expected = [{'ID': '11'}, {'ID': '21'}, {'ID': '31'}]
+ actual = output_parser.listing(self.LISTING_OUTPUT)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
+
+ def test_details_multiple_with_invalid_line(self):
+ self.assertRaises(exceptions.InvalidStructure,
+ output_parser.details_multiple,
+ self.OUTPUT_LINES)
+
+ DETAILS_LINES1 = """First Table
++----------+--------+
+| Property | Value |
++----------+--------+
+| foo | BUILD |
+| bar | ERROR |
+| bee | None |
++----------+--------+
+"""
+ DETAILS_LINES2 = """Second Table
++----------+--------+
+| Property | Value |
++----------+--------+
+| aaa | VVVVV |
+| bbb | WWWWW |
+| ccc | XXXXX |
++----------+--------+
+"""
+
+ def test_details_with_normal_line_label_false(self):
+ expected = {'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'}
+ actual = output_parser.details(self.DETAILS_LINES1)
+ self.assertEqual(expected, actual)
+
+ def test_details_with_normal_line_label_true(self):
+ expected = {'__label': 'First Table',
+ 'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'}
+ actual = output_parser.details(self.DETAILS_LINES1, with_label=True)
+ self.assertEqual(expected, actual)
+
+ def test_details_multiple_with_normal_line_label_false(self):
+ expected = [{'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'},
+ {'aaa': 'VVVVV', 'bbb': 'WWWWW', 'ccc': 'XXXXX'}]
+ actual = output_parser.details_multiple(self.DETAILS_LINES1 +
+ self.DETAILS_LINES2)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
+
+ def test_details_multiple_with_normal_line_label_true(self):
+ expected = [{'__label': 'First Table',
+ 'foo': 'BUILD', 'bar': 'ERROR', 'bee': 'None'},
+ {'__label': 'Second Table',
+ 'aaa': 'VVVVV', 'bbb': 'WWWWW', 'ccc': 'XXXXX'}]
+ actual = output_parser.details_multiple(self.DETAILS_LINES1 +
+ self.DETAILS_LINES2,
+ with_label=True)
+ self.assertIsInstance(actual, list)
+ self.assertEqual(expected, actual)
diff --git a/tempest/tests/common/test_debug.py b/tempest/tests/common/test_debug.py
new file mode 100644
index 0000000..8a880f2
--- /dev/null
+++ b/tempest/tests/common/test_debug.py
@@ -0,0 +1,122 @@
+# 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.
+
+import mock
+
+from tempest.common import debug
+from tempest import config
+from tempest.openstack.common.fixture import mockpatch
+from tempest import test
+from tempest.tests import base
+from tempest.tests import fake_config
+
+
+class TestDebug(base.TestCase):
+
+ def setUp(self):
+ super(TestDebug, self).setUp()
+ self.useFixture(fake_config.ConfigFixture())
+ self.stubs.Set(config, 'TempestConfigPrivate', fake_config.FakePrivate)
+
+ common_pre = 'tempest.common.commands'
+ self.ip_addr_raw_mock = self.patch(common_pre + '.ip_addr_raw')
+ self.ip_route_raw_mock = self.patch(common_pre + '.ip_route_raw')
+ self.iptables_raw_mock = self.patch(common_pre + '.iptables_raw')
+ self.ip_ns_list_mock = self.patch(common_pre + '.ip_ns_list')
+ self.ip_ns_addr_mock = self.patch(common_pre + '.ip_ns_addr')
+ self.ip_ns_route_mock = self.patch(common_pre + '.ip_ns_route')
+ self.iptables_ns_mock = self.patch(common_pre + '.iptables_ns')
+ self.ovs_db_dump_mock = self.patch(common_pre + '.ovs_db_dump')
+
+ self.log_mock = self.patch('tempest.common.debug.LOG')
+
+ def test_log_ip_ns_debug_disabled(self):
+ self.useFixture(mockpatch.PatchObject(test.CONF.debug,
+ 'enable', False))
+ debug.log_ip_ns()
+ self.assertFalse(self.ip_addr_raw_mock.called)
+ self.assertFalse(self.log_mock.info.called)
+
+ def test_log_ip_ns_debug_enabled(self):
+ self.useFixture(mockpatch.PatchObject(test.CONF.debug,
+ 'enable', True))
+
+ self.ip_ns_list_mock.return_value = [1, 2]
+
+ debug.log_ip_ns()
+ self.ip_addr_raw_mock.assert_called_with()
+ self.assertTrue(self.log_mock.info.called)
+ self.ip_route_raw_mock.assert_called_with()
+ self.assertEqual(len(debug.TABLES), self.iptables_raw_mock.call_count)
+ for table in debug.TABLES:
+ self.assertIn(mock.call(table),
+ self.iptables_raw_mock.call_args_list)
+
+ self.ip_ns_list_mock.assert_called_with()
+ self.assertEqual(len(self.ip_ns_list_mock.return_value),
+ self.ip_ns_addr_mock.call_count)
+ self.assertEqual(len(self.ip_ns_list_mock.return_value),
+ self.ip_ns_route_mock.call_count)
+ for ns in self.ip_ns_list_mock.return_value:
+ self.assertIn(mock.call(ns),
+ self.ip_ns_addr_mock.call_args_list)
+ self.assertIn(mock.call(ns),
+ self.ip_ns_route_mock.call_args_list)
+
+ self.assertEqual(len(debug.TABLES) *
+ len(self.ip_ns_list_mock.return_value),
+ self.iptables_ns_mock.call_count)
+ for ns in self.ip_ns_list_mock.return_value:
+ for table in debug.TABLES:
+ self.assertIn(mock.call(ns, table),
+ self.iptables_ns_mock.call_args_list)
+
+ def test_log_ovs_db_debug_disabled(self):
+ self.useFixture(mockpatch.PatchObject(test.CONF.debug,
+ 'enable', False))
+ self.useFixture(mockpatch.PatchObject(test.CONF.service_available,
+ 'neutron', False))
+ debug.log_ovs_db()
+ self.assertFalse(self.ovs_db_dump_mock.called)
+
+ self.useFixture(mockpatch.PatchObject(test.CONF.debug,
+ 'enable', True))
+ self.useFixture(mockpatch.PatchObject(test.CONF.service_available,
+ 'neutron', False))
+ debug.log_ovs_db()
+ self.assertFalse(self.ovs_db_dump_mock.called)
+
+ self.useFixture(mockpatch.PatchObject(test.CONF.debug,
+ 'enable', False))
+ self.useFixture(mockpatch.PatchObject(test.CONF.service_available,
+ 'neutron', True))
+ debug.log_ovs_db()
+ self.assertFalse(self.ovs_db_dump_mock.called)
+
+ def test_log_ovs_db_debug_enabled(self):
+ self.useFixture(mockpatch.PatchObject(test.CONF.debug,
+ 'enable', True))
+ self.useFixture(mockpatch.PatchObject(test.CONF.service_available,
+ 'neutron', True))
+ debug.log_ovs_db()
+ self.ovs_db_dump_mock.assert_called_with()
+
+ def test_log_net_debug(self):
+ self.log_ip_ns_mock = self.patch('tempest.common.debug.log_ip_ns')
+ self.log_ovs_db_mock = self.patch('tempest.common.debug.log_ovs_db')
+
+ debug.log_net_debug()
+ self.log_ip_ns_mock.assert_called_with()
+ self.log_ovs_db_mock.assert_called_with()
diff --git a/tempest/tests/fake_config.py b/tempest/tests/fake_config.py
index 8a8ebb0..4676cbd 100644
--- a/tempest/tests/fake_config.py
+++ b/tempest/tests/fake_config.py
@@ -12,16 +12,21 @@
# License for the specific language governing permissions and limitations
# under the License.
+import os
+
from oslo.config import cfg
from tempest import config
from tempest.openstack.common.fixture import config as conf_fixture
+from tempest.openstack.common import importutils
class ConfigFixture(conf_fixture.Config):
def __init__(self):
config.register_opts()
+ # Register locking options
+ importutils.import_module('tempest.openstack.common.lockutils')
super(ConfigFixture, self).__init__()
def setUp(self):
@@ -36,6 +41,10 @@
group='identity')
self.conf.set_default('neutron', True, group='service_available')
self.conf.set_default('heat', True, group='service_available')
+ if not os.path.exists(str(os.environ.get('OS_TEST_LOCK_PATH'))):
+ os.mkdir(str(os.environ.get('OS_TEST_LOCK_PATH')))
+ self.conf.set_default('lock_path',
+ str(os.environ.get('OS_TEST_LOCK_PATH')))
class FakePrivate(config.TempestConfigPrivate):
diff --git a/tempest/tests/negative/test_generate_json.py b/tempest/tests/negative/test_generate_json.py
deleted file mode 100644
index e09fcdf..0000000
--- a/tempest/tests/negative/test_generate_json.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright 2014 Deutsche Telekom AG
-# 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.generator import negative_generator
-import tempest.test
-
-
-class TestNegativeGenerator(tempest.test.BaseTestCase):
-
- fake_input_str = {"type": "string",
- "minLength": 2,
- "maxLength": 8,
- 'results': {'gen_number': 404}}
-
- fake_input_int = {"type": "integer",
- "maximum": 255,
- "minimum": 1}
-
- fake_input_obj = {"type": "object",
- "properties": {"minRam": {"type": "integer"},
- "diskName": {"type": "string"},
- "maxRam": {"type": "integer", }
- }
- }
-
- def setUp(self):
- super(TestNegativeGenerator, self).setUp()
- self.negative = negative_generator.NegativeTestGenerator()
-
- def _validate_result(self, data):
- self.assertTrue(isinstance(data, list))
- for t in data:
- self.assertTrue(isinstance(t, tuple))
-
- def test_generate_invalid_string(self):
- result = self.negative.generate(self.fake_input_str)
- self._validate_result(result)
-
- def test_generate_invalid_integer(self):
- result = self.negative.generate(self.fake_input_int)
- self._validate_result(result)
-
- def test_generate_invalid_obj(self):
- result = self.negative.generate(self.fake_input_obj)
- self._validate_result(result)
diff --git a/tempest/tests/negative/test_negative_generators.py b/tempest/tests/negative/test_negative_generators.py
index f2ed999..c77faca 100644
--- a/tempest/tests/negative/test_negative_generators.py
+++ b/tempest/tests/negative/test_negative_generators.py
@@ -16,7 +16,9 @@
import jsonschema
import mock
-import tempest.common.generator.base_generator as base_generator
+from tempest.common.generator import base_generator
+from tempest.common.generator import negative_generator
+from tempest.common.generator import valid_generator
from tempest.tests import base
@@ -79,3 +81,73 @@
self.assertRaises(jsonschema.SchemaError,
self.generator.validate_schema,
self.invalid_json_schema_desc)
+
+
+class BaseNegativeGenerator(object):
+ types = ['string', 'integer', 'object']
+
+ fake_input_str = {"type": "string",
+ "minLength": 2,
+ "maxLength": 8,
+ 'results': {'gen_int': 404}}
+
+ fake_input_int = {"type": "integer",
+ "maximum": 255,
+ "minimum": 1}
+
+ fake_input_obj = {"type": "object",
+ "properties": {"minRam": {"type": "integer"},
+ "diskName": {"type": "string"},
+ "maxRam": {"type": "integer", }
+ }
+ }
+
+ unkown_type_schema = {
+ "type": "not_defined"
+ }
+
+ def _validate_result(self, data):
+ self.assertTrue(isinstance(data, list))
+ for t in data:
+ self.assertIsInstance(t, tuple)
+ self.assertEqual(3, len(t))
+ self.assertIsInstance(t[0], str)
+
+ def test_generate_string(self):
+ result = self.generator.generate(self.fake_input_str)
+ self._validate_result(result)
+
+ def test_generate_integer(self):
+ result = self.generator.generate(self.fake_input_int)
+ self._validate_result(result)
+
+ def test_generate_obj(self):
+ result = self.generator.generate(self.fake_input_obj)
+ self._validate_result(result)
+
+ def test_generator_mandatory_functions(self):
+ for data_type in self.types:
+ self.assertIn(data_type, self.generator.types_dict)
+
+ def test_generate_with_unknown_type(self):
+ self.assertRaises(TypeError, self.generator.generate,
+ self.unkown_type_schema)
+
+
+class TestNegativeValidGenerator(base.TestCase, BaseNegativeGenerator):
+ def setUp(self):
+ super(TestNegativeValidGenerator, self).setUp()
+ self.generator = valid_generator.ValidTestGenerator()
+
+ def test_generate_valid(self):
+ result = self.generator.generate_valid(self.fake_input_obj)
+ self.assertIn("minRam", result)
+ self.assertIsInstance(result["minRam"], int)
+ self.assertIn("diskName", result)
+ self.assertIsInstance(result["diskName"], str)
+
+
+class TestNegativeNegativeGenerator(base.TestCase, BaseNegativeGenerator):
+ def setUp(self):
+ super(TestNegativeNegativeGenerator, self).setUp()
+ self.generator = negative_generator.NegativeTestGenerator()
diff --git a/tempest/tests/test_commands.py b/tempest/tests/test_commands.py
new file mode 100644
index 0000000..bdb9269
--- /dev/null
+++ b/tempest/tests/test_commands.py
@@ -0,0 +1,87 @@
+# 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.
+
+import mock
+import subprocess
+
+from tempest.common import commands
+from tempest.tests import base
+
+
+class TestCommands(base.TestCase):
+
+ def setUp(self):
+ super(TestCommands, self).setUp()
+ self.subprocess_args = {'stdout': subprocess.PIPE,
+ 'stderr': subprocess.STDOUT}
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_addr_raw(self, mock):
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'a']
+ commands.ip_addr_raw()
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_route_raw(self, mock):
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'r']
+ commands.ip_route_raw()
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_ns_raw(self, mock):
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'netns', 'list']
+ commands.ip_ns_raw()
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_iptables_raw(self, mock):
+ table = 'filter'
+ expected = ['/usr/bin/sudo', '-n', 'iptables', '-v', '-S', '-t',
+ '%s' % table]
+ commands.iptables_raw(table)
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_ns_list(self, mock):
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'netns', 'list']
+ commands.ip_ns_list()
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_ns_addr(self, mock):
+ ns_list = commands.ip_ns_list()
+ for ns in ns_list:
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'netns', 'exec', ns,
+ 'ip', 'a']
+ commands.ip_ns_addr(ns)
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_ns_route(self, mock):
+ ns_list = commands.ip_ns_list()
+ for ns in ns_list:
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'netns', 'exec', ns,
+ 'ip', 'r']
+ commands.ip_ns_route(ns)
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_iptables_ns(self, mock):
+ table = 'filter'
+ ns_list = commands.ip_ns_list()
+ for ns in ns_list:
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'netns', 'exec', ns,
+ 'iptables', '-v', '-S', '-t', table]
+ commands.iptables_ns(ns, table)
+ mock.assert_called_once_with(expected, **self.subprocess_args)
diff --git a/tempest/tests/test_compute_xml_common.py b/tempest/tests/test_compute_xml_common.py
index bfa6a10..1561931 100644
--- a/tempest/tests/test_compute_xml_common.py
+++ b/tempest/tests/test_compute_xml_common.py
@@ -13,7 +13,7 @@
# under the License.
from lxml import etree
-from tempest.services.compute.xml import common
+from tempest.common import xml_utils as common
from tempest.tests import base
diff --git a/tempest/tests/test_decorators.py b/tempest/tests/test_decorators.py
index ebf0ca0..804204a 100644
--- a/tempest/tests/test_decorators.py
+++ b/tempest/tests/test_decorators.py
@@ -13,6 +13,7 @@
# under the License.
+import mock
import testtools
from oslo.config import cfg
@@ -232,3 +233,19 @@
self._test_requires_ext_helper,
extension='enabled_ext',
service='bad_service')
+
+
+class TestSimpleNegativeDecorator(BaseDecoratorsTest):
+ @test.SimpleNegativeAutoTest
+ class FakeNegativeJSONTest(test.NegativeAutoTest):
+ _schema_file = 'fake/schemas/file.json'
+
+ def test_testfunc_exist(self):
+ self.assertIn("test_fake_negative", dir(self.FakeNegativeJSONTest))
+
+ @mock.patch('tempest.test.NegativeAutoTest.execute')
+ def test_testfunc_calls_execute(self, mock):
+ obj = self.FakeNegativeJSONTest("test_fake_negative")
+ self.assertIn("test_fake_negative", dir(obj))
+ obj.test_fake_negative()
+ mock.assert_called_once_with(self.FakeNegativeJSONTest._schema_file)
diff --git a/tempest/tests/test_rest_client.py b/tempest/tests/test_rest_client.py
index 0677aa0..64ad3bc 100644
--- a/tempest/tests/test_rest_client.py
+++ b/tempest/tests/test_rest_client.py
@@ -16,10 +16,10 @@
import json
from tempest.common import rest_client
+from tempest.common import xml_utils as xml
from tempest import config
from tempest import exceptions
from tempest.openstack.common.fixture import mockpatch
-from tempest.services.compute.xml import common as xml
from tempest.tests import base
from tempest.tests import fake_auth_provider
from tempest.tests import fake_config
@@ -139,6 +139,102 @@
self._verify_headers(resp)
+class TestRestClientUpdateHeaders(BaseRestClientTestClass):
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2()
+ super(TestRestClientUpdateHeaders, self).setUp()
+ self.useFixture(mockpatch.PatchObject(self.rest_client,
+ '_error_checker'))
+ self.headers = {'X-Configuration-Session': 'session_id'}
+
+ def test_post_update_headers(self):
+ __, return_dict = self.rest_client.post(self.url, {},
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_get_update_headers(self):
+ __, return_dict = self.rest_client.get(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_delete_update_headers(self):
+ __, return_dict = self.rest_client.delete(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_patch_update_headers(self):
+ __, return_dict = self.rest_client.patch(self.url, {},
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_put_update_headers(self):
+ __, return_dict = self.rest_client.put(self.url, {},
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_head_update_headers(self):
+ self.useFixture(mockpatch.PatchObject(self.rest_client,
+ 'response_checker'))
+
+ __, return_dict = self.rest_client.head(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_copy_update_headers(self):
+ __, return_dict = self.rest_client.copy(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+
class TestRestClientHeadersXML(TestRestClientHeadersJSON):
TYPE = "xml"
@@ -384,3 +480,63 @@
self.assertRaises(NotImplementedError,
self.rest_client.wait_for_resource_deletion,
'1234')
+
+
+class TestNegativeRestClient(BaseRestClientTestClass):
+
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2()
+ super(TestNegativeRestClient, self).setUp()
+ self.negative_rest_client = rest_client.NegativeRestClient(
+ fake_auth_provider.FakeAuthProvider())
+ self.useFixture(mockpatch.PatchObject(self.negative_rest_client,
+ '_log_request'))
+
+ def test_post(self):
+ __, return_dict = self.negative_rest_client.send_request('POST',
+ self.url,
+ [], {})
+ self.assertEqual('POST', return_dict['method'])
+
+ def test_get(self):
+ __, return_dict = self.negative_rest_client.send_request('GET',
+ self.url,
+ [])
+ self.assertEqual('GET', return_dict['method'])
+
+ def test_delete(self):
+ __, return_dict = self.negative_rest_client.send_request('DELETE',
+ self.url,
+ [])
+ self.assertEqual('DELETE', return_dict['method'])
+
+ def test_patch(self):
+ __, return_dict = self.negative_rest_client.send_request('PATCH',
+ self.url,
+ [], {})
+ self.assertEqual('PATCH', return_dict['method'])
+
+ def test_put(self):
+ __, return_dict = self.negative_rest_client.send_request('PUT',
+ self.url,
+ [], {})
+ self.assertEqual('PUT', return_dict['method'])
+
+ def test_head(self):
+ self.useFixture(mockpatch.PatchObject(self.negative_rest_client,
+ 'response_checker'))
+ __, return_dict = self.negative_rest_client.send_request('HEAD',
+ self.url,
+ [])
+ self.assertEqual('HEAD', return_dict['method'])
+
+ def test_copy(self):
+ __, return_dict = self.negative_rest_client.send_request('COPY',
+ self.url,
+ [])
+ self.assertEqual('COPY', return_dict['method'])
+
+ def test_other(self):
+ self.assertRaises(AssertionError,
+ self.negative_rest_client.send_request,
+ 'OTHER', self.url, [])
diff --git a/tempest/tests/test_tenant_isolation.py b/tempest/tests/test_tenant_isolation.py
index 2e50cfd..ae2e57d 100644
--- a/tempest/tests/test_tenant_isolation.py
+++ b/tempest/tests/test_tenant_isolation.py
@@ -19,6 +19,7 @@
from tempest.common import isolated_creds
from tempest import config
+from tempest import exceptions
from tempest.openstack.common.fixture import mockpatch
from tempest.services.identity.json import identity_client as json_iden_client
from tempest.services.identity.xml import identity_client as xml_iden_client
@@ -334,3 +335,140 @@
self.assertIn('1234', args)
self.assertIn('12345', args)
self.assertIn('123456', args)
+
+ @patch('tempest.common.rest_client.RestClient')
+ def test_network_alt_creation(self, MockRestClient):
+ iso_creds = isolated_creds.IsolatedCreds('test class',
+ password='fake_password')
+ self._mock_user_create('1234', 'fake_alt_user')
+ self._mock_tenant_create('1234', 'fake_alt_tenant')
+ self._mock_network_create(iso_creds, '1234', 'fake_alt_net')
+ self._mock_subnet_create(iso_creds, '1234', 'fake_alt_subnet')
+ self._mock_router_create('1234', 'fake_alt_router')
+ router_interface_mock = self.patch(
+ 'tempest.services.network.json.network_client.NetworkClientJSON.'
+ 'add_router_interface_with_subnet_id')
+ username, tenant_name, password = iso_creds.get_alt_creds()
+ router_interface_mock.called_once_with('1234', '1234')
+ network = iso_creds.get_alt_network()
+ subnet = iso_creds.get_alt_subnet()
+ router = iso_creds.get_alt_router()
+ self.assertEqual(network['id'], '1234')
+ self.assertEqual(network['name'], 'fake_alt_net')
+ self.assertEqual(subnet['id'], '1234')
+ self.assertEqual(subnet['name'], 'fake_alt_subnet')
+ self.assertEqual(router['id'], '1234')
+ self.assertEqual(router['name'], 'fake_alt_router')
+
+ @patch('tempest.common.rest_client.RestClient')
+ def test_network_admin_creation(self, MockRestClient):
+ iso_creds = isolated_creds.IsolatedCreds('test class',
+ password='fake_password')
+ self._mock_user_create('1234', 'fake_admin_user')
+ self._mock_tenant_create('1234', 'fake_admin_tenant')
+ self._mock_network_create(iso_creds, '1234', 'fake_admin_net')
+ self._mock_subnet_create(iso_creds, '1234', 'fake_admin_subnet')
+ self._mock_router_create('1234', 'fake_admin_router')
+ router_interface_mock = self.patch(
+ 'tempest.services.network.json.network_client.NetworkClientJSON.'
+ 'add_router_interface_with_subnet_id')
+ self.useFixture(mockpatch.PatchObject(
+ json_iden_client.IdentityClientJSON,
+ 'list_roles',
+ return_value=({'status': 200},
+ [{'id': '123456', 'name': 'admin'}])))
+ with patch.object(json_iden_client.IdentityClientJSON,
+ 'assign_user_role'):
+ username, tenant_name, password = iso_creds.get_admin_creds()
+ router_interface_mock.called_once_with('1234', '1234')
+ network = iso_creds.get_admin_network()
+ subnet = iso_creds.get_admin_subnet()
+ router = iso_creds.get_admin_router()
+ self.assertEqual(network['id'], '1234')
+ self.assertEqual(network['name'], 'fake_admin_net')
+ self.assertEqual(subnet['id'], '1234')
+ self.assertEqual(subnet['name'], 'fake_admin_subnet')
+ self.assertEqual(router['id'], '1234')
+ self.assertEqual(router['name'], 'fake_admin_router')
+
+ @patch('tempest.common.rest_client.RestClient')
+ def test_no_network_resources(self, MockRestClient):
+ net_dict = {
+ 'network': False,
+ 'router': False,
+ 'subnet': False,
+ 'dhcp': False,
+ }
+ iso_creds = isolated_creds.IsolatedCreds('test class',
+ password='fake_password',
+ network_resources=net_dict)
+ self._mock_user_create('1234', 'fake_prim_user')
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ net = patch.object(iso_creds.network_admin_client,
+ 'delete_network')
+ net_mock = net.start()
+ subnet = patch.object(iso_creds.network_admin_client,
+ 'delete_subnet')
+ subnet_mock = subnet.start()
+ router = patch.object(iso_creds.network_admin_client,
+ 'delete_router')
+ router_mock = router.start()
+
+ username, tenant_name, password = iso_creds.get_primary_creds()
+ self.assertEqual(net_mock.mock_calls, [])
+ self.assertEqual(subnet_mock.mock_calls, [])
+ self.assertEqual(router_mock.mock_calls, [])
+ network = iso_creds.get_primary_network()
+ subnet = iso_creds.get_primary_subnet()
+ router = iso_creds.get_primary_router()
+ self.assertIsNone(network)
+ self.assertIsNone(subnet)
+ self.assertIsNone(router)
+
+ @patch('tempest.common.rest_client.RestClient')
+ def test_router_without_network(self, MockRestClient):
+ net_dict = {
+ 'network': False,
+ 'router': True,
+ 'subnet': False,
+ 'dhcp': False,
+ }
+ iso_creds = isolated_creds.IsolatedCreds('test class',
+ password='fake_password',
+ network_resources=net_dict)
+ self._mock_user_create('1234', 'fake_prim_user')
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ self.assertRaises(exceptions.InvalidConfiguration,
+ iso_creds.get_primary_creds)
+
+ @patch('tempest.common.rest_client.RestClient')
+ def test_subnet_without_network(self, MockRestClient):
+ net_dict = {
+ 'network': False,
+ 'router': False,
+ 'subnet': True,
+ 'dhcp': False,
+ }
+ iso_creds = isolated_creds.IsolatedCreds('test class',
+ password='fake_password',
+ network_resources=net_dict)
+ self._mock_user_create('1234', 'fake_prim_user')
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ self.assertRaises(exceptions.InvalidConfiguration,
+ iso_creds.get_primary_creds)
+
+ @patch('tempest.common.rest_client.RestClient')
+ def test_dhcp_without_subnet(self, MockRestClient):
+ net_dict = {
+ 'network': False,
+ 'router': False,
+ 'subnet': False,
+ 'dhcp': True,
+ }
+ iso_creds = isolated_creds.IsolatedCreds('test class',
+ password='fake_password',
+ network_resources=net_dict)
+ self._mock_user_create('1234', 'fake_prim_user')
+ self._mock_tenant_create('1234', 'fake_prim_tenant')
+ self.assertRaises(exceptions.InvalidConfiguration,
+ iso_creds.get_primary_creds)
diff --git a/tempest/thirdparty/boto/test_ec2_instance_run.py b/tempest/thirdparty/boto/test_ec2_instance_run.py
index e6a1638..e8610d3 100644
--- a/tempest/thirdparty/boto/test_ec2_instance_run.py
+++ b/tempest/thirdparty/boto/test_ec2_instance_run.py
@@ -220,7 +220,6 @@
# NOTE(afazekas): doctored test case,
# with normal validation it would fail
- @test.skip_because(bug="1182679")
@test.attr(type='smoke')
def test_integration_1(self):
# EC2 1. integration test (not strict)
diff --git a/tools/check_logs.py b/tools/check_logs.py
index e28c230..bc4eaca 100755
--- a/tools/check_logs.py
+++ b/tools/check_logs.py
@@ -37,6 +37,7 @@
'ceilometer-alarm-evaluator',
'ceilometer-anotification',
'ceilometer-api',
+ 'ceilometer-collector',
'c-vol',
'g-api',
'h-api',
@@ -45,7 +46,6 @@
'n-api',
'n-cpu',
'n-net',
- 'n-sch',
'q-agt',
'q-dhcp',
'q-lbaas',
@@ -90,7 +90,7 @@
break
if not whitelisted or dump_all_errors:
if print_log_name:
- print("Log File Has Errors: %s" % name)
+ print("\nLog File Has Errors: %s" % name)
print_log_name = False
if not whitelisted:
had_errors = True
diff --git a/tools/config/check_uptodate.sh b/tools/config/check_uptodate.sh
index 45c8629..528bd5b 100755
--- a/tools/config/check_uptodate.sh
+++ b/tools/config/check_uptodate.sh
@@ -1,10 +1,25 @@
-#!/bin/sh
-TEMPDIR=`mktemp -d`
-CFGFILE=tempest.conf.sample
-tools/config/generate_sample.sh -b ./ -p tempest -o $TEMPDIR
-if ! diff $TEMPDIR/$CFGFILE etc/$CFGFILE
+#!/usr/bin/env bash
+
+PROJECT_NAME=${PROJECT_NAME:-tempest}
+CFGFILE_NAME=${PROJECT_NAME}.conf.sample
+
+if [ -e etc/${PROJECT_NAME}/${CFGFILE_NAME} ]; then
+ CFGFILE=etc/${PROJECT_NAME}/${CFGFILE_NAME}
+elif [ -e etc/${CFGFILE_NAME} ]; then
+ CFGFILE=etc/${CFGFILE_NAME}
+else
+ echo "${0##*/}: can not find config file"
+ exit 1
+fi
+
+TEMPDIR=`mktemp -d /tmp/${PROJECT_NAME}.XXXXXX`
+trap "rm -rf $TEMPDIR" EXIT
+
+tools/config/generate_sample.sh -b ./ -p ${PROJECT_NAME} -o ${TEMPDIR}
+
+if ! diff -u ${TEMPDIR}/${CFGFILE_NAME} ${CFGFILE}
then
- echo "E: tempest.conf.sample is not up to date, please run:"
- echo "tools/generate_sample.sh"
- exit 42
+ echo "${0##*/}: ${PROJECT_NAME}.conf.sample is not up to date."
+ echo "${0##*/}: Please run ${0%%${0##*/}}generate_sample.sh."
+ exit 1
fi
diff --git a/tools/config/generate_sample.sh b/tools/config/generate_sample.sh
index 607fecb..20ddfbb 100755
--- a/tools/config/generate_sample.sh
+++ b/tools/config/generate_sample.sh
@@ -4,8 +4,8 @@
echo "Try \`${0##*/} --help' for more information." >&2
}
-PARSED_OPTIONS=$(getopt -n "${0##*/}" -o hb:p:o: \
- --long help,base-dir:,package-name:,output-dir: -- "$@")
+PARSED_OPTIONS=$(getopt -n "${0##*/}" -o hb:p:m:l:o: \
+ --long help,base-dir:,package-name:,output-dir:,module:,library: -- "$@")
if [ $? != 0 ] ; then print_hint ; exit 1 ; fi
@@ -21,6 +21,8 @@
echo "-b, --base-dir=DIR project base directory"
echo "-p, --package-name=NAME project package name"
echo "-o, --output-dir=DIR file output directory"
+ echo "-m, --module=MOD extra python module to interrogate for options"
+ echo "-l, --library=LIB extra library that registers options for discovery"
exit 0
;;
-b|--base-dir)
@@ -38,6 +40,16 @@
OUTPUTDIR=`echo $1 | sed -e 's/\/*$//g'`
shift
;;
+ -m|--module)
+ shift
+ MODULES="$MODULES -m $1"
+ shift
+ ;;
+ -l|--library)
+ shift
+ LIBRARIES="$LIBRARIES -l $1"
+ shift
+ ;;
--)
break
;;
@@ -53,7 +65,7 @@
BASEDIR=$(cd "$BASEDIR" && pwd)
fi
-PACKAGENAME=${PACKAGENAME:-${BASEDIR##*/}}
+PACKAGENAME=${PACKAGENAME:-$(python setup.py --name)}
TARGETDIR=$BASEDIR/$PACKAGENAME
if ! [ -d $TARGETDIR ]
then
@@ -77,12 +89,20 @@
FILES=$(find $TARGETDIR -type f -name "*.py" ! -path "*/tests/*" \
-exec grep -l "Opt(" {} + | sed -e "s/^$BASEDIRESC\///g" | sort -u)
-EXTRA_MODULES_FILE="`dirname $0`/oslo.config.generator.rc"
-if test -r "$EXTRA_MODULES_FILE"
+RC_FILE="`dirname $0`/oslo.config.generator.rc"
+if test -r "$RC_FILE"
then
- source "$EXTRA_MODULES_FILE"
+ source "$RC_FILE"
fi
+for mod in ${TEMPEST_CONFIG_GENERATOR_EXTRA_MODULES}; do
+ MODULES="$MODULES -m $mod"
+done
+
+for lib in ${TEMPEST_CONFIG_GENERATOR_EXTRA_LIBRARIES}; do
+ LIBRARIES="$LIBRARIES -l $lib"
+done
+
export EVENTLET_NO_GREENDNS=yes
OS_VARS=$(set | sed -n '/^OS_/s/=[^=]*$//gp' | xargs)
@@ -90,7 +110,7 @@
DEFAULT_MODULEPATH=tempest.openstack.common.config.generator
MODULEPATH=${MODULEPATH:-$DEFAULT_MODULEPATH}
OUTPUTFILE=$OUTPUTDIR/$PACKAGENAME.conf.sample
-python -m $MODULEPATH $FILES > $OUTPUTFILE
+python -m $MODULEPATH $MODULES $LIBRARIES $FILES > $OUTPUTFILE
# Hook to allow projects to append custom config file snippets
CONCAT_FILES=$(ls $BASEDIR/tools/config/*.conf.sample 2>/dev/null)
diff --git a/tools/verify_tempest_config.py b/tools/verify_tempest_config.py
index aa92c0b..30785c4 100755
--- a/tools/verify_tempest_config.py
+++ b/tools/verify_tempest_config.py
@@ -16,6 +16,7 @@
import json
import sys
+import urlparse
import httplib2
@@ -39,19 +40,37 @@
not CONF.image_feature_enabled.api_v2))
-def verify_nova_api_versions(os):
- # Check nova api versions - only get base URL without PATH
- os.servers_client.skip_path = True
- # The nova base endpoint url includes the version but to get the versions
- # list the unversioned endpoint is needed
- v2_endpoint = os.servers_client.base_url
- v2_endpoint_parts = v2_endpoint.split('/')
- endpoint = v2_endpoint_parts[0] + '//' + v2_endpoint_parts[2]
+def _get_api_versions(os, service):
+ client_dict = {
+ 'nova': os.servers_client,
+ 'keystone': os.identity_client,
+ }
+ client_dict[service].skip_path()
+ endpoint_parts = urlparse.urlparse(client_dict[service])
+ endpoint = endpoint_parts.scheme + '//' + endpoint_parts.netloc
__, body = RAW_HTTP.request(endpoint, 'GET')
+ client_dict[service].reset_path()
body = json.loads(body)
- # Restore full base_url
- os.servers_client.skip_path = False
- versions = map(lambda x: x['id'], body['versions'])
+ if service == 'keystone':
+ versions = map(lambda x: x['id'], body['versions']['values'])
+ else:
+ versions = map(lambda x: x['id'], body['versions'])
+ return versions
+
+
+def verify_keystone_api_versions(os):
+ # Check keystone api versions
+ versions = _get_api_versions(os, 'keystone')
+ if CONF.identity_feature_enabled.api_v2 != ('v2.0' in versions):
+ print('Config option identity api_v2 should be change to %s' % (
+ not CONF.identity_feature_enabled.api_v2))
+ if CONF.identity_feature_enabled.api_v3 != ('v3.0' in versions):
+ print('Config option identity api_v3 should be change to %s' % (
+ not CONF.identity_feature_enabled.api_v3))
+
+
+def verify_nova_api_versions(os):
+ versions = _get_api_versions(os, 'nova')
if CONF.compute_feature_enabled.api_v3 != ('v3.0' in versions):
print('Config option compute api_v3 should be change to: %s' % (
not CONF.compute_feature_enabled.api_v3))
@@ -197,6 +216,7 @@
elif service not in services:
continue
results = verify_extensions(os, service, results)
+ verify_keystone_api_versions(os)
verify_glance_api_versions(os)
verify_nova_api_versions(os)
display_results(results)
diff --git a/tox.ini b/tox.ini
index 4a625f8..5e8d283 100644
--- a/tox.ini
+++ b/tox.ini
@@ -54,7 +54,7 @@
setenv = OS_TEST_TIMEOUT=1200
# The regex below is used to select heat api/scenario tests tagged as slow.
commands =
- bash tools/pretty_tox_serial.sh '(?=.*\[.*\bslow\b.*\])(^tempest\.(api|scenario)\.orchestration) {posargs}'
+ bash tools/pretty_tox.sh '(?=.*\[.*\bslow\b.*\])(^tempest\.(api|scenario)\.orchestration) {posargs}'
[testenv:large-ops]
sitepackages = True