Merge trunk and resolve conflict

This commit is contained in:
jaypipes@gmail.com 2011-03-16 12:11:56 -04:00
commit 09916c2a40
20 changed files with 1311 additions and 45 deletions

572
bin/glance Executable file

@ -0,0 +1,572 @@
#!/usr/bin/env python
# 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.
"""
This is the administration program for Glance. It is simply a command-line
interface for adding, modifying, and retrieving information about the images
stored in one or more Glance nodes.
"""
import optparse
import os
import re
import sys
import time
# If ../glance/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
os.pardir,
os.pardir))
if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')):
sys.path.insert(0, possible_topdir)
from glance import client
from glance import version
from glance.common import exception
from glance.common import utils
SUCCESS = 0
FAILURE = 1
def get_image_fields_from_args(args):
"""
Validate the set of arguments passed as field name/value pairs
and return them as a mapping.
"""
fields = {}
for arg in args:
pieces = arg.strip(',').split('=')
if len(pieces) != 2:
msg = ("Arguments should be in the form of field=value. "
"You specified %s." % arg)
raise RuntimeError(msg)
fields[pieces[0]] = pieces[1]
fields = dict([(k.lower().replace('-', '_'), v)
for k, v in fields.items()])
return fields
def print_image_formatted(client, image):
"""
Formatted print of image metadata.
:param client: The Glance client object
:param image: The image metadata
"""
print "URI: %s://%s/images/%s" % (client.use_ssl and "https" or "http",
client.host,
image['id'])
print "Id: %s" % image['id']
print "Public: " + (image['is_public'] and "Yes" or "No")
print "Name: %s" % image['name']
print "Size: %d" % int(image['size'])
print "Location: %s" % image['location']
print "Disk format: %s" % image['disk_format']
print "Container format: %s" % image['container_format']
if len(image['properties']) > 0:
for k, v in image['properties'].items():
print "Property '%s': %s" % (k, v)
def image_add(options, args):
"""
%(prog)s add [options] <field1=value1 field2=value2 ...> [ < /path/to/image ]
Adds a new image to Glance. Specify metadata fields as arguments.
SPECIFYING IMAGE METADATA
===============================================================================
All field/value pairs are converted into a mapping that is passed
to Glance that represents the metadata for an image.
Field names of note:
id Optional. If not specified, an image identifier will be
automatically assigned.
name Required. A name for the image.
size Optional. Should be size in bytes of the image if
specified.
is_public Optional. If specified, interpreted as a boolean value
and sets or unsets the image's availability to the public.
The default value is False.
disk_format Optional. Possible values are 'vhd','vmdk','raw', 'qcow2',
and 'ami'. Default value is 'raw'.
container_format Optional. Possible values are 'ovf' and 'ami'.
Default value is 'ovf'.
location Optional. When specified, should be a readable location
in the form of a URI: $STORE://LOCATION. For example, if
the image data is stored in a file on the local
filesystem at /usr/share/images/some.image.tar.gz
you would specify:
location=file:///usr/share/images/some.image.tar.gz
Any other field names are considered to be custom properties so be careful
to spell field names correctly. :)
STREAMING IMAGE DATA
===============================================================================
If the location field is not specified, you can stream an image file on
the command line using standard redirection. For example:
%(prog)s add name="Ubuntu 10.04 LTS 5GB" < /tmp/images/myimage.tar.gz
EXAMPLES
===============================================================================
%(prog)s add name="My Image" disk_format=raw container_format=ovf \\
location=http://images.ubuntu.org/images/lucid-10.04-i686.iso \\
distro="Ubuntu 10.04 LTS"
%(prog)s add name="My Image" distro="Ubuntu 10.04 LTS" < /tmp/myimage.iso"""
c = get_client(options)
try:
fields = get_image_fields_from_args(args)
except RuntimeError, e:
print e
return FAILURE
if 'name' not in fields.keys():
print "Please specify a name for the image using name=VALUE"
return FAILURE
image_meta = {'name': fields.pop('name'),
'is_public': utils.bool_from_string(
fields.pop('is_public', False)),
'disk_format': fields.pop('disk_format', 'raw'),
'container_format': fields.pop('container_format', 'ovf')}
# Strip any args that are not supported
unsupported_fields = ['status']
for field in unsupported_fields:
if field in fields.keys():
print 'Found non-settable field %s. Removing.' % field
fields.pop(field)
if 'location' in fields.keys():
image_meta['location'] = fields.pop('location')
# We need either a location or image data/stream to add...
image_location = image_meta.get('location')
image_data = None
if not image_location:
# Grab the image data stream from stdin or redirect,
# otherwise error out
image_data = sys.stdin
else:
# Ensure no image data has been given
if not sys.stdin.isatty():
print "Either supply a location=LOCATION argument or supply image "
print "data via a redirect. You have supplied BOTH image data "
print "AND a location."
return FAILURE
# Add custom attributes, which are all the arguments remaining
image_meta['properties'] = fields
if not options.dry_run:
try:
image_meta = c.add_image(image_meta, image_data)
image_id = image_meta['id']
print "Added new image with ID: %s" % image_id
if options.verbose:
print "Returned the following metadata for the new image:"
for k, v in sorted(image_meta.items()):
print " %(k)30s => %(v)s" % locals()
except client.ClientConnectionError, e:
host = options.host
port = options.port
print ("Failed to connect to the Glance API server "
"%(host)s:%(port)d. Is the server running?" % locals())
if options.verbose:
pieces = str(e).split('\n')
for piece in pieces:
print piece
return FAILURE
except Exception, e:
print "Failed to add image. Got error:"
pieces = str(e).split('\n')
for piece in pieces:
print piece
return FAILURE
else:
print "Dry run. We would have done the following:"
print "Add new image with metadata:"
for k, v in sorted(image_meta.items()):
print " %(k)30s => %(v)s" % locals()
return SUCCESS
def image_update(options, args):
"""
%(prog)s update [options] <ID> <field1=value1 field2=value2 ...>
Updates an image's metadata in Glance. Specify metadata fields as arguments.
All field/value pairs are converted into a mapping that is passed
to Glance that represents the metadata for an image.
Field names that can be specified:
name A name for 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
container_format Format of the container
All other field names are considered to be custom properties so be careful
to spell field names correctly. :)"""
c = get_client(options)
try:
image_id = args.pop(0)
except IndexError:
print "Please specify the ID of the image you wish to update "
print "as the first argument"
return FAILURE
try:
fields = get_image_fields_from_args(args)
except RuntimeError, e:
print e
return FAILURE
image_meta = {}
# Strip any args that are not supported
nonmodifiable_fields = ['created_on', 'deleted_on', 'deleted',
'updated_on', 'size', 'status']
for field in nonmodifiable_fields:
if field in fields.keys():
print 'Found non-modifiable field %s. Removing.' % field
fields.pop(field)
base_image_fields = ['disk_format', 'container_format',
'location']
for field in base_image_fields:
fvalue = fields.pop(field, None)
if fvalue:
image_meta[field] = fvalue
# Have to handle "boolean" values specially...
if 'is_public' in fields:
image_meta['is_public'] = utils.int_from_bool_as_string(
fields.pop('is_public'))
# Add custom attributes, which are all the arguments remaining
image_meta['properties'] = fields
if not options.dry_run:
try:
image_meta = c.update_image(image_id, image_meta=image_meta)
print "Updated image %s" % image_id
if options.verbose:
print "Updated image metadata for image %s:" % image_id
print_image_formatted(c, image_meta)
except exception.NotFound:
print "No image with ID %s was found" % image_id
return FAILURE
except Exception, e:
print "Failed to update image. Got error:"
pieces = str(e).split('\n')
for piece in pieces:
print piece
return FAILURE
else:
print "Dry run. We would have done the following:"
print "Update existing image with metadata:"
for k, v in sorted(image_meta.items()):
print " %(k)30s => %(v)s" % locals()
return SUCCESS
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:
print "Please specify the ID of the image you wish to delete "
print "as the first argument"
return FAILURE
try:
c.delete_image(image_id)
print "Deleted image %s" % image_id
return SUCCESS
except exception.NotFound:
print "No image with ID %s was found" % image_id
return FAILURE
def image_show(options, args):
"""
%(prog)s show [options] <ID>
Shows image metadata for an image in Glance"""
c = get_client(options)
try:
if len(args) > 0:
image_id = args[0]
else:
print "Please specify the image identifier as the "
print "first argument. Example: "
print "$> glance-admin show 12345"
return FAILURE
image = c.get_image_meta(image_id)
print_image_formatted(c, image)
return SUCCESS
except exception.NotFound:
print "No image with ID %s was found" % image_id
return FAILURE
except Exception, e:
print "Failed to show image. Got error:"
pieces = str(e).split('\n')
for piece in pieces:
print piece
return FAILURE
def images_index(options, args):
"""
%(prog)s index [options]
Returns basic information for all public images
a Glance server knows about"""
c = get_client(options)
try:
images = c.get_images()
if len(images) == 0:
print "No public images found."
return SUCCESS
print "Found %d public images..." % len(images)
print "%-16s %-30s %-20s %-20s %-14s" % (("ID"),
("Name"),
("Disk Format"),
("Container Format"),
("Size"))
print ('-' * 16) + " " + ('-' * 30) + " "\
+ ('-' * 20) + " " + ('-' * 20) + " " + ('-' * 14)
for image in images:
print "%-16s %-30s %-20s %-20s %14d" % (image['id'],
image['name'],
image['disk_format'],
image['container_format'],
int(image['size']))
return SUCCESS
except Exception, e:
print "Failed to show index. Got error:"
pieces = str(e).split('\n')
for piece in pieces:
print piece
return FAILURE
def images_detailed(options, args):
"""
%(prog)s details [options]
Returns detailed information for all public images
a Glance server knows about"""
c = get_client(options)
try:
images = c.get_images_detailed()
if len(images) == 0:
print "No public images found."
return SUCCESS
num_images = len(images)
print "Found %d public images..." % num_images
cur_image = 1
for image in images:
print "=" * 80
print_image_formatted(c, image)
if cur_image == num_images:
print "=" * 80
cur_image += 1
return SUCCESS
except Exception, e:
print "Failed to show details. Got error:"
pieces = str(e).split('\n')
for piece in pieces:
print piece
return FAILURE
def images_clear(options, args):
"""
%(prog)s clear [options]
Deletes all images from a Glance server"""
c = get_client(options)
images = c.get_images()
for image in images:
if options.verbose:
print 'Deleting image %s "%s" ...' % (image['id'], image['name']),
try:
c.delete_image(image['id'])
if options.verbose:
print 'done'
except Exception, e:
print 'Failed to delete image %s' % image['id']
print e
return FAILURE
return SUCCESS
def get_client(options):
"""
Returns a new client object to a Glance server
specified by the --host and --port options
supplied to the CLI
"""
return client.Client(host=options.host,
port=options.port)
def create_options(parser):
"""
Sets up the CLI and config-file options that may be
parsed and program commands.
:param parser: The option parser
"""
parser.add_option('-v', '--verbose', default=False, action="store_true",
help="Print more verbose output")
parser.add_option('-H', '--host', metavar="ADDRESS", default="0.0.0.0",
help="Address of Glance API host. "
"Default: %default")
parser.add_option('-p', '--port', dest="port", metavar="PORT",
type=int, default=9292,
help="Port the Glance API host listens on. "
"Default: %default")
parser.add_option('--dry-run', default=False, action="store_true",
help="Don't actually execute the command, just print "
"output showing what WOULD happen.")
def parse_options(parser, cli_args):
"""
Returns the parsed CLI options, command to run and its arguments, merged
with any same-named options found in a configuration file
:param parser: The option parser
"""
COMMANDS = {'help': print_help,
'add': image_add,
'update': image_update,
'delete': image_delete,
'index': images_index,
'details': images_detailed,
'show': image_show,
'clear': images_clear}
if not cli_args:
cli_args.append('-h') # Show options in usage output...
(options, args) = parser.parse_args(cli_args)
if not args:
parser.print_usage()
sys.exit(0)
else:
command_name = args.pop(0)
if command_name not in COMMANDS.keys():
sys.exit("Unknown command: %s" % command_name)
command = COMMANDS[command_name]
return (options, command, args)
def print_help(options, args):
"""
Print help specific to a command
"""
COMMANDS = {'add': image_add,
'update': image_update,
'delete': image_delete,
'index': images_index,
'details': images_detailed,
'show': image_show,
'clear': images_clear}
if len(args) != 1:
sys.exit("Please specify a command")
command = args.pop()
if command not in COMMANDS.keys():
parser.print_usage()
if args:
sys.exit("Unknown command: %s" % command)
print COMMANDS[command].__doc__ % {'prog': os.path.basename(sys.argv[0])}
if __name__ == '__main__':
usage = """
%prog <command> [options] [args]
Commands:
help <command> Output help for one of the commands below
add Adds a new image to Glance
update Updates an image's metadata in Glance
delete Deletes an image from Glance
index Return brief information about images in Glance
details Return detailed information about images in
Glance
show Show detailed information about an image in
Glance
clear Removes all images and metadata from Glance
"""
oparser = optparse.OptionParser(version='%%prog %s'
% version.version_string(),
usage=usage.strip())
create_options(oparser)
(options, command, args) = parse_options(oparser, sys.argv[1:])
try:
start_time = time.time()
result = command(options, args)
end_time = time.time()
if options.verbose:
print "Completed in %-0.4f sec." % (end_time - start_time)
sys.exit(result)
except (RuntimeError, NotImplementedError), e:
print "ERROR: ", e

374
doc/source/glance.rst Normal file

@ -0,0 +1,374 @@
..
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.
Using the Glance CLI Tool
=========================
Glance ships with a command-line tool for quering 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
Show detailed help information about a specific command
* add
Adds an image to Glance
* update
Updates an image's stored metadata in Glance
* delete
Deletes an image and its metadata from Glance
* index
Lists brief information about *public* images that Glance knows about
* details
Lists detailed information about *public* images that Glance knows about
* show
Lists detailed information about a specific image
* clear
Destroys *all* images and their associated metadata
This document describes how to use the ``glance`` tool for each of
the above commands.
The ``help`` command
--------------------
Issuing the ``help`` command with a ``<COMMAND>`` argument shows detailed help
about a specific command. Running ``glance`` without any arguments shows
a brief help message, like so::
$> glance
Usage: glance <command> [options] [args]
Commands:
help <command> Output help for one of the commands below
add Adds a new image to Glance
update Updates an image's metadata in Glance
delete Deletes an image from Glance
index Return brief information about images in Glance
details Return detailed information about images in
Glance
show Show detailed information about an image in
Glance
clear Removes all images and metadata from Glance
Options:
--version show program's version number and exit
-h, --help show this help message and exit
-v, --verbose Print more verbose output
-H ADDRESS, --host=ADDRESS
Address of Glance API host. Default: example.com
-p PORT, --port=PORT Port the Glance API host listens on. Default: 9292
--dry-run Don't actually execute the command, just print output
showing what WOULD happen.
With a ``<COMMAND>`` argument, more information on the command is shown,
like so::
$> glance help update
glance update [options] <ID> <field1=value1 field2=value2 ...>
Updates an image's metadata in Glance. Specify metadata fields as arguments.
All field/value pairs are converted into a mapping that is passed
to Glance that represents the metadata for an image.
Field names that can be specified:
name A name for 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
container_format Format of the container
All other field names are considered to be custom properties so be careful
to spell field names correctly. :)
The ``add`` command
-------------------
The ``add`` command is used to do both of the following:
* Store virtual machine image data and metadata about that image in Glance
* Let Glance know about an existing virtual machine image that may be stored
somewhere else
We cover both use cases below.
Store virtual machine image data and metadata
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When adding an actual virtual machine image to Glance, you use the ``add``
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``.
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::
$> glance add name="My Image" is_public=true < /tmp/images/myimage.tar.gz
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
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
Added new image with ID: 4
Returned the following metadata for the new image:
container_format => ovf
created_at => 2011-02-22T19:20:53.298556
deleted => False
deleted_at => None
disk_format => raw
id => 4
is_public => True
location => file:///tmp/images/4
name => My Image
properties => {}
size => 58520278
status => active
updated_at => None
Completed in 0.6141 sec.
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
Dry run. We would have done the following:
Add new image with metadata:
container_format => ovf
disk_format => raw
is_public => False
name => Foo
properties => {'is_publi': 'True', 'distro': 'Ubuntu'}
This is useful for detecting problems and for seeing what the default field
values supplied by ``glance`` are. For instance, there was a typo in
the command above (the ``is_public`` field was incorrectly spelled ``is_publi``
which resulted in the image having an ``is_publi`` custom property added to
the image and the *real* ``is_public`` field value being `False` (the default)
and not `True`...
Register a virtual machine image in another location
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sometimes, you already have stored the virtual machine image in some non-Glance
location -- perhaps even a location you have no write access to -- and you want
to tell Glance where this virtual machine image is located and some metadata
about it. The ``add`` command can do this for you.
When registering an image in this way, the only difference is that you do not
use a shell redirect to stream a virtual machine image file into Glance, but
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
Glance using the following::
$> glance --verbose add name="Some web image" location="http://example.com/images/myimage.tar.gz"
Added new image with ID: 1
Returned the following metadata for the new image:
container_format => ovf
created_at => 2011-02-23T00:42:04.688890
deleted => False
deleted_at => None
disk_format => vhd
id => 1
is_public => True
location => http://example.com/images/myimage.tar.gz
name => Some web image
properties => {}
size => 0
status => active
updated_at => None
Completed in 0.0356 sec.
The ``update`` command
----------------------
After uploading/adding a virtual machine image to Glance, it is not possible to
modify the actual virtual machine image -- images are read-only after all --
however, it *is* possible to update any metadata about the image after you add
it to Glance.
The ``update`` command allows you to update the metadata fields of a stored
image. You use this command like so::
glance update <ID> [field1=value1 field2=value2 ...]
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
Updated image 5
Using the ``--verbose`` flag will show you all the updated data about the image::
$> glance --verbose update 5 is_public=true
Updated image 5
Updated image metadata for image 5:
URI: http://example.com/images/5
Id: 5
Public? Yes
Name: My Image
Size: 58520278
Location: file:///tmp/images/5
Disk format: raw
Container format: ovf
Completed in 0.0596 sec.
The ``delete`` command
----------------------
You can delete an image by using the ``delete`` command, shown below::
$> glance --verbose delete 5
Deleted image 5
The ``index`` command
---------------------
The ``index`` command displays brief information about the *public* images
available in Glance, as shown below::
$> glance index
Found 4 public images...
ID Name Disk Format Container Format Size
---------------- ------------------------------ -------------------- -------------------- --------------
1 Ubuntu 10.10 vhd ovf 58520278
2 Ubuntu 10.04 ami ami 58520278
3 Fedora 9 vdi bare 3040
4 Vanilla Linux 2.6.22 qcow2 bare 0
The ``details`` command
-----------------------
The ``details`` command displays detailed information about the *public* images
available in Glance, as shown below::
$> glance details
Found 4 public images...
================================================================================
URI: http://example.com/images/1
Id: 1
Public? Yes
Name: Ubuntu 10.10
Size: 58520278
Location: file:///tmp/images/1
Disk format: vhd
Container format: ovf
Property 'distro_version': 10.10
Property 'distro': Ubuntu
================================================================================
URI: http://example.com/images/2
Id: 2
Public? Yes
Name: Ubuntu 10.04
Size: 58520278
Location: file:///tmp/images/2
Disk format: ami
Container format: ami
Property 'distro_version': 10.04
Property 'distro': Ubuntu
================================================================================
URI: http://example.com/images/3
Id: 3
Public? Yes
Name: Fedora 9
Size: 3040
Location: file:///tmp/images/3
Disk format: vdi
Container format: bare
Property 'distro_version': 9
Property 'distro': Fedora
================================================================================
URI: http://example.com/images/4
Id: 4
Public? Yes
Name: Vanilla Linux 2.6.22
Size: 0
Location: http://example.com/images/vanilla.tar.gz
Disk format: qcow2
Container format: bare
================================================================================
The ``show`` command
--------------------
The ``show`` command displays detailed information about a specific image, specified
with ``<ID>``, as shown below::
$> glance show 3
URI: http://example.com/images/3
Id: 3
Public? Yes
Name: Fedora 9
Size: 3040
Location: file:///tmp/images/3
Disk format: vdi
Container format: bare
Property 'distro_version': 9
Property 'distro': Fedora
The ``clear`` command
---------------------
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
Deleting image 1 "Some web image" ... done
Deleting image 2 "Some other web image" ... done
Completed in 0.0328 sec.

@ -62,6 +62,7 @@ Using Glance
installing
controllingservers
configuring
glance
glanceapi
client

@ -247,10 +247,8 @@ class Client(BaseClient):
:retval The newly-stored image's metadata.
"""
if image_meta is None:
image_meta = {}
headers = utils.image_meta_to_http_headers(image_meta)
headers = utils.image_meta_to_http_headers(image_meta or {})
if image_data:
body = image_data

@ -36,6 +36,37 @@ from glance.common.exception import ProcessExecutionError
TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
def int_from_bool_as_string(subject):
"""
Interpret a string as a boolean and return either 1 or 0.
Any string value in:
('True', 'true', 'On', 'on', '1')
is interpreted as a boolean True.
Useful for JSON-decoded stuff and config file parsing
"""
return bool_from_string(subject) and 1 or 0
def bool_from_string(subject):
"""
Interpret a string as a boolean.
Any string value in:
('True', 'true', 'On', 'on', '1')
is interpreted as a boolean True.
Useful for JSON-decoded stuff and config file parsing
"""
if type(subject) == type(bool):
return subject
if hasattr(subject, 'startswith'): # str or unicode...
if subject.strip().lower() in ('true', 'on', '1'):
return True
return False
def import_class(import_str):
"""Returns a class from a string including module and class"""
mod_str, _sep, class_str = import_str.rpartition('.')

@ -1,7 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# Copyright 2010-2011 OpenStack, LLC
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -20,12 +19,17 @@
Registry API
"""
import logging
from glance.registry import client
logger = logging.getLogger('glance.registry')
def get_registry_client(options):
return client.RegistryClient(options['registry_host'],
int(options['registry_port']))
host = options['registry_host']
port = int(options['registry_port'])
return client.RegistryClient(host, port)
def get_images_list(options):
@ -43,16 +47,51 @@ def get_image_metadata(options, image_id):
return c.get_image(image_id)
def add_image_metadata(options, image_data):
def add_image_metadata(options, image_meta):
if options['debug']:
logger.debug("Adding image metadata...")
_debug_print_metadata(image_meta)
c = get_registry_client(options)
return c.add_image(image_data)
new_image_meta = c.add_image(image_meta)
if options['debug']:
logger.debug("Returned image metadata from call to "
"RegistryClient.add_image():")
_debug_print_metadata(new_image_meta)
return new_image_meta
def update_image_metadata(options, image_id, image_data):
def update_image_metadata(options, image_id, image_meta):
if options['debug']:
logger.debug("Updating image metadata for image %s...", image_id)
_debug_print_metadata(image_meta)
c = get_registry_client(options)
return c.update_image(image_id, image_data)
new_image_meta = c.update_image(image_id, image_meta)
if options['debug']:
logger.debug("Returned image metadata from call to "
"RegistryClient.update_image():")
_debug_print_metadata(new_image_meta)
return new_image_meta
def delete_image_metadata(options, image_id):
logger.debug("Deleting image metadata for image %s...", image_id)
c = get_registry_client(options)
return c.delete_image(image_id)
def _debug_print_metadata(image_meta):
data = image_meta.copy()
properties = data.pop('properties', None)
for key, value in sorted(data.items()):
logger.debug(" %(key)20s: %(value)s" % locals())
if properties:
logger.debug(" %d custom properties...",
len(properties))
for key, value in properties.items():
logger.debug(" %(key)20s: %(value)s" % locals())

@ -24,6 +24,7 @@ Defines interface for DB access
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import exc
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import sessionmaker
@ -91,7 +92,7 @@ def unregister_models():
"""Unregister Models, useful clearing out data before testing"""
global _ENGINE
assert _ENGINE
BASE.metadata.drop_all(engine)
BASE.metadata.drop_all(_ENGINE)
def image_create(context, values):
@ -126,8 +127,7 @@ def image_get(context, image_id, session=None):
filter_by(id=image_id).\
one()
except exc.NoResultFound:
new_exc = exception.NotFound("No model for id %s" % image_id)
raise new_exc.__class__, new_exc, sys.exc_info()[2]
raise exception.NotFound("No image found with ID %s" % image_id)
def image_get_all_public(context):

@ -29,6 +29,9 @@ except ImportError:
from glance.common import exception
logger = logging.getLogger('glance.registry.db.migration')
def db_version(options):
"""Return the database's current migration number
@ -56,7 +59,7 @@ def upgrade(options, version=None):
repo_path = _find_migrate_repo()
sql_connection = options['sql_connection']
version_str = version or 'latest'
logging.info("Upgrading %(sql_connection)s to version %(version_str)s" %
logger.info("Upgrading %(sql_connection)s to version %(version_str)s" %
locals())
return versioning_api.upgrade(sql_connection, repo_path, version)
@ -71,7 +74,7 @@ def downgrade(options, version):
db_version(options) # Ensure db is under migration control
repo_path = _find_migrate_repo()
sql_connection = options['sql_connection']
logging.info("Downgrading %(sql_connection)s to version %(version)s" %
logger.info("Downgrading %(sql_connection)s to version %(version)s" %
locals())
return versioning_api.downgrade(sql_connection, repo_path, version)

@ -55,6 +55,8 @@ class Controller(wsgi.Controller):
images = db_api.image_get_all_public(None)
image_dicts = [dict(id=i['id'],
name=i['name'],
disk_format=i['disk_format'],
container_format=i['container_format'],
size=i['size']) for i in images]
return dict(images=image_dicts)
@ -144,6 +146,8 @@ class Controller(wsgi.Controller):
context = None
try:
logger.debug("Updating image %(id)s with metadata: %(image_data)r"
% locals())
updated_image = db_api.image_update(context, id, image_data)
return dict(image=make_image_dict(updated_image))
except exception.Invalid, e:

@ -90,6 +90,8 @@ class Controller(wsgi.Controller):
{'images': [
{'id': <ID>,
'name': <NAME>,
'disk_format': <DISK_FORMAT>,
'container_format': <DISK_FORMAT>,
'size': <SIZE>}, ...
]}
"""
@ -390,7 +392,6 @@ class Controller(wsgi.Controller):
image_meta = registry.update_image_metadata(self.options,
id,
new_image_meta)
if has_body:
self._upload_and_activate(req, image_meta)

@ -90,6 +90,7 @@ def delete_from_backend(uri, **kwargs):
backend_class = get_backend_class(scheme)
if hasattr(backend_class, 'delete'):
return backend_class.delete(parsed_uri, **kwargs)

@ -22,8 +22,6 @@ from __future__ import absolute_import
import httplib
import logging
from swift.common.client import Connection, ClientException
from glance.common import config
from glance.common import exception
import glance.store
@ -49,18 +47,19 @@ class SwiftBackend(glance.store.Backend):
swift instance at auth_url and downloads the file. Returns the
generator resp_body provided by get_object.
"""
from swift.common import client as swift_client
(user, key, authurl, container, obj) = parse_swift_tokens(parsed_uri)
# TODO(sirp): snet=False for now, however, if the instance of
# swift we're talking to is within our same region, we should set
# snet=True
swift_conn = Connection(
swift_conn = swift_client.Connection(
authurl=authurl, user=user, key=key, snet=False)
try:
(resp_headers, resp_body) = swift_conn.get_object(
container=container, obj=obj, resp_chunk_size=cls.CHUNKSIZE)
except ClientException, e:
except swift_client.ClientException, e:
if e.http_status == httplib.NOT_FOUND:
location = format_swift_location(user, key, authurl,
container, obj)
@ -99,6 +98,7 @@ class SwiftBackend(glance.store.Backend):
The location that was written,
and the size in bytes of the data written
"""
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')
@ -132,8 +132,8 @@ class SwiftBackend(glance.store.Backend):
"options.")
raise glance.store.BackendException(msg)
swift_conn = Connection(authurl=full_auth_address, user=user,
key=key, snet=False)
swift_conn = swift_client.Connection(
authurl=full_auth_address, user=user, key=key, snet=False)
logger.debug("Adding image object to Swift using "
"(auth_address=%(auth_address)s, user=%(user)s, "
@ -162,7 +162,7 @@ class SwiftBackend(glance.store.Backend):
if 'content-length' in resp_headers:
size = int(resp_headers['content-length'])
return (location, size)
except ClientException, e:
except swift_client.ClientException, e:
if e.http_status == httplib.CONFLICT:
raise exception.Duplicate("Swift already has an image at "
"location %(location)s" % locals())
@ -175,17 +175,18 @@ class SwiftBackend(glance.store.Backend):
"""
Deletes the swift object(s) at the parsed_uri location
"""
from swift.common import client as swift_client
(user, key, authurl, container, obj) = parse_swift_tokens(parsed_uri)
# TODO(sirp): snet=False for now, however, if the instance of
# swift we're talking to is within our same region, we should set
# snet=True
swift_conn = Connection(
swift_conn = swift_client.Connection(
authurl=authurl, user=user, key=key, snet=False)
try:
swift_conn.delete_object(container, obj)
except ClientException, e:
except swift_client.ClientException, e:
if e.http_status == httplib.NOT_FOUND:
location = format_swift_location(user, key, authurl,
container, obj)
@ -253,9 +254,10 @@ def create_container_if_missing(container, swift_conn, options):
:param swift_conn: Connection to Swift
:param options: Option mapping
"""
from swift.common import client as swift_client
try:
swift_conn.head_container(container)
except ClientException, e:
except swift_client.ClientException, e:
if e.http_status == httplib.NOT_FOUND:
add_container = config.get_option(options,
'swift_store_create_container_on_put',

@ -30,10 +30,15 @@ def image_meta_to_http_headers(image_meta):
"""
headers = {}
for k, v in image_meta.items():
if v is None:
v = ''
if k == 'properties':
for pk, pv in v.items():
if pv is None:
pv = ''
headers["x-image-meta-property-%s"
% pk.lower()] = unicode(pv)
else:
headers["x-image-meta-%s" % k.lower()] = unicode(v)
return headers
@ -76,10 +81,10 @@ def get_image_meta_from_headers(response):
key = str(key.lower())
if key.startswith('x-image-meta-property-'):
field_name = key[len('x-image-meta-property-'):].replace('-', '_')
properties[field_name] = value
properties[field_name] = value or None
elif key.startswith('x-image-meta-'):
field_name = key[len('x-image-meta-'):].replace('-', '_')
result[field_name] = value
result[field_name] = value or None
result['properties'] = properties
if 'id' in result:
result['id'] = int(result['id'])
@ -87,6 +92,8 @@ def get_image_meta_from_headers(response):
result['size'] = int(result['size'])
if 'is_public' in result:
result['is_public'] = (result['is_public'] == 'True')
if 'deleted' in result:
result['deleted'] = (result['deleted'] == 'True')
return result

@ -16,6 +16,40 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# Colorizer Code is borrowed from Twisted:
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Unittest runner for glance
To run all test::
python run_tests.py
To run a single test::
python run_tests.py test_stores:TestSwiftBackend.test_get
To run a single test module::
python run_tests.py test_stores
"""
import gettext
import os
import unittest
@ -26,14 +60,193 @@ from nose import result
from nose import core
class _AnsiColorizer(object):
"""
A colorizer is an object that loosely wraps around a stream, allowing
callers to write text to the stream in a particular color.
Colorizer classes must implement C{supported()} and C{write(text, color)}.
"""
_colors = dict(black=30, red=31, green=32, yellow=33,
blue=34, magenta=35, cyan=36, white=37)
def __init__(self, stream):
self.stream = stream
def supported(cls, stream=sys.stdout):
"""
A class method that returns True if the current platform supports
coloring terminal output using this method. Returns False otherwise.
"""
if not stream.isatty():
return False # auto color only on TTYs
try:
import curses
except ImportError:
return False
else:
try:
try:
return curses.tigetnum("colors") > 2
except curses.error:
curses.setupterm()
return curses.tigetnum("colors") > 2
except:
raise
# guess false in case of error
return False
supported = classmethod(supported)
def write(self, text, color):
"""
Write the given text to the stream in the given color.
@param text: Text to be written to the stream.
@param color: A string label for a color. e.g. 'red', 'white'.
"""
color = self._colors[color]
self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text))
class _Win32Colorizer(object):
"""
See _AnsiColorizer docstring.
"""
def __init__(self, stream):
from win32console import GetStdHandle, STD_OUT_HANDLE, \
FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \
FOREGROUND_INTENSITY
red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN,
FOREGROUND_BLUE, FOREGROUND_INTENSITY)
self.stream = stream
self.screenBuffer = GetStdHandle(STD_OUT_HANDLE)
self._colors = {
'normal': red | green | blue,
'red': red | bold,
'green': green | bold,
'blue': blue | bold,
'yellow': red | green | bold,
'magenta': red | blue | bold,
'cyan': green | blue | bold,
'white': red | green | blue | bold
}
def supported(cls, stream=sys.stdout):
try:
import win32console
screenBuffer = win32console.GetStdHandle(
win32console.STD_OUT_HANDLE)
except ImportError:
return False
import pywintypes
try:
screenBuffer.SetConsoleTextAttribute(
win32console.FOREGROUND_RED |
win32console.FOREGROUND_GREEN |
win32console.FOREGROUND_BLUE)
except pywintypes.error:
return False
else:
return True
supported = classmethod(supported)
def write(self, text, color):
color = self._colors[color]
self.screenBuffer.SetConsoleTextAttribute(color)
self.stream.write(text)
self.screenBuffer.SetConsoleTextAttribute(self._colors['normal'])
class _NullColorizer(object):
"""
See _AnsiColorizer docstring.
"""
def __init__(self, stream):
self.stream = stream
def supported(cls, stream=sys.stdout):
return True
supported = classmethod(supported)
def write(self, text, color):
self.stream.write(text)
class GlanceTestResult(result.TextTestResult):
def __init__(self, *args, **kw):
result.TextTestResult.__init__(self, *args, **kw)
self._last_case = None
self.colorizer = None
# NOTE(vish, tfukushima): reset stdout for the terminal check
stdout = sys.__stdout__
for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]:
if colorizer.supported():
self.colorizer = colorizer(self.stream)
break
sys.stdout = stdout
def getDescription(self, test):
return str(test)
# NOTE(vish, tfukushima): copied from unittest with edit to add color
def addSuccess(self, test):
unittest.TestResult.addSuccess(self, test)
if self.showAll:
self.colorizer.write("OK", 'green')
self.stream.writeln()
elif self.dots:
self.stream.write('.')
self.stream.flush()
# NOTE(vish, tfukushima): copied from unittest with edit to add color
def addFailure(self, test, err):
unittest.TestResult.addFailure(self, test, err)
if self.showAll:
self.colorizer.write("FAIL", 'red')
self.stream.writeln()
elif self.dots:
self.stream.write('F')
self.stream.flush()
# NOTE(vish, tfukushima): copied from unittest with edit to add color
def addError(self, test, err):
"""Overrides normal addError to add support for errorClasses.
If the exception is a registered class, the error will be added
to the list for that class, not errors.
"""
stream = getattr(self, 'stream', None)
ec, ev, tb = err
try:
exc_info = self._exc_info_to_string(err, test)
except TypeError:
# This is for compatibility with Python 2.3.
exc_info = self._exc_info_to_string(err)
for cls, (storage, label, isfail) in self.errorClasses.items():
if result.isclass(ec) and issubclass(ec, cls):
if isfail:
test.passwd = False
storage.append((test, exc_info))
# Might get patched into a streamless result
if stream is not None:
if self.showAll:
message = [label]
detail = result._exception_details(err[1])
if detail:
message.append(detail)
stream.writeln(": ".join(message))
elif self.dots:
stream.write(label[:1])
return
self.errors.append((test, exc_info))
test.passed = False
if stream is not None:
if self.showAll:
self.colorizer.write("ERROR", 'red')
self.stream.writeln()
elif self.dots:
stream.write('E')
def startTest(self, test):
unittest.TestResult.startTest(self, test)
current_case = test.test.__class__.__name__

@ -85,7 +85,8 @@ setup(
'Programming Language :: Python :: 2.6',
'Environment :: No Input/Output (Daemon)',
],
scripts=['bin/glance-api',
scripts=['bin/glance',
'bin/glance-api',
'bin/glance-combined',
'bin/glance-control',
'bin/glance-manage',

@ -38,6 +38,7 @@ import glance.registry.db.api
FAKE_FILESYSTEM_ROOTDIR = os.path.join('/tmp', 'glance-tests')
VERBOSE = False
DEBUG = False
def stub_out_http_backend(stubs):
@ -170,7 +171,10 @@ def stub_out_registry_and_store_server(stubs):
self.req.body = body
def getresponse(self):
options = {'sql_connection': 'sqlite://', 'verbose': VERBOSE}
sql_connection = os.environ.get('GLANCE_SQL_CONNECTION',
"sqlite://")
options = {'sql_connection': sql_connection, 'verbose': VERBOSE,
'debug': DEBUG}
res = self.req.get_response(rserver.API(options))
# httplib.Response has a read() method...fake it out
@ -217,6 +221,7 @@ def stub_out_registry_and_store_server(stubs):
def getresponse(self):
options = {'verbose': VERBOSE,
'debug': DEBUG,
'registry_host': '0.0.0.0',
'registry_port': '9191',
'default_store': 'file',

@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import os
import httplib
import json
import unittest
@ -26,6 +27,9 @@ from glance import server
from glance.registry import server as rserver
from tests import stubs
VERBOSE = False
DEBUG = False
class TestRegistryAPI(unittest.TestCase):
def setUp(self):
@ -34,7 +38,8 @@ class TestRegistryAPI(unittest.TestCase):
stubs.stub_out_registry_and_store_server(self.stubs)
stubs.stub_out_registry_db_image_api(self.stubs)
stubs.stub_out_filesystem_backend()
self.api = rserver.API({})
self.api = rserver.API({'verbose': VERBOSE,
'debug': DEBUG})
def tearDown(self):
"""Clear the test environment"""
@ -332,9 +337,12 @@ class TestGlanceAPI(unittest.TestCase):
stubs.stub_out_registry_and_store_server(self.stubs)
stubs.stub_out_registry_db_image_api(self.stubs)
stubs.stub_out_filesystem_backend()
options = {'registry_host': '0.0.0.0',
sql_connection = os.environ.get('GLANCE_SQL_CONNECTION', "sqlite://")
options = {'verbose': VERBOSE,
'debug': DEBUG,
'registry_host': '0.0.0.0',
'registry_port': '9191',
'sql_connection': 'sqlite://',
'sql_connection': sql_connection,
'default_store': 'file',
'filesystem_store_datadir': stubs.FAKE_FILESYSTEM_ROOTDIR}
self.api = server.API(options)

@ -19,6 +19,7 @@ import os
import unittest
import glance.registry.db.migration as migration_api
import glance.registry.db.api as api
import glance.common.config as config
@ -27,15 +28,16 @@ class TestMigrations(unittest.TestCase):
def setUp(self):
self.db_path = "glance_test_migration.sqlite"
if os.path.exists(self.db_path):
os.unlink(self.db_path)
self.options = dict(sql_connection="sqlite:///%s" % self.db_path,
sql_connection = os.environ.get('GLANCE_SQL_CONNECTION',
"sqlite:///%s" % self.db_path)
self.options = dict(sql_connection=sql_connection,
verbose=False)
config.setup_logging(self.options, {})
def tearDown(self):
if os.path.exists(self.db_path):
os.unlink(self.db_path)
api.configure_db(self.options)
api.unregister_models()
def test_db_sync_downgrade_then_upgrade(self):
migration_api.db_sync(self.options)

@ -93,7 +93,9 @@ class TestMiscellaneous(unittest.TestCase):
"""
fixture = {'name': 'fake public image',
'is_public': True,
'deleted': False,
'type': 'kernel',
'name': None,
'size': 19,
'location': "file:///tmp/glance-tests/2",
'properties': {'distro': 'Ubuntu 10.04 LTS'}}
@ -129,6 +131,7 @@ class TestMiscellaneous(unittest.TestCase):
api_port = 32001
reg_port = 32000
image_dir = "/tmp/test.images.%d" % api_port
sql_connection = os.environ.get('GLANCE_SQL_CONNECTION', "sqlite://")
if os.path.exists(image_dir):
shutil.rmtree(image_dir)
@ -152,7 +155,7 @@ registry_port = %(reg_port)s
paste.app_factory = glance.registry.server:app_factory
bind_host = 0.0.0.0
bind_port = %(reg_port)s
sql_connection = sqlite://
sql_connection = %(sql_connection)s
sql_idle_timeout = 3600
""" % locals()
conf_file.write(conf_contents)

@ -294,7 +294,8 @@ class TestSwiftBackend(unittest.TestCase):
Test that trying to delete a swift that doesn't exist
raises an error
"""
url_pieces = urlparse.urlparse("swift://user:key@auth_address/noexist")
url_pieces = urlparse.urlparse(
"swift://user:key@auth_address/noexist")
self.assertRaises(exception.NotFound,
SwiftBackend.delete,
url_pieces)