Merge "Fix import group ordering in test_utils.py"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 2fdbb7e..a184c76 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}
@@ -960,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]
diff --git a/requirements.txt b/requirements.txt
index 3521df0..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
@@ -13,10 +13,11 @@
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/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/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/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 13ae1c0..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'):
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 34a8e32..6bcc118 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'):
@@ -89,6 +90,18 @@
show_health_monitor = body['health_monitor']
self.assertEqual(health_monitor['id'], show_health_monitor['id'])
+ @test.attr(type='smoke')
+ def test_create_pool_from_admin_user_other_tenant(self):
+ resp, body = self.admin_client.create_pool(
+ name=data_utils.rand_name('pool-'), lb_method="ROUND_ROBIN",
+ protocol="HTTP", subnet_id=self.subnet['id'],
+ tenant_id=self.tenant_id)
+ self.assertEqual('201', resp['status'])
+ pool = body['pool']
+ self.addCleanup(self.admin_client.delete_pool, pool['id'])
+ self.assertIsNotNone(pool['id'])
+ self.assertEqual(self.tenant_id, pool['tenant_id'])
+
class LoadBalancerAdminTestXML(LoadBalancerAdminTestJSON):
_interface = 'xml'
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_load_balancer.py b/tempest/api/network/test_load_balancer.py
index 792d61d..3ab015e 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'):
@@ -156,10 +157,14 @@
# Verification of pool update
new_name = "New_pool"
resp, body = self.client.update_pool(pool['id'],
- name=new_name)
+ name=new_name,
+ description="new_description",
+ lb_method='LEAST_CONNECTIONS')
self.assertEqual('200', resp['status'])
updated_pool = body['pool']
- self.assertEqual(updated_pool['name'], new_name)
+ self.assertEqual(new_name, updated_pool['name'])
+ self.assertEqual('new_description', updated_pool['description'])
+ self.assertEqual('LEAST_CONNECTIONS', updated_pool['lb_method'])
# Verification of pool delete
resp, body = self.client.delete_pool(pool['id'])
self.assertEqual('204', resp['status'])
@@ -377,6 +382,58 @@
self.assertIn("active_connections", stats)
self.assertIn("bytes_out", stats)
+ @test.attr(type='smoke')
+ def test_update_list_of_health_monitors_associated_with_pool(self):
+ resp, _ = (self.client.associate_health_monitor_with_pool
+ (self.health_monitor['id'], self.pool['id']))
+ self.assertEqual('201', resp['status'])
+ resp, _ = self.client.update_health_monitor(
+ self.health_monitor['id'], admin_state_up=False)
+ self.assertEqual('200', resp['status'])
+ resp, body = self.client.show_pool(self.pool['id'])
+ self.assertEqual('200', resp['status'])
+ health_monitors = body['pool']['health_monitors']
+ for health_monitor_id in health_monitors:
+ resp, body = self.client.show_health_monitor(health_monitor_id)
+ self.assertEqual('200', resp['status'])
+ self.assertFalse(body['health_monitor']['admin_state_up'])
+ resp, _ = (self.client.disassociate_health_monitor_with_pool
+ (self.health_monitor['id'], self.pool['id']))
+ self.assertEqual('204', resp['status'])
+
+ @test.attr(type='smoke')
+ def test_update_admin_state_up_of_pool(self):
+ resp, _ = self.client.update_pool(self.pool['id'],
+ admin_state_up=False)
+ self.assertEqual('200', resp['status'])
+ resp, body = self.client.show_pool(self.pool['id'])
+ self.assertEqual('200', resp['status'])
+ pool = body['pool']
+ self.assertFalse(pool['admin_state_up'])
+
+ @test.attr(type='smoke')
+ def test_show_vip_associated_with_pool(self):
+ resp, body = self.client.show_pool(self.pool['id'])
+ self.assertEqual('200', resp['status'])
+ pool = body['pool']
+ resp, body = self.client.show_vip(pool['vip_id'])
+ self.assertEqual('200', resp['status'])
+ vip = body['vip']
+ self.assertEqual(self.vip['name'], vip['name'])
+ self.assertEqual(self.vip['id'], vip['id'])
+
+ @test.attr(type='smoke')
+ def test_show_members_associated_with_pool(self):
+ resp, body = self.client.show_pool(self.pool['id'])
+ self.assertEqual('200', resp['status'])
+ members = body['pool']['members']
+ for member_id in members:
+ resp, body = self.client.show_member(member_id)
+ self.assertEqual('200', resp['status'])
+ self.assertIsNotNone(body['member']['status'])
+ self.assertEqual(member_id, body['member']['id'])
+ self.assertIsNotNone(body['member']['admin_state_up'])
+
class LoadBalancerTestXML(LoadBalancerTestJSON):
_interface = 'xml'
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..66dcaa5 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()
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..f64bd33 100644
--- a/tempest/api/network/test_vpnaas_extensions.py
+++ b/tempest/api/network/test_vpnaas_extensions.py
@@ -37,6 +37,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
if not test.is_extension_enabled('vpnaas', 'network'):
msg = "vpnaas extension not enabled."
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_services.py b/tempest/api/object_storage/test_object_services.py
index 91df292..b057698 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -14,6 +14,7 @@
# under the License.
import hashlib
+import re
from six import moves
from tempest.api.object_storage import base
@@ -35,6 +36,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,32 +88,220 @@
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):
diff --git a/tempest/api/orchestration/stacks/test_server_cfn_init.py b/tempest/api/orchestration/stacks/test_server_cfn_init.py
index 5f65193..3f29269 100644
--- a/tempest/api/orchestration/stacks/test_server_cfn_init.py
+++ b/tempest/api/orchestration/stacks/test_server_cfn_init.py
@@ -93,7 +93,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/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_schema/compute/aggregates.py b/tempest/api_schema/compute/aggregates.py
index 12da618..a70b356 100644
--- a/tempest/api_schema/compute/aggregates.py
+++ b/tempest/api_schema/compute/aggregates.py
@@ -54,3 +54,5 @@
'required': ['aggregate']
}
}
+
+aggregate_set_metadata = get_aggregate
diff --git a/tempest/api_schema/compute/flavors.py b/tempest/api_schema/compute/flavors.py
index a6367d4..fd02780 100644
--- a/tempest/api_schema/compute/flavors.py
+++ b/tempest/api_schema/compute/flavors.py
@@ -35,3 +35,30 @@
'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/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/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/cli/simple_read_only/test_sahara.py b/tempest/cli/simple_read_only/test_sahara.py
index 5af8dae..36cc324 100644
--- a/tempest/cli/simple_read_only/test_sahara.py
+++ b/tempest/cli/simple_read_only/test_sahara.py
@@ -93,3 +93,48 @@
'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 693ca41..10c0014 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
@@ -463,6 +456,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
@@ -472,6 +467,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)
@@ -491,11 +487,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
@@ -517,6 +536,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
@@ -528,9 +550,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,
@@ -538,9 +564,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(
@@ -570,6 +600,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:
@@ -613,6 +646,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
@@ -620,6 +684,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
@@ -636,3 +703,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/rest_client.py b/tempest/common/rest_client.py
index 830968e..0d32f41 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -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('')
@@ -418,13 +418,22 @@
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)
diff --git a/tempest/config.py b/tempest/config.py
index 0212d8a..c0ab323 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."),
@@ -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',
@@ -844,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/__init__.py b/tempest/exceptions/__init__.py
index d313def..9eb9c1e 100644
--- a/tempest/exceptions/__init__.py
+++ b/tempest/exceptions/__init__.py
@@ -78,6 +78,12 @@
"due to '%(stack_status_reason)s'")
+class StackResourceBuildErrorException(base.TempestException):
+ message = ("Resource %(resource_name) in stack %(stack_identifier)s is "
+ "in %(resource_status)s status due to "
+ "'%(resource_status_reason)s'")
+
+
class BadRequest(base.RestClientException):
message = "Bad request"
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index f06a850..5895c37 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -19,6 +19,7 @@
import six
import subprocess
+from ironicclient import exc as ironic_exceptions
import netaddr
from neutronclient.common import exceptions as exc
from novaclient import exceptions as nova_exceptions
@@ -71,11 +72,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 +286,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 +321,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 +443,80 @@
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."""
+ 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_server_basic_ops.py b/tempest/scenario/test_server_basic_ops.py
index eefc624..13e00a5 100644
--- a/tempest/scenario/test_server_basic_ops.py
+++ b/tempest/scenario/test_server_basic_ops.py
@@ -104,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()
diff --git a/tempest/services/compute/json/aggregates_client.py b/tempest/services/compute/json/aggregates_client.py
index 68dce39..fe67102 100644
--- a/tempest/services/compute/json/aggregates_client.py
+++ b/tempest/services/compute/json/aggregates_client.py
@@ -105,4 +105,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/flavors_client.py b/tempest/services/compute/json/flavors_client.py
index bc4a64f..0206b82 100644
--- a/tempest/services/compute/json/flavors_client.py
+++ b/tempest/services/compute/json/flavors_client.py
@@ -18,6 +18,7 @@
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
@@ -47,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):
@@ -128,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):
@@ -141,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):
@@ -153,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/servers_client.py b/tempest/services/compute/json/servers_client.py
index 2d23b03..9b09a13 100644
--- a/tempest/services/compute/json/servers_client.py
+++ b/tempest/services/compute/json/servers_client.py
@@ -262,10 +262,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)
diff --git a/tempest/services/compute/v3/json/aggregates_client.py b/tempest/services/compute/v3/json/aggregates_client.py
index 1859642..8d7440e 100644
--- a/tempest/services/compute/v3/json/aggregates_client.py
+++ b/tempest/services/compute/v3/json/aggregates_client.py
@@ -105,4 +105,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/flavors_client.py b/tempest/services/compute/v3/json/flavors_client.py
index 3fdb3ca..189fe3f 100644
--- a/tempest/services/compute/v3/json/flavors_client.py
+++ b/tempest/services/compute/v3/json/flavors_client.py
@@ -18,6 +18,7 @@
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
@@ -47,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):
@@ -128,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):
@@ -141,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):
@@ -153,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/identity/json/identity_client.py b/tempest/services/identity/json/identity_client.py
index fe176bf..55239f7 100644
--- a/tempest/services/identity/json/identity_client.py
+++ b/tempest/services/identity/json/identity_client.py
@@ -275,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 4b530f1..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 = {
@@ -515,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/endpoints_client.py b/tempest/services/identity/v3/xml/endpoints_client.py
index 93dc3dc..6490e34 100644
--- a/tempest/services/identity/v3/xml/endpoints_client.py
+++ b/tempest/services/identity/v3/xml/endpoints_client.py
@@ -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 c49f361..35295d7 100644
--- a/tempest/services/identity/v3/xml/identity_client.py
+++ b/tempest/services/identity/v3/xml/identity_client.py
@@ -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",
@@ -516,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 e903089..73d831b 100644
--- a/tempest/services/identity/v3/xml/policy_client.py
+++ b/tempest/services/identity/v3/xml/policy_client.py
@@ -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/network/xml/network_client.py b/tempest/services/network/xml/network_client.py
index 0945b09..a9d4880 100644
--- a/tempest/services/network/xml/network_client.py
+++ b/tempest/services/network/xml/network_client.py
@@ -24,7 +24,7 @@
# list of plurals used for xml serialization
PLURALS = ['dns_nameservers', 'host_routes', 'allocation_pools',
'fixed_ips', 'extensions', 'extra_dhcp_opts', 'pools',
- 'health_monitors', 'vips']
+ 'health_monitors', 'vips', 'members']
def get_rest_client(self, auth_provider):
rc = rest_client.RestClient(auth_provider)
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..53a3325 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)
@@ -146,13 +150,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/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 e735a65..1fdaf19 100644
--- a/tempest/services/volume/v2/xml/volumes_client.py
+++ b/tempest/services/volume/v2/xml/volumes_client.py
@@ -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/volumes_client.py b/tempest/services/volume/xml/volumes_client.py
index 6866dad..65bc321 100644
--- a/tempest/services/volume/xml/volumes_client.py
+++ b/tempest/services/volume/xml/volumes_client.py
@@ -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 e4019f9..8df405c 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -94,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
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_rest_client.py b/tempest/tests/test_rest_client.py
index cfbb37d..64ad3bc 100644
--- a/tempest/tests/test_rest_client.py
+++ b/tempest/tests/test_rest_client.py
@@ -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"
diff --git a/tools/check_logs.py b/tools/check_logs.py
index b5b1780..bc4eaca 100755
--- a/tools/check_logs.py
+++ b/tools/check_logs.py
@@ -46,7 +46,6 @@
'n-api',
'n-cpu',
'n-net',
- 'n-sch',
'q-agt',
'q-dhcp',
'q-lbaas',
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)