Merge "RestClient to target specific services in Keystone catalog"
diff --git a/tempest/tests/test_images.py b/tempest/tests/test_images.py
index bb8dc68..4d00ebf 100644
--- a/tempest/tests/test_images.py
+++ b/tempest/tests/test_images.py
@@ -37,7 +37,7 @@
self.flavor_ref)
self.servers_client.wait_for_server_status(server['id'], 'ACTIVE')
- #Create a new image
+ # Create a new image
name = rand_name('image')
meta = {'image_type': 'test'}
resp, body = self.client.create_image(server['id'], name, meta)
@@ -45,16 +45,42 @@
self.client.wait_for_image_resp_code(image_id, 200)
self.client.wait_for_image_status(image_id, 'ACTIVE')
- #Verify the image was created correctly
+ # Verify the image was created correctly
resp, image = self.client.get_image(image_id)
self.assertEqual(name, image['name'])
self.assertEqual('test', image['metadata']['image_type'])
- #Verify minRAM and minDisk values are the same as the original image
+ # 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'])
- #Teardown
+ # Teardown
self.client.delete_image(image['id'])
self.servers_client.delete_server(server['id'])
+
+ @attr(type='negative')
+ def test_create_image_from_deleted_server(self):
+ """An image should not be created as the server instance is removed """
+ server_name = rand_name('server')
+ resp, server = self.servers_client.create_server(server_name,
+ self.image_ref,
+ self.flavor_ref)
+ self.servers_client.wait_for_server_status(server['id'], 'ACTIVE')
+
+ # Delete server before trying to create server
+ self.servers_client.delete_server(server['id'])
+
+ try:
+ # Create a new image after server is deleted
+ name = rand_name('image')
+ meta = {'image_type': 'test'}
+ resp, body = self.client.create_image(server['id'], name, meta)
+
+ except:
+ pass
+
+ else:
+ self.fail("should not create snapshot from deleted instance")
+ # Delete Image in case the test filed and image created
+ self.client.delete_image(image['id'])
diff --git a/tempest/tests/test_list_servers.py b/tempest/tests/test_list_servers.py
index a3f759f..59c31a6 100644
--- a/tempest/tests/test_list_servers.py
+++ b/tempest/tests/test_list_servers.py
@@ -1,6 +1,9 @@
+import unittest2 as unittest
+
+from tempest import exceptions
from tempest import openstack
from tempest.common.utils.data_utils import rand_name
-import unittest2 as unittest
+from tempest.tests import utils
class ServerDetailsTest(unittest.TestCase):
@@ -15,6 +18,16 @@
cls.image_ref_alt = cls.config.env.image_ref_alt
cls.flavor_ref_alt = cls.config.env.flavor_ref_alt
+ # Check to see if the alternate image ref actually exists...
+ images_client = cls.os.images_client
+ resp, images = images_client.list_images()
+
+ if any([image for image in images
+ if image['id'] == cls.image_ref_alt]):
+ cls.multiple_images = True
+ else:
+ cls.image_ref_alt = cls.image_ref
+
cls.s1_name = rand_name('server')
resp, server = cls.client.create_server(cls.s1_name, cls.image_ref,
cls.flavor_ref)
@@ -48,6 +61,7 @@
self.assertTrue(self.s2 in servers)
self.assertTrue(self.s3 in servers)
+ @utils.skip_unless_attr('multiple_images', 'Only one image found')
def test_list_servers_detailed_filter_by_image(self):
"""Filter the detailed list of servers by image"""
params = {'image': self.image_ref}
diff --git a/tempest/tests/test_server_metadata.py b/tempest/tests/test_server_metadata.py
index 64bedb9..9e194a2 100644
--- a/tempest/tests/test_server_metadata.py
+++ b/tempest/tests/test_server_metadata.py
@@ -53,6 +53,21 @@
resp, resp_metadata = self.client.list_server_metadata(self.server_id)
self.assertEqual(resp_metadata, req_metadata)
+ def test_server_create_metadata_key_too_long(self):
+ """
+ Attempt to start a server with a meta-data key that is > 255 characters
+ Try a few values
+ """
+ for sz in [256, 257, 511, 1023]:
+ key = "k" * sz
+ meta = {key: 'data1'}
+ name = rand_name('server')
+ resp, server = self.client.create_server(name, self.image_ref,
+ self.flavor_ref,
+ meta=meta)
+ self.assertEqual(413, resp.status)
+ # no teardown - all creates should fail
+
def test_update_server_metadata(self):
"""
The server's metadata values should be updated to the
diff --git a/tempest/tests/utils.py b/tempest/tests/utils.py
new file mode 100644
index 0000000..67fcfdd
--- /dev/null
+++ b/tempest/tests/utils.py
@@ -0,0 +1,73 @@
+# 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.
+
+"""Common utilities used in testing"""
+
+import nose.plugins.skip
+
+
+class skip_if(object):
+ """Decorator that skips a test if condition is true."""
+ def __init__(self, condition, msg):
+ self.condition = condition
+ self.message = msg
+
+ def __call__(self, func):
+ def _skipper(*args, **kw):
+ """Wrapped skipper function."""
+ if self.condition:
+ raise nose.SkipTest(self.message)
+ func(*args, **kw)
+ _skipper.__name__ = func.__name__
+ _skipper.__doc__ = func.__doc__
+ return _skipper
+
+
+class skip_unless(object):
+ """Decorator that skips a test if condition is not true."""
+ def __init__(self, condition, msg):
+ self.condition = condition
+ self.message = msg
+
+ def __call__(self, func):
+ def _skipper(*args, **kw):
+ """Wrapped skipper function."""
+ if not self.condition:
+ raise nose.SkipTest(self.message)
+ func(*args, **kw)
+ _skipper.__name__ = func.__name__
+ _skipper.__doc__ = func.__doc__
+ return _skipper
+
+
+class skip_unless_attr(object):
+ """Decorator that skips a test if a specified attr exists and is True."""
+ def __init__(self, attr, msg=None):
+ self.attr = attr
+ self.message = msg or ("Test case attribute %s not found "
+ "or False") % attr
+
+ def __call__(self, func):
+ def _skipper(*args, **kwargs):
+ """Wrapped skipper function."""
+ testobj = args[0]
+ if not getattr(testobj, self.attr, False):
+ raise nose.SkipTest(self.message)
+ func(*args, **kw)
+ _skipper.__name__ = func.__name__
+ _skipper.__doc__ = func.__doc__
+ return _skipper
diff --git a/tempest/tools/conf_from_devstack b/tempest/tools/conf_from_devstack
new file mode 100755
index 0000000..f3f1309
--- /dev/null
+++ b/tempest/tools/conf_from_devstack
@@ -0,0 +1,179 @@
+#!/usr/bin/env python
+# 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.
+
+"""
+Simple script that analyzes a devstack environment and constructs
+a Tempest configuration file for the devstack environment.
+"""
+
+import optparse
+import os
+import subprocess
+import sys
+
+SUCCESS = 0
+FAILURE = 1
+
+
+def execute(cmd, raise_error=True):
+ """
+ Executes a command in a subprocess. Returns a tuple
+ of (exitcode, out, err), where out is the string output
+ from stdout and err is the string output from stderr when
+ executing the command.
+
+ :param cmd: Command string to execute
+ :param raise_error: If returncode is not 0 (success), then
+ raise a RuntimeError? Default: True)
+ """
+
+ process = subprocess.Popen(cmd,
+ shell=True,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ result = process.communicate()
+ (out, err) = result
+ exitcode = process.returncode
+ if process.returncode != 0 and raise_error:
+ msg = "Command %(cmd)s did not succeed. Returned an exit "\
+ "code of %(exitcode)d."\
+ "\n\nSTDOUT: %(out)s"\
+ "\n\nSTDERR: %(err)s" % locals()
+ raise RuntimeError(msg)
+ return exitcode, out, err
+
+
+def add_options(parser):
+ """
+ Adds CLI options to a supplied option parser
+
+ :param parser: `optparse.OptionParser`
+ """
+ parser.add_option('-o', '--outfile', metavar="PATH",
+ help=("File to save generated config to. Default: "
+ "prints config to stdout."))
+ parser.add_option('-v', '--verbose', default=False, action="store_true",
+ help="Print more verbose output")
+ parser.add_option('-D', '--devstack-dir', metavar="PATH",
+ default=os.getcwd(),
+ help="Directory to find devstack. Default: $PWD")
+
+
+def get_devstack_localrc(options):
+ """
+ Finds the localrc file in the devstack directory and returns a dict
+ representing the key/value pairs in the localrc file.
+ """
+ localrc_path = os.path.join(os.path.abspath(options.devstack_dir), 'localrc')
+ if not os.path.exists(localrc_path):
+ raise RuntimeError("Failed to find localrc file in devstack dir %s" %
+ options.devstack_dir)
+
+ if options.verbose:
+ print "Reading localrc settings from %s" % localrc_path
+
+ try:
+ settings = dict([line.split('=') for line in
+ open(localrc_path, 'r').readlines()
+ if not line.startswith('#')])
+ return settings
+ except (TypeError, ValueError) as e:
+ raise RuntimeError("Failed to read settings from localrc file %s. "
+ "Got error: %s" % (localrc_path, e))
+
+
+def main():
+ oparser = optparse.OptionParser()
+ add_options(oparser)
+
+ options, args = oparser.parse_args()
+
+ localrc = get_devstack_localrc(options)
+
+ conf_settings = {
+ 'service_host': localrc.get('HOST_IP', '127.0.0.1'),
+ 'service_port': 5000, # Make this configurable when devstack does
+ 'identity_api_version': 'v2.0', # Ditto
+ 'user': localrc.get('USERNAME', 'admin'),
+ 'password': localrc.get('ADMIN_PASSWORD', 'password')
+ }
+
+ # We need to determine the UUID of the base image, so we
+ # query the Glance endpoint for a list of images...
+ cmd = "glance index -A %s" % localrc['SERVICE_TOKEN']
+ retcode, out, err = execute(cmd)
+
+ if retcode != 0:
+ raise RuntimeError("Unable to get list of images from Glance. "
+ "Got error: %s" % err)
+
+ image_lines = out.split('\n')[2:]
+ for line in image_lines:
+ if 'ami' in line:
+ conf_settings['base_image_uuid'] = line.split()[0]
+ break
+
+ if 'base_image_uuid' not in conf_settings:
+ raise RuntimeError("Unable to find any AMI images in glance index")
+
+ if options.verbose:
+ print "Found base image with UUID %s" % conf_settings['base_image_uuid']
+
+ tempest_conf = """
+[nova]
+host=%(service_host)s
+port=%(service_port)s
+apiVer=%(identity_api_version)s
+path=tokens
+user=%(user)s
+api_key=%(password)s
+tenant_name=%(user)s
+ssh_timeout=300
+build_interval=10
+build_timeout=600
+
+[environment]
+image_ref=%(base_image_uuid)s
+image_ref_alt=4
+flavor_ref=1
+flavor_ref_alt=2
+create_image_enabled=true
+resize_available=true
+authentication=keystone_v2""" % conf_settings
+
+ if options.outfile:
+ outfile_path = os.path.abspath(options.outfile)
+ if os.path.exists(outfile_path):
+ confirm = raw_input("Output file already exists. Overwrite? [y/N]")
+ if confirm != 'Y':
+ print "Exiting"
+ return SUCCESS
+ with open(outfile_path, 'wb') as outfile:
+ outfile.write(tempest_conf)
+ if options.verbose:
+ print "Wrote tempest config to %s" % outfile_path
+ else:
+ print tempest_conf
+
+
+if __name__ == '__main__':
+ try:
+ sys.exit(main())
+ except RuntimeError, e:
+ sys.exit("ERROR: %s" % e)