Merge trunk and resolve conflicts
This commit is contained in:
commit
0d7b3bb668
4
README
4
README
@ -29,10 +29,10 @@ documentation for more details).
|
||||
|
||||
|
||||
Now that Glance is installed, you can start the service. The easiest way to
|
||||
do that is by using the `glance-combined` utility which runs both the
|
||||
do that is by using the `glance-control` utility which runs both the
|
||||
`glance-api` and `glance-registry` services::
|
||||
|
||||
glance-combined
|
||||
glance-control all start
|
||||
|
||||
|
||||
Once both services are running, you can now use the `glance-upload` tool to
|
||||
|
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
|
@ -47,7 +47,7 @@ def create_options(parser):
|
||||
:param parser: The option parser
|
||||
"""
|
||||
config.add_common_options(parser)
|
||||
config.add_log_options('glance-api', parser)
|
||||
config.add_log_options(parser)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@ -57,7 +57,6 @@ if __name__ == '__main__':
|
||||
(options, args) = config.parse_options(oparser)
|
||||
|
||||
try:
|
||||
config.setup_logging(options)
|
||||
conf, app = config.load_paste_app('glance-api', options, args)
|
||||
|
||||
server = wsgi.Server()
|
||||
|
@ -1,71 +0,0 @@
|
||||
#!/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.
|
||||
|
||||
"""
|
||||
Glance API Server and reference registry server implementation
|
||||
combined in a single program.
|
||||
|
||||
WARNING: This is mostly just for testing. We do not recommend running
|
||||
this server in production!
|
||||
"""
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 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 version
|
||||
from glance.common import config
|
||||
from glance.common import wsgi
|
||||
|
||||
|
||||
def create_options(parser):
|
||||
"""
|
||||
Sets up the CLI and config-file options that may be
|
||||
parsed and program commands.
|
||||
|
||||
:param parser: The option parser
|
||||
"""
|
||||
config.add_common_options(parser)
|
||||
config.add_log_options('glance-combined', parser)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
oparser = optparse.OptionParser(version='%%prog %s'
|
||||
% version.version_string())
|
||||
create_options(oparser)
|
||||
(options, args) = config.parse_options(oparser)
|
||||
|
||||
try:
|
||||
config.setup_logging(options)
|
||||
|
||||
server = wsgi.Server()
|
||||
conf, app = config.load_paste_app('glance-api', options, args)
|
||||
server.start(app, int(conf['bind_port']), conf['bind_host'])
|
||||
conf, app = config.load_paste_app('glance-registry', options, args)
|
||||
server.start(app, int(conf['bind_port']), conf['bind_host'])
|
||||
server.wait()
|
||||
except RuntimeError, e:
|
||||
sys.exit("ERROR: %s" % e)
|
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 OpenStack, LLC.
|
||||
|
@ -54,7 +54,7 @@ def create_options(parser):
|
||||
"""
|
||||
glance.registry.db.add_options(parser)
|
||||
config.add_common_options(parser)
|
||||
config.add_log_options('glance-manage', parser)
|
||||
config.add_log_options(parser)
|
||||
|
||||
|
||||
def do_db_version(options, args):
|
||||
@ -119,7 +119,7 @@ def main():
|
||||
(options, args) = config.parse_options(oparser)
|
||||
|
||||
try:
|
||||
config.setup_logging(options)
|
||||
config.setup_logging(options, {})
|
||||
except RuntimeError, e:
|
||||
sys.exit("ERROR: %s" % e)
|
||||
|
||||
|
@ -47,7 +47,7 @@ def create_options(parser):
|
||||
:param parser: The option parser
|
||||
"""
|
||||
config.add_common_options(parser)
|
||||
config.add_log_options('glance-registry', parser)
|
||||
config.add_log_options(parser)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@ -57,7 +57,6 @@ if __name__ == '__main__':
|
||||
(options, args) = config.parse_options(oparser)
|
||||
|
||||
try:
|
||||
config.setup_logging(options)
|
||||
conf, app = config.load_paste_app('glance-registry', options, args)
|
||||
|
||||
server = wsgi.Server()
|
||||
|
@ -18,3 +18,49 @@ Configuring Glance
|
||||
==================
|
||||
|
||||
.. todo:: Complete details of configuration with paste.deploy config files
|
||||
|
||||
Configuring Logging in Glance
|
||||
-----------------------------
|
||||
|
||||
There are a number of configuration options in Glance that control how Glance
|
||||
servers log messages. The configuration options are specified in the
|
||||
``glance.conf`` config file.
|
||||
|
||||
* ``--log-config=PATH``
|
||||
|
||||
Optional. Default: ``None``
|
||||
|
||||
Specified on the command line only.
|
||||
|
||||
Takes a path to a configuration file to use for configuring logging.
|
||||
|
||||
* ``--log-format``
|
||||
|
||||
*Because of a bug in the PasteDeploy package, this option is only available
|
||||
on the command line.*
|
||||
|
||||
Optional. Default: ``%(asctime)s %(levelname)8s [%(name)s] %(message)s``
|
||||
|
||||
The format of the log records. See the
|
||||
`logging module <http://docs.python.org/library/logging.html>`_ documentation for
|
||||
more information on setting this format string.
|
||||
|
||||
* ``log_file`` (``--log-file`` when specified on the command line)
|
||||
|
||||
The filepath of the file to use for logging messages from Glance's servers. If
|
||||
missing, the default is to output messages to ``stdout``, so if you are running
|
||||
Glance servers in a daemon mode (using ``glance-control``) you should make
|
||||
sure that the ``log_file`` option is set appropriately.
|
||||
|
||||
* ``log_dir`` (``--log-dir`` when specified on the command line)
|
||||
|
||||
The filepath of the directory to use for log files. If not specified (the default)
|
||||
the ``log_file`` is used as an absolute filepath.
|
||||
|
||||
* ``log_date_format`` (``--log-date-format`` when specified from the command line)
|
||||
|
||||
The format string for timestamps in the log output.
|
||||
|
||||
Defaults to ``%Y-%m-%d %H:%M:%S``. See the
|
||||
`logging module <http://docs.python.org/library/logging.html>`_ documentation for
|
||||
more information on setting this format string.
|
||||
|
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
|
||||
|
||||
|
@ -250,10 +250,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
|
||||
|
@ -35,8 +35,6 @@ import glance.common.exception as exception
|
||||
|
||||
DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s"
|
||||
DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
DEFAULT_LOG_HANDLER = 'stream'
|
||||
LOGGING_HANDLER_CHOICES = ['syslog', 'file', 'stream']
|
||||
|
||||
|
||||
def parse_options(parser, cli_args=None):
|
||||
@ -82,38 +80,7 @@ def add_common_options(parser):
|
||||
parser.add_option_group(group)
|
||||
|
||||
|
||||
def add_daemon_options(parser):
|
||||
"""
|
||||
Given a supplied optparse.OptionParser, adds an OptionGroup that
|
||||
represents all the configuration options around daemonization.
|
||||
|
||||
:param parser: optparse.OptionParser
|
||||
"""
|
||||
help_text = "The following configuration options are specific to "\
|
||||
"the daemonizing of this program."
|
||||
|
||||
group = optparse.OptionGroup(parser, "Daemon Options", help_text)
|
||||
group.add_option('--config', default=None,
|
||||
help="Configuration file to read when loading "
|
||||
"application. If missing, the first argument is "
|
||||
"used. If no arguments are found, then a set of "
|
||||
"standard directories are searched for a config "
|
||||
"file.")
|
||||
group.add_option("--pid-file", default=None, metavar="PATH",
|
||||
help="(Optional) Name of pid file for the server. "
|
||||
"If not specified, the pid file will be named "
|
||||
"/var/run/glance/<SERVER>.pid.")
|
||||
group.add_option("--uid", type=int, default=os.getuid(),
|
||||
help="uid under which to run. Default: %default")
|
||||
group.add_option("--gid", type=int, default=os.getgid(),
|
||||
help="gid under which to run. Default: %default")
|
||||
group.add_option('--working-directory', '--working-dir',
|
||||
default=os.path.abspath(os.getcwd()),
|
||||
help="The working directory. Default: %default")
|
||||
parser.add_option_group(group)
|
||||
|
||||
|
||||
def add_log_options(prog_name, parser):
|
||||
def add_log_options(parser):
|
||||
"""
|
||||
Given a supplied optparse.OptionParser, adds an OptionGroup that
|
||||
represents all the configuration options around logging.
|
||||
@ -130,29 +97,25 @@ def add_log_options(prog_name, parser):
|
||||
"any other logging options specified. Please see "
|
||||
"the Python logging module documentation for "
|
||||
"details on logging configuration files.")
|
||||
group.add_option('--log-handler', default=DEFAULT_LOG_HANDLER,
|
||||
metavar="HANDLER",
|
||||
choices=LOGGING_HANDLER_CHOICES,
|
||||
help="What logging handler to use? "
|
||||
"Default: %default")
|
||||
group.add_option('--log-date-format', metavar="FORMAT",
|
||||
default=DEFAULT_LOG_DATE_FORMAT,
|
||||
help="Format string for %(asctime)s in log records. "
|
||||
"Default: %default")
|
||||
group.add_option('--log-file', default="%s.log" % prog_name,
|
||||
metavar="PATH",
|
||||
help="(Optional) Name of log file to output to.")
|
||||
group.add_option('--log-file', default=None, metavar="PATH",
|
||||
help="(Optional) Name of log file to output to. "
|
||||
"If not set, logging will go to stdout.")
|
||||
group.add_option("--log-dir", default=None,
|
||||
help="(Optional) The directory to keep log files in "
|
||||
"(will be prepended to --logfile)")
|
||||
parser.add_option_group(group)
|
||||
|
||||
|
||||
def setup_logging(options):
|
||||
def setup_logging(options, conf):
|
||||
"""
|
||||
Sets up the logging options for a log with supplied name
|
||||
|
||||
:param options: Mapping of typed option key/values
|
||||
:param conf: Mapping of untyped key/values from config file
|
||||
"""
|
||||
|
||||
if options.get('log_config', None):
|
||||
@ -182,27 +145,24 @@ def setup_logging(options):
|
||||
log_date_format = options.get('log_date_format', DEFAULT_LOG_DATE_FORMAT)
|
||||
formatter = logging.Formatter(log_format, log_date_format)
|
||||
|
||||
log_handler = options.get('log_handler', DEFAULT_LOG_HANDLER)
|
||||
if log_handler == 'syslog':
|
||||
syslog = logging.handlers.SysLogHandler(address='/dev/log')
|
||||
syslog.setFormatter(formatter)
|
||||
root_logger.addHandler(syslog)
|
||||
elif log_handler == 'file':
|
||||
logfile = options['log_file']
|
||||
logdir = options['log_dir']
|
||||
logfile = options.get('log_file')
|
||||
if not logfile:
|
||||
logfile = conf.get('log_file')
|
||||
|
||||
if logfile:
|
||||
logdir = options.get('log_dir')
|
||||
if not logdir:
|
||||
logdir = conf.get('log_dir')
|
||||
if logdir:
|
||||
logfile = os.path.join(logdir, logfile)
|
||||
logfile = logging.FileHandler(logfile)
|
||||
logfile.setFormatter(formatter)
|
||||
logfile.setFormatter(formatter)
|
||||
root_logger.addHandler(logfile)
|
||||
elif log_handler == 'stream':
|
||||
else:
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(formatter)
|
||||
root_logger.addHandler(handler)
|
||||
else:
|
||||
raise exception.BadInputError(
|
||||
"unrecognized log handler '%(log_handler)s'" % locals())
|
||||
|
||||
|
||||
def find_config_file(options, args):
|
||||
@ -270,6 +230,11 @@ def load_paste_app(app_name, options, args):
|
||||
"Cannot load application %s" % app_name)
|
||||
try:
|
||||
conf = deploy.appconfig("config:%s" % conf_file, name=app_name)
|
||||
|
||||
# Setup logging early, supplying both the CLI options and the
|
||||
# configuration mapping from the config file
|
||||
setup_logging(options, conf)
|
||||
|
||||
# We only update the conf dict for the verbose and debug
|
||||
# flags. Everything else must be set up in the conf file...
|
||||
conf['verbose'] = options['verbose']
|
||||
|
@ -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,8 +59,8 @@ 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" %
|
||||
locals())
|
||||
logger.info("Upgrading %(sql_connection)s to version %(version_str)s" %
|
||||
locals())
|
||||
return versioning_api.upgrade(sql_connection, repo_path, version)
|
||||
|
||||
|
||||
@ -71,8 +74,8 @@ 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" %
|
||||
locals())
|
||||
logger.info("Downgrading %(sql_connection)s to version %(version)s" %
|
||||
locals())
|
||||
return versioning_api.downgrade(sql_connection, repo_path, version)
|
||||
|
||||
|
||||
|
@ -14,8 +14,9 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Parllax Image controller
|
||||
Reference implementation registry server WSGI controller
|
||||
"""
|
||||
|
||||
import json
|
||||
@ -31,7 +32,9 @@ from glance.registry.db import api as db_api
|
||||
|
||||
logger = logging.getLogger('glance.registry.server')
|
||||
|
||||
DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size', 'checksum']
|
||||
DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size',
|
||||
'disk_format', 'container_format',
|
||||
'checksum']
|
||||
|
||||
|
||||
class Controller(wsgi.Controller):
|
||||
@ -55,6 +58,8 @@ class Controller(wsgi.Controller):
|
||||
'id': <ID>,
|
||||
'name': <NAME>,
|
||||
'size': <SIZE>,
|
||||
'disk_format': <DISK_FORMAT>,
|
||||
'container_format': <CONTAINER_FORMAT>,
|
||||
'checksum': <CHECKSUM>
|
||||
}
|
||||
|
||||
@ -154,6 +159,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:
|
||||
|
@ -83,6 +83,9 @@ class Controller(wsgi.Controller):
|
||||
|
||||
* id -- The opaque image identifier
|
||||
* name -- The name of the image
|
||||
* disk_format -- The disk image format
|
||||
* container_format -- The "container" format of the image
|
||||
* checksum -- MD5 checksum of the image data
|
||||
* size -- Size of image data in bytes
|
||||
|
||||
:param request: The WSGI/Webob Request object
|
||||
@ -91,9 +94,10 @@ class Controller(wsgi.Controller):
|
||||
{'images': [
|
||||
{'id': <ID>,
|
||||
'name': <NAME>,
|
||||
'size': <SIZE>,
|
||||
'disk_format': <DISK_FORMAT>,
|
||||
'container_format': <DISK_FORMAT>,
|
||||
'checksum': <CHECKSUM>
|
||||
}, ...
|
||||
'size': <SIZE>}, ...
|
||||
]}
|
||||
"""
|
||||
images = registry.get_images_list(self.options)
|
||||
@ -413,7 +417,6 @@ class Controller(wsgi.Controller):
|
||||
image_meta = registry.update_image_metadata(self.options,
|
||||
id,
|
||||
new_image_meta)
|
||||
|
||||
if has_body:
|
||||
image_meta = self._upload_and_activate(req, image_meta)
|
||||
|
||||
|
@ -90,7 +90,8 @@ def delete_from_backend(uri, **kwargs):
|
||||
|
||||
backend_class = get_backend_class(scheme)
|
||||
|
||||
return backend_class.delete(parsed_uri, **kwargs)
|
||||
if hasattr(backend_class, 'delete'):
|
||||
return backend_class.delete(parsed_uri, **kwargs)
|
||||
|
||||
|
||||
def get_store_from_location(location):
|
||||
|
@ -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, obj_etag)
|
||||
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)
|
||||
@ -214,7 +215,16 @@ def parse_swift_tokens(parsed_uri):
|
||||
# see lp659445 and Python issue7904
|
||||
creds, path = path.split('@')
|
||||
|
||||
user, key = creds.split(':')
|
||||
cred_parts = creds.split(':')
|
||||
|
||||
# User can be account:user, in which case cred_parts[0:2] will be
|
||||
# the account and user. Combine them into a single username of
|
||||
# account:user
|
||||
if len(cred_parts) == 3:
|
||||
user = ':'.join(cred_parts[0:2])
|
||||
else:
|
||||
user = cred_parts[0]
|
||||
key = cred_parts[-1]
|
||||
path_parts = path.split('/')
|
||||
obj = path_parts.pop()
|
||||
container = path_parts.pop()
|
||||
@ -253,9 +263,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,11 +30,16 @@ 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)
|
||||
headers["x-image-meta-%s" % k.lower()] = unicode(v)
|
||||
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
|
||||
|
||||
|
||||
|
212
run_tests.py
212
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,192 @@ 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__
|
||||
|
4
setup.py
4
setup.py
@ -85,8 +85,8 @@ setup(
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Environment :: No Input/Output (Daemon)',
|
||||
],
|
||||
scripts=['bin/glance-api',
|
||||
'bin/glance-combined',
|
||||
scripts=['bin/glance',
|
||||
'bin/glance-api',
|
||||
'bin/glance-control',
|
||||
'bin/glance-manage',
|
||||
'bin/glance-registry',
|
||||
|
@ -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',
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
import hashlib
|
||||
import httplib
|
||||
import os
|
||||
import json
|
||||
import unittest
|
||||
|
||||
@ -27,6 +28,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):
|
||||
@ -35,7 +39,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"""
|
||||
@ -339,9 +344,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)
|
||||
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)
|
||||
@ -204,3 +207,146 @@ sql_idle_timeout = 3600
|
||||
cmd = "./bin/glance-control registry stop "\
|
||||
"%s --pid-file=glance-registry.pid" % conf_file_name
|
||||
ignored, out, err = execute(cmd)
|
||||
|
||||
|
||||
# TODO(jaypipes): Move this to separate test file once
|
||||
# LP Bug#731304 moves execute() out to a common file, etc
|
||||
class TestLogging(unittest.TestCase):
|
||||
|
||||
"""Tests that logging can be configured correctly"""
|
||||
|
||||
def setUp(self):
|
||||
self.logfiles = []
|
||||
|
||||
def tearDown(self):
|
||||
self._cleanup_test_servers()
|
||||
self._cleanup_log_files()
|
||||
|
||||
def _cleanup_test_servers(self):
|
||||
# Clean up any leftover test servers...
|
||||
pid_files = ('glance-api.pid', 'glance-registry.pid')
|
||||
for pid_file in pid_files:
|
||||
if os.path.exists(pid_file):
|
||||
pid = int(open(pid_file).read().strip())
|
||||
try:
|
||||
os.killpg(pid, signal.SIGTERM)
|
||||
except:
|
||||
pass # Ignore if the process group is dead
|
||||
os.unlink(pid_file)
|
||||
|
||||
def _cleanup_log_files(self):
|
||||
for f in self.logfiles:
|
||||
if os.path.exists(f):
|
||||
os.unlink(f)
|
||||
|
||||
def test_logfile(self):
|
||||
"""
|
||||
A test that logging can be configured properly from the
|
||||
glance.conf file with the log_file option.
|
||||
|
||||
We start both servers daemonized with a temporary config
|
||||
file that has some logging options in it.
|
||||
|
||||
We then use curl to issue a few requests and verify that each server's
|
||||
logging statements were logged to the one log file
|
||||
"""
|
||||
logfile = "/tmp/test_logfile.log"
|
||||
self.logfiles.append(logfile)
|
||||
|
||||
if os.path.exists(logfile):
|
||||
os.unlink(logfile)
|
||||
|
||||
self._cleanup_test_servers()
|
||||
|
||||
# Port numbers hopefully not used by anything...
|
||||
api_port = 32001
|
||||
reg_port = 32000
|
||||
image_dir = "/tmp/test.images.%d" % api_port
|
||||
if os.path.exists(image_dir):
|
||||
shutil.rmtree(image_dir)
|
||||
|
||||
# A config file to use just for this test...we don't want
|
||||
# to trample on currently-running Glance servers, now do we?
|
||||
with tempfile.NamedTemporaryFile() as conf_file:
|
||||
conf_contents = """[DEFAULT]
|
||||
verbose = True
|
||||
debug = True
|
||||
log_file = %(logfile)s
|
||||
|
||||
[app:glance-api]
|
||||
paste.app_factory = glance.server:app_factory
|
||||
filesystem_store_datadir=%(image_dir)s
|
||||
default_store = file
|
||||
bind_host = 0.0.0.0
|
||||
bind_port = %(api_port)s
|
||||
registry_host = 0.0.0.0
|
||||
registry_port = %(reg_port)s
|
||||
|
||||
[app:glance-registry]
|
||||
paste.app_factory = glance.registry.server:app_factory
|
||||
bind_host = 0.0.0.0
|
||||
bind_port = %(reg_port)s
|
||||
sql_connection = sqlite://
|
||||
sql_idle_timeout = 3600
|
||||
""" % locals()
|
||||
conf_file.write(conf_contents)
|
||||
conf_file.flush()
|
||||
conf_file_name = conf_file.name
|
||||
|
||||
venv = ""
|
||||
if 'VIRTUAL_ENV' in os.environ:
|
||||
venv = "tools/with_venv.sh "
|
||||
|
||||
# Start up the API and default registry server
|
||||
cmd = venv + "./bin/glance-control api start "\
|
||||
"%s --pid-file=glance-api.pid" % conf_file_name
|
||||
exitcode, out, err = execute(cmd)
|
||||
|
||||
self.assertEquals(0, exitcode)
|
||||
self.assertTrue("Starting glance-api with" in out)
|
||||
|
||||
cmd = venv + "./bin/glance-control registry start "\
|
||||
"%s --pid-file=glance-registry.pid" % conf_file_name
|
||||
exitcode, out, err = execute(cmd)
|
||||
|
||||
self.assertEquals(0, exitcode)
|
||||
self.assertTrue("Starting glance-registry with" in out)
|
||||
|
||||
time.sleep(2) # Gotta give some time for spin up...
|
||||
|
||||
cmd = "curl -X POST -H 'Content-Type: application/octet-stream' "\
|
||||
"-H 'X-Image-Meta-Name: ImageName' "\
|
||||
"-H 'X-Image-Meta-Disk-Format: Invalid' "\
|
||||
"http://0.0.0.0:%d/images" % api_port
|
||||
ignored, out, err = execute(cmd)
|
||||
|
||||
self.assertTrue('Invalid disk format' in out,
|
||||
"Could not find 'Invalid disk format' "
|
||||
"in output: %s" % out)
|
||||
|
||||
self.assertTrue(os.path.exists(logfile),
|
||||
"Logfile %s does not exist!" % logfile)
|
||||
|
||||
logfile_contents = open(logfile, 'rb').read()
|
||||
|
||||
# Check that BOTH the glance API and registry server
|
||||
# modules are logged to the file.
|
||||
self.assertTrue('[glance.server]' in logfile_contents,
|
||||
"Could not find '[glance.server]' "
|
||||
"in logfile: %s" % logfile_contents)
|
||||
self.assertTrue('[glance.registry.server]' in logfile_contents,
|
||||
"Could not find '[glance.registry.server]' "
|
||||
"in logfile: %s" % logfile_contents)
|
||||
|
||||
# Test that the error we caused above is in the log
|
||||
self.assertTrue('Invalid disk format' in logfile_contents,
|
||||
"Could not find 'Invalid disk format' "
|
||||
"in logfile: %s" % logfile_contents)
|
||||
|
||||
# Spin down the API and default registry server
|
||||
cmd = "./bin/glance-control api stop "\
|
||||
"%s --pid-file=glance-api.pid" % conf_file_name
|
||||
ignored, out, err = execute(cmd)
|
||||
cmd = "./bin/glance-control registry stop "\
|
||||
"%s --pid-file=glance-registry.pid" % conf_file_name
|
||||
ignored, out, err = execute(cmd)
|
||||
|
@ -29,7 +29,9 @@ import swift.common.client
|
||||
|
||||
from glance.common import exception
|
||||
from glance.store import BackendException
|
||||
from glance.store.swift import SwiftBackend, format_swift_location
|
||||
from glance.store.swift import (SwiftBackend,
|
||||
format_swift_location,
|
||||
parse_swift_tokens)
|
||||
|
||||
FIVE_KB = (5 * 1024)
|
||||
SWIFT_OPTIONS = {'verbose': True,
|
||||
@ -155,6 +157,41 @@ class TestSwiftBackend(unittest.TestCase):
|
||||
"""Clear the test environment"""
|
||||
self.stubs.UnsetAll()
|
||||
|
||||
def test_parse_swift_tokens(self):
|
||||
"""
|
||||
Test that the parse_swift_tokens function returns
|
||||
user, key, authurl, container, and objname properly
|
||||
"""
|
||||
uri = "swift://user:key@localhost/v1.0/container/objname"
|
||||
url_pieces = urlparse.urlparse(uri)
|
||||
user, key, authurl, container, objname =\
|
||||
parse_swift_tokens(url_pieces)
|
||||
self.assertEqual("user", user)
|
||||
self.assertEqual("key", key)
|
||||
self.assertEqual("https://localhost/v1.0", authurl)
|
||||
self.assertEqual("container", container)
|
||||
self.assertEqual("objname", objname)
|
||||
|
||||
uri = "swift://user:key@localhost:9090/v1.0/container/objname"
|
||||
url_pieces = urlparse.urlparse(uri)
|
||||
user, key, authurl, container, objname =\
|
||||
parse_swift_tokens(url_pieces)
|
||||
self.assertEqual("user", user)
|
||||
self.assertEqual("key", key)
|
||||
self.assertEqual("https://localhost:9090/v1.0", authurl)
|
||||
self.assertEqual("container", container)
|
||||
self.assertEqual("objname", objname)
|
||||
|
||||
uri = "swift://account:user:key@localhost:9090/v1.0/container/objname"
|
||||
url_pieces = urlparse.urlparse(uri)
|
||||
user, key, authurl, container, objname =\
|
||||
parse_swift_tokens(url_pieces)
|
||||
self.assertEqual("account:user", user)
|
||||
self.assertEqual("key", key)
|
||||
self.assertEqual("https://localhost:9090/v1.0", authurl)
|
||||
self.assertEqual("container", container)
|
||||
self.assertEqual("objname", objname)
|
||||
|
||||
def test_get(self):
|
||||
"""Test a "normal" retrieval of an image in chunks"""
|
||||
url_pieces = urlparse.urlparse(
|
||||
@ -305,7 +342,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