Merge trunk

This commit is contained in:
jaypipes@gmail.com 2011-05-25 10:44:47 -04:00
commit ef6f306d72
33 changed files with 2107 additions and 319 deletions

11
.mailmap Normal file
View File

@ -0,0 +1,11 @@
# Format is:
# <preferred e-mail> <other e-mail 1>
# <preferred e-mail> <other e-mail 2>
<corywright@gmail.com> <cory.wright@rackspace.com>
<jsuh@isi.edu> <jsuh@bespin>
<josh@jk0.org> <josh.kearney@rackspace.com>
<rconradharris@gmail.com> <rick.harris@rackspace.com>
<rconradharris@gmail.com> <rick@quasar.racklabs.com>
<rick@openstack.org> <rclark@chat-blanc>
<soren.hansen@rackspace.com> <soren@linux2go.dk>
<soren.hansen@rackspace.com> <soren@openstack.org>

20
Authors Normal file
View File

@ -0,0 +1,20 @@
Andrey Brindeyev <abrindeyev@griddynamics.com>
Brian Waldon <brian.waldon@rackspace.com>
Christopher MacGown <chris@slicehost.com>
Cory Wright <corywright@gmail.com>
Dan Prince <dan.prince@rackspace.com>
Donal Lafferty <donal.lafferty@citrix.com>
Eldar Nugaev <enugaev@griddynamics.com>
Ewan Mellor <ewan.mellor@citrix.com>
Jay Pipes <jaypipes@gmail.com>
Jinwoo 'Joseph' Suh <jsuh@isi.edu>
Josh Kearney <josh@jk0.org>
Ken Pepple <ken.pepple@gmail.com>
Matt Dietz <matt.dietz@rackspace.com>
Monty Taylor <mordred@inaugust.com>
Rick Clark <rick@openstack.org>
Rick Harris <rconradharris@gmail.com>
Soren Hansen <soren.hansen@rackspace.com>
Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp>
Thierry Carrez <thierry@openstack.org>
Vishvananda Ishaya <vishvananda@gmail.com>

View File

@ -209,8 +209,8 @@ EXAMPLES
pieces = str(e).split('\n')
for piece in pieces:
print piece
print ("Note: Your image metadata may still be in the registry, but "
"the image's status will likely be 'killed'.")
print ("Note: Your image metadata may still be in the registry, "
"but the image's status will likely be 'killed'.")
return FAILURE
else:
print "Dry run. We would have done the following:"

View File

@ -133,7 +133,7 @@ def do_start(server, options, args):
pid_file = '/var/run/glance/%s.pid' % server
else:
pid_file = os.path.abspath(options['pid_file'])
conf_file = config.find_config_file(options, args)
conf_file = config.find_config_file(server, options, args)
if not conf_file:
sys.exit("Could not find any configuration file to use!")
launch_args = [(conf_file, pid_file)]

View File

@ -17,10 +17,34 @@
Configuring Glance
==================
Glance has a number of options that you can use to configure the Glance API
server, the Glance Registry server, and the various storage backends that
Glance can use to store images.
Most configuration is done via configuration files, with the Glance API
server and Glance Registry server using separate configuration files.
When starting up a Glance server, you can specify the configuration file to
use (see `the documentation on controller Glance servers <controllingservers>`_).
If you do **not** specify a configuration file, Glance will look in the following
directories for a configuration file, in order:
* ``$CWD``
* ``~/.glance``
* ``~/``
* ``/etc/glance``
* ``/etc``
The Glance API server configuration file should be named ``glance-api.conf``.
Similarly, the Glance Registry server configuration file should be named
``glance-registry.conf``. If you installed Glance via your operating system's
package management system, it is likely that you will have sample
configuration files installed in ``/etc/glance``.
In addition to this documentation page, you can check the
``etc/glance.conf.sample`` sample configuration file distributed with Glance
for an example configuration file with detailed comments on what each options
does.
``etc/glance-api.conf`` and ``etc/glance-registry.conf`` sample configuration
files distributed with Glance for example configuration files for each server
application with detailed comments on what each options does.
Common Configuration Options in Glance
--------------------------------------
@ -56,11 +80,15 @@ file. If it is, then we try to use that as the configuration file. If there is
no file or there were no arguments, we search for a configuration file in the
following order:
- ./glance.conf
- ~/glance.conf
- ~/.glance/glance.conf
- /etc/glance/glance.conf
- /etc/glance.conf
* ``$CWD``
* ``~/.glance``
* ``~/``
* ``/etc/glance``
* ``/etc``
The filename that is searched for depends on the server application name. So,
if you are starting up the API server, ``glance-api.conf`` is searched for,
otherwise ``glance-registry.conf``.
Configuring Logging in Glance
-----------------------------
@ -76,20 +104,16 @@ Specified on the command line only.
Takes a path to a configuration file to use for configuring logging.
Logging Options Available in ``glance.conf``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Logging Options Available Only in Configuration Files
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You will want to place the different logging options **in each of the API and registry
application sections in your glance.conf**. As an example, you might do the following::
You will want to place the different logging options in the **[DEFAULT]** section
in your application configuration file. As an example, you might do the following
for the API server, in a configuration file called ``etc/glance-api.conf``::
[app:glance-api]
... other options ...
[DEFAULT]
log_file = /var/log/glance/api.log
[app:glance-registry]
... other options ...
log_file = /var/log/glance/registry.log
* ``log_file``
The filepath of the file to use for logging messages from Glance's servers. If
@ -115,7 +139,7 @@ Configuring Glance Storage Backends
There are a number of configuration options in Glance that control how Glance
stores disk images. These configuration options are specified in the
``glance.conf`` config file `in the section [app:glance-api]`.
``glance-api.conf`` config file in the section ``[DEFAULT]``.
* ``default_store=STORE``
@ -201,7 +225,7 @@ Configuring the Glance Registry
Glance ships with a default, reference implementation registry server. There
are a number of configuration options in Glance that control how this registry
server operates. These configuration options are specified in the
``glance.conf`` config file `in the section [app:glance-registry]`.
``glance-registry.conf`` config file in the section ``[DEFAULT]``.
* ``sql_connection=CONNECTION_STRING`` (``--sql-connection`` when specified
on command line)

View File

@ -36,7 +36,7 @@ Manually starting the server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The first is by directly calling the server program, passing in command-line
options and a single argument for the ``paste.deploy`` configuration file to
options and a single argument for a ``paste.deploy`` configuration file to
use when configuring the server application.
.. note::
@ -46,18 +46,18 @@ use when configuring the server application.
adapt for your own uses. Specifically, bind_host must be set properly.
If you do `not` specifiy a configuration file on the command line, Glance will
do its best to locate a ``glance.conf`` configuration file in one of the
do its best to locate a configuration file in one of the
following directories, stopping at the first config file it finds:
* .
* ``$CWD``
* ``~/.glance``
* ``~/``
* ``/etc/glance``
* ``/etc``
* ~/.glance
* ~/
* /etc/glance/
* /etc
The filename that is searched for depends on the server application name. So,
if you are starting up the API server, ``glance-api.conf`` is searched for,
otherwise ``glance-registry.conf``.
If no configuration file is found, you will see an error, like::
@ -66,10 +66,10 @@ If no configuration file is found, you will see an error, like::
Here is an example showing how you can manually start the ``glance-api`` server and ``glance-registry`` in a shell.::
$ sudo glance-api glance.conf --debug &
$ sudo glance-api glance-api.conf --debug &
jsuh@mc-ats1:~$ 2011-04-13 14:50:12 DEBUG [glance-api] ********************************************************************************
2011-04-13 14:50:12 DEBUG [glance-api] Configuration options gathered from config file:
2011-04-13 14:50:12 DEBUG [glance-api] /home/jsuh/glance.conf
2011-04-13 14:50:12 DEBUG [glance-api] /home/jsuh/glance-api.conf
2011-04-13 14:50:12 DEBUG [glance-api] ================================================
2011-04-13 14:50:12 DEBUG [glance-api] bind_host 65.114.169.29
2011-04-13 14:50:12 DEBUG [glance-api] bind_port 9292
@ -83,7 +83,7 @@ Here is an example showing how you can manually start the ``glance-api`` server
2011-04-13 14:50:12 DEBUG [routes.middleware] Initialized with method overriding = True, and path info altering = True
2011-04-13 14:50:12 DEBUG [eventlet.wsgi.server] (21354) wsgi starting up on http://65.114.169.29:9292/
$ sudo glance-registry glance.conf &
$ sudo glance-registry glance-registry.conf &
jsuh@mc-ats1:~$ 2011-04-13 14:51:16 INFO [sqlalchemy.engine.base.Engine.0x...feac] PRAGMA table_info("images")
2011-04-13 14:51:16 INFO [sqlalchemy.engine.base.Engine.0x...feac] ()
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Col ('cid', 'name', 'type', 'notnull', 'dflt_value', 'pk')
@ -112,12 +112,13 @@ Here is an example showing how you can manually start the ``glance-api`` server
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (7, u'value', u'TEXT', 0, None, 0)
$ ps aux | grep glance
root 20009 0.7 0.1 12744 9148 pts/1 S 12:47 0:00 /usr/bin/python /usr/bin/glance-api glance.conf --debug
root 20012 2.0 0.1 25188 13356 pts/1 S 12:47 0:00 /usr/bin/python /usr/bin/glance-registry glance.conf
root 20009 0.7 0.1 12744 9148 pts/1 S 12:47 0:00 /usr/bin/python /usr/bin/glance-api glance-api.conf --debug
root 20012 2.0 0.1 25188 13356 pts/1 S 12:47 0:00 /usr/bin/python /usr/bin/glance-registry glance-registry.conf
jsuh 20017 0.0 0.0 3368 744 pts/1 S+ 12:47 0:00 grep glance
Simply supply the configuration file as the first argument
(``etc/glance.conf.sample`` in the above example) and then any common options
(the ``etc/glance-api.conf`` and ``etc/glance-registry.conf`` sample configuration
files were used in the above example) and then any common options
you want to use (``--debug`` was used above to show some of the debugging
output that the server shows when starting up. Call the server program
with ``--help`` to see all available options you can specify on the
@ -159,13 +160,15 @@ Here is an example that shows how to start the ``glance-registry`` server
with the ``glance-control`` wrapper script. ::
$ sudo glance-control all start glance.conf
$ sudo glance-control api start glance-api.conf
Starting glance-api with /home/jsuh/glance.conf
$ sudo glance-control registry start glance-registry.conf
Starting glance-registry with /home/jsuh/glance.conf
$ ps aux | grep glance
root 20038 4.0 0.1 12728 9116 ? Ss 12:51 0:00 /usr/bin/python /usr/bin/glance-api /home/jsuh/glance.conf
root 20039 6.0 0.1 25188 13356 ? Ss 12:51 0:00 /usr/bin/python /usr/bin/glance-registry /home/jsuh/glance.conf
root 20038 4.0 0.1 12728 9116 ? Ss 12:51 0:00 /usr/bin/python /usr/bin/glance-api /home/jsuh/glance-api.conf
root 20039 6.0 0.1 25188 13356 ? Ss 12:51 0:00 /usr/bin/python /usr/bin/glance-registry /home/jsuh/glance-registry.conf
jsuh 20042 0.0 0.0 3368 744 pts/1 S+ 12:51 0:00 grep glance
@ -173,11 +176,6 @@ The same ``paste.deploy`` configuration files are used by ``glance-control``
to start the Glance server programs, and you can specify (as the example above
shows) a configuration file when starting the server.
.. note::
To start each of Glance servers (currently the glance-api and glance-registry
programs), you can specify glance-api and glance-registry, respectively for the <SERVER>.
Stopping a server
-----------------
@ -201,6 +199,6 @@ Restarting a server
You can restart a server with the ``glance-control`` program, as demonstrated
here::
$> sudo glance-control registry restart etc/glance.conf.sample
$> sudo glance-control registry restart etc/glance-registry.conf
Stopping glance-registry pid: 17611 signal: 15
Starting glance-registry with /home/jpipes/repos/glance/trunk/etc/glance.conf.sample
Starting glance-registry with /home/jpipes/repos/glance/trunk/etc/glance-registry.conf

View File

@ -17,44 +17,44 @@
Using the Glance CLI Tool
=========================
Glance ships with a command-line tool for quering and managing Glance
Glance ships with a command-line tool for querying and managing Glance
It has a fairly simple but powerful interface of the form::
Usage: glance <command> [options] [args]
Where ``<command>`` is one of the following:
* help
* ``help``
Show detailed help information about a specific command
* add
* ``add``
Adds an image to Glance
* update
* ``update``
Updates an image's stored metadata in Glance
* delete
* ``delete``
Deletes an image and its metadata from Glance
* index
* ``index``
Lists brief information about *public* images that Glance knows about
* details
* ``details``
Lists detailed information about *public* images that Glance knows about
* show
* ``show``
Lists detailed information about a specific image
* clear
* ``clear``
Destroys *all* images and their associated metadata
Destroys all **public** images and their associated metadata
This document describes how to use the ``glance`` tool for each of
the above commands.
@ -134,6 +134,23 @@ The ``add`` command is used to do both of the following:
We cover both use cases below.
Important Information about Uploading Images
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before we go over the commands for adding an image to Glance, it is
important to understand that Glance **does not currently inspect** the image
files you add to it. In other words, **Glance only understands what you tell it,
via attributes and custom properties**.
If the file extension of the file you upload to Glance ends in '.vhd', Glance
**does not** know that the image you are uploading has a disk format of ``vhd``.
You have to **tell** Glance that the image you are uploading has a disk format by
using the ``disk_format=vhd`` on the command line (see more below).
By the same token, Glance does not currently allow you to upload "multi-part"
disk images at once. **The common operation of bundling a kernel image and ramdisk image
into a machine image is not done automagically by Glance.**
Store virtual machine image data and metadata
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -142,27 +159,27 @@ command. You will pass metadata about the VM image on the command line, and
you will use a standard shell redirect to stream the image data file to
``glance``.
Let's walk through a simple example. Suppose we have an image stored on our
local filesystem that we wish to "upload" to Glance. This image is stored
on our local filesystem in ``/tmp/images/myimage.tar.gz``.
Let's walk through a simple example. Suppose we have a virtual disk image
stored on our local filesystem that we wish to "upload" to Glance. This image is stored
on our local filesystem in ``/tmp/images/myimage.iso``.
We'd also like to tell Glance that this image should be called "My Image", and
that the image should be public -- anyone should be able to fetch it.
Here is how we'd upload this image to Glance. Change example ip number to your server ip number.::
$> glance add name="My Image" is_public=true < /tmp/images/myimage.tar.gz --host=65.114.169.29
$> glance add name="My Image" is_public=true < /tmp/images/myimage.iso --host=65.114.169.29
If Glance was able to successfully upload and store your VM image data and
metadata attributes, you would see something like this::
$> glance add name="My Image" is_public=true < /tmp/images/myimage.tar.gz --host=65.114.169.29
$> glance add name="My Image" is_public=true < /tmp/images/myimage.iso --host=65.114.169.29
Added new image with ID: 2
You can use the ``--verbose`` (or ``-v``) command-line option to print some more
information about the metadata that was saved with the image::
$> glance --verbose add name="My Image" is_public=true < /tmp/images/myimage.tar.gz --host=65.114.169.29
$> glance --verbose add name="My Image" is_public=true < /tmp/images/myimage.iso --host=65.114.169.29
Added new image with ID: 4
Returned the following metadata for the new image:
container_format => ovf
@ -183,7 +200,7 @@ information about the metadata that was saved with the image::
If you are unsure about what will be added, you can use the ``--dry-run``
command-line option, which will simply show you what *would* have happened::
$> glance --dry-run add name="Foo" distro="Ubuntu" is_publi=True < /tmp/images/myimage.tar.gz --host=65.114.169.29
$> glance --dry-run add name="Foo" distro="Ubuntu" is_publi=True < /tmp/images/myimage.iso --host=65.114.169.29
Dry run. We would have done the following:
Add new image with metadata:
container_format => ovf
@ -213,10 +230,11 @@ instead, you tell Glance where to find the existing virtual machine image by
setting the ``location`` field. Below is an example of doing this.
Let's assume that there is a virtual machine image located at the URL
``http://example.com/images/myimage.tar.gz``. We can register this image with
``http://example.com/images/myimage.vhd``. We can register this image with
Glance using the following::
$> glance --verbose add name="Some web image" location="http://example.com/images/myimage.tar.gz"
$> glance --verbose add name="Some web image" disk_format=vhd container_format=ovf\
location="http://example.com/images/myimage.vhd"
Added new image with ID: 1
Returned the following metadata for the new image:
container_format => ovf
@ -226,7 +244,7 @@ Glance using the following::
disk_format => vhd
id => 1
is_public => True
location => http://example.com/images/myimage.tar.gz
location => http://example.com/images/myimage.vhd
name => Some web image
properties => {}
size => 0
@ -304,6 +322,7 @@ available in Glance, as shown below::
Id: 1
Public? Yes
Name: Ubuntu 10.10
Status: active
Size: 58520278
Location: file:///tmp/images/1
Disk format: vhd
@ -315,6 +334,7 @@ available in Glance, as shown below::
Id: 2
Public? Yes
Name: Ubuntu 10.04
Status: active
Size: 58520278
Location: file:///tmp/images/2
Disk format: ami
@ -326,6 +346,7 @@ available in Glance, as shown below::
Id: 3
Public? Yes
Name: Fedora 9
Status: active
Size: 3040
Location: file:///tmp/images/3
Disk format: vdi
@ -337,8 +358,9 @@ available in Glance, as shown below::
Id: 4
Public? Yes
Name: Vanilla Linux 2.6.22
Status: active
Size: 0
Location: http://example.com/images/vanilla.tar.gz
Location: http://example.com/images/vanilla.iso
Disk format: qcow2
Container format: bare
================================================================================
@ -354,6 +376,7 @@ with ``<ID>``, as shown below::
Id: 3
Public? Yes
Name: Fedora 9
Status: active
Size: 3040
Location: file:///tmp/images/3
Disk format: vdi

View File

@ -5,9 +5,6 @@ verbose = True
# Show debugging output in logs (sets DEBUG log level output)
debug = False
[app:glance-api]
paste.app_factory = glance.server:app_factory
# Which backend store should Glance use by default is not specified
# in a request to add a new image to Glance? Default: 'file'
# Available choices are 'file', 'swift', and 's3'
@ -54,29 +51,17 @@ swift_store_container = glance
# Do we create the container if it does not exist?
swift_store_create_container_on_put = False
[app:glance-registry]
paste.app_factory = glance.registry.server:app_factory
[pipeline:glance-api]
pipeline = versionnegotiation apiv1app
# Address to bind the registry server
bind_host = 0.0.0.0
[pipeline:versions]
pipeline = versionsapp
# Port the bind the registry server to
bind_port = 9191
[app:versionsapp]
paste.app_factory = glance.api.versions:app_factory
# Log to this file. Make sure you do not set the same log
# file for both the API and registry servers!
log_file = /var/log/glance/registry.log
[app:apiv1app]
paste.app_factory = glance.api.v1:app_factory
# SQLAlchemy connection string for the reference implementation
# registry server. Any valid SQLAlchemy connection string is fine.
# See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine
sql_connection = sqlite:///glance.sqlite
# Period in seconds after which SQLAlchemy should reestablish its connection
# to the database.
#
# MySQL uses a default `wait_timeout` of 8 hours, after which it will drop
# idle connections. This can result in 'MySQL Gone Away' exceptions. If you
# notice this, you can lower this value to ensure that SQLAlchemy reconnects
# before MySQL can drop the connection.
sql_idle_timeout = 3600
[filter:versionnegotiation]
paste.filter_factory = glance.api.middleware.version_negotiation:filter_factory

33
etc/glance-registry.conf Normal file
View File

@ -0,0 +1,33 @@
[DEFAULT]
# Show more verbose log output (sets INFO log level output)
verbose = True
# Show debugging output in logs (sets DEBUG log level output)
debug = False
# Address to bind the registry server
bind_host = 0.0.0.0
# Port the bind the registry server to
bind_port = 9191
# Log to this file. Make sure you do not set the same log
# file for both the API and registry servers!
log_file = /var/log/glance/registry.log
# SQLAlchemy connection string for the reference implementation
# registry server. Any valid SQLAlchemy connection string is fine.
# See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine
sql_connection = sqlite:///glance.sqlite
# Period in seconds after which SQLAlchemy should reestablish its connection
# to the database.
#
# MySQL uses a default `wait_timeout` of 8 hours, after which it will drop
# idle connections. This can result in 'MySQL Gone Away' exceptions. If you
# notice this, you can lower this value to ensure that SQLAlchemy reconnects
# before MySQL can drop the connection.
sql_idle_timeout = 3600
[app:glance-registry]
paste.app_factory = glance.registry.server:app_factory

16
glance/api/__init__.py Normal file
View File

@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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.

View File

@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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.

View File

@ -0,0 +1,134 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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 filter middleware that inspects the requested URI for a version string
and/or Accept headers and attempts to negotiate an API controller to
return
"""
import logging
import re
import routes
from glance.api import v1
from glance.api import versions
from glance.common import wsgi
logger = logging.getLogger('glance.api.middleware.version_negotiation')
class VersionNegotiationFilter(wsgi.Middleware):
def __init__(self, app, options):
self.versions_app = versions.Controller(options)
self.version_uri_regex = re.compile(r"^v(\d+)\.?(\d+)?")
self.options = options
super(VersionNegotiationFilter, self).__init__(app)
def process_request(self, req):
"""
If there is a version identifier in the URI, simply
return the correct API controller, otherwise, if we
find an Accept: header, process it
"""
# See if a version identifier is in the URI passed to
# us already. If so, simply return the right version
# API controller
logger.debug("Processing request: %s %s Accept: %s",
req.method, req.path, req.accept)
# If the request is for /versions, just return the versions container
if req.path_info_peek() == "versions":
return self.versions_app
match = self._match_version_string(req.path_info_peek(), req)
if match:
if (req.environ['api.major_version'] == 1 and
req.environ['api.minor_version'] == 0):
logger.debug("Matched versioned URI. Version: %d.%d",
req.environ['api.major_version'],
req.environ['api.minor_version'])
# Strip the version from the path
req.path_info_pop()
return None
else:
logger.debug("Unknown version in versioned URI: %d.%d. "
"Returning version choices.",
req.environ['api.major_version'],
req.environ['api.minor_version'])
return self.versions_app
accept = req.headers['Accept']
if accept.startswith('application/vnd.openstack.images-'):
token_loc = len('application/vnd.openstack.images-')
accept_version = accept[token_loc:]
match = self._match_version_string(accept_version, req)
if match:
if (req.environ['api.major_version'] == 1 and
req.environ['api.minor_version'] == 0):
logger.debug("Matched versioned media type. "
"Version: %d.%d",
req.environ['api.major_version'],
req.environ['api.minor_version'])
return None
else:
logger.debug("Unknown version in accept header: %s.%s..."
"returning version choices.",
req.environ['api.major_version'],
req.environ['api.minor_version'])
return self.versions_app
else:
if req.accept not in ('*/*', ''):
logger.debug("Unknown accept header: %s..."
"returning version choices.", req.accept)
return self.versions_app
return None
def _match_version_string(self, subject, req):
"""
Given a subject string, tries to match a major and/or
minor version number. If found, sets the api.major_version
and api.minor_version environ variables.
Returns True if there was a match, false otherwise.
:param subject: The string to check
:param req: Webob.Request object
"""
match = self.version_uri_regex.match(subject)
if match:
major_version, minor_version = match.groups(0)
major_version = int(major_version)
minor_version = int(minor_version)
req.environ['api.major_version'] = major_version
req.environ['api.minor_version'] = minor_version
return match is not None
def filter_factory(global_conf, **local_conf):
"""
Factory method for paste.deploy
"""
conf = global_conf.copy()
conf.update(local_conf)
def filter(app):
return VersionNegotiationFilter(app, conf)
return filter

48
glance/api/v1/__init__.py Normal file
View File

@ -0,0 +1,48 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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 logging
import routes
from glance.api.v1 import images
from glance.common import wsgi
logger = logging.getLogger('glance.api.v1')
class API(wsgi.Router):
"""WSGI router for Glance v1 API requests."""
def __init__(self, options):
self.options = options
mapper = routes.Mapper()
controller = images.Controller(options)
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)
def app_factory(global_conf, **local_conf):
"""paste.deploy app factory for creating Glance API server apps"""
conf = global_conf.copy()
conf.update(local_conf)
return API(conf)

View File

@ -16,17 +16,7 @@
# under the License.
"""
=================
Glance API Server
=================
Configuration Options
---------------------
`default_store`: When no x-image-meta-store header is sent for a
`POST /images` request, this store will be used
for storing the image data. Default: 'file'
/images endpoint for Glance v1 API
"""
import httplib
@ -34,7 +24,6 @@ import json
import logging
import sys
import routes
from webob import Response
from webob.exc import (HTTPNotFound,
HTTPConflict,
@ -51,15 +40,18 @@ from glance import registry
from glance import utils
logger = logging.getLogger('glance.server')
logger = logging.getLogger('glance.api.v1.images')
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
'size_min', 'size_max']
class Controller(wsgi.Controller):
"""
Main WSGI application controller for Glance.
WSGI controller for images resource in Glance v1 API
The Glance API is a RESTful web service for image data. The API
The images resource API is a RESTful web service for image data. The API
is as follows::
GET /images -- Returns a set of brief metadata about images
@ -100,7 +92,8 @@ class Controller(wsgi.Controller):
'size': <SIZE>}, ...
]}
"""
images = registry.get_images_list(self.options)
filters = self._get_filters(req)
images = registry.get_images_list(self.options, filters)
return dict(images=images)
def detail(self, req):
@ -125,9 +118,24 @@ class Controller(wsgi.Controller):
'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ...
]}
"""
images = registry.get_images_detail(self.options)
filters = self._get_filters(req)
images = registry.get_images_detail(self.options, filters)
return dict(images=images)
def _get_filters(self, req):
"""
Return a dictionary of query param filters from the request
:param req: the Request object coming from the wsgi layer
:retval a dict of key/value filters
"""
filters = {}
for param in req.str_params:
if param in SUPPORTED_FILTERS or param.startswith('property-'):
filters[param] = req.str_params.get(param)
return filters
def meta(self, req, id):
"""
Returns metadata about an image in the HTTP headers of the
@ -142,7 +150,7 @@ class Controller(wsgi.Controller):
res = Response(request=req)
utils.inject_image_meta_into_headers(res, image)
res.headers.add('Location', "/images/%s" % id)
res.headers.add('Location', "/v1/images/%s" % id)
res.headers.add('ETag', image['checksum'])
return req.get_response(res)
@ -175,7 +183,7 @@ class Controller(wsgi.Controller):
# Using app_iter blanks content-length, so we set it here...
res.headers.add('Content-Length', image['size'])
utils.inject_image_meta_into_headers(res, image)
res.headers.add('Location', "/images/%s" % id)
res.headers.add('Location', "/v1/images/%s" % id)
res.headers.add('ETag', image['checksum'])
return req.get_response(res)
@ -397,7 +405,7 @@ class Controller(wsgi.Controller):
# URI of the resource newly-created.
res = Response(request=req, body=json.dumps(dict(image=image_meta)),
status=httplib.CREATED, content_type="text/plain")
res.headers.add('Location', "/images/%s" % image_id)
res.headers.add('Location', "/v1/images/%s" % image_id)
res.headers.add('ETag', image_meta['checksum'])
return req.get_response(res)
@ -505,26 +513,3 @@ class Controller(wsgi.Controller):
logger.error(msg)
raise HTTPBadRequest(msg, request=request,
content_type='text/plain')
class API(wsgi.Router):
"""WSGI entry point for all Glance API requests."""
def __init__(self, options):
self.options = options
mapper = routes.Mapper()
controller = Controller(options)
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)
def app_factory(global_conf, **local_conf):
"""paste.deploy app factory for creating Glance API server apps"""
conf = global_conf.copy()
conf.update(local_conf)
return API(conf)

69
glance/api/versions.py Normal file
View File

@ -0,0 +1,69 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 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.
"""
Controller that returns information on the Glance API versions
"""
import httplib
import json
import webob.dec
from glance.common import wsgi
class Controller(object):
"""
A controller that produces information on the Glance API versions.
"""
def __init__(self, options):
self.options = options
@webob.dec.wsgify
def __call__(self, req):
"""Respond to a request for all OpenStack API versions."""
version_objs = [
{
"id": "v1.0",
"status": "CURRENT",
"links": [
{
"rel": "self",
"href": self.get_href()}]}]
body = json.dumps(dict(versions=version_objs))
response = webob.Response(request=req,
status=httplib.MULTIPLE_CHOICES,
content_type='application/json')
response.body = body
return response
def get_href(self):
return "http://%s:%s/v1/" % (self.options['bind_host'],
self.options['bind_port'])
def app_factory(global_conf, **local_conf):
"""paste.deploy app factory for creating Glance API versions apps"""
conf = global_conf.copy()
conf.update(local_conf)
return Controller(conf)

View File

@ -25,6 +25,7 @@ import logging
import urlparse
import socket
import sys
import urllib
from glance import utils
from glance.common import exception
@ -94,7 +95,8 @@ class BaseClient(object):
else:
return httplib.HTTPConnection
def do_request(self, method, action, body=None, headers=None):
def do_request(self, method, action, body=None, headers=None,
params=None):
"""
Connects to the server and issues a request. Handles converting
any returned HTTP error status codes to OpenStack/Glance exceptions
@ -105,6 +107,8 @@ class BaseClient(object):
:param action: part of URL after root netloc
:param body: string of data to send, or None (default)
:param headers: mapping of key/value pairs to add as headers
:param params: dictionary of key/value pairs to add to append
to action
:note
@ -115,6 +119,9 @@ class BaseClient(object):
objects to be transferred efficiently without buffering the entire
body in memory.
"""
if type(params) is dict:
action += '?' + urllib.urlencode(params)
try:
connection_type = self.get_connection_type()
headers = headers or {}
@ -177,37 +184,44 @@ class BaseClient(object):
return response.status
class Client(BaseClient):
class V1Client(BaseClient):
"""Main client class for accessing Glance resources"""
DEFAULT_PORT = 9292
def __init__(self, host, port=None, use_ssl=False):
def __init__(self, host, port=None, use_ssl=False, doc_root="/v1"):
"""
Creates a new client to a Glance API service.
:param host: The host where Glance resides
:param port: The port where Glance resides (defaults to 9292)
:param use_ssl: Should we use HTTPS? (defaults to False)
:param doc_root: Prefix for all URLs we request from host
"""
port = port or self.DEFAULT_PORT
self.doc_root = doc_root
super(Client, self).__init__(host, port, use_ssl)
def get_images(self):
def do_request(self, method, action, body=None, headers=None, params=None):
action = "%s/%s" % (self.doc_root, action.lstrip("/"))
return super(V1Client, self).do_request(method, action, body,
headers, params)
def get_images(self, filters=None):
"""
Returns a list of image id/name mappings from Registry
"""
res = self.do_request("GET", "/images")
res = self.do_request("GET", "/images", params=filters)
data = json.loads(res.read())['images']
return data
def get_images_detailed(self):
def get_images_detailed(self, filters=None):
"""
Returns a list of detailed image data mappings from Registry
"""
res = self.do_request("GET", "/images/detail")
res = self.do_request("GET", "/images/detail", params=filters)
data = json.loads(res.read())['images']
return data
@ -288,3 +302,6 @@ class Client(BaseClient):
"""
self.do_request("DELETE", "/images/%s" % image_id)
return True
Client = V1Client

View File

@ -175,14 +175,14 @@ def setup_logging(options, conf):
root_logger.addHandler(handler)
def find_config_file(options, args):
def find_config_file(app_name, options, args):
"""
Return the first config file found.
Return the first config file found for an application.
We search for the paste config file in the following order:
* If --config-file option is used, use that
* If args[0] is a file, use that
* Search for glance.conf in standard directories:
* Search for $app.conf in standard directories:
* .
* ~.glance/
* ~
@ -200,7 +200,7 @@ def find_config_file(options, args):
if os.path.exists(args[0]):
return fix_path(args[0])
# Handle standard directory search for glance.conf
# Handle standard directory search for $app_name.conf
config_file_dirs = [fix_path(os.getcwd()),
fix_path(os.path.join('~', '.glance')),
fix_path('~'),
@ -208,7 +208,7 @@ def find_config_file(options, args):
'/etc']
for cfg_dir in config_file_dirs:
cfg_file = os.path.join(cfg_dir, 'glance.conf')
cfg_file = os.path.join(cfg_dir, '%s.conf' % app_name)
if os.path.exists(cfg_file):
return cfg_file
@ -221,7 +221,7 @@ def load_paste_config(app_name, options, args):
We search for the paste config file in the following order:
* If --config-file option is used, use that
* If args[0] is a file, use that
* Search for glance.conf in standard directories:
* Search for $app_name.conf in standard directories:
* .
* ~.glance/
* ~
@ -238,7 +238,7 @@ def load_paste_config(app_name, options, args):
:raises RuntimeError when config file cannot be located or there was a
problem loading the configuration file.
"""
conf_file = find_config_file(options, args)
conf_file = find_config_file(app_name, options, args)
if not conf_file:
raise RuntimeError("Unable to locate any configuration file. "
"Cannot load application %s" % app_name)
@ -257,7 +257,7 @@ def load_paste_app(app_name, options, args):
We search for the paste config file in the following order:
* If --config-file option is used, use that
* If args[0] is a file, use that
* Search for glance.conf in standard directories:
* Search for $app_name.conf in standard directories:
* .
* ~.glance/
* ~

View File

@ -127,23 +127,6 @@ def abspath(s):
return os.path.join(os.path.dirname(__file__), s)
# TODO(sirp): when/if utils is extracted to common library, we should remove
# the argument's default.
#def default_flagfile(filename='nova.conf'):
def default_flagfile(filename='glance.conf'):
for arg in sys.argv:
if arg.find('flagfile') != -1:
break
else:
if not os.path.isabs(filename):
# turn relative filename into an absolute path
script_dir = os.path.dirname(inspect.stack()[-1][1])
filename = os.path.abspath(os.path.join(script_dir, filename))
if os.path.exists(filename):
sys.argv = \
sys.argv[:1] + ['--flagfile=%s' % filename] + sys.argv[1:]
def debug(arg):
logging.debug('debug in callback: %s', arg)
return arg

View File

@ -32,14 +32,14 @@ def get_registry_client(options):
return client.RegistryClient(host, port)
def get_images_list(options):
def get_images_list(options, filters):
c = get_registry_client(options)
return c.get_images()
return c.get_images(filters)
def get_images_detail(options):
def get_images_detail(options, filters):
c = get_registry_client(options)
return c.get_images_detailed()
return c.get_images_detailed(filters)
def get_image_metadata(options, image_id):

View File

@ -20,14 +20,9 @@ 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
import urllib
from glance.common import exception
from glance.client import BaseClient
@ -49,28 +44,24 @@ class RegistryClient(BaseClient):
port = port or self.DEFAULT_PORT
super(RegistryClient, self).__init__(host, port, use_ssl)
def get_images(self):
def get_images(self, filters=None):
"""
Returns a list of image id/name mappings from Registry
"""
res = self.do_request("GET", "/images")
res = self.do_request("GET", "/images", params=filters)
data = json.loads(res.read())['images']
return data
def get_images_detailed(self):
def get_images_detailed(self, filters=None):
"""
Returns a list of detailed image data mappings from Registry
"""
res = self.do_request("GET", "/images/detail")
res = self.do_request("GET", "/images/detail", params=filters)
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
"""
"""Returns a mapping of image metadata from Registry"""
res = self.do_request("GET", "/images/%s" % image_id)
data = json.loads(res.read())['image']
return data

View File

@ -139,15 +139,39 @@ def image_get(context, image_id, session=None):
raise exception.NotFound("No image found with ID %s" % image_id)
def image_get_all_public(context):
"""Get all public images."""
def image_get_all_public(context, filters=None):
"""Get all public images that match zero or more filters.
:param filters: dict of filter keys and values. If a 'properties'
key is present, it is treated as a dict of key/value
filters on the image properties attribute
"""
if filters == None:
filters = {}
session = get_session()
return session.query(models.Image).\
query = session.query(models.Image).\
options(joinedload(models.Image.properties)).\
filter_by(deleted=_deleted(context)).\
filter_by(is_public=True).\
filter_by(status='active').\
all()
filter(models.Image.status != 'killed')
if 'size_min' in filters:
query = query.filter(models.Image.size >= filters['size_min'])
del filters['size_min']
if 'size_max' in filters:
query = query.filter(models.Image.size <= filters['size_max'])
del filters['size_max']
for (k, v) in filters.pop('properties', {}).items():
query = query.filter(models.Image.properties.any(name=k, value=v))
for (k, v) in filters.items():
query = query.filter(getattr(models.Image, k) == v)
return query.all()
def _drop_protected_attrs(model_class, values):

View File

@ -36,6 +36,9 @@ DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size',
'disk_format', 'container_format',
'checksum']
SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
'size_min', 'size_max']
class Controller(wsgi.Controller):
"""Controller for the reference implementation registry server"""
@ -45,7 +48,7 @@ class Controller(wsgi.Controller):
db_api.configure_db(options)
def index(self, req):
"""Return basic information for all public, non-deleted images
"""Return a basic filtered list of public, non-deleted images
:param req: the Request object coming from the wsgi layer
:retval a mapping of the following form::
@ -64,7 +67,8 @@ class Controller(wsgi.Controller):
}
"""
images = db_api.image_get_all_public(None)
images = db_api.image_get_all_public(None, self._get_filters(req))
results = []
for image in images:
result = {}
@ -74,7 +78,7 @@ class Controller(wsgi.Controller):
return dict(images=results)
def detail(self, req):
"""Return detailed information for all public, non-deleted images
"""Return a filtered list of public, non-deleted images in detail
:param req: the Request object coming from the wsgi layer
:retval a mapping of the following form::
@ -85,10 +89,33 @@ class Controller(wsgi.Controller):
all image model fields.
"""
images = db_api.image_get_all_public(None)
images = db_api.image_get_all_public(None, self._get_filters(req))
image_dicts = [make_image_dict(i) for i in images]
return dict(images=image_dicts)
def _get_filters(self, req):
"""Return a dictionary of query param filters from the request
:param req: the Request object coming from the wsgi layer
:retval a dict of key/value filters
"""
filters = {}
properties = {}
for param in req.str_params:
if param in SUPPORTED_FILTERS:
filters[param] = req.str_params.get(param)
if param.startswith('property-'):
_param = param[9:]
properties[_param] = req.str_params.get(param)
if len(properties) > 0:
filters['properties'] = properties
return filters
def show(self, req, id):
"""Return data about the given image id."""
try:

View File

@ -50,16 +50,149 @@ def runs_sql(func):
@functools.wraps(func)
def wrapped(*a, **kwargs):
test_obj = a[0]
orig_sql_connection = test_obj.sql_connection
orig_sql_connection = test_obj.registry_server.sql_connection
try:
if orig_sql_connection.startswith('sqlite'):
test_obj.sql_connection = "sqlite:///tests.sqlite"
test_obj.registry_server.sql_connection =\
"sqlite:///tests.sqlite"
func(*a, **kwargs)
finally:
test_obj.sql_connection = orig_sql_connection
test_obj.registry_server.sql_connection = orig_sql_connection
return wrapped
class Server(object):
"""
Class used to easily manage starting and stopping
a server during functional test runs.
"""
def __init__(self, test_dir, port):
"""
Creates a new Server object.
:param test_dir: The directory where all test stuff is kept. This is
passed from the FunctionalTestCase.
:param port: The port to start a server up on.
"""
self.verbose = True
self.debug = True
self.test_dir = test_dir
self.bind_port = port
self.conf_file = None
self.conf_base = None
def start(self, **kwargs):
"""
Starts the server.
Any kwargs passed to this method will override the configuration
value in the conf file used in starting the servers.
"""
if self.conf_file:
raise RuntimeError("Server configuration file already exists!")
if not self.conf_base:
raise RuntimeError("Subclass did not populate config_base!")
conf_override = self.__dict__.copy()
if kwargs:
conf_override.update(**kwargs)
# A config file to use just for this test...we don't want
# to trample on currently-running Glance servers, now do we?
conf_file = tempfile.NamedTemporaryFile()
conf_file.write(self.conf_base % conf_override)
conf_file.flush()
self.conf_file = conf_file
self.conf_file_name = conf_file.name
cmd = ("./bin/glance-control %(server_name)s start "
"%(conf_file_name)s --pid-file=%(pid_file)s"
% self.__dict__)
return execute(cmd)
def stop(self):
"""
Spin down the server.
"""
cmd = ("./bin/glance-control %(server_name)s stop "
"%(conf_file_name)s --pid-file=%(pid_file)s"
% self.__dict__)
return execute(cmd)
class ApiServer(Server):
"""
Server object that starts/stops/manages the API server
"""
def __init__(self, test_dir, port, registry_port):
super(ApiServer, self).__init__(test_dir, port)
self.server_name = 'api'
self.default_store = 'file'
self.image_dir = os.path.join(self.test_dir,
"images")
self.pid_file = os.path.join(self.test_dir,
"api.pid")
self.log_file = os.path.join(self.test_dir, "api.log")
self.registry_port = registry_port
self.conf_base = """[DEFAULT]
verbose = %(verbose)s
debug = %(debug)s
filesystem_store_datadir=%(image_dir)s
default_store = %(default_store)s
bind_host = 0.0.0.0
bind_port = %(bind_port)s
registry_host = 0.0.0.0
registry_port = %(registry_port)s
log_file = %(log_file)s
[pipeline:glance-api]
pipeline = versionnegotiation apiv1app
[pipeline:versions]
pipeline = versionsapp
[app:versionsapp]
paste.app_factory = glance.api.versions:app_factory
[app:apiv1app]
paste.app_factory = glance.api.v1:app_factory
[filter:versionnegotiation]
paste.filter_factory = glance.api.middleware.version_negotiation:filter_factory
"""
class RegistryServer(Server):
"""
Server object that starts/stops/manages the Registry server
"""
def __init__(self, test_dir, port):
super(RegistryServer, self).__init__(test_dir, port)
self.server_name = 'registry'
self.sql_connection = os.environ.get('GLANCE_TEST_SQL_CONNECTION',
"sqlite://")
self.pid_file = os.path.join(self.test_dir,
"registry.pid")
self.log_file = os.path.join(self.test_dir, "registry.log")
self.conf_base = """[DEFAULT]
verbose = %(verbose)s
debug = %(debug)s
bind_host = 0.0.0.0
bind_port = %(bind_port)s
log_file = %(log_file)s
sql_connection = %(sql_connection)s
sql_idle_timeout = 3600
[app:glance-registry]
paste.app_factory = glance.registry.server:app_factory
"""
class FunctionalTest(unittest.TestCase):
"""
@ -69,28 +202,20 @@ class FunctionalTest(unittest.TestCase):
def setUp(self):
self.verbose = True
self.debug = True
self.default_store = 'file'
self.test_id = random.randint(0, 100000)
self.test_dir = os.path.join("/", "tmp", "test.%d" % self.test_id)
self.api_port = get_unused_port()
self.api_pid_file = os.path.join(self.test_dir,
"glance-api.pid")
self.api_log_file = os.path.join(self.test_dir, "apilog")
self.registry_port = get_unused_port()
self.registry_pid_file = ("/tmp/test.%d/glance-registry.pid"
% self.test_id)
self.registry_log_file = os.path.join(self.test_dir, "registrylog")
self.image_dir = "/tmp/test.%d/images" % self.test_id
self.api_server = ApiServer(self.test_dir,
self.api_port,
self.registry_port)
self.registry_server = RegistryServer(self.test_dir,
self.registry_port)
self.sql_connection = os.environ.get('GLANCE_TEST_SQL_CONNECTION',
"sqlite://")
self.pid_files = [self.api_pid_file,
self.registry_pid_file]
self.pid_files = [self.api_server.pid_file,
self.registry_server.pid_file]
self.files_to_destroy = []
def tearDown(self):
@ -101,7 +226,7 @@ class FunctionalTest(unittest.TestCase):
self._reset_database()
def _reset_database(self):
conn_string = self.sql_connection
conn_string = self.registry_server.sql_connection
conn_pieces = urlparse.urlparse(conn_string)
if conn_string.startswith('sqlite'):
# We can just delete the SQLite database, which is
@ -161,55 +286,15 @@ class FunctionalTest(unittest.TestCase):
"""
self.cleanup()
conf_override = self.__dict__.copy()
if kwargs:
conf_override.update(**kwargs)
# A config file to use just for this test...we don't want
# to trample on currently-running Glance servers, now do we?
conf_file = tempfile.NamedTemporaryFile()
conf_contents = """[DEFAULT]
verbose = %(verbose)s
debug = %(debug)s
[app:glance-api]
paste.app_factory = glance.server:app_factory
filesystem_store_datadir=%(image_dir)s
default_store = %(default_store)s
bind_host = 0.0.0.0
bind_port = %(api_port)s
registry_host = 0.0.0.0
registry_port = %(registry_port)s
log_file = %(api_log_file)s
[app:glance-registry]
paste.app_factory = glance.registry.server:app_factory
bind_host = 0.0.0.0
bind_port = %(registry_port)s
log_file = %(registry_log_file)s
sql_connection = %(sql_connection)s
sql_idle_timeout = 3600
""" % conf_override
conf_file.write(conf_contents)
conf_file.flush()
self.conf_file_name = conf_file.name
# Start up the API and default registry server
cmd = ("./bin/glance-control api start "
"%(conf_file_name)s --pid-file=%(api_pid_file)s"
% self.__dict__)
exitcode, out, err = execute(cmd)
exitcode, out, err = self.api_server.start(**kwargs)
self.assertEqual(0, exitcode,
"Failed to spin up the API server. "
"Got: %s" % err)
self.assertTrue("Starting glance-api with" in out)
cmd = ("./bin/glance-control registry start "
"%(conf_file_name)s --pid-file=%(registry_pid_file)s"
% self.__dict__)
exitcode, out, err = execute(cmd)
exitcode, out, err = self.registry_server.start(**kwargs)
self.assertEqual(0, exitcode,
"Failed to spin up the Registry server. "
@ -218,8 +303,6 @@ sql_idle_timeout = 3600
self.wait_for_servers()
return self.api_port, self.registry_port, self.conf_file_name
def ping_server(self, port):
"""
Simple ping on the port. If responsive, return True, else
@ -265,17 +348,12 @@ sql_idle_timeout = 3600
"""
# Spin down the API and default registry server
cmd = ("./bin/glance-control api stop "
"%(conf_file_name)s --pid-file=%(api_pid_file)s"
% self.__dict__)
exitcode, out, err = execute(cmd)
exitcode, out, err = self.api_server.stop()
self.assertEqual(0, exitcode,
"Failed to spin down the API server. "
"Got: %s" % err)
cmd = ("./bin/glance-control registry stop "
"%(conf_file_name)s --pid-file=%(registry_pid_file)s"
% self.__dict__)
exitcode, out, err = execute(cmd)
exitcode, out, err = self.registry_server.stop()
self.assertEqual(0, exitcode,
"Failed to spin down the Registry server. "
"Got: %s" % err)
@ -292,5 +370,6 @@ sql_idle_timeout = 3600
DB verification within the functional tests.
The raw result set is returned.
"""
engine = create_engine(self.sql_connection, pool_recycle=30)
engine = create_engine(self.registry_server.sql_connection,
pool_recycle=30)
return engine.execute(sql)

View File

@ -42,7 +42,10 @@ class TestBinGlance(functional.FunctionalTest):
"""
self.cleanup()
api_port, reg_port, conf_file = self.start_servers()
self.start_servers()
api_port = self.api_port
registry_port = self.registry_port
# 0. Verify no public images
cmd = "bin/glance --port=%d index" % api_port
@ -105,7 +108,10 @@ class TestBinGlance(functional.FunctionalTest):
"""
self.cleanup()
api_port, reg_port, conf_file = self.start_servers()
self.start_servers()
api_port = self.api_port
registry_port = self.registry_port
# 0. Verify no public images
cmd = "bin/glance --port=%d index" % api_port
@ -192,7 +198,10 @@ class TestBinGlance(functional.FunctionalTest):
# Start servers with a Swift backend and a bad auth URL
options = {'default_store': 'swift',
'swift_store_auth_address': 'badurl'}
api_port, reg_port, conf_file = self.start_servers(**options)
self.start_servers(**options)
api_port = self.api_port
registry_port = self.registry_port
# 0. Verify no public images
cmd = "bin/glance --port=%d index" % api_port
@ -246,7 +255,10 @@ class TestBinGlance(functional.FunctionalTest):
"""
self.cleanup()
api_port, reg_port, conf_file = self.start_servers()
self.start_servers()
api_port = self.api_port
registry_port = self.registry_port
# 1. Add some images
for i in range(1, 5):

View File

@ -58,18 +58,21 @@ class TestCurlApi(functional.FunctionalTest):
- Verify 200 returned
9. GET /images/1
- Verify updated information about image was stored
# 10. PUT /images/1
10. PUT /images/1
- Remove a previously existing property.
# 11. PUT /images/1
11. PUT /images/1
- Add a previously deleted property.
"""
self.cleanup()
api_port, reg_port, conf_file = self.start_servers()
self.start_servers()
api_port = self.api_port
registry_port = self.registry_port
# 0. GET /images
# Verify no public images
cmd = "curl -g http://0.0.0.0:%d/images" % api_port
cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
exitcode, out, err = execute(cmd)
@ -78,7 +81,7 @@ class TestCurlApi(functional.FunctionalTest):
# 1. GET /images/detail
# Verify no public images
cmd = "curl -g http://0.0.0.0:%d/images/detail" % api_port
cmd = "curl http://0.0.0.0:%d/v1/images/detail" % api_port
exitcode, out, err = execute(cmd)
@ -87,7 +90,7 @@ class TestCurlApi(functional.FunctionalTest):
# 2. HEAD /images/1
# Verify 404 returned
cmd = "curl -i -X HEAD http://0.0.0.0:%d/images/1" % api_port
cmd = "curl -i -X HEAD http://0.0.0.0:%d/v1/images/1" % api_port
exitcode, out, err = execute(cmd)
@ -108,7 +111,7 @@ class TestCurlApi(functional.FunctionalTest):
"-H 'X-Image-Meta-Name: Image1' "
"-H 'X-Image-Meta-Is-Public: True' "
"--data-binary \"%s\" "
"http://0.0.0.0:%d/images") % (image_data, api_port)
"http://0.0.0.0:%d/v1/images") % (image_data, api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
@ -120,7 +123,7 @@ class TestCurlApi(functional.FunctionalTest):
# 4. HEAD /images
# Verify image found now
cmd = "curl -i -X HEAD http://0.0.0.0:%d/images/1" % api_port
cmd = "curl -i -X HEAD http://0.0.0.0:%d/v1/images/1" % api_port
exitcode, out, err = execute(cmd)
@ -135,7 +138,7 @@ class TestCurlApi(functional.FunctionalTest):
# 5. GET /images/1
# Verify all information on image we just added is correct
cmd = "curl -i -g http://0.0.0.0:%d/images/1" % api_port
cmd = "curl -i http://0.0.0.0:%d/v1/images/1" % api_port
exitcode, out, err = execute(cmd)
@ -173,7 +176,7 @@ class TestCurlApi(functional.FunctionalTest):
'X-Image-Meta-Disk_format': '',
'X-Image-Meta-Container_format': '',
'X-Image-Meta-Size': str(FIVE_KB),
'X-Image-Meta-Location': 'file://%s/1' % self.image_dir}
'X-Image-Meta-Location': 'file://%s/1' % self.api_server.image_dir}
expected_std_headers = {
'Content-Length': str(FIVE_KB),
@ -209,7 +212,7 @@ class TestCurlApi(functional.FunctionalTest):
# 6. GET /images
# Verify no public images
cmd = "curl -g http://0.0.0.0:%d/images" % api_port
cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
exitcode, out, err = execute(cmd)
@ -226,7 +229,7 @@ class TestCurlApi(functional.FunctionalTest):
# 7. GET /images/detail
# Verify image and all its metadata
cmd = "curl -g http://0.0.0.0:%d/images/detail" % api_port
cmd = "curl http://0.0.0.0:%d/v1/images/detail" % api_port
exitcode, out, err = execute(cmd)
@ -239,7 +242,7 @@ class TestCurlApi(functional.FunctionalTest):
"container_format": None,
"disk_format": None,
"id": 1,
"location": "file://%s/1" % self.image_dir,
"location": "file://%s/1" % self.api_server.image_dir,
"is_public": True,
"deleted_at": None,
"properties": {},
@ -263,7 +266,7 @@ class TestCurlApi(functional.FunctionalTest):
cmd = ("curl -i -X PUT "
"-H 'X-Image-Meta-Property-Distro: Ubuntu' "
"-H 'X-Image-Meta-Property-Arch: x86_64' "
"http://0.0.0.0:%d/images/1") % api_port
"http://0.0.0.0:%d/v1/images/1") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
@ -275,7 +278,7 @@ class TestCurlApi(functional.FunctionalTest):
# 9. GET /images/detail
# Verify image and all its metadata
cmd = "curl -g http://0.0.0.0:%d/images/detail" % api_port
cmd = "curl http://0.0.0.0:%d/v1/images/detail" % api_port
exitcode, out, err = execute(cmd)
@ -288,7 +291,7 @@ class TestCurlApi(functional.FunctionalTest):
"container_format": None,
"disk_format": None,
"id": 1,
"location": "file://%s/1" % self.image_dir,
"location": "file://%s/1" % self.api_server.image_dir,
"is_public": True,
"deleted_at": None,
"properties": {'distro': 'Ubuntu', 'arch': 'x86_64'},
@ -309,7 +312,7 @@ class TestCurlApi(functional.FunctionalTest):
# 10. PUT /images/1 and remove a previously existing property.
cmd = ("curl -i -X PUT "
"-H 'X-Image-Meta-Property-Arch: x86_64' "
"http://0.0.0.0:%d/images/1") % api_port
"http://0.0.0.0:%d/v1/images/1") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
@ -319,7 +322,7 @@ class TestCurlApi(functional.FunctionalTest):
self.assertEqual("HTTP/1.1 200 OK", status_line)
cmd = "curl -g http://0.0.0.0:%d/images/detail" % api_port
cmd = "curl http://0.0.0.0:%d/v1/images/detail" % api_port
exitcode, out, err = execute(cmd)
@ -333,7 +336,7 @@ class TestCurlApi(functional.FunctionalTest):
cmd = ("curl -i -X PUT "
"-H 'X-Image-Meta-Property-Distro: Ubuntu' "
"-H 'X-Image-Meta-Property-Arch: x86_64' "
"http://0.0.0.0:%d/images/1") % api_port
"http://0.0.0.0:%d/v1/images/1") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
@ -343,7 +346,7 @@ class TestCurlApi(functional.FunctionalTest):
self.assertEqual("HTTP/1.1 200 OK", status_line)
cmd = "curl -g http://0.0.0.0:%d/images/detail" % api_port
cmd = "curl http://0.0.0.0:%d/v1/images/detail" % api_port
exitcode, out, err = execute(cmd)
@ -356,6 +359,331 @@ class TestCurlApi(functional.FunctionalTest):
self.stop_servers()
def test_queued_process_flow(self):
"""
We test the process flow where a user registers an image
with Glance but does not immediately upload an image file.
Later, the user uploads an image file using a PUT operation.
We track the changing of image status throughout this process.
0. GET /images
- Verify no public images
1. POST /images with public image named Image1 with no location
attribute and no image data.
- Verify 201 returned
2. GET /images
- Verify one public image
3. HEAD /images/1
- Verify image now in queued status
4. PUT /images/1 with image data
- Verify 200 returned
5. HEAD /images/1
- Verify image now in active status
6. GET /images
- Verify one public image
"""
self.cleanup()
self.start_servers()
api_port = self.api_port
registry_port = self.registry_port
# 0. GET /images
# Verify no public images
cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual('{"images": []}', out.strip())
# 1. POST /images with public image named Image1
# with no location or image data
cmd = ("curl -i -X POST "
"-H 'Expect: ' " # Necessary otherwise sends 100 Continue
"-H 'X-Image-Meta-Name: Image1' "
"-H 'X-Image-Meta-Is-Public: True' "
"http://0.0.0.0:%d/v1/images") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 201 Created", status_line)
# 2. GET /images
# Verify 1 public image
cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
image = json.loads(out.strip())['images'][0]
expected = {"name": "Image1",
"container_format": None,
"disk_format": None,
"checksum": None,
"id": 1,
"size": 0}
self.assertEqual(expected, image)
# 3. HEAD /images
# Verify status is in queued
cmd = "curl -i -X HEAD http://0.0.0.0:%d/v1/images/1" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 200 OK", status_line)
self.assertTrue("X-Image-Meta-Name: Image1" in out)
self.assertTrue("X-Image-Meta-Status: queued" in out)
# 4. PUT /images/1 with image data, verify 200 returned
image_data = "*" * FIVE_KB
cmd = ("curl -i -X PUT "
"-H 'Expect: ' " # Necessary otherwise sends 100 Continue
"-H 'Content-Type: application/octet-stream' "
"--data-binary \"%s\" "
"http://0.0.0.0:%d/v1/images/1") % (image_data, api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 200 OK", status_line)
# 5. HEAD /images
# Verify status is in active
cmd = "curl -i -X HEAD http://0.0.0.0:%d/v1/images/1" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 200 OK", status_line)
self.assertTrue("X-Image-Meta-Name: Image1" in out)
self.assertTrue("X-Image-Meta-Status: active" in out)
# 6. GET /images
# Verify 1 public image still...
cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
image = json.loads(out.strip())['images'][0]
expected = {"name": "Image1",
"container_format": None,
"disk_format": None,
"checksum": 'c2e5db72bd7fd153f53ede5da5a06de3',
"id": 1,
"size": 5120}
self.assertEqual(expected, image)
def test_version_variations(self):
"""
We test that various calls to the images and root endpoints are
handled properly, and that usage of the Accept: header does
content negotiation properly.
"""
self.cleanup()
self.start_servers()
api_port = self.api_port
registry_port = self.registry_port
versions = {'versions': [{
"id": "v1.0",
"status": "CURRENT",
"links": [{
"rel": "self",
"href": "http://0.0.0.0:%d/v1/" % api_port}]}]}
versions_json = json.dumps(versions)
images = {'images': []}
images_json = json.dumps(images)
def validate_versions(response_text):
"""
Returns True if supplied response text contains an
appropriate 300 Multiple Choices and has the correct
versions output.
"""
status_line = response_text.split("\r\n")[0]
body = response_text[response_text.index("\r\n\r\n") + 1:].strip()
return ("HTTP/1.1 300 Multiple Choices" == status_line
and versions_json == body)
# 0. GET / with no Accept: header
# Verify version choices returned.
cmd = "curl -i http://0.0.0.0:%d/" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue(validate_versions(out))
# 1. GET /images with no Accept: header
# Verify version choices returned.
cmd = "curl -i http://0.0.0.0:%d/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue(validate_versions(out))
# 2. GET /v1/images with no Accept: header
# Verify empty images list returned.
cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual(images_json, out.strip())
# 3. GET / with Accept: unknown header
# Verify version choices returned. Verify message in API log about
# unknown accept header.
cmd = "curl -i -H 'Accept: unknown' http://0.0.0.0:%d/" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue(validate_versions(out))
self.assertTrue('Unknown accept header'
in open(self.api_server.log_file).read())
# 5. GET / with an Accept: application/vnd.openstack.images-v1
# Verify empty image list returned
cmd = ("curl -H 'Accept: application/vnd.openstack.images-v1' "
"http://0.0.0.0:%d/images") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual(images_json, out.strip())
# 5. GET /images with a Accept: application/vnd.openstack.compute-v1
# header. Verify version choices returned. Verify message in API log
# about unknown accept header.
cmd = ("curl -i -H 'Accept: application/vnd.openstack.compute-v1' "
"http://0.0.0.0:%d/images") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue(validate_versions(out))
api_log_text = open(self.api_server.log_file).read()
self.assertTrue('Unknown accept header' in api_log_text)
# 6. GET /v1.0/images with no Accept: header
# Verify empty image list returned
cmd = "curl http://0.0.0.0:%d/v1.0/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual(images_json, out.strip())
# 7. GET /v1.a/images with no Accept: header
# Verify empty image list returned
cmd = "curl http://0.0.0.0:%d/v1.a/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual(images_json, out.strip())
# 8. GET /va.1/images with no Accept: header
# Verify version choices returned
cmd = "curl -i http://0.0.0.0:%d/va.1/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue(validate_versions(out))
# 9. GET /versions with no Accept: header
# Verify version choices returned
cmd = "curl -i http://0.0.0.0:%d/versions" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue(validate_versions(out))
# 10. GET /versions with a Accept: application/vnd.openstack.images-v1
# header. Verify version choices returned.
cmd = ("curl -i -H 'Accept: application/vnd.openstack.images-v1' "
"http://0.0.0.0:%d/versions") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue(validate_versions(out))
# 11. GET /v1/versions with no Accept: header
# Verify 404 returned
cmd = "curl -i http://0.0.0.0:%d/v1/versions" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
status_line = out.split("\r\n")[0]
self.assertEquals("HTTP/1.1 404 Not Found", status_line)
# 12. GET /v2/versions with no Accept: header
# Verify version choices returned
cmd = "curl -i http://0.0.0.0:%d/v2/versions" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue(validate_versions(out))
# 13. GET /images with a Accept: application/vnd.openstack.compute-v2
# header. Verify version choices returned. Verify message in API log
# about unknown version in accept header.
cmd = ("curl -i -H 'Accept: application/vnd.openstack.images-v2' "
"http://0.0.0.0:%d/images") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue(validate_versions(out))
api_log_text = open(self.api_server.log_file).read()
self.assertTrue('Unknown version in accept header' in api_log_text)
# 14. GET /v1.2/images with no Accept: header
# Verify version choices returned
cmd = "curl -i http://0.0.0.0:%d/v1.2/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue(validate_versions(out))
api_log_text = open(self.api_server.log_file).read()
self.assertTrue('Unknown version in versioned URI' in api_log_text)
self.stop_servers()
def test_size_greater_2G_mysql(self):
"""
A test against the actual datastore backend for the registry
@ -365,7 +693,10 @@ class TestCurlApi(functional.FunctionalTest):
"""
self.cleanup()
api_port, reg_port, conf_file = self.start_servers()
self.start_servers()
api_port = self.api_port
registry_port = self.registry_port
# 1. POST /images with public image named Image1
# attribute and a size of 5G. Use the HTTP engine with an
@ -378,7 +709,7 @@ class TestCurlApi(functional.FunctionalTest):
"-H 'X-Image-Meta-Size: %d' "
"-H 'X-Image-Meta-Name: Image1' "
"-H 'X-Image-Meta-Is-Public: True' "
"http://0.0.0.0:%d/images") % (FIVE_GB, api_port)
"http://0.0.0.0:%d/v1/images") % (FIVE_GB, api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
@ -398,6 +729,8 @@ class TestCurlApi(functional.FunctionalTest):
self.assertTrue(new_image_uri is not None,
"Could not find a new image URI!")
self.assertTrue("v1/images" in new_image_uri,
"v1/images not in %s" % new_image_uri)
# 2. HEAD /images
# Verify image size is what was passed in, and not truncated
@ -427,7 +760,10 @@ class TestCurlApi(functional.FunctionalTest):
"""
self.cleanup()
api_port, reg_port, conf_file = self.start_servers()
self.start_servers()
api_port = self.api_port
registry_port = self.registry_port
# POST /images with binary data, but not setting
# Content-Type to application/octet-stream, verify a
@ -436,7 +772,7 @@ class TestCurlApi(functional.FunctionalTest):
test_data_file.write("XXX")
test_data_file.flush()
cmd = ("curl -i -X POST --upload-file %s "
"http://0.0.0.0:%d/images") % (test_data_file.name,
"http://0.0.0.0:%d/v1/images") % (test_data_file.name,
api_port)
exitcode, out, err = execute(cmd)
@ -451,3 +787,213 @@ class TestCurlApi(functional.FunctionalTest):
"Could not find '%s' in '%s'" % (expected, out))
self.stop_servers()
def test_filtered_images(self):
"""
Set up three test images and ensure each query param filter works
"""
self.cleanup()
self.start_servers()
api_port = self.api_port
registry_port = self.registry_port
# 0. GET /images
# Verify no public images
cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual('{"images": []}', out.strip())
# 1. POST /images with three public images with various attributes
cmd = ("curl -i -X POST "
"-H 'Expect: ' " # Necessary otherwise sends 100 Continue
"-H 'X-Image-Meta-Name: Image1' "
"-H 'X-Image-Meta-Status: active' "
"-H 'X-Image-Meta-Container-Format: ovf' "
"-H 'X-Image-Meta-Disk-Format: vdi' "
"-H 'X-Image-Meta-Size: 19' "
"-H 'X-Image-Meta-Is-Public: True' "
"-H 'X-Image-Meta-Property-pants: are on' "
"http://0.0.0.0:%d/v1/images") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 201 Created", status_line)
cmd = ("curl -i -X POST "
"-H 'Expect: ' " # Necessary otherwise sends 100 Continue
"-H 'X-Image-Meta-Name: My Image!' "
"-H 'X-Image-Meta-Status: active' "
"-H 'X-Image-Meta-Container-Format: ovf' "
"-H 'X-Image-Meta-Disk-Format: vhd' "
"-H 'X-Image-Meta-Size: 20' "
"-H 'X-Image-Meta-Is-Public: True' "
"-H 'X-Image-Meta-Property-pants: are on' "
"http://0.0.0.0:%d/v1/images") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 201 Created", status_line)
cmd = ("curl -i -X POST "
"-H 'Expect: ' " # Necessary otherwise sends 100 Continue
"-H 'X-Image-Meta-Name: My Image!' "
"-H 'X-Image-Meta-Status: saving' "
"-H 'X-Image-Meta-Container-Format: ami' "
"-H 'X-Image-Meta-Disk-Format: ami' "
"-H 'X-Image-Meta-Size: 21' "
"-H 'X-Image-Meta-Is-Public: True' "
"-H 'X-Image-Meta-Property-pants: are off' "
"http://0.0.0.0:%d/v1/images") % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\r\n")
status_line = lines[0]
self.assertEqual("HTTP/1.1 201 Created", status_line)
# 2. GET /images
# Verify three public images
cmd = "curl http://0.0.0.0:%d/v1/images" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
images = json.loads(out.strip())
self.assertEqual(len(images["images"]), 3)
# 3. GET /images with name filter
# Verify correct images returned with name
cmd = "curl http://0.0.0.0:%d/v1/images?name=My%%20Image!" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
images = json.loads(out.strip())
self.assertEqual(len(images["images"]), 2)
for image in images["images"]:
self.assertEqual(image["name"], "My Image!")
# 4. GET /images with status filter
# Verify correct images returned with status
cmd = ("curl http://0.0.0.0:%d/v1/images/detail?status=queued"
% api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
images = json.loads(out.strip())
self.assertEqual(len(images["images"]), 3)
for image in images["images"]:
self.assertEqual(image["status"], "queued")
cmd = ("curl http://0.0.0.0:%d/v1/images/detail?status=active"
% api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
images = json.loads(out.strip())
self.assertEqual(len(images["images"]), 0)
# 5. GET /images with container_format filter
# Verify correct images returned with container_format
cmd = ("curl http://0.0.0.0:%d/v1/images?container_format=ovf"
% api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
images = json.loads(out.strip())
self.assertEqual(len(images["images"]), 2)
for image in images["images"]:
self.assertEqual(image["container_format"], "ovf")
# 6. GET /images with disk_format filter
# Verify correct images returned with disk_format
cmd = ("curl http://0.0.0.0:%d/v1/images?disk_format=vdi"
% api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
images = json.loads(out.strip())
self.assertEqual(len(images["images"]), 1)
for image in images["images"]:
self.assertEqual(image["disk_format"], "vdi")
# 7. GET /images with size_max filter
# Verify correct images returned with size <= expected
cmd = ("curl http://0.0.0.0:%d/v1/images?size_max=20"
% api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
images = json.loads(out.strip())
self.assertEqual(len(images["images"]), 2)
for image in images["images"]:
self.assertTrue(image["size"] <= 20)
# 8. GET /images with size_min filter
# Verify correct images returned with size >= expected
cmd = ("curl http://0.0.0.0:%d/v1/images?size_min=20"
% api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
images = json.loads(out.strip())
self.assertEqual(len(images["images"]), 2)
for image in images["images"]:
self.assertTrue(image["size"] >= 20)
# 9. GET /images with property filter
# Verify correct images returned with property
cmd = ("curl http://0.0.0.0:%d/v1/images/detail?"
"property-pants=are%%20on" % api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
images = json.loads(out.strip())
self.assertEqual(len(images["images"]), 2)
for image in images["images"]:
self.assertEqual(image["properties"]["pants"], "are on")
# 10. GET /images with property filter and name filter
# Verify correct images returned with property and name
# Make sure you quote the url when using more than one param!
cmd = ("curl 'http://0.0.0.0:%d/v1/images/detail?"
"name=My%%20Image!&property-pants=are%%20on'" % api_port)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
images = json.loads(out.strip())
self.assertEqual(len(images["images"]), 1)
for image in images["images"]:
self.assertEqual(image["properties"]["pants"], "are on")
self.assertEqual(image["name"], "My Image!")

View File

@ -35,21 +35,21 @@ class TestLogging(functional.FunctionalTest):
"""
self.cleanup()
api_port, reg_port, conf_file = self.start_servers()
self.start_servers()
# The default functional test case has both verbose
# and debug on. Let's verify that debug statements
# appear in both the API and registry logs.
self.assertTrue(os.path.exists(self.api_log_file))
self.assertTrue(os.path.exists(self.api_server.log_file))
api_log_out = open(self.api_log_file, 'r').read()
api_log_out = open(self.api_server.log_file, 'r').read()
self.assertTrue('DEBUG [glance-api]' in api_log_out)
self.assertTrue(os.path.exists(self.registry_log_file))
self.assertTrue(os.path.exists(self.registry_server.log_file))
registry_log_out = open(self.registry_log_file, 'r').read()
registry_log_out = open(self.registry_server.log_file, 'r').read()
self.assertTrue('DEBUG [glance-registry]' in registry_log_out)
@ -62,18 +62,17 @@ class TestLogging(functional.FunctionalTest):
"""
self.cleanup()
api_port, reg_port, conf_file = self.start_servers(debug=False,
verbose=False)
self.start_servers(debug=False, verbose=False)
self.assertTrue(os.path.exists(self.api_log_file))
self.assertTrue(os.path.exists(self.api_server.log_file))
api_log_out = open(self.api_log_file, 'r').read()
api_log_out = open(self.api_server.log_file, 'r').read()
self.assertFalse('DEBUG [glance-api]' in api_log_out)
self.assertTrue(os.path.exists(self.registry_log_file))
self.assertTrue(os.path.exists(self.registry_server.log_file))
registry_log_out = open(self.registry_log_file, 'r').read()
registry_log_out = open(self.registry_server.log_file, 'r').read()
self.assertFalse('DEBUG [glance-registry]' in registry_log_out)

View File

@ -41,9 +41,12 @@ class TestMiscellaneous(functional.FunctionalTest):
"""
self.cleanup()
api_port, reg_port, conf_file = self.start_servers()
self.start_servers()
cmd = "curl -g http://0.0.0.0:%d/images" % api_port
api_port = self.api_port
registry_port = self.registry_port
cmd = "curl -g http://0.0.0.0:%d/v1/images" % api_port
exitcode, out, err = execute(cmd)
@ -53,7 +56,7 @@ class TestMiscellaneous(functional.FunctionalTest):
cmd = "curl -X POST -H 'Content-Type: application/octet-stream' "\
"-H 'X-Image-Meta-Name: ImageName' "\
"-H 'X-Image-Meta-Disk-Format: Invalid' "\
"http://0.0.0.0:%d/images" % api_port
"http://0.0.0.0:%d/v1/images" % api_port
ignored, out, err = execute(cmd)
self.assertTrue('Invalid disk format' in out,

View File

@ -29,7 +29,7 @@ import webob
from glance.common import exception
from glance.registry import server as rserver
from glance import server
from glance.api import v1 as server
import glance.store
import glance.store.filesystem
import glance.store.http
@ -386,9 +386,32 @@ def stub_out_registry_db_image_api(stubs):
else:
return images[0]
def image_get_all_public(self, _context, public=True):
return [f for f in self.images
if f['is_public'] == public]
def image_get_all_public(self, _context, filters):
images = [f for f in self.images if f['is_public'] == True]
if 'size_min' in filters:
size_min = int(filters.pop('size_min'))
images = [f for f in images if int(f['size']) >= size_min]
if 'size_max' in filters:
size_max = int(filters.pop('size_max'))
images = [f for f in images if int(f['size']) <= size_max]
def _prop_filter(key, value):
def _func(image):
for prop in image['properties']:
if prop['name'] == key:
return prop['value'] == value
return False
return _func
for k, v in filters.pop('properties', {}).items():
images = filter(_prop_filter(k, v), images)
for k, v in filters.items():
images = [f for f in images if f[k] == v]
return images
fake_datastore = FakeDatastore()
stubs.Set(glance.registry.db.api, 'image_create',

View File

@ -24,8 +24,9 @@ import unittest
import stubout
import webob
from glance import server
from glance.api import v1 as server
from glance.registry import server as rserver
import glance.registry.db.api
from tests import stubs
VERBOSE = False
@ -87,6 +88,50 @@ class TestRegistryAPI(unittest.TestCase):
for k, v in fixture.iteritems():
self.assertEquals(v, images[0][k])
def test_get_index_filter_name(self):
"""Tests that the /images registry API returns list of
public images that have a specific name. This is really a sanity
check, filtering is tested more in-depth using /images/detail
"""
fixture = {'id': 2,
'name': 'fake image #2',
'size': 19,
'checksum': None}
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
extra_fixture = {'id': 4,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 20,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
req = webob.Request.blank('/images?name=new name! #123')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
images = res_dict['images']
self.assertEquals(len(images), 2)
for image in images:
self.assertEqual('new name! #123', image['name'])
def test_get_details(self):
"""Tests that the /images/detail registry API returns
a mapping containing a list of detailed image information
@ -112,6 +157,324 @@ class TestRegistryAPI(unittest.TestCase):
for k, v in fixture.iteritems():
self.assertEquals(v, images[0][k])
def test_get_details_filter_name(self):
"""Tests that the /images/detail registry API returns list of
public images that have a specific name
"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
extra_fixture = {'id': 4,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 20,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
req = webob.Request.blank('/images/detail?name=new name! #123')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
images = res_dict['images']
self.assertEquals(len(images), 2)
for image in images:
self.assertEqual('new name! #123', image['name'])
def test_get_details_filter_status(self):
"""Tests that the /images/detail registry API returns list of
public images that have a specific status
"""
extra_fixture = {'id': 3,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'fake image #3',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
extra_fixture = {'id': 4,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'fake image #4',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
req = webob.Request.blank('/images/detail?status=saving')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
images = res_dict['images']
self.assertEquals(len(images), 1)
for image in images:
self.assertEqual('saving', image['status'])
def test_get_details_filter_container_format(self):
"""Tests that the /images/detail registry API returns list of
public images that have a specific container_format
"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vdi',
'container_format': 'ovf',
'name': 'fake image #3',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
extra_fixture = {'id': 4,
'status': 'active',
'is_public': True,
'disk_format': 'ami',
'container_format': 'ami',
'name': 'fake image #4',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
req = webob.Request.blank('/images/detail?container_format=ovf')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
images = res_dict['images']
self.assertEquals(len(images), 2)
for image in images:
self.assertEqual('ovf', image['container_format'])
def test_get_details_filter_disk_format(self):
"""Tests that the /images/detail registry API returns list of
public images that have a specific disk_format
"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'fake image #3',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
extra_fixture = {'id': 4,
'status': 'active',
'is_public': True,
'disk_format': 'ami',
'container_format': 'ami',
'name': 'fake image #4',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
req = webob.Request.blank('/images/detail?disk_format=vhd')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
images = res_dict['images']
self.assertEquals(len(images), 2)
for image in images:
self.assertEqual('vhd', image['disk_format'])
def test_get_details_filter_size_min(self):
"""Tests that the /images/detail registry API returns list of
public images that have a size greater than or equal to size_min
"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'fake image #3',
'size': 18,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
extra_fixture = {'id': 4,
'status': 'active',
'is_public': True,
'disk_format': 'ami',
'container_format': 'ami',
'name': 'fake image #4',
'size': 20,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
req = webob.Request.blank('/images/detail?size_min=19')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
images = res_dict['images']
self.assertEquals(len(images), 2)
for image in images:
self.assertTrue(image['size'] >= 19)
def test_get_details_filter_size_max(self):
"""Tests that the /images/detail registry API returns list of
public images that have a size less than or equal to size_max
"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'fake image #3',
'size': 18,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
extra_fixture = {'id': 4,
'status': 'active',
'is_public': True,
'disk_format': 'ami',
'container_format': 'ami',
'name': 'fake image #4',
'size': 20,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
req = webob.Request.blank('/images/detail?size_max=19')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
images = res_dict['images']
self.assertEquals(len(images), 2)
for image in images:
self.assertTrue(image['size'] <= 19)
def test_get_details_filter_size_min_max(self):
"""Tests that the /images/detail registry API returns list of
public images that have a size less than or equal to size_max
and greater than or equal to size_min
"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'fake image #3',
'size': 18,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
extra_fixture = {'id': 4,
'status': 'active',
'is_public': True,
'disk_format': 'ami',
'container_format': 'ami',
'name': 'fake image #4',
'size': 20,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
extra_fixture = {'id': 5,
'status': 'active',
'is_public': True,
'disk_format': 'ami',
'container_format': 'ami',
'name': 'fake image #5',
'size': 6,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
req = webob.Request.blank('/images/detail?size_min=18&size_max=19')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
images = res_dict['images']
self.assertEquals(len(images), 2)
for image in images:
self.assertTrue(image['size'] <= 19 and image['size'] >= 18)
def test_get_details_filter_property(self):
"""Tests that the /images/detail registry API returns list of
public images that have a specific custom property
"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'fake image #3',
'size': 19,
'checksum': None,
'properties': {'prop_123': 'v a'}}
glance.registry.db.api.image_create(None, extra_fixture)
extra_fixture = {'id': 4,
'status': 'active',
'is_public': True,
'disk_format': 'ami',
'container_format': 'ami',
'name': 'fake image #4',
'size': 19,
'checksum': None,
'properties': {'prop_123': 'v b'}}
glance.registry.db.api.image_create(None, extra_fixture)
req = webob.Request.blank('/images/detail?property-prop_123=v%20a')
res = req.get_response(self.api)
res_dict = json.loads(res.body)
self.assertEquals(res.status_int, 200)
images = res_dict['images']
self.assertEquals(len(images), 1)
for image in images:
self.assertEqual('v a', image['properties']['prop_123'])
def test_create_image(self):
"""Tests that the /images POST registry API creates the image"""
fixture = {'name': 'fake public image',

View File

@ -24,8 +24,9 @@ import unittest
import webob
from glance import client
from glance.registry import client as rclient
from glance.common import exception
import glance.registry.db.api
from glance.registry import client as rclient
from tests import stubs
@ -69,6 +70,26 @@ class TestRegistryClient(unittest.TestCase):
for k, v in fixture.items():
self.assertEquals(v, images[0][k])
def test_get_image_index_by_name(self):
"""Test correct set of public, name-filtered image returned. This
is just a sanity check, we test the details call more in-depth."""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images({'name': 'new name! #123'})
self.assertEquals(len(images), 1)
for image in images:
self.assertEquals('new name! #123', image['name'])
def test_get_image_details(self):
"""Tests that the detailed info about public images returned"""
fixture = {'id': 2,
@ -87,6 +108,140 @@ class TestRegistryClient(unittest.TestCase):
for k, v in fixture.items():
self.assertEquals(v, images[0][k])
def test_get_image_details_by_name(self):
"""Tests that a detailed call can be filtered by name"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images_detailed({'name': 'new name! #123'})
self.assertEquals(len(images), 1)
for image in images:
self.assertEquals('new name! #123', image['name'])
def test_get_image_details_by_status(self):
"""Tests that a detailed call can be filtered by status"""
extra_fixture = {'id': 3,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images_detailed({'status': 'saving'})
self.assertEquals(len(images), 1)
for image in images:
self.assertEquals('saving', image['status'])
def test_get_image_details_by_container_format(self):
"""Tests that a detailed call can be filtered by container_format"""
extra_fixture = {'id': 3,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images_detailed({'container_format': 'ovf'})
self.assertEquals(len(images), 2)
for image in images:
self.assertEquals('ovf', image['container_format'])
def test_get_image_details_by_disk_format(self):
"""Tests that a detailed call can be filtered by disk_format"""
extra_fixture = {'id': 3,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images_detailed({'disk_format': 'vhd'})
self.assertEquals(len(images), 2)
for image in images:
self.assertEquals('vhd', image['disk_format'])
def test_get_image_details_with_maximum_size(self):
"""Tests that a detailed call can be filtered by size_max"""
extra_fixture = {'id': 3,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 21,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images_detailed({'size_max': 20})
self.assertEquals(len(images), 1)
for image in images:
self.assertTrue(image['size'] <= 20)
def test_get_image_details_with_minimum_size(self):
"""Tests that a detailed call can be filtered by size_min"""
extra_fixture = {'id': 3,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 20,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images_detailed({'size_min': 20})
self.assertEquals(len(images), 1)
for image in images:
self.assertTrue(image['size'] >= 20)
def test_get_image_details_by_property(self):
"""Tests that a detailed call can be filtered by a property"""
extra_fixture = {'id': 3,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None,
'properties': {'p a': 'v a'}}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images_detailed({'property-p a': 'v a'})
self.assertEquals(len(images), 1)
for image in images:
self.assertEquals('v a', image['properties']['p a'])
def test_get_image(self):
"""Tests that the detailed info about an image returned"""
fixture = {'id': 1,
@ -256,7 +411,7 @@ class TestClient(unittest.TestCase):
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("0.0.0.0")
self.client = client.Client("0.0.0.0", doc_root="")
def tearDown(self):
"""Clear the test environment"""
@ -302,6 +457,43 @@ class TestClient(unittest.TestCase):
for k, v in fixture.items():
self.assertEquals(v, images[0][k])
def test_get_image_index_by_base_attribute(self):
"""Tests that an index call can be filtered by a base attribute"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images({'name': 'new name! #123'})
self.assertEquals(len(images), 1)
self.assertEquals('new name! #123', images[0]['name'])
def test_get_image_index_by_property(self):
"""Tests that an index call can be filtered by a property"""
extra_fixture = {'id': 3,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None,
'properties': {'p a': 'v a'}}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images({'property-p a': 'v a'})
self.assertEquals(len(images), 1)
self.assertEquals(3, images[0]['id'])
def test_get_image_details(self):
"""Tests that the detailed info about public images returned"""
fixture = {'id': 2,
@ -330,6 +522,45 @@ class TestClient(unittest.TestCase):
for k, v in expected.items():
self.assertEquals(v, images[0][k])
def test_get_image_details_by_base_attribute(self):
"""Tests that a detailed call can be filtered by a base attribute"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images_detailed({'name': 'new name! #123'})
self.assertEquals(len(images), 1)
for image in images:
self.assertEquals('new name! #123', image['name'])
def test_get_image_details_by_property(self):
"""Tests that a detailed call can be filtered by a property"""
extra_fixture = {'id': 3,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None,
'properties': {'p a': 'v a'}}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images_detailed({'property-p a': 'v a'})
self.assertEquals(len(images), 1)
for image in images:
self.assertEquals('v a', image['properties']['p a'])
def test_get_image_meta(self):
"""Tests that the detailed info about an image returned"""
fixture = {'id': 2,

77
tests/unit/test_misc.py Normal file
View File

@ -0,0 +1,77 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 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 unittest
def parse_mailmap(mailmap='.mailmap'):
mapping = {}
if os.path.exists(mailmap):
fp = open(mailmap, 'r')
for l in fp:
l = l.strip()
if not l.startswith('#') and ' ' in l:
canonical_email, alias = l.split(' ')
mapping[alias] = canonical_email
return mapping
def str_dict_replace(s, mapping):
for s1, s2 in mapping.iteritems():
s = s.replace(s1, s2)
return s
class AuthorsTestCase(unittest.TestCase):
def test_authors_up_to_date(self):
topdir = os.path.normpath(os.path.dirname(__file__) + '/../../')
if os.path.exists(os.path.join(topdir, '.bzr')):
contributors = set()
mailmap = parse_mailmap(os.path.join(topdir, '.mailmap'))
import bzrlib.workingtree
tree = bzrlib.workingtree.WorkingTree.open(topdir)
tree.lock_read()
try:
parents = tree.get_parent_ids()
g = tree.branch.repository.get_graph()
for p in parents:
rev_ids = [r for r, _ in g.iter_ancestry(parents)
if r != "null:"]
revs = tree.branch.repository.get_revisions(rev_ids)
for r in revs:
for author in r.get_apparent_authors():
email = author.split(' ')[-1]
mailmapped = str_dict_replace(email, mailmap)
contributors.add(mailmapped)
authors_file = open(os.path.join(topdir, 'Authors'),
'r').read()
missing = set()
for contributor in contributors:
if contributor == 'glance-core':
continue
if not contributor in authors_file:
missing.add(contributor)
self.assertTrue(len(missing) == 0,
'%r not listed in Authors' % missing)
finally:
tree.unlock()

View File

@ -0,0 +1,50 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 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 webob
from glance.api import versions
class VersionsTest(unittest.TestCase):
def setUp(self):
super(VersionsTest, self).setUp()
def tearDown(self):
super(VersionsTest, self).tearDown()
def test_get_version_list(self):
req = webob.Request.blank('/')
req.accept = "application/json"
options = {'bind_host': '0.0.0.0',
'bind_port': 9292}
res = req.get_response(versions.Controller(options))
self.assertEqual(res.status_int, 300)
self.assertEqual(res.content_type, "application/json")
results = json.loads(res.body)["versions"]
expected = [
{
"id": "v1.0",
"status": "CURRENT",
"links": [
{
"rel": "self",
"href": "http://0.0.0.0:9292/v1/"}]}]
self.assertEqual(results, expected)

View File

@ -15,3 +15,4 @@ mox==0.5.0
swift
-f http://pymox.googlecode.com/files/mox-0.5.0.tar.gz
sqlalchemy-migrate>=0.6
bzr