Move heat-cfn, heat-boto, heat-watch to new repo

The new home for these tools is
https://github.com/openstack-dev/heat-cfnclient

These tools are now aimed at heat developers only
and they will not be released or packaged.

Change-Id: I1ea62ef17e81ab53cacb5e4940f0c4e2516ed383
This commit is contained in:
Steve Baker 2013-07-22 16:01:44 +12:00
parent 1d1c7927d1
commit 34e47b64d9
22 changed files with 0 additions and 3217 deletions

View File

@ -1 +0,0 @@
heat-cfn

View File

@ -1,693 +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 optparse
import os
import os.path
import sys
import time
import logging
import httplib
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])
from heat.openstack.common import gettextutils
gettextutils.install('heat', lazy=True)
if scriptname == 'heat-boto':
from heat.cfn_client import boto_client as heat_client
else:
from heat.cfn_client import client as heat_client
from heat.version import version_info as version
from heat.common import config
from heat.common import exception
from heat.cfn_client import utils
from keystoneclient.v2_0 import client
def get_swift_template(options):
'''
Retrieve a template from the swift object store, using
the provided URL. We request a keystone token to authenticate
'''
template_body = None
if options.auth_strategy == 'keystone':
# we use the keystone credentials to get a token
# to pass in the request header
keystone = client.Client(username=options.username,
password=options.password,
tenant_name=options.tenant,
auth_url=options.auth_url)
logging.info("Getting template from swift URL: %s" %
options.template_object)
url = urlparse(options.template_object)
if url.scheme == 'https':
conn = httplib.HTTPSConnection(url.netloc)
else:
conn = httplib.HTTPConnection(url.netloc)
headers = {'X-Auth-Token': keystone.auth_token}
conn.request("GET", url.path, headers=headers)
r1 = conn.getresponse()
logging.info('status %d' % r1.status)
if r1.status == 200:
template_body = r1.read()
conn.close()
else:
logging.error("template-object option requires keystone")
return template_body
def get_template_param(options):
'''
Helper function to extract the template in whatever
format has been specified by the cli options
'''
param = {}
if options.template_file:
param['TemplateBody'] = open(options.template_file).read()
elif options.template_url:
param['TemplateUrl'] = options.template_url
elif options.template_object:
template_body = get_swift_template(options)
if template_body:
param['TemplateBody'] = template_body
else:
logging.error("Error reading swift template")
return param
@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-cfn 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 = {}
templ_param = get_template_param(options)
if templ_param:
parameters.update(templ_param)
else:
logging.error('Please specify a template file or url')
return utils.FAILURE
c = get_client(options)
parameters.update(c.format_parameters(options))
result = c.validate_template(**parameters)
print c.format_template(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
templ_param = get_template_param(options)
if templ_param:
parameters.update(templ_param)
else:
logging.error('Please specify a template file or url')
return utils.FAILURE
c = get_client(options)
parameters.update(c.format_parameters(options))
result = c.estimate_template_cost(**parameters)
print result
@utils.catch_error('gettemplate')
def get_template(options, arguments):
'''
Gets an existing stack template.
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
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(**parameters)
print result
@utils.catch_error('create')
def stack_create(options, arguments):
'''
Create a new stack from a template.
Usage: heat-cfn 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.
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
logging.error("Please specify the stack name you wish to create")
logging.error("as the first argument")
return utils.FAILURE
parameters['TimeoutInMinutes'] = options.timeout
if options.enable_rollback:
parameters['DisableRollback'] = 'False'
templ_param = get_template_param(options)
if templ_param:
parameters.update(templ_param)
else:
logging.error('Please specify a template file or url')
return utils.FAILURE
c = get_client(options)
parameters.update(c.format_parameters(options))
result = c.create_stack(**parameters)
print result
@utils.catch_error('update')
def stack_update(options, arguments):
'''
Update an existing stack.
Usage: heat-cfn 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 = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
logging.error("Please specify the stack name you wish to update")
logging.error("as the first argument")
return utils.FAILURE
templ_param = get_template_param(options)
if templ_param:
parameters.update(templ_param)
else:
logging.error('Please specify a template file or url')
return utils.FAILURE
c = get_client(options)
parameters.update(c.format_parameters(options))
result = c.update_stack(**parameters)
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-cfn delete <stack name>
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
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(**parameters)
print result
@utils.catch_error('describe')
def stack_describe(options, arguments):
'''
Describes an existing stack.
Usage: heat-cfn describe <stack name>
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
logging.info("No stack name passed, getting results for ALL stacks")
c = get_client(options)
result = c.describe_stacks(**parameters)
print c.format_stack(result)
@utils.catch_error('event-list')
def stack_events_list(options, arguments):
'''
List events associated with the given stack.
Usage: heat-cfn event-list <stack name>
'''
parameters = {}
try:
parameters['StackName'] = arguments.pop(0)
except IndexError:
pass
c = get_client(options)
result = c.list_stack_events(**parameters)
print c.format_stack_event(result)
@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
parameters = {
'StackName': stack_name,
'LogicalResourceId': resource_name,
}
result = c.describe_stack_resource(**parameters)
print c.format_stack_resource_detail(result)
@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
parameters = {
'StackName': stack_name,
}
result = c.list_stack_resources(**parameters)
print c.format_stack_resource_summary(result)
@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
parameters = {
'NameOrPid': name_or_pid,
'LogicalResourceId': logical_resource_id, }
c = get_client(options)
result = c.describe_stack_resources(**parameters)
if result:
print c.format_stack_resource(result)
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-cfn list
'''
c = get_client(options)
result = c.list_stacks()
print c.format_stack_summary(result)
def get_client(options):
"""
Returns a new client object to a heat server
specified by the --host and --port options
supplied to the CLI
Note options.host is ignored for heat-boto, host must be
set in your boto config via the cfn_region_endpoint option
"""
return heat_client.get_client(host=options.host,
port=options.port,
username=options.username,
password=options.password,
tenant=options.tenant,
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('-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=None,
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, defaults to env[OS_USERNAME]")
parser.add_option('-K', '--password', dest="password",
metavar="PASSWORD", default=None,
help="Password used to acquire an authentication "
"token, defaults to env[OS_PASSWORD]")
parser.add_option('-T', '--tenant', dest="tenant",
metavar="TENANT", default=None,
help="Tenant name used for Keystone authentication, "
"defaults to env[OS_TENANT_NAME]")
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, "
"defaults to env[OS_AUTH_URL]")
parser.add_option('-S', '--auth_strategy', dest="auth_strategy",
metavar="STRATEGY", default=None,
help="Authentication strategy (keystone or noauth)")
parser.add_option('-o', '--template-object',
metavar="template_object", default=None,
help="URL to retrieve template object (e.g from swift)")
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.")
parser.add_option('-r', '--enable-rollback', dest="enable_rollback",
default=False, action="store_true",
help="Enable rollback on failure")
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 scriptname == 'heat-boto':
if options.host is not None:
logging.error("Use boto.cfg or ~/.boto cfn_region_endpoint")
raise ValueError("--host option not supported by heat-boto")
if options.url is not None:
logging.error("Use boto.cfg or ~/.boto cfn_region_endpoint")
raise ValueError("--url option not supported by heat-boto")
if not (options.username and options.password) and not options.auth_token:
logging.error("Must specify credentials, " +
"either username/password or token")
logging.error("See %s --help for options" % scriptname)
sys.exit(1)
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,
'events_list': stack_events_list, # DEPRECATED
'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
"""
version_string = version.version_string()
oparser = optparse.OptionParser(version=version_string,
usage=usage.strip())
create_options(oparser)
try:
(opts, cmd, args) = parse_options(oparser, sys.argv[1:])
except ValueError as ex:
logging.error("Error parsing options : %s" % str(ex))
sys.exit(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) as ex:
oparser.print_usage()
logging.error("ERROR: %s" % ex)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -1,281 +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-api-cloudwatch.
It is simply a command-line interface for adding, modifying, and retrieving
information about the cloudwatch alarms and metrics belonging to a user.
It is a convenience application that talks to the heat Cloudwatch API server.
"""
import optparse
import os
import os.path
import sys
import time
import logging
# 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])
from heat.openstack.common import gettextutils
gettextutils.install('heat')
from heat.cfn_client import boto_client_cloudwatch as heat_client
from heat.version import version_info as version
from heat.common import exception
from heat.cfn_client import utils
DEFAULT_PORT = 8003
@utils.catch_error('alarm-describe')
def alarm_describe(options, arguments):
'''
Describe detail for specified alarm, or all alarms
if no AlarmName is specified
'''
parameters = {}
try:
parameters['AlarmName'] = arguments.pop(0)
except IndexError:
logging.info("No AlarmName passed, getting results for ALL alarms")
c = heat_client.get_client(options.port)
result = c.describe_alarm(**parameters)
print c.format_metric_alarm(result)
@utils.catch_error('alarm-set-state')
def alarm_set_state(options, arguments):
'''
Temporarily set state for specified alarm
'''
usage = ('''Usage:
%s alarm-set-state AlarmName StateValue [StateReason]''' %
(scriptname))
parameters = {}
try:
parameters['AlarmName'] = arguments.pop(0)
parameters['StateValue'] = arguments.pop(0)
except IndexError:
logging.error("Must specify AlarmName and StateValue")
print usage
print "StateValue must be one of %s, %s or %s" % (
heat_client.BotoCWClient.ALARM_STATES)
return utils.FAILURE
try:
parameters['StateReason'] = arguments.pop(0)
except IndexError:
parameters['StateReason'] = ""
# We don't handle attaching data to state via this cli tool (yet)
parameters['StateReasonData'] = None
c = heat_client.get_client(options.port)
result = c.set_alarm_state(**parameters)
print result
@utils.catch_error('metric-list')
def metric_list(options, arguments):
'''
List all metric data for a given metric (or all metrics if none specified)
'''
parameters = {}
try:
parameters['MetricName'] = arguments.pop(0)
except IndexError:
logging.info("No MetricName passed, getting results for ALL alarms")
c = heat_client.get_client(options.port)
result = c.list_metrics(**parameters)
print c.format_metric(result)
@utils.catch_error('metric-put-data')
def metric_put_data(options, arguments):
'''
Create a datapoint for a specified metric
'''
usage = ('''Usage:
%s metric-put-data AlarmName Namespace MetricName Units MetricValue
e.g
%s metric-put-data HttpFailureAlarm system/linux ServiceFailure Count 1
''' % (scriptname, scriptname))
# NOTE : we currently only support metric datapoints associated with a
# specific AlarmName, due to the current engine/db cloudwatch
# implementation, we should probably revisit this so we can support
# more generic metric data collection
parameters = {}
try:
parameters['AlarmName'] = arguments.pop(0)
parameters['Namespace'] = arguments.pop(0)
parameters['MetricName'] = arguments.pop(0)
parameters['MetricUnit'] = arguments.pop(0)
parameters['MetricValue'] = arguments.pop(0)
except IndexError:
logging.error("Please specify the alarm, metric, unit and value")
print usage
return utils.FAILURE
c = heat_client.get_client(options.port)
result = c.put_metric_data(**parameters)
print result
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('-p', '--port', dest="port", type=int,
default=DEFAULT_PORT,
help="Port the heat API host listens on. "
"Default: %default")
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)
# 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}
watch_commands = {
'describe': alarm_describe,
'set-state': alarm_set_state,
'metric-list': metric_list,
'metric-put-data': metric_put_data}
commands = {}
for command_set in (base_commands, watch_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
describe Describe a specified alarm (or all alarms)
set-state Temporarily set the state of an alarm
metric-list List data-points for specified metric
metric-put-data Publish data-point for specified metric
"""
version_string = version.version_string()
oparser = optparse.OptionParser(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) as ex:
oparser.print_usage()
logging.error("ERROR: %s" % ex)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -222,12 +222,6 @@ man_pages = [
('man/heat-api-cloudwatch', 'heat-api-cloudwatch',
u'CloudWatch alike API service to the heat project',
[u'Heat Developers'], 1),
('man/heat-boto', 'heat-boto',
u'Command line utility to run heat actions over the CloudFormation API',
[u'Heat Developers'], 1),
('man/heat-cfn', 'heat-cfn',
u'Command line utility to run heat actions over the CloudFormation API',
[u'Heat Developers'], 1),
('man/heat-db-setup', 'heat-db-setup',
u'Command line utility to setup the Heat database',
[u'Heat Developers'], 1),
@ -237,9 +231,6 @@ man_pages = [
('man/heat-keystone-setup', 'heat-keystone-setup',
u'Script which sets up keystone for usage by Heat',
[u'Heat Developers'], 1),
('man/heat-watch', 'heat-watch',
u'Command line utility to run heat watch actions over the CloudWatch API',
[u'Heat Developers'], 1),
]
# If true, show URL addresses after external links.

View File

@ -1,194 +0,0 @@
=========
heat-boto
=========
.. program:: heat-boto
SYNOPSIS
========
``heat-boto [OPTIONS] COMMAND [COMMAND_OPTIONS]``
DESCRIPTION
===========
heat-boto is a command-line utility for heat. It is a variant of the heat-cfn
tool which uses the boto client library (instead of the heat CFN client
library)
The tool provides an interface for adding, modifying, and retrieving
information about the stacks belonging to a user. It is a convenience
application that talks to the heat CloudFormation API.
CONFIGURATION
=============
heat-watch uses the boto client library, and expects some configuration files
to exist in your environment, see our wiki for an example configuration file:
https://wiki.openstack.org/wiki/Heat/Using-Boto
COMMANDS
========
``create``
Create stack as defined in template file
``delete``
Delete specified stack
``describe``
Provide detailed information about the specified stack, or if no arguments are given all stacks
``estimate-template-cost``
Currently not implemented
``event-list``
List events related to specified stacks, or if no arguments are given all stacks
``gettemplate``
Get the template for a running stack
``help``
Provide help/usage information
``list``
List summary information for all stacks
``resource``
List information about a specific resource
``resource-list``
List all resources for a specified stack
``resource-list-details``
List details of all resources for a specified stack or physical resource ID, optionally filtered by a logical resource ID
``update``
Update a running stack with a modified template or template parameters - currently not implemented
``validate``
Validate a template file syntax
OPTIONS
=======
Note some options are marked as having no effect due to the common implementation with heat-cfn.
These are options which work with heat-cfn, but not with heat-boto, in most cases the information
should be specified via your boto configuration file instead.
.. cmdoption:: -S, --auth_strategy
This option has no effect, credentials should be specified in your boto config
.. cmdoption:: -A, --auth_token
This option has no effect, credentials should be specified in your boto config
.. cmdoption:: -N, --auth_url
This option has no effect, credentials should be specified in your boto config
.. cmdoption:: -d, --debug
Enable verbose debug level output
.. cmdoption:: -H, --host
Note, this option does not work for heat-boto due to limitations of the boto library
You should specify cfn_region_endpoint option in your boto config.
.. cmdoption:: -k, --insecure
This option has no effect, is_secure should be specified in your boto config
.. cmdoption:: -P, --parameters
Stack input parameters
.. cmdoption:: -K, --password
This option has no effect, credentials should be specified in your boto config
.. cmdoption:: -p, --port
Specify the port to connect to for the heat API service
.. cmdoption:: -R, --region
This option has no effect, credentials should be specified in your boto config
.. cmdoption:: -f, --template-file
Path to file containing the stack template
.. cmdoption:: -u, --template-url
URL to stack template
.. cmdoption:: -T, --tenant
This option has no effect, credentials should be specified in your boto config
.. cmdoption:: -t, --timeout
Stack creation timeout (default is 60 minutes)
.. cmdoption:: -U, --url
This option has no effect, cfn_region_endpoint should be specified in your boto config
.. cmdoption:: -I, --username
This option has no effect, credentials should be specified in your boto config
.. cmdoption:: -v, --verbose
Enable verbose output
.. cmdoption:: -y, --yes
Do not prompt for confirmation, assume yes
EXAMPLES
========
heat-boto -d create wordpress \\
--template-file=templates/WordPress_Single_Instance.template\\
--parameters="InstanceType=m1.xlarge;DBUsername=${USER};\\
DBPassword=verybadpass;KeyName=${USER}_key"
heat-boto list
heat-boto describe wordpress
heat-boto resource-list wordpress
heat-boto resource-list-details wordpress
heat-boto resource-list-details wordpress WikiDatabase
heat-boto resource wordpress WikiDatabase
heat-boto event-list
heat-boto delete wordpress
BUGS
====
Heat bugs are managed through Launchpad <https://launchpad.net/heat>

View File

@ -1,199 +0,0 @@
========
heat-cfn
========
.. program:: heat-cfn
SYNOPSIS
========
``heat-cfn [OPTIONS] COMMAND [COMMAND_OPTIONS]``
DESCRIPTION
===========
heat-cfn is a command-line utility for heat. It is simply an
interface for adding, modifying, and retrieving information about the stacks
belonging to a user. It is a convenience application that talks to the heat
CloudFormation API compatable server.
CONFIGURATION
=============
heat-cfn uses keystone authentication, and expects some variables to be
set in your environment, without these heat will not be able to establish
an authenticated connection with the heat API server.
Example:
export ADMIN_TOKEN=<keystone admin token>
export OS_USERNAME=admin
export OS_PASSWORD=verybadpass
export OS_TENANT_NAME=admin
export OS_AUTH_URL=http://127.0.0.1:5000/v2.0/
export OS_AUTH_STRATEGY=keystone
COMMANDS
========
``create``
Create stack as defined in template file
``delete``
Delete specified stack
``describe``
Provide detailed information about the specified stack, or if no arguments are given all stacks
``estimate-template-cost``
Currently not implemented
``event-list``
List events related to specified stacks, or if no arguments are given all stacks
``gettemplate``
Get the template for a running stack
``help``
Provide help/usage information
``list``
List summary information for all stacks
``resource``
List information about a specific resource
``resource-list``
List all resources for a specified stack
``resource-list-details``
List details of all resources for a specified stack or physical resource ID, optionally filtered by a logical resource ID
``update``
Update a running stack with a modified template or template parameters - currently not implemented
``validate``
Validate a template file syntax
OPTIONS
=======
.. cmdoption:: -S, --auth_strategy
Authentication strategy
.. cmdoption:: -A, --auth_token
Authentication token to use to identify the client to the heat server
.. cmdoption:: -N, --auth_url
Authentication URL for keystone authentication
.. cmdoption:: -d, --debug
Enable verbose debug level output
.. cmdoption:: -H, --host
Specify the hostname running the heat API service
.. cmdoption:: -k, --insecure
Use plain HTTP instead of HTTPS
.. cmdoption:: -P, --parameters
Stack input parameters
.. cmdoption:: -K, --password
Password used to acquire an authentication token
.. cmdoption:: -p, --port
Specify the port to connect to for the heat API service
.. cmdoption:: -R, --region
Region name. When using keystone authentication "version 2.0 or later this identifies the region
.. cmdoption:: -f, --template-file
Path to file containing the stack template
.. cmdoption:: -u, --template-url
URL to stack template
.. cmdoption:: -T, --tenant
Tenant name used for Keystone authentication
.. cmdoption:: -t, --timeout
Stack creation timeout (default is 60 minutes)
.. cmdoption:: -U, --url
URL of heat service
.. cmdoption:: -I, --username
User name used to acquire an authentication token
.. cmdoption:: -v, --verbose
Enable verbose output
.. cmdoption:: -y, --yes
Do not prompt for confirmation, assume yes
EXAMPLES
========
heat-cfn -d create wordpress --template-
file=templates/WordPress_Single_Instance.template
--parameters="InstanceType=m1.xlarge;DBUsername=${USER};DBPassword=verybadpass;KeyName=${USER}_key"
heat-cfn list
heat-cfn describe wordpress
heat-cfn resource-list wordpress
heat-cfn resource-list-details wordpress
heat-cfn resource-list-details wordpress WikiDatabase
heat-cfn resource wordpress WikiDatabase
heat-cfn event-list
heat-cfn delete wordpress
BUGS
====
Heat bugs are managed through Launchpad <https://launchpad.net/heat>

View File

@ -1,95 +0,0 @@
==========
heat-watch
==========
.. program:: heat-watch
SYNOPSIS
========
``heat-watch [OPTIONS] COMMAND [COMMAND_OPTIONS]``
DESCRIPTION
===========
heat-watch is a command-line utility for heat-api-cloudwatch.
It allows manipulation of the watch alarms and metric data via the heat
cloudwatch API, so this service must be running and accessibe on the host
specified in your boto config (cloudwatch_region_endpoint)
CONFIGURATION
=============
heat-watch uses the boto client library, and expects some configuration files
to exist in your environment, see our wiki for an example configuration file:
https://wiki.openstack.org/wiki/Heat/Using-Boto
COMMANDS
========
``describe``
Provide detailed information about the specified watch rule, or if no arguments are given all watch rules
``set-state``
Temporarily set the state of a watch rule
``metric-list``
List data-points for a specified metric
``metric-put-data``
Publish data-point for specified metric
Note the metric must be associated with a CloudWatch Alarm (specified in a heat stack template), publishing arbitrary metric data is not supported.
``help``
Provide help/usage information on each command
OPTIONS
=======
.. cmdoption:: --version
show program version number and exit
.. cmdoption:: -h, --help
show this help message and exit
.. cmdoption:: -v, --verbose
Print more verbose output
.. cmdoption:: -d, --debug
Print debug output
.. cmdoption:: -p, --port
Specify port the heat CW API host listens on. Default: 8003
EXAMPLES
========
heat-watch describe
heat-watch metric-list
heat-watch metric-put-data HttpFailureAlarm system/linux ServiceFailure Count 1
heat-watch set-state HttpFailureAlarm ALARM
BUGS
====
Heat bugs are managed through Launchpad <https://launchpad.net/heat>

View File

@ -21,8 +21,5 @@ Heat utilities
.. toctree::
:maxdepth: 2
heat-cfn
heat-boto
heat-watch
heat-db-setup
heat-keystone-setup

View File

@ -1,22 +0,0 @@
_heat()
{
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
if [[ ${cur} == -* ]]; then
opts=$(heat-cfn --help | grep -A100 "^Options" | sed -r "s/^[[:space:]]*-[[:alpha:]]([[:space:]][[:alpha:]_]*,|,)[[:space:]]//" | cut -d "=" -f1 | grep "^--" | awk '{print $1}')
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
if [[ ${#COMP_WORDS[@]} -gt 2 ]]; then
return 0
else
cmds=$(heat-cfn help | awk '{print $1}' | egrep -v "^(Usage|Commands|$)")
COMPREPLY=( $(compgen -W "${cmds}" -- ${cur}) )
return 0
fi
}
complete -F _heat heat-cfn

View File

@ -1,26 +0,0 @@
#[Credentials]
# AWS credentials, from keystone ec2-credentials-list
# Note this section should only be uncommented for per-user
# boto config files, copy this file to ~/.boto
# Alternatively the credentials can be passed into the boto
# client at constructor-time in your code
#aws_access_key_id = YOUR_KEY
#aws_secret_access_key = YOUR_SECKEY
[Boto]
# Make boto output verbose debugging information
debug = 0
# Disable https connections
is_secure = 0
# Override the default AWS endpoint to connect to heat on localhost
cfn_region_name = heat
cfn_region_endpoint = 127.0.0.1
cloudwatch_region_name = heat
cloudwatch_region_endpoint = 127.0.0.1
# Set the client retries to 1, or errors connecting to heat repeat
# which is not useful when debugging API issues
num_retries = 1

View File

@ -1,317 +0,0 @@
# 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
"""
import sys
from heat.openstack.common import log as logging
logger = logging.getLogger(__name__)
from boto.cloudformation import CloudFormationConnection
class BotoClient(CloudFormationConnection):
'''
Wrapper class for boto CloudFormationConnection class
'''
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):
args = {'disable_rollback': True}
if str(kwargs.get('DisableRollback', '')).lower() == 'false':
args['disable_rollback'] = False
if 'TimeoutInMinutes' in kwargs:
try:
timeout = int(kwargs['TimeoutInMinutes'])
except ValueError:
logger.error("Invalid timeout %s" % kwargs['TimeoutInMinutes'])
return
else:
args['timeout_in_minutes'] = timeout
if 'TemplateUrl' in kwargs:
return super(BotoClient, self).create_stack(
kwargs['StackName'],
template_url=kwargs['TemplateUrl'],
parameters=kwargs['Parameters'],
**args)
elif 'TemplateBody' in kwargs:
return super(BotoClient, self).create_stack(
kwargs['StackName'],
template_body=kwargs['TemplateBody'],
parameters=kwargs['Parameters'],
**args)
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_resource_detail(self, res):
'''
Print response from describe_stack_resource call
Note pending upstream patch will make this response a
boto.cloudformation.stack.StackResourceDetail object
which aligns better with all the existing calls
see https://github.com/boto/boto/pull/857
For now, we format the dict response as a workaround
'''
resource_detail = res['DescribeStackResourceResponse'][
'DescribeStackResourceResult']['StackResourceDetail']
ret = []
for key in resource_detail:
ret.append("%s : %s" % (key, resource_detail[key]))
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,
aws_access_key=None, aws_secret_key=None):
"""
Returns a new boto Cloudformation client connection to a heat server
"""
# Note we pass None/None for the keys by default
# This means boto reads /etc/boto.cfg, or ~/.boto
# set is_secure=0 in the config to disable https
cloudformation = BotoClient(aws_access_key_id=aws_access_key,
aws_secret_access_key=aws_secret_key,
port=port,
path="/v1")
if cloudformation:
logger.debug("Got CF connection object OK")
else:
logger.error("Error establishing Cloudformation connection!")
sys.exit(1)
return cloudformation

View File

@ -1,210 +0,0 @@
# 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
"""
import sys
from heat.openstack.common import log as logging
logger = logging.getLogger(__name__)
from boto.ec2.cloudwatch import CloudWatchConnection
class BotoCWClient(CloudWatchConnection):
'''
Wrapper class for boto CloudWatchConnection class
'''
# TODO(unknown) : These should probably go in the CW API and be imported
DEFAULT_NAMESPACE = "heat/unknown"
METRIC_UNITS = ("Seconds", "Microseconds", "Milliseconds", "Bytes",
"Kilobytes", "Megabytes", "Gigabytes", "Terabytes",
"Bits", "Kilobits", "Megabits", "Gigabits", "Terabits",
"Percent", "Count", "Bytes/Second", "Kilobytes/Second",
"Megabytes/Second", "Gigabytes/Second", "Terabytes/Second",
"Bits/Second", "Kilobits/Second", "Megabits/Second",
"Gigabits/Second", "Terabits/Second", "Count/Second", None)
METRIC_COMPARISONS = (">=", ">", "<", "<=")
ALARM_STATES = ("OK", "ALARM", "INSUFFICIENT_DATA")
METRIC_STATISTICS = ("Average", "Sum", "SampleCount", "Maximum", "Minimum")
# Note, several of these boto calls take a list of alarm names, so
# we could easily handle multiple alarms per-action, but in the
# interests of keeping the client simple, we just handle one 'AlarmName'
def describe_alarm(self, **kwargs):
# If no AlarmName specified, we pass None, which returns
# results for ALL alarms
try:
name = kwargs['AlarmName']
except KeyError:
name = None
return super(BotoCWClient, self).describe_alarms(
alarm_names=[name])
def list_metrics(self, **kwargs):
# list_metrics returns non-null index in next_token if there
# are more than 500 metric results, in which case we have to
# re-read with the token to get the next batch of results
#
# Also note that we can do more advanced filtering by dimension
# and/or namespace, but for simplicity we only filter by
# MetricName for the time being
try:
name = kwargs['MetricName']
except KeyError:
name = None
results = []
token = None
while True:
results.append(super(BotoCWClient, self).list_metrics(
next_token=token,
dimensions=None,
metric_name=name,
namespace=None))
if not token:
break
return results
def put_metric_data(self, **kwargs):
'''
Publish metric data points to CloudWatch
'''
try:
metric_name = kwargs['MetricName']
metric_unit = kwargs['MetricUnit']
metric_value = kwargs['MetricValue']
metric_namespace = kwargs['Namespace']
except KeyError:
logger.error("Must pass MetricName, MetricUnit, " +
"Namespace, MetricValue!")
return
try:
metric_unit = kwargs['MetricUnit']
except KeyError:
metric_unit = None
# If we're passed AlarmName, we attach it to the metric
# as a dimension
try:
metric_dims = [{'AlarmName': kwargs['AlarmName']}]
except KeyError:
metric_dims = []
if metric_unit not in self.METRIC_UNITS:
logger.error("MetricUnit not an allowed value")
logger.error("MetricUnit must be one of %s" % self.METRIC_UNITS)
return
return super(BotoCWClient, self).put_metric_data(
namespace=metric_namespace,
name=metric_name,
value=metric_value,
timestamp=None, # This means use "now" in the engine
unit=metric_unit,
dimensions=metric_dims,
statistics=None)
def set_alarm_state(self, **kwargs):
return super(BotoCWClient, self).set_alarm_state(
alarm_name=kwargs['AlarmName'],
state_reason=kwargs['StateReason'],
state_value=kwargs['StateValue'],
state_reason_data=kwargs['StateReasonData'])
def format_metric_alarm(self, alarms):
'''
Return string formatted representation of
boto.ec2.cloudwatch.alarm.MetricAlarm objects
'''
ret = []
for s in alarms:
ret.append("AlarmName : %s" % s.name)
ret.append("AlarmDescription : %s" % s.description)
ret.append("ActionsEnabled : %s" % s.actions_enabled)
ret.append("AlarmActions : %s" % s.alarm_actions)
ret.append("AlarmArn : %s" % s.alarm_arn)
ret.append("AlarmConfigurationUpdatedTimestamp : %s" %
s.last_updated)
ret.append("ComparisonOperator : %s" % s.comparison)
ret.append("Dimensions : %s" % s.dimensions)
ret.append("EvaluationPeriods : %s" % s.evaluation_periods)
ret.append("InsufficientDataActions : %s" %
s.insufficient_data_actions)
ret.append("MetricName : %s" % s.metric)
ret.append("Namespace : %s" % s.namespace)
ret.append("OKActions : %s" % s.ok_actions)
ret.append("Period : %s" % s.period)
ret.append("StateReason : %s" % s.state_reason)
ret.append("StateUpdatedTimestamp : %s" %
s.last_updated)
ret.append("StateValue : %s" % s.state_value)
ret.append("Statistic : %s" % s.statistic)
ret.append("Threshold : %s" % s.threshold)
ret.append("Unit : %s" % s.unit)
ret.append("--")
return '\n'.join(ret)
def format_metric(self, metrics):
'''
Return string formatted representation of
boto.ec2.cloudwatch.metric.Metric objects
'''
# Boto appears to return metrics as a list-inside-a-list
# probably a bug in boto, but work around here
if len(metrics) == 1:
metlist = metrics[0]
elif len(metrics) == 0:
metlist = []
else:
# Shouldn't get here, unless boto gets fixed..
logger.error("Unexpected metric list-of-list length (boto fixed?)")
return "ERROR\n--"
ret = []
for m in metlist:
ret.append("MetricName : %s" % m.name)
ret.append("Namespace : %s" % m.namespace)
ret.append("Dimensions : %s" % m.dimensions)
ret.append("--")
return '\n'.join(ret)
def get_client(port=None, aws_access_key=None, aws_secret_key=None):
"""
Returns a new boto CloudWatch client connection to a heat server
Note : Configuration goes in /etc/boto.cfg, not via arguments
"""
# 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
cloudwatch = BotoCWClient(aws_access_key_id=aws_access_key,
aws_secret_access_key=aws_secret_key,
is_secure=False,
port=port,
path="/v1")
if cloudwatch:
logger.debug("Got CW connection object OK")
else:
logger.error("Error establishing CloudWatch connection!")
sys.exit(1)
return cloudwatch

View File

@ -1,188 +0,0 @@
# 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 classes for callers of a heat system
"""
from lxml import etree
from heat.common import client as base_client
from heat.common import exception
from heat.openstack.common import log as logging
logger = logging.getLogger(__name__)
SUPPORTED_PARAMS = ('StackName', 'TemplateBody', 'TemplateUrl',
'NotificationARNs', 'Parameters', 'Version',
'SignatureVersion', 'Timestamp', 'AWSAccessKeyId',
'Signature', 'TimeoutInMinutes', 'DisableRollback',
'LogicalResourceId', 'PhysicalResourceId', 'NextToken',
)
class V1Client(base_client.BaseClient):
"""Main client class for accessing heat resources."""
DEFAULT_DOC_ROOT = "/v1"
def _insert_common_parameters(self, params):
params['Version'] = '2010-05-15'
params['SignatureVersion'] = '2'
params['SignatureMethod'] = 'HmacSHA256'
def stack_request(self, action, method, **kwargs):
params = self._extract_params(kwargs, SUPPORTED_PARAMS)
self._insert_common_parameters(params)
params['Action'] = action
headers = {'X-Auth-User': self.creds['username'],
'X-Auth-Key': self.creds['password']}
res = self.do_request(method, "/", params=params, headers=headers)
doc = etree.fromstring(res.read())
return etree.tostring(doc, pretty_print=True)
def list_stacks(self, **kwargs):
return self.stack_request("ListStacks", "GET", **kwargs)
def describe_stacks(self, **kwargs):
return self.stack_request("DescribeStacks", "GET", **kwargs)
def create_stack(self, **kwargs):
return self.stack_request("CreateStack", "POST", **kwargs)
def update_stack(self, **kwargs):
return self.stack_request("UpdateStack", "POST", **kwargs)
def delete_stack(self, **kwargs):
return self.stack_request("DeleteStack", "GET", **kwargs)
def list_stack_events(self, **kwargs):
return self.stack_request("DescribeStackEvents", "GET", **kwargs)
def describe_stack_resource(self, **kwargs):
return self.stack_request("DescribeStackResource", "GET", **kwargs)
def describe_stack_resources(self, **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 Exception:
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)
def validate_template(self, **kwargs):
return self.stack_request("ValidateTemplate", "GET", **kwargs)
def get_template(self, **kwargs):
return self.stack_request("GetTemplate", "GET", **kwargs)
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_stack_resource_detail(self, res):
return str(res)
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('=', 1)
parameters['Parameters.member.%d.ParameterKey' % count] = n
parameters['Parameters.member.%d.ParameterValue' % count] = v
return parameters
HeatClient = V1Client
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=False):
"""
Returns a new client heat client object based on common kwargs.
If an option isn't specified falls back to common environment variable
defaults.
"""
if auth_url:
force_strategy = 'keystone'
else:
force_strategy = None
creds = dict(username=username,
password=password,
tenant=tenant,
auth_url=auth_url,
strategy=force_strategy or auth_strategy,
region=region)
if creds['strategy'] == 'keystone' and not creds['auth_url']:
msg = ("--auth_url option or OS_AUTH_URL environment variable "
"required when keystone authentication strategy is enabled\n")
raise exception.ClientConfigurationError(msg)
use_ssl = (creds['auth_url'] is not None and
creds['auth_url'].find('https') != -1)
client = HeatClient
return client(host=host,
port=port,
use_ssl=use_ssl,
auth_tok=auth_token,
creds=creds,
insecure=insecure,
service_type='cloudformation')

View File

@ -1,56 +0,0 @@
# 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.
import functools
from heat.common import exception
from heat.openstack.common import log as logging
LOG = logging.getLogger(__name__)
SUCCESS = 0
FAILURE = 1
def catch_error(action):
"""Decorator to provide sensible default error handling for CLI 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:
LOG.error("Not authorized to make this request. Check " +
"your credentials (OS_USERNAME, OS_PASSWORD, " +
"OS_TENANT_NAME, OS_AUTH_URL and OS_AUTH_STRATEGY).")
return FAILURE
except exception.ClientConfigurationError:
raise
except exception.KeystoneError as e:
LOG.error("Keystone did not finish the authentication and "
"returned the following message:\n\n%s" % e.message)
return FAILURE
except Exception as e:
options = arguments[0]
if options.debug:
raise
LOG.error("Failed to %s. Got error:" % action)
pieces = unicode(e).split('\n')
for piece in pieces:
LOG.error(piece)
return FAILURE
return wrapper
return wrap

View File

@ -1,273 +0,0 @@
# 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 auth module is intended to allow Openstack client-tools to select from a
variety of authentication strategies, including NoAuth (the default), and
Keystone (an identity management system).
> auth_plugin = AuthPlugin(creds)
> auth_plugin.authenticate()
> auth_plugin.auth_token
abcdefg
> auth_plugin.management_url
http://service_endpoint/
"""
import httplib2
import json
import urlparse
from heat.common import exception
from heat.openstack.common.gettextutils import _
class BaseStrategy(object):
def __init__(self):
self.auth_token = None
# TODO(sirp): Should expose selecting public/internal/admin URL.
self.management_url = None
def authenticate(self):
raise NotImplementedError
@property
def is_authenticated(self):
raise NotImplementedError
@property
def strategy(self):
raise NotImplementedError
class NoAuthStrategy(BaseStrategy):
def authenticate(self):
pass
@property
def is_authenticated(self):
return True
@property
def strategy(self):
return 'noauth'
class KeystoneStrategy(BaseStrategy):
MAX_REDIRECTS = 10
def __init__(self, creds, service_type):
self.creds = creds
self.service_type = service_type
super(KeystoneStrategy, self).__init__()
def check_auth_params(self):
# Ensure that supplied credential parameters are as required
for required in ('username', 'password', 'auth_url',
'strategy'):
if required not in self.creds:
raise exception.MissingCredentialError(required=required)
if self.creds['strategy'] != 'keystone':
raise exception.BadAuthStrategy(expected='keystone',
received=self.creds['strategy'])
# For v2.0 also check tenant is present
if self.creds['auth_url'].rstrip('/').endswith('v2.0'):
if 'tenant' not in self.creds:
raise exception.MissingCredentialError(required='tenant')
def authenticate(self):
"""Authenticate with the Keystone service.
There are a few scenarios to consider here:
1. Which version of Keystone are we using? v1 which uses headers to
pass the credentials, or v2 which uses a JSON encoded request body?
2. Keystone may respond back with a redirection using a 305 status
code.
3. We may attempt a v1 auth when v2 is what's called for. In this
case, we rewrite the url to contain /v2.0/ and retry using the v2
protocol.
"""
def _authenticate(auth_url):
# If OS_AUTH_URL is missing a trailing slash add one
if not auth_url.endswith('/'):
auth_url += '/'
token_url = urlparse.urljoin(auth_url, "tokens")
# 1. Check Keystone version
is_v2 = auth_url.rstrip('/').endswith('v2.0')
if is_v2:
self._v2_auth(token_url)
else:
self._v1_auth(token_url)
self.check_auth_params()
auth_url = self.creds['auth_url']
for x in range(self.MAX_REDIRECTS):
try:
_authenticate(auth_url)
except exception.AuthorizationRedirect as e:
# 2. Keystone may redirect us
auth_url = e.url
except exception.AuthorizationFailure:
# 3. In some configurations nova makes redirection to
# v2.0 keystone endpoint. Also, new location does not
# contain real endpoint, only hostname and port.
if 'v2.0' not in auth_url:
auth_url = urlparse.urljoin(auth_url, 'v2.0/')
else:
# If we sucessfully auth'd, then memorize the correct auth_url
# for future use.
self.creds['auth_url'] = auth_url
break
else:
# Guard against a redirection loop
raise exception.MaxRedirectsExceeded(redirects=self.MAX_REDIRECTS)
def _v1_auth(self, token_url):
creds = self.creds
headers = {}
headers['X-Auth-User'] = creds['username']
headers['X-Auth-Key'] = creds['password']
tenant = creds.get('tenant')
if tenant:
headers['X-Auth-Tenant'] = tenant
resp, resp_body = self._do_request(token_url, 'GET', headers=headers)
def _management_url(self, resp):
for url_header in ('x-heat-management-url',
'x-server-management-url',
'x-heat'):
try:
return resp[url_header]
except KeyError as e:
not_found = e
raise not_found
if resp.status in (200, 204):
try:
self.management_url = _management_url(self, resp)
self.auth_token = resp['x-auth-token']
except KeyError:
raise exception.AuthorizationFailure()
elif resp.status == 305:
raise exception.AuthorizationRedirect(resp['location'])
elif resp.status == 400:
raise exception.AuthBadRequest(url=token_url)
elif resp.status == 401:
raise exception.NotAuthorized()
elif resp.status == 404:
raise exception.AuthUrlNotFound(url=token_url)
else:
status = resp.status
raise Exception(_('Unexpected response: %(status)s')
% {'status': resp.status})
def _v2_auth(self, token_url):
def get_endpoint(service_catalog):
"""
Select an endpoint from the service catalog
We search the full service catalog for services
matching both type and region. If the client
supplied no region then any endpoint for the service
is considered a match. There must be one -- and
only one -- successful match in the catalog,
otherwise we will raise an exception.
"""
region = self.creds.get('region')
service_type_matches = lambda s: s.get('type') == self.service_type
region_matches = lambda e: region is None or e['region'] == region
endpoints = [ep for s in service_catalog if service_type_matches(s)
for ep in s['endpoints'] if region_matches(ep)]
if len(endpoints) > 1:
raise exception.RegionAmbiguity(region=region)
elif not endpoints:
raise exception.NoServiceEndpoint()
else:
# FIXME(sirp): for now just use the public url.
return endpoints[0]['publicURL']
creds = self.creds
creds = {
"auth": {
"tenantName": creds['tenant'],
"passwordCredentials": {
"username": creds['username'],
"password": creds['password']}}}
headers = {}
headers['Content-Type'] = 'application/json'
req_body = json.dumps(creds)
resp, resp_body = self._do_request(
token_url, 'POST', headers=headers, body=req_body)
if resp.status == 200:
resp_auth = json.loads(resp_body)['access']
self.management_url = get_endpoint(resp_auth['serviceCatalog'])
self.auth_token = resp_auth['token']['id']
elif resp.status == 305:
raise exception.RedirectException(resp['location'])
elif resp.status == 400:
raise exception.AuthBadRequest(url=token_url)
elif resp.status == 401:
raise exception.NotAuthorized()
elif resp.status == 404:
raise exception.AuthUrlNotFound(url=token_url)
else:
try:
body = json.loads(resp_body)
msg = body['error']['message']
except (ValueError, KeyError):
msg = resp_body
raise exception.KeystoneError(resp.status, msg)
@property
def is_authenticated(self):
return self.auth_token is not None
@property
def strategy(self):
return 'keystone'
@staticmethod
def _do_request(url, method, headers=None, body=None):
headers = headers or {}
conn = httplib2.Http()
conn.force_exception_to_status_code = True
headers['User-Agent'] = 'heat-client'
resp, resp_body = conn.request(url, method, headers=headers, body=body)
return resp, resp_body
def get_plugin_from_strategy(strategy, creds=None, service_type=None):
if strategy == 'noauth':
return NoAuthStrategy()
elif strategy == 'keystone':
return KeystoneStrategy(creds, service_type)
else:
raise Exception(_("Unknown auth strategy '%s'") % strategy)

View File

@ -1,548 +0,0 @@
# 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.
# HTTPSClientAuthConnection code comes courtesy of ActiveState website:
# http://code.activestate.com/recipes/
# 577548-https-httplib-client-connection-with-certificate-v/
import collections
import functools
import httplib
import os
import urllib
import urlparse
try:
from eventlet.green import socket
from eventlet.green import ssl
except ImportError:
import socket
import ssl
from heat.common import auth
from heat.common import exception
from heat.common import utils
from heat.openstack.common.gettextutils import _
# common chunk size for get and put
CHUNKSIZE = 65536
def handle_unauthorized(func):
"""
Wrap a function to re-authenticate and retry.
"""
@functools.wraps(func)
def wrapped(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
except exception.NotAuthorized:
self._authenticate(force_reauth=True)
return func(self, *args, **kwargs)
return wrapped
def handle_redirects(func):
"""
Wrap the _do_request function to handle HTTP redirects.
"""
MAX_REDIRECTS = 5
@functools.wraps(func)
def wrapped(self, method, url, body, headers):
for _ in xrange(MAX_REDIRECTS):
try:
return func(self, method, url, body, headers)
except exception.RedirectException as redirect:
if redirect.url is None:
raise exception.InvalidRedirect()
url = redirect.url
raise exception.MaxRedirectsExceeded(redirects=MAX_REDIRECTS)
return wrapped
class ImageBodyIterator(object):
"""
A class that acts as an iterator over an image file's
chunks of data. This is returned as part of the result
tuple from `heat.cfn_client.client.Client.get_image`
"""
def __init__(self, source):
"""
Constructs the object from a readable image source
(such as an HTTPResponse or file-like object)
"""
self.source = source
def __iter__(self):
"""
Exposes an iterator over the chunks of data in the
image file.
"""
while True:
chunk = self.source.read(CHUNKSIZE)
if chunk:
yield chunk
else:
break
class HTTPSClientAuthConnection(httplib.HTTPSConnection):
"""
Class to make a HTTPS connection, with support for
full client-based SSL Authentication
:see http://code.activestate.com/recipes/
577548-https-httplib-client-connection-with-certificate-v/
"""
def __init__(self, host, port, key_file, cert_file,
ca_file, timeout=None, insecure=False):
httplib.HTTPSConnection.__init__(self, host, port, key_file=key_file,
cert_file=cert_file)
self.key_file = key_file
self.cert_file = cert_file
self.ca_file = ca_file
self.timeout = timeout
self.insecure = insecure
def connect(self):
"""
Connect to a host on a given (SSL) port.
If ca_file is pointing somewhere, use it to check Server Certificate.
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
ssl.wrap_socket(), which forces SSL to check server certificate against
our client certificate.
"""
sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host:
self.sock = sock
self._tunnel()
# Check CA file unless 'insecure' is specificed
if self.insecure is True:
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
cert_reqs=ssl.CERT_NONE)
else:
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
ca_certs=self.ca_file,
cert_reqs=ssl.CERT_REQUIRED)
class BaseClient(object):
"""A base client class."""
DEFAULT_PORT = 80
DEFAULT_DOC_ROOT = None
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
# Suse, FreeBSD/OpenBSD
DEFAULT_CA_FILE_PATH = '/etc/ssl/certs/ca-certificates.crt:'\
'/etc/pki/tls/certs/ca-bundle.crt:'\
'/etc/ssl/ca-bundle.pem:'\
'/etc/ssl/cert.pem'
OK_RESPONSE_CODES = (
httplib.OK,
httplib.CREATED,
httplib.ACCEPTED,
httplib.NO_CONTENT,
)
REDIRECT_RESPONSE_CODES = (
httplib.MOVED_PERMANENTLY,
httplib.FOUND,
httplib.SEE_OTHER,
httplib.USE_PROXY,
httplib.TEMPORARY_REDIRECT,
)
def __init__(self, host=None, port=None, use_ssl=False, auth_tok=None,
creds=None, doc_root=None, key_file=None,
cert_file=None, ca_file=None, insecure=False,
configure_via_auth=True, service_type=None):
"""
Creates a new client to some service.
:param host: The host where service resides
:param port: The port where service resides
:param use_ssl: Should we use HTTPS?
:param auth_tok: The auth token to pass to the server
:param creds: The credentials to pass to the auth plugin
:param doc_root: Prefix for all URLs we request from host
:param key_file: Optional PEM-formatted file that contains the private
key.
If use_ssl is True, and this param is None (the
default), then an environ variable
HEAT_CLIENT_KEY_FILE is looked for. If no such
environ variable is found, ClientConnectionError
will be raised.
:param cert_file: Optional PEM-formatted certificate chain file.
If use_ssl is True, and this param is None (the
default), then an environ variable
HEAT_CLIENT_CERT_FILE is looked for. If no such
environ variable is found, ClientConnectionError
will be raised.
:param ca_file: Optional CA cert file to use in SSL connections
If use_ssl is True, and this param is None (the
default), then an environ variable
HEAT_CLIENT_CA_FILE is looked for.
:param insecure: Optional. If set then the server's certificate
will not be verified.
"""
self.host = host
self.port = port or self.DEFAULT_PORT
self.use_ssl = use_ssl
self.auth_tok = auth_tok
self.creds = creds or {}
self.connection = None
self.configure_via_auth = configure_via_auth
self.service_type = service_type
# doc_root can be a nullstring, which is valid, and why we
# cannot simply do doc_root or self.DEFAULT_DOC_ROOT below.
self.doc_root = (doc_root if doc_root is not None
else self.DEFAULT_DOC_ROOT)
self.auth_plugin = self.make_auth_plugin(self.creds)
self.key_file = key_file
self.cert_file = cert_file
self.ca_file = ca_file
self.insecure = insecure
self.connect_kwargs = self.get_connect_kwargs()
def get_connect_kwargs(self):
connect_kwargs = {}
if self.use_ssl:
if self.key_file is None:
self.key_file = os.environ.get('HEAT_CLIENT_KEY_FILE')
if self.cert_file is None:
self.cert_file = os.environ.get('HEAT_CLIENT_CERT_FILE')
if self.ca_file is None:
self.ca_file = os.environ.get('HEAT_CLIENT_CA_FILE')
# Check that key_file/cert_file are either both set or both unset
if self.cert_file is not None and self.key_file is None:
msg = _("You have selected to use SSL in connecting, "
"and you have supplied a cert, "
"however you have failed to supply either a "
"key_file parameter or set the "
"HEAT_CLIENT_KEY_FILE environ variable")
raise exception.ClientConnectionError(msg)
if self.key_file is not None and self.cert_file is None:
msg = _("You have selected to use SSL in connecting, "
"and you have supplied a key, "
"however you have failed to supply either a "
"cert_file parameter or set the "
"HEAT_CLIENT_CERT_FILE environ variable")
raise exception.ClientConnectionError(msg)
if (self.key_file is not None and
not os.path.exists(self.key_file)):
msg = _("The key file you specified %s does not "
"exist") % self.key_file
raise exception.ClientConnectionError(msg)
connect_kwargs['key_file'] = self.key_file
if (self.cert_file is not None and
not os.path.exists(self.cert_file)):
msg = _("The cert file you specified %s does not "
"exist") % self.cert_file
raise exception.ClientConnectionError(msg)
connect_kwargs['cert_file'] = self.cert_file
if (self.ca_file is not None and
not os.path.exists(self.ca_file)):
msg = _("The CA file you specified %s does not "
"exist") % self.ca_file
raise exception.ClientConnectionError(msg)
if self.ca_file is None:
for ca in self.DEFAULT_CA_FILE_PATH.split(":"):
if os.path.exists(ca):
self.ca_file = ca
break
connect_kwargs['ca_file'] = self.ca_file
connect_kwargs['insecure'] = self.insecure
return connect_kwargs
def set_auth_token(self, auth_tok):
"""
Updates the authentication token for this client connection.
"""
# FIXME(sirp): Nova image/heat.py currently calls this. Since this
# method isn't really doing anything useful[1], we should go ahead and
# rip it out, first in Nova, then here. Steps:
#
# 1. Change auth_tok in heat to auth_token
# 2. Change image/heat.py in Nova to use client.auth_token
# 3. Remove this method
#
# [1] http://mail.python.org/pipermail/tutor/2003-October/025932.html
self.auth_tok = auth_tok
def configure_from_url(self, url):
"""
Setups the connection based on the given url.
The form is:
<http|https>://<host>:port/doc_root
"""
parsed = urlparse.urlparse(url)
self.use_ssl = parsed.scheme == 'https'
if self.host is None:
self.host = parsed.hostname
self.port = parsed.port or 80
self.doc_root = parsed.path
# ensure connection kwargs are re-evaluated after the service catalog
# publicURL is parsed for potential SSL usage
self.connect_kwargs = self.get_connect_kwargs()
def make_auth_plugin(self, creds):
"""
Returns an instantiated authentication plugin.
"""
strategy = creds.get('strategy', 'noauth')
plugin = auth.get_plugin_from_strategy(strategy,
creds, self.service_type)
return plugin
def get_connection_type(self):
"""
Returns the proper connection type
"""
if self.use_ssl:
return HTTPSClientAuthConnection
else:
return httplib.HTTPConnection
def _authenticate(self, force_reauth=False):
"""
Use the authentication plugin to authenticate and set the auth token.
:param force_reauth: For re-authentication to bypass cache.
"""
auth_plugin = self.auth_plugin
if not auth_plugin.is_authenticated or force_reauth:
auth_plugin.authenticate()
self.auth_tok = auth_plugin.auth_token
management_url = auth_plugin.management_url
if management_url and self.configure_via_auth:
self.configure_from_url(management_url)
@handle_unauthorized
def do_request(self, method, action, body=None, headers=None,
params=None):
"""
Make a request, returning an HTTP response object.
:param method: HTTP verb (GET, POST, PUT, etc.)
:param action: Requested path to append to self.doc_root
:param body: Data to send in the body of the request
:param headers: Headers to send with the request
:param params: Key/value pairs to use in query string
:returns: HTTP response object
"""
if not self.auth_tok:
self._authenticate()
url = self._construct_url(action, params)
return self._do_request(method=method, url=url, body=body,
headers=headers)
def _construct_url(self, action, params=None):
"""
Create a URL object we can use to pass to _do_request().
"""
path = '/'.join([self.doc_root or '', action.lstrip('/')])
scheme = "https" if self.use_ssl else "http"
netloc = "%s:%d" % (self.host, self.port)
if isinstance(params, dict):
for (key, value) in params.items():
if value is None:
del params[key]
query = urllib.urlencode(params)
else:
query = None
return urlparse.ParseResult(scheme, netloc, path, '', query, '')
@handle_redirects
def _do_request(self, method, url, body, headers):
"""
Connects to the server and issues a request. Handles converting
any returned HTTP error status codes to OpenStack/heat exceptions
and closing the server connection. Returns the result data, or
raises an appropriate exception.
:param method: HTTP method ("GET", "POST", "PUT", etc...)
:param url: urlparse.ParsedResult object with URL information
:param body: data to send (as string, filelike or iterable),
or None (default)
:param headers: mapping of key/value pairs to add as headers
:note
If the body param has a read attribute, and method is either
POST or PUT, this method will automatically conduct a chunked-transfer
encoding and use the body as a file object or iterable, transferring
chunks of data using the connection's send() method. This allows large
objects to be transferred efficiently without buffering the entire
body in memory.
"""
if url.query:
path = url.path + "?" + url.query
else:
path = url.path
try:
connection_type = self.get_connection_type()
headers = headers or {}
if 'x-auth-token' not in headers and self.auth_tok:
headers['x-auth-token'] = self.auth_tok
c = connection_type(url.hostname, url.port, **self.connect_kwargs)
def _pushing(method):
return method.lower() in ('post', 'put')
def _simple(body):
return body is None or isinstance(body, basestring)
def _filelike(body):
return hasattr(body, 'read')
def _sendbody(connection, iter):
connection.endheaders()
for sent in iter:
# iterator has done the heavy lifting
pass
def _chunkbody(connection, iter):
connection.putheader('Transfer-Encoding', 'chunked')
connection.endheaders()
for chunk in iter:
connection.send('%x\r\n%s\r\n' % (len(chunk), chunk))
connection.send('0\r\n\r\n')
# Do a simple request or a chunked request, depending
# on whether the body param is file-like or iterable and
# the method is PUT or POST
#
if not _pushing(method) or _simple(body):
# Simple request...
c.request(method, path, body, headers)
elif _filelike(body) or self._iterable(body):
c.putrequest(method, path)
for header, value in headers.items():
c.putheader(header, value)
iter = self.image_iterator(c, headers, body)
_chunkbody(c, iter)
else:
raise TypeError('Unsupported image type: %s' % body.__class__)
res = c.getresponse()
def _retry(res):
return res.getheader('Retry-After')
status_code = self.get_status_code(res)
if status_code in self.OK_RESPONSE_CODES:
return res
elif status_code in self.REDIRECT_RESPONSE_CODES:
raise exception.RedirectException(res.getheader('Location'))
elif status_code == httplib.UNAUTHORIZED:
raise exception.NotAuthorized()
elif status_code == httplib.FORBIDDEN:
raise exception.NotAuthorized()
elif status_code == httplib.NOT_FOUND:
raise exception.NotFound(res.read())
elif status_code == httplib.CONFLICT:
raise exception.Duplicate(res.read())
elif status_code == httplib.BAD_REQUEST:
raise exception.Invalid(reason=res.read())
elif status_code == httplib.MULTIPLE_CHOICES:
raise exception.MultipleChoices(body=res.read())
elif status_code == httplib.REQUEST_ENTITY_TOO_LARGE:
raise exception.LimitExceeded(retry=_retry(res),
body=res.read())
elif status_code == httplib.INTERNAL_SERVER_ERROR:
raise Exception("Internal Server error: %s" % res.read())
elif status_code == httplib.SERVICE_UNAVAILABLE:
raise exception.ServiceUnavailable(retry=_retry(res))
elif status_code == httplib.REQUEST_URI_TOO_LONG:
raise exception.RequestUriTooLong(body=res.read())
else:
raise Exception("Unknown error occurred! %s" % res.read())
except (socket.error, IOError) as e:
raise exception.ClientConnectionError(e)
def _iterable(self, body):
return isinstance(body, collections.Iterable)
def image_iterator(self, connection, headers, body):
if self._iterable(body):
return utils.chunkreadable(body)
else:
return ImageBodyIterator(body)
def get_status_code(self, response):
"""
Returns the integer status code from the response, which
can be either a Webob.Response (used in testing) or httplib.Response
"""
if hasattr(response, 'status_int'):
return response.status_int
else:
return response.status
def _extract_params(self, actual_params, allowed_params):
"""
Extract a subset of keys from a dictionary. The filters key
will also be extracted, and each of its values will be returned
as an individual param.
:param actual_params: dict of keys to filter
:param allowed_params: list of keys that 'actual_params' will be
reduced to
:retval subset of 'params' dict
"""
result = {}
for param in actual_params:
if param in allowed_params:
result[param] = actual_params[param]
elif 'Parameters.member.' in param:
result[param] = actual_params[param]
return result

View File

@ -1,52 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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.
"""
System-level utilities and helper functions.
"""
from heat.openstack.common import log as logging
LOG = logging.getLogger(__name__)
def chunkreadable(iter, chunk_size=65536):
"""
Wrap a readable iterator with a reader yielding chunks of
a preferred size, otherwise leave iterator unchanged.
:param iter: an iter which may also be readable
:param chunk_size: maximum size of chunk
"""
return chunkiter(iter, chunk_size) if hasattr(iter, 'read') else iter
def chunkiter(fp, chunk_size=65536):
"""
Return an iterator to a file-like obj which yields fixed size chunks
:param fp: a file-like object
:param chunk_size: maximum size of chunk
"""
while True:
chunk = fp.read(chunk_size)
if chunk:
yield chunk
else:
break

View File

@ -1,45 +0,0 @@
# 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.
import testtools
import heat
import os
import subprocess
basepath = os.path.join(heat.__path__[0], os.path.pardir)
class CliTest(testtools.TestCase):
def test_heat_cfn(self):
self.bin_run('heat-cfn')
def test_heat_boto(self):
self.bin_run('heat-boto')
def test_heat_watch(self):
self.bin_run('heat-watch')
def bin_run(self, bin):
fullpath = basepath + '/bin/' + bin
proc = subprocess.Popen(fullpath,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
if proc.returncode:
print('Error executing %s:\n %s %s ' % (bin, stdout, stderr))
raise subprocess.CalledProcessError(proc.returncode, bin)

View File

@ -1,6 +1,5 @@
pbr>=0.5.21,<1.0
pycrypto>=2.6
boto>=2.4.0
eventlet>=0.13.0
greenlet>=0.3.2
httplib2

View File

@ -26,13 +26,10 @@ scripts =
bin/heat-api
bin/heat-api-cfn
bin/heat-api-cloudwatch
bin/heat-boto
bin/heat-cfn
bin/heat-db-setup
bin/heat-engine
bin/heat-keystone-setup
bin/heat-manage
bin/heat-watch
[global]
setup-hooks =

View File

@ -62,7 +62,6 @@ if user_wants 'Delete Heat binaries?'; then
sudo rm -f $BIN_PATH/heat-api
sudo rm -f $BIN_PATH/heat-api-cfn
sudo rm -f $BIN_PATH/heat-engine
sudo rm -f $BIN_PATH/heat-cfn
echo 1>&2
fi