Merge latest with latest Glance.

This commit is contained in:
Donal Lafferty
2011-05-17 10:47:38 +01:00
36 changed files with 1546 additions and 381 deletions

View File

@@ -1,6 +1,6 @@
*.pyc
glance.egg-info
glance.sqlite
tests.sqlite
*.glance-venv
dist/
ChangeLog

11
.mailmap Normal file
View File

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

20
Authors Normal file
View File

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

View File

@@ -77,6 +77,7 @@ def print_image_formatted(client, image):
print "Id: %s" % image['id']
print "Public: " + (image['is_public'] and "Yes" or "No")
print "Name: %s" % image['name']
print "Status: %s" % image['status']
print "Size: %d" % int(image['size'])
print "Location: %s" % image['location']
print "Disk format: %s" % image['disk_format']
@@ -208,6 +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'.")
return FAILURE
else:
print "Dry run. We would have done the following:"
@@ -232,6 +235,7 @@ to Glance that represents the metadata for an image.
Field names that can be specified:
name A name for the image.
location The location of the image.
is_public If specified, interpreted as a boolean value
and sets or unsets the image's availability to the public.
disk_format Format of the disk image
@@ -263,7 +267,7 @@ to spell field names correctly. :)"""
print 'Found non-modifiable field %s. Removing.' % field
fields.pop(field)
base_image_fields = ['disk_format', 'container_format',
base_image_fields = ['disk_format', 'container_format', 'name',
'location']
for field in base_image_fields:
fvalue = fields.pop(field, None)
@@ -308,7 +312,6 @@ def image_delete(options, args):
%(prog)s delete [options] <ID>
Deletes an image from Glance"""
c = get_client(options)
try:
image_id = args.pop()
except IndexError:
@@ -316,6 +319,13 @@ Deletes an image from Glance"""
print "as the first argument"
return FAILURE
if not options.force and \
not user_confirm("Delete image %s?" % (image_id,), default=False):
print 'Not deleting image %s' % (image_id,)
return FAILURE
c = get_client(options)
try:
c.delete_image(image_id)
print "Deleted image %s" % image_id
@@ -427,6 +437,11 @@ def images_clear(options, args):
%(prog)s clear [options]
Deletes all images from a Glance server"""
if not options.force and \
not user_confirm("Delete all images?", default=False):
print 'Not deleting any images'
return FAILURE
c = get_client(options)
images = c.get_images()
for image in images:
@@ -469,6 +484,10 @@ def create_options(parser):
type=int, default=9292,
help="Port the Glance API host listens on. "
"Default: %default")
parser.add_option('-f', '--force', dest="force", metavar="FORCE",
default=False, action="store_true",
help="Prevent select actions from requesting "
"user confirmation")
parser.add_option('--dry-run', default=False, action="store_true",
help="Don't actually execute the command, just print "
"output showing what WOULD happen.")
@@ -531,6 +550,27 @@ def print_help(options, args):
print COMMANDS[command].__doc__ % {'prog': os.path.basename(sys.argv[0])}
def user_confirm(prompt, default=False):
"""Yes/No question dialog with user.
:param prompt: question/statement to present to user (string)
:param default: boolean value to return if empty string
is received as response to prompt
"""
if default:
prompt_default = "[Y/n]"
else:
prompt_default = "[y/N]"
answer = raw_input("%s %s " % (prompt, prompt_default))
if answer == "":
return default
else:
return answer.lower() in ("yes", "y")
if __name__ == '__main__':
usage = """
%prog <command> [options] [args]

View File

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

View File

@@ -17,10 +17,34 @@
Configuring Glance
==================
Glance has a number of options that you can use to configure the Glance API
server, the Glance Registry server, and the various storage backends that
Glance can use to store images.
Most configuration is done via configuration files, with the Glance API
server and Glance Registry server using separate configuration files.
When starting up a Glance server, you can specify the configuration file to
use (see `the documentation on controller Glance servers <controllingservers>`_).
If you do **not** specify a configuration file, Glance will look in the following
directories for a configuration file, in order:
* ``$CWD``
* ``~/.glance``
* ``~/``
* ``/etc/glance``
* ``/etc``
The Glance API server configuration file should be named ``glance-api.conf``.
Similarly, the Glance Registry server configuration file should be named
``glance-registry.conf``. If you installed Glance via your operating system's
package management system, it is likely that you will have sample
configuration files installed in ``/etc/glance``.
In addition to this documentation page, you can check the
``etc/glance.conf.sample`` sample configuration file distributed with Glance
for an example configuration file with detailed comments on what each options
does.
``etc/glance-api.conf`` and ``etc/glance-registry.conf`` sample configuration
files distributed with Glance for example configuration files for each server
application with detailed comments on what each options does.
Common Configuration Options in Glance
--------------------------------------
@@ -56,18 +80,21 @@ 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
-----------------------------
There are a number of configuration options in Glance that control how Glance
servers log messages. The configuration options can be specified both on the
command line and in the ``glance.conf`` config file.
servers log messages.
* ``--log-config=PATH``
@@ -77,30 +104,29 @@ Specified on the command line only.
Takes a path to a configuration file to use for configuring logging.
* ``--log-format``
Logging Options Available Only in Configuration Files
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`Because of a bug in the PasteDeploy package, this option is only available
on the command line.`
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``::
Optional. Default: ``%(asctime)s %(levelname)8s [%(name)s] %(message)s``
[DEFAULT]
log_file = /var/log/glance/api.log
The format of the log records. See the
`logging module <http://docs.python.org/library/logging.html>`_ documentation for
more information on setting this format string.
* ``log_file`` (``--log-file`` when specified on the command line)
* ``log_file``
The filepath of the file to use for logging messages from Glance's servers. If
missing, the default is to output messages to ``stdout``, so if you are running
Glance servers in a daemon mode (using ``glance-control``) you should make
sure that the ``log_file`` option is set appropriately.
* ``log_dir`` (``--log-dir`` when specified on the command line)
* ``log_dir``
The filepath of the directory to use for log files. If not specified (the default)
the ``log_file`` is used as an absolute filepath.
* ``log_date_format`` (``--log-date-format`` when specified from the command line)
* ``log_date_format``
The format string for timestamps in the log output.
@@ -113,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``
@@ -199,7 +225,7 @@ Configuring the Glance Registry
Glance ships with a default, reference implementation registry server. There
are a number of configuration options in Glance that control how this registry
server operates. These configuration options are specified in the
``glance.conf`` config file `in the section [app:glance-registry]`.
``glance-registry.conf`` config file in the section ``[DEFAULT]``.
* ``sql_connection=CONNECTION_STRING`` (``--sql-connection`` when specified
on command line)

View File

@@ -30,60 +30,95 @@ reference implementation registry server that ships with Glance):
* Using the ``glance-control`` server daemon wrapper program
We recommend using the second way.
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::
Glance ships with an ``etc/`` directory that contains sample ``paste.deploy``
configuration files that you can copy to a standard configuation directory and
adapt for your own uses.
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
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``.
* ~/
* /etc/glance/
* /etc
If no configuration file is found, you will see any error, like so::
If no configuration file is found, you will see an error, like::
$> glance-api
ERROR: Unable to locate any configuration file. Cannot load application glance-api
Here is an example showing how you can manually start the ``glance-api`` server
in a shell.::
Here is an example showing how you can manually start the ``glance-api`` server and ``glance-registry`` in a shell.::
$> sudo glance-api etc/glance.conf.sample --debug
2011-02-09 14:58:29 DEBUG [glance-api] ********************************************************************************
2011-02-09 14:58:29 DEBUG [glance-api] Configuration options gathered from config file:
2011-02-09 14:58:29 DEBUG [glance-api] /home/jpipes/repos/glance/trunk/etc/glance.conf.sample
2011-02-09 14:58:29 DEBUG [glance-api] ================================================
2011-02-09 14:58:29 DEBUG [glance-api] bind_host 0.0.0.0
2011-02-09 14:58:29 DEBUG [glance-api] bind_port 9292
2011-02-09 14:58:29 DEBUG [glance-api] debug True
2011-02-09 14:58:29 DEBUG [glance-api] default_store file
2011-02-09 14:58:29 DEBUG [glance-api] filesystem_store_datadir /var/lib/glance/images/
2011-02-09 14:58:29 DEBUG [glance-api] registry_host 0.0.0.0
2011-02-09 14:58:29 DEBUG [glance-api] registry_port 9191
2011-02-09 14:58:29 DEBUG [glance-api] verbose False
2011-02-09 14:58:29 DEBUG [glance-api] ********************************************************************************
2011-02-09 14:58:29 DEBUG [routes.middleware] Initialized with method overriding = True, and path info altering = True
(16333) wsgi starting up on http://0.0.0.0:9292/
$ 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-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
2011-04-13 14:50:12 DEBUG [glance-api] debug True
2011-04-13 14:50:12 DEBUG [glance-api] default_store file
2011-04-13 14:50:12 DEBUG [glance-api] filesystem_store_datadir /home/jsuh/images/
2011-04-13 14:50:12 DEBUG [glance-api] registry_host 65.114.169.29
2011-04-13 14:50:12 DEBUG [glance-api] registry_port 9191
2011-04-13 14:50:12 DEBUG [glance-api] verbose False
2011-04-13 14:50:12 DEBUG [glance-api] ********************************************************************************
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-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')
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (0, u'created_at', u'DATETIME', 1, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (1, u'updated_at', u'DATETIME', 0, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (2, u'deleted_at', u'DATETIME', 0, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (3, u'deleted', u'BOOLEAN', 1, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (4, u'id', u'INTEGER', 1, None, 1)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (5, u'name', u'VARCHAR(255)', 0, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (6, u'disk_format', u'VARCHAR(20)', 0, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (7, u'container_format', u'VARCHAR(20)', 0, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (8, u'size', u'INTEGER', 0, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (9, u'status', u'VARCHAR(30)', 1, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (10, u'is_public', u'BOOLEAN', 1, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (11, u'location', u'TEXT', 0, None, 0)
2011-04-13 14:51:16 INFO [sqlalchemy.engine.base.Engine.0x...feac] PRAGMA table_info("image_properties")
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')
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (0, u'created_at', u'DATETIME', 1, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (1, u'updated_at', u'DATETIME', 0, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (2, u'deleted_at', u'DATETIME', 0, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (3, u'deleted', u'BOOLEAN', 1, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (4, u'id', u'INTEGER', 1, None, 1)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (5, u'image_id', u'INTEGER', 1, None, 0)
2011-04-13 14:51:16 DEBUG [sqlalchemy.engine.base.Engine.0x...feac] Row (6, u'key', u'VARCHAR(255)', 1, None, 0)
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-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
@@ -93,9 +128,8 @@ For more information on configuring the server via the ``paste.deploy``
configuration files, see the section entitled
:doc:`Configuring Glance servers <configuring>`
Note that the server does not `daemonize` itself when run manually
from the terminal. You can force the server to daemonize using the standard
shell backgrounding indicator, ``&``. However, for most use cases, we recommend
Note that the server `daemonizes` itself by using the standard
shell backgrounding indicator, ``&``, in the previous example. For most use cases, we recommend
using the ``glance-control`` server daemon wrapper for daemonizing. See below
for more details on daemonization with ``glance-control``.
@@ -125,18 +159,23 @@ in the following way::
Here is an example that shows how to start the ``glance-registry`` server
with the ``glance-control`` wrapper script. ::
$> sudo glance-control registry start etc/glance.conf.sample
Starting glance-registry with /home/jpipes/repos/glance/trunk/etc/glance.conf.sample
$ 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-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
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 all the Glance servers (currently the glance-api and glance-registry
programs) at once, you can specify "all" for the <SERVER>
Stopping a server
-----------------
@@ -160,6 +199,6 @@ Restarting a server
You can restart a server with the ``glance-control`` program, as demonstrated
here::
$> sudo glance-control registry restart etc/glance.conf.sample
$> sudo glance-control registry restart etc/glance-registry.conf
Stopping glance-registry pid: 17611 signal: 15
Starting glance-registry with /home/jpipes/repos/glance/trunk/etc/glance.conf.sample
Starting glance-registry with /home/jpipes/repos/glance/trunk/etc/glance-registry.conf

View File

@@ -17,44 +17,44 @@
Using the Glance CLI Tool
=========================
Glance ships with a command-line tool for quering and managing Glance
Glance ships with a command-line tool for querying and managing Glance
It has a fairly simple but powerful interface of the form::
Usage: glance <command> [options] [args]
Where ``<command>`` is one of the following:
* help
* ``help``
Show detailed help information about a specific command
* add
* ``add``
Adds an image to Glance
* update
* ``update``
Updates an image's stored metadata in Glance
* delete
* ``delete``
Deletes an image and its metadata from Glance
* index
* ``index``
Lists brief information about *public* images that Glance knows about
* details
* ``details``
Lists detailed information about *public* images that Glance knows about
* show
* ``show``
Lists detailed information about a specific image
* clear
* ``clear``
Destroys *all* images and their associated metadata
Destroys all **public** images and their associated metadata
This document describes how to use the ``glance`` tool for each of
the above commands.
@@ -134,6 +134,23 @@ The ``add`` command is used to do both of the following:
We cover both use cases below.
Important Information about Uploading Images
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before we go over the commands for adding an image to Glance, it is
important to understand that Glance **does not currently inspect** the image
files you add to it. In other words, **Glance only understands what you tell it,
via attributes and custom properties**.
If the file extension of the file you upload to Glance ends in '.vhd', Glance
**does not** know that the image you are uploading has a disk format of ``vhd``.
You have to **tell** Glance that the image you are uploading has a disk format by
using the ``disk_format=vhd`` on the command line (see more below).
By the same token, Glance does not currently allow you to upload "multi-part"
disk images at once. **The common operation of bundling a kernel image and ramdisk image
into a machine image is not done automagically by Glance.**
Store virtual machine image data and metadata
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -142,27 +159,27 @@ command. You will pass metadata about the VM image on the command line, and
you will use a standard shell redirect to stream the image data file to
``glance``.
Let's walk through a simple example. Suppose we have an image stored on our
local filesystem that we wish to "upload" to Glance. This image is stored
on our local filesystem in ``/tmp/images/myimage.tar.gz``.
Let's walk through a simple example. Suppose we have a virtual disk image
stored on our local filesystem that we wish to "upload" to Glance. This image is stored
on our local filesystem in ``/tmp/images/myimage.iso``.
We'd also like to tell Glance that this image should be called "My Image", and
that the image should be public -- anyone should be able to fetch it.
Here is how we'd upload this image to Glance::
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
$> 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
$> 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
$> 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
$> 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
@@ -250,12 +268,12 @@ image. You use this command like so::
Let's say we have an image with identifier 5 that we wish to change the is_public
attribute of the image from False to True. The following would accomplish this::
$> glance update 5 is_public=true
$> glance update 5 is_public=true --host=65.114.169.29
Updated image 5
Using the ``--verbose`` flag will show you all the updated data about the image::
$> glance --verbose update 5 is_public=true
$> glance --verbose update 5 is_public=true --host=65.114.169.29
Updated image 5
Updated image metadata for image 5:
URI: http://example.com/images/5
@@ -273,7 +291,7 @@ The ``delete`` command
You can delete an image by using the ``delete`` command, shown below::
$> glance --verbose delete 5
$> glance --verbose delete 5 --host=65.114.169.29
Deleted image 5
The ``index`` command
@@ -282,7 +300,7 @@ The ``index`` command
The ``index`` command displays brief information about the *public* images
available in Glance, as shown below::
$> glance index
$> glance index --host=65.114.169.29
Found 4 public images...
ID Name Disk Format Container Format Size
---------------- ------------------------------ -------------------- -------------------- --------------
@@ -297,13 +315,14 @@ The ``details`` command
The ``details`` command displays detailed information about the *public* images
available in Glance, as shown below::
$> glance details
$> glance details --host=65.114.169.29
Found 4 public images...
================================================================================
URI: http://example.com/images/1
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
================================================================================
@@ -349,11 +371,12 @@ The ``show`` command
The ``show`` command displays detailed information about a specific image, specified
with ``<ID>``, as shown below::
$> glance show 3
$> glance show 3 --host=65.114.169.29
URI: http://example.com/images/3
Id: 3
Public? Yes
Name: Fedora 9
Status: active
Size: 3040
Location: file:///tmp/images/3
Disk format: vdi
@@ -368,7 +391,7 @@ The ``clear`` command is an administrative command that deletes **ALL** images
and all image metadata. Passing the ``--verbose`` command will print brief
information about all the images that were deleted, as shown below::
$> glance --verbose clear
$> glance --verbose clear --host=65.114.169.29
Deleting image 1 "Some web image" ... done
Deleting image 2 "Some other web image" ... done
Completed in 0.0328 sec.

View File

@@ -5,9 +5,6 @@ verbose = True
# Show debugging output in logs (sets DEBUG log level output)
debug = False
[app:glance-api]
paste.app_factory = glance.server:app_factory
# Which backend store should Glance use by default is not specified
# in a request to add a new image to Glance? Default: 'file'
# Available choices are 'file', 'swift', and 's3'
@@ -25,11 +22,15 @@ registry_host = 0.0.0.0
# Port the registry server is listening on
registry_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/api.log
# ============ Filesystem Store Options ========================
# Directory that the Filesystem backend store
# writes image data to
filesystem_store_datadir=/var/lib/glance/images/
filesystem_store_datadir = /var/lib/glance/images/
# ============ Swift Store Options =============================
@@ -50,25 +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
# 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
[app:apiv1app]
paste.app_factory = glance.api.v1:app_factory
# Period in seconds after which SQLAlchemy should reestablish its connection
# to the database.
#
# MySQL uses a default `wait_timeout` of 8 hours, after which it will drop
# idle connections. This can result in 'MySQL Gone Away' exceptions. If you
# notice this, you can lower this value to ensure that SQLAlchemy reconnects
# before MySQL can drop the connection.
sql_idle_timeout = 3600
[filter:versionnegotiation]
paste.filter_factory = glance.api.middleware.version_negotiation:filter_factory

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

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

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

@@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

View File

@@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

View File

@@ -0,0 +1,134 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
A filter middleware that inspects the requested URI for a version string
and/or Accept headers and attempts to negotiate an API controller to
return
"""
import logging
import re
import routes
from glance.api import v1
from glance.api import versions
from glance.common import wsgi
logger = logging.getLogger('glance.api.middleware.version_negotiation')
class VersionNegotiationFilter(wsgi.Middleware):
def __init__(self, app, options):
self.versions_app = versions.Controller(options)
self.version_uri_regex = re.compile(r"^v(\d+)\.?(\d+)?")
self.options = options
super(VersionNegotiationFilter, self).__init__(app)
def process_request(self, req):
"""
If there is a version identifier in the URI, simply
return the correct API controller, otherwise, if we
find an Accept: header, process it
"""
# See if a version identifier is in the URI passed to
# us already. If so, simply return the right version
# API controller
logger.debug("Processing request: %s %s Accept: %s",
req.method, req.path, req.accept)
# If the request is for /versions, just return the versions container
if req.path_info_peek() == "versions":
return self.versions_app
match = self._match_version_string(req.path_info_peek(), req)
if match:
if (req.environ['api.major_version'] == 1 and
req.environ['api.minor_version'] == 0):
logger.debug("Matched versioned URI. Version: %d.%d",
req.environ['api.major_version'],
req.environ['api.minor_version'])
# Strip the version from the path
req.path_info_pop()
return None
else:
logger.debug("Unknown version in versioned URI: %d.%d. "
"Returning version choices.",
req.environ['api.major_version'],
req.environ['api.minor_version'])
return self.versions_app
accept = req.headers['Accept']
if accept.startswith('application/vnd.openstack.images-'):
token_loc = len('application/vnd.openstack.images-')
accept_version = accept[token_loc:]
match = self._match_version_string(accept_version, req)
if match:
if (req.environ['api.major_version'] == 1 and
req.environ['api.minor_version'] == 0):
logger.debug("Matched versioned media type. "
"Version: %d.%d",
req.environ['api.major_version'],
req.environ['api.minor_version'])
return None
else:
logger.debug("Unknown version in accept header: %s.%s..."
"returning version choices.",
req.environ['api.major_version'],
req.environ['api.minor_version'])
return self.versions_app
else:
if req.accept not in ('*/*', ''):
logger.debug("Unknown accept header: %s..."
"returning version choices.", req.accept)
return self.versions_app
return None
def _match_version_string(self, subject, req):
"""
Given a subject string, tries to match a major and/or
minor version number. If found, sets the api.major_version
and api.minor_version environ variables.
Returns True if there was a match, false otherwise.
:param subject: The string to check
:param req: Webob.Request object
"""
match = self.version_uri_regex.match(subject)
if match:
major_version, minor_version = match.groups(0)
major_version = int(major_version)
minor_version = int(minor_version)
req.environ['api.major_version'] = major_version
req.environ['api.minor_version'] = minor_version
return match is not None
def filter_factory(global_conf, **local_conf):
"""
Factory method for paste.deploy
"""
conf = global_conf.copy()
conf.update(local_conf)
def filter(app):
return VersionNegotiationFilter(app, conf)
return filter

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

@@ -0,0 +1,48 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import routes
from glance.api.v1 import images
from glance.common import wsgi
logger = logging.getLogger('glance.api.v1')
class API(wsgi.Router):
"""WSGI router for Glance v1 API requests."""
def __init__(self, options):
self.options = options
mapper = routes.Mapper()
controller = images.Controller(options)
mapper.resource("image", "images", controller=controller,
collection={'detail': 'GET'})
mapper.connect("/", controller=controller, action="index")
mapper.connect("/images/{id}", controller=controller, action="meta",
conditions=dict(method=["HEAD"]))
super(API, self).__init__(mapper)
def app_factory(global_conf, **local_conf):
"""paste.deploy app factory for creating Glance API server apps"""
conf = global_conf.copy()
conf.update(local_conf)
return API(conf)

View File

@@ -16,17 +16,7 @@
# under the License.
"""
=================
Glance API Server
=================
Configuration Options
---------------------
`default_store`: When no x-image-meta-store header is sent for a
`POST /images` request, this store will be used
for storing the image data. Default: 'file'
/images endpoint for Glance v1 API
"""
import httplib
@@ -34,7 +24,6 @@ import json
import logging
import sys
import routes
from webob import Response
from webob.exc import (HTTPNotFound,
HTTPConflict,
@@ -51,15 +40,15 @@ from glance import registry
from glance import utils
logger = logging.getLogger('glance.server')
logger = logging.getLogger('glance.api.v1.images')
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
@@ -142,7 +131,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 +164,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)
@@ -192,6 +181,13 @@ class Controller(wsgi.Controller):
:raises HTTPBadRequest if image metadata is not valid
"""
image_meta = utils.get_image_meta_from_headers(req)
if 'location' in image_meta:
store = get_store_from_location(image_meta['location'])
# check the store exists before we hit the registry, but we
# don't actually care what it is at this point
self.get_store_or_400(req, store)
image_meta['status'] = 'queued'
# Ensure that the size attribute is set to zero for all
@@ -390,7 +386,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)
@@ -453,7 +449,12 @@ class Controller(wsgi.Controller):
# to delete the image if the backend doesn't yet store it.
# See https://bugs.launchpad.net/glance/+bug/747799
if image['location']:
delete_from_backend(image['location'])
try:
delete_from_backend(image['location'])
except (UnsupportedBackend, exception.NotFound):
msg = "Failed to delete image from store (%s). " + \
"Continuing with deletion from registry."
logger.error(msg % (image['location'],))
registry.delete_image_metadata(self.options, id)
@@ -493,26 +494,3 @@ class Controller(wsgi.Controller):
logger.error(msg)
raise HTTPBadRequest(msg, request=request,
content_type='text/plain')
class API(wsgi.Router):
"""WSGI entry point for all Glance API requests."""
def __init__(self, options):
self.options = options
mapper = routes.Mapper()
controller = Controller(options)
mapper.resource("image", "images", controller=controller,
collection={'detail': 'GET'})
mapper.connect("/", controller=controller, action="index")
mapper.connect("/images/{id}", controller=controller, action="meta",
conditions=dict(method=["HEAD"]))
super(API, self).__init__(mapper)
def app_factory(global_conf, **local_conf):
"""paste.deploy app factory for creating Glance API server apps"""
conf = global_conf.copy()
conf.update(local_conf)
return API(conf)

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

@@ -0,0 +1,69 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Controller that returns information on the Glance API versions
"""
import httplib
import json
import webob.dec
from glance.common import wsgi
class Controller(object):
"""
A controller that produces information on the Glance API versions.
"""
def __init__(self, options):
self.options = options
@webob.dec.wsgify
def __call__(self, req):
"""Respond to a request for all OpenStack API versions."""
version_objs = [
{
"id": "v1.0",
"status": "CURRENT",
"links": [
{
"rel": "self",
"href": self.get_href()}]}]
body = json.dumps(dict(versions=version_objs))
response = webob.Response(request=req,
status=httplib.MULTIPLE_CHOICES,
content_type='application/json')
response.body = body
return response
def get_href(self):
return "http://%s:%s/v1/" % (self.options['bind_host'],
self.options['bind_port'])
def app_factory(global_conf, **local_conf):
"""paste.deploy app factory for creating Glance API versions apps"""
conf = global_conf.copy()
conf.update(local_conf)
return Controller(conf)

View File

@@ -177,24 +177,31 @@ 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 do_request(self, method, action, body=None, headers=None):
action = "%s/%s" % (self.doc_root, action.lstrip("/"))
return super(V1Client, self).do_request(method, action,
body, headers)
def get_images(self):
"""
Returns a list of image id/name mappings from Registry
@@ -288,3 +295,6 @@ class Client(BaseClient):
"""
self.do_request("DELETE", "/images/%s" % image_id)
return True
Client = V1Client

View File

@@ -135,8 +135,10 @@ def setup_logging(options, conf):
# If either the CLI option or the conf value
# is True, we set to True
debug = options.get('debug') or conf.get('debug', False)
verbose = options.get('verbose') or conf.get('verbose', False)
debug = options.get('debug') or \
get_option(conf, 'debug', type='bool', default=False)
verbose = options.get('verbose') or \
get_option(conf, 'verbose', type='bool', default=False)
root_logger = logging.root
if debug:
root_logger.setLevel(logging.DEBUG)
@@ -173,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/
* ~
@@ -198,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('~'),
@@ -206,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
@@ -219,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/
* ~
@@ -236,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)
@@ -255,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/
* ~
@@ -278,8 +280,10 @@ def load_paste_app(app_name, options, args):
# We only update the conf dict for the verbose and debug
# flags. Everything else must be set up in the conf file...
debug = options.get('debug') or conf.get('debug', False)
verbose = options.get('verbose') or conf.get('verbose', False)
debug = options.get('debug') or \
get_option(conf, 'debug', type='bool', default=False)
verbose = options.get('verbose') or \
get_option(conf, 'verbose', type='bool', default=False)
conf['debug'] = debug
conf['verbose'] = verbose

View File

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

View File

@@ -123,6 +123,9 @@ def image_destroy(context, image_id):
image_ref = image_get(context, image_id, session=session)
image_ref.delete(session=session)
for prop_ref in image_ref.properties:
image_property_delete(context, prop_ref, session=session)
def image_get(context, image_id, session=None):
"""Get an image or raise if it does not exist."""
@@ -144,6 +147,7 @@ def image_get_all_public(context):
options(joinedload(models.Image.properties)).\
filter_by(deleted=_deleted(context)).\
filter_by(is_public=True).\
filter(models.Image.status != 'killed').\
all()

View File

@@ -75,6 +75,15 @@ class SwiftBackend(glance.store.Backend):
return resp_body
@classmethod
def _option_get(cls, options, param):
result = options.get(param)
if not result:
msg = ("Could not find %s in configuration options." % param)
logger.error(msg)
raise glance.store.BackendException(msg)
return result
@classmethod
def add(cls, id, data, options):
"""
@@ -101,36 +110,19 @@ class SwiftBackend(glance.store.Backend):
from swift.common import client as swift_client
container = options.get('swift_store_container',
DEFAULT_SWIFT_CONTAINER)
auth_address = options.get('swift_store_auth_address')
user = options.get('swift_store_user')
key = options.get('swift_store_key')
# TODO(jaypipes): This needs to be checked every time
# because of the decision to make glance.store.Backend's
# interface all @classmethods. This is inefficient. Backend
# should be a stateful object with options parsed once in
# a constructor.
if not auth_address:
msg = ("Could not find swift_store_auth_address in configuration "
"options.")
logger.error(msg)
raise glance.store.BackendException(msg)
else:
full_auth_address = auth_address
if not full_auth_address.startswith('http'):
full_auth_address = 'https://' + full_auth_address
auth_address = cls._option_get(options, 'swift_store_auth_address')
user = cls._option_get(options, 'swift_store_user')
key = cls._option_get(options, 'swift_store_key')
if not user:
msg = ("Could not find swift_store_user in configuration "
"options.")
logger.error(msg)
raise glance.store.BackendException(msg)
if not key:
msg = ("Could not find swift_store_key in configuration "
"options.")
logger.error(msg)
raise glance.store.BackendException(msg)
full_auth_address = auth_address
if not full_auth_address.startswith('http'):
full_auth_address = 'https://' + full_auth_address
swift_conn = swift_client.Connection(
authurl=full_auth_address, user=user, key=key, snet=False)

View File

@@ -21,10 +21,10 @@ except ImportError:
'revision_id': 'LOCALREVISION',
'revno': 0}
GLANCE_VERSION = ['2011', '2']
GLANCE_VERSION = ['2011', '3']
YEAR, COUNT = GLANCE_VERSION
FINAL = True # This becomes true at Release Candidate time
FINAL = False # This becomes true at Release Candidate time
def canonical_version_string():

View File

@@ -39,7 +39,7 @@ done
function run_tests {
# Just run the test suites in current environment
${wrapper} rm -f glance.sqlite
${wrapper} rm -f tests.sqlite
${wrapper} $NOSETESTS 2> run_tests.err.log
}

View File

@@ -24,6 +24,7 @@ and spinning down the servers.
"""
import datetime
import functools
import os
import random
import shutil
@@ -36,6 +37,161 @@ import urlparse
from tests.utils import execute, get_unused_port
from sqlalchemy import create_engine
def runs_sql(func):
"""
Decorator for a test case method that ensures that the
sql_connection setting is overridden to ensure a disk-based
SQLite database so that arbitrary SQL statements can be
executed out-of-process against the datastore...
"""
@functools.wraps(func)
def wrapped(*a, **kwargs):
test_obj = a[0]
orig_sql_connection = test_obj.registry_server.sql_connection
try:
if orig_sql_connection.startswith('sqlite'):
test_obj.registry_server.sql_connection =\
"sqlite:///tests.sqlite"
func(*a, **kwargs)
finally:
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):
@@ -50,21 +206,16 @@ class FunctionalTest(unittest.TestCase):
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):
@@ -75,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
@@ -135,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 = True
debug = True
[app:glance-api]
paste.app_factory = glance.server:app_factory
filesystem_store_datadir=%(image_dir)s
default_store = file
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. "
@@ -192,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
@@ -239,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)
@@ -259,3 +363,13 @@ sql_idle_timeout = 3600
# went wrong...
if os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir)
def run_sql_cmd(self, sql):
"""
Provides a crude mechanism to run manual SQL commands for backend
DB verification within the functional tests.
The raw result set is returned.
"""
engine = create_engine(self.registry_server.sql_connection,
pool_recycle=30)
return engine.execute(sql)

View File

@@ -18,6 +18,7 @@
"""Functional test case that utilizes the bin/glance CLI tool"""
import os
import tempfile
import unittest
from tests import functional
@@ -41,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
@@ -72,7 +76,7 @@ class TestBinGlance(functional.FunctionalTest):
self.assertTrue('MyImage' in image_data_line)
# 3. Delete the image
cmd = "bin/glance --port=%d delete 1" % api_port
cmd = "bin/glance --port=%d --force delete 1" % api_port
exitcode, out, err = execute(cmd)
@@ -91,7 +95,7 @@ class TestBinGlance(functional.FunctionalTest):
def test_add_list_update_list(self):
"""
Test for LP Bug #736295
Test for LP Bugs #736295, #767203
We test the following:
0. Verify no public images in index
@@ -99,10 +103,15 @@ class TestBinGlance(functional.FunctionalTest):
2. Check that image does not appear in index
3. Update the image to be public
4. Check that image now appears in index
5. Update the image's Name attribute
6. Verify the updated name is shown
"""
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
@@ -120,7 +129,7 @@ class TestBinGlance(functional.FunctionalTest):
self.assertEqual(0, exitcode)
self.assertEqual('Added new image with ID: 1', out.strip())
# 2. Verify image added as public image
# 2. Verify image does not appear as a public image
cmd = "bin/glance --port=%d index" % api_port
exitcode, out, err = execute(cmd)
@@ -149,4 +158,137 @@ class TestBinGlance(functional.FunctionalTest):
image_data_line = lines[3]
self.assertTrue('MyImage' in image_data_line)
# 5. Update the image's Name attribute
updated_image_name = "Updated image name"
cmd = "bin/glance --port=%d update 1 is_public=True name=\"%s\"" \
% (api_port, updated_image_name)
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual('Updated image 1', out.strip())
# 6. Verify updated name shown
cmd = "bin/glance --port=%d index" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue(updated_image_name in out,
"%s not found in %s" % (updated_image_name, out))
self.stop_servers()
def test_killed_image_not_in_index(self):
"""
We test conditions that produced LP Bug #768969, where an image
in the 'killed' status is displayed in the output of glance index,
and the status column is not displayed in the output of
glance show <ID>.
Start servers with Swift backend and a bad auth URL, and then:
0. Verify no public images in index
1. Attempt to add an image
2. Verify the image does NOT appear in the index output
3. Verify the status of the image is displayed in the show output
and is in status 'killed'
"""
self.cleanup()
# Start servers with a Swift backend and a bad auth URL
options = {'default_store': 'swift',
'swift_store_auth_address': 'badurl'}
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
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual('No public images found.', out.strip())
# 1. Attempt to add an image
with tempfile.NamedTemporaryFile() as image_file:
image_file.write("XXX")
image_file.flush()
image_file_name = image_file.name
cmd = ("bin/glance --port=%d add name=Jonas is_public=True "
"disk_format=qcow2 container_format=bare < %s"
% (api_port, image_file_name))
exitcode, out, err = execute(cmd, raise_error=False)
self.assertNotEqual(0, exitcode)
self.assertTrue('Failed to add image.' in out)
# 2. Verify image does not appear as public image
cmd = "bin/glance --port=%d index" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual('No public images found.', out.strip())
# 3. Verify image status in show is 'killed'
cmd = "bin/glance --port=%d show 1" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertTrue('Status: killed' in out)
self.stop_servers()
@functional.runs_sql
def test_add_clear(self):
"""
We test the following:
1. Add a couple images with metadata
2. Clear the images
3. Verify no public images found
4. Run SQL against DB to verify no undeleted properties
"""
self.cleanup()
self.start_servers()
api_port = self.api_port
registry_port = self.registry_port
# 1. Add some images
for i in range(1, 5):
cmd = "bin/glance --port=%d add is_public=True name=MyName " \
" foo=bar" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
self.assertEqual('Added new image with ID: %i' % i, out.strip())
# 2. Clear all images
cmd = "bin/glance --port=%d --force clear" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
# 3. Verify no public images are found
cmd = "bin/glance --port=%d index" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\n")
first_line = lines[0]
self.assertEqual('No public images found.', first_line)
# 4. Lastly we manually verify with SQL that image properties are
# also getting marked as deleted.
sql = "SELECT COUNT(*) FROM image_properties WHERE deleted = 0"
recs = self.run_sql_cmd(sql)
for rec in recs:
self.assertEqual(0, rec[0])
self.stop_servers()

View File

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

View File

@@ -15,6 +15,8 @@
# License for the specific language governing permissions and limitations
# under the License.
"""Functional test case that tests logging output"""
import os
import unittest
@@ -24,56 +26,54 @@ from tests.utils import execute
class TestLogging(functional.FunctionalTest):
"""Tests that logging can be configured correctly"""
"""Functional tests for Glance's logging output"""
def test_logfile(self):
def test_verbose_debug(self):
"""
A test that logging can be configured properly from the
glance.conf file with the log_file option.
We start both servers daemonized with a temporary config
file that has some logging options in it.
We then use curl to issue a few requests and verify that each server's
logging statements were logged to the one log file
Test logging output proper when verbose and debug
is on.
"""
self.cleanup()
api_port, reg_port, conf_file = self.start_servers()
self.start_servers()
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
ignored, out, err = execute(cmd)
# 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('Invalid disk format' in out,
"Could not find 'Invalid disk format' "
"in output: %s" % out)
self.assertTrue(os.path.exists(self.api_server.log_file))
self.assertTrue(os.path.exists(self.api_log_file),
"API Logfile %s does not exist!"
% self.api_log_file)
self.assertTrue(os.path.exists(self.registry_log_file),
"Registry Logfile %s does not exist!"
% self.registry_log_file)
api_log_out = open(self.api_server.log_file, 'r').read()
api_logfile_contents = open(self.api_log_file, 'rb').read()
registry_logfile_contents = open(self.registry_log_file, 'rb').read()
self.assertTrue('DEBUG [glance-api]' in api_log_out)
# Check that BOTH the glance API and registry server
# modules are logged to their respective logfiles.
self.assertTrue('[glance.server]'
in api_logfile_contents,
"Could not find '[glance.server]' "
"in API logfile: %s" % api_logfile_contents)
self.assertTrue('[glance.registry.server]'
in registry_logfile_contents,
"Could not find '[glance.registry.server]' "
"in Registry logfile: %s" % registry_logfile_contents)
self.assertTrue(os.path.exists(self.registry_server.log_file))
# Test that the error we caused above is in the log
self.assertTrue('Invalid disk format' in api_logfile_contents,
"Could not find 'Invalid disk format' "
"in API logfile: %s" % api_logfile_contents)
registry_log_out = open(self.registry_server.log_file, 'r').read()
self.assertTrue('DEBUG [glance-registry]' in registry_log_out)
self.stop_servers()
def test_no_verbose_no_debug(self):
"""
Test logging output proper when verbose and debug
is off.
"""
self.cleanup()
self.start_servers(debug=False, verbose=False)
self.assertTrue(os.path.exists(self.api_server.log_file))
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_server.log_file))
registry_log_out = open(self.registry_server.log_file, 'r').read()
self.assertFalse('DEBUG [glance-registry]' in registry_log_out)
self.stop_servers()

View File

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

View File

@@ -29,7 +29,7 @@ import webob
from glance.common import exception
from glance.registry import server as rserver
from glance import server
from glance.api import v1 as server
import glance.store
import glance.store.filesystem
import glance.store.http

View File

@@ -24,7 +24,7 @@ import unittest
import stubout
import webob
from glance import server
from glance.api import v1 as server
from glance.registry import server as rserver
from tests import stubs

View File

@@ -256,7 +256,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"""

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

@@ -0,0 +1,77 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 OpenStack, LLC
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import unittest
def parse_mailmap(mailmap='.mailmap'):
mapping = {}
if os.path.exists(mailmap):
fp = open(mailmap, 'r')
for l in fp:
l = l.strip()
if not l.startswith('#') and ' ' in l:
canonical_email, alias = l.split(' ')
mapping[alias] = canonical_email
return mapping
def str_dict_replace(s, mapping):
for s1, s2 in mapping.iteritems():
s = s.replace(s1, s2)
return s
class AuthorsTestCase(unittest.TestCase):
def test_authors_up_to_date(self):
topdir = os.path.normpath(os.path.dirname(__file__) + '/../../')
if os.path.exists(os.path.join(topdir, '.bzr')):
contributors = set()
mailmap = parse_mailmap(os.path.join(topdir, '.mailmap'))
import bzrlib.workingtree
tree = bzrlib.workingtree.WorkingTree.open(topdir)
tree.lock_read()
try:
parents = tree.get_parent_ids()
g = tree.branch.repository.get_graph()
for p in parents:
rev_ids = [r for r, _ in g.iter_ancestry(parents)
if r != "null:"]
revs = tree.branch.repository.get_revisions(rev_ids)
for r in revs:
for author in r.get_apparent_authors():
email = author.split(' ')[-1]
mailmapped = str_dict_replace(email, mailmap)
contributors.add(mailmapped)
authors_file = open(os.path.join(topdir, 'Authors'),
'r').read()
missing = set()
for contributor in contributors:
if contributor == 'glance-core':
continue
if not contributor in authors_file:
missing.add(contributor)
self.assertTrue(len(missing) == 0,
'%r not listed in Authors' % missing)
finally:
tree.unlock()

View File

@@ -324,41 +324,33 @@ class TestSwiftBackend(unittest.TestCase):
SwiftBackend.add,
2, image_swift, SWIFT_OPTIONS)
def _assertOptionRequiredForSwift(self, key):
image_swift = StringIO.StringIO("nevergonnamakeit")
options = SWIFT_OPTIONS.copy()
del options[key]
self.assertRaises(BackendException, SwiftBackend.add,
2, image_swift, options)
def test_add_no_user(self):
"""
Tests that adding options without user raises
an appropriate exception
"""
image_swift = StringIO.StringIO("nevergonnamakeit")
options = SWIFT_OPTIONS.copy()
del options['swift_store_user']
self.assertRaises(BackendException,
SwiftBackend.add,
2, image_swift, options)
self._assertOptionRequiredForSwift('swift_store_user')
def test_no_key(self):
"""
Tests that adding options without key raises
an appropriate exception
"""
image_swift = StringIO.StringIO("nevergonnamakeit")
options = SWIFT_OPTIONS.copy()
del options['swift_store_key']
self.assertRaises(BackendException,
SwiftBackend.add,
2, image_swift, options)
self._assertOptionRequiredForSwift('swift_store_key')
def test_add_no_auth_address(self):
"""
Tests that adding options without auth address raises
an appropriate exception
"""
image_swift = StringIO.StringIO("nevergonnamakeit")
options = SWIFT_OPTIONS.copy()
del options['swift_store_auth_address']
self.assertRaises(BackendException,
SwiftBackend.add,
2, image_swift, options)
self._assertOptionRequiredForSwift('swift_store_auth_address')
def test_delete(self):
"""

View File

@@ -0,0 +1,50 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import unittest
import webob
from glance.api import versions
class VersionsTest(unittest.TestCase):
def setUp(self):
super(VersionsTest, self).setUp()
def tearDown(self):
super(VersionsTest, self).tearDown()
def test_get_version_list(self):
req = webob.Request.blank('/')
req.accept = "application/json"
options = {'bind_host': '0.0.0.0',
'bind_port': 9292}
res = req.get_response(versions.Controller(options))
self.assertEqual(res.status_int, 300)
self.assertEqual(res.content_type, "application/json")
results = json.loads(res.body)["versions"]
expected = [
{
"id": "v1.0",
"status": "CURRENT",
"links": [
{
"rel": "self",
"href": "http://0.0.0.0:9292/v1/"}]}]
self.assertEqual(results, expected)

View File

@@ -22,7 +22,18 @@ import socket
import subprocess
def execute(cmd):
def execute(cmd, raise_error=True):
"""
Executes a command in a subprocess. Returns a tuple
of (exitcode, out, err), where out is the string output
from stdout and err is the string output from stderr when
executing the command.
:param cmd: Command string to execute
:param raise_error: If returncode is not 0 (success), then
raise a RuntimeError? Default: True)
"""
env = os.environ.copy()
# Make sure that we use the programs in the
@@ -37,7 +48,7 @@ def execute(cmd):
result = process.communicate()
(out, err) = result
exitcode = process.returncode
if process.returncode != 0:
if process.returncode != 0 and raise_error:
msg = "Command %(cmd)s did not succeed. Returned an exit "\
"code of %(exitcode)d."\
"\n\nSTDOUT: %(out)s"\

View File

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