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