Merge lp:~donal-lafferty/glance/fix739433 into lp:~glance-coresec/glance/cactus-trunk

Proposed by Donal Lafferty
Status: Merged
Approved by: Jay Pipes
Approved revision: 99
Merged at revision: 104
Proposed branch: lp:~donal-lafferty/glance/fix739433
Merge into: lp:~glance-coresec/glance/cactus-trunk
Diff against target: 277 lines (+201/-3)
5 files modified
glance/registry/db/migrate_repo/schema.py (+3/-0)
glance/registry/db/migrate_repo/versions/005_size_big_integer.py (+99/-0)
glance/registry/db/models.py (+2/-2)
tests/functional/__init__.py (+37/-1)
tests/functional/test_curl_api.py (+60/-0)
To merge this branch: bzr merge lp:~donal-lafferty/glance/fix739433
Reviewer Review Type Date Requested Status
Jay Pipes (community) Approve
Devin Carlen (community) Approve
Review via email: mp+55797@code.launchpad.net

Description of the change

Provide revised schema and migration scripts for turning 'size' column in 'images' table to BIGINT. This overcomes a 2 gig limit on images sizes that can be downloaded from Glance.

To post a comment you must log in.
Revision history for this message
Devin Carlen (devcamcar) wrote :

lgtm

review: Approve
Revision history for this message
Jay Pipes (jaypipes) wrote :

Donal, I took this branch and made changes to make the migration script Python-based, instead of just MySQL .sql migrate files. The reason is because we also needed PostgreSQL to migrate.

There is an issue with sa-migrate and SQLite, however, that produced an error in the sa-migrate workaround code for SQLite not having ALTER TABLE MODIFY COLUMN abilities. But, since SQLite doesn't have a concept of BIGINT, I simply made sure that the migrate script doesn't alter anything for SQLite engines.

Please merge lp:~jaypipes/glance/bug739433 into your branch locally and bzr commit && bzr push. While you do that, please do set this Merge Request to Work In Progress status, then back to Needs Review when you've pushed. Thanks for your patience on this.

Revision history for this message
Donal Lafferty (donal-lafferty) wrote :

Updated code to reflect changes from lp:~jaypipes/glance/bug739433

Ran migration unit tests locally. Noted that making use of MySQL for tests required updating the vitualenv to include an installation of MySQLdb. This is because MySQLdb is not a dependency for the test vitualenv. E.g. after tests fail, use

sudo pip install mysql-python -e ./.glance-venv

Then run the tests again.

Revision history for this message
Jay Pipes (jaypipes) wrote :

approve from me :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'glance/registry/db/migrate_repo/schema.py'
2--- glance/registry/db/migrate_repo/schema.py 2011-02-02 23:44:56 +0000
3+++ glance/registry/db/migrate_repo/schema.py 2011-04-05 21:27:46 +0000
4@@ -47,6 +47,9 @@
5 Integer = lambda: sqlalchemy.types.Integer()
6
7
8+BigInteger = lambda: sqlalchemy.types.BigInteger()
9+
10+
11 def from_migration_import(module_name, fromlist):
12 """Import a migration file and return the module
13
14
15=== added file 'glance/registry/db/migrate_repo/versions/005_size_big_integer.py'
16--- glance/registry/db/migrate_repo/versions/005_size_big_integer.py 1970-01-01 00:00:00 +0000
17+++ glance/registry/db/migrate_repo/versions/005_size_big_integer.py 2011-04-05 21:27:46 +0000
18@@ -0,0 +1,99 @@
19+# vim: tabstop=4 shiftwidth=4 softtabstop=4
20+
21+# Copyright 2011 OpenStack LLC.
22+# All Rights Reserved.
23+#
24+# Licensed under the Apache License, Version 2.0 (the "License"); you may
25+# not use this file except in compliance with the License. You may obtain
26+# a copy of the License at
27+#
28+# http://www.apache.org/licenses/LICENSE-2.0
29+#
30+# Unless required by applicable law or agreed to in writing, software
31+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
32+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
33+# License for the specific language governing permissions and limitations
34+# under the License.
35+
36+from migrate.changeset import *
37+from sqlalchemy import *
38+
39+from glance.registry.db.migrate_repo.schema import (
40+ Boolean, DateTime, BigInteger, Integer, String,
41+ Text, from_migration_import)
42+
43+
44+def get_images_table(meta):
45+ """
46+ Returns the Table object for the images table that
47+ corresponds to the images table definition of this version.
48+ """
49+ images = Table('images', meta,
50+ Column('id', Integer(), primary_key=True, nullable=False),
51+ Column('name', String(255)),
52+ Column('disk_format', String(20)),
53+ Column('container_format', String(20)),
54+ Column('size', BigInteger()),
55+ Column('status', String(30), nullable=False),
56+ Column('is_public', Boolean(), nullable=False, default=False,
57+ index=True),
58+ Column('location', Text()),
59+ Column('created_at', DateTime(), nullable=False),
60+ Column('updated_at', DateTime()),
61+ Column('deleted_at', DateTime()),
62+ Column('deleted', Boolean(), nullable=False, default=False,
63+ index=True),
64+ mysql_engine='InnoDB',
65+ useexisting=True)
66+
67+ return images
68+
69+
70+def get_image_properties_table(meta):
71+ """
72+ No changes to the image properties table from 002...
73+ """
74+ (define_image_properties_table,) = from_migration_import(
75+ '002_add_image_properties_table', ['define_image_properties_table'])
76+
77+ image_properties = define_image_properties_table(meta)
78+ return image_properties
79+
80+
81+def upgrade(migrate_engine):
82+ meta = MetaData()
83+ meta.bind = migrate_engine
84+
85+ # No changes to SQLite stores are necessary, since
86+ # there is no BIG INTEGER type in SQLite. Unfortunately,
87+ # running the Python 005_size_big_integer.py migration script
88+ # on a SQLite datastore results in an error in the sa-migrate
89+ # code that does the workarounds for SQLite not having
90+ # ALTER TABLE MODIFY COLUMN ability
91+
92+ dialect = migrate_engine.url.get_dialect().name
93+
94+ if not dialect.startswith('sqlite'):
95+ (get_images_table,) = from_migration_import(
96+ '003_add_disk_format', ['get_images_table'])
97+
98+ images = get_images_table(meta)
99+ images.columns['size'].alter(type=BigInteger())
100+
101+
102+def downgrade(migrate_engine):
103+ meta = MetaData()
104+ meta.bind = migrate_engine
105+
106+ # No changes to SQLite stores are necessary, since
107+ # there is no BIG INTEGER type in SQLite. Unfortunately,
108+ # running the Python 005_size_big_integer.py migration script
109+ # on a SQLite datastore results in an error in the sa-migrate
110+ # code that does the workarounds for SQLite not having
111+ # ALTER TABLE MODIFY COLUMN ability
112+
113+ dialect = migrate_engine.url.get_dialect().name
114+
115+ if not dialect.startswith('sqlite'):
116+ images = get_images_table(meta)
117+ images.columns['size'].alter(type=Integer())
118
119=== modified file 'glance/registry/db/models.py'
120--- glance/registry/db/models.py 2011-03-08 15:22:44 +0000
121+++ glance/registry/db/models.py 2011-04-05 21:27:46 +0000
122@@ -24,7 +24,7 @@
123 import datetime
124
125 from sqlalchemy.orm import relationship, backref, exc, object_mapper, validates
126-from sqlalchemy import Column, Integer, String
127+from sqlalchemy import Column, Integer, String, BigInteger
128 from sqlalchemy import ForeignKey, DateTime, Boolean, Text
129 from sqlalchemy import UniqueConstraint
130 from sqlalchemy.ext.declarative import declarative_base
131@@ -100,7 +100,7 @@
132 name = Column(String(255))
133 disk_format = Column(String(20))
134 container_format = Column(String(20))
135- size = Column(Integer)
136+ size = Column(BigInteger)
137 status = Column(String(30), nullable=False)
138 is_public = Column(Boolean, nullable=False, default=False)
139 location = Column(Text)
140
141=== modified file 'tests/functional/__init__.py'
142--- tests/functional/__init__.py 2011-03-25 18:59:56 +0000
143+++ tests/functional/__init__.py 2011-04-05 21:27:46 +0000
144@@ -32,6 +32,7 @@
145 import tempfile
146 import time
147 import unittest
148+import urlparse
149
150 from tests.utils import execute, get_unused_port
151
152@@ -60,7 +61,7 @@
153
154 self.image_dir = "/tmp/test.%d/images" % self.test_id
155
156- self.sql_connection = os.environ.get('GLANCE_SQL_CONNECTION',
157+ self.sql_connection = os.environ.get('GLANCE_TEST_SQL_CONNECTION',
158 "sqlite://")
159 self.pid_files = [self.api_pid_file,
160 self.registry_pid_file]
161@@ -68,6 +69,41 @@
162
163 def tearDown(self):
164 self.cleanup()
165+ # We destroy the test data store between each test case,
166+ # and recreate it, which ensures that we have no side-effects
167+ # from the tests
168+ self._reset_database()
169+
170+ def _reset_database(self):
171+ conn_string = self.sql_connection
172+ conn_pieces = urlparse.urlparse(conn_string)
173+ if conn_string.startswith('sqlite'):
174+ # We can just delete the SQLite database, which is
175+ # the easiest and cleanest solution
176+ db_path = conn_pieces.path.strip('/')
177+ if db_path and os.path.exists(db_path):
178+ os.unlink(db_path)
179+ # No need to recreate the SQLite DB. SQLite will
180+ # create it for us if it's not there...
181+ elif conn_string.startswith('mysql'):
182+ # We can execute the MySQL client to destroy and re-create
183+ # the MYSQL database, which is easier and less error-prone
184+ # than using SQLAlchemy to do this via MetaData...trust me.
185+ database = conn_pieces.path.strip('/')
186+ loc_pieces = conn_pieces.netloc.split('@')
187+ host = loc_pieces[1]
188+ auth_pieces = loc_pieces[0].split(':')
189+ user = auth_pieces[0]
190+ password = ""
191+ if len(auth_pieces) > 1:
192+ if auth_pieces[1].strip():
193+ password = "-p%s" % auth_pieces[1]
194+ sql = ("drop database if exists %(database)s; "
195+ "create database %(database)s;") % locals()
196+ cmd = ("mysql -u%(user)s %(password)s -h%(host)s "
197+ "-e\"%(sql)s\"") % locals()
198+ exitcode, out, err = execute(cmd)
199+ self.assertEqual(0, exitcode)
200
201 def cleanup(self):
202 """
203
204=== modified file 'tests/functional/test_curl_api.py'
205--- tests/functional/test_curl_api.py 2011-03-28 18:02:21 +0000
206+++ tests/functional/test_curl_api.py 2011-04-05 21:27:46 +0000
207@@ -25,6 +25,7 @@
208 from tests.utils import execute
209
210 FIVE_KB = 5 * 1024
211+FIVE_GB = 5 * 1024 * 1024 * 1024
212
213
214 class TestCurlApi(functional.FunctionalTest):
215@@ -324,3 +325,62 @@
216 self.assertEqual('x86_64', image['properties']['arch'])
217
218 self.stop_servers()
219+
220+ def test_size_greater_2G_mysql(self):
221+ """
222+ A test against the actual datastore backend for the registry
223+ to ensure that the image size property is not truncated.
224+
225+ :see https://bugs.launchpad.net/glance/+bug/739433
226+ """
227+
228+ self.cleanup()
229+ api_port, reg_port, conf_file = self.start_servers()
230+
231+ # 1. POST /images with public image named Image1
232+ # attribute and a size of 5G. Use the HTTP engine with an
233+ # X-Image-Meta-Location attribute to make Glance forego
234+ # "adding" the image data.
235+ # Verify a 200 OK is returned
236+ cmd = ("curl -i -X POST "
237+ "-H 'Expect: ' " # Necessary otherwise sends 100 Continue
238+ "-H 'X-Image-Meta-Location: http://example.com/fakeimage' "
239+ "-H 'X-Image-Meta-Size: %d' "
240+ "-H 'X-Image-Meta-Name: Image1' "
241+ "-H 'X-Image-Meta-Is-Public: True' "
242+ "http://0.0.0.0:%d/images") % (FIVE_GB, api_port)
243+
244+ exitcode, out, err = execute(cmd)
245+ self.assertEqual(0, exitcode)
246+
247+ lines = out.split("\r\n")
248+ status_line = lines[0]
249+
250+ self.assertEqual("HTTP/1.1 201 Created", status_line)
251+
252+ # Get the ID of the just-added image. This may NOT be 1, since the
253+ # database in the environ variable TEST_GLANCE_CONNECTION may not
254+ # have been cleared between test cases... :(
255+ new_image_uri = None
256+ for line in lines:
257+ if line.startswith('Location:'):
258+ new_image_uri = line[line.find(':') + 1:].strip()
259+
260+ self.assertTrue(new_image_uri is not None,
261+ "Could not find a new image URI!")
262+
263+ # 2. HEAD /images
264+ # Verify image size is what was passed in, and not truncated
265+ cmd = "curl -i -X HEAD %s" % new_image_uri
266+
267+ exitcode, out, err = execute(cmd)
268+
269+ self.assertEqual(0, exitcode)
270+
271+ lines = out.split("\r\n")
272+ status_line = lines[0]
273+
274+ self.assertEqual("HTTP/1.1 200 OK", status_line)
275+ self.assertTrue("X-Image-Meta-Size: %d" % FIVE_GB in out,
276+ "Size was supposed to be %d. Got:\n%s."
277+ % (FIVE_GB, out))

Subscribers

People subscribed via source and target branches