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)