Merge latest with latest Glance.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
*.pyc
|
||||
glance.egg-info
|
||||
glance.sqlite
|
||||
tests.sqlite
|
||||
*.glance-venv
|
||||
dist/
|
||||
ChangeLog
|
||||
|
||||
11
.mailmap
Normal file
11
.mailmap
Normal file
@@ -0,0 +1,11 @@
|
||||
# Format is:
|
||||
# <preferred e-mail> <other e-mail 1>
|
||||
# <preferred e-mail> <other e-mail 2>
|
||||
<corywright@gmail.com> <cory.wright@rackspace.com>
|
||||
<jsuh@isi.edu> <jsuh@bespin>
|
||||
<josh@jk0.org> <josh.kearney@rackspace.com>
|
||||
<rconradharris@gmail.com> <rick.harris@rackspace.com>
|
||||
<rconradharris@gmail.com> <rick@quasar.racklabs.com>
|
||||
<rick@openstack.org> <rclark@chat-blanc>
|
||||
<soren.hansen@rackspace.com> <soren@linux2go.dk>
|
||||
<soren.hansen@rackspace.com> <soren@openstack.org>
|
||||
20
Authors
Normal file
20
Authors
Normal file
@@ -0,0 +1,20 @@
|
||||
Andrey Brindeyev <abrindeyev@griddynamics.com>
|
||||
Brian Waldon <brian.waldon@rackspace.com>
|
||||
Christopher MacGown <chris@slicehost.com>
|
||||
Cory Wright <corywright@gmail.com>
|
||||
Dan Prince <dan.prince@rackspace.com>
|
||||
Donal Lafferty <donal.lafferty@citrix.com>
|
||||
Eldar Nugaev <enugaev@griddynamics.com>
|
||||
Ewan Mellor <ewan.mellor@citrix.com>
|
||||
Jay Pipes <jaypipes@gmail.com>
|
||||
Jinwoo 'Joseph' Suh <jsuh@isi.edu>
|
||||
Josh Kearney <josh@jk0.org>
|
||||
Ken Pepple <ken.pepple@gmail.com>
|
||||
Matt Dietz <matt.dietz@rackspace.com>
|
||||
Monty Taylor <mordred@inaugust.com>
|
||||
Rick Clark <rick@openstack.org>
|
||||
Rick Harris <rconradharris@gmail.com>
|
||||
Soren Hansen <soren.hansen@rackspace.com>
|
||||
Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp>
|
||||
Thierry Carrez <thierry@openstack.org>
|
||||
Vishvananda Ishaya <vishvananda@gmail.com>
|
||||
44
bin/glance
44
bin/glance
@@ -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]
|
||||
|
||||
@@ -133,7 +133,7 @@ def do_start(server, options, args):
|
||||
pid_file = '/var/run/glance/%s.pid' % server
|
||||
else:
|
||||
pid_file = os.path.abspath(options['pid_file'])
|
||||
conf_file = config.find_config_file(options, args)
|
||||
conf_file = config.find_config_file(server, options, args)
|
||||
if not conf_file:
|
||||
sys.exit("Could not find any configuration file to use!")
|
||||
launch_args = [(conf_file, pid_file)]
|
||||
|
||||
@@ -17,10 +17,34 @@
|
||||
Configuring Glance
|
||||
==================
|
||||
|
||||
Glance has a number of options that you can use to configure the Glance API
|
||||
server, the Glance Registry server, and the various storage backends that
|
||||
Glance can use to store images.
|
||||
|
||||
Most configuration is done via configuration files, with the Glance API
|
||||
server and Glance Registry server using separate configuration files.
|
||||
|
||||
When starting up a Glance server, you can specify the configuration file to
|
||||
use (see `the documentation on controller Glance servers <controllingservers>`_).
|
||||
If you do **not** specify a configuration file, Glance will look in the following
|
||||
directories for a configuration file, in order:
|
||||
|
||||
* ``$CWD``
|
||||
* ``~/.glance``
|
||||
* ``~/``
|
||||
* ``/etc/glance``
|
||||
* ``/etc``
|
||||
|
||||
The Glance API server configuration file should be named ``glance-api.conf``.
|
||||
Similarly, the Glance Registry server configuration file should be named
|
||||
``glance-registry.conf``. If you installed Glance via your operating system's
|
||||
package management system, it is likely that you will have sample
|
||||
configuration files installed in ``/etc/glance``.
|
||||
|
||||
In addition to this documentation page, you can check the
|
||||
``etc/glance.conf.sample`` sample configuration file distributed with Glance
|
||||
for an example configuration file with detailed comments on what each options
|
||||
does.
|
||||
``etc/glance-api.conf`` and ``etc/glance-registry.conf`` sample configuration
|
||||
files distributed with Glance for example configuration files for each server
|
||||
application with detailed comments on what each options does.
|
||||
|
||||
Common Configuration Options in Glance
|
||||
--------------------------------------
|
||||
@@ -56,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
33
etc/glance-registry.conf
Normal file
@@ -0,0 +1,33 @@
|
||||
[DEFAULT]
|
||||
# Show more verbose log output (sets INFO log level output)
|
||||
verbose = True
|
||||
|
||||
# Show debugging output in logs (sets DEBUG log level output)
|
||||
debug = False
|
||||
|
||||
# Address to bind the registry server
|
||||
bind_host = 0.0.0.0
|
||||
|
||||
# Port the bind the registry server to
|
||||
bind_port = 9191
|
||||
|
||||
# Log to this file. Make sure you do not set the same log
|
||||
# file for both the API and registry servers!
|
||||
log_file = /var/log/glance/registry.log
|
||||
|
||||
# SQLAlchemy connection string for the reference implementation
|
||||
# registry server. Any valid SQLAlchemy connection string is fine.
|
||||
# See: http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html#sqlalchemy.create_engine
|
||||
sql_connection = sqlite:///glance.sqlite
|
||||
|
||||
# Period in seconds after which SQLAlchemy should reestablish its connection
|
||||
# to the database.
|
||||
#
|
||||
# MySQL uses a default `wait_timeout` of 8 hours, after which it will drop
|
||||
# idle connections. This can result in 'MySQL Gone Away' exceptions. If you
|
||||
# notice this, you can lower this value to ensure that SQLAlchemy reconnects
|
||||
# before MySQL can drop the connection.
|
||||
sql_idle_timeout = 3600
|
||||
|
||||
[app:glance-registry]
|
||||
paste.app_factory = glance.registry.server:app_factory
|
||||
16
glance/api/__init__.py
Normal file
16
glance/api/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
16
glance/api/middleware/__init__.py
Normal file
16
glance/api/middleware/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
134
glance/api/middleware/version_negotiation.py
Normal file
134
glance/api/middleware/version_negotiation.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
A filter middleware that inspects the requested URI for a version string
|
||||
and/or Accept headers and attempts to negotiate an API controller to
|
||||
return
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
import routes
|
||||
|
||||
from glance.api import v1
|
||||
from glance.api import versions
|
||||
from glance.common import wsgi
|
||||
|
||||
logger = logging.getLogger('glance.api.middleware.version_negotiation')
|
||||
|
||||
|
||||
class VersionNegotiationFilter(wsgi.Middleware):
|
||||
|
||||
def __init__(self, app, options):
|
||||
self.versions_app = versions.Controller(options)
|
||||
self.version_uri_regex = re.compile(r"^v(\d+)\.?(\d+)?")
|
||||
self.options = options
|
||||
super(VersionNegotiationFilter, self).__init__(app)
|
||||
|
||||
def process_request(self, req):
|
||||
"""
|
||||
If there is a version identifier in the URI, simply
|
||||
return the correct API controller, otherwise, if we
|
||||
find an Accept: header, process it
|
||||
"""
|
||||
# See if a version identifier is in the URI passed to
|
||||
# us already. If so, simply return the right version
|
||||
# API controller
|
||||
logger.debug("Processing request: %s %s Accept: %s",
|
||||
req.method, req.path, req.accept)
|
||||
|
||||
# If the request is for /versions, just return the versions container
|
||||
if req.path_info_peek() == "versions":
|
||||
return self.versions_app
|
||||
|
||||
match = self._match_version_string(req.path_info_peek(), req)
|
||||
if match:
|
||||
if (req.environ['api.major_version'] == 1 and
|
||||
req.environ['api.minor_version'] == 0):
|
||||
logger.debug("Matched versioned URI. Version: %d.%d",
|
||||
req.environ['api.major_version'],
|
||||
req.environ['api.minor_version'])
|
||||
# Strip the version from the path
|
||||
req.path_info_pop()
|
||||
return None
|
||||
else:
|
||||
logger.debug("Unknown version in versioned URI: %d.%d. "
|
||||
"Returning version choices.",
|
||||
req.environ['api.major_version'],
|
||||
req.environ['api.minor_version'])
|
||||
return self.versions_app
|
||||
|
||||
accept = req.headers['Accept']
|
||||
if accept.startswith('application/vnd.openstack.images-'):
|
||||
token_loc = len('application/vnd.openstack.images-')
|
||||
accept_version = accept[token_loc:]
|
||||
match = self._match_version_string(accept_version, req)
|
||||
if match:
|
||||
if (req.environ['api.major_version'] == 1 and
|
||||
req.environ['api.minor_version'] == 0):
|
||||
logger.debug("Matched versioned media type. "
|
||||
"Version: %d.%d",
|
||||
req.environ['api.major_version'],
|
||||
req.environ['api.minor_version'])
|
||||
return None
|
||||
else:
|
||||
logger.debug("Unknown version in accept header: %s.%s..."
|
||||
"returning version choices.",
|
||||
req.environ['api.major_version'],
|
||||
req.environ['api.minor_version'])
|
||||
return self.versions_app
|
||||
else:
|
||||
if req.accept not in ('*/*', ''):
|
||||
logger.debug("Unknown accept header: %s..."
|
||||
"returning version choices.", req.accept)
|
||||
return self.versions_app
|
||||
return None
|
||||
|
||||
def _match_version_string(self, subject, req):
|
||||
"""
|
||||
Given a subject string, tries to match a major and/or
|
||||
minor version number. If found, sets the api.major_version
|
||||
and api.minor_version environ variables.
|
||||
|
||||
Returns True if there was a match, false otherwise.
|
||||
|
||||
:param subject: The string to check
|
||||
:param req: Webob.Request object
|
||||
"""
|
||||
match = self.version_uri_regex.match(subject)
|
||||
if match:
|
||||
major_version, minor_version = match.groups(0)
|
||||
major_version = int(major_version)
|
||||
minor_version = int(minor_version)
|
||||
req.environ['api.major_version'] = major_version
|
||||
req.environ['api.minor_version'] = minor_version
|
||||
return match is not None
|
||||
|
||||
|
||||
def filter_factory(global_conf, **local_conf):
|
||||
"""
|
||||
Factory method for paste.deploy
|
||||
"""
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
|
||||
def filter(app):
|
||||
return VersionNegotiationFilter(app, conf)
|
||||
|
||||
return filter
|
||||
48
glance/api/v1/__init__.py
Normal file
48
glance/api/v1/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
import routes
|
||||
|
||||
from glance.api.v1 import images
|
||||
from glance.common import wsgi
|
||||
|
||||
logger = logging.getLogger('glance.api.v1')
|
||||
|
||||
|
||||
class API(wsgi.Router):
|
||||
|
||||
"""WSGI router for Glance v1 API requests."""
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = options
|
||||
mapper = routes.Mapper()
|
||||
controller = images.Controller(options)
|
||||
mapper.resource("image", "images", controller=controller,
|
||||
collection={'detail': 'GET'})
|
||||
mapper.connect("/", controller=controller, action="index")
|
||||
mapper.connect("/images/{id}", controller=controller, action="meta",
|
||||
conditions=dict(method=["HEAD"]))
|
||||
super(API, self).__init__(mapper)
|
||||
|
||||
|
||||
def app_factory(global_conf, **local_conf):
|
||||
"""paste.deploy app factory for creating Glance API server apps"""
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
return API(conf)
|
||||
@@ -16,17 +16,7 @@
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
=================
|
||||
Glance API Server
|
||||
=================
|
||||
|
||||
Configuration Options
|
||||
---------------------
|
||||
|
||||
`default_store`: When no x-image-meta-store header is sent for a
|
||||
`POST /images` request, this store will be used
|
||||
for storing the image data. Default: 'file'
|
||||
|
||||
/images endpoint for Glance v1 API
|
||||
"""
|
||||
|
||||
import httplib
|
||||
@@ -34,7 +24,6 @@ import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import routes
|
||||
from webob import Response
|
||||
from webob.exc import (HTTPNotFound,
|
||||
HTTPConflict,
|
||||
@@ -51,15 +40,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
69
glance/api/versions.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Controller that returns information on the Glance API versions
|
||||
"""
|
||||
|
||||
import httplib
|
||||
import json
|
||||
|
||||
import webob.dec
|
||||
|
||||
from glance.common import wsgi
|
||||
|
||||
|
||||
class Controller(object):
|
||||
|
||||
"""
|
||||
A controller that produces information on the Glance API versions.
|
||||
"""
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = options
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
"""Respond to a request for all OpenStack API versions."""
|
||||
version_objs = [
|
||||
{
|
||||
"id": "v1.0",
|
||||
"status": "CURRENT",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": self.get_href()}]}]
|
||||
|
||||
body = json.dumps(dict(versions=version_objs))
|
||||
|
||||
response = webob.Response(request=req,
|
||||
status=httplib.MULTIPLE_CHOICES,
|
||||
content_type='application/json')
|
||||
response.body = body
|
||||
|
||||
return response
|
||||
|
||||
def get_href(self):
|
||||
return "http://%s:%s/v1/" % (self.options['bind_host'],
|
||||
self.options['bind_port'])
|
||||
|
||||
|
||||
def app_factory(global_conf, **local_conf):
|
||||
"""paste.deploy app factory for creating Glance API versions apps"""
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
return Controller(conf)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -41,9 +41,12 @@ class TestMiscellaneous(functional.FunctionalTest):
|
||||
"""
|
||||
|
||||
self.cleanup()
|
||||
api_port, reg_port, conf_file = self.start_servers()
|
||||
self.start_servers()
|
||||
|
||||
cmd = "curl -g http://0.0.0.0:%d/images" % api_port
|
||||
api_port = self.api_port
|
||||
registry_port = self.registry_port
|
||||
|
||||
cmd = "curl -g http://0.0.0.0:%d/v1/images" % api_port
|
||||
|
||||
exitcode, out, err = execute(cmd)
|
||||
|
||||
@@ -53,7 +56,7 @@ class TestMiscellaneous(functional.FunctionalTest):
|
||||
cmd = "curl -X POST -H 'Content-Type: application/octet-stream' "\
|
||||
"-H 'X-Image-Meta-Name: ImageName' "\
|
||||
"-H 'X-Image-Meta-Disk-Format: Invalid' "\
|
||||
"http://0.0.0.0:%d/images" % api_port
|
||||
"http://0.0.0.0:%d/v1/images" % api_port
|
||||
ignored, out, err = execute(cmd)
|
||||
|
||||
self.assertTrue('Invalid disk format' in out,
|
||||
|
||||
@@ -29,7 +29,7 @@ import webob
|
||||
|
||||
from glance.common import exception
|
||||
from glance.registry import server as rserver
|
||||
from glance import server
|
||||
from glance.api import v1 as server
|
||||
import glance.store
|
||||
import glance.store.filesystem
|
||||
import glance.store.http
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
77
tests/unit/test_misc.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010-2011 OpenStack, LLC
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
|
||||
def parse_mailmap(mailmap='.mailmap'):
|
||||
mapping = {}
|
||||
if os.path.exists(mailmap):
|
||||
fp = open(mailmap, 'r')
|
||||
for l in fp:
|
||||
l = l.strip()
|
||||
if not l.startswith('#') and ' ' in l:
|
||||
canonical_email, alias = l.split(' ')
|
||||
mapping[alias] = canonical_email
|
||||
return mapping
|
||||
|
||||
|
||||
def str_dict_replace(s, mapping):
|
||||
for s1, s2 in mapping.iteritems():
|
||||
s = s.replace(s1, s2)
|
||||
return s
|
||||
|
||||
|
||||
class AuthorsTestCase(unittest.TestCase):
|
||||
def test_authors_up_to_date(self):
|
||||
topdir = os.path.normpath(os.path.dirname(__file__) + '/../../')
|
||||
if os.path.exists(os.path.join(topdir, '.bzr')):
|
||||
contributors = set()
|
||||
|
||||
mailmap = parse_mailmap(os.path.join(topdir, '.mailmap'))
|
||||
|
||||
import bzrlib.workingtree
|
||||
tree = bzrlib.workingtree.WorkingTree.open(topdir)
|
||||
tree.lock_read()
|
||||
try:
|
||||
parents = tree.get_parent_ids()
|
||||
g = tree.branch.repository.get_graph()
|
||||
for p in parents:
|
||||
rev_ids = [r for r, _ in g.iter_ancestry(parents)
|
||||
if r != "null:"]
|
||||
revs = tree.branch.repository.get_revisions(rev_ids)
|
||||
for r in revs:
|
||||
for author in r.get_apparent_authors():
|
||||
email = author.split(' ')[-1]
|
||||
mailmapped = str_dict_replace(email, mailmap)
|
||||
contributors.add(mailmapped)
|
||||
|
||||
authors_file = open(os.path.join(topdir, 'Authors'),
|
||||
'r').read()
|
||||
|
||||
missing = set()
|
||||
for contributor in contributors:
|
||||
if contributor == 'glance-core':
|
||||
continue
|
||||
if not contributor in authors_file:
|
||||
missing.add(contributor)
|
||||
|
||||
self.assertTrue(len(missing) == 0,
|
||||
'%r not listed in Authors' % missing)
|
||||
finally:
|
||||
tree.unlock()
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
50
tests/unit/test_versions.py
Normal file
50
tests/unit/test_versions.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010-2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import webob
|
||||
|
||||
from glance.api import versions
|
||||
|
||||
|
||||
class VersionsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
super(VersionsTest, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
super(VersionsTest, self).tearDown()
|
||||
|
||||
def test_get_version_list(self):
|
||||
req = webob.Request.blank('/')
|
||||
req.accept = "application/json"
|
||||
options = {'bind_host': '0.0.0.0',
|
||||
'bind_port': 9292}
|
||||
res = req.get_response(versions.Controller(options))
|
||||
self.assertEqual(res.status_int, 300)
|
||||
self.assertEqual(res.content_type, "application/json")
|
||||
results = json.loads(res.body)["versions"]
|
||||
expected = [
|
||||
{
|
||||
"id": "v1.0",
|
||||
"status": "CURRENT",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://0.0.0.0:9292/v1/"}]}]
|
||||
self.assertEqual(results, expected)
|
||||
@@ -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"\
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user