summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPádraig Brady <P@draigBrady.com>2012-05-28 15:47:45 (GMT)
committerPádraig Brady <P@draigBrady.com>2012-05-28 16:09:29 (GMT)
commit214824b097696122ae45efa7b86f56ff20a3bceb (patch)
treebf7fc3f302e297cdf7a81f845163c566043e8de7
parent820bbb6e3f1582a6c04c86b00bffda01e74a38db (diff)
downloadopenstack-glance-214824b0.zip
openstack-glance-214824b0.tar.gz
openstack-glance-214824b0.tar.xz
Update to the folsom openstack release
-rw-r--r--.gitignore1
-rw-r--r--0001-Don-t-access-the-net-while-building-docs.patch (renamed from 0006-Don-t-access-the-net-while-building-docs.patch)4
-rw-r--r--0001-Ensure-swift-auth-URL-includes-trailing-slash.patch37
-rw-r--r--0002-improve-DB-auto-create-suppression-config-presentati.patch27
-rw-r--r--0002-search-for-logger-in-PATH.patch38
-rw-r--r--0003-Fix-content-type-for-qpid-notifier.patch63
-rw-r--r--0004-Omit-Content-Length-on-chunked-transfer.patch183
-rw-r--r--0005-Fix-i18n-in-glance.notifier.notify_kombu.patch28
-rw-r--r--0007-Support-DB-auto-create-suppression.patch2222
-rw-r--r--openstack-glance.spec26
-rw-r--r--sources2
11 files changed, 41 insertions, 2590 deletions
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/0006-Don-t-access-the-net-while-building-docs.patch b/0001-Don-t-access-the-net-while-building-docs.patch
index 3b5e7f1..c111aea 100644
--- a/0006-Don-t-access-the-net-while-building-docs.patch
+++ b/0001-Don-t-access-the-net-while-building-docs.patch
@@ -1,4 +1,4 @@
-From 6a63200908b9efd846c59a0747f10502d4d819fe Mon Sep 17 00:00:00 2001
+From 0b63dcd89930d3a96651f0de8e23210f400094f9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <pbrady@redhat.com>
Date: Fri, 6 Jan 2012 17:12:54 +0000
Subject: [PATCH] Don't access the net while building docs
@@ -12,7 +12,7 @@ Change-Id: I42c6e3a5062db209a0abe00cebc04d383c79cbcb
1 files changed, 0 insertions(+), 1 deletions(-)
diff --git a/doc/source/conf.py b/doc/source/conf.py
-index a6a3c35..670e4f3 100644
+index bfe640a..6053d66 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -45,7 +45,6 @@ sys.path = [os.path.abspath('../../glance'),
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 <eglynn@redhat.com>
-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?= <pbrady@redhat.com>
+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" <jdsn@suse.de>
-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 <ewan.mellor@citrix.com>
- Gabriel Hurley <gabriel@strikeawe.com>
- Hengqing Hu <hudayou@hotmail.com>
- Isaku Yamahata <yamahata@valinux.co.jp>
-+J. Daniel Schmidt <jdsn@suse.de>
- Jason Koelker <jason@koelker.net>
- Jay Pipes <jaypipes@gmail.com>
- James E. Blair <jeblair@hp.com>
-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 <rbryant@redhat.com>
-Date: Tue, 24 Apr 2012 12:24:39 -0400
-Subject: [PATCH] Fix content type for qpid notifier.
-
-Fix bug 980872.
-
-This patch fixes a regression I introduced in
-2d36facf14f4eb2742ba46274e04a73b5231aece. In that patch, I adjusted the
-content_type for messages sent with the qpid notifier to be
-'application/json' to match a change that went into the kombu notifier.
-Unfortunately, it's wrong.
-
-I assumed based on the kombu change that notifications were being json
-encoded before being passed into the notification driver. That's not
-the case. The message is a dict. So, just revert the change to set the
-content_type and let Qpid encode the notification as 'amqp/map'.
-
-(cherry picked from commit 5bed23cbc962d3c6503f0ff93e6d1e326efbd49d)
-
-Change-Id: I8ba039612d9603377028ba72cb80ae89d675c741
----
- glance/notifier/notify_qpid.py | 9 +++------
- glance/tests/unit/test_notifier.py | 2 +-
- 2 files changed, 4 insertions(+), 7 deletions(-)
-
-diff --git a/glance/notifier/notify_qpid.py b/glance/notifier/notify_qpid.py
-index d3d5514..a0535a6 100644
---- a/glance/notifier/notify_qpid.py
-+++ b/glance/notifier/notify_qpid.py
-@@ -135,16 +135,13 @@ class QpidStrategy(strategy.Strategy):
- return self.session.sender(address)
-
- def warn(self, msg):
-- qpid_msg = qpid.messaging.Message(content=msg,
-- content_type='application/json')
-+ qpid_msg = qpid.messaging.Message(content=msg)
- self.sender_warn.send(qpid_msg)
-
- def info(self, msg):
-- qpid_msg = qpid.messaging.Message(content=msg,
-- content_type='application/json')
-+ qpid_msg = qpid.messaging.Message(content=msg)
- self.sender_info.send(qpid_msg)
-
- def error(self, msg):
-- qpid_msg = qpid.messaging.Message(content=msg,
-- content_type='application/json')
-+ qpid_msg = qpid.messaging.Message(content=msg)
- self.sender_error.send(qpid_msg)
-diff --git a/glance/tests/unit/test_notifier.py b/glance/tests/unit/test_notifier.py
-index 0e7ff75..b952ee3 100644
---- a/glance/tests/unit/test_notifier.py
-+++ b/glance/tests/unit/test_notifier.py
-@@ -316,7 +316,7 @@ class TestQpidNotifier(unittest.TestCase):
- super(TestQpidNotifier, self).tearDown()
-
- def _test_notify(self, priority):
-- test_msg = json.dumps({'a': 'b'})
-+ test_msg = {'a': 'b'}
-
- self.mock_connection = self.mocker.CreateMock(self.orig_connection)
- self.mock_session = self.mocker.CreateMock(self.orig_session)
diff --git a/0004-Omit-Content-Length-on-chunked-transfer.patch b/0004-Omit-Content-Length-on-chunked-transfer.patch
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 <mike@pistoncloud.com>
-Date: Fri, 13 Apr 2012 19:53:16 -0700
-Subject: [PATCH] Omit Content-Length on chunked transfer
-
-Content-Length and Transfer-Encoding conflict according to the HTTP
-spec. This fixes bug 981332.
-
-This also adds the ability to test both the sendfile-present and
-sendfile-absent codepaths; the sendfile-present test will be skipped on
-sendfile-absent platforms.
-
-[ This is backported from 223fbee49a55691504623fa691bbb3e48048d5f3 ]
-
-Change-Id: I20856eb51ff66fe4b7145f796a540a832c3aa4d8
----
- glance/common/client.py | 9 +++++++--
- glance/tests/stubs.py | 29 +++++++++++++++++++++++------
- glance/tests/unit/test_clients.py | 15 +++++++++++++--
- 3 files changed, 43 insertions(+), 10 deletions(-)
-
-diff --git a/glance/common/client.py b/glance/common/client.py
-index 7c6ae55..2e1f35c 100644
---- a/glance/common/client.py
-+++ b/glance/common/client.py
-@@ -506,12 +506,17 @@ class BaseClient(object):
- elif _filelike(body) or self._iterable(body):
- c.putrequest(method, path)
-
-+ use_sendfile = self._sendable(body)
-+
-+ # According to HTTP/1.1, Content-Length and Transfer-Encoding
-+ # conflict.
- for header, value in headers.items():
-- c.putheader(header, value)
-+ if use_sendfile or header.lower() != 'content-length':
-+ c.putheader(header, value)
-
- iter = self.image_iterator(c, headers, body)
-
-- if self._sendable(body):
-+ if use_sendfile:
- # send actual file without copying into userspace
- _sendbody(c, iter)
- else:
-diff --git a/glance/tests/stubs.py b/glance/tests/stubs.py
-index dba4d7f..776da40 100644
---- a/glance/tests/stubs.py
-+++ b/glance/tests/stubs.py
-@@ -59,7 +59,7 @@ def stub_out_registry_and_store_server(stubs, base_dir):
- def close(self):
- return True
-
-- def request(self, method, url, body=None, headers={}):
-+ def request(self, method, url, body=None, headers=None):
- self.req = webob.Request.blank("/" + url.lstrip("/"))
- self.req.method = method
- if headers:
-@@ -110,7 +110,8 @@ def stub_out_registry_and_store_server(stubs, base_dir):
-
- def __init__(self, *args, **kwargs):
- self.sock = FakeSocket()
-- pass
-+ self.stub_force_sendfile = kwargs.get('stub_force_sendfile',
-+ SENDFILE_SUPPORTED)
-
- def connect(self):
- return True
-@@ -120,7 +121,7 @@ def stub_out_registry_and_store_server(stubs, base_dir):
-
- def putrequest(self, method, url):
- self.req = webob.Request.blank("/" + url.lstrip("/"))
-- if SENDFILE_SUPPORTED:
-+ if self.stub_force_sendfile:
- fake_sendfile = FakeSendFile(self.req)
- stubs.Set(sendfile, 'sendfile', fake_sendfile.sendfile)
- self.req.method = method
-@@ -129,7 +130,10 @@ def stub_out_registry_and_store_server(stubs, base_dir):
- self.req.headers[key] = value
-
- def endheaders(self):
-- pass
-+ hl = [i.lower() for i in self.req.headers.keys()]
-+ assert not ('content-length' in hl and
-+ 'transfer-encoding' in hl), \
-+ 'Content-Length and Transfer-Encoding are mutually exclusive'
-
- def send(self, data):
- # send() is called during chunked-transfer encoding, and
-@@ -137,7 +141,7 @@ def stub_out_registry_and_store_server(stubs, base_dir):
- # only write the actual data in tests.
- self.req.body += data.split("\r\n")[1]
-
-- def request(self, method, url, body=None, headers={}):
-+ def request(self, method, url, body=None, headers=None):
- self.req = webob.Request.blank("/" + url.lstrip("/"))
- self.req.method = method
- if headers:
-@@ -187,8 +191,21 @@ def stub_out_registry_and_store_server(stubs, base_dir):
- for i in self.source.app_iter:
- yield i
-
-+ def fake_sendable(self, body):
-+ force = getattr(self, 'stub_force_sendfile', None)
-+ if force is None:
-+ return self._stub_orig_sendable(body)
-+ else:
-+ if force:
-+ assert glance.common.client.SENDFILE_SUPPORTED
-+ return force
-+
- stubs.Set(glance.common.client.BaseClient, 'get_connection_type',
- fake_get_connection_type)
-+ setattr(glance.common.client.BaseClient, '_stub_orig_sendable',
-+ glance.common.client.BaseClient._sendable)
-+ stubs.Set(glance.common.client.BaseClient, '_sendable',
-+ fake_sendable)
- stubs.Set(glance.common.client.ImageBodyIterator, '__iter__',
- fake_image_iter)
-
-@@ -211,7 +228,7 @@ def stub_out_registry_server(stubs, **kwargs):
- def close(self):
- return True
-
-- def request(self, method, url, body=None, headers={}):
-+ def request(self, method, url, body=None, headers=None):
- self.req = webob.Request.blank("/" + url.lstrip("/"))
- self.req.method = method
- if headers:
-diff --git a/glance/tests/unit/test_clients.py b/glance/tests/unit/test_clients.py
-index e8e71de..1548f48 100644
---- a/glance/tests/unit/test_clients.py
-+++ b/glance/tests/unit/test_clients.py
-@@ -26,7 +26,7 @@ import stubout
- import webob
-
- from glance import client
--from glance.common import context
-+from glance.common import client as base_client
- from glance.common import exception
- from glance.common import utils
- from glance.registry.db import api as db_api
-@@ -36,6 +36,7 @@ from glance.registry import context as rcontext
- from glance.tests import stubs
- from glance.tests import utils as test_utils
- from glance.tests.unit import base
-+from glance.tests import utils as test_utils
-
- CONF = {'sql_connection': 'sqlite://'}
-
-@@ -1842,7 +1843,7 @@ class TestClient(base.IsolatedUnitTest):
- for k, v in fixture.items():
- self.assertEquals(v, new_meta[k])
-
-- def test_add_image_with_image_data_as_file(self):
-+ def add_image_with_image_data_as_file(self, sendfile):
- """Tests can add image by passing image data as file"""
- fixture = {'name': 'fake public image',
- 'is_public': True,
-@@ -1852,6 +1853,8 @@ class TestClient(base.IsolatedUnitTest):
- 'properties': {'distro': 'Ubuntu 10.04 LTS'},
- }
-
-+ self.client.stub_force_sendfile = sendfile
-+
- image_data_fixture = r"chunk00000remainder"
-
- tmp_image_filepath = '/tmp/rubbish-image'
-@@ -1879,6 +1882,14 @@ class TestClient(base.IsolatedUnitTest):
- for k, v in fixture.items():
- self.assertEquals(v, new_meta[k])
-
-+ @test_utils.skip_if(not base_client.SENDFILE_SUPPORTED,
-+ 'sendfile not supported')
-+ def test_add_image_with_image_data_as_file_with_sendfile(self):
-+ self.add_image_with_image_data_as_file(sendfile=True)
-+
-+ def test_add_image_with_image_data_as_file_without_sendfile(self):
-+ self.add_image_with_image_data_as_file(sendfile=False)
-+
- def _add_image_as_iterable(self):
- fixture = {'name': 'fake public image',
- 'is_public': True,
diff --git a/0005-Fix-i18n-in-glance.notifier.notify_kombu.patch b/0005-Fix-i18n-in-glance.notifier.notify_kombu.patch
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 <bcwaldon@gmail.com>
-Date: Sun, 22 Apr 2012 22:21:53 -0700
-Subject: [PATCH] Fix i18n in glance.notifier.notify_kombu
-
-* Fixes bug 983829
-
-Change-Id: Ibc5ec12e97e69797d1952c020c3091f42480abec
----
- glance/notifier/notify_kombu.py | 5 +++--
- 1 files changed, 3 insertions(+), 2 deletions(-)
-
-diff --git a/glance/notifier/notify_kombu.py b/glance/notifier/notify_kombu.py
-index b3447d6..6623fdc 100644
---- a/glance/notifier/notify_kombu.py
-+++ b/glance/notifier/notify_kombu.py
-@@ -166,8 +166,9 @@ class RabbitStrategy(strategy.Strategy):
-
- def log_failure(self, msg, priority):
- """Fallback to logging when we can't send to rabbit."""
-- logger.error(_('Notification with priority %(priority)s failed; '
-- 'msg=%s') % msg)
-+ message = _('Notification with priority %(priority)s failed: '
-+ 'msg=%(msg)s')
-+ logger.error(message % {'msg': msg, 'priority': priority})
-
- def _send_message(self, msg, routing_key):
- """Send a message. Caller needs to catch exceptions for retry."""
diff --git a/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 <eglynn@redhat.com>
-Date: Fri, 18 May 2012 14:23:41 +0100
-Subject: [PATCH] Support DB auto-create suppression.
-
-Adds a new boolean config option, db_auto_create, to allow the
-DB auto-creation be suppressed on demand. This defaults to True
-for now to maintain the pre-existing behaviour, but should be
-changed to False before the Folsom release.
-
-The 'glance-manage db_sync' command will now create the image*
-tables if the DB did not previously exist. The db_auto_create
-flag is irrelevant in that case.
-
-The @glance.tests.function.runs_sql annotation is now obsolete
-as the glance-api/registry services launched by functional tests
-must now all run against an on-disk sqlite instance (as opposed
-to in-memory, as this makes no sense when the DB tables are
-created in advance).
-
-Change-Id: I05fc6b3ca7691dfaf00bc75a0743c921c93b9694
-
-Conflicts:
-
- glance/tests/functional/__init__.py
- glance/tests/functional/test_sqlite.py
- glance/tests/functional/v1/test_api.py
- glance/tests/functional/v2/test_images.py
----
- bin/glance-manage | 10 +-
- 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 &lt;traceback object at 0x1918d40&gt;
-+
-+ :see https://bugs.launchpad.net/glance/+bug/755912
-+ """
-+ self.cleanup()
-+ self.start_servers(**self.__dict__.copy())
-+
-+ # POST /images with binary data, but not setting
-+ # Content-Type to application/octet-stream, verify a
-+ # 400 returned and that the error is readable.
-+ with tempfile.NamedTemporaryFile() as test_data_file:
-+ test_data_file.write("XXX")
-+ test_data_file.flush()
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST',
-+ body=test_data_file.name)
-+ self.assertEqual(response.status, 400)
-+ expected = "Content-Type must be application/octet-stream"
-+ self.assertTrue(expected in content,
-+ "Could not find '%s' in '%s'" % (expected, content))
-+
-+ self.stop_servers()
-+
-+ @skip_if_disabled
-+ def test_filtered_images(self):
-+ """
-+ Set up four test images and ensure each query param filter works
-+ """
-+ self.cleanup()
-+ self.start_servers(**self.__dict__.copy())
-+
-+ # 0. GET /images
-+ # Verify no public images
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ self.assertEqual(content, '{"images": []}')
-+
-+ image_ids = []
-+
-+ # 1. POST /images with three public images, and one private image
-+ # with various attributes
-+ headers = {'Content-Type': 'application/octet-stream',
-+ 'X-Image-Meta-Name': 'Image1',
-+ 'X-Image-Meta-Status': 'active',
-+ 'X-Image-Meta-Container-Format': 'ovf',
-+ 'X-Image-Meta-Disk-Format': 'vdi',
-+ 'X-Image-Meta-Size': '19',
-+ 'X-Image-Meta-Is-Public': 'True',
-+ 'X-Image-Meta-Protected': 'True',
-+ 'X-Image-Meta-Property-pants': 'are on'}
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+ data = json.loads(content)
-+ self.assertEqual(data['image']['properties']['pants'], "are on")
-+ self.assertEqual(data['image']['is_public'], True)
-+ image_ids.append(data['image']['id'])
-+
-+ headers = {'Content-Type': 'application/octet-stream',
-+ 'X-Image-Meta-Name': 'My Image!',
-+ 'X-Image-Meta-Status': 'active',
-+ 'X-Image-Meta-Container-Format': 'ovf',
-+ 'X-Image-Meta-Disk-Format': 'vhd',
-+ 'X-Image-Meta-Size': '20',
-+ 'X-Image-Meta-Is-Public': 'True',
-+ 'X-Image-Meta-Protected': 'False',
-+ 'X-Image-Meta-Property-pants': 'are on'}
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+ data = json.loads(content)
-+ self.assertEqual(data['image']['properties']['pants'], "are on")
-+ self.assertEqual(data['image']['is_public'], True)
-+ image_ids.append(data['image']['id'])
-+
-+ headers = {'Content-Type': 'application/octet-stream',
-+ 'X-Image-Meta-Name': 'My Image!',
-+ 'X-Image-Meta-Status': 'saving',
-+ 'X-Image-Meta-Container-Format': 'ami',
-+ 'X-Image-Meta-Disk-Format': 'ami',
-+ 'X-Image-Meta-Size': '21',
-+ 'X-Image-Meta-Is-Public': 'True',
-+ 'X-Image-Meta-Protected': 'False',
-+ 'X-Image-Meta-Property-pants': 'are off'}
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+ data = json.loads(content)
-+ self.assertEqual(data['image']['properties']['pants'], "are off")
-+ self.assertEqual(data['image']['is_public'], True)
-+ image_ids.append(data['image']['id'])
-+
-+ headers = {'Content-Type': 'application/octet-stream',
-+ 'X-Image-Meta-Name': 'My Private Image',
-+ 'X-Image-Meta-Status': 'active',
-+ 'X-Image-Meta-Container-Format': 'ami',
-+ 'X-Image-Meta-Disk-Format': 'ami',
-+ 'X-Image-Meta-Size': '22',
-+ 'X-Image-Meta-Is-Public': 'False',
-+ 'X-Image-Meta-Protected': 'False'}
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+ data = json.loads(content)
-+ self.assertEqual(data['image']['is_public'], False)
-+ image_ids.append(data['image']['id'])
-+
-+ # 2. GET /images
-+ # Verify three public images
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 3)
-+
-+ # 3. GET /images with name filter
-+ # Verify correct images returned with name
-+ params = "name=My%20Image!"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 2)
-+ for image in data['images']:
-+ self.assertEqual(image['name'], "My Image!")
-+
-+ # 4. GET /images with status filter
-+ # Verify correct images returned with status
-+ params = "status=queued"
-+ path = "http://%s:%d/v1/images/detail?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 3)
-+ for image in data['images']:
-+ self.assertEqual(image['status'], "queued")
-+
-+ params = "status=active"
-+ path = "http://%s:%d/v1/images/detail?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 0)
-+
-+ # 5. GET /images with container_format filter
-+ # Verify correct images returned with container_format
-+ params = "container_format=ovf"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 2)
-+ for image in data['images']:
-+ self.assertEqual(image['container_format'], "ovf")
-+
-+ # 6. GET /images with disk_format filter
-+ # Verify correct images returned with disk_format
-+ params = "disk_format=vdi"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 1)
-+ for image in data['images']:
-+ self.assertEqual(image['disk_format'], "vdi")
-+
-+ # 7. GET /images with size_max filter
-+ # Verify correct images returned with size <= expected
-+ params = "size_max=20"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 2)
-+ for image in data['images']:
-+ self.assertTrue(image['size'] <= 20)
-+
-+ # 8. GET /images with size_min filter
-+ # Verify correct images returned with size >= expected
-+ params = "size_min=20"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 2)
-+ for image in data['images']:
-+ self.assertTrue(image['size'] >= 20)
-+
-+ # 9. Get /images with is_public=None filter
-+ # Verify correct images returned with property
-+ # Bug lp:803656 Support is_public in filtering
-+ params = "is_public=None"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 4)
-+
-+ # 10. Get /images with is_public=False filter
-+ # Verify correct images returned with property
-+ # Bug lp:803656 Support is_public in filtering
-+ params = "is_public=False"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 1)
-+ for image in data['images']:
-+ self.assertEqual(image['name'], "My Private Image")
-+
-+ # 11. Get /images with is_public=True filter
-+ # Verify correct images returned with property
-+ # Bug lp:803656 Support is_public in filtering
-+ params = "is_public=True"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 3)
-+ for image in data['images']:
-+ self.assertNotEqual(image['name'], "My Private Image")
-+
-+ # 12. Get /images with protected=False filter
-+ # Verify correct images returned with property
-+ params = "protected=False"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 2)
-+ for image in data['images']:
-+ self.assertNotEqual(image['name'], "Image1")
-+
-+ # 13. Get /images with protected=True filter
-+ # Verify correct images returned with property
-+ params = "protected=True"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 1)
-+ for image in data['images']:
-+ self.assertEqual(image['name'], "Image1")
-+
-+ # 14. GET /images with property filter
-+ # Verify correct images returned with property
-+ params = "property-pants=are%20on"
-+ path = "http://%s:%d/v1/images/detail?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 2)
-+ for image in data['images']:
-+ self.assertEqual(image['properties']['pants'], "are on")
-+
-+ # 15. GET /images with property filter and name filter
-+ # Verify correct images returned with property and name
-+ # Make sure you quote the url when using more than one param!
-+ params = "name=My%20Image!&property-pants=are%20on"
-+ path = "http://%s:%d/v1/images/detail?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 1)
-+ for image in data['images']:
-+ self.assertEqual(image['properties']['pants'], "are on")
-+ self.assertEqual(image['name'], "My Image!")
-+
-+ # 16. GET /images with past changes-since filter
-+ yesterday = utils.isotime(datetime.datetime.utcnow() -
-+ datetime.timedelta(1))
-+ params = "changes-since=%s" % yesterday
-+ path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 3)
-+
-+ # one timezone west of Greenwich equates to an hour ago
-+ # taking care to pre-urlencode '+' as '%2B', otherwise the timezone
-+ # '+' is wrongly decoded as a space
-+ # TODO(eglynn): investigate '+' --> <SPACE> decoding, an artifact
-+ # of WSGI/webob dispatch?
-+ now = datetime.datetime.utcnow()
-+ hour_ago = now.strftime('%Y-%m-%dT%H:%M:%S%%2B01:00')
-+ params = "changes-since=%s" % hour_ago
-+ path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 3)
-+
-+ # 17. GET /images with future changes-since filter
-+ tomorrow = utils.isotime(datetime.datetime.utcnow() +
-+ datetime.timedelta(1))
-+ params = "changes-since=%s" % tomorrow
-+ path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 0)
-+
-+ # one timezone east of Greenwich equates to an hour from now
-+ now = datetime.datetime.utcnow()
-+ hour_hence = now.strftime('%Y-%m-%dT%H:%M:%S-01:00')
-+ params = "changes-since=%s" % hour_hence
-+ path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 0)
-+
-+ # 18. GET /images with size_min filter
-+ # Verify correct images returned with size >= expected
-+ params = "size_min=-1"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 400)
-+ self.assertTrue("filter size_min got -1" in content)
-+
-+ # 19. GET /images with size_min filter
-+ # Verify correct images returned with size >= expected
-+ params = "size_max=-1"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 400)
-+ self.assertTrue("filter size_max got -1" in content)
-+
-+ # 20. GET /images with size_min filter
-+ # Verify correct images returned with size >= expected
-+ params = "min_ram=-1"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 400)
-+ self.assertTrue("Bad value passed to filter min_ram got -1" in content)
-+
-+ # 21. GET /images with size_min filter
-+ # Verify correct images returned with size >= expected
-+ params = "protected=imalittleteapot"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 400)
-+ self.assertTrue("protected got imalittleteapot" in content)
-+
-+ # 22. GET /images with size_min filter
-+ # Verify correct images returned with size >= expected
-+ params = "is_public=imalittleteapot"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 400)
-+ self.assertTrue("is_public got imalittleteapot" in content)
-+
-+ self.stop_servers()
-+
-+ @skip_if_disabled
-+ def test_limited_images(self):
-+ """
-+ Ensure marker and limit query params work
-+ """
-+ self.cleanup()
-+ self.start_servers(**self.__dict__.copy())
-+
-+ # 0. GET /images
-+ # Verify no public images
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ self.assertEqual(content, '{"images": []}')
-+
-+ image_ids = []
-+
-+ # 1. POST /images with three public images with various attributes
-+ headers = minimal_headers('Image1')
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+ image_ids.append(json.loads(content)['image']['id'])
-+
-+ headers = minimal_headers('Image2')
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+ image_ids.append(json.loads(content)['image']['id'])
-+
-+ headers = minimal_headers('Image3')
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+ image_ids.append(json.loads(content)['image']['id'])
-+
-+ # 2. GET /images with all images
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ images = json.loads(content)['images']
-+ self.assertEqual(len(images), 3)
-+
-+ # 3. GET /images with limit of 2
-+ # Verify only two images were returned
-+ params = "limit=2"
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)['images']
-+ self.assertEqual(len(data), 2)
-+ self.assertEqual(data[0]['id'], images[0]['id'])
-+ self.assertEqual(data[1]['id'], images[1]['id'])
-+
-+ # 4. GET /images with marker
-+ # Verify only two images were returned
-+ params = "marker=%s" % images[0]['id']
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)['images']
-+ self.assertEqual(len(data), 2)
-+ self.assertEqual(data[0]['id'], images[1]['id'])
-+ self.assertEqual(data[1]['id'], images[2]['id'])
-+
-+ # 5. GET /images with marker and limit
-+ # Verify only one image was returned with the correct id
-+ params = "limit=1&marker=%s" % images[1]['id']
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)['images']
-+ self.assertEqual(len(data), 1)
-+ self.assertEqual(data[0]['id'], images[2]['id'])
-+
-+ # 6. GET /images/detail with marker and limit
-+ # Verify only one image was returned with the correct id
-+ params = "limit=1&marker=%s" % images[1]['id']
-+ path = "http://%s:%d/v1/images?%s" % (
-+ "0.0.0.0", self.api_port, params)
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)['images']
-+ self.assertEqual(len(data), 1)
-+ self.assertEqual(data[0]['id'], images[2]['id'])
-+
-+ # DELETE images
-+ for image_id in image_ids:
-+ path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
-+ image_id)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'DELETE')
-+ self.assertEqual(response.status, 200)
-+
-+ self.stop_servers()
-+
-+ @skip_if_disabled
-+ def test_ordered_images(self):
-+ """
-+ Set up three test images and ensure each query param filter works
-+ """
-+ self.cleanup()
-+ self.start_servers(**self.__dict__.copy())
-+
-+ # 0. GET /images
-+ # Verify no public images
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ self.assertEqual(content, '{"images": []}')
-+
-+ # 1. POST /images with three public images with various attributes
-+ image_ids = []
-+ headers = {'Content-Type': 'application/octet-stream',
-+ 'X-Image-Meta-Name': 'Image1',
-+ 'X-Image-Meta-Status': 'active',
-+ 'X-Image-Meta-Container-Format': 'ovf',
-+ 'X-Image-Meta-Disk-Format': 'vdi',
-+ 'X-Image-Meta-Size': '19',
-+ 'X-Image-Meta-Is-Public': 'True'}
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+ image_ids.append(json.loads(content)['image']['id'])
-+
-+ headers = {'Content-Type': 'application/octet-stream',
-+ 'X-Image-Meta-Name': 'ASDF',
-+ 'X-Image-Meta-Status': 'active',
-+ 'X-Image-Meta-Container-Format': 'bare',
-+ 'X-Image-Meta-Disk-Format': 'iso',
-+ 'X-Image-Meta-Size': '2',
-+ 'X-Image-Meta-Is-Public': 'True'}
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+ image_ids.append(json.loads(content)['image']['id'])
-+
-+ headers = {'Content-Type': 'application/octet-stream',
-+ 'X-Image-Meta-Name': 'XYZ',
-+ 'X-Image-Meta-Status': 'saving',
-+ 'X-Image-Meta-Container-Format': 'ami',
-+ 'X-Image-Meta-Disk-Format': 'ami',
-+ 'X-Image-Meta-Size': '5',
-+ 'X-Image-Meta-Is-Public': 'True'}
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+ image_ids.append(json.loads(content)['image']['id'])
-+
-+ # 2. GET /images with no query params
-+ # Verify three public images sorted by created_at desc
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 3)
-+ self.assertEqual(data['images'][0]['id'], image_ids[2])
-+ self.assertEqual(data['images'][1]['id'], image_ids[1])
-+ self.assertEqual(data['images'][2]['id'], image_ids[0])
-+
-+ # 3. GET /images sorted by name asc
-+ params = 'sort_key=name&sort_dir=asc'
-+ path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 3)
-+ self.assertEqual(data['images'][0]['id'], image_ids[1])
-+ self.assertEqual(data['images'][1]['id'], image_ids[0])
-+ self.assertEqual(data['images'][2]['id'], image_ids[2])
-+
-+ # 4. GET /images sorted by size desc
-+ params = 'sort_key=size&sort_dir=desc'
-+ path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 3)
-+ self.assertEqual(data['images'][0]['id'], image_ids[0])
-+ self.assertEqual(data['images'][1]['id'], image_ids[2])
-+ self.assertEqual(data['images'][2]['id'], image_ids[1])
-+
-+ # 5. GET /images sorted by size desc with a marker
-+ params = 'sort_key=size&sort_dir=desc&marker=%s' % image_ids[0]
-+ path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 2)
-+ self.assertEqual(data['images'][0]['id'], image_ids[2])
-+ self.assertEqual(data['images'][1]['id'], image_ids[1])
-+
-+ # 6. GET /images sorted by name asc with a marker
-+ params = 'sort_key=name&sort_dir=asc&marker=%s' % image_ids[2]
-+ path = "http://%s:%d/v1/images?%s" % ("0.0.0.0", self.api_port, params)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ data = json.loads(content)
-+ self.assertEqual(len(data['images']), 0)
-+
-+ # DELETE images
-+ for image_id in image_ids:
-+ path = "http://%s:%d/v1/images/%s" % ("0.0.0.0", self.api_port,
-+ image_id)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'DELETE')
-+ self.assertEqual(response.status, 200)
-+
-+ self.stop_servers()
-+
-+ @skip_if_disabled
-+ def test_duplicate_image_upload(self):
-+ """
-+ Upload initial image, then attempt to upload duplicate image
-+ """
-+ self.cleanup()
-+ self.start_servers(**self.__dict__.copy())
-+
-+ # 0. GET /images
-+ # Verify no public images
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ self.assertEqual(content, '{"images": []}')
-+
-+ # 1. POST /images with public image named Image1
-+ headers = {'Content-Type': 'application/octet-stream',
-+ 'X-Image-Meta-Name': 'Image1',
-+ 'X-Image-Meta-Status': 'active',
-+ 'X-Image-Meta-Container-Format': 'ovf',
-+ 'X-Image-Meta-Disk-Format': 'vdi',
-+ 'X-Image-Meta-Size': '19',
-+ 'X-Image-Meta-Is-Public': 'True'}
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+
-+ image = json.loads(content)['image']
-+
-+ # 2. POST /images with public image named Image1, and ID: 1
-+ headers = {'Content-Type': 'application/octet-stream',
-+ 'X-Image-Meta-Name': 'Image1 Update',
-+ 'X-Image-Meta-Status': 'active',
-+ 'X-Image-Meta-Container-Format': 'ovf',
-+ 'X-Image-Meta-Disk-Format': 'vdi',
-+ 'X-Image-Meta-Size': '19',
-+ 'X-Image-Meta-Id': image['id'],
-+ 'X-Image-Meta-Is-Public': 'True'}
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 409)
-+
-+ self.stop_servers()
-+
-+ @skip_if_disabled
-+ def test_delete_not_existing(self):
-+ """
-+ We test the following:
-+
-+ 0. GET /images/1
-+ - Verify 404
-+ 1. DELETE /images/1
-+ - Verify 404
-+ """
-+ self.cleanup()
-+ self.start_servers(**self.__dict__.copy())
-+
-+ api_port = self.api_port
-+ registry_port = self.registry_port
-+
-+ # 0. GET /images
-+ # Verify no public images
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'GET')
-+ self.assertEqual(response.status, 200)
-+ self.assertEqual(content, '{"images": []}')
-+
-+ # 1. DELETE /images/1
-+ # Verify 404 returned
-+ path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'DELETE')
-+ self.assertEqual(response.status, 404)
-+
-+ self.stop_servers()
-+
-+ @skip_if_disabled
-+ def test_unsupported_default_store(self):
-+ """
-+ We test that a mis-configured default_store causes the API server
-+ to fail to start.
-+ """
-+ self.cleanup()
-+ self.default_store = 'shouldnotexist'
-+
-+ # ensure failure exit code is available to assert on
-+ self.api_server.server_control_options += ' --await-child=1'
-+
-+ # ensure that the API server fails to launch
-+ self.start_server(self.api_server,
-+ expect_launch=False,
-+ expected_exitcode=255,
-+ **self.__dict__.copy())
-+
-+ def _do_test_post_image_content_missing_format(self, format):
-+ """
-+ We test that missing container/disk format fails with 400 "Bad Request"
-+
-+ :see https://bugs.launchpad.net/glance/+bug/933702
-+ """
-+ self.cleanup()
-+ self.start_servers(**self.__dict__.copy())
-+
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+
-+ # POST /images without given format being specified
-+ headers = minimal_headers('Image1')
-+ del headers['X-Image-Meta-' + format]
-+ with tempfile.NamedTemporaryFile() as test_data_file:
-+ test_data_file.write("XXX")
-+ test_data_file.flush()
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST',
-+ headers=headers,
-+ body=test_data_file.name)
-+ self.assertEqual(response.status, 400)
-+ type = format.replace('_format', '')
-+ expected = "Details: Invalid %s format 'None' for image" % type
-+ self.assertTrue(expected in content,
-+ "Could not find '%s' in '%s'" % (expected, content))
-+
-+ self.stop_servers()
-+
-+ @skip_if_disabled
-+ def _do_test_post_image_content_missing_diskformat(self):
-+ self._do_test_post_image_content_missing_format('container_format')
-+
-+ @skip_if_disabled
-+ def _do_test_post_image_content_missing_disk_format(self):
-+ self._do_test_post_image_content_missing_format('disk_format')
-+
-+ def _do_test_put_image_content_missing_format(self, format):
-+ """
-+ We test that missing container/disk format only fails with
-+ 400 "Bad Request" when the image content is PUT (i.e. not
-+ on the original POST of a queued image).
-+
-+ :see https://bugs.launchpad.net/glance/+bug/937216
-+ """
-+ self.cleanup()
-+ self.start_servers(**self.__dict__.copy())
-+
-+ # POST queued image
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ headers = {
-+ 'X-Image-Meta-Name': 'Image1',
-+ 'X-Image-Meta-Is-Public': 'True',
-+ }
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=headers)
-+ self.assertEqual(response.status, 201)
-+ data = json.loads(content)
-+ image_id = data['image']['id']
-+
-+ # PUT image content images without given format being specified
-+ path = ("http://%s:%d/v1/images/%s" %
-+ ("0.0.0.0", self.api_port, image_id))
-+ headers = minimal_headers('Image1')
-+ del headers['X-Image-Meta-' + format]
-+ with tempfile.NamedTemporaryFile() as test_data_file:
-+ test_data_file.write("XXX")
-+ test_data_file.flush()
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'PUT',
-+ headers=headers,
-+ body=test_data_file.name)
-+ self.assertEqual(response.status, 400)
-+ type = format.replace('_format', '')
-+ expected = "Details: Invalid %s format 'None' for image" % type
-+ self.assertTrue(expected in content,
-+ "Could not find '%s' in '%s'" % (expected, content))
-+
-+ self.stop_servers()
-+
-+ @skip_if_disabled
-+ def _do_test_put_image_content_missing_diskformat(self):
-+ self._do_test_put_image_content_missing_format('container_format')
-+
-+ @skip_if_disabled
-+ def _do_test_put_image_content_missing_disk_format(self):
-+ self._do_test_put_image_content_missing_format('disk_format')
-+
-+ @skip_if_disabled
-+ def test_ownership(self):
-+ self.cleanup()
-+ self.api_server.deployment_flavor = 'fakeauth'
-+ self.registry_server.deployment_flavor = 'fakeauth'
-+ self.start_servers(**self.__dict__.copy())
-+
-+ # Add an image with admin privileges and ensure the owner
-+ # can be set to something other than what was used to authenticate
-+ auth_headers = {
-+ 'X-Auth-Token': 'user1:tenant1:admin',
-+ }
-+
-+ create_headers = {
-+ 'X-Image-Meta-Name': 'MyImage',
-+ 'X-Image-Meta-disk_format': 'raw',
-+ 'X-Image-Meta-container_format': 'ovf',
-+ 'X-Image-Meta-Is-Public': 'True',
-+ 'X-Image-Meta-Owner': 'tenant2',
-+ }
-+ create_headers.update(auth_headers)
-+
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=create_headers)
-+ self.assertEqual(response.status, 201)
-+ data = json.loads(content)
-+ image_id = data['image']['id']
-+
-+ path = ("http://%s:%d/v1/images/%s" %
-+ ("0.0.0.0", self.api_port, image_id))
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'HEAD', headers=auth_headers)
-+ self.assertEqual(response.status, 200)
-+ self.assertEqual('tenant2', response['x-image-meta-owner'])
-+
-+ # Now add an image without admin privileges and ensure the owner
-+ # cannot be set to something other than what was used to authenticate
-+ auth_headers = {
-+ 'X-Auth-Token': 'user1:tenant1:role1',
-+ }
-+ create_headers.update(auth_headers)
-+
-+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'POST', headers=create_headers)
-+ self.assertEqual(response.status, 201)
-+ data = json.loads(content)
-+ image_id = data['image']['id']
-+
-+ # We have to be admin to see the owner
-+ auth_headers = {
-+ 'X-Auth-Token': 'user1:tenant1:admin',
-+ }
-+ create_headers.update(auth_headers)
-+
-+ path = ("http://%s:%d/v1/images/%s" %
-+ ("0.0.0.0", self.api_port, image_id))
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'HEAD', headers=auth_headers)
-+ self.assertEqual(response.status, 200)
-+ self.assertEqual('tenant1', response['x-image-meta-owner'])
-+
-+ # Make sure the non-privileged user can't update their owner either
-+ update_headers = {
-+ 'X-Image-Meta-Name': 'MyImage2',
-+ 'X-Image-Meta-Owner': 'tenant2',
-+ 'X-Auth-Token': 'user1:tenant1:role1',
-+ }
-+
-+ path = ("http://%s:%d/v1/images/%s" %
-+ ("0.0.0.0", self.api_port, image_id))
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'PUT', headers=update_headers)
-+ self.assertEqual(response.status, 200)
-+
-+ # We have to be admin to see the owner
-+ auth_headers = {
-+ 'X-Auth-Token': 'user1:tenant1:admin',
-+ }
-+
-+ path = ("http://%s:%d/v1/images/%s" %
-+ ("0.0.0.0", self.api_port, image_id))
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'HEAD', headers=auth_headers)
-+ self.assertEqual(response.status, 200)
-+ self.assertEqual('tenant1', response['x-image-meta-owner'])
-+
-+ # An admin user should be able to update the owner
-+ auth_headers = {
-+ 'X-Auth-Token': 'user1:tenant3:admin',
-+ }
-+
-+ update_headers = {
-+ 'X-Image-Meta-Name': 'MyImage2',
-+ 'X-Image-Meta-Owner': 'tenant2',
-+ }
-+ update_headers.update(auth_headers)
-+
-+ path = ("http://%s:%d/v1/images/%s" %
-+ ("0.0.0.0", self.api_port, image_id))
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'PUT', headers=update_headers)
-+ self.assertEqual(response.status, 200)
-+
-+ path = ("http://%s:%d/v1/images/%s" %
-+ ("0.0.0.0", self.api_port, image_id))
-+ http = httplib2.Http()
-+ response, content = http.request(path, 'HEAD', headers=auth_headers)
-+ self.assertEqual(response.status, 200)
-+ self.assertEqual('tenant2', response['x-image-meta-owner'])
-+
-+ self.stop_servers()
-diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py
-new file mode 100644
-index 0000000..bbaa052
---- /dev/null
-+++ b/glance/tests/functional/v2/test_images.py
-@@ -0,0 +1,468 @@
-+# vim: tabstop=4 shiftwidth=4 softtabstop=4
-+
-+# Copyright 2012 OpenStack, LLC
-+# All Rights Reserved.
-+#
-+# Licensed under the Apache License, Version 2.0 (the "License"); you may
-+# not use this file except in compliance with the License. You may obtain
-+# a copy of the License at
-+#
-+# http://www.apache.org/licenses/LICENSE-2.0
-+#
-+# Unless required by applicable law or agreed to in writing, software
-+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-+# License for the specific language governing permissions and limitations
-+# under the License.
-+
-+import json
-+
-+import requests
-+
-+from glance.tests import functional
-+from glance.common import utils
-+
-+
-+TENANT1 = utils.generate_uuid()
-+TENANT2 = utils.generate_uuid()
-+TENANT3 = utils.generate_uuid()
-+TENANT4 = utils.generate_uuid()
-+
-+
-+class TestImages(functional.FunctionalTest):
-+
-+ def setUp(self):
-+ super(TestImages, self).setUp()
-+ self.cleanup()
-+ self.api_server.deployment_flavor = 'noauth'
-+ self.start_servers(**self.__dict__.copy())
-+
-+ def _url(self, path):
-+ return 'http://0.0.0.0:%d/v2%s' % (self.api_port, path)
-+
-+ def _headers(self, custom_headers=None):
-+ base_headers = {
-+ 'X-Identity-Status': 'Confirmed',
-+ 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
-+ 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
-+ 'X-Tenant-Id': TENANT1,
-+ 'X-Roles': 'member',
-+ }
-+ base_headers.update(custom_headers or {})
-+ return base_headers
-+
-+ def test_image_lifecycle(self):
-+ # Image list should be empty
-+ path = self._url('/images')
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ images = json.loads(response.text)['images']
-+ self.assertEqual(0, len(images))
-+
-+ # Create an image
-+ path = self._url('/images')
-+ headers = self._headers({'content-type': 'application/json'})
-+ data = json.dumps({'name': 'image-1'})
-+ response = requests.post(path, headers=headers, data=data)
-+ self.assertEqual(200, response.status_code)
-+ image_location_header = response.headers['Location']
-+
-+ # Returned image entity should have a generated id
-+ image = json.loads(response.text)['image']
-+ image_id = image['id']
-+
-+ # Image list should now have one entry
-+ path = self._url('/images')
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ images = json.loads(response.text)['images']
-+ self.assertEqual(1, len(images))
-+ self.assertEqual(images[0]['id'], image_id)
-+
-+ # Get the image using the returned Location header
-+ response = requests.get(image_location_header, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ image = json.loads(response.text)['image']
-+ self.assertEqual(image_id, image['id'])
-+
-+ # The image should be mutable
-+ path = self._url('/images/%s' % image_id)
-+ data = json.dumps({'name': 'image-2'})
-+ response = requests.put(path, headers=self._headers(), data=data)
-+ self.assertEqual(200, response.status_code)
-+
-+ # Returned image entity should reflect the changes
-+ image = json.loads(response.text)['image']
-+ self.assertEqual('image-2', image['name'])
-+
-+ # Updates should persist across requests
-+ path = self._url('/images/%s' % image_id)
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ image = json.loads(response.text)['image']
-+ self.assertEqual(image_id, image['id'])
-+ self.assertEqual('image-2', image['name'])
-+
-+ # Try to download data before its uploaded
-+ path = self._url('/images/%s/file' % image_id)
-+ headers = self._headers()
-+ response = requests.get(path, headers=headers)
-+ self.assertEqual(404, response.status_code)
-+
-+ # Upload some image data
-+ path = self._url('/images/%s/file' % image_id)
-+ headers = self._headers({'Content-Type': 'application/octet-stream'})
-+ response = requests.put(path, headers=headers, data='ZZZZZ')
-+ self.assertEqual(200, response.status_code)
-+
-+ # Try to download the data that was just uploaded
-+ path = self._url('/images/%s/file' % image_id)
-+ headers = self._headers()
-+ response = requests.get(path, headers=headers)
-+ self.assertEqual(200, response.status_code)
-+ self.assertEqual(response.text, 'ZZZZZ')
-+
-+ # Deletion should work
-+ path = self._url('/images/%s' % image_id)
-+ response = requests.delete(path, headers=self._headers())
-+ self.assertEqual(204, response.status_code)
-+
-+ # This image should be no longer be directly accessible
-+ path = self._url('/images/%s' % image_id)
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(404, response.status_code)
-+
-+ # And neither should its data
-+ path = self._url('/images/%s/file' % image_id)
-+ headers = self._headers()
-+ response = requests.get(path, headers=headers)
-+ self.assertEqual(404, response.status_code)
-+
-+ # Image list should now be empty
-+ path = self._url('/images')
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ images = json.loads(response.text)['images']
-+ self.assertEqual(0, len(images))
-+
-+ self.stop_servers()
-+
-+ def test_upload_duplicate_data(self):
-+ # Create an image
-+ path = self._url('/images')
-+ headers = self._headers({'content-type': 'application/json'})
-+ data = json.dumps({'name': 'image-1'})
-+ response = requests.post(path, headers=headers, data=data)
-+ self.assertEqual(200, response.status_code)
-+
-+ # Returned image entity should have a generated id
-+ image = json.loads(response.text)['image']
-+ image_id = image['id']
-+
-+ # Upload some image data
-+ path = self._url('/images/%s/file' % image_id)
-+ headers = self._headers({'Content-Type': 'application/octet-stream'})
-+ response = requests.put(path, headers=headers, data='ZZZZZ')
-+ self.assertEqual(200, response.status_code)
-+
-+ # Uploading duplicate data should be rejected with a 409
-+ path = self._url('/images/%s/file' % image_id)
-+ headers = self._headers({'Content-Type': 'application/octet-stream'})
-+ response = requests.put(path, headers=headers, data='XXX')
-+ self.assertEqual(409, response.status_code)
-+
-+ # Data should not have been overwritten
-+ path = self._url('/images/%s/file' % image_id)
-+ headers = self._headers()
-+ response = requests.get(path, headers=headers)
-+ self.assertEqual(200, response.status_code)
-+ self.assertEqual(response.text, 'ZZZZZ')
-+
-+ self.stop_servers()
-+
-+ def test_permissions(self):
-+ # Create an image that belongs to TENANT1
-+ path = self._url('/images')
-+ headers = self._headers({'Content-Type': 'application/json'})
-+ data = json.dumps({'name': 'image-1'})
-+ response = requests.post(path, headers=headers, data=data)
-+ self.assertEqual(200, response.status_code)
-+ image_id = json.loads(response.text)['image']['id']
-+
-+ # TENANT1 should see the image in their list
-+ path = self._url('/images')
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ images = json.loads(response.text)['images']
-+ self.assertEqual(image_id, images[0]['id'])
-+
-+ # TENANT1 should be able to access the image directly
-+ path = self._url('/images/%s' % image_id)
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+
-+ # TENANT2 should not see the image in their list
-+ path = self._url('/images')
-+ headers = self._headers({'X-Tenant-Id': TENANT2})
-+ response = requests.get(path, headers=headers)
-+ self.assertEqual(200, response.status_code)
-+ images = json.loads(response.text)['images']
-+ self.assertEqual(0, len(images))
-+
-+ # TENANT2 should not be able to access the image directly
-+ path = self._url('/images/%s' % image_id)
-+ headers = self._headers({'X-Tenant-Id': TENANT2})
-+ response = requests.get(path, headers=headers)
-+ self.assertEqual(404, response.status_code)
-+
-+ # TENANT2 should not be able to modify the image, either
-+ path = self._url('/images/%s' % image_id)
-+ headers = self._headers({
-+ 'Content-Type': 'application/json',
-+ 'X-Tenant-Id': TENANT2,
-+ })
-+ data = json.dumps({'name': 'image-2'})
-+ response = requests.put(path, headers=headers, data=data)
-+ self.assertEqual(404, response.status_code)
-+
-+ # TENANT2 should not be able to delete the image, either
-+ path = self._url('/images/%s' % image_id)
-+ headers = self._headers({'X-Tenant-Id': TENANT2})
-+ response = requests.delete(path, headers=headers)
-+ self.assertEqual(404, response.status_code)
-+
-+ # Share the image with TENANT2
-+ path = self._url('/images/%s/access' % image_id)
-+ data = json.dumps({'tenant_id': TENANT2, 'can_share': False})
-+ request_headers = {'Content-Type': 'application/json'}
-+ headers = self._headers(request_headers)
-+ response = requests.post(path, headers=headers, data=data)
-+ self.assertEqual(201, response.status_code)
-+
-+ # TENANT2 should see the image in their list
-+ path = self._url('/images')
-+ headers = self._headers({'X-Tenant-Id': TENANT2})
-+ response = requests.get(path, headers=headers)
-+ self.assertEqual(200, response.status_code)
-+ images = json.loads(response.text)['images']
-+ self.assertEqual(image_id, images[0]['id'])
-+
-+ # TENANT2 should be able to access the image directly
-+ path = self._url('/images/%s' % image_id)
-+ headers = self._headers({'X-Tenant-Id': TENANT2})
-+ response = requests.get(path, headers=headers)
-+ self.assertEqual(200, response.status_code)
-+
-+ # TENANT2 should not be able to modify the image
-+ path = self._url('/images/%s' % image_id)
-+ headers = self._headers({
-+ 'Content-Type': 'application/json',
-+ 'X-Tenant-Id': TENANT2,
-+ })
-+ data = json.dumps({'name': 'image-2'})
-+ response = requests.put(path, headers=headers, data=data)
-+ self.assertEqual(404, response.status_code)
-+
-+ # TENANT2 should not be able to delete the image, either
-+ path = self._url('/images/%s' % image_id)
-+ headers = self._headers({'X-Tenant-Id': TENANT2})
-+ response = requests.delete(path, headers=headers)
-+ self.assertEqual(404, response.status_code)
-+
-+ # As an unshared tenant, TENANT3 should not have access to the image
-+ path = self._url('/images/%s' % image_id)
-+ headers = self._headers({'X-Tenant-Id': TENANT3})
-+ response = requests.get(path, headers=headers)
-+ self.assertEqual(404, response.status_code)
-+
-+ # Publicize the image as an admin of TENANT1
-+ path = self._url('/images/%s' % image_id)
-+ headers = self._headers({
-+ 'Content-Type': 'application/json',
-+ 'X-Roles': 'admin',
-+ })
-+ data = json.dumps({'visibility': 'public'})
-+ response = requests.put(path, headers=headers, data=data)
-+ self.assertEqual(200, response.status_code)
-+
-+ # TENANT3 should now see the image in their list
-+ path = self._url('/images')
-+ headers = self._headers({'X-Tenant-Id': TENANT3})
-+ response = requests.get(path, headers=headers)
-+ self.assertEqual(200, response.status_code)
-+ images = json.loads(response.text)['images']
-+ self.assertEqual(image_id, images[0]['id'])
-+
-+ # TENANT3 should also be able to access the image directly
-+ path = self._url('/images/%s' % image_id)
-+ headers = self._headers({'X-Tenant-Id': TENANT3})
-+ response = requests.get(path, headers=headers)
-+ self.assertEqual(200, response.status_code)
-+
-+ # TENANT3 still should not be able to modify the image
-+ path = self._url('/images/%s' % image_id)
-+ headers = self._headers({
-+ 'Content-Type': 'application/json',
-+ 'X-Tenant-Id': TENANT3,
-+ })
-+ data = json.dumps({'name': 'image-2'})
-+ response = requests.put(path, headers=headers, data=data)
-+ self.assertEqual(404, response.status_code)
-+
-+ # TENANT3 should not be able to delete the image, either
-+ path = self._url('/images/%s' % image_id)
-+ headers = self._headers({'X-Tenant-Id': TENANT3})
-+ response = requests.delete(path, headers=headers)
-+ self.assertEqual(404, response.status_code)
-+
-+ self.stop_servers()
-+
-+ def test_access_lifecycle(self):
-+ # Create an image for our tests
-+ path = self._url('/images')
-+ headers = self._headers({'Content-Type': 'application/json'})
-+ data = json.dumps({'name': 'image-1'})
-+ response = requests.post(path, headers=headers, data=data)
-+ self.assertEqual(200, response.status_code)
-+ image_id = json.loads(response.text)['image']['id']
-+
-+ # Image acccess list should be empty
-+ path = self._url('/images/%s/access' % image_id)
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ access_records = json.loads(response.text)['access_records']
-+ self.assertEqual(0, len(access_records))
-+
-+ # Other tenants shouldn't be able to share by default, and shouldn't
-+ # even know the image exists
-+ path = self._url('/images/%s/access' % image_id)
-+ data = json.dumps({'tenant_id': TENANT3, 'can_share': False})
-+ request_headers = {
-+ 'Content-Type': 'application/json',
-+ 'X-Tenant-Id': TENANT2,
-+ }
-+ headers = self._headers(request_headers)
-+ response = requests.post(path, headers=headers, data=data)
-+ self.assertEqual(404, response.status_code)
-+
-+ # Share the image with another tenant
-+ path = self._url('/images/%s/access' % image_id)
-+ data = json.dumps({'tenant_id': TENANT2, 'can_share': True})
-+ headers = self._headers({'Content-Type': 'application/json'})
-+ response = requests.post(path, headers=headers, data=data)
-+ self.assertEqual(201, response.status_code)
-+ access_location = response.headers['Location']
-+
-+ # Ensure the access record was actually created
-+ response = requests.get(access_location, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+
-+ # Make sure the sharee can further share the image
-+ path = self._url('/images/%s/access' % image_id)
-+ data = json.dumps({'tenant_id': TENANT3, 'can_share': False})
-+ request_headers = {
-+ 'Content-Type': 'application/json',
-+ 'X-Tenant-Id': TENANT2,
-+ }
-+ headers = self._headers(request_headers)
-+ response = requests.post(path, headers=headers, data=data)
-+ self.assertEqual(201, response.status_code)
-+ access_location = response.headers['Location']
-+
-+ # Ensure the access record was actually created
-+ response = requests.get(access_location, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+
-+ # The third tenant should not be able to share it further
-+ path = self._url('/images/%s/access' % image_id)
-+ data = json.dumps({'tenant_id': TENANT4, 'can_share': False})
-+ request_headers = {
-+ 'Content-Type': 'application/json',
-+ 'X-Tenant-Id': TENANT3,
-+ }
-+ headers = self._headers(request_headers)
-+ response = requests.post(path, headers=headers, data=data)
-+ self.assertEqual(403, response.status_code)
-+
-+ # Image acccess list should now contain 2 entries
-+ path = self._url('/images/%s/access' % image_id)
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ access_records = json.loads(response.text)['access_records']
-+ self.assertEqual(2, len(access_records))
-+
-+ # Delete an access record
-+ response = requests.delete(access_location, headers=self._headers())
-+ self.assertEqual(204, response.status_code)
-+
-+ # Ensure the access record was actually deleted
-+ response = requests.get(access_location, headers=self._headers())
-+ self.assertEqual(404, response.status_code)
-+
-+ # Image acccess list should now contain 1 entry
-+ path = self._url('/images/%s/access' % image_id)
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ access_records = json.loads(response.text)['access_records']
-+ self.assertEqual(1, len(access_records))
-+
-+ self.stop_servers()
-+
-+ def test_tag_lifecycle(self):
-+ # Create an image for our tests
-+ path = self._url('/images')
-+ headers = self._headers({'Content-Type': 'application/json'})
-+ data = json.dumps({'name': 'image-1'})
-+ response = requests.post(path, headers=headers, data=data)
-+ self.assertEqual(200, response.status_code)
-+ image_id = json.loads(response.text)['image']['id']
-+
-+ # List of image tags should be empty
-+ path = self._url('/images/%s/tags' % image_id)
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ tags = json.loads(response.text)
-+ self.assertEqual([], tags)
-+
-+ # Create a tag
-+ path = self._url('/images/%s/tags/sniff' % image_id)
-+ response = requests.put(path, headers=self._headers())
-+ self.assertEqual(204, response.status_code)
-+
-+ # List should now have an entry
-+ path = self._url('/images/%s/tags' % image_id)
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ tags = json.loads(response.text)
-+ self.assertEqual(['sniff'], tags)
-+
-+ # Create a more complex tag
-+ path = self._url('/images/%s/tags/someone%%40example.com' % image_id)
-+ response = requests.put(path, headers=self._headers())
-+ self.assertEqual(204, response.status_code)
-+
-+ # List should reflect our new tag
-+ path = self._url('/images/%s/tags' % image_id)
-+ response = requests.get(path, headers=self._headers())
-+ self.assertEqual(200, response.status_code)
-+ tags = json.loads(response.text)
-+ self.assertEqual(['sniff', 'someone@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 <P@draigBrady.com> - 2012.2-1
+- Update to Folsom milestone 1
+
* Tue May 22 2012 Pádraig Brady <P@draigBrady.com> - 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