Major refactoring...
This commit is contained in:
@@ -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`
|
||||
|
||||
|
||||
146
glance/client.py
146
glance/client.py
@@ -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
|
||||
|
||||
@@ -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
105
glance/registry/client.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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'])
|
||||
@@ -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')
|
||||
|
||||
|
||||
###################
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
290
glance/server.py
Normal 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
103
glance/store/__init__.py
Normal 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
|
||||
@@ -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,
|
||||
97
glance/store/filesystem.py
Normal file
97
glance/store/filesystem.py
Normal 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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
78
glance/util.py
Normal 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
|
||||
174
tests/stubs.py
174
tests/stubs.py
@@ -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)
|
||||
|
||||
@@ -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
269
tests/unit/test_api.py
Normal 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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user