Initial commit (basics copied from glance)
Signed-off-by: Angus Salkeld <asalkeld@redhat.com>changes/40/40/1
commit
3b9c41fb6c
|
@ -0,0 +1,6 @@
|
|||
*.pyc
|
||||
*.swp
|
||||
*.log
|
||||
build
|
||||
dist
|
||||
heat/vcsversion.py
|
|
@ -0,0 +1,176 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
|
@ -0,0 +1,377 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack, LLC
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
This is the administration program for heat. It is simply a command-line
|
||||
interface for adding, modifying, and retrieving information about the stacks
|
||||
belonging to a user.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import gettext
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
|
||||
from urlparse import urlparse
|
||||
|
||||
# If ../heat/__init__.py exists, add ../ to Python search path, so that
|
||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(possible_topdir, 'heat', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
gettext.install('heat', unicode=1)
|
||||
|
||||
from heat import client as heat_client
|
||||
from heat.common import exception
|
||||
from heat import version
|
||||
|
||||
|
||||
SUCCESS = 0
|
||||
FAILURE = 1
|
||||
|
||||
DEFAULT_PORT = 8000
|
||||
|
||||
def catch_error(action):
|
||||
"""Decorator to provide sensible default error handling for actions."""
|
||||
def wrap(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*arguments, **kwargs):
|
||||
try:
|
||||
ret = func(*arguments, **kwargs)
|
||||
return SUCCESS if ret is None else ret
|
||||
except exception.NotAuthorized:
|
||||
print "Not authorized to make this request. Check "\
|
||||
"your credentials (OS_AUTH_USER, OS_AUTH_KEY, ...)."
|
||||
return FAILURE
|
||||
except exception.ClientConfigurationError:
|
||||
raise
|
||||
except Exception, e:
|
||||
options = arguments[0]
|
||||
if options.debug:
|
||||
raise
|
||||
print "Failed to %s. Got error:" % action
|
||||
pieces = unicode(e).split('\n')
|
||||
for piece in pieces:
|
||||
print piece
|
||||
return FAILURE
|
||||
|
||||
return wrapper
|
||||
return wrap
|
||||
|
||||
@catch_error('validate')
|
||||
def template_validate(options, arguments):
|
||||
'''
|
||||
'''
|
||||
pass
|
||||
|
||||
@catch_error('gettemplate')
|
||||
def get_template(options, arguments):
|
||||
'''
|
||||
'''
|
||||
pass
|
||||
|
||||
@catch_error('create')
|
||||
def stack_create(options, arguments):
|
||||
'''
|
||||
'''
|
||||
|
||||
parameters = {}
|
||||
try:
|
||||
parameters['StackName'] = arguments.pop(0)
|
||||
except IndexError:
|
||||
print "Please specify the stack name you wish to create "
|
||||
print "as the first argument"
|
||||
return FAILURE
|
||||
|
||||
if options.parameters:
|
||||
for p in options.parameters.split(';'):
|
||||
(n, v) = p.split('=')
|
||||
parameters[n] = v
|
||||
|
||||
if options.template_file:
|
||||
parameters['TemplateBody'] = open(options.template_file).read()
|
||||
elif options.template_url:
|
||||
parameters['TemplateUrl'] = options.template_url
|
||||
else:
|
||||
print 'Please specify a template file or url'
|
||||
return FAILURE
|
||||
|
||||
c = get_client(options)
|
||||
result = c.create_stack(**parameters)
|
||||
print json.dumps(result, indent=2)
|
||||
|
||||
@catch_error('update')
|
||||
def stack_update(options, arguments):
|
||||
'''
|
||||
'''
|
||||
parameters = {}
|
||||
try:
|
||||
parameters['StackName'] = arguments.pop(0)
|
||||
except IndexError:
|
||||
print "Please specify the stack name you wish to update "
|
||||
print "as the first argument"
|
||||
return FAILURE
|
||||
|
||||
c = get_client(options)
|
||||
result = c.update_stack(parameters)
|
||||
print json.dumps(result, indent=2)
|
||||
|
||||
@catch_error('delete')
|
||||
def stack_delete(options, arguments):
|
||||
'''
|
||||
'''
|
||||
parameters = {}
|
||||
try:
|
||||
parameters['StackName'] = arguments.pop(0)
|
||||
except IndexError:
|
||||
print "Please specify the stack name you wish to delete "
|
||||
print "as the first argument"
|
||||
return FAILURE
|
||||
|
||||
c = get_client(options)
|
||||
result = c.delete_stack(parameters)
|
||||
print json.dumps(result, indent=2)
|
||||
|
||||
@catch_error('describe')
|
||||
def stack_describe(options, arguments):
|
||||
'''
|
||||
'''
|
||||
parameters = {}
|
||||
try:
|
||||
parameters['StackName'] = arguments.pop(0)
|
||||
except IndexError:
|
||||
print "Describing all stacks"
|
||||
|
||||
c = get_client(options)
|
||||
result = c.describe_stacks(parameters)
|
||||
print json.dumps(result, indent=2)
|
||||
|
||||
@catch_error('list')
|
||||
def stack_list(options, arguments):
|
||||
'''
|
||||
'''
|
||||
c = get_client(options)
|
||||
result = c.list_stacks()
|
||||
print json.dumps(result, indent=2)
|
||||
|
||||
def get_client(options):
|
||||
"""
|
||||
Returns a new client object to a heat server
|
||||
specified by the --host and --port options
|
||||
supplied to the CLI
|
||||
"""
|
||||
return heat_client.get_client(host=options.host,
|
||||
port=options.port,
|
||||
username=options.username,
|
||||
password=options.password,
|
||||
auth_url=options.auth_url,
|
||||
auth_strategy=options.auth_strategy,
|
||||
auth_token=options.auth_token,
|
||||
region=options.region,
|
||||
insecure=options.insecure)
|
||||
|
||||
|
||||
def create_options(parser):
|
||||
"""
|
||||
Sets up the CLI and config-file options that may be
|
||||
parsed and program commands.
|
||||
|
||||
:param parser: The option parser
|
||||
"""
|
||||
parser.add_option('-v', '--verbose', default=False, action="store_true",
|
||||
help="Print more verbose output")
|
||||
parser.add_option('-d', '--debug', default=False, action="store_true",
|
||||
help="Print more verbose output")
|
||||
parser.add_option('-H', '--host', metavar="ADDRESS", default="0.0.0.0",
|
||||
help="Address of heat API host. "
|
||||
"Default: %default")
|
||||
parser.add_option('-p', '--port', dest="port", metavar="PORT",
|
||||
type=int, default=DEFAULT_PORT,
|
||||
help="Port the heat API host listens on. "
|
||||
"Default: %default")
|
||||
parser.add_option('-U', '--url', metavar="URL", default=None,
|
||||
help="URL of heat service. This option can be used "
|
||||
"to specify the hostname, port and protocol "
|
||||
"(http/https) of the heat server, for example "
|
||||
"-U https://localhost:" + str(DEFAULT_PORT) +
|
||||
"/v1 Default: No<F3>ne")
|
||||
parser.add_option('-k', '--insecure', dest="insecure",
|
||||
default=False, action="store_true",
|
||||
help="Explicitly allow heat to perform \"insecure\" "
|
||||
"SSL (https) requests. The server's certificate will "
|
||||
"not be verified against any certificate authorities. "
|
||||
"This option should be used with caution.")
|
||||
parser.add_option('-A', '--auth_token', dest="auth_token",
|
||||
metavar="TOKEN", default=None,
|
||||
help="Authentication token to use to identify the "
|
||||
"client to the heat server")
|
||||
parser.add_option('-I', '--username', dest="username",
|
||||
metavar="USER", default=None,
|
||||
help="User name used to acquire an authentication token")
|
||||
parser.add_option('-K', '--password', dest="password",
|
||||
metavar="PASSWORD", default=None,
|
||||
help="Password used to acquire an authentication token")
|
||||
parser.add_option('-R', '--region', dest="region",
|
||||
metavar="REGION", default=None,
|
||||
help="Region name. When using keystone authentication "
|
||||
"version 2.0 or later this identifies the region "
|
||||
"name to use when selecting the service endpoint. A "
|
||||
"region name must be provided if more than one "
|
||||
"region endpoint is available")
|
||||
parser.add_option('-N', '--auth_url', dest="auth_url",
|
||||
metavar="AUTH_URL", default=None,
|
||||
help="Authentication URL")
|
||||
parser.add_option('-S', '--auth_strategy', dest="auth_strategy",
|
||||
metavar="STRATEGY", default=None,
|
||||
help="Authentication strategy (keystone or noauth)")
|
||||
|
||||
parser.add_option('-u', '--template-url', metavar="template_url", default=None,
|
||||
help="URL of template. Default: None")
|
||||
parser.add_option('-t', '--template-file', metavar="template_file", default=None,
|
||||
help="Path to the template. Default: None")
|
||||
|
||||
parser.add_option('-P', '--parameters', metavar="parameters", default=None,
|
||||
help="Parameter values used to create the stack.")
|
||||
|
||||
def parse_options(parser, cli_args):
|
||||
"""
|
||||
Returns the parsed CLI options, command to run and its arguments, merged
|
||||
with any same-named options found in a configuration file
|
||||
|
||||
:param parser: The option parser
|
||||
"""
|
||||
if not cli_args:
|
||||
cli_args.append('-h') # Show options in usage output...
|
||||
|
||||
(options, args) = parser.parse_args(cli_args)
|
||||
if options.url is not None:
|
||||
u = urlparse(options.url)
|
||||
options.port = u.port
|
||||
options.host = u.hostname
|
||||
|
||||
options.use_ssl = (options.url is not None and u.scheme == 'https')
|
||||
|
||||
# HACK(sirp): Make the parser available to the print_help method
|
||||
# print_help is a command, so it only accepts (options, args); we could
|
||||
# one-off have it take (parser, options, args), however, for now, I think
|
||||
# this little hack will suffice
|
||||
options.__parser = parser
|
||||
|
||||
if not args:
|
||||
parser.print_usage()
|
||||
sys.exit(0)
|
||||
|
||||
command_name = args.pop(0)
|
||||
command = lookup_command(parser, command_name)
|
||||
|
||||
return (options, command, args)
|
||||
|
||||
|
||||
def print_help(options, args):
|
||||
"""
|
||||
Print help specific to a command
|
||||
"""
|
||||
if len(args) != 1:
|
||||
sys.exit("Please specify a command")
|
||||
|
||||
parser = options.__parser
|
||||
command_name = args.pop()
|
||||
command = lookup_command(parser, command_name)
|
||||
|
||||
print command.__doc__ % {'prog': os.path.basename(sys.argv[0])}
|
||||
|
||||
|
||||
def lookup_command(parser, command_name):
|
||||
base_commands = {'help': print_help}
|
||||
|
||||
image_commands = {
|
||||
'create': stack_create,
|
||||
'update': stack_update,
|
||||
'delete': stack_delete,
|
||||
'list': stack_list,
|
||||
'validate': template_validate,
|
||||
'gettemplate': get_template,
|
||||
'describe': stack_describe}
|
||||
|
||||
commands = {}
|
||||
for command_set in (base_commands, image_commands):
|
||||
commands.update(command_set)
|
||||
|
||||
try:
|
||||
command = commands[command_name]
|
||||
except KeyError:
|
||||
parser.print_usage()
|
||||
sys.exit("Unknown command: %s" % command_name)
|
||||
|
||||
return command
|
||||
|
||||
def main():
|
||||
'''
|
||||
'''
|
||||
usage = """
|
||||
%prog <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
|
||||
|
||||
validate Validate a template
|
||||
|
||||
"""
|
||||
|
||||
oparser = optparse.OptionParser(version='%%prog %s'
|
||||
% version.version_string(),
|
||||
usage=usage.strip())
|
||||
create_options(oparser)
|
||||
(opts, cmd, args) = parse_options(oparser, sys.argv[1:])
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
result = cmd(opts, args)
|
||||
end_time = time.time()
|
||||
if opts.verbose:
|
||||
print "Completed in %-0.4f sec." % (end_time - start_time)
|
||||
sys.exit(result)
|
||||
except (RuntimeError,
|
||||
NotImplementedError,
|
||||
exception.ClientConfigurationError), ex:
|
||||
oparser.print_usage()
|
||||
print >> sys.stderr, "ERROR: ", ex
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Heat API Server
|
||||
"""
|
||||
|
||||
import gettext
|
||||
import os
|
||||
import sys
|
||||
|
||||
# If ../heat/__init__.py exists, add ../ to Python search path, so that
|
||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(possible_topdir, 'heat', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
gettext.install('heat', unicode=1)
|
||||
|
||||
from heat.common import config
|
||||
from heat.common import wsgi
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
conf = config.HeatConfigOpts()
|
||||
conf()
|
||||
|
||||
app = config.load_paste_app(conf)
|
||||
|
||||
server = wsgi.Server()
|
||||
server.start(app, conf, default_port=9292)
|
||||
server.wait()
|
||||
except RuntimeError, e:
|
||||
sys.exit("ERROR: %s" % e)
|
|
@ -0,0 +1,25 @@
|
|||
[DEFAULT]
|
||||
# Show more verbose log output (sets INFO log level output)
|
||||
verbose = True
|
||||
|
||||
# Show debugging output in logs (sets DEBUG log level output)
|
||||
debug = True
|
||||
|
||||
# Address to bind the server to
|
||||
bind_host = 0.0.0.0
|
||||
|
||||
# Port the bind the server to
|
||||
bind_port = 8000
|
||||
|
||||
# Log to this file. Make sure the user running heat-api has
|
||||
# permissions to write to this file!
|
||||
log_file = /var/log/heat/api.log
|
||||
|
||||
# ================= Syslog Options ============================
|
||||
|
||||
# Send logs to syslog (/dev/log) instead of to file specified
|
||||
# by `log_file`
|
||||
use_syslog = False
|
||||
|
||||
# Facility to use. If unset defaults to LOG_USER.
|
||||
# syslog_log_facility = LOG_LOCAL0
|
|
@ -0,0 +1,80 @@
|
|||
# Default minimal pipeline
|
||||
[pipeline:heat-api]
|
||||
pipeline = versionnegotiation context apiv1app
|
||||
|
||||
# Use the following pipeline for keystone auth
|
||||
# i.e. in heat-api.conf:
|
||||
# [paste_deploy]
|
||||
# flavor = keystone
|
||||
#
|
||||
[pipeline:heat-api-keystone]
|
||||
pipeline = versionnegotiation authtoken auth-context apiv1app
|
||||
|
||||
# Use the following pipeline to enable transparent caching of image files
|
||||
# i.e. in heat-api.conf:
|
||||
# [paste_deploy]
|
||||
# flavor = caching
|
||||
#
|
||||
[pipeline:heat-api-caching]
|
||||
pipeline = versionnegotiation context cache apiv1app
|
||||
|
||||
# Use the following pipeline for keystone auth with caching
|
||||
# i.e. in heat-api.conf:
|
||||
# [paste_deploy]
|
||||
# flavor = keystone+caching
|
||||
#
|
||||
[pipeline:heat-api-keystone+caching]
|
||||
pipeline = versionnegotiation authtoken auth-context cache apiv1app
|
||||
|
||||
# Use the following pipeline to enable the Image Cache Management API
|
||||
# i.e. in heat-api.conf:
|
||||
# [paste_deploy]
|
||||
# flavor = cachemanagement
|
||||
#
|
||||
[pipeline:heat-api-cachemanagement]
|
||||
pipeline = versionnegotiation context cache cachemanage apiv1app
|
||||
|
||||
# Use the following pipeline for keystone auth with cache management
|
||||
# i.e. in heat-api.conf:
|
||||
# [paste_deploy]
|
||||
# flavor = keystone+cachemanagement
|
||||
#
|
||||
[pipeline:heat-api-keystone+cachemanagement]
|
||||
pipeline = versionnegotiation authtoken auth-context cache cachemanage apiv1app
|
||||
|
||||
[app:apiv1app]
|
||||
paste.app_factory = heat.common.wsgi:app_factory
|
||||
heat.app_factory = heat.api.v1.router:API
|
||||
|
||||
[filter:versionnegotiation]
|
||||
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||
heat.filter_factory = heat.api.middleware.version_negotiation:VersionNegotiationFilter
|
||||
|
||||
[filter:cache]
|
||||
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||
heat.filter_factory = heat.api.middleware.cache:CacheFilter
|
||||
|
||||
[filter:cachemanage]
|
||||
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||
heat.filter_factory = heat.api.middleware.cache_manage:CacheManageFilter
|
||||
|
||||
[filter:context]
|
||||
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||
heat.filter_factory = heat.common.context:ContextMiddleware
|
||||
|
||||
[filter:authtoken]
|
||||
paste.filter_factory = keystone.middleware.auth_token:filter_factory
|
||||
service_protocol = http
|
||||
service_host = 127.0.0.1
|
||||
service_port = 5000
|
||||
auth_host = 127.0.0.1
|
||||
auth_port = 35357
|
||||
auth_protocol = http
|
||||
auth_uri = http://127.0.0.1:5000/
|
||||
admin_tenant_name = %SERVICE_TENANT_NAME%
|
||||
admin_user = %SERVICE_USER%
|
||||
admin_password = %SERVICE_PASSWORD%
|
||||
|
||||
[filter:auth-context]
|
||||
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||
heat.filter_factory = keystone.middleware.heat_auth_token:KeystoneContextMiddleware
|
|
@ -0,0 +1,20 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010-2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import gettext
|
||||
|
||||
gettext.install('heat', unicode=1)
|
|
@ -0,0 +1,16 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
|
@ -0,0 +1,16 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
|
@ -0,0 +1,64 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Middleware that attaches a context to the WSGI request
|
||||
"""
|
||||
|
||||
from heat.common import utils
|
||||
from heat.common import wsgi
|
||||
from heat.common import context
|
||||
|
||||
|
||||
class ContextMiddleware(wsgi.Middleware):
|
||||
def __init__(self, app, options):
|
||||
self.options = options
|
||||
super(ContextMiddleware, self).__init__(app)
|
||||
|
||||
def make_context(self, *args, **kwargs):
|
||||
"""
|
||||
Create a context with the given arguments.
|
||||
"""
|
||||
|
||||
# Determine the context class to use
|
||||
ctxcls = context.RequestContext
|
||||
if 'context_class' in self.options:
|
||||
ctxcls = utils.import_class(self.options['context_class'])
|
||||
|
||||
return ctxcls(*args, **kwargs)
|
||||
|
||||
def process_request(self, req):
|
||||
"""
|
||||
Extract any authentication information in the request and
|
||||
construct an appropriate context from it.
|
||||
"""
|
||||
# Use the default empty context, with admin turned on for
|
||||
# backwards compatibility
|
||||
req.context = self.make_context(is_admin=True)
|
||||
|
||||
|
||||
def filter_factory(global_conf, **local_conf):
|
||||
"""
|
||||
Factory method for paste.deploy
|
||||
"""
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
|
||||
def filter(app):
|
||||
return ContextMiddleware(app, conf)
|
||||
|
||||
return filter
|
|
@ -0,0 +1,123 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
A filter middleware that inspects the requested URI for a version string
|
||||
and/or Accept headers and attempts to negotiate an API controller to
|
||||
return
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
import routes
|
||||
|
||||
from heat.api import v1
|
||||
from heat.api import versions
|
||||
from heat.common import wsgi
|
||||
|
||||
logger = logging.getLogger('heat.api.middleware.version_negotiation')
|
||||
|
||||
|
||||
class VersionNegotiationFilter(wsgi.Middleware):
|
||||
|
||||
def __init__(self, app, conf, **local_conf):
|
||||
self.versions_app = versions.Controller(conf)
|
||||
self.version_uri_regex = re.compile(r"^v(\d+)\.?(\d+)?")
|
||||
self.conf = conf
|
||||
super(VersionNegotiationFilter, self).__init__(app)
|
||||
|
||||
def process_request(self, req):
|
||||
"""
|
||||
If there is a version identifier in the URI, simply
|
||||
return the correct API controller, otherwise, if we
|
||||
find an Accept: header, process it
|
||||
"""
|
||||
# See if a version identifier is in the URI passed to
|
||||
# us already. If so, simply return the right version
|
||||
# API controller
|
||||
msg = _("Processing request: %(method)s %(path)s Accept: "
|
||||
"%(accept)s") % ({'method': req.method,
|
||||
'path': req.path, 'accept': req.accept})
|
||||
logger.debug(msg)
|
||||
|
||||
# If the request is for /versions, just return the versions container
|
||||
if req.path_info_peek() == "versions":
|
||||
return self.versions_app
|
||||
|
||||
match = self._match_version_string(req.path_info_peek(), req)
|
||||
if match:
|
||||
if (req.environ['api.major_version'] == 1 and
|
||||
req.environ['api.minor_version'] == 0):
|
||||
logger.debug(_("Matched versioned URI. Version: %d.%d"),
|
||||
req.environ['api.major_version'],
|
||||
req.environ['api.minor_version'])
|
||||
# Strip the version from the path
|
||||
req.path_info_pop()
|
||||
return None
|
||||
else:
|
||||
logger.debug(_("Unknown version in versioned URI: %d.%d. "
|
||||
"Returning version choices."),
|
||||
req.environ['api.major_version'],
|
||||
req.environ['api.minor_version'])
|
||||
return self.versions_app
|
||||
|
||||
accept = str(req.accept)
|
||||
if accept.startswith('application/vnd.openstack.images-'):
|
||||
token_loc = len('application/vnd.openstack.images-')
|
||||
accept_version = accept[token_loc:]
|
||||
match = self._match_version_string(accept_version, req)
|
||||
if match:
|
||||
if (req.environ['api.major_version'] == 1 and
|
||||
req.environ['api.minor_version'] == 0):
|
||||
logger.debug(_("Matched versioned media type. "
|
||||
"Version: %d.%d"),
|
||||
req.environ['api.major_version'],
|
||||
req.environ['api.minor_version'])
|
||||
return None
|
||||
else:
|
||||
logger.debug(_("Unknown version in accept header: %d.%d..."
|
||||
"returning version choices."),
|
||||
req.environ['api.major_version'],
|
||||
req.environ['api.minor_version'])
|
||||
return self.versions_app
|
||||
else:
|
||||
if req.accept not in ('*/*', ''):
|
||||
logger.debug(_("Unknown accept header: %s..."
|
||||
"returning version choices."), req.accept)
|
||||
return self.versions_app
|
||||
return None
|
||||
|
||||
def _match_version_string(self, subject, req):
|
||||
"""
|
||||
Given a subject string, tries to match a major and/or
|
||||
minor version number. If found, sets the api.major_version
|
||||
and api.minor_version environ variables.
|
||||
|
||||
Returns True if there was a match, false otherwise.
|
||||
|
||||
:param subject: The string to check
|
||||
:param req: Webob.Request object
|
||||
"""
|
||||
match = self.version_uri_regex.match(subject)
|
||||
if match:
|
||||
major_version, minor_version = match.groups(0)
|
||||
major_version = int(major_version)
|
||||
minor_version = int(minor_version)
|
||||
req.environ['api.major_version'] = major_version
|
||||
req.environ['api.minor_version'] = minor_version
|
||||
return match is not None
|
|
@ -0,0 +1,21 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
SUPPORTED_PARAMS = ('StackName', 'TemplateBody', 'NotificationARNs', 'Parameters',
|
||||
'Version', 'SignatureVersion', 'Timestamp', 'AWSAccessKeyId',
|
||||
'Signature')
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
import routes
|
||||
|
||||
from heat.api.v1 import stacks
|
||||
from heat.common import wsgi
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class API(wsgi.Router):
|
||||
|
||||
"""WSGI router for Heat v1 API requests."""
|
||||
#TODO
|
||||
#DeleteStack
|
||||
#GetTemplate
|
||||
#UpdateStack
|
||||
#ValidateTemplate
|
||||
|
||||
|
||||
def __init__(self, conf, **local_conf):
|
||||
self.conf = conf
|
||||
mapper = routes.Mapper()
|
||||
|
||||
stacks_resource = stacks.create_resource(conf)
|
||||
|
||||
mapper.resource("stack", "stacks", controller=stacks_resource,
|
||||
collection={'detail': 'GET'})
|
||||
|
||||
mapper.connect("/CreateStack", controller=stacks_resource,
|
||||
action="create", conditions=dict(method=["POST"]))
|
||||
mapper.connect("/", controller=stacks_resource, action="index")
|
||||
mapper.connect("/ListStacks", controller=stacks_resource,
|
||||
action="list", conditions=dict(method=["GET"]))
|
||||
mapper.connect("/DescribeStacks", controller=stacks_resource,
|
||||
action="show", conditions=dict(method=["GET"]))
|
||||
|
||||
super(API, self).__init__(mapper)
|
|
@ -0,0 +1,157 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
/stack endpoint for heat v1 API
|
||||
"""
|
||||
|
||||
import httplib
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import webob
|
||||
from webob.exc import (HTTPNotFound,
|
||||
HTTPConflict,
|
||||
HTTPBadRequest)
|
||||
|
||||
from heat.common import exception
|
||||
from heat.common import wsgi
|
||||
|
||||
logger = logging.getLogger('heat.api.v1.stacks')
|
||||
|
||||
class StackController(object):
|
||||
|
||||
"""
|
||||
WSGI controller for stacks resource in heat v1 API
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = options
|
||||
|
||||
def list(self, req):
|
||||
"""
|
||||
Returns the following information for all stacks:
|
||||
"""
|
||||
return {'ListStacksResponse': [
|
||||
{'ListStacksResult': [
|
||||
{'StackSummaries': [
|
||||
{'member': [
|
||||
{'StackId': 'arn:aws:cloudformation:us-east-1:1234567:stack/TestCreate1/aaaaa',
|
||||
'StackStatus': 'CREATE_IN_PROGRESS',
|
||||
'StackName': 'vpc1',
|
||||
'CreationTime': '2011-05-23T15:47:44Z',
|
||||
'TemplateDescription': 'Creates one EC2 instance and a load balancer.',
|
||||
}]
|
||||
},
|
||||
{'member': [
|
||||
{'StackId': 'arn:aws:cloudformation:us-east-1:1234567:stack/TestDelete2/bbbbb',
|
||||
'StackStatus': 'DELETE_COMPLETE',
|
||||
'StackName': 'WP1',
|
||||
'CreationTime': '2011-03-05T19:57:58Z',
|
||||
'TemplateDescription': 'A simple basic Cloudformation Template.',
|
||||
}]
|
||||
}
|
||||
]}]}]}
|
||||
|
||||
|
||||
def describe(self, req):
|
||||
|
||||
return {'stack': [
|
||||
{'id': 'id',
|
||||
'name': '<stack NAME',
|
||||
'disk_format': '<DISK_FORMAT>',
|
||||
'container_format': '<CONTAINER_FORMAT>' } ] }
|
||||
|
||||
|
||||
def create(self, req):
|
||||
for p in req.params:
|
||||
print 'create %s=%s' % (p, req.params[p])
|
||||
|
||||
return {'CreateStackResult': [{'StackId': '007'}]}
|
||||
|
||||
def update(self, req, id, image_meta, image_data):
|
||||
"""
|
||||
Updates an existing image with the registry.
|
||||
|
||||
:param request: The WSGI/Webob Request object
|
||||
:param id: The opaque image identifier
|
||||
|
||||
:retval Returns the updated image information as a mapping
|
||||
"""
|
||||
|
||||
return {'image_meta': 'bla'}
|
||||
|
||||
|
||||
def delete(self, req, id):
|
||||
"""
|
||||
Deletes the image and all its chunks from heat
|
||||
|
||||
:param req: The WSGI/Webob Request object
|
||||
:param id: The opaque image identifier
|
||||
|
||||
:raises HttpBadRequest if image registry is invalid
|
||||
:raises HttpNotFound if image or any chunk is not available
|
||||
:raises HttpNotAuthorized if image or any chunk is not
|
||||
deleteable by the requesting user
|
||||
"""
|
||||
|
||||
|
||||
class StackDeserializer(wsgi.JSONRequestDeserializer):
|
||||
"""Handles deserialization of specific controller method requests."""
|
||||
|
||||
def _deserialize(self, request):
|
||||
result = {}
|
||||
return result
|
||||
|
||||
def create(self, request):
|
||||
return self._deserialize(request)
|
||||
|
||||
def update(self, request):
|
||||
return self._deserialize(request)
|
||||
|
||||
|
||||
class StackSerializer(wsgi.JSONResponseSerializer):
|
||||
"""Handles serialization of specific controller method responses."""
|
||||
|
||||
def _inject_location_header(self, response, image_meta):
|
||||
response.headers['Location'] = 'location'
|
||||
|
||||
def _inject_checksum_header(self, response, image_meta):
|
||||
response.headers['ETag'] = 'checksum'
|
||||
|
||||
def update(self, response, result):
|
||||
return
|
||||
|
||||
def create(self, response, result):
|
||||
""" Create """
|
||||
response.status = 201
|
||||
response.headers['Content-Type'] = 'application/json'
|
||||
response.body = self.to_json(dict(CreateStackResult=result))
|
||||
self._inject_location_header(response, result)
|
||||
self._inject_checksum_header(response, result)
|
||||
return response
|
||||
|
||||
def handle_stack(self, req, id):
|
||||
return {'got-stack-id': id}
|
||||
|
||||
def create_resource(options):
|
||||
"""Stacks resource factory method"""
|
||||
deserializer = StackDeserializer()
|
||||
serializer = StackSerializer()
|
||||
return wsgi.Resource(StackController(options), deserializer, serializer)
|
|
@ -0,0 +1,68 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Controller that returns information on the heat API versions
|
||||
"""
|
||||
|
||||
import httplib
|
||||
import json
|
||||
|
||||
import webob.dec
|
||||
|
||||
from heat.common import wsgi
|
||||
|
||||
|
||||
class Controller(object):
|
||||
|
||||
"""
|
||||
A controller that produces information on the heat API versions.
|
||||
"""
|
||||
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
"""Respond to a request for all OpenStack API versions."""
|
||||
version_objs = [
|
||||
{
|
||||
"id": "v1.1",
|
||||
"status": "CURRENT",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": self.get_href(req)}]},
|
||||
{
|
||||
"id": "v1.0",
|
||||
"status": "SUPPORTED",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": self.get_href(req)}]}]
|
||||
|
||||
body = json.dumps(dict(versions=version_objs))
|
||||
|
||||
response = webob.Response(request=req,
|
||||
status=httplib.MULTIPLE_CHOICES,
|
||||
content_type='application/json')
|
||||
response.body = body
|
||||
|
||||
return response
|
||||
|
||||
def get_href(self, req):
|
||||
return "%s/v1/" % req.host_url
|
|
@ -0,0 +1,133 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010-2011 OpenStack, LLC
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Client classes for callers of a heat system
|
||||
"""
|
||||
|
||||
import errno
|
||||
import httplib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
import heat.api.v1
|
||||
from heat.common import client as base_client
|
||||
from heat.common import exception
|
||||
from heat.common import utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
SUPPORTED_PARAMS = heat.api.v1.SUPPORTED_PARAMS
|
||||
|
||||
|
||||
class V1Client(base_client.BaseClient):
|
||||
|
||||
"""Main client class for accessing heat resources"""
|
||||
|
||||
DEFAULT_PORT = 8000
|
||||
DEFAULT_DOC_ROOT = "/v1"
|
||||
|
||||
def _insert_common_parameters(self, params):
|
||||
params['Version'] = '2010-05-15'
|
||||
params['SignatureVersion'] = '2'
|
||||
params['SignatureMethod'] = 'HmacSHA256'
|
||||
|
||||
def list_stacks(self, **kwargs):
|
||||
params = self._extract_params({}, SUPPORTED_PARAMS)
|
||||
self._insert_common_parameters(params)
|
||||
|
||||
res = self.do_request("GET", "/ListStacks", params=params)
|
||||
data = json.loads(res.read())
|
||||
return data
|
||||
|
||||
def show_stack(self, **kwargs):
|
||||
params = self._extract_params(kwargs, SUPPORTED_PARAMS)
|
||||
self._insert_common_parameters(params)
|
||||
|
||||
res = self.do_request("GET", "/DescribeStacks", params=params)
|
||||
data = json.loads(res.read())
|
||||
return data
|
||||
|
||||
def create_stack(self, **kwargs):
|
||||
|
||||
params = self._extract_params(kwargs, SUPPORTED_PARAMS)
|
||||
self._insert_common_parameters(params)
|
||||
res = self.do_request("POST", "/CreateStack", params=params)
|
||||
|
||||
data = json.loads(res.read())
|
||||
return data
|
||||
|
||||
def update_stack(self, **kwargs):
|
||||
return
|
||||
|
||||
def delete_stack(self, **kwargs):
|
||||
self._insert_common_parameters(params)
|
||||
params = self._extract_params(kwargs, SUPPORTED_PARAMS)
|
||||
self.do_request("DELETE", "/DeleteStack", params)
|
||||
return True
|
||||
|
||||
Client = 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 or os.getenv('OS_AUTH_URL'):
|
||||
force_strategy = 'keystone'
|
||||
else:
|
||||
force_strategy = None
|
||||
|
||||
creds = dict(username=username or
|
||||
os.getenv('OS_AUTH_USER', os.getenv('OS_USERNAME')),
|
||||
password=password or
|
||||
os.getenv('OS_AUTH_KEY', os.getenv('OS_PASSWORD')),
|
||||
tenant=tenant or
|
||||
os.getenv('OS_AUTH_TENANT',
|
||||
os.getenv('OS_TENANT_NAME')),
|
||||
auth_url=auth_url or os.getenv('OS_AUTH_URL'),
|
||||
strategy=force_strategy or auth_strategy or
|
||||
os.getenv('OS_AUTH_STRATEGY', 'noauth'),
|
||||
region=region or os.getenv('OS_REGION_NAME'),
|
||||
)
|
||||
|
||||
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 = Client
|
||||
|
||||
return client(host=host,
|
||||
port=port,
|
||||
use_ssl=use_ssl,
|
||||
auth_tok=auth_token or
|
||||
os.getenv('OS_TOKEN'),
|
||||
creds=creds,
|
||||
insecure=insecure)
|
|
@ -0,0 +1,16 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010-2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
|
@ -0,0 +1,267 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
This 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
|
||||
|
||||
|
||||
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):
|
||||
self.creds = creds
|
||||
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 _ 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-image-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:
|
||||
raise Exception(_('Unexpected response: %s' % 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 'image' endpoint
|
||||
is considered a match. There must be one -- and
|
||||
only one -- successful match in the catalog,
|
||||
otherwise we will raise an exception.
|
||||
"""
|
||||
# FIXME(sirp): for now just use the public url.
|
||||