Merge trunk and resolve conflict
This commit is contained in:
commit
09916c2a40
572
bin/glance
Executable file
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
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
|
||||
|
||||
|
||||
|
213
run_tests.py
213
run_tests.py
@ -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__
|
||||
|
3
setup.py
3
setup.py
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user