From 214824b097696122ae45efa7b86f56ff20a3bceb Mon Sep 17 00:00:00 2001 From: Pádraig Brady Date: May 28 2012 16:09:29 +0000 Subject: Update to the folsom openstack release --- diff --git a/.gitignore b/.gitignore index 94a6b6a..d85deb5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /glance-2012.1~rc1.tar.gz /glance-2012.1~rc2.tar.gz /glance-2012.1.tar.gz +/glance-2012.2~f1.tar.gz diff --git a/0001-Don-t-access-the-net-while-building-docs.patch b/0001-Don-t-access-the-net-while-building-docs.patch new file mode 100644 index 0000000..c111aea --- /dev/null +++ b/0001-Don-t-access-the-net-while-building-docs.patch @@ -0,0 +1,25 @@ +From 0b63dcd89930d3a96651f0de8e23210f400094f9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?P=C3=A1draig=20Brady?= +Date: Fri, 6 Jan 2012 17:12:54 +0000 +Subject: [PATCH] Don't access the net while building docs + +(Note this hasn't been submitted upstream) + +Change-Id: I42c6e3a5062db209a0abe00cebc04d383c79cbcb +(cherry picked from commit f2b4bb4e45afcc178200966193a7b87401c534d7) +--- + doc/source/conf.py | 1 - + 1 files changed, 0 insertions(+), 1 deletions(-) + +diff --git a/doc/source/conf.py b/doc/source/conf.py +index bfe640a..6053d66 100644 +--- a/doc/source/conf.py ++++ b/doc/source/conf.py +@@ -45,7 +45,6 @@ sys.path = [os.path.abspath('../../glance'), + extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.coverage', + 'sphinx.ext.ifconfig', +- 'sphinx.ext.intersphinx', + 'sphinx.ext.pngmath', + 'sphinx.ext.graphviz'] + diff --git a/0001-Ensure-swift-auth-URL-includes-trailing-slash.patch b/0001-Ensure-swift-auth-URL-includes-trailing-slash.patch deleted file mode 100644 index 0356dde..0000000 --- a/0001-Ensure-swift-auth-URL-includes-trailing-slash.patch +++ /dev/null @@ -1,37 +0,0 @@ -From f136e7e6804bc02c9a623905d94eac955e2df0a3 Mon Sep 17 00:00:00 2001 -From: Eoghan Glynn -Date: Thu, 12 Apr 2012 11:27:18 +0100 -Subject: [PATCH] Ensure swift auth URL includes trailing slash - -Fixes bug 979745 - -Image objects in swift were previously leaked post-deletion -due to a silent auth failure caused by the absense of the -trailing forward slash on the swift connection auth URL. - -Change-Id: I9c73a2f75a6466e73801ababdd81db77701ccb20 ---- - glance/store/swift.py | 6 ++++-- - 1 files changed, 4 insertions(+), 2 deletions(-) - -diff --git a/glance/store/swift.py b/glance/store/swift.py -index 2659daa..6db54c4 100644 ---- a/glance/store/swift.py -+++ b/glance/store/swift.py -@@ -303,12 +303,14 @@ class Store(glance.store.base.Store): - """ - snet = self.snet - auth_version = self.auth_version -+ full_auth_url = (auth_url if not auth_url or auth_url.endswith('/') -+ else auth_url + '/') - logger.debug(_("Creating Swift connection with " -- "(auth_address=%(auth_url)s, user=%(user)s, " -+ "(auth_address=%(full_auth_url)s, user=%(user)s, " - "snet=%(snet)s, auth_version=%(auth_version)s)") % - locals()) - return swift_client.Connection( -- authurl=auth_url, user=user, key=key, snet=snet, -+ authurl=full_auth_url, user=user, key=key, snet=snet, - auth_version=auth_version) - - def _option_get(self, param): diff --git a/0002-improve-DB-auto-create-suppression-config-presentati.patch b/0002-improve-DB-auto-create-suppression-config-presentati.patch new file mode 100644 index 0000000..4b87eef --- /dev/null +++ b/0002-improve-DB-auto-create-suppression-config-presentati.patch @@ -0,0 +1,27 @@ +From 21919c82bcbcf4bbd41e2ca5df3728ed672fa57e Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?P=C3=A1draig=20Brady?= +Date: Fri, 18 May 2012 14:23:41 +0100 +Subject: [PATCH] improve DB auto-create suppression config presentation + +Add the default option (matching upstream), +so that we can provide comments in the config file, +and also position the option close to related ones. +--- + etc/glance-registry.conf | 4 ++++ + 1 files changed, 4 insertions(+), 0 deletions(-) + +diff --git a/etc/glance-registry.conf b/etc/glance-registry.conf +index 8597411..37d71ff 100644 +--- a/etc/glance-registry.conf ++++ b/etc/glance-registry.conf +@@ -23,6 +23,10 @@ backlog = 4096 + # See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine + sql_connection = sqlite:///glance.sqlite + ++# Whether the glance service creates the database tables ++# automatically at startup, or explicitly with db_sync ++db_auto_create = True ++ + # Period in seconds after which SQLAlchemy should reestablish its connection + # to the database. + # diff --git a/0002-search-for-logger-in-PATH.patch b/0002-search-for-logger-in-PATH.patch deleted file mode 100644 index 68e4984..0000000 --- a/0002-search-for-logger-in-PATH.patch +++ /dev/null @@ -1,38 +0,0 @@ -From 98913da774a7b5a978e010d754b77a352bd51ec9 Mon Sep 17 00:00:00 2001 -From: "J. Daniel Schmidt" -Date: Wed, 11 Apr 2012 15:19:55 +0200 -Subject: [PATCH] search for logger in PATH - -fixes bug 978907 - -Change-Id: I16a20982cc90f3857e20ab23e2b3f5d1aa2722a0 ---- - Authors | 1 + - bin/glance-control | 2 +- - 2 files changed, 2 insertions(+), 1 deletions(-) - -diff --git a/Authors b/Authors -index 96a506b..31778b9 100644 ---- a/Authors -+++ b/Authors -@@ -20,6 +20,7 @@ Ewan Mellor - Gabriel Hurley - Hengqing Hu - Isaku Yamahata -+J. Daniel Schmidt - Jason Koelker - Jay Pipes - James E. Blair -diff --git a/bin/glance-control b/bin/glance-control -index fac89b2..15473ac 100755 ---- a/bin/glance-control -+++ b/bin/glance-control -@@ -133,7 +133,7 @@ def do_start(verb, server, conf, args): - pass - - def redirect_to_syslog(fds, server): -- log_cmd = '/usr/bin/logger -t "%s[%d]"' % (server, os.getpid()) -+ log_cmd = 'logger -t "%s[%d]"' % (server, os.getpid()) - process = subprocess.Popen(log_cmd, - shell=True, - stdin=subprocess.PIPE) diff --git a/0003-Fix-content-type-for-qpid-notifier.patch b/0003-Fix-content-type-for-qpid-notifier.patch deleted file mode 100644 index 032a37a..0000000 --- a/0003-Fix-content-type-for-qpid-notifier.patch +++ /dev/null @@ -1,63 +0,0 @@ -From 5838b6390353e4c34762828684cee90410e4f8b1 Mon Sep 17 00:00:00 2001 -From: Russell Bryant -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 deleted file mode 100644 index eebc8d9..0000000 --- a/0004-Omit-Content-Length-on-chunked-transfer.patch +++ /dev/null @@ -1,183 +0,0 @@ -From 7a9e3a7dc53a20971dbd653f2061337efec136a2 Mon Sep 17 00:00:00 2001 -From: Mike Lundy -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 deleted file mode 100644 index 551d557..0000000 --- a/0005-Fix-i18n-in-glance.notifier.notify_kombu.patch +++ /dev/null @@ -1,28 +0,0 @@ -From 54622953c4cc09196c7f16d8229ba438afbe626f Mon Sep 17 00:00:00 2001 -From: Brian Waldon -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/0006-Don-t-access-the-net-while-building-docs.patch b/0006-Don-t-access-the-net-while-building-docs.patch deleted file mode 100644 index 3b5e7f1..0000000 --- a/0006-Don-t-access-the-net-while-building-docs.patch +++ /dev/null @@ -1,25 +0,0 @@ -From 6a63200908b9efd846c59a0747f10502d4d819fe Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?P=C3=A1draig=20Brady?= -Date: Fri, 6 Jan 2012 17:12:54 +0000 -Subject: [PATCH] Don't access the net while building docs - -(Note this hasn't been submitted upstream) - -Change-Id: I42c6e3a5062db209a0abe00cebc04d383c79cbcb -(cherry picked from commit f2b4bb4e45afcc178200966193a7b87401c534d7) ---- - doc/source/conf.py | 1 - - 1 files changed, 0 insertions(+), 1 deletions(-) - -diff --git a/doc/source/conf.py b/doc/source/conf.py -index a6a3c35..670e4f3 100644 ---- a/doc/source/conf.py -+++ b/doc/source/conf.py -@@ -45,7 +45,6 @@ sys.path = [os.path.abspath('../../glance'), - extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.ifconfig', -- 'sphinx.ext.intersphinx', - 'sphinx.ext.pngmath', - 'sphinx.ext.graphviz'] - diff --git a/0007-Support-DB-auto-create-suppression.patch b/0007-Support-DB-auto-create-suppression.patch deleted file mode 100644 index 5e5080b..0000000 --- a/0007-Support-DB-auto-create-suppression.patch +++ /dev/null @@ -1,2222 +0,0 @@ -From d44afa05c270335d6c9812913f35d75669f0886a Mon Sep 17 00:00:00 2001 -From: Eoghan Glynn -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 +- - etc/glance-registry.conf | 4 + - glance/registry/db/api.py | 26 +- - 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 +++++++++ - 9 files changed, 1964 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/etc/glance-registry.conf b/etc/glance-registry.conf -index 8597411..37d71ff 100644 ---- a/etc/glance-registry.conf -+++ b/etc/glance-registry.conf -@@ -23,6 +23,10 @@ backlog = 4096 - # See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine - sql_connection = sqlite:///glance.sqlite - -+# Whether the glance service creates the database tables -+# automatically at startup, or explicitly with db_sync -+db_auto_create = True -+ - # Period in seconds after which SQLAlchemy should reestablish its connection - # to the database. - # -diff --git a/glance/registry/db/api.py b/glance/registry/db/api.py -index 04fe2e5..b2e3c00 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,12 @@ 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 -+ try: -+ getattr(conf, opt.name) -+ except cfg.NoSuchOptError: -+ conf.register_opt(opt) - sql_connection = conf.sql_connection - _MAX_RETRIES = conf.sql_max_retries - _RETRY_INTERVAL = conf.sql_retry_interval -@@ -131,12 +137,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 '+' --> 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@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 4745932..8026955 100644 --- a/openstack-glance.spec +++ b/openstack-glance.spec @@ -1,26 +1,21 @@ Name: openstack-glance -Version: 2012.1 -Release: 8%{?dist} +Version: 2012.2 +Release: 1%{?dist} Summary: OpenStack Image Service Group: Applications/System License: ASL 2.0 URL: http://glance.openstack.org -Source0: https://launchpad.net/glance/essex/2012.1/+download/glance-%{version}.tar.gz +Source0: https://launchpad.net/glance/folsom/folsom-1/+download/glance-%{version}~f1.tar.gz Source1: openstack-glance-api.service Source2: openstack-glance-registry.service Source3: openstack-glance.logrotate # -# patches_base=2012.1 +# patches_base=folsom-1 # -Patch0001: 0001-Ensure-swift-auth-URL-includes-trailing-slash.patch -Patch0002: 0002-search-for-logger-in-PATH.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 +Patch0001: 0001-Don-t-access-the-net-while-building-docs.patch +Patch0002: 0002-improve-DB-auto-create-suppression-config-presentati.patch BuildArch: noarch BuildRequires: python2-devel @@ -55,6 +50,7 @@ Requires: pysendfile Requires: python-eventlet Requires: python-httplib2 Requires: python-iso8601 +Requires: python-jsonschema Requires: python-migrate Requires: python-paste-deploy Requires: python-routes @@ -99,11 +95,6 @@ This package contains documentation files for glance. %patch0001 -p1 %patch0002 -p1 -%patch0003 -p1 -%patch0004 -p1 -%patch0005 -p1 -%patch0006 -p1 -%patch0007 -p1 sed -i '/\/usr\/bin\/env python/d' glance/common/config.py glance/registry/db/migrate_repo/manage.py @@ -236,6 +227,9 @@ fi %doc doc/build/html %changelog +* Tue May 28 2012 Pádraig Brady - 2012.2-1 +- Update to Folsom milestone 1 + * Tue May 22 2012 Pádraig Brady - 2012.1-8 - Fix an issue with glance-manage db_sync (#823702) diff --git a/sources b/sources index 0be26f0..bca0c75 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -751273bbb1bd0a05a66409e32aa2ba63 glance-2012.1.tar.gz +06ea4bf13949ae8714fed54158098a5b glance-2012.2~f1.tar.gz