Major refactoring...

This commit is contained in:
jaypipes@gmail.com
2010-12-17 16:37:06 +00:00
committed by Tarmac
30 changed files with 1776 additions and 634 deletions

View File

@@ -6,25 +6,321 @@
Welcome to Glance's documentation!
==================================
Glance provides an image registration and discovery service (Parallax) and
an image delivery service (Teller). These services are used in conjunction
by Nova to deliver images from object stores, such as OpenStack's Swift
service, to Nova's compute nodes.
The Glance project provides services for discovering, registering, and
retrieving virtual machine images. Glance has a RESTful API that allows
querying of VM image metadata as well as retrieval of the actual image.
.. toctree::
:maxdepth: 1
Overview:
VM images made available through Glance can be stored in a variety of
locations from simple filesystems to object-storage systems like the
OpenStack Swift project.
.. toctree::
:maxdepth: 1
Parallax:
Overview
========
The Glance project provides services for discovering, registering, and
retrieving virtual machine images. Glance has a RESTful API that allows
querying of VM image metadata as well as retrieval of the actual image.
.. toctree::
:maxdepth: 1
Teller:
The Glance API
==============
Glance has a RESTful API that exposes both metadata about registered virtual
machine images and the image data itself.
A host that runs the `bin/glance-api` service is said to be a *Glance API
Server*.
Assume there is a Glance API server running at the URL
http://glance.openstack.org.
Let's walk through how a user might request information from this server.
Requesting a List of Public VM Images
-------------------------------------
We want to see a list of available virtual machine images that the Glance
server knows about.
We issue a `GET` request to http://glance.openstack.org/images/ to retrieve
this list of available *public* images. The data is returned as a JSON-encoded
mapping in the following format::
{'images': [
{'uri': 'http://glance.openstack.org/images/1',
'name': 'Ubuntu 10.04 Plain',
'type': 'kernel',
'size': '5368709120'}
...]}
Notes:
* All images returned from the above `GET` request are *public* images
Requesting Detailed Metadata on Public VM Images
------------------------------------------------
We want to see more detailed information on available virtual machine images
that the Glance server knows about.
We issue a `GET` request to http://glance.openstack.org/images/detail to
retrieve this list of available *public* images. The data is returned as a
JSON-encoded mapping in the following format::
{'images': [
{'uri': 'http://glance.openstack.org/images/1',
'name': 'Ubuntu 10.04 Plain 5GB',
'type': 'kernel',
'size': '5368709120',
'store': 'swift',
'created_at': '2010-02-03 09:34:01',
'updated_at': '2010-02-03 09:34:01',
'deleted_at': '',
'status': 'available',
'is_public': True,
'properties': {'distro': 'Ubuntu 10.04 LTS'}},
...]}
Notes:
* All images returned from the above `GET` request are *public* images
* All timestamps returned are in UTC
* The `updated_at` timestamp is the timestamp when an image's metadata
was last updated, not it's image data, as all image data is immutable
once stored in Glance
* The `properties` field is a mapping of free-form key/value pairs that
have been saved with the image metadata
Requesting Detailed Metadata on a Specific Image
------------------------------------------------
We want to see detailed information for a specific virtual machine image
that the Glance server knows about.
We have queried the Glance server for a list of public images and the
data returned includes the `uri` field for each available image. This
`uri` field value contains the exact location needed to get the metadata
for a specific image.
Continuing the example from above, in order to get metadata about the
first public image returned, we can issue a `HEAD` request to the Glance
server for the image's URI.
We issue a `HEAD` request to http://glance.openstack.org/images/1 to
retrieve complete metadata for that image. The metadata is returned as a
set of HTTP headers that begin with the prefix `x-image-meta-`. The
following shows an example of the HTTP headers returned from the above
`HEAD` request::
x-image-meta-uri http://glance.openstack.org/images/1
x-image-meta-name Ubuntu 10.04 Plain 5GB
x-image-meta-type kernel
x-image-meta-size 5368709120
x-image-meta-store swift
x-image-meta-created_at 2010-02-03 09:34:01
x-image-meta-updated_at 2010-02-03 09:34:01
x-image-meta-deleted_at
x-image-meta-status available
x-image-meta-is_public True
x-image-meta-property-distro Ubuntu 10.04 LTS
Notes:
* All timestamps returned are in UTC
* The `x-image-meta-updated_at` timestamp is the timestamp when an
image's metadata was last updated, not it's image data, as all
image data is immutable once stored in Glance
* There may be multiple headers that begin with the prefix
`x-image-meta-property-`. These headers are free-form key/value pairs
that have been saved with the image metadata. The key is the string
after `x-image-meta-property-` and the value is the value of the header
Retrieving a Virtual Machine Image
----------------------------------
We want to retrieve that actual raw data for a specific virtual machine image
that the Glance server knows about.
We have queried the Glance server for a list of public images and the
data returned includes the `uri` field for each available image. This
`uri` field value contains the exact location needed to get the metadata
for a specific image.
Continuing the example from above, in order to get metadata about the
first public image returned, we can issue a `HEAD` request to the Glance
server for the image's URI.
We issue a `GET` request to http://glance.openstack.org/images/1 to
retrieve metadata for that image as well as the image itself encoded
into the response body.
The metadata is returned as a set of HTTP headers that begin with the
prefix `x-image-meta-`. The following shows an example of the HTTP headers
returned from the above `GET` request::
x-image-meta-uri http://glance.openstack.org/images/1
x-image-meta-name Ubuntu 10.04 Plain 5GB
x-image-meta-type kernel
x-image-meta-size 5368709120
x-image-meta-store swift
x-image-meta-created_at 2010-02-03 09:34:01
x-image-meta-updated_at 2010-02-03 09:34:01
x-image-meta-deleted_at
x-image-meta-status available
x-image-meta-is_public True
x-image-meta-property-distro Ubuntu 10.04 LTS
Notes:
* All timestamps returned are in UTC
* The `x-image-meta-updated_at` timestamp is the timestamp when an
image's metadata was last updated, not it's image data, as all
image data is immutable once stored in Glance
* There may be multiple headers that begin with the prefix
`x-image-meta-property-`. These headers are free-form key/value pairs
that have been saved with the image metadata. The key is the string
after `x-image-meta-property-` and the value is the value of the header
* The response's `Content-Length` header shall be equal to the value of
the `x-image-meta-size` header
* The image data itself will be the body of the HTTP response returned
from the request, which will have content-type of
`application/octet-stream`.
.. toctree::
:maxdepth: 1
Image Identifiers
=================
Images are uniquely identified by way of a URI that
matches the following signature::
<Glance Server Location>/images/<ID>
where `<Glance Server Location>` is the resource location of the Glance service
that knows about an image, and `<ID>` is the image's identifier that is
*unique to that Glance server*.
.. toctree::
:maxdepth: 1
Image Registration
==================
Image metadata made available through Glance can be stored in image
*registries*. Image registries are any web service that adheres to the
Glance RESTful API for image metadata.
.. toctree::
:maxdepth: 1
Using Glance Programmatically with Glance's Client
==================================================
While it is perfectly acceptable to issue HTTP requests directly to Glance
via its RESTful API, sometimes it is better to be able to access and modify
image resources via a client class that removes some of the complexity and
tedium of dealing with raw HTTP requests.
Glance includes a client class for just this purpose. You can retrieve
metadata about an image, change metadata about an image, remove images, and
of course retrieve an image itself via this client class.
Below are some examples of using Glance's Client class. We assume that
there is a Glance server running at the address `glance.openstack.org`
on port `9292`.
Requesting a List of Public VM Images
-------------------------------------
We want to see a list of available virtual machine images that the Glance
server knows about.
Using Glance's Client, we can do this using the following code::
from glance import client
c = client.Client("glance.openstack.org", 9292)
print c.get_images()
Requesting Detailed Metadata on Public VM Images
------------------------------------------------
We want to see more detailed information on available virtual machine images
that the Glance server knows about.
Using Glance's Client, we can do this using the following code::
from glance import client
c = client.Client("glance.openstack.org", 9292)
print c.get_images_detailed()
Requesting Detailed Metadata on a Specific Image
------------------------------------------------
We want to see detailed information for a specific virtual machine image
that the Glance server knows about.
We have queried the Glance server for a list of public images and the
data returned includes the `uri` field for each available image. This
`uri` field value contains the exact location needed to get the metadata
for a specific image.
Continuing the example from above, in order to get metadata about the
first public image returned, we can use the following code::
from glance import client
c = client.Client("glance.openstack.org", 9292)
print c.get_image_meta("http://glance.openstack.org/images/1")
Retrieving a Virtual Machine Image
----------------------------------
We want to retrieve that actual raw data for a specific virtual machine image
that the Glance server knows about.
We have queried the Glance server for a list of public images and the
data returned includes the `uri` field for each available image. This
`uri` field value contains the exact location needed to get the metadata
for a specific image.
Continuing the example from above, in order to get both the metadata about the
first public image returned and its image data, we can use the following code::
from glance import client
c = client.Client("glance.openstack.org", 9292)
meta, image_file = c.get_image("http://glance.openstack.org/images/1")
print meta
f = open('some_local_file', 'wb')
for chunk in image_file:
f.write(chunk)
f.close()
Note that the return from Client.get_image() is a tuple of (`metadata`, `file`)
where `metadata` is a mapping of metadata about the image and `file` is a
generator that yields chunks of image data.
.. toctree::
:maxdepth: 1
@@ -35,4 +331,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@@ -26,6 +26,7 @@ import urlparse
import socket
import sys
from glance import util
from glance.common import exception
#TODO(jaypipes) Allow a logger param for client classes
@@ -50,6 +51,35 @@ class BadInputError(Exception):
pass
class ImageBodyIterator(object):
"""
A class that acts as an iterator over an image file's
chunks of data. This is returned as part of the result
tuple from `glance.client.Client.get_image`
"""
CHUNKSIZE = 65536
def __init__(self, response):
"""
Constructs the object from an HTTPResponse object
"""
self.response = response
def __iter__(self):
"""
Exposes an iterator over the chunks of data in the
image file.
"""
while True:
chunk = self.response.read(ImageBodyIterator.CHUNKSIZE)
if chunk:
yield chunk
else:
break
class BaseClient(object):
"""A base client class"""
@@ -87,7 +117,7 @@ class BaseClient(object):
" to connect to server."
% self.protocol)
def do_request(self, method, action, body=None):
def do_request(self, method, action, body=None, headers=None):
"""
Connects to the server and issues a request. Handles converting
any returned HTTP error status codes to OpenStack/Glance exceptions
@@ -96,13 +126,17 @@ class BaseClient(object):
:param method: HTTP method ("GET", "POST", "PUT", etc...)
:param action: part of URL after root netloc
:param headers: mapping of headers to send
:param data: string of data to send, or None (default)
:param body: string of data to send, or None (default)
:param headers: mapping of key/value pairs to add as headers
"""
try:
connection_type = self.get_connection_type()
c = connection_type(self.netloc, self.port)
c.request(method, action, body)
if headers:
for k, v in headers.iteritems():
c.putheader(k, v)
res = c.getresponse()
status_code = self.get_status_code(res)
if status_code == httplib.OK:
@@ -135,67 +169,27 @@ class BaseClient(object):
return response.status
class TellerClient(BaseClient):
class Client(BaseClient):
"""A client for the Teller image caching and delivery service"""
"""Main client class for accessing Glance resources"""
DEFAULT_ADDRESS = 'http://127.0.0.1'
DEFAULT_PORT = 9292
def __init__(self, **kwargs):
"""
Creates a new client to a Teller service. All args are keyword
Creates a new client to a Glance service. All args are keyword
arguments.
:param address: The address where Teller resides (defaults to
:param address: The address where Glance resides (defaults to
http://127.0.0.1)
:param port: The port where Teller resides (defaults to 9292)
:param port: The port where Glance resides (defaults to 9292)
"""
super(TellerClient, self).__init__(**kwargs)
def get_image(self, image_id):
"""
Returns the raw disk image as a mime-encoded blob stream for the
supplied opaque image identifier.
:param image_id: The opaque image identifier
:raises exception.NotFound if image is not found
"""
# TODO(jaypipes): Handle other registries than Parallax...
res = self.do_request("GET", "/images/%s" % image_id)
return res.read()
def delete_image(self, image_id):
"""
Deletes Tellers's information about an image.
"""
self.do_request("DELETE", "/images/%s" % image_id)
return True
class ParallaxClient(BaseClient):
"""A client for the Parallax image metadata service"""
DEFAULT_ADDRESS = 'http://127.0.0.1'
DEFAULT_PORT = 9191
def __init__(self, **kwargs):
"""
Creates a new client to a Parallax service. All args are keyword
arguments.
:param address: The address where Parallax resides (defaults to
http://127.0.0.1)
:param port: The port where Parallax resides (defaults to 9191)
"""
super(ParallaxClient, self).__init__(**kwargs)
super(Client, self).__init__(**kwargs)
def get_images(self):
"""
Returns a list of image id/name mappings from Parallax
Returns a list of image id/name mappings from Registry
"""
res = self.do_request("GET", "/images")
data = json.loads(res.read())['images']
@@ -203,7 +197,7 @@ class ParallaxClient(BaseClient):
def get_images_detailed(self):
"""
Returns a list of detailed image data mappings from Parallax
Returns a list of detailed image data mappings from Registry
"""
res = self.do_request("GET", "/images/detail")
data = json.loads(res.read())['images']
@@ -211,29 +205,53 @@ class ParallaxClient(BaseClient):
def get_image(self, image_id):
"""
Returns a mapping of image metadata from Parallax
Returns a tuple with the image's metadata and the raw disk image as
a mime-encoded blob stream for the supplied opaque image identifier.
:param image_id: The opaque image identifier
:retval Tuple containing (image_meta, image_blob)
:raises exception.NotFound if image is not found
"""
# TODO(jaypipes): Handle other registries than Registry...
res = self.do_request("GET", "/images/%s" % image_id)
image = util.get_image_meta_from_headers(res)
return image, ImageBodyIterator(res)
def get_image_meta(self, image_id):
"""
Returns a mapping of image metadata from Registry
:raises exception.NotFound if image is not in registry
"""
res = self.do_request("GET", "/images/%s" % image_id)
data = json.loads(res.read())['image']
return data
res = self.do_request("HEAD", "/images/%s" % image_id)
def add_image(self, image_metadata):
image = util.get_image_meta_from_headers(res)
return image
def add_image(self, image_meta, image_data=None):
"""
Tells parallax about an image's metadata
Tells Glance about an image's metadata as well
as optionally the image_data itself
"""
if 'image' not in image_metadata.keys():
image_metadata = dict(image=image_metadata)
body = json.dumps(image_metadata)
res = self.do_request("POST", "/images", body)
# Parallax returns a JSONified dict(image=image_info)
if not image_data and 'location' not in image_meta.keys():
raise exception.Invalid("You must either specify a location "
"for the image or supply the actual "
"image data when adding an image to "
"Glance")
body = image_data
headers = util.image_meta_to_http_headers(image_meta)
res = self.do_request("POST", "/images", body, headers)
# Registry returns a JSONified dict(image=image_info)
data = json.loads(res.read())
return data['image']['id']
def update_image(self, image_id, image_metadata):
"""
Updates Parallax's information about an image
Updates Glance's information about an image
"""
if 'image' not in image_metadata.keys():
image_metadata = dict(image=image_metadata)
@@ -243,7 +261,7 @@ class ParallaxClient(BaseClient):
def delete_image(self, image_id):
"""
Deletes Parallax's information about an image
Deletes Glance's information about an image
"""
self.do_request("DELETE", "/images/%s" % image_id)
return True

View File

@@ -17,5 +17,37 @@
# under the License.
"""
Parallax API
Registry API
"""
from glance.registry import client
def get_images_list():
c = client.RegistryClient()
return c.get_images()
def get_images_detail():
c = client.RegistryClient()
return c.get_images_detailed()
def get_image_metadata(image_id):
c = client.RegistryClient()
return c.get_image(image_id)
def add_image_metadata(image_data):
c = client.RegistryClient()
return c.add_image(image_data)
def update_image_metadata(image_id, image_data):
c = client.RegistryClient()
return c.update_image(image_id, image_data)
def delete_image_metadata(image_id):
c = client.RegistryClient()
return c.delete_image(image_id)

105
glance/registry/client.py Normal file
View File

@@ -0,0 +1,105 @@
# 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.
"""
Simple client class to speak with any RESTful service that implements
the Glance Registry API
"""
import httplib
import json
import logging
import urlparse
import socket
import sys
from glance.common import exception
from glance.client import BaseClient
class RegistryClient(BaseClient):
"""A client for the Registry image metadata service"""
DEFAULT_ADDRESS = 'http://127.0.0.1'
DEFAULT_PORT = 9191
def __init__(self, **kwargs):
"""
Creates a new client to a Registry service. All args are keyword
arguments.
:param address: The address where Registry resides (defaults to
http://127.0.0.1)
:param port: The port where Registry resides (defaults to 9191)
"""
super(RegistryClient, self).__init__(**kwargs)
def get_images(self):
"""
Returns a list of image id/name mappings from Registry
"""
res = self.do_request("GET", "/images")
data = json.loads(res.read())['images']
return data
def get_images_detailed(self):
"""
Returns a list of detailed image data mappings from Registry
"""
res = self.do_request("GET", "/images/detail")
data = json.loads(res.read())['images']
return data
def get_image(self, image_id):
"""
Returns a mapping of image metadata from Registry
:raises exception.NotFound if image is not in registry
"""
res = self.do_request("GET", "/images/%s" % image_id)
data = json.loads(res.read())['image']
return data
def add_image(self, image_metadata):
"""
Tells registry about an image's metadata
"""
if 'image' not in image_metadata.keys():
image_metadata = dict(image=image_metadata)
body = json.dumps(image_metadata)
res = self.do_request("POST", "/images", body)
# Registry returns a JSONified dict(image=image_info)
data = json.loads(res.read())
return data['image']
def update_image(self, image_id, image_metadata):
"""
Updates Registry's information about an image
"""
if 'image' not in image_metadata.keys():
image_metadata = dict(image=image_metadata)
body = json.dumps(image_metadata)
self.do_request("PUT", "/images/%s" % image_id, body)
return True
def delete_image(self, image_id):
"""
Deletes Registry's information about an image
"""
self.do_request("DELETE", "/images/%s" % image_id)
return True

View File

@@ -24,7 +24,7 @@ from webob import exc
from glance.common import wsgi
from glance.common import exception
from glance.parallax import db
from glance.registry import db
class ImageController(wsgi.Controller):
@@ -74,7 +74,8 @@ class ImageController(wsgi.Controller):
return dict(image=make_image_dict(image))
def delete(self, req, id):
"""Deletes an existing image with the registry.
"""
Deletes an existing image with the registry.
:param req: Request body. Ignored.
:param id: The opaque internal identifier for the image
@@ -89,7 +90,8 @@ class ImageController(wsgi.Controller):
return exc.HTTPNotFound()
def create(self, req):
"""Registers a new image with the registry.
"""
Registers a new image with the registry.
:param req: Request body. A JSON-ified dict of information about
the image.
@@ -106,8 +108,8 @@ class ImageController(wsgi.Controller):
context = None
try:
new_image = db.image_create(context, image_data)
return dict(image=new_image)
image_data = db.image_create(context, image_data)
return dict(image=make_image_dict(image_data))
except exception.Duplicate:
return exc.HTTPConflict()
except exception.Invalid:
@@ -129,16 +131,16 @@ class ImageController(wsgi.Controller):
context = None
try:
updated_image = db.image_update(context, id, image_data)
return dict(image=updated_image)
return dict(image=make_image_dict(updated_image))
except exception.NotFound:
return exc.HTTPNotFound()
class API(wsgi.Router):
"""WSGI entry point for all Parallax requests."""
"""WSGI entry point for all Registry requests."""
def __init__(self):
# TODO(sirp): should we add back the middleware for parallax?
# TODO(sirp): should we add back the middleware for registry?
mapper = routes.Mapper()
mapper.resource("image", "images", controller=ImageController(),
collection={'detail': 'GET'})
@@ -156,8 +158,6 @@ def make_image_dict(image):
return dict([(a, d[a]) for a in attrs
if a in d.keys()])
files = [_fetch_attrs(f, db.IMAGE_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
@@ -166,6 +166,5 @@ def make_image_dict(image):
image_dict = _fetch_attrs(image, db.IMAGE_ATTRS)
image_dict['files'] = files
image_dict['properties'] = properties
return image_dict

View File

@@ -20,13 +20,11 @@
DB abstraction for Nova and Glance
"""
from glance.parallax.db.api import *
from glance.registry.db.api import *
# attributes common to all models
BASE_MODEL_ATTRS = set(['id', 'created_at', 'updated_at', 'deleted_at',
'deleted'])
IMAGE_FILE_ATTRS = BASE_MODEL_ATTRS | set(['location', 'size'])
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'image_type', 'status',
'is_public'])
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'type', 'status', 'size',
'is_public', 'location'])

View File

@@ -30,7 +30,7 @@ flags.DEFINE_string('db_backend', 'sqlalchemy',
IMPL = utils.LazyPluggable(FLAGS['db_backend'],
sqlalchemy='glance.parallax.db.sqlalchemy.api')
sqlalchemy='glance.registry.db.sqlalchemy.api')
###################

View File

@@ -20,8 +20,7 @@
SQLAlchemy models for glance data
"""
from glance.parallax.db.sqlalchemy import models
from glance.registry.db.sqlalchemy import models
models.register_models()

View File

@@ -24,7 +24,7 @@ from glance.common import db
from glance.common import exception
from glance.common import flags
from glance.common.db.sqlalchemy.session import get_session
from glance.parallax.db.sqlalchemy import models
from glance.registry.db.sqlalchemy import models
from sqlalchemy.orm import exc
#from sqlalchemy.orm import joinedload_all

View File

@@ -141,16 +141,18 @@ class Image(BASE, ModelBase):
id = Column(Integer, primary_key=True)
name = Column(String(255))
image_type = Column(String(30))
type = Column(String(30))
size = Column(Integer)
status = Column(String(30))
is_public = Column(Boolean, default=False)
location = Column(Text)
@validates('image_type')
def validate_image_type(self, key, image_type):
if not image_type in ('machine', 'kernel', 'ramdisk', 'raw'):
@validates('type')
def validate_type(self, key, type):
if not type in ('machine', 'kernel', 'ramdisk', 'raw'):
raise exception.Invalid(
"Invalid image type '%s' for image." % image_type)
return image_type
"Invalid image type '%s' for image." % type)
return type
@validates('status')
def validate_status(self, key, status):
@@ -176,18 +178,6 @@ class Image(BASE, ModelBase):
# assert(val is None)
class ImageFile(BASE, ModelBase):
"""Represents an image file in the datastore"""
__tablename__ = 'image_files'
__prefix__ = 'img-file'
id = Column(Integer, primary_key=True)
image_id = Column(Integer, ForeignKey('images.id'), nullable=False)
image = relationship(Image, backref=backref('files'))
location = Column(String(255))
size = Column(Integer)
class ImageProperty(BASE, ModelBase):
"""Represents an image properties in the datastore"""
__tablename__ = 'image_properties'

290
glance/server.py Normal file
View File

@@ -0,0 +1,290 @@
# 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.
"""
=================
Glance API Server
=================
Configuration Options
---------------------
"""
import json
import logging
import routes
from webob import Response
from webob.exc import (HTTPNotFound,
HTTPConflict,
HTTPBadRequest)
from glance.common import exception
from glance.common import flags
from glance.common import wsgi
from glance.store import (get_from_backend,
delete_from_backend,
get_store_from_location)
from glance import registry
from glance import util
FLAGS = flags.FLAGS
class Controller(wsgi.Controller):
"""
Main WSGI application controller for Glance.
The Glance API is a RESTful web service for image data. The API
is as follows::
GET /images -- Returns a set of brief metadata about images
GET /images/detail -- Returns a set of detailed metadata about
images
HEAD /images/<ID> -- Return metadata about an image with id <ID>
GET /images/<ID> -- Return image data for image with id <ID>
POST /images -- Store image data and return metadata about the
newly-stored image
PUT /images/<ID> -- Update image metadata (not image data, since
image data is immutable once stored)
DELETE /images/<ID> -- Delete the image with id <ID>
"""
def index(self, req):
"""
Returns the following information for all public, available images:
* id -- The opaque image identifier
* name -- The name of the image
* size -- Size of image data in bytes
* type -- One of 'kernel', 'ramdisk', 'raw', or 'machine'
:param request: The WSGI/Webob Request object
:retval The response body is a mapping of the following form::
{'images': [
{'id': <ID>,
'name': <NAME>,
'size': <SIZE>,
'type': <TYPE>}, ...
]}
"""
images = registry.get_images_list()
return dict(images=images)
def detail(self, req):
"""
Returns detailed information for all public, available images
:param request: The WSGI/Webob Request object
:retval The response body is a mapping of the following form::
{'images': [
{'id': <ID>,
'name': <NAME>,
'size': <SIZE>,
'type': <TYPE>,
'store': <STORE>,
'status': <STATUS>,
'created_at': <TIMESTAMP>,
'updated_at': <TIMESTAMP>,
'deleted_at': <TIMESTAMP>|<NONE>,
'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ...
]}
"""
images = registry.get_images_detail()
return dict(images=images)
def meta(self, req, id):
"""
Returns metadata about an image in the HTTP headers of the
response object
:param request: The WSGI/Webob Request object
:param id: The opaque image identifier
:raises HTTPNotFound if image metadata is not available to user
"""
image = self.get_image_meta_or_404(req, id)
res = Response(request=req)
util.inject_image_meta_into_headers(res, image)
return req.get_response(res)
def show(self, req, id):
"""
Returns an iterator as a Response object that
can be used to retrieve an image's data. The
content-type of the response is the content-type
of the image, or application/octet-stream if none
is known or found.
:param request: The WSGI/Webob Request object
:param id: The opaque image identifier
:raises HTTPNotFound if image is not available to user
"""
image = self.get_image_meta_or_404(req, id)
def image_iterator():
chunks = get_from_backend(image['location'],
expected_size=image['size'])
for chunk in chunks:
yield chunk
res = Response(app_iter=image_iterator(),
content_type="text/plain")
util.inject_image_meta_into_headers(res, image)
return req.get_response(res)
def create(self, req):
"""
Adds a new image to Glance. The body of the request may be a
mime-encoded image data. Metadata about the image is sent via
HTTP Headers.
If the metadata about the image does not include a location
to find the image, or if the image location is not valid,
the request body *must* be encoded as application/octet-stream
and be the image data itself, otherwise an HTTPBadRequest is
returned.
Upon a successful save of the image data and metadata, a response
containing metadata about the image is returned, including its
opaque identifier.
:param request: The WSGI/Webob Request object
:see The `id_type` configuration option (default: uuid) determines
the type of identifier that Glance generates for an image
:raises HTTPBadRequest if no x-image-meta-location is missing
and the request body is not application/octet-stream
image data.
"""
# Verify the request and headers before we generate a new id
image_in_body = False
image_store = None
header_keys = [k.lower() for k in req.headers.keys()]
if 'x-image-meta-location' not in header_keys:
if ('content-type' not in header_keys or
req.headers['content-type'] != 'application/octet-stream'):
raise HTTPBadRequest("Image location was not specified in "
"headers and the request body was not "
"mime-encoded as application/"
"octet-stream.", request=req)
else:
if 'x-image-meta-store' in headers_keys:
image_store = req.headers['x-image-meta-store']
image_status = 'pending' # set to available when stored...
image_in_body = True
else:
image_location = req.headers['x-image-meta-location']
image_store = get_store_from_location(image_location)
image_status = 'available'
image_meta = util.get_image_meta_from_headers(req)
image_meta['status'] = image_status
image_meta['store'] = image_store
try:
image_meta = registry.add_image_metadata(image_meta)
if image_in_body:
#store = stores.get_store()
#store.add_image(req.body)
image_meta['status'] = 'available'
registries.update_image(image_meta)
return dict(image=image_meta)
except exception.Duplicate:
return HTTPConflict()
except exception.Invalid:
return HTTPBadRequest()
def update(self, req, id):
"""
Updates an existing image with the registry.
:param request: The WSGI/Webob Request object
:param id: The opaque image identifier
:retval Returns the updated image information as a mapping,
"""
image = self.get_image_meta_or_404(req, id)
image_data = json.loads(req.body)['image']
updated_image = registry.update_image_metadata(id, image_data)
return dict(image=updated_image)
def delete(self, req, id):
"""
Deletes the image and all its chunks from the Glance
:param request: The WSGI/Webob Request object
:param id: The opaque image identifier
:raises HttpBadRequest if image registry is invalid
:raises HttpNotFound if image or any chunk is not available
:raises HttpNotAuthorized if image or any chunk is not
deleteable by the requesting user
"""
image = self.get_image_meta_or_404(req, id)
delete_from_backend(image['location'])
registry.delete_image_metadata(id)
def get_image_meta_or_404(self, request, id):
"""
Grabs the image metadata for an image with a supplied
identifier or raises an HTTPNotFound (404) response
:param request: The WSGI/Webob Request object
:param id: The opaque image identifier
:raises HTTPNotFound if image does not exist
"""
try:
return registry.get_image_metadata(id)
except exception.NotFound:
raise HTTPNotFound(body='Image not found',
request=request,
content_type='text/plain')
class API(wsgi.Router):
"""WSGI entry point for all Glance API requests."""
def __init__(self):
mapper = routes.Mapper()
mapper.resource("image", "images", controller=Controller(),
collection={'detail': 'GET'})
mapper.connect("/", controller=Controller(), action="index")
mapper.connect("/images/{id}", controller=Controller(), action="meta",
conditions=dict(method=["HEAD"]))
super(API, self).__init__(mapper)

103
glance/store/__init__.py Normal file
View File

@@ -0,0 +1,103 @@
# 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 os
import urlparse
from glance.common import exception
# TODO(sirp): should this be moved out to common/utils.py ?
def _file_iter(f, size):
"""
Return an iterator for a file-like object
"""
chunk = f.read(size)
while chunk:
yield chunk
chunk = f.read(size)
class BackendException(Exception):
pass
class UnsupportedBackend(BackendException):
pass
class Backend(object):
CHUNKSIZE = 4096
def get_backend_class(backend):
"""
Returns the backend class as designated in the
backend name
:param backend: Name of backend to create
"""
# NOTE(sirp): avoiding circular import
from glance.store.http import HTTPBackend
from glance.store.swift import SwiftBackend
from glance.store.filesystem import FilesystemBackend
BACKENDS = {
"file": FilesystemBackend,
"http": HTTPBackend,
"https": HTTPBackend,
"swift": SwiftBackend
}
try:
return BACKENDS[backend]
except KeyError:
raise UnsupportedBackend("No backend found for '%s'" % scheme)
def get_from_backend(uri, **kwargs):
"""Yields chunks of data from backend specified by uri"""
parsed_uri = urlparse.urlparse(uri)
scheme = parsed_uri.scheme
backend_class = get_backend_class(scheme)
return backend_class.get(parsed_uri, **kwargs)
def delete_from_backend(uri, **kwargs):
"""Removes chunks of data from backend specified by uri"""
parsed_uri = urlparse.urlparse(uri)
scheme = parsed_uri.scheme
backend_class = get_backend_class(scheme)
return backend_class.delete(parsed_uri, **kwargs)
def get_store_from_location(location):
"""
Given a location (assumed to be a URL), attempt to determine
the store from the location. We use here a simple guess that
the scheme of the parsed URL is the store...
:param location: Location to check for the store
"""
loc_pieces = urlparse.urlparse(location)
return loc_pieces.scheme

View File

@@ -84,8 +84,8 @@ def get_backend_class(backend):
:param backend: Name of backend to create
"""
# NOTE(sirp): avoiding circular import
from glance.teller.backends.http import HTTPBackend
from glance.teller.backends.swift import SwiftBackend
from glance.store.backends.http import HTTPBackend
from glance.store.backends.swift import SwiftBackend
BACKENDS = {
"file": FilesystemBackend,

View File

@@ -0,0 +1,97 @@
# 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.
"""
A simple filesystem-backed store
"""
import os
import urlparse
from glance.common import exception
from glance.common import flags
import glance.store
FLAGS = flags.FLAGS
class ChunkedFile(object):
"""
We send this back to the Glance API server as
something that can iterate over a large file
"""
CHUNKSIZE = 65536
def __init__(self, filepath):
self.filepath = filepath
self.fp = open(self.filepath, 'rb')
def __iter__(self):
"""Return an iterator over the image file"""
try:
while True:
chunk = self.fp.read(ChunkedFile.CHUNKSIZE)
if chunk:
yield chunk
else:
break
finally:
self.close()
def close(self):
"""Close the internal file pointer"""
if self.fp:
self.fp.close()
self.fp = None
class FilesystemBackend(glance.store.Backend):
@classmethod
def get(cls, parsed_uri, opener=lambda p: open(p, "rb"), expected_size=None):
""" Filesystem-based backend
file:///path/to/file.tar.gz.0
"""
filepath = parsed_uri.path
if not os.path.exists(filepath):
raise exception.NotFound("Image file %s not found" % filepath)
else:
return ChunkedFile(filepath)
@classmethod
def delete(cls, parsed_uri):
"""
Removes a file from the filesystem backend.
:param parsed_uri: Parsed pieces of URI in form of::
file:///path/to/filename.ext
:raises NotFound if file does not exist
:raises NotAuthorized if cannot delete because of permissions
"""
fn = parsed_uri.path
if os.path.exists(fn):
try:
os.unlink(fn)
except OSError:
raise exception.NotAuthorized("You cannot delete file %s" % fn)
else:
raise exception.NotFound("Image file %s does not exist" % fn)

View File

@@ -16,9 +16,10 @@
# under the License.
import httplib
from glance.teller import backends
class HTTPBackend(backends.Backend):
import glance.store
class HTTPBackend(glance.store.Backend):
""" An implementation of the HTTP Backend Adapter """
@classmethod
@@ -34,12 +35,12 @@ class HTTPBackend(backends.Backend):
elif parsed_uri.scheme == "https":
conn_class = httplib.HTTPSConnection
else:
raise BackendException("scheme '%s' not supported for HTTPBackend")
raise glance.store.BackendException("scheme '%s' not supported for HTTPBackend")
conn = conn_class(parsed_uri.netloc)
conn.request("GET", parsed_uri.path, "", {})
try:
return backends._file_iter(conn.getresponse(), cls.CHUNKSIZE)
return glance.store._file_iter(conn.getresponse(), cls.CHUNKSIZE)
finally:
conn.close()

View File

@@ -15,15 +15,17 @@
# License for the specific language governing permissions and limitations
# under the License.
from glance.teller.backends import Backend, BackendException
import glance.store
class SwiftBackend(Backend):
class SwiftBackend(glance.store.Backend):
"""
An implementation of the swift backend adapter.
"""
EXAMPLE_URL = "swift://user:password@auth_url/container/file.gz.0"
CHUNKSIZE = 65536
@classmethod
def get(cls, parsed_uri, expected_size, conn_class=None):
"""
@@ -48,7 +50,7 @@ class SwiftBackend(Backend):
obj_size = int(resp_headers['content-length'])
if obj_size != expected_size:
raise BackendException("Expected %s byte file, Swift has %s bytes"
raise glance.store.BackendException("Expected %s byte file, Swift has %s bytes"
% (expected_size, obj_size))
return resp_body
@@ -100,7 +102,7 @@ class SwiftBackend(Backend):
obj = path_parts.pop()
container = path_parts.pop()
except (ValueError, IndexError):
raise BackendException(
raise glance.store.BackendException(
"Expected four values to unpack in: swift:%s. "
"Should have received something like: %s."
% (parsed_uri.path, cls.EXAMPLE_URL))
@@ -120,7 +122,7 @@ def get_connection_class(conn_class):
# up importing ourselves.
#
# NOTE(jaypipes): This can be resolved by putting this code in
# /glance/teller/backends/swift/__init__.py
# /glance/store/swift/__init__.py
#
# see http://docs.python.org/library/functions.html#__import__
PERFORM_ABSOLUTE_IMPORTS = 0

View File

@@ -1,137 +0,0 @@
# 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.
"""
Teller Image controller
"""
import logging
import routes
from webob import exc, Response
from glance.common import wsgi
from glance.common import exception
from glance.parallax import db
from glance.teller import backends
from glance.teller import registries
class ImageController(wsgi.Controller):
"""Image Controller"""
def show(self, req, id):
"""
Query the parallax service for the image registry for the passed in
req['uri']. If it exists, we connect to the appropriate backend as
determined by the URI scheme and yield chunks of data back to the
client.
Optionally, we can pass in 'registry' which will use a given
RegistryAdapter for the request. This is useful for testing.
"""
registry, image = self.get_registry_and_image(req, id)
def image_iterator():
for file in image['files']:
chunks = backends.get_from_backend(file['location'],
expected_size=file['size'])
for chunk in chunks:
yield chunk
res = Response(app_iter=image_iterator(),
content_type="text/plain")
return req.get_response(res)
def index(self, req):
"""Index is not currently supported """
raise exc.HTTPNotImplemented()
def delete(self, req, id):
"""
Deletes the image and all its chunks from the Teller service.
Note that this DOES NOT delete the image from the image
registry (Parallax or other registry service). The caller
should delete the metadata from the registry if necessary.
:param request: The WSGI/Webob Request object
:param id: The opaque image identifier
:raises HttpBadRequest if image registry is invalid
:raises HttpNotFound if image or any chunk is not available
:raises HttpNotAuthorized if image or any chunk is not
deleteable by the requesting user
"""
registry, image = self.get_registry_and_image(req, id)
try:
for file in image['files']:
backends.delete_from_backend(file['location'])
except exception.NotAuthorized:
raise exc.HTTPNotAuthorized(body='You are not authorized to '
'delete image chunk %s' % file,
request=req,
content_type='text/plain')
except exception.NotFound:
raise exc.HTTPNotFound(body='Image chunk %s not found' %
file, request=req,
content_type='text/plain')
def create(self, req):
"""Create is not currently supported """
raise exc.HTTPNotImplemented()
def update(self, req, id):
"""Update is not currently supported """
raise exc.HTTPNotImplemented()
def get_registry_and_image(self, req, id):
"""
Returns the registry used and the image metadata for a
supplied image ID and request object.
:param req: The WSGI/Webob Request object
:param id: The opaque image identifier
:raises HttpBadRequest if image registry is invalid
:raises HttpNotFound if image is not available
:retval tuple with (registry, image)
"""
registry = req.str_GET.get('registry', 'parallax')
try:
image = registries.lookup_by_registry(registry, id)
return registry, image
except registries.UnknownImageRegistry:
logging.debug("Could not find image registry: %s.", registry)
raise exc.HTTPBadRequest(body="Unknown registry '%s'" % registry,
request=req,
content_type="text/plain")
except exception.NotFound:
raise exc.HTTPNotFound(body='Image not found', request=req,
content_type='text/plain')
class API(wsgi.Router):
"""WSGI entry point for all Teller requests."""
def __init__(self):
mapper = routes.Mapper()
mapper.resource("image", "images", controller=ImageController())
super(API, self).__init__(mapper)

78
glance/util.py Normal file
View File

@@ -0,0 +1,78 @@
# 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.
"""
A few utility routines used throughout Glance
"""
def image_meta_to_http_headers(image_meta):
"""
Returns a set of image metadata into a dict
of HTTP headers that can be fed to either a Webob
Request object or an httplib.HTTP(S)Connection object
:param image_meta: Mapping of image metadata
"""
headers = {}
for k, v in image_meta.iteritems():
if k == 'properties':
for pk, pv in v.iteritems():
headers["x-image-meta-property-%s"
% pk.lower()] = pv
headers["x-image-meta-%s" % k.lower()] = v
return headers
def inject_image_meta_into_headers(response, image_meta):
"""
Given a response and mapping of image metadata, injects
the Response with a set of HTTP headers for the image
metadata. Each main image metadata field is injected
as a HTTP header with key 'x-image-meta-<FIELD>' except
for the properties field, which is further broken out
into a set of 'x-image-meta-property-<KEY>' headers
:param response: The Webob Response object
:param image_meta: Mapping of image metadata
"""
headers = image_meta_to_http_headers(image_meta)
for k, v in headers.iteritems():
response.headers.add(k, v)
def get_image_meta_from_headers(response):
"""
Processes HTTP headers from a supplied response that
match the x-image-meta and x-image-meta-property and
returns a mapping of image metadata and properties
:param response: Response to process
"""
result = {}
properties = {}
for key, value in response.headers.iteritems():
key = str(key.lower())
if key.startswith('x-image-meta-property-'):
properties[key[len('x-image-meta-property-'):]] = value
if key.startswith('x-image-meta-'):
field_name = key[len('x-image-meta-'):].replace('-', '_')
result[field_name] = value
result['properties'] = properties
return result

View File

@@ -23,20 +23,21 @@ import os
import shutil
import StringIO
import sys
import gzip
import stubout
import webob
from glance.common import exception
from glance.parallax import controllers as parallax_controllers
from glance.teller import controllers as teller_controllers
import glance.teller.backends
import glance.teller.backends.swift
import glance.parallax.db.sqlalchemy.api
from glance.registry import controllers as registry_controllers
from glance import server
import glance.store
import glance.store.filesystem
import glance.store.http
import glance.store.swift
import glance.registry.db.sqlalchemy.api
FAKE_FILESYSTEM_ROOTDIR = os.path.join('/tmp', 'glance-tests')
FAKE_FILESYSTEM_ROOTDIR = os.path.join('//tmp', 'glance-tests')
def stub_out_http_backend(stubs):
@@ -80,79 +81,32 @@ def clean_out_fake_filesystem_backend():
shutil.rmtree(FAKE_FILESYSTEM_ROOTDIR, ignore_errors=True)
def stub_out_filesystem_backend(stubs):
def stub_out_filesystem_backend():
"""
Stubs out the Filesystem Teller service to return fake
gzipped image data from files.
Stubs out the Filesystem Glance service to return fake
pped image data from files.
We establish a few fake images in a directory under /tmp/glance-tests
We establish a few fake images in a directory under //tmp/glance-tests
and ensure that this directory contains the following files:
/acct/2.gz.0 <-- zipped tarfile containing "chunk0"
/acct/2.gz.1 <-- zipped tarfile containing "chunk42"
//tmp/glance-tests/2 <-- file containing "chunk00000remainder"
The stubbed service yields the data in the above files.
:param stubs: Set of stubout stubs
"""
class FakeFilesystemBackend(object):
CHUNKSIZE = 100
@classmethod
def get(cls, parsed_uri, expected_size, opener=None):
filepath = os.path.join('/',
parsed_uri.netloc.lstrip('/'),
parsed_uri.path.strip(os.path.sep))
if os.path.exists(filepath):
f = gzip.open(filepath, 'rb')
data = f.read()
f.close()
return data
else:
raise exception.NotFound("File %s does not exist" % filepath)
@classmethod
def delete(self, parsed_uri):
filepath = os.path.join('/',
parsed_uri.netloc.lstrip('/'),
parsed_uri.path.strip(os.path.sep))
if os.path.exists(filepath):
try:
os.unlink(filepath)
except OSError:
raise exception.NotAuthorized("You cannot delete file %s" %
filepath)
else:
raise exception.NotFound("File %s does not exist" % filepath)
# Establish a clean faked filesystem with dummy images
if os.path.exists(FAKE_FILESYSTEM_ROOTDIR):
shutil.rmtree(FAKE_FILESYSTEM_ROOTDIR, ignore_errors=True)
os.mkdir(FAKE_FILESYSTEM_ROOTDIR)
os.mkdir(os.path.join(FAKE_FILESYSTEM_ROOTDIR, 'acct'))
f = gzip.open(os.path.join(FAKE_FILESYSTEM_ROOTDIR, 'acct', '2.gz.0'),
"wb")
f.write("chunk0")
f = open(os.path.join(FAKE_FILESYSTEM_ROOTDIR, '2'), "wb")
f.write("chunk00000remainder")
f.close()
f = gzip.open(os.path.join(FAKE_FILESYSTEM_ROOTDIR, 'acct', '2.gz.1'),
"wb")
f.write("chunk42")
f.close()
fake_filesystem_backend = FakeFilesystemBackend()
stubs.Set(glance.teller.backends.FilesystemBackend, 'get',
fake_filesystem_backend.get)
stubs.Set(glance.teller.backends.FilesystemBackend, 'delete',
fake_filesystem_backend.delete)
def stub_out_swift_backend(stubs):
"""Stubs out the Swift Teller backend with fake data
"""Stubs out the Swift Glance backend with fake data
and calls.
The stubbed swift backend provides back an iterator over
@@ -173,7 +127,7 @@ def stub_out_swift_backend(stubs):
@classmethod
def get(cls, parsed_uri, expected_size, conn_class=None):
SwiftBackend = glance.teller.backends.swift.SwiftBackend
SwiftBackend = glance.store.swift.SwiftBackend
# raise BackendException if URI is bad.
(user, key, authurl, container, obj) = \
@@ -186,14 +140,14 @@ def stub_out_swift_backend(stubs):
return chunk_it()
fake_swift_backend = FakeSwiftBackend()
stubs.Set(glance.teller.backends.swift.SwiftBackend, 'get',
stubs.Set(glance.store.swift.SwiftBackend, 'get',
fake_swift_backend.get)
def stub_out_parallax(stubs):
"""Stubs out the Parallax registry with fake data returns.
def stub_out_registry(stubs):
"""Stubs out the Registry registry with fake data returns.
The stubbed Parallax always returns the following fixture::
The stubbed Registry always returns the following fixture::
{'files': [
{'location': 'file:///chunk0', 'size': 12345},
@@ -203,7 +157,7 @@ def stub_out_parallax(stubs):
:param stubs: Set of stubout stubs
"""
class FakeParallax(object):
class FakeRegistry(object):
DATA = \
{'files': [
@@ -215,19 +169,19 @@ def stub_out_parallax(stubs):
def lookup(cls, _parsed_uri):
return cls.DATA
fake_parallax_registry = FakeParallax()
stubs.Set(glance.teller.registries.Parallax, 'lookup',
fake_parallax_registry.lookup)
fake_registry_registry = FakeRegistry()
stubs.Set(glance.store.registries.Registry, 'lookup',
fake_registry_registry.lookup)
def stub_out_parallax_and_teller_server(stubs):
def stub_out_registry_and_store_server(stubs):
"""
Mocks calls to 127.0.0.1 on 9191 and 9292 for testing so
that a real Teller server does not need to be up and
that a real Glance server does not need to be up and
running
"""
class FakeParallaxConnection(object):
class FakeRegistryConnection(object):
def __init__(self, *args, **kwargs):
pass
@@ -245,7 +199,7 @@ def stub_out_parallax_and_teller_server(stubs):
self.req.body = body
def getresponse(self):
res = self.req.get_response(parallax_controllers.API())
res = self.req.get_response(registry_controllers.API())
# httplib.Response has a read() method...fake it out
def fake_reader():
@@ -254,7 +208,7 @@ def stub_out_parallax_and_teller_server(stubs):
setattr(res, 'read', fake_reader)
return res
class FakeTellerConnection(object):
class FakeGlanceConnection(object):
def __init__(self, *args, **kwargs):
pass
@@ -264,6 +218,9 @@ def stub_out_parallax_and_teller_server(stubs):
def close(self):
return True
def putheader(self, k, v):
self.req.headers[k] = v
def request(self, method, url, body=None):
self.req = webob.Request.blank("/" + url.lstrip("/"))
@@ -272,7 +229,7 @@ def stub_out_parallax_and_teller_server(stubs):
self.req.body = body
def getresponse(self):
res = self.req.get_response(teller_controllers.API())
res = self.req.get_response(server.API())
# httplib.Response has a read() method...fake it out
def fake_reader():
@@ -290,10 +247,10 @@ def stub_out_parallax_and_teller_server(stubs):
if (client.port == DEFAULT_TELLER_PORT and
client.netloc == '127.0.0.1'):
return FakeTellerConnection
return FakeGlanceConnection
elif (client.port == DEFAULT_PARALLAX_PORT and
client.netloc == '127.0.0.1'):
return FakeParallaxConnection
return FakeRegistryConnection
else:
try:
connection_type = {'http': httplib.HTTPConnection,
@@ -304,13 +261,18 @@ def stub_out_parallax_and_teller_server(stubs):
raise UnsupportedProtocolError("Unsupported protocol %s. Unable "
" to connect to server."
% self.protocol)
def fake_image_iter(self):
for i in self.response.app_iter:
yield i
stubs.Set(glance.client.BaseClient, 'get_connection_type',
fake_get_connection_type)
stubs.Set(glance.client.ImageBodyIterator, '__iter__',
fake_image_iter)
def stub_out_parallax_db_image_api(stubs):
"""Stubs out the database set/fetch API calls for Parallax
def stub_out_registry_db_image_api(stubs):
"""Stubs out the database set/fetch API calls for Registry
so the calls are routed to an in-memory dict. This helps us
avoid having to manually clear or flush the SQLite database.
@@ -325,32 +287,26 @@ def stub_out_parallax_db_image_api(stubs):
{'id': 1,
'name': 'fake image #1',
'status': 'available',
'image_type': 'kernel',
'type': 'kernel',
'is_public': False,
'created_at': datetime.datetime.utcnow(),
'updated_at': datetime.datetime.utcnow(),
'deleted_at': None,
'deleted': False,
'files': [
{"location": "swift://user:passwd@acct/container/obj.tar.gz.0",
"size": 6},
{"location": "swift://user:passwd@acct/container/obj.tar.gz.1",
"size": 7}],
'size': 13,
'location': "swift://user:passwd@acct/container/obj.tar.0",
'properties': []},
{'id': 2,
'name': 'fake image #2',
'status': 'available',
'image_type': 'kernel',
'type': 'kernel',
'is_public': True,
'created_at': datetime.datetime.utcnow(),
'updated_at': datetime.datetime.utcnow(),
'deleted_at': None,
'deleted': False,
'files': [
{"location": "file://tmp/glance-tests/acct/2.gz.0",
"size": 6},
{"location": "file://tmp/glance-tests/acct/2.gz.1",
"size": 7}],
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': []}]
VALID_STATUSES = ('available', 'disabled', 'pending')
@@ -375,17 +331,25 @@ def stub_out_parallax_db_image_api(stubs):
values['status'])
values['deleted'] = False
values['files'] = values.get('files', [])
values['properties'] = values.get('properties', [])
values['properties'] = values.get('properties', {})
values['created_at'] = datetime.datetime.utcnow()
values['updated_at'] = datetime.datetime.utcnow()
values['deleted_at'] = None
for p in values['properties']:
p['deleted'] = False
p['created_at'] = datetime.datetime.utcnow()
p['updated_at'] = datetime.datetime.utcnow()
p['deleted_at'] = None
props = []
if 'properties' in values.keys():
for k,v in values['properties'].iteritems():
p = {}
p['key'] = k
p['value'] = v
p['deleted'] = False
p['created_at'] = datetime.datetime.utcnow()
p['updated_at'] = datetime.datetime.utcnow()
p['deleted_at'] = None
props.append(p)
values['properties'] = props
self.next_id += 1
self.images.append(values)
@@ -416,13 +380,13 @@ def stub_out_parallax_db_image_api(stubs):
if f['is_public'] == public]
fake_datastore = FakeDatastore()
stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_create',
stubs.Set(glance.registry.db.sqlalchemy.api, 'image_create',
fake_datastore.image_create)
stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_update',
stubs.Set(glance.registry.db.sqlalchemy.api, 'image_update',
fake_datastore.image_update)
stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_destroy',
stubs.Set(glance.registry.db.sqlalchemy.api, 'image_destroy',
fake_datastore.image_destroy)
stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_get',
stubs.Set(glance.registry.db.sqlalchemy.api, 'image_get',
fake_datastore.image_get)
stubs.Set(glance.parallax.db.sqlalchemy.api, 'image_get_all_public',
stubs.Set(glance.registry.db.sqlalchemy.api, 'image_get_all_public',
fake_datastore.image_get_all_public)

View File

@@ -15,7 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from glance.parallax import db
from glance.registry import db
def make_swift_image():

269
tests/unit/test_api.py Normal file
View File

@@ -0,0 +1,269 @@
# 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 unittest
import stubout
import webob
from glance import server
from glance.registry import controllers
from tests import stubs
class TestImageController(unittest.TestCase):
def setUp(self):
"""Establish a clean test environment"""
self.stubs = stubout.StubOutForTesting()
stubs.stub_out_registry_and_store_server(self.stubs)
stubs.stub_out_registry_db_image_api(self.stubs)
stubs.stub_out_filesystem_backend()
def tearDown(self):
"""Clear the test environment"""
stubs.clean_out_fake_filesystem_backend()
self.stubs.UnsetAll()
def test_get_root(self):
"""Tests that the root registry API returns "index",
which is a list of public images
"""
fixture = {'id': 2,
'name': 'fake image #2'}
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_index(self):
"""Tests that the /images registry API returns list of
public images
"""
fixture = {'id': 2,
'name': 'fake image #2'}
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_get_details(self):
"""Tests that the /images/detail registry API returns
a mapping containing a list of detailed image information
"""
fixture = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'type': 'kernel',
'status': 'available'
}
req = webob.Request.blank('/images/detail')
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 registry API creates the image"""
fixture = {'name': 'fake public image',
'is_public': True,
'type': 'kernel'
}
req = webob.Request.blank('/images')
req.method = 'POST'
req.body = json.dumps(dict(image=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['image'][k])
# Test ID auto-assigned properly
self.assertEquals(3, res_dict['image']['id'])
# Test status was updated properly
self.assertEquals('available', res_dict['image']['status'])
def test_create_image_with_bad_status(self):
"""Tests proper exception is raised if a bad status is set"""
fixture = {'id': 3,
'name': 'fake public image',
'is_public': True,
'type': 'kernel',
'status': 'bad status'
}
req = webob.Request.blank('/images')
req.method = 'POST'
req.body = json.dumps(dict(image=fixture))
# TODO(jaypipes): Port Nova's Fault infrastructure
# over to Glance to support exception catching into
# standard HTTP errors.
res = req.get_response(controllers.API())
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
def test_update_image(self):
"""Tests that the /images PUT registry API updates the image"""
fixture = {'name': 'fake public image #2',
'type': 'ramdisk'
}
req = webob.Request.blank('/images/2')
req.method = 'PUT'
req.body = json.dumps(dict(image=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['image'][k])
def test_update_image_not_existing(self):
"""Tests proper exception is raised if attempt to update non-existing
image"""
fixture = {'id': 3,
'name': 'fake public image',
'is_public': True,
'type': 'kernel',
'status': 'bad status'
}
req = webob.Request.blank('/images/3')
req.method = 'PUT'
req.body = json.dumps(dict(image=fixture))
# TODO(jaypipes): Port Nova's Fault infrastructure
# over to Glance to support exception catching into
# standard HTTP errors.
res = req.get_response(controllers.API())
self.assertEquals(res.status_int,
webob.exc.HTTPNotFound.code)
def test_delete_image(self):
"""Tests that the /images DELETE registry API deletes the image"""
# Grab the original number of images
req = webob.Request.blank('/images')
res = req.get_response(controllers.API())
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
orig_num_images = len(res_dict['images'])
# Delete image #2
req = webob.Request.blank('/images/2')
req.method = 'DELETE'
res = req.get_response(controllers.API())
self.assertEquals(res.status_int, 200)
# Verify one less image
req = webob.Request.blank('/images')
res = req.get_response(controllers.API())
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
new_num_images = len(res_dict['images'])
self.assertEquals(new_num_images, orig_num_images - 1)
def test_delete_image_not_existing(self):
"""Tests proper exception is raised if attempt to delete non-existing
image"""
req = webob.Request.blank('/images/3')
req.method = 'DELETE'
# TODO(jaypipes): Port Nova's Fault infrastructure
# over to Glance to support exception catching into
# standard HTTP errors.
res = req.get_response(controllers.API())
self.assertEquals(res.status_int,
webob.exc.HTTPNotFound.code)
def test_image_meta(self):
expected_headers = {'x-image-meta-id': 2,
'x-image-meta-name': 'fake image #2'}
req = webob.Request.blank("/images/2")
req.method = 'HEAD'
res = req.get_response(server.API())
self.assertEquals(res.status_int, 200)
for key, value in expected_headers.iteritems():
self.assertEquals(value, res.headers[key])
def test_show_image_basic(self):
req = webob.Request.blank("/images/2")
res = req.get_response(server.API())
self.assertEqual('chunk00000remainder', res.body)
def test_show_non_exists_image(self):
req = webob.Request.blank("/images/42")
res = req.get_response(server.API())
self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code)
def test_delete_image(self):
req = webob.Request.blank("/images/2")
req.method = 'DELETE'
res = req.get_response(server.API())
self.assertEquals(res.status_int, 200)
req = webob.Request.blank("/images/2")
req.method = 'GET'
res = req.get_response(server.API())
self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code, res.body)
def test_delete_non_exists_image(self):
req = webob.Request.blank("/images/42")
req.method = 'DELETE'
res = req.get_response(server.API())
self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code)

View File

@@ -17,11 +17,13 @@
import json
import stubout
import StringIO
import unittest
import webob
from glance import client
from glance.registry import client as rclient
from glance.common import exception
from tests import stubs
@@ -32,32 +34,32 @@ class TestBadClients(unittest.TestCase):
def test_bad_protocol(self):
"""Test unsupported protocol raised"""
c = client.ParallaxClient(address="hdsa://127.012..1./")
c = client.Client(address="hdsa://127.012..1./")
self.assertRaises(client.UnsupportedProtocolError,
c.get_image,
1)
def test_bad_address(self):
"""Test unsupported protocol raised"""
c = client.ParallaxClient(address="http://127.999.1.1/")
c = client.Client(address="http://127.999.1.1/")
self.assertRaises(client.ClientConnectionError,
c.get_image,
1)
class TestParallaxClient(unittest.TestCase):
class TestRegistryClient(unittest.TestCase):
"""
Test proper actions made for both valid and invalid requests
against a Parallax service
against a Registry service
"""
def setUp(self):
"""Establish a clean test environment"""
self.stubs = stubout.StubOutForTesting()
stubs.stub_out_parallax_db_image_api(self.stubs)
stubs.stub_out_parallax_and_teller_server(self.stubs)
self.client = client.ParallaxClient()
stubs.stub_out_registry_db_image_api(self.stubs)
stubs.stub_out_registry_and_store_server(self.stubs)
self.client = rclient.RegistryClient()
def tearDown(self):
"""Clear the test environment"""
@@ -78,25 +80,19 @@ class TestParallaxClient(unittest.TestCase):
fixture = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'image_type': 'kernel',
'type': 'kernel',
'status': 'available',
'files': [
{"location": "file://tmp/glance-tests/acct/2.gz.0",
"size": 6},
{"location": "file://tmp/glance-tests/acct/2.gz.1",
"size": 7}],
'properties': []}
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {}}
expected = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'image_type': 'kernel',
'type': 'kernel',
'status': 'available',
'files': [
{"location": "file://tmp/glance-tests/acct/2.gz.0",
"size": 6},
{"location": "file://tmp/glance-tests/acct/2.gz.1",
"size": 7}],
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {}}
images = self.client.get_images_detailed()
@@ -110,25 +106,19 @@ class TestParallaxClient(unittest.TestCase):
fixture = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'image_type': 'kernel',
'type': 'kernel',
'status': 'available',
'files': [
{"location": "file://tmp/glance-tests/acct/2.gz.0",
"size": 6},
{"location": "file://tmp/glance-tests/acct/2.gz.1",
"size": 7}],
'properties': []}
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {}}
expected = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'image_type': 'kernel',
'type': 'kernel',
'status': 'available',
'files': [
{"location": "file://tmp/glance-tests/acct/2.gz.0",
"size": 6},
{"location": "file://tmp/glance-tests/acct/2.gz.1",
"size": 7}],
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {}}
data = self.client.get_image(2)
@@ -147,13 +137,15 @@ class TestParallaxClient(unittest.TestCase):
"""Tests that we can add image metadata and returns the new id"""
fixture = {'name': 'fake public image',
'is_public': True,
'image_type': 'kernel'
'type': 'kernel',
'size': 19,
'location': "file:///tmp/glance-tests/acct/3.gz.0",
}
new_id = self.client.add_image(fixture)
new_image = self.client.add_image(fixture)
# Test ID auto-assigned properly
self.assertEquals(3, new_id)
self.assertEquals(3, new_image['id'])
# Test all other attributes set
data = self.client.get_image(3)
@@ -169,38 +161,40 @@ class TestParallaxClient(unittest.TestCase):
"""Tests that we can add image metadata with properties"""
fixture = {'name': 'fake public image',
'is_public': True,
'image_type': 'kernel',
'properties': [{'key':'disco',
'value': 'baby'}]
'type': 'kernel',
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {'distro': 'Ubuntu 10.04 LTS'}
}
expected = {'name': 'fake public image',
'is_public': True,
'image_type': 'kernel',
'properties': {'disco': 'baby'}
'type': 'kernel',
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {'distro': 'Ubuntu 10.04 LTS'}
}
new_id = self.client.add_image(fixture)
new_image = self.client.add_image(fixture)
# Test ID auto-assigned properly
self.assertEquals(3, new_id)
# Test all other attributes set
data = self.client.get_image(3)
self.assertEquals(3, new_image['id'])
for k,v in expected.iteritems():
self.assertEquals(v, data[k])
self.assertEquals(v, new_image[k])
# Test status was updated properly
self.assertTrue('status' in data.keys())
self.assertEquals('available', data['status'])
self.assertTrue('status' in new_image.keys())
self.assertEquals('available', new_image['status'])
def test_add_image_already_exists(self):
"""Tests proper exception is raised if image with ID already exists"""
fixture = {'id': 2,
'name': 'fake public image',
'is_public': True,
'image_type': 'kernel',
'status': 'bad status'
'type': 'kernel',
'status': 'bad status',
'size': 19,
'location': "file:///tmp/glance-tests/2",
}
self.assertRaises(exception.Duplicate,
@@ -212,8 +206,10 @@ class TestParallaxClient(unittest.TestCase):
fixture = {'id': 3,
'name': 'fake public image',
'is_public': True,
'image_type': 'kernel',
'status': 'bad status'
'type': 'kernel',
'status': 'bad status',
'size': 19,
'location': "file:///tmp/glance-tests/2",
}
self.assertRaises(client.BadInputError,
@@ -221,9 +217,9 @@ class TestParallaxClient(unittest.TestCase):
fixture)
def test_update_image(self):
"""Tests that the /images PUT parallax API updates the image"""
"""Tests that the /images PUT registry API updates the image"""
fixture = {'name': 'fake public image #2',
'image_type': 'ramdisk'
'type': 'ramdisk'
}
self.assertTrue(self.client.update_image(2, fixture))
@@ -239,7 +235,7 @@ class TestParallaxClient(unittest.TestCase):
fixture = {'id': 3,
'name': 'fake public image',
'is_public': True,
'image_type': 'kernel',
'type': 'kernel',
'status': 'bad status'
}
@@ -270,21 +266,20 @@ class TestParallaxClient(unittest.TestCase):
3)
class TestTellerClient(unittest.TestCase):
class TestClient(unittest.TestCase):
"""
Test proper actions made for both valid and invalid requests
against a Teller service
against a Glance service
"""
def setUp(self):
"""Establish a clean test environment"""
self.stubs = stubout.StubOutForTesting()
stubs.stub_out_parallax_db_image_api(self.stubs)
stubs.stub_out_parallax_and_teller_server(self.stubs)
stubs.stub_out_filesystem_backend(self.stubs)
self.client = client.TellerClient()
self.pclient = client.ParallaxClient()
stubs.stub_out_registry_db_image_api(self.stubs)
stubs.stub_out_registry_and_store_server(self.stubs)
stubs.stub_out_filesystem_backend()
self.client = client.Client()
def tearDown(self):
"""Clear the test environment"""
@@ -293,10 +288,24 @@ class TestTellerClient(unittest.TestCase):
def test_get_image(self):
"""Test a simple file backend retrieval works as expected"""
expected = 'chunk0chunk42'
image = self.client.get_image(2)
expected_image = 'chunk00000remainder'
expected_meta = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'type': 'kernel',
'status': 'available',
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {}}
meta, image_chunks = self.client.get_image(2)
self.assertEquals(expected, image)
image_data = ""
for image_chunk in image_chunks:
image_data += image_chunk
self.assertEquals(expected_image, image_data)
for k,v in expected_meta.iteritems():
self.assertEquals(v, meta[k])
def test_get_image_not_existing(self):
"""Test retrieval of a non-existing image returns a 404"""
@@ -305,26 +314,216 @@ class TestTellerClient(unittest.TestCase):
self.client.get_image,
3)
def test_get_image_index(self):
"""Test correct set of public image returned"""
fixture = {'id': 2,
'name': 'fake image #2'}
images = self.client.get_images()
self.assertEquals(len(images), 1)
for k,v in fixture.iteritems():
self.assertEquals(v, images[0][k])
def test_get_image_details(self):
"""Tests that the detailed info about public images returned"""
fixture = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'type': 'kernel',
'status': 'available',
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {}}
expected = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'type': 'kernel',
'status': 'available',
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {}}
images = self.client.get_images_detailed()
self.assertEquals(len(images), 1)
for k,v in expected.iteritems():
self.assertEquals(v, images[0][k])
def test_get_image_meta(self):
"""Tests that the detailed info about an image returned"""
fixture = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'type': 'kernel',
'status': 'available',
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {}}
expected = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'type': 'kernel',
'status': 'available',
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {}}
data = self.client.get_image_meta(2)
for k,v in expected.iteritems():
self.assertEquals(v, data[k])
def test_get_image_non_existing(self):
"""Tests that NotFound is raised when getting a non-existing image"""
self.assertRaises(exception.NotFound,
self.client.get_image,
42)
def test_add_image_without_location_or_raw_data(self):
"""Tests client throws Invalid if missing both location and raw data"""
fixture = {'name': 'fake public image',
'is_public': True,
'type': 'kernel'
}
self.assertRaises(exception.Invalid,
self.client.add_image,
fixture)
def test_add_image_basic(self):
"""Tests that we can add image metadata and returns the new id"""
fixture = {'name': 'fake public image',
'is_public': True,
'type': 'kernel',
'size': 19,
'location': "file:///tmp/glance-tests/2",
}
new_id = self.client.add_image(fixture)
# Test ID auto-assigned properly
self.assertEquals(3, new_id)
# Test all other attributes set
data = self.client.get_image_meta(3)
for k,v in fixture.iteritems():
self.assertEquals(v, data[k])
# Test status was updated properly
self.assertTrue('status' in data.keys())
self.assertEquals('available', data['status'])
def test_add_image_with_properties(self):
"""Tests that we can add image metadata with properties"""
fixture = {'name': 'fake public image',
'is_public': True,
'type': 'kernel',
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {'distro': 'Ubuntu 10.04 LTS'}
}
expected = {'name': 'fake public image',
'is_public': True,
'type': 'kernel',
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {'distro': 'Ubuntu 10.04 LTS'}
}
new_id = self.client.add_image(fixture)
# Test ID auto-assigned properly
self.assertEquals(3, new_id)
# Test all other attributes set
data = self.client.get_image_meta(3)
for k,v in expected.iteritems():
self.assertEquals(v, data[k])
# Test status was updated properly
self.assertTrue('status' in data.keys())
self.assertEquals('available', data['status'])
def test_add_image_already_exists(self):
"""Tests proper exception is raised if image with ID already exists"""
fixture = {'id': 2,
'name': 'fake public image',
'is_public': True,
'type': 'kernel',
'status': 'bad status',
'size': 19,
'location': "file:///tmp/glance-tests/2",
}
self.assertRaises(exception.Duplicate,
self.client.add_image,
fixture)
def test_add_image_with_bad_status(self):
"""Tests a bad status is set to a proper one by server"""
fixture = {'name': 'fake public image',
'is_public': True,
'type': 'kernel',
'status': 'bad status',
'size': 19,
'location': "file:///tmp/glance-tests/2",
}
new_id = self.client.add_image(fixture)
data = self.client.get_image_meta(new_id)
self.assertEquals(data['status'], 'available')
def test_update_image(self):
"""Tests that the /images PUT registry API updates the image"""
fixture = {'name': 'fake public image #2',
'type': 'ramdisk'
}
self.assertTrue(self.client.update_image(2, fixture))
# Test all other attributes set
data = self.client.get_image_meta(2)
for k,v in fixture.iteritems():
self.assertEquals(v, data[k])
def test_update_image_not_existing(self):
"""Tests non existing image update doesn't work"""
fixture = {'id': 3,
'name': 'fake public image',
'is_public': True,
'type': 'kernel',
'status': 'bad status'
}
self.assertRaises(exception.NotFound,
self.client.update_image,
3,
fixture)
def test_delete_image(self):
"""Tests that image data is deleted properly"""
"""Tests that image metadata is deleted properly"""
expected = 'chunk0chunk42'
image = self.client.get_image(2)
self.assertEquals(expected, image)
# Grab the original number of images
orig_num_images = len(self.client.get_images())
# Delete image #2
self.assertTrue(self.client.delete_image(2))
# Delete the image metadata for #2 from Parallax
self.assertTrue(self.pclient.delete_image(2))
# Verify one less image
new_num_images = len(self.client.get_images())
self.assertRaises(exception.NotFound,
self.client.get_image,
2)
self.assertEquals(new_num_images, orig_num_images - 1)
def test_delete_image_not_existing(self):
"""Test deletion of a non-existing image returns a 404"""
"""Tests cannot delete non-existing image"""
self.assertRaises(exception.NotFound,
self.client.delete_image,

View File

@@ -1,75 +0,0 @@
# 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 unittest
import sqlalchemy.exceptions as sa_exc
from glance.common import exception
from glance.parallax import db
from glance.common import flags
from glance.parallax.db.sqlalchemy import models
FLAGS = flags.FLAGS
class TestModels(unittest.TestCase):
""" Test Parllax SQLAlchemy models using an in-memory sqlite DB"""
def setUp(self):
FLAGS.sql_connection = "sqlite://" # in-memory db
models.unregister_models()
models.register_models()
self.image = self._make_image(id=2, name='fake image #2')
def test_metadata_key_constraint_ok(self):
"""Two different images are permitted to have metadata that share the
same key
"""
self._make_property(self.image, key="spam", value="eggs")
second_image = self._make_image(id=3, name='fake image #3')
self._make_property(second_image, key="spam", value="eggs")
def test_metadata_key_constraint_bad(self):
"""The same image cannot have two distinct pieces of metadata with the
same key.
"""
self._make_property(self.image, key="spam", value="eggs")
self.assertRaises(sa_exc.IntegrityError,
self._make_property, self.image, key="spam", value="eggs")
def _make_image(self, id, name):
"""Convenience method to create an image with a given name and id"""
fixture = {'id': id,
'name': name,
'is_public': True,
'image_type': 'kernel',
'status': 'available'}
context = None
image = db.api.image_create(context, fixture)
return image
def _make_property(self, image, key, value):
"""Convenience method to create metadata attached to an image"""
metadata = {'image_id': image['id'], 'key': key, 'value': value}
context = None
property = db.api.image_property_create(context, metadata)
return property

View File

@@ -21,7 +21,7 @@ import unittest
import webob
from glance.common import exception
from glance.parallax import controllers
from glance.registry import controllers
from tests import stubs
@@ -29,14 +29,14 @@ class TestImageController(unittest.TestCase):
def setUp(self):
"""Establish a clean test environment"""
self.stubs = stubout.StubOutForTesting()
stubs.stub_out_parallax_db_image_api(self.stubs)
stubs.stub_out_registry_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",
"""Tests that the root registry API returns "index",
which is a list of public images
"""
@@ -54,7 +54,7 @@ class TestImageController(unittest.TestCase):
self.assertEquals(v, images[0][k])
def test_get_index(self):
"""Tests that the /images parallax API returns list of
"""Tests that the /images registry API returns list of
public images
"""
@@ -72,14 +72,14 @@ class TestImageController(unittest.TestCase):
self.assertEquals(v, images[0][k])
def test_get_details(self):
"""Tests that the /images/detail parallax API returns
"""Tests that the /images/detail registry API returns
a mapping containing a list of detailed image information
"""
fixture = {'id': 2,
'name': 'fake image #2',
'is_public': True,
'image_type': 'kernel',
'type': 'kernel',
'status': 'available'
}
req = webob.Request.blank('/images/detail')
@@ -94,10 +94,10 @@ class TestImageController(unittest.TestCase):
self.assertEquals(v, images[0][k])
def test_create_image(self):
"""Tests that the /images POST parallax API creates the image"""
"""Tests that the /images POST registry API creates the image"""
fixture = {'name': 'fake public image',
'is_public': True,
'image_type': 'kernel'
'type': 'kernel'
}
req = webob.Request.blank('/images')
@@ -125,7 +125,7 @@ class TestImageController(unittest.TestCase):
fixture = {'id': 3,
'name': 'fake public image',
'is_public': True,
'image_type': 'kernel',
'type': 'kernel',
'status': 'bad status'
}
@@ -141,9 +141,9 @@ class TestImageController(unittest.TestCase):
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
def test_update_image(self):
"""Tests that the /images PUT parallax API updates the image"""
"""Tests that the /images PUT registry API updates the image"""
fixture = {'name': 'fake public image #2',
'image_type': 'ramdisk'
'type': 'ramdisk'
}
req = webob.Request.blank('/images/2')
@@ -166,7 +166,7 @@ class TestImageController(unittest.TestCase):
fixture = {'id': 3,
'name': 'fake public image',
'is_public': True,
'image_type': 'kernel',
'type': 'kernel',
'status': 'bad status'
}
@@ -183,7 +183,7 @@ class TestImageController(unittest.TestCase):
webob.exc.HTTPNotFound.code)
def test_delete_image(self):
"""Tests that the /images DELETE parallax API deletes the image"""
"""Tests that the /images DELETE registry API deletes the image"""
# Grab the original number of images
req = webob.Request.blank('/images')

View File

@@ -21,8 +21,8 @@ import stubout
import unittest
import urlparse
from glance.teller.backends.swift import SwiftBackend
from glance.teller.backends import Backend, BackendException, get_from_backend
from glance.store.swift import SwiftBackend
from glance.store import Backend, BackendException, get_from_backend
from tests import stubs
Backend.CHUNKSIZE = 2
@@ -39,19 +39,23 @@ class TestBackend(unittest.TestCase):
class TestFilesystemBackend(TestBackend):
def setUp(self):
"""Establish a clean test environment"""
stubs.stub_out_filesystem_backend()
def tearDown(self):
"""Clear the test environment"""
stubs.clean_out_fake_filesystem_backend()
def test_get(self):
class FakeFile(object):
def __enter__(self, *args, **kwargs):
return StringIO('fakedata')
def __exit__(self, *args, **kwargs):
pass
fetcher = get_from_backend("file:///path/to/file.tar.gz",
expected_size=8,
opener=lambda _: FakeFile())
fetcher = get_from_backend("file:///tmp/glance-tests/2",
expected_size=19)
chunks = [c for c in fetcher]
self.assertEqual(chunks, ["fa", "ke", "da", "ta"])
data = ""
for chunk in fetcher:
data += chunk
self.assertEqual(data, "chunk00000remainder")
class TestHTTPBackend(TestBackend):
@@ -122,6 +126,3 @@ class TestSwiftBackend(TestBackend):
self.assertEqual(authurl, 'https://localhost/v1.0')
self.assertEqual(container, 'container1')
self.assertEqual(obj, 'file.tar.gz')

View File

@@ -1,86 +0,0 @@
# 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 unittest
import stubout
import webob
from glance.teller import controllers as teller_controllers
from glance.parallax import controllers as parallax_controllers
from tests import stubs
class TestImageController(unittest.TestCase):
def setUp(self):
"""Establish a clean test environment"""
self.stubs = stubout.StubOutForTesting()
stubs.stub_out_parallax_and_teller_server(self.stubs)
stubs.stub_out_parallax_db_image_api(self.stubs)
stubs.stub_out_filesystem_backend(self.stubs)
def tearDown(self):
"""Clear the test environment"""
stubs.clean_out_fake_filesystem_backend()
self.stubs.UnsetAll()
def test_index_raises_not_implemented(self):
req = webob.Request.blank("/images")
res = req.get_response(teller_controllers.API())
self.assertEquals(res.status_int, webob.exc.HTTPNotImplemented.code)
def test_show_image_unrecognized_registry_adapter(self):
req = webob.Request.blank("/images/1?registry=unknown")
res = req.get_response(teller_controllers.API())
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
def test_show_image_basic(self):
req = webob.Request.blank("/images/2")
res = req.get_response(teller_controllers.API())
self.assertEqual('chunk0chunk42', res.body)
def test_show_non_exists_image(self):
req = webob.Request.blank("/images/42")
res = req.get_response(teller_controllers.API())
self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code)
def test_delete_image(self):
req = webob.Request.blank("/images/2")
req.method = 'DELETE'
res = req.get_response(teller_controllers.API())
self.assertEquals(res.status_int, 200)
# Deletion from registry is not done from Teller on
# purpose to allow the most flexibility for migrating
# image file/chunk locations while keeping an image
# identifier stable.
req = webob.Request.blank("/images/2")
req.method = 'DELETE'
res = req.get_response(parallax_controllers.API())
self.assertEquals(res.status_int, 200)
req = webob.Request.blank("/images/2")
req.method = 'GET'
res = req.get_response(teller_controllers.API())
self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code, res.body)
def test_delete_non_exists_image(self):
req = webob.Request.blank("/images/42")
req.method = 'DELETE'
res = req.get_response(teller_controllers.API())
self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code)