1# Copyright 2012 OpenStack Foundation
2# All Rights Reserved.
3#
4#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5#    not use this file except in compliance with the License. You may obtain
6#    a copy of the License at
7#
8#         http://www.apache.org/licenses/LICENSE-2.0
9#
10#    Unless required by applicable law or agreed to in writing, software
11#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13#    License for the specific language governing permissions and limitations
14#    under the License.
15
16import json
17import os
18import sys
19
20from oslo_utils import strutils
21
22from glanceclient._i18n import _
23from glanceclient.common import progressbar
24from glanceclient.common import utils
25from glanceclient import exc
26from glanceclient.v2 import image_members
27from glanceclient.v2 import image_schema
28from glanceclient.v2 import images
29from glanceclient.v2 import namespace_schema
30from glanceclient.v2 import resource_type_schema
31from glanceclient.v2 import tasks
32
33MEMBER_STATUS_VALUES = image_members.MEMBER_STATUS_VALUES
34IMAGE_SCHEMA = None
35DATA_FIELDS = ('location', 'copy_from', 'file', 'uri')
36
37
38def get_image_schema():
39    global IMAGE_SCHEMA
40    if IMAGE_SCHEMA is None:
41        schema_path = os.path.expanduser("~/.glanceclient/image_schema.json")
42        if os.path.isfile(schema_path):
43            with open(schema_path, "r") as f:
44                schema_raw = f.read()
45                IMAGE_SCHEMA = json.loads(schema_raw)
46        else:
47            return image_schema._BASE_SCHEMA
48    return IMAGE_SCHEMA
49
50
51@utils.schema_args(get_image_schema, omit=['created_at', 'updated_at', 'file',
52                                           'checksum', 'virtual_size', 'size',
53                                           'status', 'schema', 'direct_url',
54                                           'locations', 'self', 'os_hidden',
55                                           'os_hash_value', 'os_hash_algo'])
56# NOTE(rosmaita): to make this option more intuitive for end users, we
57# do not use the Glance image property name 'os_hidden' here.  This means
58# we must include 'os_hidden' in the 'omit' list above and handle the
59# --hidden argument by hand
60@utils.arg('--hidden', type=strutils.bool_from_string, metavar='[True|False]',
61           default=None,
62           dest='os_hidden',
63           help=("If true, image will not appear in default image list "
64                 "response."))
65@utils.arg('--property', metavar="<key=value>", action='append',
66           default=[], help=_('Arbitrary property to associate with image.'
67                              ' May be used multiple times.'))
68@utils.arg('--file', metavar='<FILE>',
69           help=_('Local file that contains disk image to be uploaded '
70                  'during creation. Alternatively, the image data can be '
71                  'passed to the client via stdin.'))
72@utils.arg('--progress', action='store_true', default=False,
73           help=_('Show upload progress bar.'))
74@utils.arg('--store', metavar='<STORE>',
75           default=utils.env('OS_IMAGE_STORE', default=None),
76           help='Backend store to upload image to.')
77@utils.on_data_require_fields(DATA_FIELDS)
78def do_image_create(gc, args):
79    """Create a new image."""
80    schema = gc.schemas.get("image")
81    _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()]
82
83    fields = dict(filter(lambda x: x[1] is not None and
84                         (x[0] == 'property' or
85                          schema.is_core_property(x[0])),
86                         _args))
87
88    raw_properties = fields.pop('property', [])
89    for datum in raw_properties:
90        key, value = datum.split('=', 1)
91        fields[key] = value
92
93    backend = args.store
94
95    file_name = fields.pop('file', None)
96    using_stdin = not sys.stdin.isatty()
97    if args.store and not (file_name or using_stdin):
98        utils.exit("--store option should only be provided with --file "
99                   "option or stdin.")
100
101    if backend:
102        # determine if backend is valid
103        _validate_backend(backend, gc)
104
105    if file_name is not None and os.access(file_name, os.R_OK) is False:
106        utils.exit("File %s does not exist or user does not have read "
107                   "privileges to it" % file_name)
108    image = gc.images.create(**fields)
109    try:
110        if utils.get_data_file(args) is not None:
111            backend = fields.get('store', None)
112            args.id = image['id']
113            args.size = None
114            do_image_upload(gc, args)
115            image = gc.images.get(args.id)
116    finally:
117        utils.print_image(image)
118
119
120@utils.schema_args(get_image_schema, omit=['created_at', 'updated_at', 'file',
121                                           'checksum', 'virtual_size', 'size',
122                                           'status', 'schema', 'direct_url',
123                                           'locations', 'self', 'os_hidden',
124                                           'os_hash_value', 'os_hash_algo'])
125# NOTE: --hidden requires special handling; see note at do_image_create
126@utils.arg('--hidden', type=strutils.bool_from_string, metavar='[True|False]',
127           default=None,
128           dest='os_hidden',
129           help=("If true, image will not appear in default image list "
130                 "response."))
131@utils.arg('--property', metavar="<key=value>", action='append',
132           default=[], help=_('Arbitrary property to associate with image.'
133                              ' May be used multiple times.'))
134@utils.arg('--file', metavar='<FILE>',
135           help=_('Local file that contains disk image to be uploaded '
136                  'during creation. Alternatively, the image data can be '
137                  'passed to the client via stdin.'))
138@utils.arg('--progress', action='store_true', default=False,
139           help=_('Show upload progress bar.'))
140@utils.arg('--import-method', metavar='<METHOD>',
141           default=utils.env('OS_IMAGE_IMPORT_METHOD', default=None),
142           help=_('Import method used for Image Import workflow. '
143                  'Valid values can be retrieved with import-info command. '
144                  'Defaults to env[OS_IMAGE_IMPORT_METHOD] or if that is '
145                  'undefined uses \'glance-direct\' if data is provided using '
146                  '--file or stdin. Otherwise, simply creates an image '
147                  'record if no import-method and no data is supplied'))
148@utils.arg('--uri', metavar='<IMAGE_URL>', default=None,
149           help=_('URI to download the external image.'))
150@utils.arg('--store', metavar='<STORE>',
151           default=utils.env('OS_IMAGE_STORE', default=None),
152           help='Backend store to upload image to.')
153@utils.arg('--stores', metavar='<STORES>',
154           default=utils.env('OS_IMAGE_STORES', default=None),
155           help=_('Stores to upload image to if multi-stores import '
156                  'available. Comma separated list. Available stores can be '
157                  'listed with "stores-info" call.'))
158@utils.arg('--all-stores', type=strutils.bool_from_string,
159           metavar='[True|False]',
160           default=None,
161           dest='os_all_stores',
162           help=_('"all-stores" can be ued instead of "stores"-list to '
163                  'indicate that image should be imported into all available '
164                  'stores.'))
165@utils.arg('--allow-failure', type=strutils.bool_from_string,
166           metavar='[True|False]',
167           dest='os_allow_failure',
168           default=utils.env('OS_IMAGE_ALLOW_FAILURE', default=True),
169           help=_('Indicator if all stores listed (or available) must '
170                  'succeed. "True" by default meaning that we allow some '
171                  'stores to fail and the status can be monitored from the '
172                  'image metadata. If this is set to "False" the import will '
173                  'be reverted should any of the uploads fail. Only usable '
174                  'with "stores" or "all-stores".'))
175@utils.on_data_require_fields(DATA_FIELDS)
176def do_image_create_via_import(gc, args):
177    """EXPERIMENTAL: Create a new image via image import.
178
179    Use the interoperable image import workflow to create an image.  This
180    command is designed to be backward compatible with the current image-create
181    command, so its behavior is as follows:
182
183    * If an import-method is specified (either on the command line or through
184      the OS_IMAGE_IMPORT_METHOD environment variable, then you must provide a
185      data source appropriate to that method (for example, --file for
186      glance-direct, or --uri for web-download).
187    * If no import-method is specified AND you provide either a --file or
188      data to stdin, the command will assume you are using the 'glance-direct'
189      import-method and will act accordingly.
190    * If no import-method is specified and no data is supplied via --file or
191      stdin, the command will simply create an image record in 'queued' status.
192    """
193    schema = gc.schemas.get("image")
194    _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()]
195
196    fields = dict(filter(lambda x: x[1] is not None and
197                         (x[0] == 'property' or
198                          schema.is_core_property(x[0])),
199                         _args))
200
201    raw_properties = fields.pop('property', [])
202    for datum in raw_properties:
203        key, value = datum.split('=', 1)
204        fields[key] = value
205
206    file_name = fields.pop('file', None)
207    using_stdin = not sys.stdin.isatty()
208
209    # special processing for backward compatibility with image-create
210    if args.import_method is None and (file_name or using_stdin):
211        args.import_method = 'glance-direct'
212
213    if args.import_method == 'copy-image':
214        utils.exit("Import method 'copy-image' cannot be used "
215                   "while creating the image.")
216
217    # determine whether the requested import method is valid
218    import_methods = gc.images.get_import_info().get('import-methods')
219    if args.import_method and args.import_method not in import_methods.get(
220            'value'):
221        utils.exit("Import method '%s' is not valid for this cloud. "
222                   "Valid values can be retrieved with import-info command." %
223                   args.import_method)
224
225    # determine if backend is valid
226    backend = None
227    stores = getattr(args, "stores", None)
228    all_stores = getattr(args, "os_all_stores", None)
229
230    if (args.store and (stores or all_stores)) or (stores and all_stores):
231        utils.exit("Only one of --store, --stores and --all-stores can be "
232                   "provided")
233    elif args.store:
234        backend = args.store
235        # determine if backend is valid
236        _validate_backend(backend, gc)
237    elif stores:
238        # NOTE(jokke): Making sure here that we do not include the stores in
239        # the create call
240        fields.pop("stores")
241        stores = str(stores).split(',')
242        for store in stores:
243            # determine if backend is valid
244            _validate_backend(store, gc)
245
246    # make sure we have all and only correct inputs for the requested method
247    if args.import_method is None:
248        if args.uri:
249            utils.exit("You cannot use --uri without specifying an import "
250                       "method.")
251    if args.import_method == 'glance-direct':
252        if backend and not (file_name or using_stdin):
253            utils.exit("--store option should only be provided with --file "
254                       "option or stdin for the glance-direct import method.")
255        if stores and not (file_name or using_stdin):
256            utils.exit("--stores option should only be provided with --file "
257                       "option or stdin for the glance-direct import method.")
258        if all_stores and not (file_name or using_stdin):
259            utils.exit("--all-stores option should only be provided with "
260                       "--file option or stdin for the glance-direct import "
261                       "method.")
262
263        if args.uri:
264            utils.exit("You cannot specify a --uri with the glance-direct "
265                       "import method.")
266        if file_name is not None and os.access(file_name, os.R_OK) is False:
267            utils.exit("File %s does not exist or user does not have read "
268                       "privileges to it." % file_name)
269        if file_name is not None and using_stdin:
270            utils.exit("You cannot use both --file and stdin with the "
271                       "glance-direct import method.")
272        if not file_name and not using_stdin:
273            utils.exit("You must specify a --file or provide data via stdin "
274                       "for the glance-direct import method.")
275    if args.import_method == 'web-download':
276        if backend and not args.uri:
277            utils.exit("--store option should only be provided with --uri "
278                       "option for the web-download import method.")
279        if stores and not args.uri:
280            utils.exit("--stores option should only be provided with --uri "
281                       "option for the web-download import method.")
282        if all_stores and not args.uri:
283            utils.exit("--all-stores option should only be provided with "
284                       "--uri option for the web-download import method.")
285        if not args.uri:
286            utils.exit("URI is required for web-download import method. "
287                       "Please use '--uri <uri>'.")
288        if file_name:
289            utils.exit("You cannot specify a --file with the web-download "
290                       "import method.")
291        if using_stdin:
292            utils.exit("You cannot pass data via stdin with the web-download "
293                       "import method.")
294
295    # process
296    image = gc.images.create(**fields)
297    try:
298        args.id = image['id']
299        if args.import_method:
300            if utils.get_data_file(args) is not None:
301                args.size = None
302                do_image_stage(gc, args)
303            args.from_create = True
304            args.stores = stores
305            do_image_import(gc, args)
306        image = gc.images.get(args.id)
307    finally:
308        utils.print_image(image)
309
310
311def _validate_backend(backend, gc):
312    try:
313        enabled_backends = gc.images.get_stores_info().get('stores')
314    except exc.HTTPNotFound:
315        # NOTE(abhishekk): To maintain backward compatibility
316        return
317
318    if backend:
319        valid_backend = False
320        for available_backend in enabled_backends:
321            if available_backend['id'] == backend:
322                valid_backend = True
323                break
324
325        if not valid_backend:
326            utils.exit("Store '%s' is not valid for this cloud. Valid "
327                       "values can be retrieved with stores-info command." %
328                       backend)
329
330
331@utils.arg('id', metavar='<IMAGE_ID>', help=_('ID of image to update.'))
332@utils.schema_args(get_image_schema, omit=['id', 'locations', 'created_at',
333                                           'updated_at', 'file', 'checksum',
334                                           'virtual_size', 'size', 'status',
335                                           'schema', 'direct_url', 'tags',
336                                           'self', 'os_hidden',
337                                           'os_hash_value', 'os_hash_algo'])
338# NOTE: --hidden requires special handling; see note at do_image_create
339@utils.arg('--hidden', type=strutils.bool_from_string, metavar='[True|False]',
340           default=None,
341           dest='os_hidden',
342           help=("If true, image will not appear in default image list "
343                 "response."))
344@utils.arg('--property', metavar="<key=value>", action='append',
345           default=[], help=_('Arbitrary property to associate with image.'
346                              ' May be used multiple times.'))
347@utils.arg('--remove-property', metavar="key", action='append', default=[],
348           help=_("Name of arbitrary property to remove from the image."))
349def do_image_update(gc, args):
350    """Update an existing image."""
351    schema = gc.schemas.get("image")
352    _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()]
353
354    fields = dict(filter(lambda x: x[1] is not None and
355                         (x[0] in ['property', 'remove_property'] or
356                          schema.is_core_property(x[0])),
357                         _args))
358
359    raw_properties = fields.pop('property', [])
360    for datum in raw_properties:
361        key, value = datum.split('=', 1)
362        fields[key] = value
363
364    remove_properties = fields.pop('remove_property', None)
365
366    image_id = fields.pop('id')
367    image = gc.images.update(image_id, remove_properties, **fields)
368    utils.print_image(image)
369
370
371@utils.arg('--limit', metavar='<LIMIT>', default=None, type=int,
372           help=_('Maximum number of images to get.'))
373@utils.arg('--page-size', metavar='<SIZE>', default=None, type=int,
374           help=_('Number of images to request in each paginated request.'))
375@utils.arg('--visibility', metavar='<VISIBILITY>',
376           help=_('The visibility of the images to display.'))
377@utils.arg('--member-status', metavar='<MEMBER_STATUS>',
378           help=_('The status of images to display.'))
379@utils.arg('--owner', metavar='<OWNER>',
380           help=_('Display images owned by <OWNER>.'))
381@utils.arg('--property-filter', metavar='<KEY=VALUE>',
382           help=_("Filter images by a user-defined image property."),
383           action='append', dest='properties', default=[])
384@utils.arg('--checksum', metavar='<CHECKSUM>',
385           help=_('Displays images that match the MD5 checksum.'))
386@utils.arg('--hash', dest='os_hash_value', default=None,
387           metavar='<HASH_VALUE>',
388           help=_('Displays images that match the specified hash value.'))
389@utils.arg('--tag', metavar='<TAG>', action='append',
390           help=_("Filter images by a user-defined tag."))
391@utils.arg('--sort-key', default=[], action='append',
392           choices=images.SORT_KEY_VALUES,
393           help=_('Sort image list by specified fields.'
394                  ' May be used multiple times.'))
395@utils.arg('--sort-dir', default=[], action='append',
396           choices=images.SORT_DIR_VALUES,
397           help=_('Sort image list in specified directions.'))
398@utils.arg('--sort', metavar='<key>[:<direction>]', default=None,
399           help=(_("Comma-separated list of sort keys and directions in the "
400                   "form of <key>[:<asc|desc>]. Valid keys: %s. OPTIONAL."
401                   ) % ', '.join(images.SORT_KEY_VALUES)))
402@utils.arg('--hidden',
403           dest='os_hidden',
404           metavar='[True|False]',
405           default=None,
406           type=strutils.bool_from_string,
407           const=True,
408           nargs='?',
409           help="Filters results by hidden status. Default=None.")
410@utils.arg('--include-stores',
411           metavar='[True|False]',
412           default=None,
413           type=strutils.bool_from_string,
414           const=True,
415           nargs='?',
416           help="Print backend store id.")
417def do_image_list(gc, args):
418    """List images you can access."""
419    filter_keys = ['visibility', 'member_status', 'owner', 'checksum', 'tag',
420                   'os_hidden', 'os_hash_value']
421    filter_items = [(key, getattr(args, key)) for key in filter_keys]
422
423    if args.properties:
424        filter_properties = [prop.split('=', 1) for prop in args.properties]
425        if any(len(pair) != 2 for pair in filter_properties):
426            utils.exit('Argument --property-filter expected properties in the'
427                       ' format KEY=VALUE')
428        filter_items += filter_properties
429    filters = dict([item for item in filter_items if item[1] is not None])
430
431    kwargs = {'filters': filters}
432    if args.limit is not None:
433        kwargs['limit'] = args.limit
434    if args.page_size is not None:
435        kwargs['page_size'] = args.page_size
436
437    if args.sort_key:
438        kwargs['sort_key'] = args.sort_key
439    if args.sort_dir:
440        kwargs['sort_dir'] = args.sort_dir
441    if args.sort is not None:
442        kwargs['sort'] = args.sort
443    elif not args.sort_dir and not args.sort_key:
444        kwargs['sort_key'] = 'name'
445        kwargs['sort_dir'] = 'asc'
446
447    columns = ['ID', 'Name']
448
449    if args.verbose:
450        columns += ['Disk_format', 'Container_format', 'Size', 'Status',
451                    'Owner']
452
453    if args.include_stores:
454        columns += ['Stores']
455
456    images = gc.images.list(**kwargs)
457    utils.print_list(images, columns)
458
459
460@utils.arg('id', metavar='<IMAGE_ID>', help=_('ID of image to describe.'))
461@utils.arg('--human-readable', action='store_true', default=False,
462           help=_('Print image size in a human-friendly format.'))
463@utils.arg('--max-column-width', metavar='<integer>', default=80,
464           help=_('The max column width of the printed table.'))
465def do_image_show(gc, args):
466    """Describe a specific image."""
467    image = gc.images.get(args.id)
468    utils.print_image(image, args.human_readable, int(args.max_column_width))
469
470
471@utils.arg('id', metavar='<IMAGE_ID>', help=_('ID of image to get tasks.'))
472def do_image_tasks(gc, args):
473    """Get tasks associated with image"""
474    columns = ['Message', 'Status', 'Updated at']
475    if args.verbose:
476        columns_to_prepend = ['Image Id', 'Task Id']
477        columns_to_extend = ['User Id', 'Request Id',
478                             'Result', 'Owner', 'Input', 'Expires at']
479        columns = columns_to_prepend + columns + columns_to_extend
480    try:
481        tasks = gc.images.get_associated_image_tasks(args.id)
482        utils.print_dict_list(tasks['tasks'], columns)
483    except exc.HTTPNotFound:
484        utils.exit('Image %s not found.' % args.id)
485    except exc.HTTPNotImplemented:
486        utils.exit('Server does not support image tasks API (v2.12)')
487
488
489@utils.arg('--image-id', metavar='<IMAGE_ID>', required=True,
490           help=_('Image to display members of.'))
491def do_member_list(gc, args):
492    """Describe sharing permissions by image."""
493    members = gc.image_members.list(args.image_id)
494    columns = ['Image ID', 'Member ID', 'Status']
495    utils.print_list(members, columns)
496
497
498@utils.arg('image_id', metavar='<IMAGE_ID>',
499           help=_('Image from which to display member.'))
500@utils.arg('member_id', metavar='<MEMBER_ID>',
501           help=_('Project to display.'))
502def do_member_get(gc, args):
503    """Show details of an image member"""
504    member = gc.image_members.get(args.image_id, args.member_id)
505    utils.print_dict(member)
506
507@utils.arg('image_id', metavar='<IMAGE_ID>',
508           help=_('Image from which to remove member.'))
509@utils.arg('member_id', metavar='<MEMBER_ID>',
510           help=_('Tenant to remove as member.'))
511def do_member_delete(gc, args):
512    """Delete image member."""
513    if not (args.image_id and args.member_id):
514        utils.exit('Unable to delete member. Specify image_id and member_id')
515    else:
516        gc.image_members.delete(args.image_id, args.member_id)
517
518
519@utils.arg('image_id', metavar='<IMAGE_ID>',
520           help=_('Image from which to update member.'))
521@utils.arg('member_id', metavar='<MEMBER_ID>',
522           help=_('Tenant to update.'))
523@utils.arg('member_status', metavar='<MEMBER_STATUS>',
524           choices=MEMBER_STATUS_VALUES,
525           help=(_('Updated status of member. Valid Values: %s') %
526                 ', '.join(str(val) for val in MEMBER_STATUS_VALUES)))
527def do_member_update(gc, args):
528    """Update the status of a member for a given image."""
529    if not (args.image_id and args.member_id and args.member_status):
530        utils.exit('Unable to update member. Specify image_id, member_id and'
531                   ' member_status')
532    else:
533        member = gc.image_members.update(args.image_id, args.member_id,
534                                         args.member_status)
535        member = [member]
536        columns = ['Image ID', 'Member ID', 'Status']
537        utils.print_list(member, columns)
538
539
540@utils.arg('image_id', metavar='<IMAGE_ID>',
541           help=_('Image with which to create member.'))
542@utils.arg('member_id', metavar='<MEMBER_ID>',
543           help=_('Tenant to add as member.'))
544def do_member_create(gc, args):
545    """Create member for a given image."""
546    if not (args.image_id and args.member_id):
547        utils.exit('Unable to create member. Specify image_id and member_id')
548    else:
549        member = gc.image_members.create(args.image_id, args.member_id)
550        member = [member]
551        columns = ['Image ID', 'Member ID', 'Status']
552        utils.print_list(member, columns)
553
554
555@utils.arg('model', metavar='<MODEL>', help=_('Name of model to describe.'))
556def do_explain(gc, args):
557    """Describe a specific model."""
558    try:
559        schema = gc.schemas.get(args.model)
560    except exc.HTTPNotFound:
561        utils.exit('Unable to find requested model \'%s\'' % args.model)
562    else:
563        formatters = {'Attribute': lambda m: m.name}
564        columns = ['Attribute', 'Description']
565        utils.print_list(schema.properties, columns, formatters)
566
567
568def do_import_info(gc, args):
569    """Print import methods available from Glance."""
570    try:
571        import_info = gc.images.get_import_info()
572    except exc.HTTPNotFound:
573        utils.exit('Target Glance does not support Image Import workflow')
574    else:
575        utils.print_dict(import_info)
576
577
578def do_stores_info(gc, args):
579    """Print available backends from Glance."""
580    try:
581        stores_info = gc.images.get_stores_info()
582    except exc.HTTPNotFound:
583        utils.exit('Multi Backend support is not enabled')
584    else:
585        utils.print_dict(stores_info)
586
587
588@utils.arg('id', metavar='<IMAGE_ID>', help=_('ID of image to update.'))
589@utils.arg('--store', metavar='<STORE_ID>', required=True,
590           help=_('Store to delete image from.'))
591def do_stores_delete(gc, args):
592    """Delete image from specific store."""
593    try:
594        gc.images.delete_from_store(args.store, args.id)
595    except exc.HTTPNotFound:
596        utils.exit('Multi Backend support is not enabled or Image/store not '
597                   'found.')
598    except (exc.HTTPForbidden, exc.HTTPException) as e:
599        msg = ("Unable to delete image '%s' from store '%s'. (%s)" % (
600               args.id,
601               args.store,
602               e))
603        utils.exit(msg)
604
605
606@utils.arg('--allow-md5-fallback', action='store_true',
607           default=utils.env('OS_IMAGE_ALLOW_MD5_FALLBACK', default=False),
608           help=_('If os_hash_algo and os_hash_value properties are available '
609                  'on the image, they will be used to validate the downloaded '
610                  'image data. If the indicated secure hash algorithm is not '
611                  'available on the client, the download will fail. Use this '
612                  'flag to indicate that in such a case the legacy MD5 image '
613                  'checksum should be used to validate the downloaded data. '
614                  'You can also set the environment variable '
615                  'OS_IMAGE_ALLOW_MD5_FALLBACK to any value to activate this '
616                  'option.'))
617@utils.arg('--file', metavar='<FILE>',
618           help=_('Local file to save downloaded image data to. '
619                  'If this is not specified and there is no redirection '
620                  'the image data will not be saved.'))
621@utils.arg('id', metavar='<IMAGE_ID>', help=_('ID of image to download.'))
622@utils.arg('--progress', action='store_true', default=False,
623           help=_('Show download progress bar.'))
624def do_image_download(gc, args):
625    """Download a specific image."""
626    if sys.stdout.isatty() and (args.file is None):
627        msg = ('No redirection or local file specified for downloaded image '
628               'data. Please specify a local file with --file to save '
629               'downloaded image or redirect output to another source.')
630        utils.exit(msg)
631
632    try:
633        body = gc.images.data(args.id,
634                              allow_md5_fallback=args.allow_md5_fallback)
635    except (exc.HTTPForbidden, exc.HTTPException) as e:
636        msg = "Unable to download image '%s'. (%s)" % (args.id, e)
637        utils.exit(msg)
638
639    if body.wrapped is None:
640        msg = ('Image %s has no data.' % args.id)
641        utils.exit(msg)
642
643    if args.progress:
644        body = progressbar.VerboseIteratorWrapper(body, len(body))
645
646    utils.save_image(body, args.file)
647
648
649@utils.arg('--file', metavar='<FILE>',
650           help=_('Local file that contains disk image to be uploaded.'
651                  ' Alternatively, images can be passed'
652                  ' to the client via stdin.'))
653@utils.arg('--size', metavar='<IMAGE_SIZE>', type=int,
654           help=_('Size in bytes of image to be uploaded. Default is to get '
655                  'size from provided data object but this is supported in '
656                  'case where size cannot be inferred.'),
657           default=None)
658@utils.arg('--progress', action='store_true', default=False,
659           help=_('Show upload progress bar.'))
660@utils.arg('id', metavar='<IMAGE_ID>',
661           help=_('ID of image to upload data to.'))
662@utils.arg('--store', metavar='<STORE>',
663           default=utils.env('OS_IMAGE_STORE', default=None),
664           help='Backend store to upload image to.')
665def do_image_upload(gc, args):
666    """Upload data for a specific image."""
667    backend = None
668    if args.store:
669        backend = args.store
670        # determine if backend is valid
671        _validate_backend(backend, gc)
672
673    image_data = utils.get_data_file(args)
674    if args.progress:
675        filesize = utils.get_file_size(image_data)
676        if filesize is not None:
677            # NOTE(kragniz): do not show a progress bar if the size of the
678            # input is unknown (most likely a piped input)
679            image_data = progressbar.VerboseFileWrapper(image_data, filesize)
680    gc.images.upload(args.id, image_data, args.size, backend=backend)
681
682
683@utils.arg('--file', metavar='<FILE>',
684           help=_('Local file that contains disk image to be uploaded.'
685                  ' Alternatively, images can be passed'
686                  ' to the client via stdin.'))
687@utils.arg('--size', metavar='<IMAGE_SIZE>', type=int,
688           help=_('Size in bytes of image to be uploaded. Default is to get '
689                  'size from provided data object but this is supported in '
690                  'case where size cannot be inferred.'),
691           default=None)
692@utils.arg('--progress', action='store_true', default=False,
693           help=_('Show upload progress bar.'))
694@utils.arg('id', metavar='<IMAGE_ID>',
695           help=_('ID of image to upload data to.'))
696def do_image_stage(gc, args):
697    """Upload data for a specific image to staging."""
698    image_data = utils.get_data_file(args)
699    if args.progress:
700        filesize = utils.get_file_size(image_data)
701        if filesize is not None:
702            # NOTE(kragniz): do not show a progress bar if the size of the
703            # input is unknown (most likely a piped input)
704            image_data = progressbar.VerboseFileWrapper(image_data, filesize)
705    gc.images.stage(args.id, image_data, args.size)
706
707
708@utils.arg('--import-method', metavar='<METHOD>', default='glance-direct',
709           help=_('Import method used for Image Import workflow. '
710                  'Valid values can be retrieved with import-info command '
711                  'and the default "glance-direct" is used with '
712                  '"image-stage".'))
713@utils.arg('--uri', metavar='<IMAGE_URL>', default=None,
714           help=_('URI to download the external image.'))
715@utils.arg('id', metavar='<IMAGE_ID>',
716           help=_('ID of image to import.'))
717@utils.arg('--store', metavar='<STORE>',
718           default=utils.env('OS_IMAGE_STORE', default=None),
719           help='Backend store to upload image to.')
720@utils.arg('--stores', metavar='<STORES>',
721           default=utils.env('OS_IMAGE_STORES', default=None),
722           help='Stores to upload image to if multi-stores import available.')
723@utils.arg('--all-stores', type=strutils.bool_from_string,
724           metavar='[True|False]',
725           default=None,
726           dest='os_all_stores',
727           help=_('"all-stores" can be ued instead of "stores"-list to '
728                  'indicate that image should be imported all available '
729                  'stores.'))
730@utils.arg('--allow-failure', type=strutils.bool_from_string,
731           metavar='[True|False]',
732           dest='os_allow_failure',
733           default=utils.env('OS_IMAGE_ALLOW_FAILURE', default=True),
734           help=_('Indicator if all stores listed (or available) must '
735                  'succeed. "True" by default meaning that we allow some '
736                  'stores to fail and the status can be monitored from the '
737                  'image metadata. If this is set to "False" the import will '
738                  'be reverted should any of the uploads fail. Only usable '
739                  'with "stores" or "all-stores".'))
740def do_image_import(gc, args):
741    """Initiate the image import taskflow."""
742    backend = getattr(args, "store", None)
743    stores = getattr(args, "stores", None)
744    all_stores = getattr(args, "os_all_stores", None)
745    allow_failure = getattr(args, "os_allow_failure", True)
746
747    if not getattr(args, 'from_create', False):
748        if (args.store and (stores or all_stores)) or (stores and all_stores):
749            utils.exit("Only one of --store, --stores and --all-stores can be "
750                       "provided")
751        elif args.store:
752            backend = args.store
753            # determine if backend is valid
754            _validate_backend(backend, gc)
755        elif stores:
756            stores = str(stores).split(',')
757
758        # determine if backend is valid
759        if stores:
760            for store in stores:
761                _validate_backend(store, gc)
762
763    if getattr(args, 'from_create', False):
764        # this command is being called "internally" so we can skip
765        # validation -- just do the import and get out of here
766        gc.images.image_import(args.id, args.import_method, args.uri,
767                               backend=backend,
768                               stores=stores, all_stores=all_stores,
769                               allow_failure=allow_failure)
770        return
771
772    # do input validation
773    try:
774        import_methods = gc.images.get_import_info().get('import-methods')
775    except exc.HTTPNotFound:
776        utils.exit('Target Glance does not support Image Import workflow')
777
778    if args.import_method not in import_methods.get('value'):
779        utils.exit("Import method '%s' is not valid for this cloud. "
780                   "Valid values can be retrieved with import-info command." %
781                   args.import_method)
782
783    if args.import_method == 'web-download' and not args.uri:
784        utils.exit("Provide URI for web-download import method.")
785    if args.uri and args.import_method != 'web-download':
786        utils.exit("Import method should be 'web-download' if URI is "
787                   "provided.")
788
789    if args.import_method == 'copy-image' and not (stores or all_stores):
790        utils.exit("Provide either --stores or --all-stores for "
791                   "'copy-image' import method.")
792
793    # check image properties
794    image = gc.images.get(args.id)
795    container_format = image.get('container_format')
796    disk_format = image.get('disk_format')
797    if not (container_format and disk_format):
798        utils.exit("The 'container_format' and 'disk_format' properties "
799                   "must be set on an image before it can be imported.")
800
801    image_status = image.get('status')
802    if args.import_method == 'glance-direct':
803        if image_status != 'uploading':
804            utils.exit("The 'glance-direct' import method can only be applied "
805                       "to an image in status 'uploading'")
806    if args.import_method == 'web-download':
807        if image_status != 'queued':
808            utils.exit("The 'web-download' import method can only be applied "
809                       "to an image in status 'queued'")
810    if args.import_method == 'copy-image':
811        if image_status != 'active':
812            utils.exit("The 'copy-image' import method can only be used on "
813                       "an image with status 'active'.")
814
815    # finally, do the import
816    gc.images.image_import(args.id, args.import_method, args.uri,
817                           backend=backend,
818                           stores=stores, all_stores=all_stores,
819                           allow_failure=allow_failure)
820
821    image = gc.images.get(args.id)
822    utils.print_image(image)
823
824
825@utils.arg('id', metavar='<IMAGE_ID>', nargs='+',
826           help=_('ID of image(s) to delete.'))
827def do_image_delete(gc, args):
828    """Delete specified image."""
829    failure_flag = False
830    for args_id in args.id:
831        try:
832            gc.images.delete(args_id)
833        except exc.HTTPForbidden:
834            msg = "You are not permitted to delete the image '%s'." % args_id
835            utils.print_err(msg)
836            failure_flag = True
837        except exc.HTTPNotFound:
838            msg = "No image with an ID of '%s' exists." % args_id
839            utils.print_err(msg)
840            failure_flag = True
841        except exc.HTTPConflict:
842            msg = "Unable to delete image '%s' because it is in use." % args_id
843            utils.print_err(msg)
844            failure_flag = True
845        except exc.HTTPException as e:
846            msg = "'%s': Unable to delete image '%s'" % (e, args_id)
847            utils.print_err(msg)
848            failure_flag = True
849    if failure_flag:
850        utils.exit()
851
852
853@utils.arg('id', metavar='<IMAGE_ID>',
854           help=_('ID of image to deactivate.'))
855def do_image_deactivate(gc, args):
856    """Deactivate specified image."""
857    gc.images.deactivate(args.id)
858
859
860@utils.arg('id', metavar='<IMAGE_ID>',
861           help=_('ID of image to reactivate.'))
862def do_image_reactivate(gc, args):
863    """Reactivate specified image."""
864    gc.images.reactivate(args.id)
865
866
867@utils.arg('image_id', metavar='<IMAGE_ID>',
868           help=_('Image to be updated with the given tag.'))
869@utils.arg('tag_value', metavar='<TAG_VALUE>',
870           help=_('Value of the tag.'))
871def do_image_tag_update(gc, args):
872    """Update an image with the given tag."""
873    if not (args.image_id and args.tag_value):
874        utils.exit('Unable to update tag. Specify image_id and tag_value')
875    else:
876        gc.image_tags.update(args.image_id, args.tag_value)
877        image = gc.images.get(args.image_id)
878        image = [image]
879        columns = ['ID', 'Tags']
880        utils.print_list(image, columns)
881
882
883@utils.arg('image_id', metavar='<IMAGE_ID>',
884           help=_('ID of the image from which to delete tag.'))
885@utils.arg('tag_value', metavar='<TAG_VALUE>',
886           help=_('Value of the tag.'))
887def do_image_tag_delete(gc, args):
888    """Delete the tag associated with the given image."""
889    if not (args.image_id and args.tag_value):
890        utils.exit('Unable to delete tag. Specify image_id and tag_value')
891    else:
892        gc.image_tags.delete(args.image_id, args.tag_value)
893
894
895@utils.arg('--url', metavar='<URL>', required=True,
896           help=_('URL of location to add.'))
897@utils.arg('--metadata', metavar='<STRING>', default='{}',
898           help=_('Metadata associated with the location. '
899                  'Must be a valid JSON object (default: %(default)s)'))
900@utils.arg('--checksum', metavar='<STRING>',
901           help=_('md5 checksum of image contents'))
902@utils.arg('--hash-algo', metavar='<STRING>',
903           help=_('Multihash algorithm'))
904@utils.arg('--hash-value', metavar='<STRING>',
905           help=_('Multihash value'))
906@utils.arg('id', metavar='<IMAGE_ID>',
907           help=_('ID of image to which the location is to be added.'))
908def do_location_add(gc, args):
909    """Add a location (and related metadata) to an image."""
910    validation_data = {}
911    if args.checksum:
912        validation_data['checksum'] = args.checksum
913    if args.hash_algo:
914        validation_data['os_hash_algo'] = args.hash_algo
915    if args.hash_value:
916        validation_data['os_hash_value'] = args.hash_value
917    try:
918        metadata = json.loads(args.metadata)
919    except ValueError:
920        utils.exit('Metadata is not a valid JSON object.')
921    else:
922        image = gc.images.add_location(args.id, args.url, metadata,
923                                       validation_data=validation_data)
924        utils.print_dict(image)
925
926
927@utils.arg('--url', metavar='<URL>', action='append', required=True,
928           help=_('URL of location to remove. May be used multiple times.'))
929@utils.arg('id', metavar='<IMAGE_ID>',
930           help=_('ID of image whose locations are to be removed.'))
931def do_location_delete(gc, args):
932    """Remove locations (and related metadata) from an image."""
933    gc.images.delete_locations(args.id, set(args.url))
934
935
936@utils.arg('--url', metavar='<URL>', required=True,
937           help=_('URL of location to update.'))
938@utils.arg('--metadata', metavar='<STRING>', default='{}',
939           help=_('Metadata associated with the location. '
940                  'Must be a valid JSON object (default: %(default)s)'))
941@utils.arg('id', metavar='<IMAGE_ID>',
942           help=_('ID of image whose location is to be updated.'))
943def do_location_update(gc, args):
944    """Update metadata of an image's location."""
945    try:
946        metadata = json.loads(args.metadata)
947
948        if metadata == {}:
949            print("WARNING -- The location's metadata will be updated to "
950                  "an empty JSON object.")
951    except ValueError:
952        utils.exit('Metadata is not a valid JSON object.')
953    else:
954        image = gc.images.update_location(args.id, args.url, metadata)
955        utils.print_dict(image)
956
957
958# Metadata - catalog
959NAMESPACE_SCHEMA = None
960
961
962def get_namespace_schema():
963    global NAMESPACE_SCHEMA
964    if NAMESPACE_SCHEMA is None:
965        schema_path = os.path.expanduser("~/.glanceclient/"
966                                         "namespace_schema.json")
967        if os.path.isfile(schema_path):
968            with open(schema_path, "r") as f:
969                schema_raw = f.read()
970                NAMESPACE_SCHEMA = json.loads(schema_raw)
971        else:
972            return namespace_schema.BASE_SCHEMA
973    return NAMESPACE_SCHEMA
974
975
976def _namespace_show(namespace, max_column_width=None):
977    namespace = dict(namespace)  # Warlock objects are compatible with dicts
978    # Flatten dicts for display
979    if 'properties' in namespace:
980        props = [k for k in namespace['properties']]
981        namespace['properties'] = props
982    if 'resource_type_associations' in namespace:
983        assocs = [assoc['name']
984                  for assoc in namespace['resource_type_associations']]
985        namespace['resource_type_associations'] = assocs
986    if 'objects' in namespace:
987        objects = [obj['name'] for obj in namespace['objects']]
988        namespace['objects'] = objects
989
990    if 'tags' in namespace:
991        tags = [tag['name'] for tag in namespace['tags']]
992        namespace['tags'] = tags
993
994    if max_column_width:
995        utils.print_dict(namespace, max_column_width)
996    else:
997        utils.print_dict(namespace)
998
999
1000@utils.arg('namespace', metavar='<NAMESPACE>',
1001           help=_('Name of the namespace.'))
1002@utils.schema_args(get_namespace_schema, omit=['namespace', 'property_count',
1003                                               'properties', 'tag_count',
1004                                               'tags', 'object_count',
1005                                               'objects', 'resource_types'])
1006def do_md_namespace_create(gc, args):
1007    """Create a new metadata definitions namespace."""
1008    schema = gc.schemas.get('metadefs/namespace')
1009    _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()]
1010    fields = dict(filter(lambda x: x[1] is not None and
1011                         (schema.is_core_property(x[0])),
1012                         _args))
1013    namespace = gc.metadefs_namespace.create(**fields)
1014
1015    _namespace_show(namespace)
1016
1017
1018@utils.arg('--file', metavar='<FILEPATH>',
1019           help=_('Path to file with namespace schema to import. '
1020                  'Alternatively, namespaces schema can be passed to the '
1021                  'client via stdin.'))
1022def do_md_namespace_import(gc, args):
1023    """Import a metadata definitions namespace from file or standard input."""
1024    namespace_data = utils.get_data_file(args)
1025    if not namespace_data:
1026        utils.exit('No metadata definition namespace passed via stdin or '
1027                   '--file argument.')
1028
1029    try:
1030        namespace_json = json.load(namespace_data)
1031    except ValueError:
1032        utils.exit('Schema is not a valid JSON object.')
1033    else:
1034        namespace = gc.metadefs_namespace.create(**namespace_json)
1035        _namespace_show(namespace)
1036
1037
1038@utils.arg('id', metavar='<NAMESPACE>', help=_('Name of namespace to update.'))
1039@utils.schema_args(get_namespace_schema, omit=['property_count', 'properties',
1040                                               'tag_count', 'tags',
1041                                               'object_count', 'objects',
1042                                               'resource_type_associations',
1043                                               'schema'])
1044def do_md_namespace_update(gc, args):
1045    """Update an existing metadata definitions namespace."""
1046    schema = gc.schemas.get('metadefs/namespace')
1047
1048    _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()]
1049    fields = dict(filter(lambda x: x[1] is not None and
1050                         (schema.is_core_property(x[0])),
1051                         _args))
1052    namespace = gc.metadefs_namespace.update(args.id, **fields)
1053
1054    _namespace_show(namespace)
1055
1056
1057@utils.arg('namespace', metavar='<NAMESPACE>',
1058           help=_('Name of namespace to describe.'))
1059@utils.arg('--resource-type', metavar='<RESOURCE_TYPE>',
1060           help=_('Applies prefix of given resource type associated to a '
1061                  'namespace to all properties of a namespace.'), default=None)
1062@utils.arg('--max-column-width', metavar='<integer>', default=80,
1063           help=_('The max column width of the printed table.'))
1064def do_md_namespace_show(gc, args):
1065    """Describe a specific metadata definitions namespace.
1066
1067    Lists also the namespace properties, objects and resource type
1068    associations.
1069    """
1070    kwargs = {}
1071    if args.resource_type:
1072        kwargs['resource_type'] = args.resource_type
1073
1074    namespace = gc.metadefs_namespace.get(args.namespace, **kwargs)
1075    _namespace_show(namespace, int(args.max_column_width))
1076
1077
1078@utils.arg('--resource-types', metavar='<RESOURCE_TYPES>', action='append',
1079           help=_('Resource type to filter namespaces.'))
1080@utils.arg('--visibility', metavar='<VISIBILITY>',
1081           help=_('Visibility parameter to filter namespaces.'))
1082@utils.arg('--page-size', metavar='<SIZE>', default=None, type=int,
1083           help=_('Number of namespaces to request '
1084                  'in each paginated request.'))
1085def do_md_namespace_list(gc, args):
1086    """List metadata definitions namespaces."""
1087    filter_keys = ['resource_types', 'visibility']
1088    filter_items = [(key, getattr(args, key, None)) for key in filter_keys]
1089    filters = dict([item for item in filter_items if item[1] is not None])
1090
1091    kwargs = {'filters': filters}
1092    if args.page_size is not None:
1093        kwargs['page_size'] = args.page_size
1094
1095    namespaces = gc.metadefs_namespace.list(**kwargs)
1096    columns = ['namespace']
1097    utils.print_list(namespaces, columns)
1098
1099
1100@utils.arg('namespace', metavar='<NAMESPACE>',
1101           help=_('Name of namespace to delete.'))
1102def do_md_namespace_delete(gc, args):
1103    """Delete specified metadata definitions namespace with its contents."""
1104    gc.metadefs_namespace.delete(args.namespace)
1105
1106
1107# Metadata - catalog
1108RESOURCE_TYPE_SCHEMA = None
1109
1110
1111def get_resource_type_schema():
1112    global RESOURCE_TYPE_SCHEMA
1113    if RESOURCE_TYPE_SCHEMA is None:
1114        schema_path = os.path.expanduser("~/.glanceclient/"
1115                                         "resource_type_schema.json")
1116        if os.path.isfile(schema_path):
1117            with open(schema_path, "r") as f:
1118                schema_raw = f.read()
1119                RESOURCE_TYPE_SCHEMA = json.loads(schema_raw)
1120        else:
1121            return resource_type_schema.BASE_SCHEMA
1122    return RESOURCE_TYPE_SCHEMA
1123
1124
1125@utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.'))
1126@utils.schema_args(get_resource_type_schema)
1127def do_md_resource_type_associate(gc, args):
1128    """Associate resource type with a metadata definitions namespace."""
1129    schema = gc.schemas.get('metadefs/resource_type')
1130    _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()]
1131    fields = dict(filter(lambda x: x[1] is not None and
1132                         (schema.is_core_property(x[0])),
1133                         _args))
1134    resource_type = gc.metadefs_resource_type.associate(args.namespace,
1135                                                        **fields)
1136    utils.print_dict(resource_type)
1137
1138
1139@utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.'))
1140@utils.arg('resource_type', metavar='<RESOURCE_TYPE>',
1141           help=_('Name of resource type.'))
1142def do_md_resource_type_deassociate(gc, args):
1143    """Deassociate resource type with a metadata definitions namespace."""
1144    gc.metadefs_resource_type.deassociate(args.namespace, args.resource_type)
1145
1146
1147def do_md_resource_type_list(gc, args):
1148    """List available resource type names."""
1149    resource_types = gc.metadefs_resource_type.list()
1150    utils.print_list(resource_types, ['name'])
1151
1152
1153@utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.'))
1154def do_md_namespace_resource_type_list(gc, args):
1155    """List resource types associated to specific namespace."""
1156    resource_types = gc.metadefs_resource_type.get(args.namespace)
1157    utils.print_list(resource_types, ['name', 'prefix', 'properties_target'])
1158
1159
1160@utils.arg('namespace', metavar='<NAMESPACE>',
1161           help=_('Name of namespace the property will belong.'))
1162@utils.arg('--name', metavar='<NAME>', required=True,
1163           help=_('Internal name of a property.'))
1164@utils.arg('--title', metavar='<TITLE>', required=True,
1165           help=_('Property name displayed to the user.'))
1166@utils.arg('--schema', metavar='<SCHEMA>', required=True,
1167           help=_('Valid JSON schema of a property.'))
1168def do_md_property_create(gc, args):
1169    """Create a new metadata definitions property inside a namespace."""
1170    try:
1171        schema = json.loads(args.schema)
1172    except ValueError:
1173        utils.exit('Schema is not a valid JSON object.')
1174    else:
1175        fields = {'name': args.name, 'title': args.title}
1176        fields.update(schema)
1177        new_property = gc.metadefs_property.create(args.namespace, **fields)
1178        utils.print_dict(new_property)
1179
1180
1181@utils.arg('namespace', metavar='<NAMESPACE>',
1182           help=_('Name of namespace the property belongs.'))
1183@utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.'))
1184@utils.arg('--name', metavar='<NAME>', default=None,
1185           help=_('New name of a property.'))
1186@utils.arg('--title', metavar='<TITLE>', default=None,
1187           help=_('Property name displayed to the user.'))
1188@utils.arg('--schema', metavar='<SCHEMA>', default=None,
1189           help=_('Valid JSON schema of a property.'))
1190def do_md_property_update(gc, args):
1191    """Update metadata definitions property inside a namespace."""
1192    fields = {}
1193    if args.name:
1194        fields['name'] = args.name
1195    if args.title:
1196        fields['title'] = args.title
1197    if args.schema:
1198        try:
1199            schema = json.loads(args.schema)
1200        except ValueError:
1201            utils.exit('Schema is not a valid JSON object.')
1202        else:
1203            fields.update(schema)
1204
1205    new_property = gc.metadefs_property.update(args.namespace, args.property,
1206                                               **fields)
1207    utils.print_dict(new_property)
1208
1209
1210@utils.arg('namespace', metavar='<NAMESPACE>',
1211           help=_('Name of namespace the property belongs.'))
1212@utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.'))
1213@utils.arg('--max-column-width', metavar='<integer>', default=80,
1214           help=_('The max column width of the printed table.'))
1215def do_md_property_show(gc, args):
1216    """Describe a specific metadata definitions property inside a namespace."""
1217    prop = gc.metadefs_property.get(args.namespace, args.property)
1218    utils.print_dict(prop, int(args.max_column_width))
1219
1220
1221@utils.arg('namespace', metavar='<NAMESPACE>',
1222           help=_('Name of namespace the property belongs.'))
1223@utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.'))
1224def do_md_property_delete(gc, args):
1225    """Delete a specific metadata definitions property inside a namespace."""
1226    gc.metadefs_property.delete(args.namespace, args.property)
1227
1228
1229@utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.'))
1230def do_md_namespace_properties_delete(gc, args):
1231    """Delete all metadata definitions property inside a specific namespace."""
1232    gc.metadefs_property.delete_all(args.namespace)
1233
1234
1235@utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.'))
1236def do_md_property_list(gc, args):
1237    """List metadata definitions properties inside a specific namespace."""
1238    properties = gc.metadefs_property.list(args.namespace)
1239    columns = ['name', 'title', 'type']
1240    utils.print_list(properties, columns)
1241
1242
1243def _object_show(obj, max_column_width=None):
1244    obj = dict(obj)  # Warlock objects are compatible with dicts
1245    # Flatten dicts for display
1246    if 'properties' in obj:
1247        objects = [k for k in obj['properties']]
1248        obj['properties'] = objects
1249
1250    if max_column_width:
1251        utils.print_dict(obj, max_column_width)
1252    else:
1253        utils.print_dict(obj)
1254
1255
1256@utils.arg('namespace', metavar='<NAMESPACE>',
1257           help=_('Name of namespace the object will belong.'))
1258@utils.arg('--name', metavar='<NAME>', required=True,
1259           help=_('Internal name of an object.'))
1260@utils.arg('--schema', metavar='<SCHEMA>', required=True,
1261           help=_('Valid JSON schema of an object.'))
1262def do_md_object_create(gc, args):
1263    """Create a new metadata definitions object inside a namespace."""
1264    try:
1265        schema = json.loads(args.schema)
1266    except ValueError:
1267        utils.exit('Schema is not a valid JSON object.')
1268    else:
1269        fields = {'name': args.name}
1270        fields.update(schema)
1271        new_object = gc.metadefs_object.create(args.namespace, **fields)
1272        _object_show(new_object)
1273
1274
1275@utils.arg('namespace', metavar='<NAMESPACE>',
1276           help=_('Name of namespace the object belongs.'))
1277@utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.'))
1278@utils.arg('--name', metavar='<NAME>', default=None,
1279           help=_('New name of an object.'))
1280@utils.arg('--schema', metavar='<SCHEMA>', default=None,
1281           help=_('Valid JSON schema of an object.'))
1282def do_md_object_update(gc, args):
1283    """Update metadata definitions object inside a namespace."""
1284    fields = {}
1285    if args.name:
1286        fields['name'] = args.name
1287    if args.schema:
1288        try:
1289            schema = json.loads(args.schema)
1290        except ValueError:
1291            utils.exit('Schema is not a valid JSON object.')
1292        else:
1293            fields.update(schema)
1294
1295    new_object = gc.metadefs_object.update(args.namespace, args.object,
1296                                           **fields)
1297    _object_show(new_object)
1298
1299
1300@utils.arg('namespace', metavar='<NAMESPACE>',
1301           help=_('Name of namespace the object belongs.'))
1302@utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.'))
1303@utils.arg('--max-column-width', metavar='<integer>', default=80,
1304           help=_('The max column width of the printed table.'))
1305def do_md_object_show(gc, args):
1306    """Describe a specific metadata definitions object inside a namespace."""
1307    obj = gc.metadefs_object.get(args.namespace, args.object)
1308    _object_show(obj, int(args.max_column_width))
1309
1310
1311@utils.arg('namespace', metavar='<NAMESPACE>',
1312           help=_('Name of namespace the object belongs.'))
1313@utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.'))
1314@utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.'))
1315@utils.arg('--max-column-width', metavar='<integer>', default=80,
1316           help=_('The max column width of the printed table.'))
1317def do_md_object_property_show(gc, args):
1318    """Describe a specific metadata definitions property inside an object."""
1319    obj = gc.metadefs_object.get(args.namespace, args.object)
1320    try:
1321        prop = obj['properties'][args.property]
1322        prop['name'] = args.property
1323    except KeyError:
1324        utils.exit('Property %s not found in object %s.' % (args.property,
1325                   args.object))
1326    utils.print_dict(prop, int(args.max_column_width))
1327
1328
1329@utils.arg('namespace', metavar='<NAMESPACE>',
1330           help=_('Name of namespace the object belongs.'))
1331@utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.'))
1332def do_md_object_delete(gc, args):
1333    """Delete a specific metadata definitions object inside a namespace."""
1334    gc.metadefs_object.delete(args.namespace, args.object)
1335
1336
1337@utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.'))
1338def do_md_namespace_objects_delete(gc, args):
1339    """Delete all metadata definitions objects inside a specific namespace."""
1340    gc.metadefs_object.delete_all(args.namespace)
1341
1342
1343@utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.'))
1344def do_md_object_list(gc, args):
1345    """List metadata definitions objects inside a specific namespace."""
1346    objects = gc.metadefs_object.list(args.namespace)
1347    columns = ['name', 'description']
1348    column_settings = {
1349        "description": {
1350            "max_width": 50,
1351            "align": "l"
1352        }
1353    }
1354    utils.print_list(objects, columns, field_settings=column_settings)
1355
1356
1357def _tag_show(tag, max_column_width=None):
1358    tag = dict(tag)  # Warlock objects are compatible with dicts
1359    if max_column_width:
1360        utils.print_dict(tag, max_column_width)
1361    else:
1362        utils.print_dict(tag)
1363
1364
1365@utils.arg('namespace', metavar='<NAMESPACE>',
1366           help=_('Name of the namespace the tag will belong to.'))
1367@utils.arg('--name', metavar='<NAME>', required=True,
1368           help=_('The name of the new tag to add.'))
1369def do_md_tag_create(gc, args):
1370    """Add a new metadata definitions tag inside a namespace."""
1371    name = args.name.strip()
1372    if name:
1373        new_tag = gc.metadefs_tag.create(args.namespace, name)
1374        _tag_show(new_tag)
1375    else:
1376        utils.exit('Please supply at least one non-blank tag name.')
1377
1378
1379@utils.arg('namespace', metavar='<NAMESPACE>',
1380           help=_('Name of the namespace the tags will belong to.'))
1381@utils.arg('--names', metavar='<NAMES>', required=True,
1382           help=_('A comma separated list of tag names.'))
1383@utils.arg('--delim', metavar='<DELIM>', required=False,
1384           help=_('The delimiter used to separate the names'
1385                  ' (if none is provided then the default is a comma).'))
1386def do_md_tag_create_multiple(gc, args):
1387    """Create new metadata definitions tags inside a namespace."""
1388    delim = args.delim or ','
1389
1390    tags = []
1391    names_list = args.names.split(delim)
1392    for name in names_list:
1393        name = name.strip()
1394        if name:
1395            tags.append(name)
1396
1397    if not tags:
1398        utils.exit('Please supply at least one tag name. For example: '
1399                   '--names Tag1')
1400
1401    fields = {'tags': tags}
1402    new_tags = gc.metadefs_tag.create_multiple(args.namespace, **fields)
1403    columns = ['name']
1404    column_settings = {
1405        "description": {
1406            "max_width": 50,
1407            "align": "l"
1408        }
1409    }
1410    utils.print_list(new_tags, columns, field_settings=column_settings)
1411
1412
1413@utils.arg('namespace', metavar='<NAMESPACE>',
1414           help=_('Name of the namespace to which the tag belongs.'))
1415@utils.arg('tag', metavar='<TAG>', help=_('Name of the old tag.'))
1416@utils.arg('--name', metavar='<NAME>', default=None, required=True,
1417           help=_('New name of the new tag.'))
1418def do_md_tag_update(gc, args):
1419    """Rename a metadata definitions tag inside a namespace."""
1420    name = args.name.strip()
1421    if name:
1422        fields = {'name': name}
1423        new_tag = gc.metadefs_tag.update(args.namespace, args.tag,
1424                                         **fields)
1425        _tag_show(new_tag)
1426    else:
1427        utils.exit('Please supply at least one non-blank tag name.')
1428
1429
1430@utils.arg('namespace', metavar='<NAMESPACE>',
1431           help=_('Name of the namespace to which the tag belongs.'))
1432@utils.arg('tag', metavar='<TAG>', help=_('Name of the tag.'))
1433def do_md_tag_show(gc, args):
1434    """Describe a specific metadata definitions tag inside a namespace."""
1435    tag = gc.metadefs_tag.get(args.namespace, args.tag)
1436    _tag_show(tag)
1437
1438
1439@utils.arg('namespace', metavar='<NAMESPACE>',
1440           help=_('Name of the namespace to which the tag belongs.'))
1441@utils.arg('tag', metavar='<TAG>', help=_('Name of the tag.'))
1442def do_md_tag_delete(gc, args):
1443    """Delete a specific metadata definitions tag inside a namespace."""
1444    gc.metadefs_tag.delete(args.namespace, args.tag)
1445
1446
1447@utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.'))
1448def do_md_namespace_tags_delete(gc, args):
1449    """Delete all metadata definitions tags inside a specific namespace."""
1450    gc.metadefs_tag.delete_all(args.namespace)
1451
1452
1453@utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.'))
1454def do_md_tag_list(gc, args):
1455    """List metadata definitions tags inside a specific namespace."""
1456    tags = gc.metadefs_tag.list(args.namespace)
1457    columns = ['name']
1458    column_settings = {
1459        "description": {
1460            "max_width": 50,
1461            "align": "l"
1462        }
1463    }
1464    utils.print_list(tags, columns, field_settings=column_settings)
1465
1466
1467@utils.arg('--sort-key', default='status',
1468           choices=tasks.SORT_KEY_VALUES,
1469           help=_('Sort task list by specified field.'))
1470@utils.arg('--sort-dir', default='desc',
1471           choices=tasks.SORT_DIR_VALUES,
1472           help=_('Sort task list in specified direction.'))
1473@utils.arg('--page-size', metavar='<SIZE>', default=None, type=int,
1474           help=_('Number of tasks to request in each paginated request.'))
1475@utils.arg('--type', metavar='<TYPE>',
1476           help=_('Filter tasks to those that have this type.'))
1477@utils.arg('--status', metavar='<STATUS>',
1478           help=_('Filter tasks to those that have this status.'))
1479def do_task_list(gc, args):
1480    """List tasks you can access."""
1481    filter_keys = ['type', 'status']
1482    filter_items = [(key, getattr(args, key)) for key in filter_keys]
1483    filters = dict([item for item in filter_items if item[1] is not None])
1484
1485    kwargs = {'filters': filters}
1486    if args.page_size is not None:
1487        kwargs['page_size'] = args.page_size
1488
1489    kwargs['sort_key'] = args.sort_key
1490    kwargs['sort_dir'] = args.sort_dir
1491
1492    tasks = gc.tasks.list(**kwargs)
1493
1494    columns = ['ID', 'Type', 'Status', 'Owner']
1495    utils.print_list(tasks, columns)
1496
1497
1498@utils.arg('id', metavar='<TASK_ID>', help=_('ID of task to describe.'))
1499def do_task_show(gc, args):
1500    """Describe a specific task."""
1501    task = gc.tasks.get(args.id)
1502    ignore = ['self', 'schema']
1503    task = dict([item for item in task.items() if item[0] not in ignore])
1504    utils.print_dict(task)
1505
1506
1507@utils.arg('--type', metavar='<TYPE>',
1508           help=_('Type of Task. Please refer to Glance schema or '
1509                  'documentation to see which tasks are supported.'))
1510@utils.arg('--input', metavar='<STRING>', default='{}',
1511           help=_('Parameters of the task to be launched'))
1512def do_task_create(gc, args):
1513    """Create a new task."""
1514    if not (args.type and args.input):
1515        utils.exit('Unable to create task. Specify task type and input.')
1516    else:
1517        try:
1518            input = json.loads(args.input)
1519        except ValueError:
1520            utils.exit('Failed to parse the "input" parameter. Must be a '
1521                       'valid JSON object.')
1522
1523        task_values = {'type': args.type, 'input': input}
1524        task = gc.tasks.create(**task_values)
1525        ignore = ['self', 'schema']
1526        task = dict([item for item in task.items()
1527                     if item[0] not in ignore])
1528        utils.print_dict(task)
1529