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:
parent
1d1c7927d1
commit
34e47b64d9
@ -1 +0,0 @@
|
||||
heat-cfn
|
693
bin/heat-cfn
693
bin/heat-cfn
@ -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()
|
281
bin/heat-watch
281
bin/heat-watch
@ -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()
|
@ -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.
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -21,8 +21,5 @@ Heat utilities
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
heat-cfn
|
||||
heat-boto
|
||||
heat-watch
|
||||
heat-db-setup
|
||||
heat-keystone-setup
|
||||
|
@ -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
|
26
etc/boto.cfg
26
etc/boto.cfg
@ -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
|
@ -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
|
@ -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
|
@ -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')
|
@ -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
|
@ -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)
|
@ -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
|
@ -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
|
@ -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)
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user