Merge lp:~jaypipes/glance/glance-cli-tool into lp:~glance-coresec/glance/cactus-trunk
- glance-cli-tool
- Merge into cactus-trunk
Status: | Merged |
---|---|
Approved by: | Jay Pipes |
Approved revision: | 54 |
Merged at revision: | 85 |
Proposed branch: | lp:~jaypipes/glance/glance-cli-tool |
Merge into: | lp:~glance-coresec/glance/cactus-trunk |
Prerequisite: | lp:~jaypipes/glance/api-image-format |
Diff against target: |
1300 lines (+1058/-26) 14 files modified
bin/glance (+572/-0) doc/source/glance.rst (+374/-0) doc/source/index.rst (+1/-0) glance/client.py (+1/-3) glance/common/utils.py (+31/-0) glance/registry/__init__.py (+51/-12) glance/registry/db/api.py (+2/-2) glance/registry/server.py (+4/-0) glance/server.py (+2/-1) glance/store/__init__.py (+2/-1) glance/utils.py (+4/-3) setup.py (+2/-1) tests/stubs.py (+4/-1) tests/unit/test_api.py (+8/-2) |
To merge this branch: | bzr merge lp:~jaypipes/glance/glance-cli-tool |
Related bugs: | |
Related blueprints: |
Create bin/glance
(High)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jay Pipes (community) | Approve | ||
Rick Harris (community) | Approve | ||
Cory Wright (community) | Approve | ||
Sandy Walsh (community) | Approve | ||
Devin Carlen (community) | Approve | ||
Sandy Walsh | Pending | ||
Review via email: mp+51322@code.launchpad.net |
Commit message
Add Glance CLI tool
Description of the change
Adds a CLI tool to Glance (bin/glance) that allows a user to
interact with the Glance API server:
* add images
* update image metadata
* delete images and metadata
* delete all images (clear)
* show an image
* list public images
* show detailed info on public images
Adds documentation for the tool and cleans up a few issues
that came up in initial testing.
Sandy Walsh (sandy-walsh) wrote : | # |
Looks great. I thought I'd pass along some things I like in novatools:
1. the use of PrettyTable to make the output look nice. It's a small, flexible package and would remove the need for the '-' * 16 stuff. http://
2. Novatools goes through great pain to enforce Don't Repeat Yourself (DRY). All of the command line options are decorated so that they produce the --help output and we don't have to run around to update 3-4 different files whenever a command signature changes. It would be great to see that in this client shell as well.
These aren't critical for this merge, but something that would be nice to see for subsequent refactorings.
Jay Pipes (jaypipes) wrote : | # |
> Looks great. I thought I'd pass along some things I like in novatools:
>
> 1. the use of PrettyTable to make the output look nice. It's a small, flexible
> package and would remove the need for the '-' * 16 stuff.
> http://
Hmm, I'll check that out. Thanks for the heads up!
> 2. Novatools goes through great pain to enforce Don't Repeat Yourself (DRY).
> All of the command line options are decorated so that they produce the --help
> output and we don't have to run around to update 3-4 different files whenever
> a command signature changes. It would be great to see that in this client
> shell as well.
All --help documentation is generated from the relevant function's docstrings. Other than the actual RST docs, there's only a single place to change. Does Novatools have RST docs that are auto-generated from the decorators somehow? Is the --help output for Novatools only the function signature (because there's a lot more to the help output in bin/glance, including example usage...)?
-jay
Cory Wright (corywright) wrote : | # |
Looks like setup.py needs to be updated to install bin/glance as well.
Hurry up and get this in, I can't wait to use it. :)
Cory Wright (corywright) wrote : | # |
The 'details' view doesn't appear to be working properly. It finds my 7 images, but only prints details on the first one of them.
cwright@
Found 7 public images...
=====
URI: http://
Id: 1
Public? Yes
Name: ramdisk
Size: 3976716
Location: file://
'disk_format'
'disk_format'
cwright@
From the code it looks like it should print image properties, but it isn't for one of my images. Also, notice the duplicate 'disk_format' lines in the output.
It would also be nice to be able to specify a particular image to get details for instead of having it print all of them. glance details could accept an optional image id.
Jay Pipes (jaypipes) wrote : | # |
back from jury duty... will check out Cory's concerns.
Jay Pipes (jaypipes) wrote : | # |
> The 'details' view doesn't appear to be working properly. It finds my 7
> images, but only prints details on the first one of them.
>
> cwright@
> Found 7 public images...
> =======
> ====
> URI: http://
> Id: 1
> Public? Yes
> Name: ramdisk
> Size: 3976716
> Location: file://
> 'disk_format'
> 'disk_format'
> cwright@
>
> From the code it looks like it should print image properties, but it isn't for
> one of my images. Also, notice the duplicate 'disk_format' lines in the
> output.
Hmm, looks like this might be because of the admittedly missing database migration file. Looks like disk_format isn't a column returned from the db because the schema hasn't been migrated. Are you using an existing db?
> It would also be nice to be able to specify a particular image to get details
> for instead of having it print all of them. glance details could accept an
> optional image id.
You mean glance show <ID>? :)
-jay
Cory Wright (corywright) wrote : | # |
> Hmm, looks like this might be because of the admittedly missing database
> migration file. Looks like disk_format isn't a column returned from the db
> because the schema hasn't been migrated. Are you using an existing db?
Yes, I was using my existing db. Sorry about that, I'll try again after migrating the db.
> > It would also be nice to be able to specify a particular image to get
> details
> > for instead of having it print all of them. glance details could accept an
> > optional image id.
>
> You mean glance show <ID>? :)
So 'details' is equivalent to 'show' for all images? I would expect 'show' to give basic information on an image, 'details' to give more information on an image such as custom image properties, and something else (showall?) to list everything.
One other nitpick, in the 'show' output is there a good reason to use 'Public?' instead of 'Public:' ? Using colons as the delimiter everywhere would make this output easy to parse in shell scripts and such.
Jay Pipes (jaypipes) wrote : | # |
On Tue, Mar 1, 2011 at 10:57 AM, Cory Wright <email address hidden> wrote:
>> Hmm, looks like this might be because of the admittedly missing database
>> migration file. Looks like disk_format isn't a column returned from the db
>> because the schema hasn't been migrated. Are you using an existing db?
>
> Yes, I was using my existing db. Sorry about that, I'll try again after migrating the db.
>
>> > It would also be nice to be able to specify a particular image to get
>> details
>> > for instead of having it print all of them. glance details could accept an
>> > optional image id.
>>
>> You mean glance show <ID>? :)
>
> So 'details' is equivalent to 'show' for all images? I would expect 'show' to give basic information on an image, 'details' to give more information on an image such as custom image properties, and something else (showall?) to list everything.
Well, show(), index(), and details() do exactly the same as they do in
the OpenStack API controller. That's where the method names come from.
> One other nitpick, in the 'show' output is there a good reason to use 'Public?' instead of 'Public:' ? Using colons as the delimiter everywhere would make this output easy to parse in shell scripts and such.
No, I can change that. I was planning on adding a -m/--machine option
to make the output ideal for machine-reading.
-jay
Jay Pipes (jaypipes) wrote : | # |
Changed Public? => Public: per Cory's comment and added some more robust exception handling in a few of the bin/glance methods... also all merged up with trunk and resolved any conflicts. Please re-review at your earliest.
Cory Wright (corywright) : | # |
Rick Harris (rconradharris) wrote : | # |
Looks good, great work Jay!
OpenStack Infra (hudson-openstack) wrote : | # |
Attempt to merge into lp:glance failed due to conflicts:
text conflict in glance/utils.py
Jay Pipes (jaypipes) wrote : | # |
Resolved conflict after merging trunk.
OpenStack Infra (hudson-openstack) wrote : | # |
The attempt to merge lp:~jaypipes/glance/glance-cli-tool into lp:glance failed. Below is the output from the failed tests.
running test
running egg_info
creating glance.egg-info
writing glance.
writing top-level names to glance.
writing dependency_links to glance.
writing manifest file 'glance.
reading manifest file 'glance.
reading manifest template 'MANIFEST.in'
warning: no files found matching 'LICENSE'
warning: no files found matching 'ChangeLog'
warning: no files found matching 'tests/
writing manifest file 'glance.
running build_ext
Tests raises BadRequest for invalid store header ... ok
Tests to add a basic image in the file store ... ok
Tests creates a queued image for no body and no loc header ... ok
test_bad_
test_bad_
test_delete_image (tests.
test_delete_
Test for HEAD /images/<ID> ... ok
test_show_
test_show_
Tests that the /images POST registry API creates the image ... ok
Tests proper exception is raised if a bad disk_format is set ... ok
Tests proper exception is raised if a bad disk_format is set ... ok
Tests proper exception is raised if a bad status is set ... ok
Tests that exception raised for bad matching disk and container ... ok
Tests that the /images DELETE registry API deletes the image ... ok
Tests proper exception is raised if attempt to delete non-existing ... ok
Tests that the /images/detail registry API returns ... ok
Tests that the /images registry API returns list of ... ok
Tests that the root registry API returns "index", ... ok
Tests that the /images PUT registry API updates the image ... ok
Tests proper exception is raised if attempt to update non-existing ... ok
Tests that exception raised trying to set a bad container_format ... ok
Tests that exception raised trying to set a bad disk_format ... ok
Tests that exception raised trying to set a bad status ... ok
Tests that exception raised for bad matching disk and container ... ok
Test ClientConnectio
Tests proper exception is raised if image with ID already exists ... ok
Tests that we can add image metadata and returns the new id ... ok
Tests a bad status is set to a proper one by server ... ok
Tests BadRequest raised when supplying bad store name in meta ... ok
Tests can add image by passing image data as file ... ok
Tests can add image by passing image data as string ... ok
Tests add image by passing image data as string w/ no size attr ... ok
Tests that we can add image metadata with properties ... ok
Tests client returns image as queued ... ok
Tests that image metadata is deleted properly ... ok
Tests cannot delete non-existing image ... ok
Test a simple file backend retrieval works as expected ... ok
Tests that the detailed info about public images returned ... ok
Test correct...
Vish Ishaya (vishvananda) wrote : | # |
looks like you mismerged here:
=== modified file 'glance/utils.py'
1200 --- glance/utils.py 2011-03-08 20:12:41 +0000
1201 +++ glance/utils.py 2011-03-09 00:10:38 +0000
1202 @@ -36,9 +36,10 @@
1203 pv = ''
1204 headers[
1205 % pk.lower()] = unicode(pv)
1206 + else:
1207 + headers[
1208 if v is None:
1209 v = ''
1210 - headers[
1211 return headers
the if v is None block needs to be inside the else and above the headers line.
Jay Pipes (jaypipes) wrote : | # |
Well, duh :) Fixing now :)
On Tue, Mar 8, 2011 at 9:22 PM, Vish Ishaya <email address hidden> wrote:
> looks like you mismerged here:
>
> === modified file 'glance/utils.py'
> 1200 --- glance/utils.py 2011-03-08 20:12:41 +0000
> 1201 +++ glance/utils.py 2011-03-09 00:10:38 +0000
> 1202 @@ -36,9 +36,10 @@
> 1203 pv = ''
> 1204 headers[
> 1205 % pk.lower()] = unicode(pv)
> 1206 + else:
> 1207 + headers[
> 1208 if v is None:
> 1209 v = ''
> 1210 - headers[
> 1211 return headers
>
> the if v is None block needs to be inside the else and above the headers line.
>
> --
> https:/
> You are the owner of lp:~jaypipes/glance/glance-cli-tool.
>
Jay Pipes (jaypipes) wrote : | # |
fixed merge mistake..
Preview Diff
1 | === added file 'bin/glance' |
2 | --- bin/glance 1970-01-01 00:00:00 +0000 |
3 | +++ bin/glance 2011-03-09 13:40:45 +0000 |
4 | @@ -0,0 +1,572 @@ |
5 | +#!/usr/bin/env python |
6 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
7 | + |
8 | +# Copyright 2011 OpenStack, LLC |
9 | +# All Rights Reserved. |
10 | +# |
11 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
12 | +# not use this file except in compliance with the License. You may obtain |
13 | +# a copy of the License at |
14 | +# |
15 | +# http://www.apache.org/licenses/LICENSE-2.0 |
16 | +# |
17 | +# Unless required by applicable law or agreed to in writing, software |
18 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
19 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
20 | +# License for the specific language governing permissions and limitations |
21 | +# under the License. |
22 | + |
23 | +""" |
24 | +This is the administration program for Glance. It is simply a command-line |
25 | +interface for adding, modifying, and retrieving information about the images |
26 | +stored in one or more Glance nodes. |
27 | +""" |
28 | + |
29 | +import optparse |
30 | +import os |
31 | +import re |
32 | +import sys |
33 | +import time |
34 | + |
35 | +# If ../glance/__init__.py exists, add ../ to Python search path, so that |
36 | +# it will override what happens to be installed in /usr/(local/)lib/python... |
37 | +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), |
38 | + os.pardir, |
39 | + os.pardir)) |
40 | +if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')): |
41 | + sys.path.insert(0, possible_topdir) |
42 | + |
43 | +from glance import client |
44 | +from glance import version |
45 | +from glance.common import exception |
46 | +from glance.common import utils |
47 | + |
48 | +SUCCESS = 0 |
49 | +FAILURE = 1 |
50 | + |
51 | + |
52 | +def get_image_fields_from_args(args): |
53 | + """ |
54 | + Validate the set of arguments passed as field name/value pairs |
55 | + and return them as a mapping. |
56 | + """ |
57 | + fields = {} |
58 | + for arg in args: |
59 | + pieces = arg.strip(',').split('=') |
60 | + if len(pieces) != 2: |
61 | + msg = ("Arguments should be in the form of field=value. " |
62 | + "You specified %s." % arg) |
63 | + raise RuntimeError(msg) |
64 | + fields[pieces[0]] = pieces[1] |
65 | + |
66 | + fields = dict([(k.lower().replace('-', '_'), v) |
67 | + for k, v in fields.items()]) |
68 | + return fields |
69 | + |
70 | + |
71 | +def print_image_formatted(client, image): |
72 | + """ |
73 | + Formatted print of image metadata. |
74 | + |
75 | + :param client: The Glance client object |
76 | + :param image: The image metadata |
77 | + """ |
78 | + print "URI: %s://%s/images/%s" % (client.use_ssl and "https" or "http", |
79 | + client.host, |
80 | + image['id']) |
81 | + print "Id: %s" % image['id'] |
82 | + print "Public: " + (image['is_public'] and "Yes" or "No") |
83 | + print "Name: %s" % image['name'] |
84 | + print "Size: %d" % int(image['size']) |
85 | + print "Location: %s" % image['location'] |
86 | + print "Disk format: %s" % image['disk_format'] |
87 | + print "Container format: %s" % image['container_format'] |
88 | + if len(image['properties']) > 0: |
89 | + for k, v in image['properties'].items(): |
90 | + print "Property '%s': %s" % (k, v) |
91 | + |
92 | + |
93 | +def image_add(options, args): |
94 | + """ |
95 | +%(prog)s add [options] <field1=value1 field2=value2 ...> [ < /path/to/image ] |
96 | + |
97 | +Adds a new image to Glance. Specify metadata fields as arguments. |
98 | + |
99 | +SPECIFYING IMAGE METADATA |
100 | +=============================================================================== |
101 | + |
102 | +All field/value pairs are converted into a mapping that is passed |
103 | +to Glance that represents the metadata for an image. |
104 | + |
105 | +Field names of note: |
106 | + |
107 | +id Optional. If not specified, an image identifier will be |
108 | + automatically assigned. |
109 | +name Required. A name for the image. |
110 | +size Optional. Should be size in bytes of the image if |
111 | + specified. |
112 | +is_public Optional. If specified, interpreted as a boolean value |
113 | + and sets or unsets the image's availability to the public. |
114 | + The default value is False. |
115 | +disk_format Optional. Possible values are 'vhd','vmdk','raw', 'qcow2', |
116 | + and 'ami'. Default value is 'raw'. |
117 | +container_format Optional. Possible values are 'ovf' and 'ami'. |
118 | + Default value is 'ovf'. |
119 | +location Optional. When specified, should be a readable location |
120 | + in the form of a URI: $STORE://LOCATION. For example, if |
121 | + the image data is stored in a file on the local |
122 | + filesystem at /usr/share/images/some.image.tar.gz |
123 | + you would specify: |
124 | + location=file:///usr/share/images/some.image.tar.gz |
125 | + |
126 | +Any other field names are considered to be custom properties so be careful |
127 | +to spell field names correctly. :) |
128 | + |
129 | +STREAMING IMAGE DATA |
130 | +=============================================================================== |
131 | + |
132 | +If the location field is not specified, you can stream an image file on |
133 | +the command line using standard redirection. For example: |
134 | + |
135 | +%(prog)s add name="Ubuntu 10.04 LTS 5GB" < /tmp/images/myimage.tar.gz |
136 | + |
137 | +EXAMPLES |
138 | +=============================================================================== |
139 | + |
140 | +%(prog)s add name="My Image" disk_format=raw container_format=ovf \\ |
141 | + location=http://images.ubuntu.org/images/lucid-10.04-i686.iso \\ |
142 | + distro="Ubuntu 10.04 LTS" |
143 | + |
144 | +%(prog)s add name="My Image" distro="Ubuntu 10.04 LTS" < /tmp/myimage.iso""" |
145 | + c = get_client(options) |
146 | + |
147 | + try: |
148 | + fields = get_image_fields_from_args(args) |
149 | + except RuntimeError, e: |
150 | + print e |
151 | + return FAILURE |
152 | + |
153 | + if 'name' not in fields.keys(): |
154 | + print "Please specify a name for the image using name=VALUE" |
155 | + return FAILURE |
156 | + |
157 | + image_meta = {'name': fields.pop('name'), |
158 | + 'is_public': utils.bool_from_string( |
159 | + fields.pop('is_public', False)), |
160 | + 'disk_format': fields.pop('disk_format', 'raw'), |
161 | + 'container_format': fields.pop('container_format', 'ovf')} |
162 | + |
163 | + # Strip any args that are not supported |
164 | + unsupported_fields = ['status'] |
165 | + for field in unsupported_fields: |
166 | + if field in fields.keys(): |
167 | + print 'Found non-settable field %s. Removing.' % field |
168 | + fields.pop(field) |
169 | + |
170 | + if 'location' in fields.keys(): |
171 | + image_meta['location'] = fields.pop('location') |
172 | + |
173 | + # We need either a location or image data/stream to add... |
174 | + image_location = image_meta.get('location') |
175 | + image_data = None |
176 | + if not image_location: |
177 | + # Grab the image data stream from stdin or redirect, |
178 | + # otherwise error out |
179 | + image_data = sys.stdin |
180 | + else: |
181 | + # Ensure no image data has been given |
182 | + if not sys.stdin.isatty(): |
183 | + print "Either supply a location=LOCATION argument or supply image " |
184 | + print "data via a redirect. You have supplied BOTH image data " |
185 | + print "AND a location." |
186 | + return FAILURE |
187 | + |
188 | + # Add custom attributes, which are all the arguments remaining |
189 | + image_meta['properties'] = fields |
190 | + |
191 | + if not options.dry_run: |
192 | + try: |
193 | + image_meta = c.add_image(image_meta, image_data) |
194 | + image_id = image_meta['id'] |
195 | + print "Added new image with ID: %s" % image_id |
196 | + if options.verbose: |
197 | + print "Returned the following metadata for the new image:" |
198 | + for k, v in sorted(image_meta.items()): |
199 | + print " %(k)30s => %(v)s" % locals() |
200 | + except client.ClientConnectionError, e: |
201 | + host = options.host |
202 | + port = options.port |
203 | + print ("Failed to connect to the Glance API server " |
204 | + "%(host)s:%(port)d. Is the server running?" % locals()) |
205 | + if options.verbose: |
206 | + pieces = str(e).split('\n') |
207 | + for piece in pieces: |
208 | + print piece |
209 | + return FAILURE |
210 | + except Exception, e: |
211 | + print "Failed to add image. Got error:" |
212 | + pieces = str(e).split('\n') |
213 | + for piece in pieces: |
214 | + print piece |
215 | + return FAILURE |
216 | + else: |
217 | + print "Dry run. We would have done the following:" |
218 | + print "Add new image with metadata:" |
219 | + for k, v in sorted(image_meta.items()): |
220 | + print " %(k)30s => %(v)s" % locals() |
221 | + |
222 | + return SUCCESS |
223 | + |
224 | + |
225 | +def image_update(options, args): |
226 | + """ |
227 | +%(prog)s update [options] <ID> <field1=value1 field2=value2 ...> |
228 | + |
229 | +Updates an image's metadata in Glance. Specify metadata fields as arguments. |
230 | + |
231 | +All field/value pairs are converted into a mapping that is passed |
232 | +to Glance that represents the metadata for an image. |
233 | + |
234 | +Field names that can be specified: |
235 | + |
236 | +name A name for the image. |
237 | +is_public If specified, interpreted as a boolean value |
238 | + and sets or unsets the image's availability to the public. |
239 | +disk_format Format of the disk image |
240 | +container_format Format of the container |
241 | + |
242 | +All other field names are considered to be custom properties so be careful |
243 | +to spell field names correctly. :)""" |
244 | + c = get_client(options) |
245 | + try: |
246 | + image_id = args.pop(0) |
247 | + except IndexError: |
248 | + print "Please specify the ID of the image you wish to update " |
249 | + print "as the first argument" |
250 | + return FAILURE |
251 | + |
252 | + try: |
253 | + fields = get_image_fields_from_args(args) |
254 | + except RuntimeError, e: |
255 | + print e |
256 | + return FAILURE |
257 | + |
258 | + image_meta = {} |
259 | + |
260 | + # Strip any args that are not supported |
261 | + nonmodifiable_fields = ['created_on', 'deleted_on', 'deleted', |
262 | + 'updated_on', 'size', 'status'] |
263 | + for field in nonmodifiable_fields: |
264 | + if field in fields.keys(): |
265 | + print 'Found non-modifiable field %s. Removing.' % field |
266 | + fields.pop(field) |
267 | + |
268 | + base_image_fields = ['disk_format', 'container_format', |
269 | + 'location'] |
270 | + for field in base_image_fields: |
271 | + fvalue = fields.pop(field, None) |
272 | + if fvalue: |
273 | + image_meta[field] = fvalue |
274 | + |
275 | + # Have to handle "boolean" values specially... |
276 | + if 'is_public' in fields: |
277 | + image_meta['is_public'] = utils.int_from_bool_as_string( |
278 | + fields.pop('is_public')) |
279 | + |
280 | + # Add custom attributes, which are all the arguments remaining |
281 | + image_meta['properties'] = fields |
282 | + |
283 | + if not options.dry_run: |
284 | + try: |
285 | + image_meta = c.update_image(image_id, image_meta=image_meta) |
286 | + print "Updated image %s" % image_id |
287 | + |
288 | + if options.verbose: |
289 | + print "Updated image metadata for image %s:" % image_id |
290 | + print_image_formatted(c, image_meta) |
291 | + except exception.NotFound: |
292 | + print "No image with ID %s was found" % image_id |
293 | + return FAILURE |
294 | + except Exception, e: |
295 | + print "Failed to update image. Got error:" |
296 | + pieces = str(e).split('\n') |
297 | + for piece in pieces: |
298 | + print piece |
299 | + return FAILURE |
300 | + else: |
301 | + print "Dry run. We would have done the following:" |
302 | + print "Update existing image with metadata:" |
303 | + for k, v in sorted(image_meta.items()): |
304 | + print " %(k)30s => %(v)s" % locals() |
305 | + return SUCCESS |
306 | + |
307 | + |
308 | +def image_delete(options, args): |
309 | + """ |
310 | +%(prog)s delete [options] <ID> |
311 | + |
312 | +Deletes an image from Glance""" |
313 | + c = get_client(options) |
314 | + try: |
315 | + image_id = args.pop() |
316 | + except IndexError: |
317 | + print "Please specify the ID of the image you wish to delete " |
318 | + print "as the first argument" |
319 | + return FAILURE |
320 | + |
321 | + try: |
322 | + c.delete_image(image_id) |
323 | + print "Deleted image %s" % image_id |
324 | + return SUCCESS |
325 | + except exception.NotFound: |
326 | + print "No image with ID %s was found" % image_id |
327 | + return FAILURE |
328 | + |
329 | + |
330 | +def image_show(options, args): |
331 | + """ |
332 | +%(prog)s show [options] <ID> |
333 | + |
334 | +Shows image metadata for an image in Glance""" |
335 | + c = get_client(options) |
336 | + try: |
337 | + if len(args) > 0: |
338 | + image_id = args[0] |
339 | + else: |
340 | + print "Please specify the image identifier as the " |
341 | + print "first argument. Example: " |
342 | + print "$> glance-admin show 12345" |
343 | + return FAILURE |
344 | + |
345 | + image = c.get_image_meta(image_id) |
346 | + print_image_formatted(c, image) |
347 | + return SUCCESS |
348 | + except exception.NotFound: |
349 | + print "No image with ID %s was found" % image_id |
350 | + return FAILURE |
351 | + except Exception, e: |
352 | + print "Failed to show image. Got error:" |
353 | + pieces = str(e).split('\n') |
354 | + for piece in pieces: |
355 | + print piece |
356 | + return FAILURE |
357 | + |
358 | + |
359 | +def images_index(options, args): |
360 | + """ |
361 | +%(prog)s index [options] |
362 | + |
363 | +Returns basic information for all public images |
364 | +a Glance server knows about""" |
365 | + c = get_client(options) |
366 | + try: |
367 | + images = c.get_images() |
368 | + if len(images) == 0: |
369 | + print "No public images found." |
370 | + return SUCCESS |
371 | + |
372 | + print "Found %d public images..." % len(images) |
373 | + print "%-16s %-30s %-20s %-20s %-14s" % (("ID"), |
374 | + ("Name"), |
375 | + ("Disk Format"), |
376 | + ("Container Format"), |
377 | + ("Size")) |
378 | + print ('-' * 16) + " " + ('-' * 30) + " "\ |
379 | + + ('-' * 20) + " " + ('-' * 20) + " " + ('-' * 14) |
380 | + for image in images: |
381 | + print "%-16s %-30s %-20s %-20s %14d" % (image['id'], |
382 | + image['name'], |
383 | + image['disk_format'], |
384 | + image['container_format'], |
385 | + int(image['size'])) |
386 | + return SUCCESS |
387 | + except Exception, e: |
388 | + print "Failed to show index. Got error:" |
389 | + pieces = str(e).split('\n') |
390 | + for piece in pieces: |
391 | + print piece |
392 | + return FAILURE |
393 | + |
394 | + |
395 | +def images_detailed(options, args): |
396 | + """ |
397 | +%(prog)s details [options] |
398 | + |
399 | +Returns detailed information for all public images |
400 | +a Glance server knows about""" |
401 | + c = get_client(options) |
402 | + try: |
403 | + images = c.get_images_detailed() |
404 | + if len(images) == 0: |
405 | + print "No public images found." |
406 | + return SUCCESS |
407 | + |
408 | + num_images = len(images) |
409 | + print "Found %d public images..." % num_images |
410 | + cur_image = 1 |
411 | + for image in images: |
412 | + print "=" * 80 |
413 | + print_image_formatted(c, image) |
414 | + if cur_image == num_images: |
415 | + print "=" * 80 |
416 | + cur_image += 1 |
417 | + |
418 | + return SUCCESS |
419 | + except Exception, e: |
420 | + print "Failed to show details. Got error:" |
421 | + pieces = str(e).split('\n') |
422 | + for piece in pieces: |
423 | + print piece |
424 | + return FAILURE |
425 | + |
426 | + |
427 | +def images_clear(options, args): |
428 | + """ |
429 | +%(prog)s clear [options] |
430 | + |
431 | +Deletes all images from a Glance server""" |
432 | + c = get_client(options) |
433 | + images = c.get_images() |
434 | + for image in images: |
435 | + if options.verbose: |
436 | + print 'Deleting image %s "%s" ...' % (image['id'], image['name']), |
437 | + try: |
438 | + c.delete_image(image['id']) |
439 | + if options.verbose: |
440 | + print 'done' |
441 | + except Exception, e: |
442 | + print 'Failed to delete image %s' % image['id'] |
443 | + print e |
444 | + return FAILURE |
445 | + return SUCCESS |
446 | + |
447 | + |
448 | +def get_client(options): |
449 | + """ |
450 | + Returns a new client object to a Glance server |
451 | + specified by the --host and --port options |
452 | + supplied to the CLI |
453 | + """ |
454 | + return client.Client(host=options.host, |
455 | + port=options.port) |
456 | + |
457 | + |
458 | +def create_options(parser): |
459 | + """ |
460 | + Sets up the CLI and config-file options that may be |
461 | + parsed and program commands. |
462 | + |
463 | + :param parser: The option parser |
464 | + """ |
465 | + parser.add_option('-v', '--verbose', default=False, action="store_true", |
466 | + help="Print more verbose output") |
467 | + parser.add_option('-H', '--host', metavar="ADDRESS", default="0.0.0.0", |
468 | + help="Address of Glance API host. " |
469 | + "Default: %default") |
470 | + parser.add_option('-p', '--port', dest="port", metavar="PORT", |
471 | + type=int, default=9292, |
472 | + help="Port the Glance API host listens on. " |
473 | + "Default: %default") |
474 | + parser.add_option('--dry-run', default=False, action="store_true", |
475 | + help="Don't actually execute the command, just print " |
476 | + "output showing what WOULD happen.") |
477 | + |
478 | + |
479 | +def parse_options(parser, cli_args): |
480 | + """ |
481 | + Returns the parsed CLI options, command to run and its arguments, merged |
482 | + with any same-named options found in a configuration file |
483 | + |
484 | + :param parser: The option parser |
485 | + """ |
486 | + COMMANDS = {'help': print_help, |
487 | + 'add': image_add, |
488 | + 'update': image_update, |
489 | + 'delete': image_delete, |
490 | + 'index': images_index, |
491 | + 'details': images_detailed, |
492 | + 'show': image_show, |
493 | + 'clear': images_clear} |
494 | + |
495 | + if not cli_args: |
496 | + cli_args.append('-h') # Show options in usage output... |
497 | + |
498 | + (options, args) = parser.parse_args(cli_args) |
499 | + |
500 | + if not args: |
501 | + parser.print_usage() |
502 | + sys.exit(0) |
503 | + else: |
504 | + command_name = args.pop(0) |
505 | + if command_name not in COMMANDS.keys(): |
506 | + sys.exit("Unknown command: %s" % command_name) |
507 | + command = COMMANDS[command_name] |
508 | + |
509 | + return (options, command, args) |
510 | + |
511 | + |
512 | +def print_help(options, args): |
513 | + """ |
514 | + Print help specific to a command |
515 | + """ |
516 | + COMMANDS = {'add': image_add, |
517 | + 'update': image_update, |
518 | + 'delete': image_delete, |
519 | + 'index': images_index, |
520 | + 'details': images_detailed, |
521 | + 'show': image_show, |
522 | + 'clear': images_clear} |
523 | + |
524 | + if len(args) != 1: |
525 | + sys.exit("Please specify a command") |
526 | + |
527 | + command = args.pop() |
528 | + if command not in COMMANDS.keys(): |
529 | + parser.print_usage() |
530 | + if args: |
531 | + sys.exit("Unknown command: %s" % command) |
532 | + |
533 | + print COMMANDS[command].__doc__ % {'prog': os.path.basename(sys.argv[0])} |
534 | + |
535 | + |
536 | +if __name__ == '__main__': |
537 | + usage = """ |
538 | +%prog <command> [options] [args] |
539 | + |
540 | +Commands: |
541 | + |
542 | + help <command> Output help for one of the commands below |
543 | + |
544 | + add Adds a new image to Glance |
545 | + |
546 | + update Updates an image's metadata in Glance |
547 | + |
548 | + delete Deletes an image from Glance |
549 | + |
550 | + index Return brief information about images in Glance |
551 | + |
552 | + details Return detailed information about images in |
553 | + Glance |
554 | + |
555 | + show Show detailed information about an image in |
556 | + Glance |
557 | + |
558 | + clear Removes all images and metadata from Glance |
559 | + |
560 | +""" |
561 | + |
562 | + oparser = optparse.OptionParser(version='%%prog %s' |
563 | + % version.version_string(), |
564 | + usage=usage.strip()) |
565 | + create_options(oparser) |
566 | + (options, command, args) = parse_options(oparser, sys.argv[1:]) |
567 | + |
568 | + try: |
569 | + start_time = time.time() |
570 | + result = command(options, args) |
571 | + end_time = time.time() |
572 | + if options.verbose: |
573 | + print "Completed in %-0.4f sec." % (end_time - start_time) |
574 | + sys.exit(result) |
575 | + except (RuntimeError, NotImplementedError), e: |
576 | + print "ERROR: ", e |
577 | |
578 | === added file 'doc/source/glance.rst' |
579 | --- doc/source/glance.rst 1970-01-01 00:00:00 +0000 |
580 | +++ doc/source/glance.rst 2011-03-09 13:40:45 +0000 |
581 | @@ -0,0 +1,374 @@ |
582 | +.. |
583 | + Copyright 2011 OpenStack, LLC |
584 | + All Rights Reserved. |
585 | + |
586 | + Licensed under the Apache License, Version 2.0 (the "License"); you may |
587 | + not use this file except in compliance with the License. You may obtain |
588 | + a copy of the License at |
589 | + |
590 | + http://www.apache.org/licenses/LICENSE-2.0 |
591 | + |
592 | + Unless required by applicable law or agreed to in writing, software |
593 | + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
594 | + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
595 | + License for the specific language governing permissions and limitations |
596 | + under the License. |
597 | + |
598 | +Using the Glance CLI Tool |
599 | +========================= |
600 | + |
601 | +Glance ships with a command-line tool for quering and managing Glance |
602 | +It has a fairly simple but powerful interface of the form:: |
603 | + |
604 | + Usage: glance <command> [options] [args] |
605 | + |
606 | +Where ``<command>`` is one of the following: |
607 | + |
608 | +* help |
609 | + |
610 | + Show detailed help information about a specific command |
611 | + |
612 | +* add |
613 | + |
614 | + Adds an image to Glance |
615 | + |
616 | +* update |
617 | + |
618 | + Updates an image's stored metadata in Glance |
619 | + |
620 | +* delete |
621 | + |
622 | + Deletes an image and its metadata from Glance |
623 | + |
624 | +* index |
625 | + |
626 | + Lists brief information about *public* images that Glance knows about |
627 | + |
628 | +* details |
629 | + |
630 | + Lists detailed information about *public* images that Glance knows about |
631 | + |
632 | +* show |
633 | + |
634 | + Lists detailed information about a specific image |
635 | + |
636 | +* clear |
637 | + |
638 | + Destroys *all* images and their associated metadata |
639 | + |
640 | +This document describes how to use the ``glance`` tool for each of |
641 | +the above commands. |
642 | + |
643 | +The ``help`` command |
644 | +-------------------- |
645 | + |
646 | +Issuing the ``help`` command with a ``<COMMAND>`` argument shows detailed help |
647 | +about a specific command. Running ``glance`` without any arguments shows |
648 | +a brief help message, like so:: |
649 | + |
650 | + $> glance |
651 | + Usage: glance <command> [options] [args] |
652 | + |
653 | + Commands: |
654 | + |
655 | + help <command> Output help for one of the commands below |
656 | + |
657 | + add Adds a new image to Glance |
658 | + |
659 | + update Updates an image's metadata in Glance |
660 | + |
661 | + delete Deletes an image from Glance |
662 | + |
663 | + index Return brief information about images in Glance |
664 | + |
665 | + details Return detailed information about images in |
666 | + Glance |
667 | + |
668 | + show Show detailed information about an image in |
669 | + Glance |
670 | + |
671 | + clear Removes all images and metadata from Glance |
672 | + |
673 | + Options: |
674 | + --version show program's version number and exit |
675 | + -h, --help show this help message and exit |
676 | + -v, --verbose Print more verbose output |
677 | + -H ADDRESS, --host=ADDRESS |
678 | + Address of Glance API host. Default: example.com |
679 | + -p PORT, --port=PORT Port the Glance API host listens on. Default: 9292 |
680 | + --dry-run Don't actually execute the command, just print output |
681 | + showing what WOULD happen. |
682 | + |
683 | +With a ``<COMMAND>`` argument, more information on the command is shown, |
684 | +like so:: |
685 | + |
686 | + $> glance help update |
687 | + |
688 | + glance update [options] <ID> <field1=value1 field2=value2 ...> |
689 | + |
690 | + Updates an image's metadata in Glance. Specify metadata fields as arguments. |
691 | + |
692 | + All field/value pairs are converted into a mapping that is passed |
693 | + to Glance that represents the metadata for an image. |
694 | + |
695 | + Field names that can be specified: |
696 | + |
697 | + name A name for the image. |
698 | + is_public If specified, interpreted as a boolean value |
699 | + and sets or unsets the image's availability to the public. |
700 | + disk_format Format of the disk image |
701 | + container_format Format of the container |
702 | + |
703 | + All other field names are considered to be custom properties so be careful |
704 | + to spell field names correctly. :) |
705 | + |
706 | +The ``add`` command |
707 | +------------------- |
708 | + |
709 | +The ``add`` command is used to do both of the following: |
710 | + |
711 | +* Store virtual machine image data and metadata about that image in Glance |
712 | + |
713 | +* Let Glance know about an existing virtual machine image that may be stored |
714 | + somewhere else |
715 | + |
716 | +We cover both use cases below. |
717 | + |
718 | +Store virtual machine image data and metadata |
719 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
720 | + |
721 | +When adding an actual virtual machine image to Glance, you use the ``add`` |
722 | +command. You will pass metadata about the VM image on the command line, and |
723 | +you will use a standard shell redirect to stream the image data file to |
724 | +``glance``. |
725 | + |
726 | +Let's walk through a simple example. Suppose we have an image stored on our |
727 | +local filesystem that we wish to "upload" to Glance. This image is stored |
728 | +on our local filesystem in ``/tmp/images/myimage.tar.gz``. |
729 | + |
730 | +We'd also like to tell Glance that this image should be called "My Image", and |
731 | +that the image should be public -- anyone should be able to fetch it. |
732 | + |
733 | +Here is how we'd upload this image to Glance:: |
734 | + |
735 | + $> glance add name="My Image" is_public=true < /tmp/images/myimage.tar.gz |
736 | + |
737 | +If Glance was able to successfully upload and store your VM image data and |
738 | +metadata attributes, you would see something like this:: |
739 | + |
740 | + $> glance add name="My Image" is_public=true < /tmp/images/myimage.tar.gz |
741 | + Added new image with ID: 2 |
742 | + |
743 | +You can use the ``--verbose`` (or ``-v``) command-line option to print some more |
744 | +information about the metadata that was saved with the image:: |
745 | + |
746 | + $> glance --verbose add name="My Image" is_public=true < /tmp/images/myimage.tar.gz |
747 | + Added new image with ID: 4 |
748 | + Returned the following metadata for the new image: |
749 | + container_format => ovf |
750 | + created_at => 2011-02-22T19:20:53.298556 |
751 | + deleted => False |
752 | + deleted_at => None |
753 | + disk_format => raw |
754 | + id => 4 |
755 | + is_public => True |
756 | + location => file:///tmp/images/4 |
757 | + name => My Image |
758 | + properties => {} |
759 | + size => 58520278 |
760 | + status => active |
761 | + updated_at => None |
762 | + Completed in 0.6141 sec. |
763 | + |
764 | +If you are unsure about what will be added, you can use the ``--dry-run`` |
765 | +command-line option, which will simply show you what *would* have happened:: |
766 | + |
767 | + $> glance --dry-run add name="Foo" distro="Ubuntu" is_publi=True < /tmp/images/myimage.tar.gz |
768 | + Dry run. We would have done the following: |
769 | + Add new image with metadata: |
770 | + container_format => ovf |
771 | + disk_format => raw |
772 | + is_public => False |
773 | + name => Foo |
774 | + properties => {'is_publi': 'True', 'distro': 'Ubuntu'} |
775 | + |
776 | +This is useful for detecting problems and for seeing what the default field |
777 | +values supplied by ``glance`` are. For instance, there was a typo in |
778 | +the command above (the ``is_public`` field was incorrectly spelled ``is_publi`` |
779 | +which resulted in the image having an ``is_publi`` custom property added to |
780 | +the image and the *real* ``is_public`` field value being `False` (the default) |
781 | +and not `True`... |
782 | + |
783 | +Register a virtual machine image in another location |
784 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
785 | + |
786 | +Sometimes, you already have stored the virtual machine image in some non-Glance |
787 | +location -- perhaps even a location you have no write access to -- and you want |
788 | +to tell Glance where this virtual machine image is located and some metadata |
789 | +about it. The ``add`` command can do this for you. |
790 | + |
791 | +When registering an image in this way, the only difference is that you do not |
792 | +use a shell redirect to stream a virtual machine image file into Glance, but |
793 | +instead, you tell Glance where to find the existing virtual machine image by |
794 | +setting the ``location`` field. Below is an example of doing this. |
795 | + |
796 | +Let's assume that there is a virtual machine image located at the URL |
797 | +``http://example.com/images/myimage.tar.gz``. We can register this image with |
798 | +Glance using the following:: |
799 | + |
800 | + $> glance --verbose add name="Some web image" location="http://example.com/images/myimage.tar.gz" |
801 | + Added new image with ID: 1 |
802 | + Returned the following metadata for the new image: |
803 | + container_format => ovf |
804 | + created_at => 2011-02-23T00:42:04.688890 |
805 | + deleted => False |
806 | + deleted_at => None |
807 | + disk_format => vhd |
808 | + id => 1 |
809 | + is_public => True |
810 | + location => http://example.com/images/myimage.tar.gz |
811 | + name => Some web image |
812 | + properties => {} |
813 | + size => 0 |
814 | + status => active |
815 | + updated_at => None |
816 | + Completed in 0.0356 sec. |
817 | + |
818 | +The ``update`` command |
819 | +---------------------- |
820 | + |
821 | +After uploading/adding a virtual machine image to Glance, it is not possible to |
822 | +modify the actual virtual machine image -- images are read-only after all -- |
823 | +however, it *is* possible to update any metadata about the image after you add |
824 | +it to Glance. |
825 | + |
826 | +The ``update`` command allows you to update the metadata fields of a stored |
827 | +image. You use this command like so:: |
828 | + |
829 | + glance update <ID> [field1=value1 field2=value2 ...] |
830 | + |
831 | +Let's say we have an image with identifier 5 that we wish to change the is_public |
832 | +attribute of the image from False to True. The following would accomplish this:: |
833 | + |
834 | + $> glance update 5 is_public=true |
835 | + Updated image 5 |
836 | + |
837 | +Using the ``--verbose`` flag will show you all the updated data about the image:: |
838 | + |
839 | + $> glance --verbose update 5 is_public=true |
840 | + Updated image 5 |
841 | + Updated image metadata for image 5: |
842 | + URI: http://example.com/images/5 |
843 | + Id: 5 |
844 | + Public? Yes |
845 | + Name: My Image |
846 | + Size: 58520278 |
847 | + Location: file:///tmp/images/5 |
848 | + Disk format: raw |
849 | + Container format: ovf |
850 | + Completed in 0.0596 sec. |
851 | + |
852 | +The ``delete`` command |
853 | +---------------------- |
854 | + |
855 | +You can delete an image by using the ``delete`` command, shown below:: |
856 | + |
857 | + $> glance --verbose delete 5 |
858 | + Deleted image 5 |
859 | + |
860 | +The ``index`` command |
861 | +--------------------- |
862 | + |
863 | +The ``index`` command displays brief information about the *public* images |
864 | +available in Glance, as shown below:: |
865 | + |
866 | + $> glance index |
867 | + Found 4 public images... |
868 | + ID Name Disk Format Container Format Size |
869 | + ---------------- ------------------------------ -------------------- -------------------- -------------- |
870 | + 1 Ubuntu 10.10 vhd ovf 58520278 |
871 | + 2 Ubuntu 10.04 ami ami 58520278 |
872 | + 3 Fedora 9 vdi bare 3040 |
873 | + 4 Vanilla Linux 2.6.22 qcow2 bare 0 |
874 | + |
875 | +The ``details`` command |
876 | +----------------------- |
877 | + |
878 | +The ``details`` command displays detailed information about the *public* images |
879 | +available in Glance, as shown below:: |
880 | + |
881 | + $> glance details |
882 | + Found 4 public images... |
883 | + ================================================================================ |
884 | + URI: http://example.com/images/1 |
885 | + Id: 1 |
886 | + Public? Yes |
887 | + Name: Ubuntu 10.10 |
888 | + Size: 58520278 |
889 | + Location: file:///tmp/images/1 |
890 | + Disk format: vhd |
891 | + Container format: ovf |
892 | + Property 'distro_version': 10.10 |
893 | + Property 'distro': Ubuntu |
894 | + ================================================================================ |
895 | + URI: http://example.com/images/2 |
896 | + Id: 2 |
897 | + Public? Yes |
898 | + Name: Ubuntu 10.04 |
899 | + Size: 58520278 |
900 | + Location: file:///tmp/images/2 |
901 | + Disk format: ami |
902 | + Container format: ami |
903 | + Property 'distro_version': 10.04 |
904 | + Property 'distro': Ubuntu |
905 | + ================================================================================ |
906 | + URI: http://example.com/images/3 |
907 | + Id: 3 |
908 | + Public? Yes |
909 | + Name: Fedora 9 |
910 | + Size: 3040 |
911 | + Location: file:///tmp/images/3 |
912 | + Disk format: vdi |
913 | + Container format: bare |
914 | + Property 'distro_version': 9 |
915 | + Property 'distro': Fedora |
916 | + ================================================================================ |
917 | + URI: http://example.com/images/4 |
918 | + Id: 4 |
919 | + Public? Yes |
920 | + Name: Vanilla Linux 2.6.22 |
921 | + Size: 0 |
922 | + Location: http://example.com/images/vanilla.tar.gz |
923 | + Disk format: qcow2 |
924 | + Container format: bare |
925 | + ================================================================================ |
926 | + |
927 | +The ``show`` command |
928 | +-------------------- |
929 | + |
930 | +The ``show`` command displays detailed information about a specific image, specified |
931 | +with ``<ID>``, as shown below:: |
932 | + |
933 | + $> glance show 3 |
934 | + URI: http://example.com/images/3 |
935 | + Id: 3 |
936 | + Public? Yes |
937 | + Name: Fedora 9 |
938 | + Size: 3040 |
939 | + Location: file:///tmp/images/3 |
940 | + Disk format: vdi |
941 | + Container format: bare |
942 | + Property 'distro_version': 9 |
943 | + Property 'distro': Fedora |
944 | + |
945 | +The ``clear`` command |
946 | +--------------------- |
947 | + |
948 | +The ``clear`` command is an administrative command that deletes **ALL** images |
949 | +and all image metadata. Passing the ``--verbose`` command will print brief |
950 | +information about all the images that were deleted, as shown below:: |
951 | + |
952 | + $> glance --verbose clear |
953 | + Deleting image 1 "Some web image" ... done |
954 | + Deleting image 2 "Some other web image" ... done |
955 | + Completed in 0.0328 sec. |
956 | |
957 | === modified file 'doc/source/index.rst' |
958 | --- doc/source/index.rst 2011-02-25 14:55:26 +0000 |
959 | +++ doc/source/index.rst 2011-03-09 13:40:45 +0000 |
960 | @@ -62,6 +62,7 @@ |
961 | installing |
962 | controllingservers |
963 | configuring |
964 | + glance |
965 | glanceapi |
966 | client |
967 | |
968 | |
969 | === modified file 'glance/client.py' |
970 | --- glance/client.py 2011-03-06 15:37:28 +0000 |
971 | +++ glance/client.py 2011-03-09 13:40:45 +0000 |
972 | @@ -247,10 +247,8 @@ |
973 | |
974 | :retval The newly-stored image's metadata. |
975 | """ |
976 | - if image_meta is None: |
977 | - image_meta = {} |
978 | |
979 | - headers = utils.image_meta_to_http_headers(image_meta) |
980 | + headers = utils.image_meta_to_http_headers(image_meta or {}) |
981 | |
982 | if image_data: |
983 | body = image_data |
984 | |
985 | === modified file 'glance/common/utils.py' |
986 | --- glance/common/utils.py 2011-01-28 21:54:34 +0000 |
987 | +++ glance/common/utils.py 2011-03-09 13:40:45 +0000 |
988 | @@ -36,6 +36,37 @@ |
989 | TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" |
990 | |
991 | |
992 | +def int_from_bool_as_string(subject): |
993 | + """ |
994 | + Interpret a string as a boolean and return either 1 or 0. |
995 | + |
996 | + Any string value in: |
997 | + ('True', 'true', 'On', 'on', '1') |
998 | + is interpreted as a boolean True. |
999 | + |
1000 | + Useful for JSON-decoded stuff and config file parsing |
1001 | + """ |
1002 | + return bool_from_string(subject) and 1 or 0 |
1003 | + |
1004 | + |
1005 | +def bool_from_string(subject): |
1006 | + """ |
1007 | + Interpret a string as a boolean. |
1008 | + |
1009 | + Any string value in: |
1010 | + ('True', 'true', 'On', 'on', '1') |
1011 | + is interpreted as a boolean True. |
1012 | + |
1013 | + Useful for JSON-decoded stuff and config file parsing |
1014 | + """ |
1015 | + if type(subject) == type(bool): |
1016 | + return subject |
1017 | + if hasattr(subject, 'startswith'): # str or unicode... |
1018 | + if subject.strip().lower() in ('true', 'on', '1'): |
1019 | + return True |
1020 | + return False |
1021 | + |
1022 | + |
1023 | def import_class(import_str): |
1024 | """Returns a class from a string including module and class""" |
1025 | mod_str, _sep, class_str = import_str.rpartition('.') |
1026 | |
1027 | === modified file 'glance/registry/__init__.py' |
1028 | --- glance/registry/__init__.py 2011-01-28 21:54:34 +0000 |
1029 | +++ glance/registry/__init__.py 2011-03-09 13:40:45 +0000 |
1030 | @@ -1,7 +1,6 @@ |
1031 | # vim: tabstop=4 shiftwidth=4 softtabstop=4 |
1032 | |
1033 | -# Copyright 2010 United States Government as represented by the |
1034 | -# Administrator of the National Aeronautics and Space Administration. |
1035 | +# Copyright 2010-2011 OpenStack, LLC |
1036 | # All Rights Reserved. |
1037 | # |
1038 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
1039 | @@ -20,12 +19,17 @@ |
1040 | Registry API |
1041 | """ |
1042 | |
1043 | +import logging |
1044 | + |
1045 | from glance.registry import client |
1046 | |
1047 | +logger = logging.getLogger('glance.registry') |
1048 | + |
1049 | |
1050 | def get_registry_client(options): |
1051 | - return client.RegistryClient(options['registry_host'], |
1052 | - int(options['registry_port'])) |
1053 | + host = options['registry_host'] |
1054 | + port = int(options['registry_port']) |
1055 | + return client.RegistryClient(host, port) |
1056 | |
1057 | |
1058 | def get_images_list(options): |
1059 | @@ -43,16 +47,51 @@ |
1060 | return c.get_image(image_id) |
1061 | |
1062 | |
1063 | -def add_image_metadata(options, image_data): |
1064 | - c = get_registry_client(options) |
1065 | - return c.add_image(image_data) |
1066 | - |
1067 | - |
1068 | -def update_image_metadata(options, image_id, image_data): |
1069 | - c = get_registry_client(options) |
1070 | - return c.update_image(image_id, image_data) |
1071 | +def add_image_metadata(options, image_meta): |
1072 | + if options['debug']: |
1073 | + logger.debug("Adding image metadata...") |
1074 | + _debug_print_metadata(image_meta) |
1075 | + |
1076 | + c = get_registry_client(options) |
1077 | + new_image_meta = c.add_image(image_meta) |
1078 | + |
1079 | + if options['debug']: |
1080 | + logger.debug("Returned image metadata from call to " |
1081 | + "RegistryClient.add_image():") |
1082 | + _debug_print_metadata(new_image_meta) |
1083 | + |
1084 | + return new_image_meta |
1085 | + |
1086 | + |
1087 | +def update_image_metadata(options, image_id, image_meta): |
1088 | + if options['debug']: |
1089 | + logger.debug("Updating image metadata for image %s...", image_id) |
1090 | + _debug_print_metadata(image_meta) |
1091 | + |
1092 | + c = get_registry_client(options) |
1093 | + new_image_meta = c.update_image(image_id, image_meta) |
1094 | + |
1095 | + if options['debug']: |
1096 | + logger.debug("Returned image metadata from call to " |
1097 | + "RegistryClient.update_image():") |
1098 | + _debug_print_metadata(new_image_meta) |
1099 | + |
1100 | + return new_image_meta |
1101 | |
1102 | |
1103 | def delete_image_metadata(options, image_id): |
1104 | + logger.debug("Deleting image metadata for image %s...", image_id) |
1105 | c = get_registry_client(options) |
1106 | return c.delete_image(image_id) |
1107 | + |
1108 | + |
1109 | +def _debug_print_metadata(image_meta): |
1110 | + data = image_meta.copy() |
1111 | + properties = data.pop('properties', None) |
1112 | + for key, value in sorted(data.items()): |
1113 | + logger.debug(" %(key)20s: %(value)s" % locals()) |
1114 | + if properties: |
1115 | + logger.debug(" %d custom properties...", |
1116 | + len(properties)) |
1117 | + for key, value in properties.items(): |
1118 | + logger.debug(" %(key)20s: %(value)s" % locals()) |
1119 | |
1120 | === modified file 'glance/registry/db/api.py' |
1121 | --- glance/registry/db/api.py 2011-03-05 17:04:43 +0000 |
1122 | +++ glance/registry/db/api.py 2011-03-09 13:40:45 +0000 |
1123 | @@ -24,6 +24,7 @@ |
1124 | |
1125 | from sqlalchemy import create_engine |
1126 | from sqlalchemy.ext.declarative import declarative_base |
1127 | +from sqlalchemy.orm import exc |
1128 | from sqlalchemy.orm import joinedload |
1129 | from sqlalchemy.orm import sessionmaker |
1130 | |
1131 | @@ -126,8 +127,7 @@ |
1132 | filter_by(id=image_id).\ |
1133 | one() |
1134 | except exc.NoResultFound: |
1135 | - new_exc = exception.NotFound("No model for id %s" % image_id) |
1136 | - raise new_exc.__class__, new_exc, sys.exc_info()[2] |
1137 | + raise exception.NotFound("No image found with ID %s" % image_id) |
1138 | |
1139 | |
1140 | def image_get_all_public(context): |
1141 | |
1142 | === modified file 'glance/registry/server.py' |
1143 | --- glance/registry/server.py 2011-02-25 14:55:26 +0000 |
1144 | +++ glance/registry/server.py 2011-03-09 13:40:45 +0000 |
1145 | @@ -55,6 +55,8 @@ |
1146 | images = db_api.image_get_all_public(None) |
1147 | image_dicts = [dict(id=i['id'], |
1148 | name=i['name'], |
1149 | + disk_format=i['disk_format'], |
1150 | + container_format=i['container_format'], |
1151 | size=i['size']) for i in images] |
1152 | return dict(images=image_dicts) |
1153 | |
1154 | @@ -144,6 +146,8 @@ |
1155 | |
1156 | context = None |
1157 | try: |
1158 | + logger.debug("Updating image %(id)s with metadata: %(image_data)r" |
1159 | + % locals()) |
1160 | updated_image = db_api.image_update(context, id, image_data) |
1161 | return dict(image=make_image_dict(updated_image)) |
1162 | except exception.Invalid, e: |
1163 | |
1164 | === modified file 'glance/server.py' |
1165 | --- glance/server.py 2011-03-06 15:41:29 +0000 |
1166 | +++ glance/server.py 2011-03-09 13:40:45 +0000 |
1167 | @@ -90,6 +90,8 @@ |
1168 | {'images': [ |
1169 | {'id': <ID>, |
1170 | 'name': <NAME>, |
1171 | + 'disk_format': <DISK_FORMAT>, |
1172 | + 'container_format': <DISK_FORMAT>, |
1173 | 'size': <SIZE>}, ... |
1174 | ]} |
1175 | """ |
1176 | @@ -390,7 +392,6 @@ |
1177 | image_meta = registry.update_image_metadata(self.options, |
1178 | id, |
1179 | new_image_meta) |
1180 | - |
1181 | if has_body: |
1182 | self._upload_and_activate(req, image_meta) |
1183 | |
1184 | |
1185 | === modified file 'glance/store/__init__.py' |
1186 | --- glance/store/__init__.py 2011-02-02 16:40:57 +0000 |
1187 | +++ glance/store/__init__.py 2011-03-09 13:40:45 +0000 |
1188 | @@ -90,7 +90,8 @@ |
1189 | |
1190 | backend_class = get_backend_class(scheme) |
1191 | |
1192 | - return backend_class.delete(parsed_uri, **kwargs) |
1193 | + if hasattr(backend_class, 'delete'): |
1194 | + return backend_class.delete(parsed_uri, **kwargs) |
1195 | |
1196 | |
1197 | def get_store_from_location(location): |
1198 | |
1199 | === modified file 'glance/utils.py' |
1200 | --- glance/utils.py 2011-03-08 20:12:41 +0000 |
1201 | +++ glance/utils.py 2011-03-09 13:40:45 +0000 |
1202 | @@ -30,15 +30,16 @@ |
1203 | """ |
1204 | headers = {} |
1205 | for k, v in image_meta.items(): |
1206 | + if v is None: |
1207 | + v = '' |
1208 | if k == 'properties': |
1209 | for pk, pv in v.items(): |
1210 | if pv is None: |
1211 | pv = '' |
1212 | headers["x-image-meta-property-%s" |
1213 | % pk.lower()] = unicode(pv) |
1214 | - if v is None: |
1215 | - v = '' |
1216 | - headers["x-image-meta-%s" % k.lower()] = unicode(v) |
1217 | + else: |
1218 | + headers["x-image-meta-%s" % k.lower()] = unicode(v) |
1219 | return headers |
1220 | |
1221 | |
1222 | |
1223 | === modified file 'setup.py' |
1224 | --- setup.py 2011-02-09 21:56:48 +0000 |
1225 | +++ setup.py 2011-03-09 13:40:45 +0000 |
1226 | @@ -85,7 +85,8 @@ |
1227 | 'Programming Language :: Python :: 2.6', |
1228 | 'Environment :: No Input/Output (Daemon)', |
1229 | ], |
1230 | - scripts=['bin/glance-api', |
1231 | + scripts=['bin/glance', |
1232 | + 'bin/glance-api', |
1233 | 'bin/glance-combined', |
1234 | 'bin/glance-control', |
1235 | 'bin/glance-manage', |
1236 | |
1237 | === modified file 'tests/stubs.py' |
1238 | --- tests/stubs.py 2011-03-06 15:41:29 +0000 |
1239 | +++ tests/stubs.py 2011-03-09 13:40:45 +0000 |
1240 | @@ -38,6 +38,7 @@ |
1241 | |
1242 | FAKE_FILESYSTEM_ROOTDIR = os.path.join('/tmp', 'glance-tests') |
1243 | VERBOSE = False |
1244 | +DEBUG = False |
1245 | |
1246 | |
1247 | def stub_out_http_backend(stubs): |
1248 | @@ -170,7 +171,8 @@ |
1249 | self.req.body = body |
1250 | |
1251 | def getresponse(self): |
1252 | - options = {'sql_connection': 'sqlite://', 'verbose': VERBOSE} |
1253 | + options = {'sql_connection': 'sqlite://', 'verbose': VERBOSE, |
1254 | + 'debug': DEBUG} |
1255 | res = self.req.get_response(rserver.API(options)) |
1256 | |
1257 | # httplib.Response has a read() method...fake it out |
1258 | @@ -217,6 +219,7 @@ |
1259 | |
1260 | def getresponse(self): |
1261 | options = {'verbose': VERBOSE, |
1262 | + 'debug': DEBUG, |
1263 | 'registry_host': '0.0.0.0', |
1264 | 'registry_port': '9191', |
1265 | 'default_store': 'file', |
1266 | |
1267 | === modified file 'tests/unit/test_api.py' |
1268 | --- tests/unit/test_api.py 2011-03-06 15:37:28 +0000 |
1269 | +++ tests/unit/test_api.py 2011-03-09 13:40:45 +0000 |
1270 | @@ -26,6 +26,9 @@ |
1271 | from glance.registry import server as rserver |
1272 | from tests import stubs |
1273 | |
1274 | +VERBOSE = False |
1275 | +DEBUG = False |
1276 | + |
1277 | |
1278 | class TestRegistryAPI(unittest.TestCase): |
1279 | def setUp(self): |
1280 | @@ -34,7 +37,8 @@ |
1281 | stubs.stub_out_registry_and_store_server(self.stubs) |
1282 | stubs.stub_out_registry_db_image_api(self.stubs) |
1283 | stubs.stub_out_filesystem_backend() |
1284 | - self.api = rserver.API({}) |
1285 | + self.api = rserver.API({'verbose': VERBOSE, |
1286 | + 'debug': DEBUG}) |
1287 | |
1288 | def tearDown(self): |
1289 | """Clear the test environment""" |
1290 | @@ -332,7 +336,9 @@ |
1291 | stubs.stub_out_registry_and_store_server(self.stubs) |
1292 | stubs.stub_out_registry_db_image_api(self.stubs) |
1293 | stubs.stub_out_filesystem_backend() |
1294 | - options = {'registry_host': '0.0.0.0', |
1295 | + options = {'verbose': VERBOSE, |
1296 | + 'debug': DEBUG, |
1297 | + 'registry_host': '0.0.0.0', |
1298 | 'registry_port': '9191', |
1299 | 'sql_connection': 'sqlite://', |
1300 | 'default_store': 'file', |
Looks great, and excellent docs!