Merge lp:~jaypipes/glance/parallax-register-image into lp:~hudson-openstack/glance/trunk

Proposed by Jay Pipes
Status: Merged
Approved by: Michael Gundlach
Approved revision: 15
Merged at revision: 14
Proposed branch: lp:~jaypipes/glance/parallax-register-image
Merge into: lp:~hudson-openstack/glance/trunk
Diff against target: 515 lines (+310/-37)
8 files modified
glance/common/wsgi.py (+0/-1)
glance/parallax/controllers.py (+58/-15)
glance/parallax/db/sqlalchemy/api.py (+1/-1)
glance/parallax/db/sqlalchemy/models.py (+17/-15)
glance/teller/controllers.py (+1/-1)
run_tests.sh (+4/-4)
tests/stubs.py (+87/-0)
tests/unit/test_parallax_api.py (+142/-0)
To merge this branch: bzr merge lp:~jaypipes/glance/parallax-register-image
Reviewer Review Type Date Requested Status
Michael Gundlach (community) Approve
Rick Harris (community) Approve
Review via email: mp+38258@code.launchpad.net

Description of the change

Implements Parallax API call to register a new image

* Adds unit test for Parallax API controller
* Adds stubouts for glance.parallax.db.sqlalchemy.api calls
  regarding images
* Adds unittest for bad status on image creation
* Adds --logging-clear-handlers arg to nosetests in run_tests.sh to
  prevent extra output in running tests when tests complete successfully

To post a comment you must log in.
Revision history for this message
Michael Gundlach (gundlach) wrote :

Why does ImageController.__init__ exist?

57: should be
  if 'status' not in image_data:
or possibly 56/57/58 could be replaced with
  image_data.setdefault('status', 'available')

101: i assume you meant to remove support for GET /images/detail.

Otherwise, lgtm.

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

> Why does ImageController.__init__ exist?

lol. :) Leftover from when I had some debugging statements in there. ::blush::

> 57: should be
> if 'status' not in image_data:
> or possibly 56/57/58 could be replaced with
> image_data.setdefault('status', 'available')

Ah, nice. Thanks.

> 101: i assume you meant to remove support for GET /images/detail.

Yes, that does not comply with the Atom publishing protocol. No need for it AFAICT.

> Otherwise, lgtm.

Thanks! I'll push those little fixes later today :)

Revision history for this message
Michael Gundlach (gundlach) wrote :

On Tue, Oct 12, 2010 at 4:36 PM, Jay Pipes <email address hidden> wrote:

> > 101: i assume you meant to remove support for GET /images/detail.
>
> Yes, that does not comply with the Atom publishing protocol. No need for
> it AFAICT.
>

We need it in order to comply as much as possible with the current Rackspace
API. GET /images should just return id and name, while /images/detail
returns all other available data. (same for GET /images/4 and GET
/images/4/detail .)

I think our goal was to conform to the RS API, not to conform to the Atom
protocol. Then again, we're planning to iterate on the API shortly after
Austin, so I'm not positive what this buys us.

cc'ing a couple of people from whom I've heard the "conform to the RS API"
message, to get their second opinions.

Michael

Revision history for this message
Rick Clark (dendrobates) wrote :

On 10/13/2010 11:00 AM, Michael Gundlach wrote:
> I think our goal was to conform to the RS API, not to conform to the Atom
> protocol. Then again, we're planning to iterate on the API shortly after
> Austin, so I'm not positive what this buys us.

We need to conform as much as possible to the RS API until they release
1.1 and hand off API development to us. At that point we can
diverge/fix in future versions.

I am not commenting on the technical merits of this discussion though.

> cc'ing a couple of people from whom I've heard the "conform to the RS API"
> message, to get their second opinions.
>
> Michael
>

Revision history for this message
Rick Harris (rconradharris) wrote :

++ on gundlach's cleanup suggestions, otherwise, lgtm.

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

cool, understood as far as RS API. fix coming shortly.

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

Alrighty, added the /images/detail call, a unit test for it, and gundlach's cleanups. Please mark the merge prop Approved if all meets expectations. :)

15. By Jay Pipes

Adds a /images/detail route to the Parallax controller, adds a unit test for it, and cleans up Michael's suggestions.

Revision history for this message
Michael Gundlach (gundlach) wrote :

yeehaw

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'glance/common/wsgi.py'
--- glance/common/wsgi.py 2010-10-01 23:07:46 +0000
+++ glance/common/wsgi.py 2010-10-13 18:50:33 +0000
@@ -224,7 +224,6 @@
224 Uses self._serialization_metadata if it exists, which is a dict mapping224 Uses self._serialization_metadata if it exists, which is a dict mapping
225 MIME types to information needed to serialize to that type.225 MIME types to information needed to serialize to that type.
226 """226 """
227 # FIXME(sirp): type(self) should just be `self`
228 _metadata = getattr(type(self), "_serialization_metadata", {})227 _metadata = getattr(type(self), "_serialization_metadata", {})
229 serializer = Serializer(request.environ, _metadata)228 serializer = Serializer(request.environ, _metadata)
230 return serializer.to_content_type(data)229 return serializer.to_content_type(data)
231230
=== modified file 'glance/parallax/controllers.py'
--- glance/parallax/controllers.py 2010-10-11 18:28:35 +0000
+++ glance/parallax/controllers.py 2010-10-13 18:50:33 +0000
@@ -18,26 +18,52 @@
18Parllax Image controller18Parllax Image controller
19"""19"""
2020
21import json
21import routes22import routes
23from webob import exc
24
22from glance.common import wsgi25from glance.common import wsgi
23from glance.common import exception26from glance.common import exception
24from glance.parallax import db27from glance.parallax import db
25from webob import exc
2628
2729
28class ImageController(wsgi.Controller):30class ImageController(wsgi.Controller):
31
29 """Image Controller """32 """Image Controller """
30 33
31 def index(self, req):34 def index(self, req):
32 """Return data for all public, non-deleted images """35 """Return basic information for all public, non-deleted images
36
37 :param req: the Request object coming from the wsgi layer
38 :retval a mapping of the following form::
39
40 dict(images=[image_list])
41
42 Where image_list is a sequence of mappings::
43
44 {'id': image_id, 'name': image_name}
45
46 """
47 images = db.image_get_all_public(None)
48 image_dicts = [dict(id=i['id'], name=i['name']) for i in images]
49 return dict(images=image_dicts)
50
51 def detail(self, req):
52 """Return detailed information for all public, non-deleted images
53
54 :param req: the Request object coming from the wsgi layer
55 :retval a mapping of the following form::
56
57 dict(images=[image_list])
58
59 Where image_list is a sequence of mappings containing
60 all image model fields.
61
62 """
33 images = db.image_get_all_public(None)63 images = db.image_get_all_public(None)
34 image_dicts = [self._make_image_dict(i) for i in images]64 image_dicts = [self._make_image_dict(i) for i in images]
35 return dict(images=image_dicts)65 return dict(images=image_dicts)
3666
37 def detail(self, req):
38 """Detail is not currently supported """
39 raise exc.HTTPNotImplemented()
40
41 def show(self, req, id):67 def show(self, req, id):
42 """Return data about the given image id."""68 """Return data about the given image id."""
43 try:69 try:
@@ -52,8 +78,24 @@
52 raise exc.HTTPNotImplemented()78 raise exc.HTTPNotImplemented()
5379
54 def create(self, req):80 def create(self, req):
55 """Create is not currently supported """81 """Registers a new image with the registry.
56 raise exc.HTTPNotImplemented()82
83 :param req: Request body. A JSON-ified dict of information about
84 the image.
85
86 :retval Returns the newly-created image information as a mapping,
87 which will include the newly-created image's internal id
88 in the 'id' field
89
90 """
91 image_data = json.loads(req.body)
92
93 # Ensure the image has a status set
94 image_data.setdefault('status', 'available')
95
96 context = None
97 new_image = db.image_create(context, image_data)
98 return dict(new_image)
5799
58 def update(self, req, id):100 def update(self, req, id):
59 """Update is not currently supported """101 """Update is not currently supported """
@@ -64,23 +106,23 @@
64 """ Create a dict represenation of an image which we can use to106 """ Create a dict represenation of an image which we can use to
65 serialize the image.107 serialize the image.
66 """108 """
67 def _fetch_attrs(obj, attrs):109 def _fetch_attrs(d, attrs):
68 return dict([(a, getattr(obj, a)) for a in attrs])110 return dict([(a, d[a]) for a in attrs])
69111
70 # attributes common to all models112 # attributes common to all models
71 base_attrs = set(['id', 'created_at', 'updated_at', 'deleted_at',113 base_attrs = set(['id', 'created_at', 'updated_at', 'deleted_at',
72 'deleted'])114 'deleted'])
73115
74 file_attrs = base_attrs | set(['location', 'size'])116 file_attrs = base_attrs | set(['location', 'size'])
75 files = [_fetch_attrs(f, file_attrs) for f in image.files]117 files = [_fetch_attrs(f, file_attrs) for f in image['files']]
76118
77 # TODO(sirp): should this be a dict, or a list of dicts?119 # TODO(sirp): should this be a dict, or a list of dicts?
78 # A plain dict is more convenient, but list of dicts would provide120 # A plain dict is more convenient, but list of dicts would provide
79 # access to created_at, etc121 # access to created_at, etc
80 metadata = dict((m.key, m.value) for m in image.metadata 122 metadata = dict((m['key'], m['value']) for m in image['metadata']
81 if not m.deleted)123 if not m['deleted'])
82124
83 image_attrs = base_attrs | set(['name', 'image_type', 'state', 'public'])125 image_attrs = base_attrs | set(['name', 'image_type', 'status', 'is_public'])
84 image_dict = _fetch_attrs(image, image_attrs)126 image_dict = _fetch_attrs(image, image_attrs)
85127
86 image_dict['files'] = files128 image_dict['files'] = files
@@ -95,5 +137,6 @@
95 # TODO(sirp): should we add back the middleware for parallax?137 # TODO(sirp): should we add back the middleware for parallax?
96 mapper = routes.Mapper()138 mapper = routes.Mapper()
97 mapper.resource("image", "images", controller=ImageController(),139 mapper.resource("image", "images", controller=ImageController(),
98 collection={'detail': 'GET'})140 collection={'detail': 'GET'})
141 mapper.connect("/", controller=ImageController(), action="index")
99 super(API, self).__init__(mapper)142 super(API, self).__init__(mapper)
100143
=== modified file 'glance/parallax/db/sqlalchemy/api.py'
--- glance/parallax/db/sqlalchemy/api.py 2010-10-01 20:27:48 +0000
+++ glance/parallax/db/sqlalchemy/api.py 2010-10-13 18:50:33 +0000
@@ -95,7 +95,7 @@
95 ).options(joinedload(models.Image.files)95 ).options(joinedload(models.Image.files)
96 ).options(joinedload(models.Image.metadata)96 ).options(joinedload(models.Image.metadata)
97 ).filter_by(deleted=_deleted(context)97 ).filter_by(deleted=_deleted(context)
98 ).filter_by(public=public98 ).filter_by(is_public=public
99 ).all()99 ).all()
100100
101101
102102
=== modified file 'glance/parallax/db/sqlalchemy/models.py'
--- glance/parallax/db/sqlalchemy/models.py 2010-10-01 19:51:29 +0000
+++ glance/parallax/db/sqlalchemy/models.py 2010-10-13 18:50:33 +0000
@@ -23,16 +23,13 @@
23import sys23import sys
24import datetime24import datetime
2525
26# TODO(vish): clean up these imports26from sqlalchemy.orm import relationship, backref, exc, object_mapper, validates
27from sqlalchemy.orm import relationship, backref, exc, object_mapper
28from sqlalchemy import Column, Integer, String27from sqlalchemy import Column, Integer, String
29from sqlalchemy import ForeignKey, DateTime, Boolean, Text28from sqlalchemy import ForeignKey, DateTime, Boolean, Text
30from sqlalchemy.ext.declarative import declarative_base29from sqlalchemy.ext.declarative import declarative_base
3130
32from glance.common.db.sqlalchemy.session import get_session31from glance.common.db.sqlalchemy.session import get_session
3332
34# FIXME(sirp): confirm this is not needed
35#from common import auth
36from glance.common import exception33from glance.common import exception
37from glance.common import flags34from glance.common import flags
3835
@@ -40,6 +37,7 @@
4037
41BASE = declarative_base()38BASE = declarative_base()
4239
40
43#TODO(sirp): ModelBase should be moved out so Glance and Nova can share it41#TODO(sirp): ModelBase should be moved out so Glance and Nova can share it
44class ModelBase(object):42class ModelBase(object):
45 """Base class for Nova and Glance Models"""43 """Base class for Nova and Glance Models"""
@@ -128,18 +126,22 @@
128 126
129 id = Column(Integer, primary_key=True)127 id = Column(Integer, primary_key=True)
130 name = Column(String(255))128 name = Column(String(255))
131 image_type = Column(String(255))129 image_type = Column(String(30))
132 state = Column(String(255))130 status = Column(String(30))
133 public = Column(Boolean, default=False)131 is_public = Column(Boolean, default=False)
134132
135 #@validates('image_type')133 @validates('image_type')
136 #def validate_image_type(self, key, image_type):134 def validate_image_type(self, key, image_type):
137 # assert(image_type in ('machine', 'kernel', 'ramdisk', 'raw'))135 if not image_type in ('machine', 'kernel', 'ramdisk', 'raw'):
138 #136 raise exception.Invalid("Invalid image type '%s' for image." % image_type)
139 #@validates('state')137 return image_type
140 #def validate_state(self, key, state):138
141 # assert(state in ('available', 'pending', 'disabled'))139 @validates('status')
142 #140 def validate_status(self, key, state):
141 if not state in ('available', 'pending', 'disabled'):
142 raise exception.Invalid("Invalid status '%s' for image." % status)
143 return image_type
144
143 # TODO(sirp): should these be stored as metadata?145 # TODO(sirp): should these be stored as metadata?
144 #user_id = Column(String(255))146 #user_id = Column(String(255))
145 #project_id = Column(String(255))147 #project_id = Column(String(255))
146148
=== modified file 'glance/teller/controllers.py'
--- glance/teller/controllers.py 2010-10-11 18:28:35 +0000
+++ glance/teller/controllers.py 2010-10-13 18:50:33 +0000
@@ -95,7 +95,7 @@
9595
9696
97class API(wsgi.Router):97class API(wsgi.Router):
98 """WSGI entry point for all Parallax requests."""98 """WSGI entry point for all Teller requests."""
9999
100 def __init__(self):100 def __init__(self):
101 mapper = routes.Mapper()101 mapper = routes.Mapper()
102102
=== modified file 'run_tests.sh'
--- run_tests.sh 2010-10-08 20:42:13 +0000
+++ run_tests.sh 2010-10-13 18:50:33 +0000
@@ -41,12 +41,12 @@
4141
42if [ $never_venv -eq 1 ]; then42if [ $never_venv -eq 1 ]; then
43 # Just run the test suites in current environment43 # Just run the test suites in current environment
44 python run_tests.py44 nosetests --logging-clear-handlers
45 exit45 exit
46fi46fi
4747
48if [ -e ${venv} ]; then48if [ -e ${venv} ]; then
49 ${with_venv} nosetests49 ${with_venv} nosetests --logging-clear-handlers
50else 50else
51 if [ $always_venv -eq 1 ]; then51 if [ $always_venv -eq 1 ]; then
52 # Automatically install the virtualenv52 # Automatically install the virtualenv
@@ -58,9 +58,9 @@
58 # Install the virtualenv and run the test suite in it58 # Install the virtualenv and run the test suite in it
59 python tools/install_venv.py59 python tools/install_venv.py
60 else60 else
61 nosetests61 nosetests --logging-clear-handlers
62 exit62 exit
63 fi63 fi
64 fi64 fi
65 ${with_venv} nosetests65 ${with_venv} nosetests --logging-clear-handlers
66fi66fi
6767
=== modified file 'tests/stubs.py'
--- tests/stubs.py 2010-10-11 18:28:35 +0000
+++ tests/stubs.py 2010-10-13 18:50:33 +0000
@@ -17,12 +17,15 @@
1717
18"""Stubouts, mocks and fixtures for the test suite"""18"""Stubouts, mocks and fixtures for the test suite"""
1919
20import datetime
20import httplib21import httplib
21import StringIO22import StringIO
2223
23import stubout24import stubout
2425
26from glance.common import exception
25import glance.teller.backends.swift27import glance.teller.backends.swift
28import glance.parallax.db.sqlalchemy.api
2629
27def stub_out_http_backend(stubs):30def stub_out_http_backend(stubs):
28 """Stubs out the httplib.HTTPRequest.getresponse to return31 """Stubs out the httplib.HTTPRequest.getresponse to return
@@ -147,3 +150,87 @@
147 fake_parallax_registry = FakeParallax()150 fake_parallax_registry = FakeParallax()
148 stubs.Set(glance.teller.registries.Parallax, 'lookup',151 stubs.Set(glance.teller.registries.Parallax, 'lookup',
149 fake_parallax_registry.lookup)152 fake_parallax_registry.lookup)
153
154
155def stub_out_parallax_db_image_api(stubs):
156 """Stubs out the database set/fetch API calls for Parallax
157 so the calls are routed to an in-memory dict. This helps us
158 avoid having to manually clear or flush the SQLite database.
159
160 The "datastore" always starts with this set of image fixtures.
161
162 :param stubs: Set of stubout stubs
163
164 """
165 class FakeDatastore(object):
166
167 FIXTURES = [
168 {'id': 1,
169 'name': 'fake image #1',
170 'status': 'available',
171 'image_type': 'kernel',
172 'is_public': False,
173 'created_at': datetime.datetime.utcnow(),
174 'updated_at': datetime.datetime.utcnow(),
175 'deleted_at': None,
176 'deleted': False,
177 'files': [],
178 'metadata': []},
179 {'id': 2,
180 'name': 'fake image #2',
181 'status': 'available',
182 'image_type': 'kernel',
183 'is_public': True,
184 'created_at': datetime.datetime.utcnow(),
185 'updated_at': datetime.datetime.utcnow(),
186 'deleted_at': None,
187 'deleted': False,
188 'files': [],
189 'metadata': []}]
190
191 VALID_STATUSES = ('available', 'disabled', 'pending')
192
193 def __init__(self):
194 self.images = self.FIXTURES
195 self.next_id = 3
196
197 def image_create(self, _context, values):
198 values['id'] = self.next_id
199
200 if 'status' not in values.keys():
201 values['status'] = 'available'
202 else:
203 if not values['status'] in self.VALID_STATUSES:
204 raise exception.Invalid("Invalid status '%s' for image" % values['status'])
205
206 self.next_id += 1
207 self.images.extend(values)
208 return values
209
210 def image_destroy(self, _context, image_id):
211 try:
212 del self.images[image_id]
213 except KeyError:
214 new_exc = exception.NotFound("No model for id %s" % image_id)
215 raise new_exc.__class__, new_exc, sys.exc_info()[2]
216
217 def image_get(self, _context, image_id):
218 if image_id not in self.images.keys() or self.images[image_id]['deleted']:
219 new_exc = exception.NotFound("No model for id %s" % image_id)
220 raise new_exc.__class__, new_exc, sys.exc_info()[2]
221 else:
222 return self.images[image_id]
223
224 def image_get_all_public(self, _context, public):
225 return [f for f in self.images
226 if f['is_public'] == public]
227
228 fake_datastore = FakeDatastore()
229 stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_create',
230 fake_datastore.image_create)
231 stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_destroy',
232 fake_datastore.image_destroy)
233 stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_get',
234 fake_datastore.image_get)
235 stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_get_all_public',
236 fake_datastore.image_get_all_public)
150237
=== added file 'tests/unit/test_parallax_api.py'
--- tests/unit/test_parallax_api.py 1970-01-01 00:00:00 +0000
+++ tests/unit/test_parallax_api.py 2010-10-13 18:50:33 +0000
@@ -0,0 +1,142 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2010 OpenStack, LLC
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18import json
19import stubout
20import unittest
21import webob
22
23from glance.common import exception
24from glance.parallax import controllers
25from glance.parallax import db
26from tests import stubs
27
28
29class TestImageController(unittest.TestCase):
30 def setUp(self):
31 """Establish a clean test environment"""
32 self.stubs = stubout.StubOutForTesting()
33 self.image_controller = controllers.ImageController()
34 stubs.stub_out_parallax_db_image_api(self.stubs)
35
36 def tearDown(self):
37 """Clear the test environment"""
38 self.stubs.UnsetAll()
39
40 def test_get_root(self):
41 """Tests that the root parallax API returns "index",
42 which is a list of public images
43
44 """
45 fixture = {'id': 2,
46 'name': 'fake image #2'}
47 req = webob.Request.blank('/')
48 res = req.get_response(controllers.API())
49 res_dict = json.loads(res.body)
50 self.assertEquals(res.status_int, 200)
51
52 images = res_dict['images']
53 self.assertEquals(len(images), 1)
54
55 for k,v in fixture.iteritems():
56 self.assertEquals(v, images[0][k])
57
58 def test_get_index(self):
59 """Tests that the /images parallax API returns list of
60 public images
61
62 """
63 fixture = {'id': 2,
64 'name': 'fake image #2'}
65 req = webob.Request.blank('/images')
66 res = req.get_response(controllers.API())
67 res_dict = json.loads(res.body)
68 self.assertEquals(res.status_int, 200)
69
70 images = res_dict['images']
71 self.assertEquals(len(images), 1)
72
73 for k,v in fixture.iteritems():
74 self.assertEquals(v, images[0][k])
75
76 def test_get_details(self):
77 """Tests that the /images/detail parallax API returns
78 a mapping containing a list of detailed image information
79
80 """
81 fixture = {'id': 2,
82 'name': 'fake image #2',
83 'is_public': True,
84 'image_type': 'kernel',
85 'status': 'available'
86 }
87 req = webob.Request.blank('/images/detail')
88 res = req.get_response(controllers.API())
89 res_dict = json.loads(res.body)
90 self.assertEquals(res.status_int, 200)
91
92 images = res_dict['images']
93 self.assertEquals(len(images), 1)
94
95 for k,v in fixture.iteritems():
96 self.assertEquals(v, images[0][k])
97
98 def test_create_image(self):
99 """Tests that the /images POST parallax API creates the image"""
100 fixture = {'name': 'fake public image',
101 'is_public': True,
102 'image_type': 'kernel'
103 }
104
105 req = webob.Request.blank('/images')
106
107 req.method = 'POST'
108 req.body = json.dumps(fixture)
109
110 res = req.get_response(controllers.API())
111
112 self.assertEquals(res.status_int, 200)
113
114 res_dict = json.loads(res.body)
115
116 for k,v in fixture.iteritems():
117 self.assertEquals(v, res_dict[k])
118
119 # Test ID auto-assigned properly
120 self.assertEquals(3, res_dict['id'])
121
122 # Test status was updated properly
123 self.assertEquals('available', res_dict['status'])
124
125 def test_create_image_with_bad_status(self):
126 """Tests proper exception is raised if a bad status is set"""
127 fixture = {'id': 3,
128 'name': 'fake public image',
129 'is_public': True,
130 'image_type': 'kernel',
131 'status': 'bad status'
132 }
133
134 req = webob.Request.blank('/images')
135
136 req.method = 'POST'
137 req.body = json.dumps(fixture)
138
139 # TODO(jaypipes): Port Nova's Fault infrastructure
140 # over to Glance to support exception catching into
141 # standard HTTP errors.
142 self.assertRaises(exception.Invalid, req.get_response, controllers.API())

Subscribers

People subscribed via source and target branches