Merge "Cleanup and make HACKING.rst DRYer"
diff --git a/README.rst b/README.rst
index 992d19b..da0f5f3 100644
--- a/README.rst
+++ b/README.rst
@@ -40,7 +40,7 @@
$> nosetests tempest
To run one single test ::
- $> nosetests -sv tempest.tests.compute.servers.test_server_actions.py:
+ $> nosetests -sv tempest.api.compute.servers.test_server_actions.py:
ServerActionsTestJSON.test_rebuild_nonexistent_server
Configuration
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 12aa399..f5e51cd 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -97,6 +97,9 @@
# Number of seconds to wait for output from ssh channel
ssh_channel_timeout = 60
+# Dose the SSH uses Floating IP?
+use_floatingip_for_ssh = True
+
# The type of endpoint for a Compute API service. Unless you have a
# custom Keystone service catalog implementation, you probably want to leave
# this value as "compute"
@@ -336,6 +339,10 @@
# ssh username for the image file
ssh_user = cirros
+# specifies how many resources to request at once. Used for large operations
+# testing."
+large_ops_number = 0
+
[cli]
# Enable cli tests
enabled = True
diff --git a/requirements.txt b/requirements.txt
index b3c706b..06aa9f3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,7 +12,7 @@
python-keystoneclient>=0.2.0
python-novaclient>=2.10.0
python-neutronclient>=2.2.3,<3.0.0
-python-cinderclient>=1.0.4,<2
+python-cinderclient>=1.0.4
testresources
keyring
testrepository
diff --git a/tempest/api/compute/images/test_images_oneserver.py b/tempest/api/compute/images/test_images_oneserver.py
index 4163245..7740cfc 100644
--- a/tempest/api/compute/images/test_images_oneserver.py
+++ b/tempest/api/compute/images/test_images_oneserver.py
@@ -40,7 +40,6 @@
def setUpClass(cls):
super(ImagesOneServerTestJSON, cls).setUpClass()
cls.client = cls.images_client
- cls.servers_client = cls.servers_client
try:
resp, cls.server = cls.create_server(wait_until='ACTIVE')
@@ -104,6 +103,10 @@
self.assertRaises(exceptions.NotFound,
self.alt_client.delete_image, image_id)
+ def _get_default_flavor_disk_size(self, flavor_id):
+ resp, flavor = self.flavors_client.get_flavor_details(flavor_id)
+ return flavor['disk']
+
@testtools.skipUnless(compute.CREATE_IMAGE_ENABLED,
'Environment unable to create images.')
@attr(type='smoke')
@@ -123,10 +126,15 @@
self.assertEqual(name, image['name'])
self.assertEqual('test', image['metadata']['image_type'])
- # Verify minRAM and minDisk values are the same as the original image
resp, original_image = self.client.get_image(self.image_ref)
- self.assertEqual(original_image['minRam'], image['minRam'])
- self.assertEqual(original_image['minDisk'], image['minDisk'])
+
+ # Verify minRAM is the same as the original image
+ self.assertEqual(image['minRam'], original_image['minRam'])
+
+ # Verify minDisk is the same as the original image or the flavor size
+ flavor_disk_size = self._get_default_flavor_disk_size(self.flavor_ref)
+ self.assertIn(str(image['minDisk']),
+ (str(original_image['minDisk']), str(flavor_disk_size)))
# Verify the image was deleted correctly
resp, body = self.client.delete_image(image_id)
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 304a512..8b76f7f 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -112,7 +112,7 @@
meta = {'rebuild': 'server'}
new_name = rand_name('server')
file_contents = 'Test server rebuild.'
- personality = [{'path': '/etc/rebuild.txt',
+ personality = [{'path': 'rebuild.txt',
'contents': base64.b64encode(file_contents)}]
password = 'rebuildPassw0rd'
resp, rebuilt_server = self.client.rebuild(self.server_id,
diff --git a/tempest/api/identity/admin/v3/test_tokens.py b/tempest/api/identity/admin/v3/test_tokens.py
new file mode 100644
index 0000000..2a20493
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_tokens.py
@@ -0,0 +1,57 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# 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.identity import base
+from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
+from tempest.test import attr
+
+
+class UsersTestJSON(base.BaseIdentityAdminTest):
+ _interface = 'json'
+
+ @attr(type='smoke')
+ def test_tokens(self):
+ # Valid user's token is authenticated
+ # Create a User
+ u_name = rand_name('user-')
+ u_desc = '%s-description' % u_name
+ u_email = '%s@testmail.tm' % u_name
+ u_password = rand_name('pass-')
+ resp, user = self.v3_client.create_user(
+ u_name, description=u_desc, password=u_password,
+ email=u_email)
+ self.assertTrue(resp['status'].startswith('2'))
+ self.addCleanup(self.v3_client.delete_user, user['id'])
+ # Perform Authentication
+ resp, body = self.v3_token.auth(user['id'], u_password)
+ self.assertEqual(resp['status'], '201')
+ subject_token = resp['x-subject-token']
+ # Perform GET Token
+ resp, token_details = self.v3_client.get_token(subject_token)
+ self.assertEqual(resp['status'], '200')
+ self.assertEqual(resp['x-subject-token'], subject_token)
+ self.assertEqual(token_details['user']['id'], user['id'])
+ self.assertEqual(token_details['user']['name'], u_name)
+ # Perform Delete Token
+ resp, _ = self.v3_client.delete_token(subject_token)
+ self.assertRaises(exceptions.Unauthorized, self.v3_client.get_token,
+ subject_token)
+
+
+class UsersTestXML(UsersTestJSON):
+ _interface = 'xml'
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index db55509..1237ce4 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -32,6 +32,7 @@
cls.v3_client = os.identity_v3_client
cls.service_client = os.service_client
cls.policy_client = os.policy_client
+ cls.v3_token = os.token_v3_client
if not cls.client.has_admin_extensions():
raise cls.skipException("Admin extensions disabled")
diff --git a/tempest/api/orchestration/stacks/test_instance_cfn_init.py b/tempest/api/orchestration/stacks/test_instance_cfn_init.py
index 16509ea..4f22158 100644
--- a/tempest/api/orchestration/stacks/test_instance_cfn_init.py
+++ b/tempest/api/orchestration/stacks/test_instance_cfn_init.py
@@ -46,6 +46,13 @@
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:
@@ -79,6 +86,8 @@
ImageId: {Ref: ImageId}
InstanceType: {Ref: InstanceType}
KeyName: {Ref: KeyName}
+ SecurityGroups:
+ - {Ref: SmokeSecurityGroup}
UserData:
Fn::Base64:
Fn::Join:
diff --git a/tempest/clients.py b/tempest/clients.py
index d7a740a..5efce98 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -75,12 +75,14 @@
EndPointClientJSON
from tempest.services.identity.v3.json.identity_client import \
IdentityV3ClientJSON
+from tempest.services.identity.v3.json.identity_client import V3TokenClientJSON
from tempest.services.identity.v3.json.policy_client import PolicyClientJSON
from tempest.services.identity.v3.json.service_client import \
ServiceClientJSON
from tempest.services.identity.v3.xml.endpoints_client import EndPointClientXML
from tempest.services.identity.v3.xml.identity_client import \
IdentityV3ClientXML
+from tempest.services.identity.v3.xml.identity_client import V3TokenClientXML
from tempest.services.identity.v3.xml.policy_client import PolicyClientXML
from tempest.services.identity.v3.xml.service_client import \
ServiceClientXML
@@ -239,6 +241,11 @@
"xml": HypervisorClientXML,
}
+V3_TOKEN_CLIENT = {
+ "json": V3TokenClientJSON,
+ "xml": V3TokenClientXML,
+}
+
class Manager(object):
@@ -319,6 +326,7 @@
TENANT_USAGES_CLIENT[interface](*client_args)
self.policy_client = POLICY_CLIENT[interface](*client_args)
self.hypervisor_client = HYPERVISOR_CLIENT[interface](*client_args)
+ self.token_v3_client = V3_TOKEN_CLIENT[interface](*client_args)
if client_args_v3_auth:
self.servers_client_v3_auth = SERVERS_CLIENTS[interface](
diff --git a/tempest/config.py b/tempest/config.py
index 96b144c..2e56628 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -177,6 +177,9 @@
cfg.IntOpt('ip_version_for_ssh',
default=4,
help="IP version used for SSH connections."),
+ cfg.BoolOpt('use_floatingip_for_ssh',
+ default=True,
+ help="Dose the SSH uses Floating IP?"),
cfg.StrOpt('catalog_type',
default='compute',
help="Catalog type of the Compute service."),
@@ -520,7 +523,12 @@
help='AKI image file name'),
cfg.StrOpt('ssh_user',
default='cirros',
- help='ssh username for the image file')
+ help='ssh username for the image file'),
+ cfg.IntOpt(
+ 'large_ops_number',
+ default=0,
+ help="specifies how many resources to request at once. Used "
+ "for large operations testing.")
]
diff --git a/tempest/scenario/test_large_ops.py b/tempest/scenario/test_large_ops.py
new file mode 100644
index 0000000..1f75e2f
--- /dev/null
+++ b/tempest/scenario/test_large_ops.py
@@ -0,0 +1,103 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 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.common import log as logging
+from tempest.common.utils.data_utils import rand_name
+from tempest.scenario import manager
+
+
+LOG = logging.getLogger(__name__)
+
+
+class TestLargeOpsScenario(manager.OfficialClientTest):
+
+ """
+ Test large operations.
+
+ This test below:
+ * Spin up multiple instances in one nova call
+ * as a regular user
+ * TODO: same thing for cinder
+
+ """
+
+ def _wait_for_server_status(self, status):
+ for server in self.servers:
+ self.status_timeout(
+ self.compute_client.servers, server.id, status)
+
+ def _wait_for_volume_status(self, status):
+ volume_id = self.volume.id
+ self.status_timeout(
+ self.volume_client.volumes, volume_id, status)
+
+ def _image_create(self, name, fmt, path, properties={}):
+ name = rand_name('%s-' % name)
+ image_file = open(path, 'rb')
+ self.addCleanup(image_file.close)
+ params = {
+ 'name': name,
+ 'container_format': fmt,
+ 'disk_format': fmt,
+ 'is_public': 'True',
+ }
+ params.update(properties)
+ image = self.image_client.images.create(**params)
+ self.addCleanup(self.image_client.images.delete, image)
+ self.assertEqual("queued", image.status)
+ image.update(data=image_file)
+ return image.id
+
+ def glance_image_create(self):
+ aki_img_path = self.config.scenario.img_dir + "/" + \
+ self.config.scenario.aki_img_file
+ ari_img_path = self.config.scenario.img_dir + "/" + \
+ self.config.scenario.ari_img_file
+ ami_img_path = self.config.scenario.img_dir + "/" + \
+ self.config.scenario.ami_img_file
+ LOG.debug("paths: ami: %s, ari: %s, aki: %s"
+ % (ami_img_path, ari_img_path, aki_img_path))
+ kernel_id = self._image_create('scenario-aki', 'aki', aki_img_path)
+ ramdisk_id = self._image_create('scenario-ari', 'ari', ari_img_path)
+ properties = {
+ 'properties': {'kernel_id': kernel_id, 'ramdisk_id': ramdisk_id}
+ }
+ self.image = self._image_create('scenario-ami', 'ami',
+ path=ami_img_path,
+ properties=properties)
+
+ def nova_boot(self):
+ def delete(servers):
+ [x.delete() for x in servers]
+
+ name = rand_name('scenario-server-')
+ client = self.compute_client
+ flavor_id = self.config.compute.flavor_ref
+ self.servers = client.servers.create(
+ name=name, image=self.image,
+ flavor=flavor_id,
+ min_count=self.config.scenario.large_ops_number)
+ # needed because of bug 1199788
+ self.servers = [x for x in client.servers.list() if name in x.name]
+ self.addCleanup(delete, self.servers)
+ self._wait_for_server_status('ACTIVE')
+
+ def test_large_ops_scenario(self):
+ if self.config.scenario.large_ops_number < 1:
+ return
+ self.glance_image_create()
+ self.nova_boot()
diff --git a/tempest/scenario/test_snapshot_pattern.py b/tempest/scenario/test_snapshot_pattern.py
index f21a00b..76fac82 100644
--- a/tempest/scenario/test_snapshot_pattern.py
+++ b/tempest/scenario/test_snapshot_pattern.py
@@ -85,17 +85,21 @@
self.addCleanup(self.compute_client.security_group_rules.delete,
sg_rule.id)
- def _ssh_to_server(self, server):
+ def _ssh_to_server(self, server_or_ip):
+ if isinstance(server_or_ip, basestring):
+ ip = server_or_ip
+ else:
+ network_name_for_ssh = self.config.compute.network_for_ssh
+ ip = server_or_ip.networks[network_name_for_ssh][0]
username = self.config.scenario.ssh_user
- ip = server.networks[self.config.compute.network_for_ssh][0]
linux_client = RemoteClient(ip,
username,
pkey=self.keypair.private_key)
return linux_client.ssh_client
- def _write_timestamp(self, server):
- ssh_client = self._ssh_to_server(server)
+ def _write_timestamp(self, server_or_ip):
+ ssh_client = self._ssh_to_server(server_or_ip)
ssh_client.exec_command('date > /tmp/timestamp; sync')
self.timestamp = ssh_client.exec_command('cat /tmp/timestamp')
@@ -110,11 +114,19 @@
self.assertEquals(snapshot_name, snapshot_image.name)
return image_id
- def _check_timestamp(self, server):
- ssh_client = self._ssh_to_server(server)
+ def _check_timestamp(self, server_or_ip):
+ ssh_client = self._ssh_to_server(server_or_ip)
got_timestamp = ssh_client.exec_command('cat /tmp/timestamp')
self.assertEqual(self.timestamp, got_timestamp)
+ def _create_floating_ip(self):
+ floating_ip = self.compute_client.floating_ips.create()
+ self.addCleanup(floating_ip.delete)
+ return floating_ip
+
+ def _set_floating_ip_to_server(self, server, floating_ip):
+ server.add_floating_ip(floating_ip)
+
def test_snapshot_pattern(self):
# prepare for booting a instance
self._add_keypair()
@@ -122,7 +134,12 @@
# boot a instance and create a timestamp file in it
server = self._boot_image(self.config.compute.image_ref)
- self._write_timestamp(server)
+ if self.config.compute.use_floatingip_for_ssh:
+ fip_for_server = self._create_floating_ip()
+ self._set_floating_ip_to_server(server, fip_for_server)
+ self._write_timestamp(fip_for_server.ip)
+ else:
+ self._write_timestamp(server)
# snapshot the instance
snapshot_image_id = self._create_image(server)
@@ -131,4 +148,10 @@
server_from_snapshot = self._boot_image(snapshot_image_id)
# check the existence of the timestamp file in the second instance
- self._check_timestamp(server_from_snapshot)
+ if self.config.compute.use_floatingip_for_ssh:
+ fip_for_snapshot = self._create_floating_ip()
+ self._set_floating_ip_to_server(server_from_snapshot,
+ fip_for_snapshot)
+ self._check_timestamp(fip_for_snapshot.ip)
+ else:
+ self._check_timestamp(server_from_snapshot)
diff --git a/tempest/services/identity/v3/json/identity_client.py b/tempest/services/identity/v3/json/identity_client.py
index adbdc83..56a1a72 100644
--- a/tempest/services/identity/v3/json/identity_client.py
+++ b/tempest/services/identity/v3/json/identity_client.py
@@ -208,3 +208,61 @@
resp, body = self.get('domains/%s' % domain_id)
body = json.loads(body)
return resp, body['domain']
+
+ def get_token(self, resp_token):
+ """Get token details."""
+ headers = {'X-Subject-Token': resp_token}
+ resp, body = self.get("auth/tokens", headers=headers)
+ body = json.loads(body)
+ return resp, body['token']
+
+ def delete_token(self, resp_token):
+ """Deletes token."""
+ headers = {'X-Subject-Token': resp_token}
+ resp, body = self.delete("auth/tokens", headers=headers)
+ return resp, body
+
+
+class V3TokenClientJSON(RestClient):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(V3TokenClientJSON, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.identity.catalog_type
+ self.endpoint_url = 'adminURL'
+
+ auth_url = config.identity.uri
+
+ if 'tokens' not in auth_url:
+ auth_url = auth_url.rstrip('/') + '/tokens'
+
+ self.auth_url = auth_url
+ self.config = config
+
+ def auth(self, user_id, password):
+ creds = {
+ 'auth': {
+ 'identity': {
+ 'methods': ['password'],
+ 'password': {
+ 'user': {
+ 'id': user_id,
+ 'password': password
+ }
+ }
+ }
+ }
+ }
+ headers = {'Content-Type': 'application/json'}
+ body = json.dumps(creds)
+ resp, body = self.post("auth/tokens", headers=headers, body=body)
+ return resp, body
+
+ def request(self, method, url, headers=None, body=None, wait=None):
+ """Overriding the existing HTTP request in super class rest_client."""
+ self._set_auth()
+ self.base_url = self.base_url.replace(urlparse(self.base_url).path,
+ "/v3")
+ return super(V3TokenClientJSON, self).request(method, url,
+ 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 708ee28..571b491 100644
--- a/tempest/services/identity/v3/xml/identity_client.py
+++ b/tempest/services/identity/v3/xml/identity_client.py
@@ -22,9 +22,9 @@
from tempest.common.rest_client import RestClientXML
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
-
XMLNS = "http://docs.openstack.org/identity/api/v3"
@@ -241,3 +241,65 @@
resp, body = self.get('domains/%s' % domain_id, self.headers)
body = self._parse_body(etree.fromstring(body))
return resp, body
+
+ def get_token(self, resp_token):
+ """GET a Token Details."""
+ headers = {'Content-Type': 'application/xml',
+ 'Accept': 'application/xml',
+ 'X-Subject-Token': resp_token}
+ resp, body = self.get("auth/tokens", headers=headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def delete_token(self, resp_token):
+ """Delete a Given Token."""
+ headers = {'X-Subject-Token': resp_token}
+ resp, body = self.delete("auth/tokens", headers=headers)
+ return resp, body
+
+
+class V3TokenClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(V3TokenClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.identity.catalog_type
+ self.endpoint_url = 'adminURL'
+
+ auth_url = config.identity.uri
+
+ if 'tokens' not in auth_url:
+ auth_url = auth_url.rstrip('/') + '/tokens'
+
+ self.auth_url = auth_url
+ self.config = config
+
+ def auth(self, user_id, password):
+ user = Element('user',
+ id=user_id,
+ password=password)
+ password = Element('password')
+ password.append(user)
+
+ method = Element('method')
+ method.append(Text('password'))
+ methods = Element('methods')
+ methods.append(method)
+ identity = Element('identity')
+ identity.append(methods)
+ identity.append(password)
+ auth = Element('auth')
+ auth.append(identity)
+ headers = {'Content-Type': 'application/xml'}
+ resp, body = self.post("auth/tokens", headers=headers,
+ body=str(Document(auth)))
+ return resp, body
+
+ def request(self, method, url, headers=None, body=None, wait=None):
+ """Overriding the existing HTTP request in super class rest_client."""
+ self._set_auth()
+ self.base_url = self.base_url.replace(urlparse(self.base_url).path,
+ "/v3")
+ return super(V3TokenClientXML, self).request(method, url,
+ headers=headers,
+ body=body)
diff --git a/tempest/test.py b/tempest/test.py
index 6be37be..d7008a7 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -137,7 +137,7 @@
thing = things.get(thing_id)
new_status = thing.status
if new_status == 'ERROR':
- self.fail("%s failed to get to expected status."
+ self.fail("%s failed to get to expected status. "
"In ERROR state."
% thing)
elif new_status == expected_status: