]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
heat cli : Rework to separate cli tool from client-API wrappers
authorSteven Hardy <shardy@redhat.com>
Wed, 15 Aug 2012 13:09:54 +0000 (14:09 +0100)
committerSteven Hardy <shardy@redhat.com>
Thu, 16 Aug 2012 11:23:50 +0000 (12:23 +0100)
Rework to remove duplication between heat and heat-boto, and to
provide better separation between the CLI tool logic and the
underlying client API (should allow easier porting to new ReST API)

Ref #175 (partially fixes)
Fixes #192

Change-Id: Ib1f821667c40c78770a345204af923163daeffae
Signed-off-by: Steven Hardy <shardy@redhat.com>
bin/heat
bin/heat-boto [changed from file to symlink]
heat/boto_client.py [new file with mode: 0644]
heat/client.py

index 3fb0ec83f788e3d13df26e8838734b791ac5b9df..fbfb305130406ea617c173e45f324faa75be6315 100755 (executable)
--- a/bin/heat
+++ b/bin/heat
@@ -42,23 +42,16 @@ scriptname = os.path.basename(sys.argv[0])
 
 gettext.install('heat', unicode=1)
 
-from heat import client as heat_client
+if scriptname == 'heat-boto':
+    from heat import  boto_client as heat_client
+else:
+    from heat import client as heat_client
 from heat import version
 from heat.common import config
 from heat.common import exception
 from heat import utils
 
 
-def format_parameters(options):
-    parameters = {}
-    if options.parameters:
-        for count, p in enumerate(options.parameters.split(';'), 1):
-            (n, v) = p.split('=')
-            parameters['Parameters.member.%d.ParameterKey' % count] = n
-            parameters['Parameters.member.%d.ParameterValue' % count] = v
-    return parameters
-
-
 @utils.catch_error('validate')
 def template_validate(options, arguments):
     '''
@@ -81,11 +74,10 @@ def template_validate(options, arguments):
         logging.error('Please specify a template file or url')
         return utils.FAILURE
 
-    parameters.update(format_parameters(options))
-
-    client = get_client(options)
-    result = client.validate_template(**parameters)
-    print result
+    c = get_client(options)
+    parameters.update(c.format_parameters(options))
+    result = c.validate_template(**parameters)
+    print c.format_template(result)
 
 
 @utils.catch_error('estimatetemplatecost')
@@ -98,8 +90,6 @@ def estimate_template_cost(options, arguments):
         logging.error("as the first argument")
         return utils.FAILURE
 
-    parameters.update(format_parameters(options))
-
     if options.template_file:
         parameters['TemplateBody'] = open(options.template_file).read()
     elif options.template_url:
@@ -109,6 +99,7 @@ def estimate_template_cost(options, arguments):
         return utils.FAILURE
 
     c = get_client(options)
+    parameters.update(c.format_parameters(options))
     result = c.estimate_template_cost(**parameters)
     print result
 
@@ -156,8 +147,6 @@ def stack_create(options, arguments):
         logging.error("as the first argument")
         return utils.FAILURE
 
-    parameters.update(format_parameters(options))
-
     parameters['TimeoutInMinutes'] = options.timeout
 
     if options.template_file:
@@ -169,6 +158,7 @@ def stack_create(options, arguments):
         return utils.FAILURE
 
     c = get_client(options)
+    parameters.update(c.format_parameters(options))
     result = c.create_stack(**parameters)
     print result
 
@@ -206,9 +196,8 @@ def stack_update(options, arguments):
     elif options.template_url:
         parameters['TemplateUrl'] = options.template_url
 
-    parameters.update(format_parameters(options))
-
     c = get_client(options)
+    parameters.update(c.format_parameters(options))
     result = c.update_stack(**parameters)
     print result
 
@@ -250,7 +239,7 @@ def stack_describe(options, arguments):
 
     c = get_client(options)
     result = c.describe_stacks(**parameters)
-    print result
+    print c.format_stack(result)
 
 
 @utils.catch_error('event-list')
@@ -268,7 +257,7 @@ def stack_events_list(options, arguments):
 
     c = get_client(options)
     result = c.list_stack_events(**parameters)
-    print result
+    print c.format_stack_event(result)
 
 
 @utils.catch_error('resource')
@@ -288,7 +277,7 @@ def stack_resource_show(options, arguments):
         'LogicalResourceId': resource_name,
     }
     result = c.describe_stack_resource(**parameters)
-    print result
+    print c.format_stack_resource(result)
 
 
 @utils.catch_error('resource-list')
@@ -307,35 +296,7 @@ def stack_resources_list(options, arguments):
         'StackName': stack_name,
     }
     result = c.list_stack_resources(**parameters)
-    print result
-
-
-def _resources_list_details(options, lookup_key='StackName',
-                            lookup_value=None, log_resid=None):
-    '''
-    Helper function to reduce duplication in stack_resources_list_details
-    Looks up resource details based on StackName or PhysicalResourceId
-    '''
-    c = get_client(options)
-    parameters = {}
-
-    if lookup_key in ['StackName', 'PhysicalResourceId']:
-        parameters[lookup_key] = lookup_value
-    else:
-        logging.error("Unexpected key %s" % lookup_key)
-        return
-
-    if log_resid:
-        parameters['LogicalResourceId'] = log_resid
-
-    try:
-        result = c.describe_stack_resources(**parameters)
-    except:
-        logging.debug("Failed to lookup resource details with key %s:%s" %
-            (lookup_key, lookup_value))
-        return
-
-    return result
+    print c.format_stack_resource_summary(result)
 
 
 @utils.catch_error('resource-list-details')
@@ -357,26 +318,23 @@ def stack_resources_list_details(options, arguments):
     try:
         name_or_pid = arguments.pop(0)
     except IndexError:
-        logging.error("No valid stack_name or physical_resource_id")
+        logging.error("Must pass a stack_name or physical_resource_id")
         print usage
         return
 
     logical_resource_id = arguments.pop(0) if arguments else None
+    parameters = {
+        'NameOrPid': name_or_pid,
+        'LogicalResourceId': logical_resource_id, }
 
-    # Try StackName first as it seems the most likely..
-    lookup_keys = ['StackName', 'PhysicalResourceId']
-    for key in lookup_keys:
-        logging.debug("Looking up resources for %s:%s" % (key, name_or_pid))
-        result = _resources_list_details(options, lookup_key=key,
-                                         lookup_value=name_or_pid,
-                                         log_resid=logical_resource_id)
-        if result:
-            break
+    c = get_client(options)
+    result = c.describe_stack_resources(**parameters)
 
     if result:
-        print result
+        print c.format_stack_resource(result)
     else:
-        logging.error("No valid stack_name or physical_resource_id")
+        logging.error("Invalid stack_name, physical_resource_id " +
+                      "or logical_resource_id")
         print usage
 
 
@@ -389,7 +347,7 @@ def stack_list(options, arguments):
     '''
     c = get_client(options)
     result = c.list_stacks()
-    print result
+    print c.format_stack_summary(result)
 
 
 def get_client(options):
@@ -640,7 +598,7 @@ Commands:
             NotImplementedError,
             exception.ClientConfigurationError), ex:
         oparser.print_usage()
-        logging.error("ERROR: " % ex)
+        logging.error("ERROR: %s" % ex)
         sys.exit(1)
 
 
deleted file mode 100755 (executable)
index 91f15188de057bcfca24529170b955f075111848..0000000000000000000000000000000000000000
+++ /dev/null
@@ -1,731 +0,0 @@
-#!/usr/bin/env python
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-#
-#    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.  It is a convenience application that talks to the heat
-API server.
-"""
-
-import gettext
-import optparse
-import os
-import os.path
-import sys
-import time
-import json
-import logging
-
-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)
-
-scriptname = os.path.basename(sys.argv[0])
-
-gettext.install('heat', unicode=1)
-
-import boto
-from heat import version
-from heat.common import config
-from heat.common import exception
-from heat import utils
-
-
-# FIXME : would be better if each of the boto response classes
-# implemented __str__ or a print method, so we could just print
-# them, avoiding all these print functions, boto patch TODO
-
-
-def print_stack_event(event):
-    '''
-    Print contents of a boto.cloudformation.stack.StackEvent object
-    '''
-    print "EventId : %s" % event.event_id
-    print "LogicalResourceId : %s" % event.logical_resource_id
-    print "PhysicalResourceId : %s" % event.physical_resource_id
-    print "ResourceProperties : %s" % event.resource_properties
-    print "ResourceStatus : %s" % event.resource_status
-    print "ResourceStatusReason : %s" % event.resource_status_reason
-    print "ResourceType : %s" % event.resource_type
-    print "StackId : %s" % event.stack_id
-    print "StackName : %s" % event.stack_name
-    print "Timestamp : %s" % event.timestamp
-    print "--"
-
-
-def print_stack(s):
-    '''
-    Print contents of a boto.cloudformation.stack.Stack object
-    '''
-    print "Capabilities : %s" % s.capabilities
-    print "CreationTime : %s" % s.creation_time
-    print "Description : %s" % s.description
-    print "DisableRollback : %s" % s.disable_rollback
-    # FIXME : boto doesn't populate this field, but AWS defines it.
-    # bit unclear because the docs say LastUpdatedTime, where all
-    # other response structures define LastUpdatedTimestamp
-    # need confirmation of real AWS format, probably a documentation bug..
-#    print "LastUpdatedTime : %s" % s.last_updated_time
-    print "NotificationARNs : %s" % s.notification_arns
-    print "Outputs : %s" % s.outputs
-    print "Parameters : %s" % s.parameters
-    print "StackId : %s" % s.stack_id
-    print "StackName : %s" % s.stack_name
-    print "StackStatus : %s" % s.stack_status
-    print "StackStatusReason : %s" % s.stack_status_reason
-    print "TimeoutInMinutes : %s" % s.timeout_in_minutes
-    print "--"
-
-
-def print_stack_resource(res):
-    '''
-    Print contents of a boto.cloudformation.stack.StackResource object
-    '''
-    print "LogicalResourceId : %s" % res.logical_resource_id
-    print "PhysicalResourceId : %s" % res.physical_resource_id
-    print "ResourceStatus : %s" % res.resource_status
-    print "ResourceStatusReason : %s" % res.resource_status_reason
-    print "ResourceType : %s" % res.resource_type
-    print "StackId : %s" % res.stack_id
-    print "StackName : %s" % res.stack_name
-    print "Timestamp : %s" % res.timestamp
-    print "--"
-
-
-def print_stack_resource_summary(res):
-    '''
-    Print contents of a boto.cloudformation.stack.StackResourceSummary object
-    '''
-    print "LastUpdatedTimestamp : %s" % res.last_updated_timestamp
-    print "LogicalResourceId : %s" % res.logical_resource_id
-    print "PhysicalResourceId : %s" % res.physical_resource_id
-    print "ResourceStatus : %s" % res.resource_status
-    print "ResourceStatusReason : %s" % res.resource_status_reason
-    print "ResourceType : %s" % res.resource_type
-    print "--"
-
-
-def print_stack_summary(s):
-    '''
-    Print contents of a boto.cloudformation.stack.StackSummary object
-    '''
-    print "StackId : %s" % s.stack_id
-    print "StackName : %s" % s.stack_name
-    print "CreationTime : %s" % s.creation_time
-    print "StackStatus : %s" % s.stack_status
-    print "TemplateDescription : %s" % s.template_description
-    print "--"
-
-
-@utils.catch_error('validate')
-def template_validate(options, arguments):
-    '''
-    Validate a template.  This command parses a template and verifies that
-    it is in the correct format.
-
-    Usage: heat validate \\
-        [--template-file=<template file>|--template-url=<template URL>] \\
-        [options]
-
-    --template-file: Specify a local template file.
-    --template-url:  Specify a URL pointing to a stack description template.
-    '''
-    parameters = {}
-    if options.template_file:
-        parameters['TemplateBody'] = open(options.template_file).read()
-    elif options.template_url:
-        parameters['TemplateUrl'] = options.template_url
-    else:
-        logging.error('Please specify a template file or url')
-        return utils.FAILURE
-
-    if options.parameters:
-        count = 1
-        for p in options.parameters.split(';'):
-            (n, v) = p.split('=')
-            parameters['Parameters.member.%d.ParameterKey' % count] = n
-            parameters['Parameters.member.%d.ParameterValue' % count] = v
-            count = count + 1
-
-    client = get_client(options)
-    result = client.validate_template(**parameters)
-    print result
-
-
-@utils.catch_error('estimatetemplatecost')
-def estimate_template_cost(options, arguments):
-    parameters = {}
-    try:
-        parameters['StackName'] = arguments.pop(0)
-    except IndexError:
-        logging.error("Please specify the stack name you wish to estimate")
-        logging.error("as the first argument")
-        return utils.FAILURE
-
-    if options.parameters:
-        count = 1
-        for p in options.parameters.split(';'):
-            (n, v) = p.split('=')
-            parameters['Parameters.member.%d.ParameterKey' % count] = n
-            parameters['Parameters.member.%d.ParameterValue' % count] = v
-            count = count + 1
-
-    if options.template_file:
-        parameters['TemplateBody'] = open(options.template_file).read()
-    elif options.template_url:
-        parameters['TemplateUrl'] = options.template_url
-    else:
-        logging.error('Please specify a template file or url')
-        return utils.FAILURE
-
-    c = get_client(options)
-    result = c.estimate_template_cost(**parameters)
-    print result
-
-
-@utils.catch_error('gettemplate')
-def get_template(options, arguments):
-    '''
-    Gets an existing stack template.
-    '''
-    if len(arguments):
-        stack_name = arguments.pop(0)
-    else:
-        logging.error("Please specify the stack you wish to get")
-        logging.error("as the first argument")
-        return utils.FAILURE
-
-    c = get_client(options)
-    result = c.get_template(stack_name)
-    print json.dumps(result)
-
-
-@utils.catch_error('create')
-def stack_create(options, arguments):
-    '''
-    Create a new stack from a template.
-
-    Usage: heat create <stack name> \\
-        [--template-file=<template file>|--template-url=<template URL>] \\
-        [options]
-
-    Stack Name: The user specified name of the stack you wish to create.
-
-    --template-file: Specify a local template file containing a valid
-                     stack description template.
-    --template-url:  Specify a URL pointing to a valid stack description
-                     template.
-    '''
-
-    if len(arguments):
-        stack_name = arguments.pop(0)
-    else:
-        logging.error("Please specify the stack name you wish to create")
-        logging.error("as the first argument")
-        return utils.FAILURE
-
-    params = []
-    if options.parameters:
-        for p in options.parameters.split(';'):
-            (n, v) = p.split('=')
-            # Boto expects parameters as a list-of-tuple
-            params.append((n, v))
-
-    result = None
-    c = get_client(options)
-    if options.template_file:
-        t = open(options.template_file).read()
-        result = c.create_stack(stack_name, template_body=t, parameters=params)
-    elif options.template_url:
-        result = c.create_stack(stack_name, template_url=options.template_url)
-    else:
-        logging.error('Please specify a template file or url')
-        return utils.FAILURE
-
-    print result
-
-
-@utils.catch_error('update')
-def stack_update(options, arguments):
-    '''
-    Update an existing stack.
-
-    Usage: heat update <stack name> \\
-        [--template-file=<template file>|--template-url=<template URL>] \\
-        [options]
-
-    Stack Name: The name of the stack you wish to modify.
-
-    --template-file: Specify a local template file containing a valid
-                     stack description template.
-    --template-url:  Specify a URL pointing to a valid stack description
-                     template.
-
-    Options:
-        --parameters: A list of key/value pairs separated by ';'s used
-                      to specify allowed values in the template file.
-    '''
-    parameters = {}
-    if len(arguments):
-        stack_name = arguments.pop(0)
-    else:
-        logging.error("Please specify the stack name you wish to update")
-        logging.error("as the first argument")
-        return utils.FAILURE
-
-    result = None
-    c = get_client(options)
-    if options.template_file:
-        t = open(options.template_file).read()
-        result = c.update_stack(stack_name, template_body=t)
-    elif options.template_url:
-        t = options.template_url
-        result = c.update_stack(stack_name, template_url=t)
-
-    if options.parameters:
-        count = 1
-        for p in options.parameters.split(';'):
-            (n, v) = p.split('=')
-            parameters['Parameters.member.%d.ParameterKey' % count] = n
-            parameters['Parameters.member.%d.ParameterValue' % count] = v
-            count = count + 1
-
-    print result
-
-
-@utils.catch_error('delete')
-def stack_delete(options, arguments):
-    '''
-    Delete an existing stack.  This shuts down all VMs associated with
-    the stack and (perhaps wrongly) also removes all events associated
-    with the given stack.
-
-    Usage: heat delete <stack name>
-    '''
-    if len(arguments):
-        stack_name = arguments.pop(0)
-    else:
-        logging.error("Please specify the stack name you wish to delete")
-        logging.error("as the first argument")
-        return utils.FAILURE
-
-    c = get_client(options)
-    result = c.delete_stack(stack_name)
-    print result
-
-
-@utils.catch_error('describe')
-def stack_describe(options, arguments):
-    '''
-    Describes an existing stack.
-
-    Usage: heat describe <stack name>
-    '''
-    if len(arguments):
-        stack_name = arguments.pop(0)
-    else:
-        logging.info("No stack name passed, getting results for ALL stacks")
-        stack_name = None
-
-    c = get_client(options)
-    result = c.describe_stacks(stack_name)
-    for s in result:
-        print_stack(s)
-
-
-@utils.catch_error('event-list')
-def stack_events_list(options, arguments):
-    '''
-    List events associated with the given stack.
-
-    Usage: heat event-list <stack name>
-    '''
-    if len(arguments):
-        stack_name = arguments.pop(0)
-    else:
-        logging.info("No stack name passed, getting events for ALL stacks")
-        stack_name = None
-
-    c = get_client(options)
-    result = c.describe_stack_events(stack_name)
-    for e in result:
-        print_stack_event(e)
-
-
-@utils.catch_error('resource')
-def stack_resource_show(options, arguments):
-    '''
-    Display details of the specified resource.
-    '''
-    c = get_client(options)
-    try:
-        stack_name, resource_name = arguments
-    except ValueError:
-        print 'Enter stack name and logical resource id'
-        return
-
-    result = c.describe_stack_resources(stack_name, resource_name)
-    for r in result:
-        print_stack_resource(r)
-
-
-@utils.catch_error('resource-list')
-def stack_resources_list(options, arguments):
-    '''
-    Display summary of all resources in the specified stack.
-    '''
-    c = get_client(options)
-    try:
-        stack_name = arguments.pop(0)
-    except IndexError:
-        print 'Enter stack name'
-        return
-
-    result = c.list_stack_resources(stack_name)
-    for r in result:
-        print_stack_resource_summary(r)
-
-
-@utils.catch_error('resource-list-details')
-def stack_resources_list_details(options, arguments):
-    '''
-    Display details of resources in the specified stack.
-
-    - If stack name is specified, all associated resources are returned
-    - If physical resource ID is specified, all associated resources of the
-    stack the resource belongs to are returned
-    - You must specify stack name *or* physical resource ID
-    - You may optionally specify a Logical resource ID to filter the result
-    '''
-    usage = ('''Usage:
-%s resource-list-details stack_name [logical_resource_id]
-%s resource-list-details physical_resource_id [logical_resource_id]''' %
-        (scriptname, scriptname))
-
-    try:
-        name_or_pid = arguments.pop(0)
-    except IndexError:
-        logging.error("Must pass a stack_name or physical_resource_id")
-        print usage
-        return
-
-    logical_resource_id = arguments.pop(0) if arguments else None
-
-    c = get_client(options)
-
-    # Check if this is a StackName, if not assume it's a physical res ID
-    # Note this is slower (for the common case, which is probably StackName)
-    # than just doing a try/catch over the StackName case then retrying
-    # on failure with name_or_pid as the physical resource ID, however
-    # boto spews errors when raising an exception so we can't do that
-    list_stacks = c.list_stacks()
-    stack_names = [s.stack_name for s in list_stacks]
-    if name_or_pid in stack_names:
-        logging.debug("Looking up resources for StackName:%s" % name_or_pid)
-        result = c.describe_stack_resources(stack_name_or_id=name_or_pid,
-            logical_resource_id=logical_resource_id)
-    else:
-        logging.debug("Looking up resources for PhysicalResourceId:%s" %
-            name_or_pid)
-        result = c.describe_stack_resources(stack_name_or_id=None,
-            logical_resource_id=logical_resource_id,
-            physical_resource_id=name_or_pid)
-
-    if result:
-        for r in result:
-            print_stack_resource(r)
-    else:
-        logging.error("Invalid stack_name, physical_resource_id " +
-                      "or logical_resource_id")
-        print usage
-
-
-@utils.catch_error('list')
-def stack_list(options, arguments):
-    '''
-    List all running stacks.
-
-    Usage: heat list
-    '''
-    c = get_client(options)
-    result = c.list_stacks()
-    for s in result:
-        print_stack_summary(s)
-
-
-def get_client(options):
-    """
-    Returns a new boto client object to a heat server
-    """
-
-    # FIXME : reopen boto pull-request so that --host can override the
-    # host specified in the hostname by passing it via the constructor
-    # I implemented this previously, but they preferred the config-file
-    # solution..
-    # Note we pass None/None for the keys so boto reads /etc/boto.cfg
-    cloudformation = boto.connect_cloudformation(aws_access_key_id=None,
-        aws_secret_access_key=None, debug=options.debug, is_secure=False,
-        port=options.port, path="/v1")
-    if cloudformation:
-        logging.debug("Got CF connection object OK")
-    else:
-        logging.error("Error establishing connection!")
-        sys.exit(1)
-
-    return cloudformation
-
-
-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('-y', '--yes', default=False, action="store_true",
-                      help="Don't prompt for user input; assume the answer to "
-                           "every question is 'yes'.")
-    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=config.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(config.DEFAULT_PORT) +
-                           "/v1 Default: No<F3>ne")
-    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('-T', '--tenant', dest="tenant",
-                      metavar="TENANT", default=None,
-                      help="Tenant name used for Keystone authentication")
-    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('-f', '--template-file', metavar="template_file",
-                      default=None, help="Path to the template. Default: None")
-    parser.add_option('-t', '--timeout', metavar="timeout",
-                      default='60',
-                      help='Stack creation timeout in minutes. Default: 60')
-
-    parser.add_option('-P', '--parameters', metavar="parameters", default=None,
-                      help="Parameter values used to create the stack.")
-
-
-def credentials_from_env():
-    return dict(username=os.getenv('OS_USERNAME'),
-                password=os.getenv('OS_PASSWORD'),
-                tenant=os.getenv('OS_TENANT_NAME'),
-                auth_url=os.getenv('OS_AUTH_URL'),
-                auth_strategy=os.getenv('OS_AUTH_STRATEGY'))
-
-
-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)
-    env_opts = credentials_from_env()
-    for option, env_val in env_opts.items():
-        if not getattr(options, option):
-            setattr(options, option, env_val)
-
-    if options.url is not None:
-        u = urlparse(options.url)
-        options.port = u.port
-        options.host = u.hostname
-
-    if not options.auth_strategy:
-        options.auth_strategy = 'noauth'
-
-    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)
-
-    if options.debug:
-        logging.basicConfig(format='%(levelname)s:%(message)s',
-            level=logging.DEBUG)
-        logging.debug("Debug level logging enabled")
-    elif options.verbose:
-        logging.basicConfig(format='%(levelname)s:%(message)s',
-            level=logging.INFO)
-    else:
-        logging.basicConfig(format='%(levelname)s:%(message)s',
-            level=logging.WARNING)
-
-    return (options, command, args)
-
-
-def print_help(options, args):
-    """
-    Print help specific to a command
-    """
-    parser = options.__parser
-
-    if not args:
-        parser.print_usage()
-
-    subst = {'prog': os.path.basename(sys.argv[0])}
-    docs = [lookup_command(parser, cmd).__doc__ % subst for cmd in args]
-    print '\n\n'.join(docs)
-
-
-def lookup_command(parser, command_name):
-    base_commands = {'help': print_help}
-
-    stack_commands = {
-                'create': stack_create,
-                'update': stack_update,
-                'delete': stack_delete,
-                'list': stack_list,
-                'event-list': stack_events_list,
-                'resource': stack_resource_show,
-                'resource-list': stack_resources_list,
-                'resource-list-details': stack_resources_list_details,
-                'validate': template_validate,
-                'gettemplate': get_template,
-                'estimate-template-cost': estimate_template_cost,
-                'describe': stack_describe}
-
-    commands = {}
-    for command_set in (base_commands, stack_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 <command> [options] [args]
-
-Commands:
-
-    help <command>  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
-
-    estimate-template-cost     Returns the estimated monthly cost of a template
-
-    validate        Validate a template
-
-    event-list      List events for a stack
-
-    resource        Describe the resource
-
-    resource-list   Show list of resources belonging to a stack
-
-    resource-list-details    Detailed view of resources belonging to a stack
-
-"""
-
-    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()
-        logging.debug("Completed in %-0.4f sec." % (end_time - start_time))
-        sys.exit(result)
-    except (RuntimeError,
-            NotImplementedError,
-            exception.ClientConfigurationError), ex:
-        oparser.print_usage()
-        logging.error("ERROR: " % ex)
-        sys.exit(1)
-
-
-if __name__ == '__main__':
-    main()
new file mode 120000 (symlink)
index 0000000000000000000000000000000000000000..238b5a934e380367360bfbd32345b7548e6de9d5
--- /dev/null
@@ -0,0 +1 @@
+heat
\ No newline at end of file
diff --git a/heat/boto_client.py b/heat/boto_client.py
new file mode 100644 (file)
index 0000000..b296371
--- /dev/null
@@ -0,0 +1,273 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#
+#    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.
+
+"""
+Client implementation based on the boto AWS client library
+"""
+
+from heat.openstack.common import log as logging
+logger = logging.getLogger(__name__)
+
+from boto.cloudformation import CloudFormationConnection
+
+
+class BotoClient(CloudFormationConnection):
+
+    def list_stacks(self, **kwargs):
+        return super(BotoClient, self).list_stacks()
+
+    def describe_stacks(self, **kwargs):
+        try:
+            stack_name = kwargs['StackName']
+        except KeyError:
+            stack_name = None
+        return super(BotoClient, self).describe_stacks(stack_name)
+
+    def create_stack(self, **kwargs):
+        if 'TemplateUrl' in kwargs:
+            return super(BotoClient, self).create_stack(kwargs['StackName'],
+                                     template_url=kwargs['TemplateUrl'],
+                                     parameters=kwargs['Parameters'])
+        elif 'TemplateBody' in kwargs:
+            return super(BotoClient, self).create_stack(kwargs['StackName'],
+                                     template_body=kwargs['TemplateBody'],
+                                     parameters=kwargs['Parameters'])
+        else:
+            logger.error("Must specify TemplateUrl or TemplateBody!")
+
+    def update_stack(self, **kwargs):
+        if 'TemplateUrl' in kwargs:
+            return super(BotoClient, self).update_stack(kwargs['StackName'],
+                                     template_url=kwargs['TemplateUrl'],
+                                     parameters=kwargs['Parameters'])
+        elif 'TemplateBody' in kwargs:
+            return super(BotoClient, self).update_stack(kwargs['StackName'],
+                                     template_body=kwargs['TemplateBody'],
+                                     parameters=kwargs['Parameters'])
+        else:
+            logger.error("Must specify TemplateUrl or TemplateBody!")
+
+    def delete_stack(self, **kwargs):
+        return super(BotoClient, self).delete_stack(kwargs['StackName'])
+
+    def list_stack_events(self, **kwargs):
+        return super(BotoClient, self).describe_stack_events(
+                     kwargs['StackName'])
+
+    def describe_stack_resource(self, **kwargs):
+        return super(BotoClient, self).describe_stack_resource(
+                     kwargs['StackName'], kwargs['LogicalResourceId'])
+
+    def describe_stack_resources(self, **kwargs):
+        # Check if this is a StackName, if not assume it's a physical res ID
+        # Note this is slower for the common case, which is probably StackName
+        # than just doing a try/catch over the StackName case then retrying
+        # on failure with kwargs['NameOrPid'] as the physical resource ID,
+        # however boto spews errors when raising an exception so we can't
+        list_stacks = self.list_stacks()
+        stack_names = [s.stack_name for s in list_stacks]
+        if kwargs['NameOrPid'] in stack_names:
+            logger.debug("Looking up resources for StackName:%s" %
+                          kwargs['NameOrPid'])
+            return super(BotoClient, self).describe_stack_resources(
+                         stack_name_or_id=kwargs['NameOrPid'],
+                         logical_resource_id=kwargs['LogicalResourceId'])
+        else:
+            logger.debug("Looking up resources for PhysicalResourceId:%s" %
+                          kwargs['NameOrPid'])
+            return super(BotoClient, self).describe_stack_resources(
+                         stack_name_or_id=None,
+                         logical_resource_id=kwargs['LogicalResourceId'],
+                         physical_resource_id=kwargs['NameOrPid'])
+
+    def list_stack_resources(self, **kwargs):
+        return super(BotoClient, self).list_stack_resources(
+                     kwargs['StackName'])
+
+    def validate_template(self, **kwargs):
+        if 'TemplateUrl' in kwargs:
+            return super(BotoClient, self).validate_template(
+                         template_url=kwargs['TemplateUrl'])
+        elif 'TemplateBody' in kwargs:
+            return super(BotoClient, self).validate_template(
+                         template_body=kwargs['TemplateBody'])
+        else:
+            logger.error("Must specify TemplateUrl or TemplateBody!")
+
+    def get_template(self, **kwargs):
+        return super(BotoClient, self).get_template(kwargs['StackName'])
+
+    def estimate_template_cost(self, **kwargs):
+        if 'TemplateUrl' in kwargs:
+            return super(BotoClient, self).estimate_template_cost(
+                         kwargs['StackName'],
+                         template_url=kwargs['TemplateUrl'],
+                         parameters=kwargs['Parameters'])
+        elif 'TemplateBody' in kwargs:
+            return super(BotoClient, self).estimate_template_cost(
+                         kwargs['StackName'],
+                         template_body=kwargs['TemplateBody'],
+                         parameters=kwargs['Parameters'])
+        else:
+            logger.error("Must specify TemplateUrl or TemplateBody!")
+
+    def format_stack_event(self, events):
+        '''
+        Return string formatted representation of
+        boto.cloudformation.stack.StackEvent objects
+        '''
+        ret = []
+        for event in events:
+            ret.append("EventId : %s" % event.event_id)
+            ret.append("LogicalResourceId : %s" % event.logical_resource_id)
+            ret.append("PhysicalResourceId : %s" % event.physical_resource_id)
+            ret.append("ResourceProperties : %s" % event.resource_properties)
+            ret.append("ResourceStatus : %s" % event.resource_status)
+            ret.append("ResourceStatusReason : %s" %
+                        event.resource_status_reason)
+            ret.append("ResourceType : %s" % event.resource_type)
+            ret.append("StackId : %s" % event.stack_id)
+            ret.append("StackName : %s" % event.stack_name)
+            ret.append("Timestamp : %s" % event.timestamp)
+            ret.append("--")
+        return '\n'.join(ret)
+
+    def format_stack(self, stacks):
+        '''
+        Return string formatted representation of
+        boto.cloudformation.stack.Stack objects
+        '''
+        ret = []
+        for s in stacks:
+            ret.append("Capabilities : %s" % s.capabilities)
+            ret.append("CreationTime : %s" % s.creation_time)
+            ret.append("Description : %s" % s.description)
+            ret.append("DisableRollback : %s" % s.disable_rollback)
+            ret.append("NotificationARNs : %s" % s.notification_arns)
+            ret.append("Outputs : %s" % s.outputs)
+            ret.append("Parameters : %s" % s.parameters)
+            ret.append("StackId : %s" % s.stack_id)
+            ret.append("StackName : %s" % s.stack_name)
+            ret.append("StackStatus : %s" % s.stack_status)
+            ret.append("StackStatusReason : %s" % s.stack_status_reason)
+            ret.append("TimeoutInMinutes : %s" % s.timeout_in_minutes)
+            ret.append("--")
+        return '\n'.join(ret)
+
+    def format_stack_resource(self, resources):
+        '''
+        Return string formatted representation of
+        boto.cloudformation.stack.StackResource objects
+        '''
+        ret = []
+        for res in resources:
+            ret.append("LogicalResourceId : %s" % res.logical_resource_id)
+            ret.append("PhysicalResourceId : %s" % res.physical_resource_id)
+            ret.append("ResourceStatus : %s" % res.resource_status)
+            ret.append("ResourceStatusReason : %s" %
+                        res.resource_status_reason)
+            ret.append("ResourceType : %s" % res.resource_type)
+            ret.append("StackId : %s" % res.stack_id)
+            ret.append("StackName : %s" % res.stack_name)
+            ret.append("Timestamp : %s" % res.timestamp)
+            ret.append("--")
+        return '\n'.join(ret)
+
+    def format_stack_resource_summary(self, resources):
+        '''
+        Return string formatted representation of
+        boto.cloudformation.stack.StackResourceSummary objects
+        '''
+        ret = []
+        for res in resources:
+            ret.append("LastUpdatedTimestamp : %s" %
+                        res.last_updated_timestamp)
+            ret.append("LogicalResourceId : %s" % res.logical_resource_id)
+            ret.append("PhysicalResourceId : %s" % res.physical_resource_id)
+            ret.append("ResourceStatus : %s" % res.resource_status)
+            ret.append("ResourceStatusReason : %s" %
+                        res.resource_status_reason)
+            ret.append("ResourceType : %s" % res.resource_type)
+            ret.append("--")
+        return '\n'.join(ret)
+
+    def format_stack_summary(self, summaries):
+        '''
+        Return string formatted representation of
+        boto.cloudformation.stack.StackSummary objects
+        '''
+        ret = []
+        for s in summaries:
+            ret.append("StackId : %s" % s.stack_id)
+            ret.append("StackName : %s" % s.stack_name)
+            ret.append("CreationTime : %s" % s.creation_time)
+            ret.append("StackStatus : %s" % s.stack_status)
+            ret.append("TemplateDescription : %s" % s.template_description)
+            ret.append("--")
+        return '\n'.join(ret)
+
+    def format_template(self, template):
+        '''
+        String formatted representation of
+        boto.cloudformation.template.Template object
+        '''
+        ret = []
+        ret.append("Description : %s" % template.description)
+        for p in template.template_parameters:
+            ret.append("Parameter : ")
+            ret.append("  NoEcho : %s" % p.no_echo)
+            ret.append("  Description : %s" % p.description)
+            ret.append("  ParameterKey : %s" % p.parameter_key)
+        ret.append("--")
+        return '\n'.join(ret)
+
+    def format_parameters(self, options):
+        '''
+        Returns a dict containing list-of-tuple format
+        as expected by boto for request parameters
+        '''
+        parameters = {}
+        params = []
+        if options.parameters:
+            for p in options.parameters.split(';'):
+                (n, v) = p.split('=')
+                params.append((n, v))
+        parameters['Parameters'] = params
+        return parameters
+
+
+def get_client(host, port=None, username=None,
+               password=None, tenant=None,
+               auth_url=None, auth_strategy=None,
+               auth_token=None, region=None,
+               is_silent_upload=False, insecure=True):
+
+    """
+    Returns a new boto client object to a heat server
+    """
+
+    # Note we pass None/None for the keys so boto reads /etc/boto.cfg
+    # Also note is_secure is defaulted to False as HTTPS connections
+    # don't seem to work atm, FIXME
+    cloudformation = BotoClient(aws_access_key_id=None,
+        aws_secret_access_key=None, is_secure=False,
+        port=port, path="/v1")
+    if cloudformation:
+        logger.debug("Got CF connection object OK")
+    else:
+        logger.error("Error establishing connection!")
+        sys.exit(1)
+
+    return cloudformation
index 904ad6c79948e5bc71e7f730bbbea694e6c5f28d..2a40f428db633e26a5a174903af0ebc9901f9d11 100644 (file)
@@ -72,7 +72,21 @@ class V1Client(base_client.BaseClient):
         return self.stack_request("DescribeStackResource", "GET", **kwargs)
 
     def describe_stack_resources(self, **kwargs):
-        return self.stack_request("DescribeStackResources", "GET", **kwargs)
+        for lookup_key in ['StackName', 'PhysicalResourceId']:
+            lookup_value = kwargs['NameOrPid']
+            parameters = {
+                lookup_key: lookup_value,
+                'LogicalResourceId': kwargs['LogicalResourceId']}
+            try:
+                result = self.stack_request("DescribeStackResources", "GET",
+                                        **parameters)
+            except:
+                logger.debug("Failed to lookup resource details with key %s:%s"
+                             % (lookup_key, lookup_value))
+            else:
+                logger.debug("Got lookup resource details with key %s:%s" %
+                             (lookup_key, lookup_value))
+                return result
 
     def list_stack_resources(self, **kwargs):
         return self.stack_request("ListStackResources", "GET", **kwargs)
@@ -86,6 +100,40 @@ class V1Client(base_client.BaseClient):
     def estimate_template_cost(self, **kwargs):
         return self.stack_request("EstimateTemplateCost", "GET", **kwargs)
 
+    # Dummy print functions for alignment with the boto-based client
+    # which has to extract class fields for printing, we could also
+    # align output format here by decoding the XML/JSON
+    def format_stack_event(self, event):
+        return str(event)
+
+    def format_stack(self, stack):
+        return str(stack)
+
+    def format_stack_resource(self, res):
+        return str(res)
+
+    def format_stack_resource_summary(self, res):
+        return str(res)
+
+    def format_stack_summary(self, summary):
+        return str(summary)
+
+    def format_template(self, template):
+        return str(template)
+
+    def format_parameters(self, options):
+        '''
+        Reformat parameters into dict of format expected by the API
+        '''
+        parameters = {}
+        if options.parameters:
+            for count, p in enumerate(options.parameters.split(';'), 1):
+                (n, v) = p.split('=')
+                parameters['Parameters.member.%d.ParameterKey' % count] = n
+                parameters['Parameters.member.%d.ParameterValue' % count] = v
+        return parameters
+
+
 HeatClient = V1Client