[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 <traceback object at 0x1918d40>
++
++ :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