Merge trunk
This commit is contained in:
commit
ef6f306d72
11
.mailmap
Normal file
11
.mailmap
Normal 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
20
Authors
Normal 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>
|
@ -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:"
|
||||
|
@ -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)]
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
33
etc/glance-registry.conf
Normal 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
16
glance/api/__init__.py
Normal 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.
|
16
glance/api/middleware/__init__.py
Normal file
16
glance/api/middleware/__init__.py
Normal 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.
|
134
glance/api/middleware/version_negotiation.py
Normal file
134
glance/api/middleware/version_negotiation.py
Normal 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
48
glance/api/v1/__init__.py
Normal 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)
|
@ -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
69
glance/api/versions.py
Normal 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)
|
@ -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
|
||||
|
@ -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/
|
||||
* ~
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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!")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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
77
tests/unit/test_misc.py
Normal 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()
|
50
tests/unit/test_versions.py
Normal file
50
tests/unit/test_versions.py
Normal 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)
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user