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 --logging-clear-handlers arg to nosetests in run_tests.sh to prevent extra output in running tests when tests complete successfully
This commit is contained in:
parent
4c5a45f42d
commit
d59d1f1ca1
|
@ -224,7 +224,6 @@ class Controller(object):
|
|||
Uses self._serialization_metadata if it exists, which is a dict mapping
|
||||
MIME types to information needed to serialize to that type.
|
||||
"""
|
||||
# FIXME(sirp): type(self) should just be `self`
|
||||
_metadata = getattr(type(self), "_serialization_metadata", {})
|
||||
serializer = Serializer(request.environ, _metadata)
|
||||
return serializer.to_content_type(data)
|
||||
|
|
|
@ -18,15 +18,20 @@
|
|||
Parllax Image controller
|
||||
"""
|
||||
|
||||
import json
|
||||
import routes
|
||||
from webob import exc
|
||||
|
||||
from glance.common import wsgi
|
||||
from glance.common import exception
|
||||
from glance.parallax import db
|
||||
from webob import exc
|
||||
|
||||
|
||||
class ImageController(wsgi.Controller):
|
||||
"""Image Controller """
|
||||
|
||||
def __init__(self):
|
||||
super(ImageController, self).__init__()
|
||||
|
||||
def index(self, req):
|
||||
"""Return data for all public, non-deleted images """
|
||||
|
@ -52,8 +57,25 @@ class ImageController(wsgi.Controller):
|
|||
raise exc.HTTPNotImplemented()
|
||||
|
||||
def create(self, req):
|
||||
"""Create is not currently supported """
|
||||
raise exc.HTTPNotImplemented()
|
||||
"""Registers a new image with the registry.
|
||||
|
||||
:param req: Request body. A JSON-ified dict of information about
|
||||
the image.
|
||||
|
||||
:retval Returns the newly-created image information as a mapping,
|
||||
which will include the newly-created image's internal id
|
||||
in the 'id' field
|
||||
|
||||
"""
|
||||
image_data = json.loads(req.body)
|
||||
|
||||
# Ensure the image has a status set
|
||||
if 'status' not in image_data.keys():
|
||||
image_data['status'] = 'available'
|
||||
|
||||
context = None
|
||||
new_image = db.image_create(context, image_data)
|
||||
return dict(new_image)
|
||||
|
||||
def update(self, req, id):
|
||||
"""Update is not currently supported """
|
||||
|
@ -64,23 +86,23 @@ class ImageController(wsgi.Controller):
|
|||
""" Create a dict represenation of an image which we can use to
|
||||
serialize the image.
|
||||
"""
|
||||
def _fetch_attrs(obj, attrs):
|
||||
return dict([(a, getattr(obj, a)) for a in attrs])
|
||||
def _fetch_attrs(d, attrs):
|
||||
return dict([(a, d[a]) for a in attrs])
|
||||
|
||||
# attributes common to all models
|
||||
base_attrs = set(['id', 'created_at', 'updated_at', 'deleted_at',
|
||||
'deleted'])
|
||||
|
||||
file_attrs = base_attrs | set(['location', 'size'])
|
||||
files = [_fetch_attrs(f, file_attrs) for f in image.files]
|
||||
files = [_fetch_attrs(f, file_attrs) for f in image['files']]
|
||||
|
||||
# TODO(sirp): should this be a dict, or a list of dicts?
|
||||
# A plain dict is more convenient, but list of dicts would provide
|
||||
# access to created_at, etc
|
||||
metadata = dict((m.key, m.value) for m in image.metadata
|
||||
if not m.deleted)
|
||||
metadata = dict((m['key'], m['value']) for m in image['metadata']
|
||||
if not m['deleted'])
|
||||
|
||||
image_attrs = base_attrs | set(['name', 'image_type', 'state', 'public'])
|
||||
image_attrs = base_attrs | set(['name', 'image_type', 'status', 'is_public'])
|
||||
image_dict = _fetch_attrs(image, image_attrs)
|
||||
|
||||
image_dict['files'] = files
|
||||
|
@ -94,6 +116,6 @@ class API(wsgi.Router):
|
|||
def __init__(self):
|
||||
# TODO(sirp): should we add back the middleware for parallax?
|
||||
mapper = routes.Mapper()
|
||||
mapper.resource("image", "images", controller=ImageController(),
|
||||
collection={'detail': 'GET'})
|
||||
mapper.resource("image", "images", controller=ImageController())
|
||||
mapper.connect("/", controller=ImageController(), action="index")
|
||||
super(API, self).__init__(mapper)
|
||||
|
|
|
@ -95,7 +95,7 @@ def image_get_all_public(context, public):
|
|||
).options(joinedload(models.Image.files)
|
||||
).options(joinedload(models.Image.metadata)
|
||||
).filter_by(deleted=_deleted(context)
|
||||
).filter_by(public=public
|
||||
).filter_by(is_public=public
|
||||
).all()
|
||||
|
||||
|
||||
|
|
|
@ -23,16 +23,13 @@ SQLAlchemy models for glance data
|
|||
import sys
|
||||
import datetime
|
||||
|
||||
# TODO(vish): clean up these imports
|
||||
from sqlalchemy.orm import relationship, backref, exc, object_mapper
|
||||
from sqlalchemy.orm import relationship, backref, exc, object_mapper, validates
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy import ForeignKey, DateTime, Boolean, Text
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from glance.common.db.sqlalchemy.session import get_session
|
||||
|
||||
# FIXME(sirp): confirm this is not needed
|
||||
#from common import auth
|
||||
from glance.common import exception
|
||||
from glance.common import flags
|
||||
|
||||
|
@ -40,6 +37,7 @@ FLAGS = flags.FLAGS
|
|||
|
||||
BASE = declarative_base()
|
||||
|
||||
|
||||
#TODO(sirp): ModelBase should be moved out so Glance and Nova can share it
|
||||
class ModelBase(object):
|
||||
"""Base class for Nova and Glance Models"""
|
||||
|
@ -128,18 +126,18 @@ class Image(BASE, ModelBase):
|
|||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(255))
|
||||
image_type = Column(String(255))
|
||||
state = Column(String(255))
|
||||
public = Column(Boolean, default=False)
|
||||
image_type = Column(String(30))
|
||||
status = Column(String(30))
|
||||
is_public = Column(Boolean, default=False)
|
||||
|
||||
#@validates('image_type')
|
||||
#def validate_image_type(self, key, image_type):
|
||||
# assert(image_type in ('machine', 'kernel', 'ramdisk', 'raw'))
|
||||
#
|
||||
#@validates('state')
|
||||
#def validate_state(self, key, state):
|
||||
# assert(state in ('available', 'pending', 'disabled'))
|
||||
#
|
||||
@validates('image_type')
|
||||
def validate_image_type(self, key, image_type):
|
||||
assert(image_type in ('machine', 'kernel', 'ramdisk', 'raw'))
|
||||
|
||||
@validates('status')
|
||||
def validate_status(self, key, state):
|
||||
assert(state in ('available', 'pending', 'disabled'))
|
||||
|
||||
# TODO(sirp): should these be stored as metadata?
|
||||
#user_id = Column(String(255))
|
||||
#project_id = Column(String(255))
|
||||
|
|
|
@ -95,7 +95,7 @@ class ImageController(wsgi.Controller):
|
|||
|
||||
|
||||
class API(wsgi.Router):
|
||||
"""WSGI entry point for all Parallax requests."""
|
||||
"""WSGI entry point for all Teller requests."""
|
||||
|
||||
def __init__(self):
|
||||
mapper = routes.Mapper()
|
||||
|
|
|
@ -41,12 +41,12 @@ process_options $options
|
|||
|
||||
if [ $never_venv -eq 1 ]; then
|
||||
# Just run the test suites in current environment
|
||||
python run_tests.py
|
||||
nosetests --logging-clear-handlers
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -e ${venv} ]; then
|
||||
${with_venv} nosetests
|
||||
${with_venv} nosetests --logging-clear-handlers
|
||||
else
|
||||
if [ $always_venv -eq 1 ]; then
|
||||
# Automatically install the virtualenv
|
||||
|
@ -58,9 +58,9 @@ else
|
|||
# Install the virtualenv and run the test suite in it
|
||||
python tools/install_venv.py
|
||||
else
|
||||
nosetests
|
||||
nosetests --logging-clear-handlers
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
${with_venv} nosetests
|
||||
${with_venv} nosetests --logging-clear-handlers
|
||||
fi
|
||||
|
|
|
@ -17,12 +17,14 @@
|
|||
|
||||
"""Stubouts, mocks and fixtures for the test suite"""
|
||||
|
||||
import datetime
|
||||
import httplib
|
||||
import StringIO
|
||||
|
||||
import stubout
|
||||
|
||||
import glance.teller.backends.swift
|
||||
import glance.parallax.db.sqlalchemy.api
|
||||
|
||||
def stub_out_http_backend(stubs):
|
||||
"""Stubs out the httplib.HTTPRequest.getresponse to return
|
||||
|
@ -147,3 +149,80 @@ def stub_out_parallax(stubs):
|
|||
fake_parallax_registry = FakeParallax()
|
||||
stubs.Set(glance.teller.registries.Parallax, 'lookup',
|
||||
fake_parallax_registry.lookup)
|
||||
|
||||
|
||||
def stub_out_parallax_db_image_api(stubs):
|
||||
"""Stubs out the database set/fetch API calls for Parallax
|
||||
so the calls are routed to an in-memory dict. This helps us
|
||||
avoid having to manually clear or flush the SQLite database.
|
||||
|
||||
The "datastore" always starts with this set of image fixtures.
|
||||
|
||||
:param stubs: Set of stubout stubs
|
||||
|
||||
"""
|
||||
class FakeDatastore(object):
|
||||
|
||||
FIXTURES = [
|
||||
{'id': 1,
|
||||
'name': 'fake image #1',
|
||||
'status': 'available',
|
||||
'image_type': 'kernel',
|
||||
'is_public': False,
|
||||
'created_at': datetime.datetime.utcnow(),
|
||||
'updated_at': datetime.datetime.utcnow(),
|
||||
'deleted_at': None,
|
||||
'deleted': False,
|
||||
'files': [],
|
||||
'metadata': []},
|
||||
{'id': 2,
|
||||
'name': 'fake image #2',
|
||||
'status': 'available',
|
||||
'image_type': 'kernel',
|
||||
'is_public': True,
|
||||
'created_at': datetime.datetime.utcnow(),
|
||||
'updated_at': datetime.datetime.utcnow(),
|
||||
'deleted_at': None,
|
||||
'deleted': False,
|
||||
'files': [],
|
||||
'metadata': []}]
|
||||
|
||||
def __init__(self):
|
||||
self.images = self.FIXTURES
|
||||
self.next_id = 3
|
||||
|
||||
def image_create(self, _context, values):
|
||||
values['id'] = self.next_id
|
||||
if 'status' not in values.keys():
|
||||
values['status'] = 'available'
|
||||
self.next_id += 1
|
||||
self.images.extend(values)
|
||||
return values
|
||||
|
||||
def image_destroy(self, _context, image_id):
|
||||
try:
|
||||
del self.images[image_id]
|
||||
except KeyError:
|
||||
new_exc = exception.NotFound("No model for id %s" % image_id)
|
||||
raise new_exc.__class__, new_exc, sys.exc_info()[2]
|
||||
|
||||
def image_get(self, _context, image_id):
|
||||
if image_id not in self.images.keys() or self.images[image_id]['deleted']:
|
||||
new_exc = exception.NotFound("No model for id %s" % image_id)
|
||||
raise new_exc.__class__, new_exc, sys.exc_info()[2]
|
||||
else:
|
||||
return self.images[image_id]
|
||||
|
||||
def image_get_all_public(self, _context, public):
|
||||
return [f for f in self.images
|
||||
if f['is_public'] == public]
|
||||
|
||||
fake_datastore = FakeDatastore()
|
||||
stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_create',
|
||||
fake_datastore.image_create)
|
||||
stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_destroy',
|
||||
fake_datastore.image_destroy)
|
||||
stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_get',
|
||||
fake_datastore.image_get)
|
||||
stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_get_all_public',
|
||||
fake_datastore.image_get_all_public)
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 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 stubout
|
||||
import unittest
|
||||
import webob
|
||||
|
||||
from glance.parallax import controllers
|
||||
from glance.parallax import db
|
||||
from tests import stubs
|
||||
|
||||
|
||||
class TestImageController(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Establish a clean test environment"""
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
self.image_controller = controllers.ImageController()
|
||||
stubs.stub_out_parallax_db_image_api(self.stubs)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clear the test environment"""
|
||||
self.stubs.UnsetAll()
|
||||
|
||||
def test_get_root(self):
|
||||
"""Tests that the root parallax API returns "index",
|
||||
which is a list of public images
|
||||
|
||||
"""
|
||||
fixture = {'id': 2,
|
||||
'name': 'fake image #2',
|
||||
'is_public': True,
|
||||
'image_type': 'kernel',
|
||||
'status': 'available'
|
||||
}
|
||||
req = webob.Request.blank('/')
|
||||
res = req.get_response(controllers.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
images = res_dict['images']
|
||||
self.assertEquals(len(images), 1)
|
||||
|
||||
for k,v in fixture.iteritems():
|
||||
self.assertEquals(v, images[0][k])
|
||||
|
||||
def test_get_images(self):
|
||||
"""Tests that the /images parallax API returns list of
|
||||
public images
|
||||
|
||||
"""
|
||||
fixture = {'id': 2,
|
||||
'name': 'fake image #2',
|
||||
'is_public': True,
|
||||
'image_type': 'kernel',
|
||||
'status': 'available'
|
||||
}
|
||||
req = webob.Request.blank('/images')
|
||||
res = req.get_response(controllers.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
images = res_dict['images']
|
||||
self.assertEquals(len(images), 1)
|
||||
|
||||
for k,v in fixture.iteritems():
|
||||
self.assertEquals(v, images[0][k])
|
||||
|
||||
def test_create_image(self):
|
||||
"""Tests that the /images POST parallax API creates the image"""
|
||||
fixture = {'id': 3,
|
||||
'name': 'fake public image',
|
||||
'is_public': True,
|
||||
'image_type': 'kernel'
|
||||
}
|
||||
|
||||
req = webob.Request.blank('/images')
|
||||
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(fixture)
|
||||
|
||||
res = req.get_response(controllers.API())
|
||||
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
res_dict = json.loads(res.body)
|
||||
|
||||
for k,v in fixture.iteritems():
|
||||
self.assertEquals(v, res_dict[k])
|
||||
|
||||
# Test status was updated properly
|
||||
self.assertEquals('available', res_dict['status'])
|
Loading…
Reference in New Issue