[openstack-glance] sync with essex stable

Pádraig Brady pbrady at fedoraproject.org
Mon May 21 11:41:53 UTC 2012


commit bee8b13d12cdf2247c397a3c335b3bf4e9a26054
Author: Pádraig Brady <P at draigBrady.com>
Date:   Mon May 21 10:43:09 2012 +0100

    sync with essex stable

 0003-Fix-content-type-for-qpid-notifier.patch      |   63 +
 0004-Omit-Content-Length-on-chunked-transfer.patch |  183 ++
 ...-Fix-i18n-in-glance.notifier.notify_kombu.patch |   28 +
 ...-Don-t-access-the-net-while-building-docs.patch |    2 +-
 0007-Support-DB-auto-create-suppression.patch      | 2204 ++++++++++++++++++++
 openstack-glance.spec                              |   15 +-
 6 files changed, 2492 insertions(+), 3 deletions(-)
---
diff --git a/0003-Fix-content-type-for-qpid-notifier.patch b/0003-Fix-content-type-for-qpid-notifier.patch
new file mode 100644
index 0000000..032a37a
--- /dev/null
+++ b/0003-Fix-content-type-for-qpid-notifier.patch
@@ -0,0 +1,63 @@
+From 5838b6390353e4c34762828684cee90410e4f8b1 Mon Sep 17 00:00:00 2001
+From: Russell Bryant <rbryant at redhat.com>
+Date: Tue, 24 Apr 2012 12:24:39 -0400
+Subject: [PATCH] Fix content type for qpid notifier.
+
+Fix bug 980872.
+
+This patch fixes a regression I introduced in
+2d36facf14f4eb2742ba46274e04a73b5231aece.  In that patch, I adjusted the
+content_type for messages sent with the qpid notifier to be
+'application/json' to match a change that went into the kombu notifier.
+Unfortunately, it's wrong.
+
+I assumed based on the kombu change that notifications were being json
+encoded before being passed into the notification driver.  That's not
+the case.  The message is a dict.  So, just revert the change to set the
+content_type and let Qpid encode the notification as 'amqp/map'.
+
+(cherry picked from commit 5bed23cbc962d3c6503f0ff93e6d1e326efbd49d)
+
+Change-Id: I8ba039612d9603377028ba72cb80ae89d675c741
+---
+ glance/notifier/notify_qpid.py     |    9 +++------
+ glance/tests/unit/test_notifier.py |    2 +-
+ 2 files changed, 4 insertions(+), 7 deletions(-)
+
+diff --git a/glance/notifier/notify_qpid.py b/glance/notifier/notify_qpid.py
+index d3d5514..a0535a6 100644
+--- a/glance/notifier/notify_qpid.py
++++ b/glance/notifier/notify_qpid.py
+@@ -135,16 +135,13 @@ class QpidStrategy(strategy.Strategy):
+         return self.session.sender(address)
+ 
+     def warn(self, msg):
+-        qpid_msg = qpid.messaging.Message(content=msg,
+-                                          content_type='application/json')
++        qpid_msg = qpid.messaging.Message(content=msg)
+         self.sender_warn.send(qpid_msg)
+ 
+     def info(self, msg):
+-        qpid_msg = qpid.messaging.Message(content=msg,
+-                                          content_type='application/json')
++        qpid_msg = qpid.messaging.Message(content=msg)
+         self.sender_info.send(qpid_msg)
+ 
+     def error(self, msg):
+-        qpid_msg = qpid.messaging.Message(content=msg,
+-                                          content_type='application/json')
++        qpid_msg = qpid.messaging.Message(content=msg)
+         self.sender_error.send(qpid_msg)
+diff --git a/glance/tests/unit/test_notifier.py b/glance/tests/unit/test_notifier.py
+index 0e7ff75..b952ee3 100644
+--- a/glance/tests/unit/test_notifier.py
++++ b/glance/tests/unit/test_notifier.py
+@@ -316,7 +316,7 @@ class TestQpidNotifier(unittest.TestCase):
+         super(TestQpidNotifier, self).tearDown()
+ 
+     def _test_notify(self, priority):
+-        test_msg = json.dumps({'a': 'b'})
++        test_msg = {'a': 'b'}
+ 
+         self.mock_connection = self.mocker.CreateMock(self.orig_connection)
+         self.mock_session = self.mocker.CreateMock(self.orig_session)
diff --git a/0004-Omit-Content-Length-on-chunked-transfer.patch b/0004-Omit-Content-Length-on-chunked-transfer.patch
new file mode 100644
index 0000000..eebc8d9
--- /dev/null
+++ b/0004-Omit-Content-Length-on-chunked-transfer.patch
@@ -0,0 +1,183 @@
+From 7a9e3a7dc53a20971dbd653f2061337efec136a2 Mon Sep 17 00:00:00 2001
+From: Mike Lundy <mike at pistoncloud.com>
+Date: Fri, 13 Apr 2012 19:53:16 -0700
+Subject: [PATCH] Omit Content-Length on chunked transfer
+
+Content-Length and Transfer-Encoding conflict according to the HTTP
+spec. This fixes bug 981332.
+
+This also adds the ability to test both the sendfile-present and
+sendfile-absent codepaths; the sendfile-present test will be skipped on
+sendfile-absent platforms.
+
+[ This is backported from 223fbee49a55691504623fa691bbb3e48048d5f3 ]
+
+Change-Id: I20856eb51ff66fe4b7145f796a540a832c3aa4d8
+---
+ glance/common/client.py           |    9 +++++++--
+ glance/tests/stubs.py             |   29 +++++++++++++++++++++++------
+ glance/tests/unit/test_clients.py |   15 +++++++++++++--
+ 3 files changed, 43 insertions(+), 10 deletions(-)
+
+diff --git a/glance/common/client.py b/glance/common/client.py
+index 7c6ae55..2e1f35c 100644
+--- a/glance/common/client.py
++++ b/glance/common/client.py
+@@ -506,12 +506,17 @@ class BaseClient(object):
+             elif _filelike(body) or self._iterable(body):
+                 c.putrequest(method, path)
+ 
++                use_sendfile = self._sendable(body)
++
++                # According to HTTP/1.1, Content-Length and Transfer-Encoding
++                # conflict.
+                 for header, value in headers.items():
+-                    c.putheader(header, value)
++                    if use_sendfile or header.lower() != 'content-length':
++                        c.putheader(header, value)
+ 
+                 iter = self.image_iterator(c, headers, body)
+ 
+-                if self._sendable(body):
++                if use_sendfile:
+                     # send actual file without copying into userspace
+                     _sendbody(c, iter)
+                 else:
+diff --git a/glance/tests/stubs.py b/glance/tests/stubs.py
+index dba4d7f..776da40 100644
+--- a/glance/tests/stubs.py
++++ b/glance/tests/stubs.py
+@@ -59,7 +59,7 @@ def stub_out_registry_and_store_server(stubs, base_dir):
+         def close(self):
+             return True
+ 
+-        def request(self, method, url, body=None, headers={}):
++        def request(self, method, url, body=None, headers=None):
+             self.req = webob.Request.blank("/" + url.lstrip("/"))
+             self.req.method = method
+             if headers:
+@@ -110,7 +110,8 @@ def stub_out_registry_and_store_server(stubs, base_dir):
+ 
+         def __init__(self, *args, **kwargs):
+             self.sock = FakeSocket()
+-            pass
++            self.stub_force_sendfile = kwargs.get('stub_force_sendfile',
++                                                  SENDFILE_SUPPORTED)
+ 
+         def connect(self):
+             return True
+@@ -120,7 +121,7 @@ def stub_out_registry_and_store_server(stubs, base_dir):
+ 
+         def putrequest(self, method, url):
+             self.req = webob.Request.blank("/" + url.lstrip("/"))
+-            if SENDFILE_SUPPORTED:
++            if self.stub_force_sendfile:
+                 fake_sendfile = FakeSendFile(self.req)
+                 stubs.Set(sendfile, 'sendfile', fake_sendfile.sendfile)
+             self.req.method = method
+@@ -129,7 +130,10 @@ def stub_out_registry_and_store_server(stubs, base_dir):
+             self.req.headers[key] = value
+ 
+         def endheaders(self):
+-            pass
++            hl = [i.lower() for i in self.req.headers.keys()]
++            assert not ('content-length' in hl and
++                        'transfer-encoding' in hl), \
++                'Content-Length and Transfer-Encoding are mutually exclusive'
+ 
+         def send(self, data):
+             # send() is called during chunked-transfer encoding, and
+@@ -137,7 +141,7 @@ def stub_out_registry_and_store_server(stubs, base_dir):
+             # only write the actual data in tests.
+             self.req.body += data.split("\r\n")[1]
+ 
+-        def request(self, method, url, body=None, headers={}):
++        def request(self, method, url, body=None, headers=None):
+             self.req = webob.Request.blank("/" + url.lstrip("/"))
+             self.req.method = method
+             if headers:
+@@ -187,8 +191,21 @@ def stub_out_registry_and_store_server(stubs, base_dir):
+         for i in self.source.app_iter:
+             yield i
+ 
++    def fake_sendable(self, body):
++        force = getattr(self, 'stub_force_sendfile', None)
++        if force is None:
++            return self._stub_orig_sendable(body)
++        else:
++            if force:
++                assert glance.common.client.SENDFILE_SUPPORTED
++            return force
++
+     stubs.Set(glance.common.client.BaseClient, 'get_connection_type',
+               fake_get_connection_type)
++    setattr(glance.common.client.BaseClient, '_stub_orig_sendable',
++              glance.common.client.BaseClient._sendable)
++    stubs.Set(glance.common.client.BaseClient, '_sendable',
++              fake_sendable)
+     stubs.Set(glance.common.client.ImageBodyIterator, '__iter__',
+               fake_image_iter)
+ 
+@@ -211,7 +228,7 @@ def stub_out_registry_server(stubs, **kwargs):
+         def close(self):
+             return True
+ 
+-        def request(self, method, url, body=None, headers={}):
++        def request(self, method, url, body=None, headers=None):
+             self.req = webob.Request.blank("/" + url.lstrip("/"))
+             self.req.method = method
+             if headers:
+diff --git a/glance/tests/unit/test_clients.py b/glance/tests/unit/test_clients.py
+index e8e71de..1548f48 100644
+--- a/glance/tests/unit/test_clients.py
++++ b/glance/tests/unit/test_clients.py
+@@ -26,7 +26,7 @@ import stubout
+ import webob
+ 
+ from glance import client
+-from glance.common import context
++from glance.common import client as base_client
+ from glance.common import exception
+ from glance.common import utils
+ from glance.registry.db import api as db_api
+@@ -36,6 +36,7 @@ from glance.registry import context as rcontext
+ from glance.tests import stubs
+ from glance.tests import utils as test_utils
+ from glance.tests.unit import base
++from glance.tests import utils as test_utils
+ 
+ CONF = {'sql_connection': 'sqlite://'}
+ 
+@@ -1842,7 +1843,7 @@ class TestClient(base.IsolatedUnitTest):
+         for k, v in fixture.items():
+             self.assertEquals(v, new_meta[k])
+ 
+-    def test_add_image_with_image_data_as_file(self):
++    def add_image_with_image_data_as_file(self, sendfile):
+         """Tests can add image by passing image data as file"""
+         fixture = {'name': 'fake public image',
+                    'is_public': True,
+@@ -1852,6 +1853,8 @@ class TestClient(base.IsolatedUnitTest):
+                    'properties': {'distro': 'Ubuntu 10.04 LTS'},
+                   }
+ 
++        self.client.stub_force_sendfile = sendfile
++
+         image_data_fixture = r"chunk00000remainder"
+ 
+         tmp_image_filepath = '/tmp/rubbish-image'
+@@ -1879,6 +1882,14 @@ class TestClient(base.IsolatedUnitTest):
+         for k, v in fixture.items():
+             self.assertEquals(v, new_meta[k])
+ 
++    @test_utils.skip_if(not base_client.SENDFILE_SUPPORTED,
++                        'sendfile not supported')
++    def test_add_image_with_image_data_as_file_with_sendfile(self):
++        self.add_image_with_image_data_as_file(sendfile=True)
++
++    def test_add_image_with_image_data_as_file_without_sendfile(self):
++        self.add_image_with_image_data_as_file(sendfile=False)
++
+     def _add_image_as_iterable(self):
+         fixture = {'name': 'fake public image',
+                    'is_public': True,
diff --git a/0005-Fix-i18n-in-glance.notifier.notify_kombu.patch b/0005-Fix-i18n-in-glance.notifier.notify_kombu.patch
new file mode 100644
index 0000000..551d557
--- /dev/null
+++ b/0005-Fix-i18n-in-glance.notifier.notify_kombu.patch
@@ -0,0 +1,28 @@
+From 54622953c4cc09196c7f16d8229ba438afbe626f Mon Sep 17 00:00:00 2001
+From: Brian Waldon <bcwaldon at gmail.com>
+Date: Sun, 22 Apr 2012 22:21:53 -0700
+Subject: [PATCH] Fix i18n in glance.notifier.notify_kombu
+
+* Fixes bug 983829
+
+Change-Id: Ibc5ec12e97e69797d1952c020c3091f42480abec
+---
+ glance/notifier/notify_kombu.py |    5 +++--
+ 1 files changed, 3 insertions(+), 2 deletions(-)
+
+diff --git a/glance/notifier/notify_kombu.py b/glance/notifier/notify_kombu.py
+index b3447d6..6623fdc 100644
+--- a/glance/notifier/notify_kombu.py
++++ b/glance/notifier/notify_kombu.py
+@@ -166,8 +166,9 @@ class RabbitStrategy(strategy.Strategy):
+ 
+     def log_failure(self, msg, priority):
+         """Fallback to logging when we can't send to rabbit."""
+-        logger.error(_('Notification with priority %(priority)s failed; '
+-                'msg=%s') % msg)
++        message = _('Notification with priority %(priority)s failed: '
++                    'msg=%(msg)s')
++        logger.error(message % {'msg': msg, 'priority': priority})
+ 
+     def _send_message(self, msg, routing_key):
+         """Send a message.  Caller needs to catch exceptions for retry."""
diff --git a/0003-Don-t-access-the-net-while-building-docs.patch b/0006-Don-t-access-the-net-while-building-docs.patch
similarity index 92%
rename from 0003-Don-t-access-the-net-while-building-docs.patch
rename to 0006-Don-t-access-the-net-while-building-docs.patch
index f4aba04..3b5e7f1 100644
--- a/0003-Don-t-access-the-net-while-building-docs.patch
+++ b/0006-Don-t-access-the-net-while-building-docs.patch
@@ -1,4 +1,4 @@
-From 332b3d0f916561bc85eaa1167fda4821815b0a3d Mon Sep 17 00:00:00 2001
+From 6a63200908b9efd846c59a0747f10502d4d819fe Mon Sep 17 00:00:00 2001
 From: =?UTF-8?q?P=C3=A1draig=20Brady?= <pbrady at redhat.com>
 Date: Fri, 6 Jan 2012 17:12:54 +0000
 Subject: [PATCH] Don't access the net while building docs
diff --git a/0007-Support-DB-auto-create-suppression.patch b/0007-Support-DB-auto-create-suppression.patch
new file mode 100644
index 0000000..8aec148
--- /dev/null
+++ b/0007-Support-DB-auto-create-suppression.patch
@@ -0,0 +1,2204 @@
+From f4372facffb42c5c1e02e51a4a2b34edac08cbca Mon Sep 17 00:00:00 2001
+From: Eoghan Glynn <eglynn at redhat.com>
+Date: Fri, 18 May 2012 14:23:41 +0100
+Subject: [PATCH] Support DB auto-create suppression.
+
+Adds a new boolean config option, db_auto_create, to allow the
+DB auto-creation be suppressed on demand. This defaults to True
+for now to maintain the pre-existing behaviour, but should be
+changed to False before the Folsom release.
+
+The 'glance-manage db_sync' command will now create the image*
+tables if the DB did not previously exist. The db_auto_create
+flag is irrelevant in that case.
+
+The @glance.tests.function.runs_sql annotation is now obsolete
+as the glance-api/registry services launched by functional tests
+must now all run against an on-disk sqlite instance (as opposed
+to in-memory, as this makes no sense when the DB tables are
+created in advance).
+
+Change-Id: I05fc6b3ca7691dfaf00bc75a0743c921c93b9694
+
+Conflicts:
+
+	glance/tests/functional/__init__.py
+	glance/tests/functional/test_sqlite.py
+	glance/tests/functional/v1/test_api.py
+	glance/tests/functional/v2/test_images.py
+---
+ bin/glance-manage                             |   10 +-
+ glance/registry/db/api.py                     |   24 +-
+ glance/tests/functional/__init__.py           |   58 +-
+ glance/tests/functional/test_bin_glance.py    |    3 -
+ glance/tests/functional/test_glance_manage.py |   77 ++
+ glance/tests/functional/test_sqlite.py        |    1 -
+ glance/tests/functional/v1/test_api.py        | 1357 +++++++++++++++++++++++++
+ glance/tests/functional/v2/test_images.py     |  468 +++++++++
+ 8 files changed, 1958 insertions(+), 40 deletions(-)
+ create mode 100644 glance/tests/functional/test_glance_manage.py
+ create mode 100644 glance/tests/functional/v1/test_api.py
+ create mode 100644 glance/tests/functional/v2/test_images.py
+
+diff --git a/bin/glance-manage b/bin/glance-manage
+index 3a50c11..aeee4fd 100755
+--- a/bin/glance-manage
++++ b/bin/glance-manage
+@@ -44,6 +44,7 @@ from glance.common import cfg
+ from glance.common import config
+ from glance.common import exception
+ import glance.registry.db
++import glance.registry.db.api
+ import glance.registry.db.migration
+ 
+ 
+@@ -75,7 +76,14 @@ def do_version_control(conf, args):
+ 
+ 
+ def do_db_sync(conf, args):
+-    """Place a database under migration control and upgrade"""
++    """
++    Place a database under migration control and upgrade,
++    creating first if necessary.
++    """
++    # override auto-create flag, as complete DB should always
++    # be created on sync if not already existing
++    conf.db_auto_create = True
++    glance.registry.db.api.configure_db(conf)
+     version = args.pop(0) if args else None
+     current_version = args.pop(0) if args else None
+     glance.registry.db.migration.db_sync(conf, version, current_version)
+diff --git a/glance/registry/db/api.py b/glance/registry/db/api.py
+index 04fe2e5..9e78725 100644
+--- a/glance/registry/db/api.py
++++ b/glance/registry/db/api.py
+@@ -68,7 +68,8 @@ db_opts = [
+     cfg.IntOpt('sql_idle_timeout', default=3600),
+     cfg.StrOpt('sql_connection', default='sqlite:///glance.sqlite'),
+     cfg.IntOpt('sql_max_retries', default=10),
+-    cfg.IntOpt('sql_retry_interval', default=1)
++    cfg.IntOpt('sql_retry_interval', default=1),
++    cfg.BoolOpt('db_auto_create', default=True),
+     ]
+ 
+ 
+@@ -102,7 +103,10 @@ def configure_db(conf):
+     """
+     global _ENGINE, sa_logger, logger, _MAX_RETRIES, _RETRY_INTERVAL
+     if not _ENGINE:
+-        conf.register_opts(db_opts)
++        for opt in db_opts:
++            # avoid duplicate registration
++            if not opt.name in conf:
++                conf.register_opt(opt)
+         sql_connection = conf.sql_connection
+         _MAX_RETRIES = conf.sql_max_retries
+         _RETRY_INTERVAL = conf.sql_retry_interval
+@@ -131,12 +135,16 @@ def configure_db(conf):
+         elif conf.verbose:
+             sa_logger.setLevel(logging.INFO)
+ 
+-        models.register_models(_ENGINE)
+-        try:
+-            migration.version_control(conf)
+-        except exception.DatabaseMigrationError:
+-            # only arises when the DB exists and is under version control
+-            pass
++        if conf.db_auto_create:
++            logger.info('auto-creating glance registry DB')
++            models.register_models(_ENGINE)
++            try:
++                migration.version_control(conf)
++            except exception.DatabaseMigrationError:
++                # only arises when the DB exists and is under version control
++                pass
++        else:
++            logger.info('not auto-creating glance registry DB')
+ 
+ 
+ def check_mutate_authorization(context, image_ref):
+diff --git a/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py
+index 5260a89..da0c944 100644
+--- a/glance/tests/functional/__init__.py
++++ b/glance/tests/functional/__init__.py
+@@ -43,27 +43,6 @@ from glance.tests import utils as test_utils
+ execute, get_unused_port = test_utils.execute, test_utils.get_unused_port
+ 
+ 
+-def runs_sql(func):
+-    """
+-    Decorator for a test case method that ensures that the
+-    sql_connection setting is overridden to ensure a disk-based
+-    SQLite database so that arbitrary SQL statements can be
+-    executed out-of-process against the datastore...
+-    """
+-    @functools.wraps(func)
+-    def wrapped(*a, **kwargs):
+-        test_obj = a[0]
+-        orig_sql_connection = test_obj.registry_server.sql_connection
+-        try:
+-            if orig_sql_connection.startswith('sqlite'):
+-                test_obj.registry_server.sql_connection =\
+-                        "sqlite:///tests.sqlite"
+-            func(*a, **kwargs)
+-        finally:
+-            test_obj.registry_server.sql_connection = orig_sql_connection
+-    return wrapped
+-
+-
+ class Server(object):
+     """
+     Class used to easily manage starting and stopping
+@@ -89,6 +68,7 @@ class Server(object):
+         self.exec_env = None
+         self.deployment_flavor = ''
+         self.server_control_options = ''
++        self.needs_database = False
+ 
+     def write_conf(self, **kwargs):
+         """
+@@ -145,6 +125,8 @@ class Server(object):
+         # Ensure the configuration file is written
+         overridden = self.write_conf(**kwargs)[1]
+ 
++        self.create_database()
++
+         cmd = ("%(server_control)s %(server_name)s start "
+                "%(conf_file_name)s --pid-file=%(pid_file)s "
+                "%(server_control_options)s"
+@@ -156,6 +138,23 @@ class Server(object):
+                        expected_exitcode=expected_exitcode,
+                        context=overridden)
+ 
++    def create_database(self):
++        """Create database if required for this server"""
++        if self.needs_database:
++            conf_dir = os.path.join(self.test_dir, 'etc')
++            utils.safe_mkdirs(conf_dir)
++            conf_filepath = os.path.join(conf_dir, 'glance-manage.conf')
++
++            with open(conf_filepath, 'wb') as conf_file:
++                conf_file.write('[DEFAULT]\n')
++                conf_file.write('sql_connection = %s' % self.sql_connection)
++                conf_file.flush()
++
++            cmd = ('bin/glance-manage db_sync --config-file %s'
++                   % conf_filepath)
++            execute(cmd, no_venv=self.no_venv, exec_env=self.exec_env,
++                    expect_exit=True)
++
+     def stop(self):
+         """
+         Spin down the server.
+@@ -212,6 +211,12 @@ class ApiServer(Server):
+         self.policy_file = policy_file
+         self.policy_default_rule = 'default'
+         self.server_control_options = '--capture-output'
++
++        self.needs_database = True
++        default_sql_connection = 'sqlite:///tests.sqlite'
++        self.sql_connection = os.environ.get('GLANCE_TEST_SQL_CONNECTION',
++                                             default_sql_connection)
++
+         self.conf_base = """[DEFAULT]
+ verbose = %(verbose)s
+ debug = %(debug)s
+@@ -248,6 +253,8 @@ image_cache_dir = %(image_cache_dir)s
+ image_cache_driver = %(image_cache_driver)s
+ policy_file = %(policy_file)s
+ policy_default_rule = %(policy_default_rule)s
++db_auto_create = False
++sql_connection = %(sql_connection)s
+ [paste_deploy]
+ flavor = %(deployment_flavor)s
+ """
+@@ -300,7 +307,8 @@ class RegistryServer(Server):
+         super(RegistryServer, self).__init__(test_dir, port)
+         self.server_name = 'registry'
+ 
+-        default_sql_connection = 'sqlite:///'
++        self.needs_database = True
++        default_sql_connection = 'sqlite:///tests.sqlite'
+         self.sql_connection = os.environ.get('GLANCE_TEST_SQL_CONNECTION',
+                                              default_sql_connection)
+ 
+@@ -315,6 +323,7 @@ debug = %(debug)s
+ bind_host = 0.0.0.0
+ bind_port = %(bind_port)s
+ log_file = %(log_file)s
++db_auto_create = False
+ sql_connection = %(sql_connection)s
+ sql_idle_timeout = 3600
+ api_limit_max = 1000
+@@ -625,11 +634,6 @@ class FunctionalTest(unittest.TestCase):
+         if os.path.exists(self.test_dir):
+             shutil.rmtree(self.test_dir)
+ 
+-        # We do this here because the @runs_sql decorator above
+-        # actually resets the registry server's sql_connection
+-        # to the original (usually memory-based SQLite connection)
+-        # and this block of code is run *before* the finally:
+-        # block in that decorator...
+         self._reset_database(self.registry_server.sql_connection)
+ 
+     def run_sql_cmd(self, sql):
+diff --git a/glance/tests/functional/test_bin_glance.py b/glance/tests/functional/test_bin_glance.py
+index a989b58..5393872 100644
+--- a/glance/tests/functional/test_bin_glance.py
++++ b/glance/tests/functional/test_bin_glance.py
+@@ -643,7 +643,6 @@ class TestBinGlance(functional.FunctionalTest):
+ 
+         self.stop_servers()
+ 
+-    @functional.runs_sql
+     def test_add_location_with_checksum(self):
+         """
+         We test the following:
+@@ -675,7 +674,6 @@ class TestBinGlance(functional.FunctionalTest):
+ 
+         self.stop_servers()
+ 
+-    @functional.runs_sql
+     def test_add_location_without_checksum(self):
+         """
+         We test the following:
+@@ -707,7 +705,6 @@ class TestBinGlance(functional.FunctionalTest):
+ 
+         self.stop_servers()
+ 
+-    @functional.runs_sql
+     def test_add_clear(self):
+         """
+         We test the following:
+diff --git a/glance/tests/functional/test_glance_manage.py b/glance/tests/functional/test_glance_manage.py
+new file mode 100644
+index 0000000..4b627c5
+--- /dev/null
++++ b/glance/tests/functional/test_glance_manage.py
+@@ -0,0 +1,77 @@
++# vim: tabstop=4 shiftwidth=4 softtabstop=4
++
++# Copyright 2012 Red Hat, Inc
++# 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.
++
++"""Functional test cases for glance-manage"""
++
++import os
++
++from glance.common import utils
++from glance.tests import functional
++from glance.tests.utils import execute, depends_on_exe, skip_if_disabled
++
++
++class TestGlanceManage(functional.FunctionalTest):
++    """Functional tests for glance-manage"""
++
++    def setUp(self):
++        super(TestGlanceManage, self).setUp()
++        conf_dir = os.path.join(self.test_dir, 'etc')
++        utils.safe_mkdirs(conf_dir)
++        self.conf_filepath = os.path.join(conf_dir, 'glance-manage.conf')
++        self.db_filepath = os.path.join(conf_dir, 'test.sqlite')
++        self.connection = ('sql_connection = sqlite:///%s' %
++                           self.db_filepath)
++
++    def _sync_db(self, auto_create):
++        with open(self.conf_filepath, 'wb') as conf_file:
++            conf_file.write('[DEFAULT]\n')
++            conf_file.write('db_auto_create = %r\n' % auto_create)
++            conf_file.write(self.connection)
++            conf_file.flush()
++
++        cmd = ('bin/glance-manage db_sync --config-file %s' %
++               self.conf_filepath)
++        execute(cmd, raise_error=True)
++
++    def _assert_tables(self):
++        cmd = "sqlite3 %s '.schema'" % self.db_filepath
++        exitcode, out, err = execute(cmd, raise_error=True)
++
++        self.assertTrue('CREATE TABLE images' in out)
++        self.assertTrue('CREATE TABLE image_tags' in out)
++        self.assertTrue('CREATE TABLE image_members' in out)
++        self.assertTrue('CREATE TABLE image_properties' in out)
++
++    @depends_on_exe('sqlite3')
++    @skip_if_disabled
++    def test_db_creation(self):
++        """Test DB creation by db_sync on a fresh DB"""
++        self._sync_db(True)
++
++        self._assert_tables()
++
++        self.stop_servers()
++
++    @depends_on_exe('sqlite3')
++    @skip_if_disabled
++    def test_db_creation_auto_create_overridden(self):
++        """Test DB creation with db_auto_create False"""
++        self._sync_db(False)
++
++        self._assert_tables()
++
++        self.stop_servers()
+diff --git a/glance/tests/functional/test_sqlite.py b/glance/tests/functional/test_sqlite.py
+index 4cfff6a..36afcb3 100644
+--- a/glance/tests/functional/test_sqlite.py
++++ b/glance/tests/functional/test_sqlite.py
+@@ -25,7 +25,6 @@ from glance.tests.utils import execute
+ class TestSqlite(functional.FunctionalTest):
+     """Functional tests for sqlite-specific logic"""
+ 
+-    @functional.runs_sql
+     def test_big_int_mapping(self):
+         """Ensure BigInteger not mapped to BIGINT"""
+         self.cleanup()
+diff --git a/glance/tests/functional/v1/test_api.py b/glance/tests/functional/v1/test_api.py
+new file mode 100644
+index 0000000..b22e88f
+--- /dev/null
++++ b/glance/tests/functional/v1/test_api.py
+@@ -0,0 +1,1357 @@
++# vim: tabstop=4 shiftwidth=4 softtabstop=4
++
++# Copyright 2011 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.
++
++"""Functional test case that utilizes httplib2 against the API server"""
++
++import datetime
++import hashlib
++import json
++import tempfile
++
++import httplib2
++
++from glance.common import utils
++from glance.tests import functional
++from glance.tests.utils import skip_if_disabled, minimal_headers
++
++FIVE_KB = 5 * 1024
++FIVE_GB = 5 * 1024 * 1024 * 1024
++
++
++class TestApi(functional.FunctionalTest):
++
++    """Functional tests using httplib2 against the API server"""
++
++    @skip_if_disabled
++    def test_get_head_simple_post(self):
++        """
++        We test the following sequential series of actions:
++
++        0. GET /images
++        - Verify no public images
++        1. GET /images/detail
++        - Verify no public images
++        2. POST /images with public image named Image1
++        and no custom properties
++        - Verify 201 returned
++        3. HEAD image
++        - Verify HTTP headers have correct information we just added
++        4. GET image
++        - Verify all information on image we just added is correct
++        5. GET /images
++        - Verify the image we just added is returned
++        6. GET /images/detail
++        - Verify the image we just added is returned
++        7. PUT image with custom properties of "distro" and "arch"
++        - Verify 200 returned
++        8. GET image
++        - Verify updated information about image was stored
++        9. PUT image
++        - Remove a previously existing property.
++        10. PUT image
++        - Add a previously deleted property.
++        """
++        self.cleanup()
++        self.start_servers(**self.__dict__.copy())
++
++        # 0. GET /images
++        # Verify no public images
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(content, '{"images": []}')
++
++        # 1. GET /images/detail
++        # Verify no public images
++        path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(content, '{"images": []}')
++
++        # 2. POST /images with public image named Image1
++        # attribute and no custom properties. Verify a 200 OK is returned
++        image_data = "*" * FIVE_KB
++        headers = minimal_headers('Image1')
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers,
++                                         body=image_data)
++        self.assertEqual(response.status, 201)
++        data = json.loads(content)
++        image_id = data['image']['id']
++        self.assertEqual(data['image']['checksum'],
++                         hashlib.md5(image_data).hexdigest())
++        self.assertEqual(data['image']['size'], FIVE_KB)
++        self.assertEqual(data['image']['name'], "Image1")
++        self.assertEqual(data['image']['is_public'], True)
++
++        # 3. HEAD image
++        # Verify image found now
++        path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                              image_id)
++        http = httplib2.Http()
++        response, content = http.request(path, 'HEAD')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(response['x-image-meta-name'], "Image1")
++
++        # 4. GET image
++        # Verify all information on image we just added is correct
++        path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                              image_id)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++
++        expected_image_headers = {
++            'x-image-meta-id': image_id,
++            'x-image-meta-name': 'Image1',
++            'x-image-meta-is_public': 'True',
++            'x-image-meta-status': 'active',
++            'x-image-meta-disk_format': 'raw',
++            'x-image-meta-container_format': 'ovf',
++            'x-image-meta-size': str(FIVE_KB)}
++
++        expected_std_headers = {
++            'content-length': str(FIVE_KB),
++            'content-type': 'application/octet-stream'}
++
++        for expected_key, expected_value in expected_image_headers.items():
++            self.assertEqual(response[expected_key], expected_value,
++                            "For key '%s' expected header value '%s'. Got '%s'"
++                            % (expected_key, expected_value,
++                               response[expected_key]))
++
++        for expected_key, expected_value in expected_std_headers.items():
++            self.assertEqual(response[expected_key], expected_value,
++                            "For key '%s' expected header value '%s'. Got '%s'"
++                            % (expected_key,
++                               expected_value,
++                               response[expected_key]))
++
++        self.assertEqual(content, "*" * FIVE_KB)
++        self.assertEqual(hashlib.md5(content).hexdigest(),
++                         hashlib.md5("*" * FIVE_KB).hexdigest())
++
++        # 5. GET /images
++        # Verify no public images
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++
++        expected_result = {"images": [
++            {"container_format": "ovf",
++             "disk_format": "raw",
++             "id": image_id,
++             "name": "Image1",
++             "checksum": "c2e5db72bd7fd153f53ede5da5a06de3",
++             "size": 5120}]}
++        self.assertEqual(json.loads(content), expected_result)
++
++        # 6. GET /images/detail
++        # Verify image and all its metadata
++        path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++
++        expected_image = {
++            "status": "active",
++            "name": "Image1",
++            "deleted": False,
++            "container_format": "ovf",
++            "disk_format": "raw",
++            "id": image_id,
++            "is_public": True,
++            "deleted_at": None,
++            "properties": {},
++            "size": 5120}
++
++        image = json.loads(content)
++
++        for expected_key, expected_value in expected_image.items():
++            self.assertEqual(expected_value, image['images'][0][expected_key],
++                            "For key '%s' expected header value '%s'. Got '%s'"
++                            % (expected_key,
++                               expected_value,
++                               image['images'][0][expected_key]))
++
++        # 7. PUT image with custom properties of "distro" and "arch"
++        # Verify 200 returned
++        headers = {'X-Image-Meta-Property-Distro': 'Ubuntu',
++                   'X-Image-Meta-Property-Arch': 'x86_64'}
++        path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                              image_id)
++        http = httplib2.Http()
++        response, content = http.request(path, 'PUT', headers=headers)
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(data['image']['properties']['arch'], "x86_64")
++        self.assertEqual(data['image']['properties']['distro'], "Ubuntu")
++
++        # 8. GET /images/detail
++        # Verify image and all its metadata
++        path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++
++        expected_image = {
++            "status": "active",
++            "name": "Image1",
++            "deleted": False,
++            "container_format": "ovf",
++            "disk_format": "raw",
++            "id": image_id,
++            "is_public": True,
++            "deleted_at": None,
++            "properties": {'distro': 'Ubuntu', 'arch': 'x86_64'},
++            "size": 5120}
++
++        image = json.loads(content)
++
++        for expected_key, expected_value in expected_image.items():
++            self.assertEqual(expected_value, image['images'][0][expected_key],
++                            "For key '%s' expected header value '%s'. Got '%s'"
++                            % (expected_key,
++                               expected_value,
++                               image['images'][0][expected_key]))
++
++        # 9. PUT image and remove a previously existing property.
++        headers = {'X-Image-Meta-Property-Arch': 'x86_64'}
++        path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                              image_id)
++        http = httplib2.Http()
++        response, content = http.request(path, 'PUT', headers=headers)
++        self.assertEqual(response.status, 200)
++
++        path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)['images'][0]
++        self.assertEqual(len(data['properties']), 1)
++        self.assertEqual(data['properties']['arch'], "x86_64")
++
++        # 10. PUT image and add a previously deleted property.
++        headers = {'X-Image-Meta-Property-Distro': 'Ubuntu',
++                   'X-Image-Meta-Property-Arch': 'x86_64'}
++        path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                              image_id)
++        http = httplib2.Http()
++        response, content = http.request(path, 'PUT', headers=headers)
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++
++        path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)['images'][0]
++        self.assertEqual(len(data['properties']), 2)
++        self.assertEqual(data['properties']['arch'], "x86_64")
++        self.assertEqual(data['properties']['distro'], "Ubuntu")
++
++        # DELETE image
++        path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                              image_id)
++        http = httplib2.Http()
++        response, content = http.request(path, 'DELETE')
++        self.assertEqual(response.status, 200)
++
++        self.stop_servers()
++
++    @skip_if_disabled
++    def test_queued_process_flow(self):
++        """
++        We test the process flow where a user registers an image
++        with Glance but does not immediately upload an image file.
++        Later, the user uploads an image file using a PUT operation.
++        We track the changing of image status throughout this process.
++
++        0. GET /images
++        - Verify no public images
++        1. POST /images with public image named Image1 with no location
++           attribute and no image data.
++        - Verify 201 returned
++        2. GET /images
++        - Verify one public image
++        3. HEAD image
++        - Verify image now in queued status
++        4. PUT image with image data
++        - Verify 200 returned
++        5. HEAD images
++        - Verify image now in active status
++        6. GET /images
++        - Verify one public image
++        """
++
++        self.cleanup()
++        self.start_servers(**self.__dict__.copy())
++
++        # 0. GET /images
++        # Verify no public images
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(content, '{"images": []}')
++
++        # 1. POST /images with public image named Image1
++        # with no location or image data
++        headers = minimal_headers('Image1')
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        data = json.loads(content)
++        self.assertEqual(data['image']['checksum'], None)
++        self.assertEqual(data['image']['size'], 0)
++        self.assertEqual(data['image']['container_format'], 'ovf')
++        self.assertEqual(data['image']['disk_format'], 'raw')
++        self.assertEqual(data['image']['name'], "Image1")
++        self.assertEqual(data['image']['is_public'], True)
++
++        image_id = data['image']['id']
++
++        # 2. GET /images
++        # Verify 1 public image
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(data['images'][0]['id'], image_id)
++        self.assertEqual(data['images'][0]['checksum'], None)
++        self.assertEqual(data['images'][0]['size'], 0)
++        self.assertEqual(data['images'][0]['container_format'], 'ovf')
++        self.assertEqual(data['images'][0]['disk_format'], 'raw')
++        self.assertEqual(data['images'][0]['name'], "Image1")
++
++        # 3. HEAD /images
++        # Verify status is in queued
++        path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                              image_id)
++        http = httplib2.Http()
++        response, content = http.request(path, 'HEAD')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(response['x-image-meta-name'], "Image1")
++        self.assertEqual(response['x-image-meta-status'], "queued")
++        self.assertEqual(response['x-image-meta-size'], '0')
++        self.assertEqual(response['x-image-meta-id'], image_id)
++
++        # 4. PUT image with image data, verify 200 returned
++        image_data = "*" * FIVE_KB
++        headers = {'Content-Type': 'application/octet-stream'}
++        path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                             image_id)
++        http = httplib2.Http()
++        response, content = http.request(path, 'PUT', headers=headers,
++                                         body=image_data)
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(data['image']['checksum'],
++                         hashlib.md5(image_data).hexdigest())
++        self.assertEqual(data['image']['size'], FIVE_KB)
++        self.assertEqual(data['image']['name'], "Image1")
++        self.assertEqual(data['image']['is_public'], True)
++
++        # 5. HEAD /images
++        # Verify status is in active
++        path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                              image_id)
++        http = httplib2.Http()
++        response, content = http.request(path, 'HEAD')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(response['x-image-meta-name'], "Image1")
++        self.assertEqual(response['x-image-meta-status'], "active")
++
++        # 6. GET /images
++        # Verify 1 public image still...
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(data['images'][0]['checksum'],
++                         hashlib.md5(image_data).hexdigest())
++        self.assertEqual(data['images'][0]['id'], image_id)
++        self.assertEqual(data['images'][0]['size'], FIVE_KB)
++        self.assertEqual(data['images'][0]['container_format'], 'ovf')
++        self.assertEqual(data['images'][0]['disk_format'], 'raw')
++        self.assertEqual(data['images'][0]['name'], "Image1")
++
++        # DELETE image
++        path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                              image_id)
++        http = httplib2.Http()
++        response, content = http.request(path, 'DELETE')
++        self.assertEqual(response.status, 200)
++
++        self.stop_servers()
++
++    @skip_if_disabled
++    def test_size_greater_2G_mysql(self):
++        """
++        A test against the actual datastore backend for the registry
++        to ensure that the image size property is not truncated.
++
++        :see https://bugs.launchpad.net/glance/+bug/739433
++        """
++
++        self.cleanup()
++        self.start_servers(**self.__dict__.copy())
++
++        # 1. POST /images with public image named Image1
++        # attribute and a size of 5G. Use the HTTP engine with an
++        # X-Image-Meta-Location attribute to make Glance forego
++        # "adding" the image data.
++        # Verify a 201 OK is returned
++        headers = {'Content-Type': 'application/octet-stream',
++                   'X-Image-Meta-Location': 'http://example.com/fakeimage',
++                   'X-Image-Meta-Size': str(FIVE_GB),
++                   'X-Image-Meta-Name': 'Image1',
++                   'X-Image-Meta-disk_format': 'raw',
++                   'X-image-Meta-container_format': 'ovf',
++                   'X-Image-Meta-Is-Public': 'True'}
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++
++        # 2. HEAD /images
++        # Verify image size is what was passed in, and not truncated
++        path = response.get('location')
++        http = httplib2.Http()
++        response, content = http.request(path, 'HEAD')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(response['x-image-meta-size'], str(FIVE_GB))
++        self.assertEqual(response['x-image-meta-name'], 'Image1')
++        self.assertEqual(response['x-image-meta-is_public'], 'True')
++
++        self.stop_servers()
++
++    @skip_if_disabled
++    def test_traceback_not_consumed(self):
++        """
++        A test that errors coming from the POST API do not
++        get consumed and print the actual error message, and
++        not something like &lt;traceback object at 0x1918d40&gt;
++
++        :see https://bugs.launchpad.net/glance/+bug/755912
++        """
++        self.cleanup()
++        self.start_servers(**self.__dict__.copy())
++
++        # POST /images with binary data, but not setting
++        # Content-Type to application/octet-stream, verify a
++        # 400 returned and that the error is readable.
++        with tempfile.NamedTemporaryFile() as test_data_file:
++            test_data_file.write("XXX")
++            test_data_file.flush()
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST',
++                            body=test_data_file.name)
++        self.assertEqual(response.status, 400)
++        expected = "Content-Type must be application/octet-stream"
++        self.assertTrue(expected in content,
++                        "Could not find '%s' in '%s'" % (expected, content))
++
++        self.stop_servers()
++
++    @skip_if_disabled
++    def test_filtered_images(self):
++        """
++        Set up four test images and ensure each query param filter works
++        """
++        self.cleanup()
++        self.start_servers(**self.__dict__.copy())
++
++        # 0. GET /images
++        # Verify no public images
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(content, '{"images": []}')
++
++        image_ids = []
++
++        # 1. POST /images with three public images, and one private image
++        # with various attributes
++        headers = {'Content-Type': 'application/octet-stream',
++                   'X-Image-Meta-Name': 'Image1',
++                   'X-Image-Meta-Status': 'active',
++                   'X-Image-Meta-Container-Format': 'ovf',
++                   'X-Image-Meta-Disk-Format': 'vdi',
++                   'X-Image-Meta-Size': '19',
++                   'X-Image-Meta-Is-Public': 'True',
++                   'X-Image-Meta-Protected': 'True',
++                   'X-Image-Meta-Property-pants': 'are on'}
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        data = json.loads(content)
++        self.assertEqual(data['image']['properties']['pants'], "are on")
++        self.assertEqual(data['image']['is_public'], True)
++        image_ids.append(data['image']['id'])
++
++        headers = {'Content-Type': 'application/octet-stream',
++                   'X-Image-Meta-Name': 'My Image!',
++                   'X-Image-Meta-Status': 'active',
++                   'X-Image-Meta-Container-Format': 'ovf',
++                   'X-Image-Meta-Disk-Format': 'vhd',
++                   'X-Image-Meta-Size': '20',
++                   'X-Image-Meta-Is-Public': 'True',
++                   'X-Image-Meta-Protected': 'False',
++                   'X-Image-Meta-Property-pants': 'are on'}
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        data = json.loads(content)
++        self.assertEqual(data['image']['properties']['pants'], "are on")
++        self.assertEqual(data['image']['is_public'], True)
++        image_ids.append(data['image']['id'])
++
++        headers = {'Content-Type': 'application/octet-stream',
++                   'X-Image-Meta-Name': 'My Image!',
++                   'X-Image-Meta-Status': 'saving',
++                   'X-Image-Meta-Container-Format': 'ami',
++                   'X-Image-Meta-Disk-Format': 'ami',
++                   'X-Image-Meta-Size': '21',
++                   'X-Image-Meta-Is-Public': 'True',
++                   'X-Image-Meta-Protected': 'False',
++                   'X-Image-Meta-Property-pants': 'are off'}
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        data = json.loads(content)
++        self.assertEqual(data['image']['properties']['pants'], "are off")
++        self.assertEqual(data['image']['is_public'], True)
++        image_ids.append(data['image']['id'])
++
++        headers = {'Content-Type': 'application/octet-stream',
++                   'X-Image-Meta-Name': 'My Private Image',
++                   'X-Image-Meta-Status': 'active',
++                   'X-Image-Meta-Container-Format': 'ami',
++                   'X-Image-Meta-Disk-Format': 'ami',
++                   'X-Image-Meta-Size': '22',
++                   'X-Image-Meta-Is-Public': 'False',
++                   'X-Image-Meta-Protected': 'False'}
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        data = json.loads(content)
++        self.assertEqual(data['image']['is_public'], False)
++        image_ids.append(data['image']['id'])
++
++        # 2. GET /images
++        # Verify three public images
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 3)
++
++        # 3. GET /images with name filter
++        # Verify correct images returned with name
++        params = "name=My%20Image!"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 2)
++        for image in data['images']:
++            self.assertEqual(image['name'], "My Image!")
++
++        # 4. GET /images with status filter
++        # Verify correct images returned with status
++        params = "status=queued"
++        path = "http://%s:%d/v1/images/detail?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 3)
++        for image in data['images']:
++            self.assertEqual(image['status'], "queued")
++
++        params = "status=active"
++        path = "http://%s:%d/v1/images/detail?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 0)
++
++        # 5. GET /images with container_format filter
++        # Verify correct images returned with container_format
++        params = "container_format=ovf"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 2)
++        for image in data['images']:
++            self.assertEqual(image['container_format'], "ovf")
++
++        # 6. GET /images with disk_format filter
++        # Verify correct images returned with disk_format
++        params = "disk_format=vdi"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 1)
++        for image in data['images']:
++            self.assertEqual(image['disk_format'], "vdi")
++
++        # 7. GET /images with size_max filter
++        # Verify correct images returned with size <= expected
++        params = "size_max=20"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 2)
++        for image in data['images']:
++            self.assertTrue(image['size'] <= 20)
++
++        # 8. GET /images with size_min filter
++        # Verify correct images returned with size >= expected
++        params = "size_min=20"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 2)
++        for image in data['images']:
++            self.assertTrue(image['size'] >= 20)
++
++        # 9. Get /images with is_public=None filter
++        # Verify correct images returned with property
++        # Bug lp:803656  Support is_public in filtering
++        params = "is_public=None"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 4)
++
++        # 10. Get /images with is_public=False filter
++        # Verify correct images returned with property
++        # Bug lp:803656  Support is_public in filtering
++        params = "is_public=False"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 1)
++        for image in data['images']:
++            self.assertEqual(image['name'], "My Private Image")
++
++        # 11. Get /images with is_public=True filter
++        # Verify correct images returned with property
++        # Bug lp:803656  Support is_public in filtering
++        params = "is_public=True"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 3)
++        for image in data['images']:
++            self.assertNotEqual(image['name'], "My Private Image")
++
++        # 12. Get /images with protected=False filter
++        # Verify correct images returned with property
++        params = "protected=False"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 2)
++        for image in data['images']:
++            self.assertNotEqual(image['name'], "Image1")
++
++        # 13. Get /images with protected=True filter
++        # Verify correct images returned with property
++        params = "protected=True"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 1)
++        for image in data['images']:
++            self.assertEqual(image['name'], "Image1")
++
++        # 14. GET /images with property filter
++        # Verify correct images returned with property
++        params = "property-pants=are%20on"
++        path = "http://%s:%d/v1/images/detail?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 2)
++        for image in data['images']:
++            self.assertEqual(image['properties']['pants'], "are on")
++
++        # 15. GET /images with property filter and name filter
++        # Verify correct images returned with property and name
++        # Make sure you quote the url when using more than one param!
++        params = "name=My%20Image!&property-pants=are%20on"
++        path = "http://%s:%d/v1/images/detail?%s" % (
++                "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 1)
++        for image in data['images']:
++            self.assertEqual(image['properties']['pants'], "are on")
++            self.assertEqual(image['name'], "My Image!")
++
++        # 16. GET /images with past changes-since filter
++        yesterday = utils.isotime(datetime.datetime.utcnow() -
++                                  datetime.timedelta(1))
++        params = "changes-since=%s" % yesterday
++        path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 3)
++
++        # one timezone west of Greenwich equates to an hour ago
++        # taking care to pre-urlencode '+' as '%2B', otherwise the timezone
++        # '+' is wrongly decoded as a space
++        # TODO(eglynn): investigate '+' --> <SPACE> decoding, an artifact
++        # of WSGI/webob dispatch?
++        now = datetime.datetime.utcnow()
++        hour_ago = now.strftime('%Y-%m-%dT%H:%M:%S%%2B01:00')
++        params = "changes-since=%s" % hour_ago
++        path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 3)
++
++        # 17. GET /images with future changes-since filter
++        tomorrow = utils.isotime(datetime.datetime.utcnow() +
++                                 datetime.timedelta(1))
++        params = "changes-since=%s" % tomorrow
++        path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 0)
++
++        # one timezone east of Greenwich equates to an hour from now
++        now = datetime.datetime.utcnow()
++        hour_hence = now.strftime('%Y-%m-%dT%H:%M:%S-01:00')
++        params = "changes-since=%s" % hour_hence
++        path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 0)
++
++        # 18. GET /images with size_min filter
++        # Verify correct images returned with size >= expected
++        params = "size_min=-1"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 400)
++        self.assertTrue("filter size_min got -1" in content)
++
++        # 19. GET /images with size_min filter
++        # Verify correct images returned with size >= expected
++        params = "size_max=-1"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 400)
++        self.assertTrue("filter size_max got -1" in content)
++
++        # 20. GET /images with size_min filter
++        # Verify correct images returned with size >= expected
++        params = "min_ram=-1"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 400)
++        self.assertTrue("Bad value passed to filter min_ram got -1" in content)
++
++        # 21. GET /images with size_min filter
++        # Verify correct images returned with size >= expected
++        params = "protected=imalittleteapot"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 400)
++        self.assertTrue("protected got imalittleteapot" in content)
++
++        # 22. GET /images with size_min filter
++        # Verify correct images returned with size >= expected
++        params = "is_public=imalittleteapot"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 400)
++        self.assertTrue("is_public got imalittleteapot" in content)
++
++        self.stop_servers()
++
++    @skip_if_disabled
++    def test_limited_images(self):
++        """
++        Ensure marker and limit query params work
++        """
++        self.cleanup()
++        self.start_servers(**self.__dict__.copy())
++
++        # 0. GET /images
++        # Verify no public images
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(content, '{"images": []}')
++
++        image_ids = []
++
++        # 1. POST /images with three public images with various attributes
++        headers = minimal_headers('Image1')
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        image_ids.append(json.loads(content)['image']['id'])
++
++        headers = minimal_headers('Image2')
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        image_ids.append(json.loads(content)['image']['id'])
++
++        headers = minimal_headers('Image3')
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        image_ids.append(json.loads(content)['image']['id'])
++
++        # 2. GET /images with all images
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        images = json.loads(content)['images']
++        self.assertEqual(len(images), 3)
++
++        # 3. GET /images with limit of 2
++        # Verify only two images were returned
++        params = "limit=2"
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)['images']
++        self.assertEqual(len(data), 2)
++        self.assertEqual(data[0]['id'], images[0]['id'])
++        self.assertEqual(data[1]['id'], images[1]['id'])
++
++        # 4. GET /images with marker
++        # Verify only two images were returned
++        params = "marker=%s" % images[0]['id']
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)['images']
++        self.assertEqual(len(data), 2)
++        self.assertEqual(data[0]['id'], images[1]['id'])
++        self.assertEqual(data[1]['id'], images[2]['id'])
++
++        # 5. GET /images with marker and limit
++        # Verify only one image was returned with the correct id
++        params = "limit=1&marker=%s" % images[1]['id']
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)['images']
++        self.assertEqual(len(data), 1)
++        self.assertEqual(data[0]['id'], images[2]['id'])
++
++        # 6. GET /images/detail with marker and limit
++        # Verify only one image was returned with the correct id
++        params = "limit=1&marker=%s" % images[1]['id']
++        path = "http://%s:%d/v1/images?%s" % (
++               "0.0.0.0", self.api_port, params)
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)['images']
++        self.assertEqual(len(data), 1)
++        self.assertEqual(data[0]['id'], images[2]['id'])
++
++        # DELETE images
++        for image_id in image_ids:
++            path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                                  image_id)
++            http = httplib2.Http()
++            response, content = http.request(path, 'DELETE')
++            self.assertEqual(response.status, 200)
++
++        self.stop_servers()
++
++    @skip_if_disabled
++    def test_ordered_images(self):
++        """
++        Set up three test images and ensure each query param filter works
++        """
++        self.cleanup()
++        self.start_servers(**self.__dict__.copy())
++
++        # 0. GET /images
++        # Verify no public images
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(content, '{"images": []}')
++
++        # 1. POST /images with three public images with various attributes
++        image_ids = []
++        headers = {'Content-Type': 'application/octet-stream',
++                   'X-Image-Meta-Name': 'Image1',
++                   'X-Image-Meta-Status': 'active',
++                   'X-Image-Meta-Container-Format': 'ovf',
++                   'X-Image-Meta-Disk-Format': 'vdi',
++                   'X-Image-Meta-Size': '19',
++                   'X-Image-Meta-Is-Public': 'True'}
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        image_ids.append(json.loads(content)['image']['id'])
++
++        headers = {'Content-Type': 'application/octet-stream',
++                   'X-Image-Meta-Name': 'ASDF',
++                   'X-Image-Meta-Status': 'active',
++                   'X-Image-Meta-Container-Format': 'bare',
++                   'X-Image-Meta-Disk-Format': 'iso',
++                   'X-Image-Meta-Size': '2',
++                   'X-Image-Meta-Is-Public': 'True'}
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        image_ids.append(json.loads(content)['image']['id'])
++
++        headers = {'Content-Type': 'application/octet-stream',
++                   'X-Image-Meta-Name': 'XYZ',
++                   'X-Image-Meta-Status': 'saving',
++                   'X-Image-Meta-Container-Format': 'ami',
++                   'X-Image-Meta-Disk-Format': 'ami',
++                   'X-Image-Meta-Size': '5',
++                   'X-Image-Meta-Is-Public': 'True'}
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        image_ids.append(json.loads(content)['image']['id'])
++
++        # 2. GET /images with no query params
++        # Verify three public images sorted by created_at desc
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 3)
++        self.assertEqual(data['images'][0]['id'], image_ids[2])
++        self.assertEqual(data['images'][1]['id'], image_ids[1])
++        self.assertEqual(data['images'][2]['id'], image_ids[0])
++
++        # 3. GET /images sorted by name asc
++        params = 'sort_key=name&sort_dir=asc'
++        path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 3)
++        self.assertEqual(data['images'][0]['id'], image_ids[1])
++        self.assertEqual(data['images'][1]['id'], image_ids[0])
++        self.assertEqual(data['images'][2]['id'], image_ids[2])
++
++        # 4. GET /images sorted by size desc
++        params = 'sort_key=size&sort_dir=desc'
++        path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 3)
++        self.assertEqual(data['images'][0]['id'], image_ids[0])
++        self.assertEqual(data['images'][1]['id'], image_ids[2])
++        self.assertEqual(data['images'][2]['id'], image_ids[1])
++
++        # 5. GET /images sorted by size desc with a marker
++        params = 'sort_key=size&sort_dir=desc&marker=%s' % image_ids[0]
++        path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 2)
++        self.assertEqual(data['images'][0]['id'], image_ids[2])
++        self.assertEqual(data['images'][1]['id'], image_ids[1])
++
++        # 6. GET /images sorted by name asc with a marker
++        params = 'sort_key=name&sort_dir=asc&marker=%s' % image_ids[2]
++        path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        data = json.loads(content)
++        self.assertEqual(len(data['images']), 0)
++
++        # DELETE images
++        for image_id in image_ids:
++            path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
++                                                  image_id)
++            http = httplib2.Http()
++            response, content = http.request(path, 'DELETE')
++            self.assertEqual(response.status, 200)
++
++        self.stop_servers()
++
++    @skip_if_disabled
++    def test_duplicate_image_upload(self):
++        """
++        Upload initial image, then attempt to upload duplicate image
++        """
++        self.cleanup()
++        self.start_servers(**self.__dict__.copy())
++
++        # 0. GET /images
++        # Verify no public images
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(content, '{"images": []}')
++
++        # 1. POST /images with public image named Image1
++        headers = {'Content-Type': 'application/octet-stream',
++                   'X-Image-Meta-Name': 'Image1',
++                   'X-Image-Meta-Status': 'active',
++                   'X-Image-Meta-Container-Format': 'ovf',
++                   'X-Image-Meta-Disk-Format': 'vdi',
++                   'X-Image-Meta-Size': '19',
++                   'X-Image-Meta-Is-Public': 'True'}
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++
++        image = json.loads(content)['image']
++
++        # 2. POST /images with public image named Image1, and ID: 1
++        headers = {'Content-Type': 'application/octet-stream',
++                   'X-Image-Meta-Name': 'Image1 Update',
++                   'X-Image-Meta-Status': 'active',
++                   'X-Image-Meta-Container-Format': 'ovf',
++                   'X-Image-Meta-Disk-Format': 'vdi',
++                   'X-Image-Meta-Size': '19',
++                   'X-Image-Meta-Id': image['id'],
++                   'X-Image-Meta-Is-Public': 'True'}
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 409)
++
++        self.stop_servers()
++
++    @skip_if_disabled
++    def test_delete_not_existing(self):
++        """
++        We test the following:
++
++        0. GET /images/1
++        - Verify 404
++        1. DELETE /images/1
++        - Verify 404
++        """
++        self.cleanup()
++        self.start_servers(**self.__dict__.copy())
++
++        api_port = self.api_port
++        registry_port = self.registry_port
++
++        # 0. GET /images
++        # Verify no public images
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'GET')
++        self.assertEqual(response.status, 200)
++        self.assertEqual(content, '{"images": []}')
++
++        # 1. DELETE /images/1
++        # Verify 404 returned
++        path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'DELETE')
++        self.assertEqual(response.status, 404)
++
++        self.stop_servers()
++
++    @skip_if_disabled
++    def test_unsupported_default_store(self):
++        """
++        We test that a mis-configured default_store causes the API server
++        to fail to start.
++        """
++        self.cleanup()
++        self.default_store = 'shouldnotexist'
++
++        # ensure failure exit code is available to assert on
++        self.api_server.server_control_options += ' --await-child=1'
++
++        # ensure that the API server fails to launch
++        self.start_server(self.api_server,
++                          expect_launch=False,
++                          expected_exitcode=255,
++                          **self.__dict__.copy())
++
++    def _do_test_post_image_content_missing_format(self, format):
++        """
++        We test that missing container/disk format fails with 400 "Bad Request"
++
++        :see https://bugs.launchpad.net/glance/+bug/933702
++        """
++        self.cleanup()
++        self.start_servers(**self.__dict__.copy())
++
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++
++        # POST /images without given format being specified
++        headers = minimal_headers('Image1')
++        del headers['X-Image-Meta-' + format]
++        with tempfile.NamedTemporaryFile() as test_data_file:
++            test_data_file.write("XXX")
++            test_data_file.flush()
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST',
++                            headers=headers,
++                            body=test_data_file.name)
++        self.assertEqual(response.status, 400)
++        type = format.replace('_format', '')
++        expected = "Details: Invalid %s format 'None' for image" % type
++        self.assertTrue(expected in content,
++                        "Could not find '%s' in '%s'" % (expected, content))
++
++        self.stop_servers()
++
++    @skip_if_disabled
++    def _do_test_post_image_content_missing_diskformat(self):
++        self._do_test_post_image_content_missing_format('container_format')
++
++    @skip_if_disabled
++    def _do_test_post_image_content_missing_disk_format(self):
++        self._do_test_post_image_content_missing_format('disk_format')
++
++    def _do_test_put_image_content_missing_format(self, format):
++        """
++        We test that missing container/disk format only fails with
++        400 "Bad Request" when the image content is PUT (i.e. not
++        on the original POST of a queued image).
++
++        :see https://bugs.launchpad.net/glance/+bug/937216
++        """
++        self.cleanup()
++        self.start_servers(**self.__dict__.copy())
++
++        # POST queued image
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        headers = {
++           'X-Image-Meta-Name': 'Image1',
++           'X-Image-Meta-Is-Public': 'True',
++        }
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=headers)
++        self.assertEqual(response.status, 201)
++        data = json.loads(content)
++        image_id = data['image']['id']
++
++        # PUT image content images without given format being specified
++        path = ("http://%s:%d/v1/images/%s" %
++                ("0.0.0.0", self.api_port, image_id))
++        headers = minimal_headers('Image1')
++        del headers['X-Image-Meta-' + format]
++        with tempfile.NamedTemporaryFile() as test_data_file:
++            test_data_file.write("XXX")
++            test_data_file.flush()
++        http = httplib2.Http()
++        response, content = http.request(path, 'PUT',
++                            headers=headers,
++                            body=test_data_file.name)
++        self.assertEqual(response.status, 400)
++        type = format.replace('_format', '')
++        expected = "Details: Invalid %s format 'None' for image" % type
++        self.assertTrue(expected in content,
++                        "Could not find '%s' in '%s'" % (expected, content))
++
++        self.stop_servers()
++
++    @skip_if_disabled
++    def _do_test_put_image_content_missing_diskformat(self):
++        self._do_test_put_image_content_missing_format('container_format')
++
++    @skip_if_disabled
++    def _do_test_put_image_content_missing_disk_format(self):
++        self._do_test_put_image_content_missing_format('disk_format')
++
++    @skip_if_disabled
++    def test_ownership(self):
++        self.cleanup()
++        self.api_server.deployment_flavor = 'fakeauth'
++        self.registry_server.deployment_flavor = 'fakeauth'
++        self.start_servers(**self.__dict__.copy())
++
++        # Add an image with admin privileges and ensure the owner
++        # can be set to something other than what was used to authenticate
++        auth_headers = {
++            'X-Auth-Token': 'user1:tenant1:admin',
++        }
++
++        create_headers = {
++            'X-Image-Meta-Name': 'MyImage',
++            'X-Image-Meta-disk_format': 'raw',
++            'X-Image-Meta-container_format': 'ovf',
++            'X-Image-Meta-Is-Public': 'True',
++            'X-Image-Meta-Owner': 'tenant2',
++        }
++        create_headers.update(auth_headers)
++
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=create_headers)
++        self.assertEqual(response.status, 201)
++        data = json.loads(content)
++        image_id = data['image']['id']
++
++        path = ("http://%s:%d/v1/images/%s" %
++                ("0.0.0.0", self.api_port, image_id))
++        http = httplib2.Http()
++        response, content = http.request(path, 'HEAD', headers=auth_headers)
++        self.assertEqual(response.status, 200)
++        self.assertEqual('tenant2', response['x-image-meta-owner'])
++
++        # Now add an image without admin privileges and ensure the owner
++        # cannot be set to something other than what was used to authenticate
++        auth_headers = {
++            'X-Auth-Token': 'user1:tenant1:role1',
++        }
++        create_headers.update(auth_headers)
++
++        path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
++        http = httplib2.Http()
++        response, content = http.request(path, 'POST', headers=create_headers)
++        self.assertEqual(response.status, 201)
++        data = json.loads(content)
++        image_id = data['image']['id']
++
++        # We have to be admin to see the owner
++        auth_headers = {
++            'X-Auth-Token': 'user1:tenant1:admin',
++        }
++        create_headers.update(auth_headers)
++
++        path = ("http://%s:%d/v1/images/%s" %
++                ("0.0.0.0", self.api_port, image_id))
++        http = httplib2.Http()
++        response, content = http.request(path, 'HEAD', headers=auth_headers)
++        self.assertEqual(response.status, 200)
++        self.assertEqual('tenant1', response['x-image-meta-owner'])
++
++        # Make sure the non-privileged user can't update their owner either
++        update_headers = {
++            'X-Image-Meta-Name': 'MyImage2',
++            'X-Image-Meta-Owner': 'tenant2',
++            'X-Auth-Token': 'user1:tenant1:role1',
++        }
++
++        path = ("http://%s:%d/v1/images/%s" %
++                ("0.0.0.0", self.api_port, image_id))
++        http = httplib2.Http()
++        response, content = http.request(path, 'PUT', headers=update_headers)
++        self.assertEqual(response.status, 200)
++
++        # We have to be admin to see the owner
++        auth_headers = {
++            'X-Auth-Token': 'user1:tenant1:admin',
++        }
++
++        path = ("http://%s:%d/v1/images/%s" %
++                ("0.0.0.0", self.api_port, image_id))
++        http = httplib2.Http()
++        response, content = http.request(path, 'HEAD', headers=auth_headers)
++        self.assertEqual(response.status, 200)
++        self.assertEqual('tenant1', response['x-image-meta-owner'])
++
++        # An admin user should be able to update the owner
++        auth_headers = {
++            'X-Auth-Token': 'user1:tenant3:admin',
++        }
++
++        update_headers = {
++            'X-Image-Meta-Name': 'MyImage2',
++            'X-Image-Meta-Owner': 'tenant2',
++        }
++        update_headers.update(auth_headers)
++
++        path = ("http://%s:%d/v1/images/%s" %
++                ("0.0.0.0", self.api_port, image_id))
++        http = httplib2.Http()
++        response, content = http.request(path, 'PUT', headers=update_headers)
++        self.assertEqual(response.status, 200)
++
++        path = ("http://%s:%d/v1/images/%s" %
++                ("0.0.0.0", self.api_port, image_id))
++        http = httplib2.Http()
++        response, content = http.request(path, 'HEAD', headers=auth_headers)
++        self.assertEqual(response.status, 200)
++        self.assertEqual('tenant2', response['x-image-meta-owner'])
++
++        self.stop_servers()
+diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py
+new file mode 100644
+index 0000000..bbaa052
+--- /dev/null
++++ b/glance/tests/functional/v2/test_images.py
+@@ -0,0 +1,468 @@
++# 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.
++
++import json
++
++import requests
++
++from glance.tests import functional
++from glance.common import utils
++
++
++TENANT1 = utils.generate_uuid()
++TENANT2 = utils.generate_uuid()
++TENANT3 = utils.generate_uuid()
++TENANT4 = utils.generate_uuid()
++
++
++class TestImages(functional.FunctionalTest):
++
++    def setUp(self):
++        super(TestImages, self).setUp()
++        self.cleanup()
++        self.api_server.deployment_flavor = 'noauth'
++        self.start_servers(**self.__dict__.copy())
++
++    def _url(self, path):
++        return 'http://0.0.0.0:%d/v2%s' % (self.api_port, path)
++
++    def _headers(self, custom_headers=None):
++        base_headers = {
++            'X-Identity-Status': 'Confirmed',
++            'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
++            'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
++            'X-Tenant-Id': TENANT1,
++            'X-Roles': 'member',
++        }
++        base_headers.update(custom_headers or {})
++        return base_headers
++
++    def test_image_lifecycle(self):
++        # Image list should be empty
++        path = self._url('/images')
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        images = json.loads(response.text)['images']
++        self.assertEqual(0, len(images))
++
++        # Create an image
++        path = self._url('/images')
++        headers = self._headers({'content-type': 'application/json'})
++        data = json.dumps({'name': 'image-1'})
++        response = requests.post(path, headers=headers, data=data)
++        self.assertEqual(200, response.status_code)
++        image_location_header = response.headers['Location']
++
++        # Returned image entity should have a generated id
++        image = json.loads(response.text)['image']
++        image_id = image['id']
++
++        # Image list should now have one entry
++        path = self._url('/images')
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        images = json.loads(response.text)['images']
++        self.assertEqual(1, len(images))
++        self.assertEqual(images[0]['id'], image_id)
++
++        # Get the image using the returned Location header
++        response = requests.get(image_location_header, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        image = json.loads(response.text)['image']
++        self.assertEqual(image_id, image['id'])
++
++        # The image should be mutable
++        path = self._url('/images/%s' % image_id)
++        data = json.dumps({'name': 'image-2'})
++        response = requests.put(path, headers=self._headers(), data=data)
++        self.assertEqual(200, response.status_code)
++
++        # Returned image entity should reflect the changes
++        image = json.loads(response.text)['image']
++        self.assertEqual('image-2', image['name'])
++
++        # Updates should persist across requests
++        path = self._url('/images/%s' % image_id)
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        image = json.loads(response.text)['image']
++        self.assertEqual(image_id, image['id'])
++        self.assertEqual('image-2', image['name'])
++
++        # Try to download data before its uploaded
++        path = self._url('/images/%s/file' % image_id)
++        headers = self._headers()
++        response = requests.get(path, headers=headers)
++        self.assertEqual(404, response.status_code)
++
++        # Upload some image data
++        path = self._url('/images/%s/file' % image_id)
++        headers = self._headers({'Content-Type': 'application/octet-stream'})
++        response = requests.put(path, headers=headers, data='ZZZZZ')
++        self.assertEqual(200, response.status_code)
++
++        # Try to download the data that was just uploaded
++        path = self._url('/images/%s/file' % image_id)
++        headers = self._headers()
++        response = requests.get(path, headers=headers)
++        self.assertEqual(200, response.status_code)
++        self.assertEqual(response.text, 'ZZZZZ')
++
++        # Deletion should work
++        path = self._url('/images/%s' % image_id)
++        response = requests.delete(path, headers=self._headers())
++        self.assertEqual(204, response.status_code)
++
++        # This image should be no longer be directly accessible
++        path = self._url('/images/%s' % image_id)
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(404, response.status_code)
++
++        # And neither should its data
++        path = self._url('/images/%s/file' % image_id)
++        headers = self._headers()
++        response = requests.get(path, headers=headers)
++        self.assertEqual(404, response.status_code)
++
++        # Image list should now be empty
++        path = self._url('/images')
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        images = json.loads(response.text)['images']
++        self.assertEqual(0, len(images))
++
++        self.stop_servers()
++
++    def test_upload_duplicate_data(self):
++        # Create an image
++        path = self._url('/images')
++        headers = self._headers({'content-type': 'application/json'})
++        data = json.dumps({'name': 'image-1'})
++        response = requests.post(path, headers=headers, data=data)
++        self.assertEqual(200, response.status_code)
++
++        # Returned image entity should have a generated id
++        image = json.loads(response.text)['image']
++        image_id = image['id']
++
++        # Upload some image data
++        path = self._url('/images/%s/file' % image_id)
++        headers = self._headers({'Content-Type': 'application/octet-stream'})
++        response = requests.put(path, headers=headers, data='ZZZZZ')
++        self.assertEqual(200, response.status_code)
++
++        # Uploading duplicate data should be rejected with a 409
++        path = self._url('/images/%s/file' % image_id)
++        headers = self._headers({'Content-Type': 'application/octet-stream'})
++        response = requests.put(path, headers=headers, data='XXX')
++        self.assertEqual(409, response.status_code)
++
++        # Data should not have been overwritten
++        path = self._url('/images/%s/file' % image_id)
++        headers = self._headers()
++        response = requests.get(path, headers=headers)
++        self.assertEqual(200, response.status_code)
++        self.assertEqual(response.text, 'ZZZZZ')
++
++        self.stop_servers()
++
++    def test_permissions(self):
++        # Create an image that belongs to TENANT1
++        path = self._url('/images')
++        headers = self._headers({'Content-Type': 'application/json'})
++        data = json.dumps({'name': 'image-1'})
++        response = requests.post(path, headers=headers, data=data)
++        self.assertEqual(200, response.status_code)
++        image_id = json.loads(response.text)['image']['id']
++
++        # TENANT1 should see the image in their list
++        path = self._url('/images')
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        images = json.loads(response.text)['images']
++        self.assertEqual(image_id, images[0]['id'])
++
++        # TENANT1 should be able to access the image directly
++        path = self._url('/images/%s' % image_id)
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++
++        # TENANT2 should not see the image in their list
++        path = self._url('/images')
++        headers = self._headers({'X-Tenant-Id': TENANT2})
++        response = requests.get(path, headers=headers)
++        self.assertEqual(200, response.status_code)
++        images = json.loads(response.text)['images']
++        self.assertEqual(0, len(images))
++
++        # TENANT2 should not be able to access the image directly
++        path = self._url('/images/%s' % image_id)
++        headers = self._headers({'X-Tenant-Id': TENANT2})
++        response = requests.get(path, headers=headers)
++        self.assertEqual(404, response.status_code)
++
++        # TENANT2 should not be able to modify the image, either
++        path = self._url('/images/%s' % image_id)
++        headers = self._headers({
++            'Content-Type': 'application/json',
++            'X-Tenant-Id': TENANT2,
++        })
++        data = json.dumps({'name': 'image-2'})
++        response = requests.put(path, headers=headers, data=data)
++        self.assertEqual(404, response.status_code)
++
++        # TENANT2 should not be able to delete the image, either
++        path = self._url('/images/%s' % image_id)
++        headers = self._headers({'X-Tenant-Id': TENANT2})
++        response = requests.delete(path, headers=headers)
++        self.assertEqual(404, response.status_code)
++
++        # Share the image with TENANT2
++        path = self._url('/images/%s/access' % image_id)
++        data = json.dumps({'tenant_id': TENANT2, 'can_share': False})
++        request_headers = {'Content-Type': 'application/json'}
++        headers = self._headers(request_headers)
++        response = requests.post(path, headers=headers, data=data)
++        self.assertEqual(201, response.status_code)
++
++        # TENANT2 should see the image in their list
++        path = self._url('/images')
++        headers = self._headers({'X-Tenant-Id': TENANT2})
++        response = requests.get(path, headers=headers)
++        self.assertEqual(200, response.status_code)
++        images = json.loads(response.text)['images']
++        self.assertEqual(image_id, images[0]['id'])
++
++        # TENANT2 should be able to access the image directly
++        path = self._url('/images/%s' % image_id)
++        headers = self._headers({'X-Tenant-Id': TENANT2})
++        response = requests.get(path, headers=headers)
++        self.assertEqual(200, response.status_code)
++
++        # TENANT2 should not be able to modify the image
++        path = self._url('/images/%s' % image_id)
++        headers = self._headers({
++            'Content-Type': 'application/json',
++            'X-Tenant-Id': TENANT2,
++        })
++        data = json.dumps({'name': 'image-2'})
++        response = requests.put(path, headers=headers, data=data)
++        self.assertEqual(404, response.status_code)
++
++        # TENANT2 should not be able to delete the image, either
++        path = self._url('/images/%s' % image_id)
++        headers = self._headers({'X-Tenant-Id': TENANT2})
++        response = requests.delete(path, headers=headers)
++        self.assertEqual(404, response.status_code)
++
++        # As an unshared tenant, TENANT3 should not have access to the image
++        path = self._url('/images/%s' % image_id)
++        headers = self._headers({'X-Tenant-Id': TENANT3})
++        response = requests.get(path, headers=headers)
++        self.assertEqual(404, response.status_code)
++
++        # Publicize the image as an admin of TENANT1
++        path = self._url('/images/%s' % image_id)
++        headers = self._headers({
++            'Content-Type': 'application/json',
++            'X-Roles': 'admin',
++        })
++        data = json.dumps({'visibility': 'public'})
++        response = requests.put(path, headers=headers, data=data)
++        self.assertEqual(200, response.status_code)
++
++        # TENANT3 should now see the image in their list
++        path = self._url('/images')
++        headers = self._headers({'X-Tenant-Id': TENANT3})
++        response = requests.get(path, headers=headers)
++        self.assertEqual(200, response.status_code)
++        images = json.loads(response.text)['images']
++        self.assertEqual(image_id, images[0]['id'])
++
++        # TENANT3 should also be able to access the image directly
++        path = self._url('/images/%s' % image_id)
++        headers = self._headers({'X-Tenant-Id': TENANT3})
++        response = requests.get(path, headers=headers)
++        self.assertEqual(200, response.status_code)
++
++        # TENANT3 still should not be able to modify the image
++        path = self._url('/images/%s' % image_id)
++        headers = self._headers({
++            'Content-Type': 'application/json',
++            'X-Tenant-Id': TENANT3,
++        })
++        data = json.dumps({'name': 'image-2'})
++        response = requests.put(path, headers=headers, data=data)
++        self.assertEqual(404, response.status_code)
++
++        # TENANT3 should not be able to delete the image, either
++        path = self._url('/images/%s' % image_id)
++        headers = self._headers({'X-Tenant-Id': TENANT3})
++        response = requests.delete(path, headers=headers)
++        self.assertEqual(404, response.status_code)
++
++        self.stop_servers()
++
++    def test_access_lifecycle(self):
++        # Create an image for our tests
++        path = self._url('/images')
++        headers = self._headers({'Content-Type': 'application/json'})
++        data = json.dumps({'name': 'image-1'})
++        response = requests.post(path, headers=headers, data=data)
++        self.assertEqual(200, response.status_code)
++        image_id = json.loads(response.text)['image']['id']
++
++        # Image acccess list should be empty
++        path = self._url('/images/%s/access' % image_id)
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        access_records = json.loads(response.text)['access_records']
++        self.assertEqual(0, len(access_records))
++
++        # Other tenants shouldn't be able to share by default, and shouldn't
++        # even know the image exists
++        path = self._url('/images/%s/access' % image_id)
++        data = json.dumps({'tenant_id': TENANT3, 'can_share': False})
++        request_headers = {
++            'Content-Type': 'application/json',
++            'X-Tenant-Id': TENANT2,
++        }
++        headers = self._headers(request_headers)
++        response = requests.post(path, headers=headers, data=data)
++        self.assertEqual(404, response.status_code)
++
++        # Share the image with another tenant
++        path = self._url('/images/%s/access' % image_id)
++        data = json.dumps({'tenant_id': TENANT2, 'can_share': True})
++        headers = self._headers({'Content-Type': 'application/json'})
++        response = requests.post(path, headers=headers, data=data)
++        self.assertEqual(201, response.status_code)
++        access_location = response.headers['Location']
++
++        # Ensure the access record was actually created
++        response = requests.get(access_location, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++
++        # Make sure the sharee can further share the image
++        path = self._url('/images/%s/access' % image_id)
++        data = json.dumps({'tenant_id': TENANT3, 'can_share': False})
++        request_headers = {
++            'Content-Type': 'application/json',
++            'X-Tenant-Id': TENANT2,
++        }
++        headers = self._headers(request_headers)
++        response = requests.post(path, headers=headers, data=data)
++        self.assertEqual(201, response.status_code)
++        access_location = response.headers['Location']
++
++        # Ensure the access record was actually created
++        response = requests.get(access_location, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++
++        # The third tenant should not be able to share it further
++        path = self._url('/images/%s/access' % image_id)
++        data = json.dumps({'tenant_id': TENANT4, 'can_share': False})
++        request_headers = {
++            'Content-Type': 'application/json',
++            'X-Tenant-Id': TENANT3,
++        }
++        headers = self._headers(request_headers)
++        response = requests.post(path, headers=headers, data=data)
++        self.assertEqual(403, response.status_code)
++
++        # Image acccess list should now contain 2 entries
++        path = self._url('/images/%s/access' % image_id)
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        access_records = json.loads(response.text)['access_records']
++        self.assertEqual(2, len(access_records))
++
++        # Delete an access record
++        response = requests.delete(access_location, headers=self._headers())
++        self.assertEqual(204, response.status_code)
++
++        # Ensure the access record was actually deleted
++        response = requests.get(access_location, headers=self._headers())
++        self.assertEqual(404, response.status_code)
++
++        # Image acccess list should now contain 1 entry
++        path = self._url('/images/%s/access' % image_id)
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        access_records = json.loads(response.text)['access_records']
++        self.assertEqual(1, len(access_records))
++
++        self.stop_servers()
++
++    def test_tag_lifecycle(self):
++        # Create an image for our tests
++        path = self._url('/images')
++        headers = self._headers({'Content-Type': 'application/json'})
++        data = json.dumps({'name': 'image-1'})
++        response = requests.post(path, headers=headers, data=data)
++        self.assertEqual(200, response.status_code)
++        image_id = json.loads(response.text)['image']['id']
++
++        # List of image tags should be empty
++        path = self._url('/images/%s/tags' % image_id)
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        tags = json.loads(response.text)
++        self.assertEqual([], tags)
++
++        # Create a tag
++        path = self._url('/images/%s/tags/sniff' % image_id)
++        response = requests.put(path, headers=self._headers())
++        self.assertEqual(204, response.status_code)
++
++        # List should now have an entry
++        path = self._url('/images/%s/tags' % image_id)
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        tags = json.loads(response.text)
++        self.assertEqual(['sniff'], tags)
++
++        # Create a more complex tag
++        path = self._url('/images/%s/tags/someone%%40example.com' % image_id)
++        response = requests.put(path, headers=self._headers())
++        self.assertEqual(204, response.status_code)
++
++        # List should reflect our new tag
++        path = self._url('/images/%s/tags' % image_id)
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        tags = json.loads(response.text)
++        self.assertEqual(['sniff', 'someone at example.com'], tags)
++
++        # The tag should be deletable
++        path = self._url('/images/%s/tags/someone%%40example.com' % image_id)
++        response = requests.delete(path, headers=self._headers())
++        self.assertEqual(204, response.status_code)
++
++        # List should reflect the deletion
++        path = self._url('/images/%s/tags' % image_id)
++        response = requests.get(path, headers=self._headers())
++        self.assertEqual(200, response.status_code)
++        tags = json.loads(response.text)
++        self.assertEqual(['sniff'], tags)
++
++        # Deleting the same tag should return a 404
++        path = self._url('/images/%s/tags/someonei%%40example.com' % image_id)
++        response = requests.delete(path, headers=self._headers())
++        self.assertEqual(404, response.status_code)
++
++        self.stop_servers()
diff --git a/openstack-glance.spec b/openstack-glance.spec
index 5eebb7e..01c73b7 100644
--- a/openstack-glance.spec
+++ b/openstack-glance.spec
@@ -1,6 +1,6 @@
 Name:             openstack-glance
 Version:          2012.1
-Release:          5%{?dist}
+Release:          6%{?dist}
 Summary:          OpenStack Image Service
 
 Group:            Applications/System
@@ -17,7 +17,11 @@ Source4:          openstack-glance-db-setup
 #
 Patch0001: 0001-Ensure-swift-auth-URL-includes-trailing-slash.patch
 Patch0002: 0002-search-for-logger-in-PATH.patch
-Patch0003: 0003-Don-t-access-the-net-while-building-docs.patch
+Patch0003: 0003-Fix-content-type-for-qpid-notifier.patch
+Patch0004: 0004-Omit-Content-Length-on-chunked-transfer.patch
+Patch0005: 0005-Fix-i18n-in-glance.notifier.notify_kombu.patch
+Patch0006: 0006-Don-t-access-the-net-while-building-docs.patch
+Patch0007: 0007-Support-DB-auto-create-suppression.patch
 
 BuildArch:        noarch
 BuildRequires:    python2-devel
@@ -95,6 +99,10 @@ This package contains documentation files for glance.
 %patch0001 -p1
 %patch0002 -p1
 %patch0003 -p1
+%patch0004 -p1
+%patch0005 -p1
+%patch0006 -p1
+%patch0007 -p1
 
 sed -i 's|\(sql_connection = \)sqlite:///glance.sqlite|\1mysql://glance:glance@localhost/glance|' etc/glance-registry.conf
 
@@ -228,6 +236,9 @@ fi
 %doc doc/build/html
 
 %changelog
+* Mon May 21 2012 Pádraig Brady <P at draigBrady.com> - 2012.1-6
+- Sync with essex stable
+
 * Fri May 18 2012 Alan Pevec <apevec at redhat.com> - 2012.1-5
 - Drop hard dep on python-kombu, notifications are configurable
 


More information about the scm-commits mailing list