#!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2011 OpenStack, LLC # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ This is the administration program for heat. It is simply a command-line interface for adding, modifying, and retrieving information about the stacks belonging to a user. """ import functools import gettext import optparse import os import sys import time import json from urlparse import urlparse # If ../heat/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), os.pardir, os.pardir)) if os.path.exists(os.path.join(possible_topdir, 'heat', '__init__.py')): sys.path.insert(0, possible_topdir) gettext.install('heat', unicode=1) from heat import client as heat_client from heat.common import exception from heat import version SUCCESS = 0 FAILURE = 1 DEFAULT_PORT = 8000 def catch_error(action): """Decorator to provide sensible default error handling for actions.""" def wrap(func): @functools.wraps(func) def wrapper(*arguments, **kwargs): try: ret = func(*arguments, **kwargs) return SUCCESS if ret is None else ret except exception.NotAuthorized: print "Not authorized to make this request. Check "\ "your credentials (OS_AUTH_USER, OS_AUTH_KEY, ...)." return FAILURE except exception.ClientConfigurationError: raise except Exception, e: options = arguments[0] if options.debug: raise print "Failed to %s. Got error:" % action pieces = unicode(e).split('\n') for piece in pieces: print piece return FAILURE return wrapper return wrap @catch_error('validate') def template_validate(options, arguments): ''' ''' pass @catch_error('gettemplate') def get_template(options, arguments): ''' ''' pass @catch_error('create') def stack_create(options, arguments): ''' ''' parameters = {} try: parameters['StackName'] = arguments.pop(0) except IndexError: print "Please specify the stack name you wish to create " print "as the first argument" return FAILURE if options.parameters: for p in options.parameters.split(';'): (n, v) = p.split('=') parameters[n] = v if options.template_file: parameters['TemplateBody'] = open(options.template_file).read() elif options.template_url: parameters['TemplateUrl'] = options.template_url else: print 'Please specify a template file or url' return FAILURE c = get_client(options) result = c.create_stack(**parameters) print json.dumps(result, indent=2) @catch_error('update') def stack_update(options, arguments): ''' ''' parameters = {} try: parameters['StackName'] = arguments.pop(0) except IndexError: print "Please specify the stack name you wish to update " print "as the first argument" return FAILURE c = get_client(options) result = c.update_stack(parameters) print json.dumps(result, indent=2) @catch_error('delete') def stack_delete(options, arguments): ''' ''' parameters = {} try: parameters['StackName'] = arguments.pop(0) except IndexError: print "Please specify the stack name you wish to delete " print "as the first argument" return FAILURE c = get_client(options) result = c.delete_stack(parameters) print json.dumps(result, indent=2) @catch_error('describe') def stack_describe(options, arguments): ''' ''' parameters = {} try: parameters['StackName'] = arguments.pop(0) except IndexError: print "Describing all stacks" c = get_client(options) result = c.describe_stacks(parameters) print json.dumps(result, indent=2) @catch_error('list') def stack_list(options, arguments): ''' ''' c = get_client(options) result = c.list_stacks() print json.dumps(result, indent=2) def get_client(options): """ Returns a new client object to a heat server specified by the --host and --port options supplied to the CLI """ return heat_client.get_client(host=options.host, port=options.port, username=options.username, password=options.password, auth_url=options.auth_url, auth_strategy=options.auth_strategy, auth_token=options.auth_token, region=options.region, insecure=options.insecure) def create_options(parser): """ Sets up the CLI and config-file options that may be parsed and program commands. :param parser: The option parser """ parser.add_option('-v', '--verbose', default=False, action="store_true", help="Print more verbose output") parser.add_option('-d', '--debug', default=False, action="store_true", help="Print more verbose output") parser.add_option('-H', '--host', metavar="ADDRESS", default="0.0.0.0", help="Address of heat API host. " "Default: %default") parser.add_option('-p', '--port', dest="port", metavar="PORT", type=int, default=DEFAULT_PORT, help="Port the heat API host listens on. " "Default: %default") parser.add_option('-U', '--url', metavar="URL", default=None, help="URL of heat service. This option can be used " "to specify the hostname, port and protocol " "(http/https) of the heat server, for example " "-U https://localhost:" + str(DEFAULT_PORT) + "/v1 Default: None") parser.add_option('-k', '--insecure', dest="insecure", default=False, action="store_true", help="Explicitly allow heat to perform \"insecure\" " "SSL (https) requests. The server's certificate will " "not be verified against any certificate authorities. " "This option should be used with caution.") parser.add_option('-A', '--auth_token', dest="auth_token", metavar="TOKEN", default=None, help="Authentication token to use to identify the " "client to the heat server") parser.add_option('-I', '--username', dest="username", metavar="USER", default=None, help="User name used to acquire an authentication token") parser.add_option('-K', '--password', dest="password", metavar="PASSWORD", default=None, help="Password used to acquire an authentication token") parser.add_option('-R', '--region', dest="region", metavar="REGION", default=None, help="Region name. When using keystone authentication " "version 2.0 or later this identifies the region " "name to use when selecting the service endpoint. A " "region name must be provided if more than one " "region endpoint is available") parser.add_option('-N', '--auth_url', dest="auth_url", metavar="AUTH_URL", default=None, help="Authentication URL") parser.add_option('-S', '--auth_strategy', dest="auth_strategy", metavar="STRATEGY", default=None, help="Authentication strategy (keystone or noauth)") parser.add_option('-u', '--template-url', metavar="template_url", default=None, help="URL of template. Default: None") parser.add_option('-t', '--template-file', metavar="template_file", default=None, help="Path to the template. Default: None") parser.add_option('-P', '--parameters', metavar="parameters", default=None, help="Parameter values used to create the stack.") def parse_options(parser, cli_args): """ Returns the parsed CLI options, command to run and its arguments, merged with any same-named options found in a configuration file :param parser: The option parser """ if not cli_args: cli_args.append('-h') # Show options in usage output... (options, args) = parser.parse_args(cli_args) if options.url is not None: u = urlparse(options.url) options.port = u.port options.host = u.hostname options.use_ssl = (options.url is not None and u.scheme == 'https') # HACK(sirp): Make the parser available to the print_help method # print_help is a command, so it only accepts (options, args); we could # one-off have it take (parser, options, args), however, for now, I think # this little hack will suffice options.__parser = parser if not args: parser.print_usage() sys.exit(0) command_name = args.pop(0) command = lookup_command(parser, command_name) return (options, command, args) def print_help(options, args): """ Print help specific to a command """ if len(args) != 1: sys.exit("Please specify a command") parser = options.__parser command_name = args.pop() command = lookup_command(parser, command_name) print command.__doc__ % {'prog': os.path.basename(sys.argv[0])} def lookup_command(parser, command_name): base_commands = {'help': print_help} image_commands = { 'create': stack_create, 'update': stack_update, 'delete': stack_delete, 'list': stack_list, 'validate': template_validate, 'gettemplate': get_template, 'describe': stack_describe} commands = {} for command_set in (base_commands, image_commands): commands.update(command_set) try: command = commands[command_name] except KeyError: parser.print_usage() sys.exit("Unknown command: %s" % command_name) return command def main(): ''' ''' usage = """ %prog [options] [args] Commands: help Output help for one of the commands below create Create the stack delete Delete the stack describe Describe the stack update Update the stack list List the user's stacks gettemplate Get the template validate Validate a template """ oparser = optparse.OptionParser(version='%%prog %s' % version.version_string(), usage=usage.strip()) create_options(oparser) (opts, cmd, args) = parse_options(oparser, sys.argv[1:]) try: start_time = time.time() result = cmd(opts, args) end_time = time.time() if opts.verbose: print "Completed in %-0.4f sec." % (end_time - start_time) sys.exit(result) except (RuntimeError, NotImplementedError, exception.ClientConfigurationError), ex: oparser.print_usage() print >> sys.stderr, "ERROR: ", ex sys.exit(1) if __name__ == '__main__': main()