# Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import datetime import inspect import json import os.path import sys from saharaclient.openstack.common.apiclient import exceptions from saharaclient.openstack.common import cliutils as utils def _print_list_field(field): return lambda obj: ', '.join(getattr(obj, field)) def _filter_call_args(args, func, remap={}): """Filter args according to func's parameter list. Take three arguments: * args - a dictionary * func - a function * remap - a dictionary Remove from dct all the keys which are not among the parameters of func. Before filtering, remap the keys in the args dict according to remap dict. """ for name, new_name in remap.items(): if name in args: args[new_name] = args[name] del args[name] valid_args = inspect.getargspec(func).args for name in args.keys(): if name not in valid_args: print('WARNING: "%s" is not a valid parameter and will be ' 'discarded from the request' % name) del args[name] def _print_node_group_field(cluster): return ', '.join(map(lambda x: ': '.join(x), [(node_group['name'], str(node_group['count'])) for node_group in cluster.node_groups])) def _show_node_group_template(template): template._info['node_processes'] = ( ', '.join(template._info['node_processes']) ) utils.print_dict(template._info) def _show_cluster_template(template): template._info['node_groups'] = _print_node_group_field(template) utils.print_dict(template._info) def _show_cluster(cluster): # TODO(mattf): Make this pretty, e.g format node_groups and info urls # Forcing wrap=47 allows for clean display on a terminal of width 80 utils.print_dict(cluster._info, wrap=47) def _show_job_binary_data(data): columns = ('id', 'name') utils.print_list(data, columns) def _show_data_source(source): # TODO(mattf): why are we passing credentials around like this? if 'credentials' in source._info: del source._info['credentials'] utils.print_dict(source._info) def _show_job_binary(binary): # TODO(mattf): why are we passing credentials around like this? if 'extra' in binary._info: del binary._info['extra'] utils.print_dict(binary._info) def _show_job_template(template): # TODO(mattf): Make "mains" property pretty # TODO(mattf): handle/remove "extra" creds utils.print_dict(template._info) def _show_job(job): # TODO(mattf): make display of info pretty, until then # extract the important status information job._info['status'] = job._info['info']['status'] del job._info['info'] utils.print_dict(job._info) def _get_by_id_or_name(manager, id=None, name=None, **kwargs): if not (name or id): raise exceptions.CommandError("either NAME or ID is required") if id: return manager.get(id, **kwargs) ls = manager.find(name=name) if len(ls) == 0: raise exceptions.CommandError("%s '%s' not found" % (manager.resource_class.resource_name, name)) elif len(ls) > 1: raise exceptions.CommandError("%s '%s' not unique, try by ID" % (manager.resource_class.resource_name, name)) return manager.get(ls[0].id, **kwargs) # # Plugins # ~~~~~~~ # plugin-list # # plugin-show --name [--version ] # def do_plugin_list(cs, args): """Print a list of available plugins.""" plugins = cs.plugins.list() columns = ('name', 'versions', 'title') utils.print_list(plugins, columns, {'versions': _print_list_field('versions')}) @utils.arg('--name', metavar='', required=True, help='Name of the plugin.') # TODO(mattf) - saharaclient does not support query w/ version # @utils.arg('--version', # metavar='', # help='Optional version') def do_plugin_show(cs, args): """Show details of a plugin.""" plugin = cs.plugins.get(args.name) plugin._info['versions'] = ', '.join(plugin._info['versions']) utils.print_dict(plugin._info) # # Image Registry # ~~~~~~~~~~~~~~ # image-list [--tag ]* # # image-show --name |--id # # image-register --name |--id # [--username ] [--description ] # # image-unregister --name |--id # # image-add-tag --name |--id --tag + # # image-remove-tag --name |--id --tag + # # TODO(mattf): [--tag ]* def do_image_list(cs, args): """Print a list of available images.""" images = cs.images.list() columns = ('name', 'id', 'username', 'tags', 'description') utils.print_list(images, columns, {'tags': _print_list_field('tags')}) @utils.arg('--name', help='Name of the image.') @utils.arg('--id', metavar='', help='ID of the image.') def do_image_show(cs, args): """Show details of an image.""" image = _get_by_id_or_name(cs.images, args.id, args.name) del image._info['metadata'] image._info['tags'] = ', '.join(image._info['tags']) utils.print_dict(image._info) # TODO(mattf): Add --name, must lookup in glance index @utils.arg('--id', metavar='', required=True, help='ID of image, run "glance image-list" to see all IDs.') @utils.arg('--username', default='root', metavar='', help='Username of privileged user in the image.') @utils.arg('--description', default='', metavar='', help='Description of the image.') def do_image_register(cs, args): """Register an image from the Image index.""" # TODO(mattf): image register should not be through update cs.images.update_image(args.id, args.username, args.description) # TODO(mattf): No indication of result, expect image details @utils.arg('--name', help='Name of the image.') @utils.arg('--id', metavar='', help='ID of image to unregister.') def do_image_unregister(cs, args): """Unregister an image.""" cs.images.unregister_image( args.id or _get_by_id_or_name(cs.images, name=args.name).id ) # TODO(mattf): No indication of result, expect result to display @utils.arg('--name', help='Name of the image.') @utils.arg('--id', metavar='', help='ID of image to tag.') # TODO(mattf): Change --tag to --tag+ @utils.arg('--tag', metavar='', required=True, help='Tag to add.') def do_image_add_tag(cs, args): """Add a tag to an image.""" # TODO(mattf): Need proper add_tag API call id = args.id or _get_by_id_or_name(cs.images, name=args.name).id cs.images.update_tags(id, cs.images.get(id).tags + [args.tag, ]) # TODO(mattf): No indication of result, expect image details @utils.arg('--name', help='Name of the image.') @utils.arg('--id', metavar='', help='Image to tag.') # TODO(mattf): Change --tag to --tag+ @utils.arg('--tag', metavar='', required=True, help='Tag to remove.') def do_image_remove_tag(cs, args): """Remove a tag from an image.""" # TODO(mattf): Need proper remove_tag API call id = args.id or _get_by_id_or_name(cs.images, name=args.name).id cs.images.update_tags(id, filter(lambda x: x != args.tag, cs.images.get(id).tags)) # TODO(mattf): No indication of result, expect image details # # Clusters # ~~~~~~~~ # cluster-list # # cluster-show --name |--id [--json] [--show-progress] # # cluster-create [--json ] # # cluster-scale --name |--id [--json ] # # cluster-delete --name |--id # def do_cluster_list(cs, args): """Print a list of available clusters.""" clusters = cs.clusters.list() for cluster in clusters: cluster.node_count = sum(map(lambda g: g['count'], cluster.node_groups)) columns = ('name', 'id', 'status', 'node_count') utils.print_list(clusters, columns) @utils.arg('--name', help='Name of the cluster.') @utils.arg('--id', metavar='', help='ID of the cluster to show.') @utils.arg('--show-progress', help='Show provision progress events of the cluster.') @utils.arg('--json', action='store_true', default=False, help='Print JSON representation of the cluster.') def do_cluster_show(cs, args): """Show details of a cluster.""" cluster = _get_by_id_or_name(cs.clusters, args.id, args.name, show_progress=args.show_progress) if args.json: print(json.dumps(cluster._info)) else: _show_cluster(cluster) @utils.arg('--json', default=sys.stdin, type=argparse.FileType('r'), help='JSON representation of cluster.') @utils.arg('--count', default=1, type=int, help='Number of clusters to create.') def do_cluster_create(cs, args): """Create a cluster.""" # TODO(mattf): improve template validation, e.g. template w/o name key template = json.loads(args.json.read()) # The neutron_management_network parameter to clusters.create is # called net_id. Therefore, we must translate before invoking # create w/ **template. It may be desirable to simple change # clusters.create in the future. remap = {'neutron_management_network': 'net_id'} template['count'] = args.count _filter_call_args(template, cs.clusters.create, remap) _show_cluster(cs.clusters.create(**template)) @utils.arg('--name', help='Name of the cluster.') @utils.arg('--id', metavar='', help='ID of the cluster.') @utils.arg('--json', default=sys.stdin, type=argparse.FileType('r'), help='JSON representation of cluster scale.') def do_cluster_scale(cs, args): """Scale a cluster.""" cluster_id = args.id or _get_by_id_or_name(cs.clusters, name=args.name).id scale_template = json.loads(args.json.read()) _show_cluster(cs.clusters.scale(cluster_id, **scale_template)) @utils.arg('--name', help='Name of the cluster.') @utils.arg('--id', metavar='', help='ID of the cluster to delete.') def do_cluster_delete(cs, args): """Delete a cluster.""" cs.clusters.delete( args.id or _get_by_id_or_name(cs.clusters, name=args.name).id ) # TODO(mattf): No indication of result # # Node Group Templates # ~~~~~~~~~~~~~~~~~~~~ # node-group-template-list # # node-group-template-show --name