Initial commit (basics copied from glance)
Signed-off-by: Angus Salkeld <asalkeld@redhat.com>
This commit is contained in:
commit
3b9c41fb6c
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
*.pyc
|
||||
*.swp
|
||||
*.log
|
||||
build
|
||||
dist
|
||||
heat/vcsversion.py
|
176
LICENSE
Normal file
176
LICENSE
Normal file
@ -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.
|
||||
|
377
bin/heat
Executable file
377
bin/heat
Executable file
@ -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()
|
54
bin/heat-api
Executable file
54
bin/heat-api
Executable file
@ -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)
|
25
etc/heat-api
Normal file
25
etc/heat-api
Normal file
@ -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
|
80
etc/heat-api-paste.ini
Normal file
80
etc/heat-api-paste.ini
Normal file
@ -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
|
20
heat/__init__.py
Normal file
20
heat/__init__.py
Normal file
@ -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)
|
16
heat/api/__init__.py
Normal file
16
heat/api/__init__.py
Normal file
@ -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.
|
16
heat/api/middleware/__init__.py
Normal file
16
heat/api/middleware/__init__.py
Normal file
@ -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.
|
64
heat/api/middleware/context.py
Normal file
64
heat/api/middleware/context.py
Normal file
@ -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
|
123
heat/api/middleware/version_negotiation.py
Normal file
123
heat/api/middleware/version_negotiation.py
Normal file
@ -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
|
21
heat/api/v1/__init__.py
Normal file
21
heat/api/v1/__init__.py
Normal file
@ -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')
|
||||
|
54
heat/api/v1/router.py
Normal file
54
heat/api/v1/router.py
Normal file
@ -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)
|
157
heat/api/v1/stacks.py
Normal file
157
heat/api/v1/stacks.py
Normal file
@ -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)
|
68
heat/api/versions.py
Normal file
68
heat/api/versions.py
Normal file
@ -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
|
133
heat/client.py
Normal file
133
heat/client.py
Normal file
@ -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)
|
16
heat/common/__init__.py
Normal file
16
heat/common/__init__.py
Normal file
@ -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.
|
267
heat/common/auth.py
Normal file
267
heat/common/auth.py
Normal file
@ -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.
|
||||
endpoint = None
|
||||
region = self.creds.get('region')
|
||||
for service in service_catalog:
|
||||
if service['type'] == 'image':
|
||||
for ep in service['endpoints']:
|
||||
if region is None or region == ep['region']:
|
||||
if endpoint is not None:
|
||||
# This is a second match, abort
|
||||
raise exception.RegionAmbiguity(region=region)
|
||||
endpoint = ep
|
||||
if endpoint is None:
|
||||
raise exception.NoServiceEndpoint()
|
||||
return endpoint['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:
|
||||
raise Exception(_('Unexpected response: %s') % resp.status)
|
||||
|
||||
@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):
|
||||
if strategy == 'noauth':
|
||||
return NoAuthStrategy()
|
||||
elif strategy == 'keystone':
|
||||
return KeystoneStrategy(creds)
|
||||
else:
|
||||
raise Exception(_("Unknown auth strategy '%s'") % strategy)
|
1135
heat/common/cfg.py
Normal file
1135
heat/common/cfg.py
Normal file
File diff suppressed because it is too large
Load Diff
593
heat/common/client.py
Normal file
593
heat/common/client.py
Normal file
@ -0,0 +1,593 @@
|
||||
# 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.
|
||||
|
||||
# HTTPSClientAuthConnection code comes courtesy of ActiveState website:
|
||||
# http://code.activestate.com/recipes/
|
||||
# 577548-https-httplib-client-connection-with-certificate-v/
|
||||
|
||||
import collections
|
||||
import errno
|
||||
import functools
|
||||
import httplib
|
||||
import logging
|
||||
import os
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
try:
|
||||
from eventlet.green import socket, ssl
|
||||
except ImportError:
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
try:
|
||||
import sendfile
|
||||
SENDFILE_SUPPORTED = True
|
||||
except ImportError:
|
||||
SENDFILE_SUPPORTED = False
|
||||
|
||||
from heat.common import auth
|
||||
from heat.common import exception, utils
|
||||
|
||||
|
||||
# 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.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 SendFileIterator:
|
||||
"""
|
||||
Emulate iterator pattern over sendfile, in order to allow
|
||||
send progress be followed by wrapping the iteration.
|
||||
"""
|
||||
def __init__(self, connection, body):
|
||||
self.connection = connection
|
||||
self.body = body
|
||||
self.offset = 0
|
||||
self.sending = True
|
||||
|
||||
def __iter__(self):
|
||||
class OfLength:
|
||||
def __init__(self, len):
|
||||
self.len = len
|
||||
|
||||
def __len__(self):
|
||||
return self.len
|
||||
|
||||
while self.sending:
|
||||
sent = sendfile.sendfile(self.connection.sock.fileno(),
|
||||
self.body.fileno(),
|
||||
self.offset,
|
||||
CHUNKSIZE)
|
||||
self.sending = (sent != 0)
|
||||
self.offset += sent
|
||||
yield OfLength(sent)
|
||||
|
||||
|
||||
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, 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):
|
||||
"""
|
||||
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
|
||||
# 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'
|
||||
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)
|
||||
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)
|
||||
|
||||
if self._sendable(body):
|
||||
# send actual file without copying into userspace
|
||||
_sendbody(c, iter)
|
||||
else:
|
||||
# otherwise iterate and chunk
|
||||
_chunkbody(c, iter)
|
||||
else:
|
||||
raise TypeError('Unsupported image type: %s' % body.__class__)
|
||||
|
||||
res = c.getresponse()
|
||||
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(res.read())
|
||||
elif status_code == httplib.FORBIDDEN:
|
||||
raise exception.NotAuthorized(res.read())
|
||||
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(res.read())
|
||||
elif status_code == httplib.MULTIPLE_CHOICES:
|
||||
raise exception.MultipleChoices(body=res.read())
|
||||
elif status_code == httplib.INTERNAL_SERVER_ERROR:
|
||||
raise Exception("Internal Server error: %s" % res.read())
|
||||
else:
|
||||
raise Exception("Unknown error occurred! %s" % res.read())
|
||||
|
||||
except (socket.error, IOError), e:
|
||||
raise exception.ClientConnectionError(e)
|
||||
|
||||
def _seekable(self, body):
|
||||
# pipes are not seekable, avoids sendfile() failure on e.g.
|
||||
# cat /path/to/image | heat add ...
|
||||
# or where add command is launched via popen
|
||||
try:
|
||||
os.lseek(body.fileno(), 0, os.SEEK_SET)
|
||||
return True
|
||||
except OSError as e:
|
||||
return (e.errno != errno.ESPIPE)
|
||||
|
||||
def _sendable(self, body):
|
||||
return (SENDFILE_SUPPORTED and
|
||||
hasattr(body, 'fileno') and
|
||||
self._seekable(body) and
|
||||
not self.use_ssl)
|
||||
|
||||
def _iterable(self, body):
|
||||
return isinstance(body, collections.Iterable)
|
||||
|
||||
def image_iterator(self, connection, headers, body):
|
||||
if self._sendable(body):
|
||||
return SendFileIterator(connection, body)
|
||||
elif 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 allowed_param in allowed_params:
|
||||
# if allowed_param in actual_params:
|
||||
# result[allowed_param] = actual_params[allowed_param]
|
||||
|
||||
#return result
|
||||
|
||||
# allow user parameters
|
||||
return actual_params
|
184
heat/common/config.py
Normal file
184
heat/common/config.py
Normal file
@ -0,0 +1,184 @@
|
||||
#!/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.
|
||||
|
||||
"""
|
||||
Routines for configuring Heat
|
||||
"""
|
||||
|
||||
import logging
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
|
||||
from heat import version
|
||||
from heat.common import cfg
|
||||
from heat.common import wsgi
|
||||
|
||||
|
||||
paste_deploy_group = cfg.OptGroup('paste_deploy')
|
||||
paste_deploy_opts = [
|
||||
cfg.StrOpt('flavor'),
|
||||
cfg.StrOpt('config_file'),
|
||||
]
|
||||
|
||||
|
||||
class HeatConfigOpts(cfg.CommonConfigOpts):
|
||||
|
||||
def __init__(self, default_config_files=None, **kwargs):
|
||||
super(HeatConfigOpts, self).__init__(
|
||||
project='heat',
|
||||
version='%%prog %s' % version.version_string(),
|
||||
default_config_files=default_config_files,
|
||||
**kwargs)
|
||||
|
||||
|
||||
class HeatCacheConfigOpts(HeatConfigOpts):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
config_files = cfg.find_config_files(project='heat',
|
||||
prog='heat-cache')
|
||||
super(HeatCacheConfigOpts, self).__init__(config_files, **kwargs)
|
||||
|
||||
|
||||
def setup_logging(conf):
|
||||
"""
|
||||
Sets up the logging options for a log with supplied name
|
||||
|
||||
:param conf: a cfg.ConfOpts object
|
||||
"""
|
||||
|
||||
if conf.log_config:
|
||||
# Use a logging configuration file for all settings...
|
||||
if os.path.exists(conf.log_config):
|
||||
logging.config.fileConfig(conf.log_config)
|
||||
return
|
||||
else:
|
||||
raise RuntimeError("Unable to locate specified logging "
|
||||
"config file: %s" % conf.log_config)
|
||||
|
||||
root_logger = logging.root
|
||||
if conf.debug:
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
elif conf.verbose:
|
||||
root_logger.setLevel(logging.INFO)
|
||||
else:
|
||||
root_logger.setLevel(logging.WARNING)
|
||||
|
||||
formatter = logging.Formatter(conf.log_format, conf.log_date_format)
|
||||
|
||||
if conf.use_syslog:
|
||||
try:
|
||||
facility = getattr(logging.handlers.SysLogHandler,
|
||||
conf.syslog_log_facility)
|
||||
except AttributeError:
|
||||
raise ValueError(_("Invalid syslog facility"))
|
||||
|
||||
handler = logging.handlers.SysLogHandler(address='/dev/log',
|
||||
facility=facility)
|
||||
elif conf.log_file:
|
||||
logfile = conf.log_file
|
||||
if conf.log_dir:
|
||||
logfile = os.path.join(conf.log_dir, logfile)
|
||||
handler = logging.handlers.WatchedFileHandler(logfile)
|
||||
else:
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
|
||||
handler.setFormatter(formatter)
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
|
||||
def _register_paste_deploy_opts(conf):
|
||||
"""
|
||||
Idempotent registration of paste_deploy option group
|
||||
|
||||
:param conf: a cfg.ConfigOpts object
|
||||
"""
|
||||
conf.register_group(paste_deploy_group)
|
||||
conf.register_opts(paste_deploy_opts, group=paste_deploy_group)
|
||||
|
||||
|
||||
def _get_deployment_flavor(conf):
|
||||
"""
|
||||
Retrieve the paste_deploy.flavor config item, formatted appropriately
|
||||
for appending to the application name.
|
||||
|
||||
:param conf: a cfg.ConfigOpts object
|
||||
"""
|
||||
_register_paste_deploy_opts(conf)
|
||||
flavor = conf.paste_deploy.flavor
|
||||
return '' if not flavor else ('-' + flavor)
|
||||
|
||||
|
||||
def _get_deployment_config_file(conf):
|
||||
"""
|
||||
Retrieve the deployment_config_file config item, formatted as an
|
||||
absolute pathname.
|
||||
|
||||
:param conf: a cfg.ConfigOpts object
|
||||
"""
|
||||
_register_paste_deploy_opts(conf)
|
||||
config_file = conf.paste_deploy.config_file
|
||||
if not config_file:
|
||||
# Assume paste config is in a paste.ini file corresponding
|
||||
# to the last config file
|
||||
path = conf.config_file[-1].replace(".conf", "-paste.ini")
|
||||
else:
|
||||
path = config_file
|
||||
return os.path.abspath(path)
|
||||
|
||||
|
||||
def load_paste_app(conf, app_name=None):
|
||||
"""
|
||||
Builds and returns a WSGI app from a paste config file.
|
||||
|
||||
We assume the last config file specified in the supplied ConfigOpts
|
||||
object is the paste config file.
|
||||
|
||||
:param conf: a cfg.ConfigOpts object
|
||||
:param app_name: name of the application to load
|
||||
|
||||
:raises RuntimeError when config file cannot be located or application
|
||||
cannot be loaded from config file
|
||||
"""
|
||||
if app_name is None:
|
||||
app_name = conf.prog
|
||||
|
||||
# append the deployment flavor to the application name,
|
||||
# in order to identify the appropriate paste pipeline
|
||||
app_name += _get_deployment_flavor(conf)
|
||||
|
||||
conf_file = _get_deployment_config_file(conf)
|
||||
|
||||
try:
|
||||
# Setup logging early
|
||||
setup_logging(conf)
|
||||
|
||||
logger = logging.getLogger(app_name)
|
||||
|
||||
app = wsgi.paste_deploy_app(conf_file, app_name, conf)
|
||||
|
||||
# Log the options used when starting if we're in debug mode...
|
||||
if conf.debug:
|
||||
conf.log_opt_values(logging.getLogger(app_name), logging.DEBUG)
|
||||
|
||||
return app
|
||||
except (LookupError, ImportError), e:
|
||||
raise RuntimeError("Unable to load %(app_name)s from "
|
||||
"configuration file %(conf_file)s."
|
||||
"\nGot: %(e)r" % locals())
|
124
heat/common/context.py
Normal file
124
heat/common/context.py
Normal file
@ -0,0 +1,124 @@
|
||||
# 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.
|
||||
|
||||
from heat.common import cfg
|
||||
from heat.common import exception
|
||||
from heat.common import utils
|
||||
from heat.common import wsgi
|
||||
|
||||
|
||||
class RequestContext(object):
|
||||
"""
|
||||
Stores information about the security context under which the user
|
||||
accesses the system, as well as additional request information.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_tok=None, user=None, tenant=None, roles=None,
|
||||
is_admin=False, read_only=False, show_deleted=False,
|
||||
owner_is_tenant=True):
|
||||
self.auth_tok = auth_tok
|
||||
self.user = user
|
||||
self.tenant = tenant
|
||||
self.roles = roles or []
|
||||
self.is_admin = is_admin
|
||||
self.read_only = read_only
|
||||
self._show_deleted = show_deleted
|
||||
self.owner_is_tenant = owner_is_tenant
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
"""Return the owner to correlate with an image."""
|
||||
return self.tenant if self.owner_is_tenant else self.user
|
||||
|
||||
@property
|
||||
def show_deleted(self):
|
||||
"""Admins can see deleted by default"""
|
||||
if self._show_deleted or self.is_admin:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ContextMiddleware(wsgi.Middleware):
|
||||
|
||||
opts = [
|
||||
cfg.BoolOpt('owner_is_tenant', default=True),
|
||||
]
|
||||
|
||||
def __init__(self, app, conf, **local_conf):
|
||||
self.conf = conf
|
||||
self.conf.register_opts(self.opts)
|
||||
|
||||
# Determine the context class to use
|
||||
self.ctxcls = RequestContext
|
||||
if 'context_class' in local_conf:
|
||||
self.ctxcls = utils.import_class(local_conf['context_class'])
|
||||
|
||||
super(ContextMiddleware, self).__init__(app)
|
||||
|
||||
def make_context(self, *args, **kwargs):
|
||||
"""
|
||||
Create a context with the given arguments.
|
||||
"""
|
||||
kwargs.setdefault('owner_is_tenant', self.conf.owner_is_tenant)
|
||||
|
||||
return self.ctxcls(*args, **kwargs)
|
||||
|
||||
def process_request(self, req):
|
||||
"""
|
||||
Extract any authentication information in the request and
|
||||
construct an appropriate context from it.
|
||||
|
||||
A few scenarios exist:
|
||||
|
||||
1. If X-Auth-Token is passed in, then consult TENANT and ROLE headers
|
||||
to determine permissions.
|
||||
|
||||
2. An X-Auth-Token was passed in, but the Identity-Status is not
|
||||
confirmed. For now, just raising a NotAuthorized exception.
|
||||
|
||||
3. X-Auth-Token is omitted. If we were using Keystone, then the
|
||||
tokenauth middleware would have rejected the request, so we must be
|
||||
using NoAuth. In that case, assume that is_admin=True.
|
||||
"""
|
||||
# TODO(sirp): should we be using the heat_tokeauth shim from
|
||||
# Keystone here? If we do, we need to make sure it handles the NoAuth
|
||||
# case
|
||||
auth_tok = req.headers.get('X-Auth-Token',
|
||||
req.headers.get('X-Storage-Token'))
|
||||
if auth_tok:
|
||||
if req.headers.get('X-Identity-Status') == 'Confirmed':
|
||||
# 1. Auth-token is passed, check other headers
|
||||
user = req.headers.get('X-User')
|
||||
tenant = req.headers.get('X-Tenant')
|
||||
roles = [r.strip()
|
||||
for r in req.headers.get('X-Role', '').split(',')]
|
||||
is_admin = 'Admin' in roles
|
||||
else:
|
||||
# 2. Indentity-Status not confirmed
|
||||
# FIXME(sirp): not sure what the correct behavior in this case
|
||||
# is; just raising NotAuthorized for now
|
||||
raise exception.NotAuthorized()
|
||||
else:
|
||||
# 3. Auth-token is ommited, assume NoAuth
|
||||
user = None
|
||||
tenant = None
|
||||
roles = []
|
||||
is_admin = True
|
||||
|
||||
req.context = self.make_context(
|
||||
auth_tok=auth_tok, user=user, tenant=tenant, roles=roles,
|
||||
is_admin=is_admin)
|
70
heat/common/crypt.py
Normal file
70
heat/common/crypt.py
Normal file
@ -0,0 +1,70 @@
|
||||
#!/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.
|
||||
|
||||
"""
|
||||
Routines for URL-safe encrypting/decrypting
|
||||
"""
|
||||
|
||||
import base64
|
||||
import string
|
||||
import os
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto import Random
|
||||
from Crypto.Random import random
|
||||
|
||||
|
||||
def urlsafe_encrypt(key, plaintext, blocksize=16):
|
||||
"""
|
||||
Encrypts plaintext. Resulting ciphertext will contain URL-safe characters
|
||||
:param key: AES secret key
|
||||
:param plaintext: Input text to be encrypted
|
||||
:param blocksize: Non-zero integer multiple of AES blocksize in bytes (16)
|
||||
|
||||
:returns : Resulting ciphertext
|
||||
"""
|
||||
def pad(text):
|
||||
"""
|
||||
Pads text to be encrypted
|
||||
"""
|
||||
pad_length = (blocksize - len(text) % blocksize)
|
||||
sr = random.StrongRandom()
|
||||
pad = ''.join(chr(sr.randint(1, 0xFF)) for i in range(pad_length - 1))
|
||||
# We use chr(0) as a delimiter between text and padding
|
||||
return text + chr(0) + pad
|
||||
|
||||
# random initial 16 bytes for CBC
|
||||
init_vector = Random.get_random_bytes(16)
|
||||
cypher = AES.new(key, AES.MODE_CBC, init_vector)
|
||||
padded = cypher.encrypt(pad(str(plaintext)))
|
||||
return base64.urlsafe_b64encode(init_vector + padded)
|
||||
|
||||
|
||||
def urlsafe_decrypt(key, ciphertext):
|
||||
"""
|
||||
Decrypts URL-safe base64 encoded ciphertext
|
||||
:param key: AES secret key
|
||||
:param ciphertext: The encrypted text to decrypt
|
||||
|
||||
:returns : Resulting plaintext
|
||||
"""
|
||||
# Cast from unicode
|
||||
ciphertext = base64.urlsafe_b64decode(str(ciphertext))
|
||||
cypher = AES.new(key, AES.MODE_CBC, ciphertext[:16])
|
||||
padded = cypher.decrypt(ciphertext[16:])
|
||||
return padded[:padded.rfind(chr(0))]
|
193
heat/common/exception.py
Normal file
193
heat/common/exception.py
Normal file
@ -0,0 +1,193 @@
|
||||
# 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.
|
||||
|
||||
"""Heat exception subclasses"""
|
||||
|
||||
import urlparse
|
||||
|
||||
|
||||
class RedirectException(Exception):
|
||||
def __init__(self, url):
|
||||
self.url = urlparse.urlparse(url)
|
||||
|
||||
|
||||
class HeatException(Exception):
|
||||
"""
|
||||
Base Heat Exception
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a 'message' property. That message will get printf'd
|
||||
with the keyword arguments provided to the constructor.
|
||||
"""
|
||||
message = _("An unknown exception occurred")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self._error_string = self.message % kwargs
|
||||
except Exception:
|
||||
# at least get the core message out if something happened
|
||||
self._error_string = self.message
|
||||
if len(args) > 0:
|
||||
# If there is a non-kwarg parameter, assume it's the error
|
||||
# message or reason description and tack it on to the end
|
||||
# of the exception message
|
||||
# Convert all arguments into their string representations...
|
||||
args = ["%s" % arg for arg in args]
|
||||
self._error_string = (self._error_string +
|
||||
"\nDetails: %s" % '\n'.join(args))
|
||||
|
||||
def __str__(self):
|
||||
return self._error_string
|
||||
|
||||
|
||||
class MissingArgumentError(HeatException):
|
||||
message = _("Missing required argument.")
|
||||
|
||||
|
||||
class MissingCredentialError(HeatException):
|
||||
message = _("Missing required credential: %(required)s")
|
||||
|
||||
|
||||
class BadAuthStrategy(HeatException):
|
||||
message = _("Incorrect auth strategy, expected \"%(expected)s\" but "
|
||||
"received \"%(received)s\"")
|
||||
|
||||
|
||||
class NotFound(HeatException):
|
||||
message = _("An object with the specified identifier was not found.")
|
||||
|
||||
|
||||
class UnknownScheme(HeatException):
|
||||
message = _("Unknown scheme '%(scheme)s' found in URI")
|
||||
|
||||
|
||||
class BadStoreUri(HeatException):
|
||||
message = _("The Store URI %(uri)s was malformed. Reason: %(reason)s")
|
||||
|
||||
|
||||
class Duplicate(HeatException):
|
||||
message = _("An object with the same identifier already exists.")
|
||||
|
||||
|
||||
class StorageFull(HeatException):
|
||||
message = _("There is not enough disk space on the image storage media.")
|
||||
|
||||
|
||||
class StorageWriteDenied(HeatException):
|
||||
message = _("Permission to write image storage media denied.")
|
||||
|
||||
|
||||
class ImportFailure(HeatException):
|
||||
message = _("Failed to import requested object/class: '%(import_str)s'. "
|
||||
"Reason: %(reason)s")
|
||||
|
||||
|
||||
class AuthBadRequest(HeatException):
|
||||
message = _("Connect error/bad request to Auth service at URL %(url)s.")
|
||||
|
||||
|
||||
class AuthUrlNotFound(HeatException):
|
||||
message = _("Auth service at URL %(url)s not found.")
|
||||
|
||||
|
||||
class AuthorizationFailure(HeatException):
|
||||
message = _("Authorization failed.")
|
||||
|
||||
|
||||
class NotAuthorized(HeatException):
|
||||
message = _("You are not authorized to complete this action.")
|
||||
|
||||
|
||||
class NotAuthorizedPublicImage(NotAuthorized):
|
||||
message = _("You are not authorized to complete this action.")
|
||||
|
||||
|
||||
class Invalid(HeatException):
|
||||
message = _("Data supplied was not valid.")
|
||||
|
||||
|
||||
class AuthorizationRedirect(HeatException):
|
||||
message = _("Redirecting to %(uri)s for authorization.")
|
||||
|
||||
|
||||
class DatabaseMigrationError(HeatException):
|
||||
message = _("There was an error migrating the database.")
|
||||
|
||||
|
||||
class ClientConnectionError(HeatException):
|
||||
message = _("There was an error connecting to a server")
|
||||
|
||||
|
||||
class ClientConfigurationError(HeatException):
|
||||
message = _("There was an error configuring the client.")
|
||||
|
||||
|
||||
class MultipleChoices(HeatException):
|
||||
message = _("The request returned a 302 Multiple Choices. This generally "
|
||||
"means that you have not included a version indicator in a "
|
||||
"request URI.\n\nThe body of response returned:\n%(body)s")
|
||||
|
||||
|
||||
class InvalidContentType(HeatException):
|
||||
message = _("Invalid content type %(content_type)s")
|
||||
|
||||
|
||||
class BadRegistryConnectionConfiguration(HeatException):
|
||||
message = _("Registry was not configured correctly on API server. "
|
||||
"Reason: %(reason)s")
|
||||
|
||||
|
||||
class BadStoreConfiguration(HeatException):
|
||||
message = _("Store %(store_name)s could not be configured correctly. "
|
||||
"Reason: %(reason)s")
|
||||
|
||||
|
||||
class BadDriverConfiguration(HeatException):
|
||||
message = _("Driver %(driver_name)s could not be configured correctly. "
|
||||
"Reason: %(reason)s")
|
||||
|
||||
|
||||
class StoreDeleteNotSupported(HeatException):
|
||||
message = _("Deleting images from this store is not supported.")
|
||||
|
||||
|
||||
class StoreAddDisabled(HeatException):
|
||||
message = _("Configuration for store failed. Adding images to this "
|
||||
"store is disabled.")
|
||||
|
||||
|
||||
class InvalidNotifierStrategy(HeatException):
|
||||
message = _("'%(strategy)s' is not an available notifier strategy.")
|
||||
|
||||
|
||||
class MaxRedirectsExceeded(HeatException):
|
||||
message = _("Maximum redirects (%(redirects)s) was exceeded.")
|
||||
|
||||
|
||||
class InvalidRedirect(HeatException):
|
||||
message = _("Received invalid HTTP redirect.")
|
||||
|
||||
|
||||
class NoServiceEndpoint(HeatException):
|
||||
message = _("Response from Keystone does not contain a Heat endpoint.")
|
||||
|
||||
|
||||
class RegionAmbiguity(HeatException):
|
||||
message = _("Multiple 'image' service matches for region %(region)s. This "
|
||||
"generally means that a region is required and you have not "
|
||||
"supplied one.")
|
182
heat/common/policy.py
Normal file
182
heat/common/policy.py
Normal file
@ -0,0 +1,182 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 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.
|
||||
|
||||
"""Common Policy Engine Implementation"""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class NotAuthorized(Exception):
|
||||
pass
|
||||
|
||||
|
||||
_BRAIN = None
|
||||
|
||||
|
||||
def set_brain(brain):
|
||||
"""Set the brain used by enforce().
|
||||
|
||||
Defaults use Brain() if not set.
|
||||
|
||||
"""
|
||||
global _BRAIN
|
||||
_BRAIN = brain
|
||||
|
||||
|
||||
def reset():
|
||||
"""Clear the brain used by enforce()."""
|
||||
global _BRAIN
|
||||
_BRAIN = None
|
||||
|
||||
|
||||
def enforce(match_list, target_dict, credentials_dict):
|
||||
"""Enforces authorization of some rules against credentials.
|
||||
|
||||
:param match_list: nested tuples of data to match against
|
||||
The basic brain supports three types of match lists:
|
||||
1) rules
|
||||
looks like: ('rule:compute:get_instance',)
|
||||
Retrieves the named rule from the rules dict and recursively
|
||||
checks against the contents of the rule.
|
||||
2) roles
|
||||
looks like: ('role:compute:admin',)
|
||||
Matches if the specified role is in credentials_dict['roles'].
|
||||
3) generic
|
||||
('tenant_id:%(tenant_id)s',)
|
||||
Substitutes values from the target dict into the match using
|
||||
the % operator and matches them against the creds dict.
|
||||
|
||||
Combining rules:
|
||||
The brain returns True if any of the outer tuple of rules match
|
||||
and also True if all of the inner tuples match. You can use this to
|
||||
perform simple boolean logic. For example, the following rule would
|
||||
return True if the creds contain the role 'admin' OR the if the
|
||||
tenant_id matches the target dict AND the the creds contains the
|
||||
role 'compute_sysadmin':
|
||||
|
||||
{
|
||||
"rule:combined": (
|
||||
'role:admin',
|
||||
('tenant_id:%(tenant_id)s', 'role:compute_sysadmin')
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Note that rule and role are reserved words in the credentials match, so
|
||||
you can't match against properties with those names. Custom brains may
|
||||
also add new reserved words. For example, the HttpBrain adds http as a
|
||||
reserved word.
|
||||
|
||||
:param target_dict: dict of object properties
|
||||
Target dicts contain as much information as we can about the object being
|
||||
operated on.
|
||||
|
||||
:param credentials_dict: dict of actor properties
|
||||
Credentials dicts contain as much information as we can about the user
|
||||
performing the action.
|
||||
|
||||
:raises NotAuthorized if the check fails
|
||||
|
||||
"""
|
||||
global _BRAIN
|
||||
if not _BRAIN:
|
||||
_BRAIN = Brain()
|
||||
if not _BRAIN.check(match_list, target_dict, credentials_dict):
|
||||
raise NotAuthorized()
|
||||
|
||||
|
||||
class Brain(object):
|
||||
"""Implements policy checking."""
|
||||
@classmethod
|
||||
def load_json(cls, data, default_rule=None):
|
||||
"""Init a brain using json instead of a rules dictionary."""
|
||||
rules_dict = json.loads(data)
|
||||
return cls(rules=rules_dict, default_rule=default_rule)
|
||||
|
||||
def __init__(self, rules=None, default_rule=None):
|
||||
self.rules = rules or {}
|
||||
self.default_rule = default_rule
|
||||
|
||||
def add_rule(self, key, match):
|
||||
self.rules[key] = match
|
||||
|
||||
def _check(self, match, target_dict, cred_dict):
|
||||
match_kind, match_value = match.split(':', 1)
|
||||
try:
|
||||
f = getattr(self, '_check_%s' % match_kind)
|
||||
except AttributeError:
|
||||
if not self._check_generic(match, target_dict, cred_dict):
|
||||
return False
|
||||
else:
|
||||
if not f(match_value, target_dict, cred_dict):
|
||||
return False
|
||||
return True
|
||||
|
||||
def check(self, match_list, target_dict, cred_dict):
|
||||
"""Checks authorization of some rules against credentials.
|
||||
|
||||
Detailed description of the check with examples in policy.enforce().
|
||||
|
||||
:param match_list: nested tuples of data to match against
|
||||
:param target_dict: dict of object properties
|
||||
:param credentials_dict: dict of actor properties
|
||||
|
||||
:returns: True if the check passes
|
||||
|
||||
"""
|
||||
if not match_list:
|
||||
return True
|
||||
for and_list in match_list:
|
||||
if isinstance(and_list, basestring):
|
||||
and_list = (and_list,)
|
||||
if all([self._check(item, target_dict, cred_dict)
|
||||
for item in and_list]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_rule(self, match, target_dict, cred_dict):
|
||||
"""Recursively checks credentials based on the brains rules."""
|
||||
try:
|
||||
new_match_list = self.rules[match]
|
||||
except KeyError:
|
||||
if self.default_rule and match != self.default_rule:
|
||||
new_match_list = ('rule:%s' % self.default_rule,)
|
||||
else:
|
||||
return False
|
||||
|
||||
return self.check(new_match_list, target_dict, cred_dict)
|
||||
|
||||
def _check_role(self, match, target_dict, cred_dict):
|
||||
"""Check that there is a matching role in the cred dict."""
|
||||
return match in cred_dict['roles']
|
||||
|
||||
def _check_generic(self, match, target_dict, cred_dict):
|
||||
"""Check an individual match.
|
||||
|
||||
Matches look like:
|
||||
|
||||
tenant:%(tenant_id)s
|
||||
role:compute:admin
|
||||
|
||||
"""
|
||||
|
||||
# TODO(termie): do dict inspection via dot syntax
|
||||
match = match % target_dict
|
||||
key, value = match.split(':', 1)
|
||||
if key in cred_dict:
|
||||
return value == cred_dict[key]
|
||||
return False
|
372
heat/common/utils.py
Normal file
372
heat/common/utils.py
Normal file
@ -0,0 +1,372 @@
|
||||
# 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.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import errno
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import subprocess
|
||||
import socket
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import iso8601
|
||||
|
||||
from heat.common import exception
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def image_meta_to_http_headers(image_meta):
|
||||
"""
|
||||
Returns a set of image metadata into a dict
|
||||
of HTTP headers that can be fed to either a Webob
|
||||
Request object or an httplib.HTTP(S)Connection object
|
||||
|
||||
:param image_meta: Mapping of image metadata
|
||||
"""
|
||||
headers = {}
|
||||
for k, v in image_meta.items():
|
||||
if v is not None:
|
||||
if k == 'properties':
|
||||
for pk, pv in v.items():
|
||||
if pv is not None:
|
||||
headers["x-image-meta-property-%s"
|
||||
% pk.lower()] = unicode(pv)
|
||||
else:
|
||||
headers["x-image-meta-%s" % k.lower()] = unicode(v)
|
||||
return headers
|
||||
|
||||
|
||||
def add_features_to_http_headers(features, headers):
|
||||
"""
|
||||
Adds additional headers representing heat features to be enabled.
|
||||
|
||||
:param headers: Base set of headers
|
||||
:param features: Map of enabled features
|
||||
"""
|
||||
if features:
|
||||
for k, v in features.items():
|
||||
if v is not None:
|
||||
headers[k.lower()] = unicode(v)
|
||||
|
||||
|
||||
def get_image_meta_from_headers(response):
|
||||
"""
|
||||
Processes HTTP headers from a supplied response that
|
||||
match the x-image-meta and x-image-meta-property and
|
||||
returns a mapping of image metadata and properties
|
||||
|
||||
:param response: Response to process
|
||||
"""
|
||||
result = {}
|
||||
properties = {}
|
||||
|
||||
if hasattr(response, 'getheaders'): # httplib.HTTPResponse
|
||||
headers = response.getheaders()
|
||||
else: # webob.Response
|
||||
headers = response.headers.items()
|
||||
|
||||
for key, value in headers:
|
||||
key = str(key.lower())
|
||||
if key.startswith('x-image-meta-property-'):
|
||||
field_name = key[len('x-image-meta-property-'):].replace('-', '_')
|
||||
properties[field_name] = value or None
|
||||
elif key.startswith('x-image-meta-'):
|
||||
field_name = key[len('x-image-meta-'):].replace('-', '_')
|
||||
result[field_name] = value or None
|
||||
result['properties'] = properties
|
||||
if 'size' in result:
|
||||
try:
|
||||
result['size'] = int(result['size'])
|
||||
except ValueError:
|
||||
raise exception.Invalid
|
||||
for key in ('is_public', 'deleted', 'protected'):
|
||||
if key in result:
|
||||
result[key] = bool_from_header_value(result[key])
|
||||
return result
|
||||
|
||||
|
||||
def bool_from_header_value(value):
|
||||
"""
|
||||
Returns True if value is a boolean True or the
|
||||
string 'true', case-insensitive, False otherwise
|
||||
"""
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
elif isinstance(value, (basestring, unicode)):
|
||||
if str(value).lower() == 'true':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def bool_from_string(subject):
|
||||
"""
|
||||
Interpret a string as a boolean.
|
||||
|
||||
Any string value in:
|
||||
('True', 'true', 'On', 'on', '1')
|
||||
is interpreted as a boolean True.
|
||||
|
||||
Useful for JSON-decoded stuff and config file parsing
|
||||
"""
|
||||
if isinstance(subject, bool):
|
||||
return subject
|
||||
elif isinstance(subject, int):
|
||||
return subject == 1
|
||||
if hasattr(subject, 'startswith'): # str or unicode...
|
||||
if subject.strip().lower() in ('true', 'on', '1'):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def import_class(import_str):
|
||||
"""Returns a class from a string including module and class"""
|
||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||
try:
|
||||
__import__(mod_str)
|
||||
return getattr(sys.modules[mod_str], class_str)
|
||||
except (ImportError, ValueError, AttributeError), e:
|
||||
raise exception.ImportFailure(import_str=import_str,
|
||||
reason=e)
|
||||
|
||||
|
||||
def import_object(import_str):
|
||||
"""Returns an object including a module or module and class"""
|
||||
try:
|
||||
__import__(import_str)
|
||||
return sys.modules[import_str]
|
||||
except ImportError:
|
||||
cls = import_class(import_str)
|
||||
return cls()
|
||||
|
||||
|
||||
def generate_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def is_uuid_like(value):
|
||||
try:
|
||||
uuid.UUID(value)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def isotime(at=None):
|
||||
"""Stringify time in ISO 8601 format"""
|
||||
if not at:
|
||||
at = datetime.datetime.utcnow()
|
||||
str = at.strftime(TIME_FORMAT)
|
||||
tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC'
|
||||
str += ('Z' if tz == 'UTC' else tz)
|
||||
return str
|
||||
|
||||
|
||||
def parse_isotime(timestr):
|
||||
"""Parse time from ISO 8601 format"""
|
||||
try:
|
||||
return iso8601.parse_date(timestr)
|
||||
except iso8601.ParseError as e:
|
||||
raise ValueError(e.message)
|
||||
except TypeError as e:
|
||||
raise ValueError(e.message)
|
||||
|
||||
|
||||
def normalize_time(timestamp):
|
||||
"""Normalize time in arbitrary timezone to UTC"""
|
||||
offset = timestamp.utcoffset()
|
||||
return timestamp.replace(tzinfo=None) - offset if offset else timestamp
|
||||
|
||||
|
||||
def safe_mkdirs(path):
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError, e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
|
||||
def safe_remove(path):
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError, e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
|
||||
|
||||
class PrettyTable(object):
|
||||
"""Creates an ASCII art table for use in bin/heat
|
||||
|
||||
Example:
|
||||
|
||||
ID Name Size Hits
|
||||
--- ----------------- ------------ -----
|
||||
122 image 22 0
|
||||
"""
|
||||
def __init__(self):
|
||||
self.columns = []
|
||||
|
||||
def add_column(self, width, label="", just='l'):
|
||||
"""Add a column to the table
|
||||
|
||||
:param width: number of characters wide the column should be
|
||||
:param label: column heading
|
||||
:param just: justification for the column, 'l' for left,
|
||||
'r' for right
|
||||
"""
|
||||
self.columns.append((width, label, just))
|
||||
|
||||
def make_header(self):
|
||||
label_parts = []
|
||||
break_parts = []
|
||||
for width, label, _ in self.columns:
|
||||
# NOTE(sirp): headers are always left justified
|
||||
label_part = self._clip_and_justify(label, width, 'l')
|
||||
label_parts.append(label_part)
|
||||
|
||||
break_part = '-' * width
|
||||
break_parts.append(break_part)
|
||||
|
||||
label_line = ' '.join(label_parts)
|
||||
break_line = ' '.join(break_parts)
|
||||
return '\n'.join([label_line, break_line])
|
||||
|
||||
def make_row(self, *args):
|
||||
row = args
|
||||
row_parts = []
|
||||
for data, (width, _, just) in zip(row, self.columns):
|
||||
row_part = self._clip_and_justify(data, width, just)
|
||||
row_parts.append(row_part)
|
||||
|
||||
row_line = ' '.join(row_parts)
|
||||
return row_line
|
||||
|
||||
@staticmethod
|
||||
def _clip_and_justify(data, width, just):
|
||||
# clip field to column width
|
||||
clipped_data = str(data)[:width]
|
||||
|
||||
if just == 'r':
|
||||
# right justify
|
||||
justified = clipped_data.rjust(width)
|
||||
else:
|
||||
# left justify
|
||||
justified = clipped_data.ljust(width)
|
||||
|
||||
return justified
|
||||
|
||||
|
||||
def get_terminal_size():
|
||||
|
||||
def _get_terminal_size_posix():
|
||||
import fcntl
|
||||
import struct
|
||||
import termios
|
||||
|
||||
height_width = None
|
||||
|
||||
try:
|
||||
height_width = struct.unpack('hh', fcntl.ioctl(sys.stderr.fileno(),
|
||||
termios.TIOCGWINSZ,
|
||||
struct.pack('HH', 0, 0)))
|
||||
except:
|
||||
pass
|
||||
|
||||
if not height_width:
|
||||
try:
|
||||
p = subprocess.Popen(['stty', 'size'],
|
||||
shell=false,
|
||||
stdout=subprocess.PIPE)
|
||||
return tuple(int(x) for x in p.communicate()[0].split())
|
||||
except:
|
||||
pass
|
||||
|
||||
return height_width
|
||||
|
||||
def _get_terminal_size_win32():
|
||||
try:
|
||||
from ctypes import windll, create_string_buffer
|
||||
handle = windll.kernel32.GetStdHandle(-12)
|
||||
csbi = create_string_buffer(22)
|
||||
res = windll.kernel32.GetConsoleScreenBufferInfo(handle, csbi)
|
||||
except:
|
||||
return None
|
||||
if res:
|
||||
import struct
|
||||
unpack_tmp = struct.unpack("hhhhHhhhhhh", csbi.raw)
|
||||
(bufx, bufy, curx, cury, wattr,
|
||||
left, top, right, bottom, maxx, maxy) = unpack_tmp
|
||||
height = bottom - top + 1
|
||||
width = right - left + 1
|
||||
return (height, width)
|
||||
else:
|
||||
return None
|
||||
|
||||
def _get_terminal_size_unknownOS():
|
||||
raise NotImplementedError
|
||||
|
||||
func = {'posix': _get_terminal_size_posix,
|
||||
'win32': _get_terminal_size_win32}
|
||||
|
||||
height_width = func.get(platform.os.name, _get_terminal_size_unknownOS)()
|
||||
|
||||
if height_width == None:
|
||||
raise exception.Invalid()
|
||||
|
||||
for i in height_width:
|
||||
if not isinstance(i, int) or i <= 0:
|
||||
raise exception.Invalid()
|
||||
|
||||
return height_width[0], height_width[1]
|
649
heat/common/wsgi.py
Normal file
649
heat/common/wsgi.py
Normal file
@ -0,0 +1,649 @@
|
||||
# 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 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.
|
||||
|
||||
"""
|
||||
Utility methods for working with WSGI servers
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import errno
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
import eventlet
|
||||
import eventlet.greenio
|
||||
from eventlet.green import socket, ssl
|
||||
import eventlet.wsgi
|
||||
from paste import deploy
|
||||
import routes
|
||||
import routes.middleware
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from heat.common import cfg
|
||||
from heat.common import exception
|
||||
from heat.common import utils
|
||||
|
||||
|
||||
bind_opts = [
|
||||
cfg.StrOpt('bind_host', default='0.0.0.0'),
|
||||
cfg.IntOpt('bind_port'),
|
||||
]
|
||||
|
||||
socket_opts = [
|
||||
cfg.IntOpt('backlog', default=4096),
|
||||
cfg.StrOpt('cert_file'),
|
||||
cfg.StrOpt('key_file'),
|
||||
]
|
||||
|
||||
workers_opt = cfg.IntOpt('workers', default=0)
|
||||
|
||||
|
||||
class WritableLogger(object):
|
||||
"""A thin wrapper that responds to `write` and logs."""
|
||||
|
||||
def __init__(self, logger, level=logging.DEBUG):
|
||||
self.logger = logger
|
||||
self.level = level
|
||||
|
||||
def write(self, msg):
|
||||
self.logger.log(self.level, msg.strip("\n"))
|
||||
|
||||
|
||||
def get_bind_addr(conf, default_port=None):
|
||||
"""Return the host and port to bind to."""
|
||||
conf.register_opts(bind_opts)
|
||||
return (conf.bind_host, conf.bind_port or default_port)
|
||||
|
||||
|
||||
def get_socket(conf, default_port):
|
||||
"""
|
||||
Bind socket to bind ip:port in conf
|
||||
|
||||
note: Mostly comes from Swift with a few small changes...
|
||||
|
||||
:param conf: a cfg.ConfigOpts object
|
||||
:param default_port: port to bind to if none is specified in conf
|
||||
|
||||
:returns : a socket object as returned from socket.listen or
|
||||
ssl.wrap_socket if conf specifies cert_file
|
||||
"""
|
||||
bind_addr = get_bind_addr(conf, default_port)
|
||||
|
||||
# TODO(jaypipes): eventlet's greened socket module does not actually
|
||||
# support IPv6 in getaddrinfo(). We need to get around this in the
|
||||
# future or monitor upstream for a fix
|
||||
address_family = [addr[0] for addr in socket.getaddrinfo(bind_addr[0],
|
||||
bind_addr[1], socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||
if addr[0] in (socket.AF_INET, socket.AF_INET6)][0]
|
||||
|
||||
conf.register_opts(socket_opts)
|
||||
|
||||
cert_file = conf.cert_file
|
||||
key_file = conf.key_file
|
||||
use_ssl = cert_file or key_file
|
||||
if use_ssl and (not cert_file or not key_file):
|
||||
raise RuntimeError(_("When running server in SSL mode, you must "
|
||||
"specify both a cert_file and key_file "
|
||||
"option value in your configuration file"))
|
||||
|
||||
sock = None
|
||||
retry_until = time.time() + 30
|
||||
while not sock and time.time() < retry_until:
|
||||
try:
|
||||
sock = eventlet.listen(bind_addr, backlog=conf.backlog,
|
||||
family=address_family)
|
||||
if use_ssl:
|
||||
sock = ssl.wrap_socket(sock, certfile=cert_file,
|
||||
keyfile=key_file)
|
||||
except socket.error, err:
|
||||
if err.args[0] != errno.EADDRINUSE:
|
||||
raise
|
||||
eventlet.sleep(0.1)
|
||||
if not sock:
|
||||
raise RuntimeError(_("Could not bind to %s:%s after trying for 30 "
|
||||
"seconds") % bind_addr)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
# in my experience, sockets can hang around forever without keepalive
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
|
||||
# This option isn't available in the OS X version of eventlet
|
||||
if hasattr(socket, 'TCP_KEEPIDLE'):
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 600)
|
||||
|
||||
return sock
|
||||
|
||||
|
||||
class Server(object):
|
||||
"""Server class to manage multiple WSGI sockets and applications."""
|
||||
|
||||
def __init__(self, threads=1000):
|
||||
self.threads = threads
|
||||
self.children = []
|
||||
self.running = True
|
||||
|
||||
def start(self, application, conf, default_port):
|
||||
"""
|
||||
Run a WSGI server with the given application.
|
||||
|
||||
:param application: The application to run in the WSGI server
|
||||
:param conf: a cfg.ConfigOpts object
|
||||
:param default_port: Port to bind to if none is specified in conf
|
||||
"""
|
||||
def kill_children(*args):
|
||||
"""Kills the entire process group."""
|
||||
self.logger.error(_('SIGTERM received'))
|
||||
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||
self.running = False
|
||||
os.killpg(0, signal.SIGTERM)
|
||||
|
||||
def hup(*args):
|
||||
"""
|
||||
Shuts down the server, but allows running requests to complete
|
||||
"""
|
||||
self.logger.error(_('SIGHUP received'))
|
||||
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
||||
self.running = False
|
||||
|
||||
self.application = application
|
||||
self.sock = get_socket(conf, default_port)
|
||||
conf.register_opt(workers_opt)
|
||||
|
||||
self.logger = logging.getLogger('eventlet.wsgi.server')
|
||||
|
||||
if conf.workers == 0:
|
||||
# Useful for profiling, test, debug etc.
|
||||
self.pool = eventlet.GreenPool(size=self.threads)
|
||||
self.pool.spawn_n(self._single_run, application, self.sock)
|
||||
return
|
||||
|
||||
self.logger.info(_("Starting %d workers") % conf.workers)
|
||||
signal.signal(signal.SIGTERM, kill_children)
|
||||
signal.signal(signal.SIGHUP, hup)
|
||||
while len(self.children) < conf.workers:
|
||||
self.run_child()
|
||||
|
||||
def wait_on_children(self):
|
||||
while self.running:
|
||||
try:
|
||||
pid, status = os.wait()
|
||||
if os.WIFEXITED(status) or os.WIFSIGNALED(status):
|
||||
self.logger.error(_('Removing dead child %s') % pid)
|
||||
self.children.remove(pid)
|
||||
self.run_child()
|
||||
except OSError, err:
|
||||
if err.errno not in (errno.EINTR, errno.ECHILD):
|
||||
raise
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(1)
|
||||
self.logger.info(_('Caught keyboard interrupt. Exiting.'))
|
||||
break
|
||||
eventlet.greenio.shutdown_safe(self.sock)
|
||||
self.sock.close()
|
||||
self.logger.debug(_('Exited'))
|
||||
|
||||
def wait(self):
|
||||
"""Wait until all servers have completed running."""
|
||||
try:
|
||||
if self.children:
|
||||
self.wait_on_children()
|
||||
else:
|
||||
self.pool.waitall()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
def run_child(self):
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
signal.signal(signal.SIGHUP, signal.SIG_DFL)
|
||||
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||
self.run_server()
|
||||
self.logger.info(_('Child %d exiting normally') % os.getpid())
|
||||
return
|
||||
else:
|
||||
self.logger.info(_('Started child %s') % pid)
|
||||
self.children.append(pid)
|
||||
|
||||
def run_server(self):
|
||||
"""Run a WSGI server."""
|
||||
eventlet.wsgi.HttpProtocol.default_request_version = "HTTP/1.0"
|
||||
eventlet.hubs.use_hub('poll')
|
||||
eventlet.patcher.monkey_patch(all=False, socket=True)
|
||||
self.pool = eventlet.GreenPool(size=self.threads)
|
||||
try:
|
||||
eventlet.wsgi.server(self.sock, self.application,
|
||||
log=WritableLogger(self.logger), custom_pool=self.pool)
|
||||
except socket.error, err:
|
||||
if err[0] != errno.EINVAL:
|
||||
raise
|
||||
self.pool.waitall()
|
||||
|
||||
def _single_run(self, application, sock):
|
||||
"""Start a WSGI server in a new green thread."""
|
||||
self.logger.info(_("Starting single process server"))
|
||||
eventlet.wsgi.server(sock, application, custom_pool=self.pool,
|
||||
log=WritableLogger(self.logger))
|
||||
|
||||
|
||||
class Middleware(object):
|
||||
"""
|
||||
Base WSGI middleware wrapper. These classes require an application to be
|
||||
initialized that will be called next. By default the middleware will
|
||||
simply call its wrapped app, or you can override __call__ to customize its
|
||||
behavior.
|
||||
"""
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
|
||||
def process_request(self, req):
|
||||
"""
|
||||
Called on each request.
|
||||
|
||||
If this returns None, the next application down the stack will be
|
||||
executed. If it returns a response then that response will be returned
|
||||
and execution will stop here.
|
||||
|
||||
"""
|
||||
return None
|
||||
|
||||
def process_response(self, response):
|
||||
"""Do whatever you'd like to the response."""
|
||||
return response
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
response = self.process_request(req)
|
||||
if response:
|
||||
return response
|
||||
response = req.get_response(self.application)
|
||||
return self.process_response(response)
|
||||
|
||||
|
||||
class Debug(Middleware):
|
||||
"""
|
||||
Helper class that can be inserted into any WSGI application chain
|
||||
to get information about the request and response.
|
||||
"""
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
print ("*" * 40) + " REQUEST ENVIRON"
|
||||
for key, value in req.environ.items():
|
||||
print key, "=", value
|
||||
print
|
||||
resp = req.get_response(self.application)
|
||||
|
||||
print ("*" * 40) + " RESPONSE HEADERS"
|
||||
for (key, value) in resp.headers.iteritems():
|
||||
print key, "=", value
|
||||
print
|
||||
|
||||
resp.app_iter = self.print_generator(resp.app_iter)
|
||||
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def print_generator(app_iter):
|
||||
"""
|
||||
Iterator that prints the contents of a wrapper string iterator
|
||||
when iterated.
|
||||
"""
|
||||
print ("*" * 40) + " BODY"
|
||||
for part in app_iter:
|
||||
sys.stdout.write(part)
|
||||
sys.stdout.flush()
|
||||
yield part
|
||||
print
|
||||
|
||||
|
||||
class Router(object):
|
||||
"""
|
||||
WSGI middleware that maps incoming requests to WSGI apps.
|
||||
"""
|
||||
|
||||
def __init__(self, mapper):
|
||||
"""
|
||||
Create a router for the given routes.Mapper.
|
||||
|
||||
Each route in `mapper` must specify a 'controller', which is a
|
||||
WSGI app to call. You'll probably want to specify an 'action' as
|
||||
well and have your controller be a wsgi.Controller, who will route
|
||||
the request to the action method.
|
||||
|
||||
Examples:
|
||||
mapper = routes.Mapper()
|
||||
sc = ServerController()
|
||||
|
||||
# Explicit mapping of one route to a controller+action
|
||||
mapper.connect(None, "/svrlist", controller=sc, action="list")
|
||||
|
||||
# Actions are all implicitly defined
|
||||
mapper.resource("server", "servers", controller=sc)
|
||||
|
||||
# Pointing to an arbitrary WSGI app. You can specify the
|
||||
# {path_info:.*} parameter so the target app can be handed just that
|
||||
# section of the URL.
|
||||
mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp())
|
||||
"""
|
||||
self.map = mapper
|
||||
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
||||
self.map)
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
"""
|
||||
Route the incoming request to a controller based on self.map.
|
||||
If no match, return a 404.
|
||||
"""
|
||||
return self._router
|
||||
|
||||
@staticmethod
|
||||
@webob.dec.wsgify
|
||||
def _dispatch(req):
|
||||
"""
|
||||
Called by self._router after matching the incoming request to a route
|
||||
and putting the information into req.environ. Either returns 404
|
||||
or the routed WSGI app's response.
|
||||
"""
|
||||
match = req.environ['wsgiorg.routing_args'][1]
|
||||
if not match:
|
||||
return webob.exc.HTTPNotFound()
|
||||
app = match['controller']
|
||||
return app
|
||||
|
||||
|
||||
class Request(webob.Request):
|
||||
"""Add some Openstack API-specific logic to the base webob.Request."""
|
||||
|
||||
def best_match_content_type(self):
|
||||
"""Determine the requested response content-type."""
|
||||
supported = ('application/json',)
|
||||
bm = self.accept.best_match(supported)
|
||||
return bm or 'application/json'
|
||||
|
||||
def get_content_type(self, allowed_content_types):
|
||||
"""Determine content type of the request body."""
|
||||
if not "Content-Type" in self.headers:
|
||||
raise exception.InvalidContentType(content_type=None)
|
||||
|
||||
content_type = self.content_type
|
||||
|
||||
if content_type not in allowed_content_types:
|
||||
raise exception.InvalidContentType(content_type=content_type)
|
||||
else:
|
||||
return content_type
|
||||
|
||||
|
||||
class JSONRequestDeserializer(object):
|
||||
def has_body(self, request):
|
||||
"""
|
||||
Returns whether a Webob.Request object will possess an entity body.
|
||||
|
||||
:param request: Webob.Request object
|
||||
"""
|
||||
if 'transfer-encoding' in request.headers:
|
||||
return True
|
||||
elif request.content_length > 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def from_json(self, datastring):
|
||||
return json.loads(datastring)
|
||||
|
||||
def default(self, request):
|
||||
if self.has_body(request):
|
||||
return {'body': self.from_json(request.body)}
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
class JSONResponseSerializer(object):
|
||||
|
||||
def to_json(self, data):
|
||||
def sanitizer(obj):
|
||||
if isinstance(obj, datetime.datetime):
|
||||
return obj.isoformat()
|
||||
return obj
|
||||
|
||||
return json.dumps(data, default=sanitizer)
|
||||
|
||||
def default(self, response, result):
|
||||
response.content_type = 'application/json'
|
||||
response.body = self.to_json(result)
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""
|
||||
WSGI app that handles (de)serialization and controller dispatch.
|
||||
|
||||
Reads routing information supplied by RoutesMiddleware and calls
|
||||
the requested action method upon its deserializer, controller,
|
||||
and serializer. Those three objects may implement any of the basic
|
||||
controller action methods (create, update, show, index, delete)
|
||||
along with any that may be specified in the api router. A 'default'
|
||||
method may also be implemented to be used in place of any
|
||||
non-implemented actions. Deserializer methods must accept a request
|
||||
argument and return a dictionary. Controller methods must accept a
|
||||
request argument. Additionally, they must also accept keyword
|
||||
arguments that represent the keys returned by the Deserializer. They
|
||||
may raise a webob.exc exception or return a dict, which will be
|
||||
serialized by requested content type.
|
||||
"""
|
||||
def __init__(self, controller, deserializer, serializer):
|
||||
"""
|
||||
:param controller: object that implement methods created by routes lib
|
||||
:param deserializer: object that supports webob request deserialization
|
||||
through controller-like actions
|
||||
:param serializer: object that supports webob response serialization
|
||||
through controller-like actions
|
||||
"""
|
||||
self.controller = controller
|
||||
self.serializer = serializer
|
||||
self.deserializer = deserializer
|
||||
|
||||
@webob.dec.wsgify(RequestClass=Request)
|
||||
def __call__(self, request):
|
||||
"""WSGI method that controls (de)serialization and method dispatch."""
|
||||
action_args = self.get_action_args(request.environ)
|
||||
action = action_args.pop('action', None)
|
||||
|
||||
deserialized_request = self.dispatch(self.deserializer,
|
||||
action, request)
|
||||
action_args.update(deserialized_request)
|
||||
|
||||
action_result = self.dispatch(self.controller, action,
|
||||
request, **action_args)
|
||||
try:
|
||||
response = webob.Response(request=request)
|
||||
self.dispatch(self.serializer, action, response, action_result)
|
||||
return response
|
||||
|
||||
# return unserializable result (typically a webob exc)
|
||||
except Exception:
|
||||
return action_result
|
||||
|
||||
def dispatch(self, obj, action, *args, **kwargs):
|
||||
"""Find action-specific method on self and call it."""
|
||||
try:
|
||||
method = getattr(obj, action)
|
||||
except AttributeError:
|
||||
method = getattr(obj, 'default')
|
||||
|
||||
return method(*args, **kwargs)
|
||||
|
||||
def get_action_args(self, request_environment):
|
||||
"""Parse dictionary created by routes library."""
|
||||
try:
|
||||
args = request_environment['wsgiorg.routing_args'][1].copy()
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
try:
|
||||
del args['controller']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
del args['format']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return args
|
||||
|
||||
|
||||
class BasePasteFactory(object):
|
||||
|
||||
"""A base class for paste app and filter factories.
|
||||
|
||||
Sub-classes must override the KEY class attribute and provide
|
||||
a __call__ method.
|
||||
"""
|
||||
|
||||
KEY = None
|
||||
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
|
||||
def __call__(self, global_conf, **local_conf):
|
||||
raise NotImplementedError
|
||||
|
||||
def _import_factory(self, local_conf):
|
||||
"""Import an app/filter class.
|
||||
|
||||
Lookup the KEY from the PasteDeploy local conf and import the
|
||||
class named there. This class can then be used as an app or
|
||||
filter factory.
|
||||
|
||||
Note we support the <module>:<class> format.
|
||||
|
||||
Note also that if you do e.g.
|
||||
|
||||
key =
|
||||
value
|
||||
|
||||
then ConfigParser returns a value with a leading newline, so
|
||||
we strip() the value before using it.
|
||||
"""
|
||||
class_name = local_conf[self.KEY].replace(':', '.').strip()
|
||||
return utils.import_class(class_name)
|
||||
|
||||
|
||||
class AppFactory(BasePasteFactory):
|
||||
|
||||
"""A Generic paste.deploy app factory.
|
||||
|
||||
This requires heat.app_factory to be set to a callable which returns a
|
||||
WSGI app when invoked. The format of the name is <module>:<callable> e.g.
|
||||
|
||||
[app:apiv1app]
|
||||
paste.app_factory = heat.common.wsgi:app_factory
|
||||
heat.app_factory = heat.api.v1:API
|
||||
|
||||
The WSGI app constructor must accept a ConfigOpts object and a local config
|
||||
dict as its two arguments.
|
||||
"""
|
||||
|
||||
KEY = 'heat.app_factory'
|
||||
|
||||
def __call__(self, global_conf, **local_conf):
|
||||
"""The actual paste.app_factory protocol method."""
|
||||
factory = self._import_factory(local_conf)
|
||||
return factory(self.conf, **local_conf)
|
||||
|
||||
|
||||
class FilterFactory(AppFactory):
|
||||
|
||||
"""A Generic paste.deploy filter factory.
|
||||
|
||||
This requires heat.filter_factory to be set to a callable which returns a
|
||||
WSGI filter when invoked. The format is <module>:<callable> e.g.
|
||||
|
||||
[filter:cache]
|
||||
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||
heat.filter_factory = heat.api.middleware.cache:CacheFilter
|
||||
|
||||
The WSGI filter constructor must accept a WSGI app, a ConfigOpts object and
|
||||
a local config dict as its three arguments.
|
||||
"""
|
||||
|
||||
KEY = 'heat.filter_factory'
|
||||
|
||||
def __call__(self, global_conf, **local_conf):
|
||||
"""The actual paste.filter_factory protocol method."""
|
||||
factory = self._import_factory(local_conf)
|
||||
|
||||
def filter(app):
|
||||
return factory(app, self.conf, **local_conf)
|
||||
|
||||
return filter
|
||||
|
||||
|
||||
def setup_paste_factories(conf):
|
||||
"""Set up the generic paste app and filter factories.
|
||||
|
||||
Set things up so that:
|
||||
|
||||
paste.app_factory = heat.common.wsgi:app_factory
|
||||
|
||||
and
|
||||
|
||||
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||
|
||||
work correctly while loading PasteDeploy configuration.
|
||||
|
||||
The app factories are constructed at runtime to allow us to pass a
|
||||
ConfigOpts object to the WSGI classes.
|
||||
|
||||
:param conf: a ConfigOpts object
|
||||
"""
|
||||
global app_factory, filter_factory
|
||||
app_factory = AppFactory(conf)
|
||||
filter_factory = FilterFactory(conf)
|
||||
|
||||
|
||||
def teardown_paste_factories():
|
||||
"""Reverse the effect of setup_paste_factories()."""
|
||||
global app_factory, filter_factory
|
||||
del app_factory
|
||||
del filter_factory
|
||||
|
||||
|
||||
def paste_deploy_app(paste_config_file, app_name, conf):
|
||||
"""Load a WSGI app from a PasteDeploy configuration.
|
||||
|
||||
Use deploy.loadapp() to load the app from the PasteDeploy configuration,
|
||||
ensuring that the supplied ConfigOpts object is passed to the app and
|
||||
filter constructors.
|
||||
|
||||
:param paste_config_file: a PasteDeploy config file
|
||||
:param app_name: the name of the app/pipeline to load from the file
|
||||
:param conf: a ConfigOpts object to supply to the app and its filters
|
||||
:returns: the WSGI app
|
||||
"""
|
||||
setup_paste_factories(conf)
|
||||
try:
|
||||
return deploy.loadapp("config:%s" % paste_config_file, name=app_name)
|
||||
finally:
|
||||
teardown_paste_factories()
|
46
heat/version.py
Normal file
46
heat/version.py
Normal file
@ -0,0 +1,46 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC
|
||||
#
|
||||
# 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.
|
||||
|
||||
try:
|
||||
from glance.vcsversion import version_info
|
||||
except ImportError:
|
||||
version_info = {'branch_nick': u'LOCALBRANCH',
|
||||
'revision_id': 'LOCALREVISION',
|
||||
'revno': 0}
|
||||
|
||||
GLANCE_VERSION = ['2012', '1', None]
|
||||
YEAR, COUNT, REVSISION = GLANCE_VERSION
|
||||
|
||||
FINAL = False # This becomes true at Release Candidate time
|
||||
|
||||
|
||||
def canonical_version_string():
|
||||
return '.'.join(filter(None, GLANCE_VERSION))
|
||||
|
||||
|
||||
def version_string():
|
||||
if FINAL:
|
||||
return canonical_version_string()
|
||||
else:
|
||||
return '%s-dev' % (canonical_version_string(),)
|
||||
|
||||
|
||||
def vcs_version_string():
|
||||
return "%s:%s" % (version_info['branch_nick'], version_info['revision_id'])
|
||||
|
||||
|
||||
def version_string_with_vcs():
|
||||
return "%s-%s" % (canonical_version_string(), vcs_version_string())
|
27
pylintrc
Normal file
27
pylintrc
Normal file
@ -0,0 +1,27 @@
|
||||
[Messages Control]
|
||||
# W0511: TODOs in code comments are fine.
|
||||
# W0142: *args and **kwargs are fine.
|
||||
# W0622: Redefining id is fine.
|
||||
disable-msg=W0511,W0142,W0622
|
||||
|
||||
[Basic]
|
||||
# Variable names can be 1 to 31 characters long, with lowercase and underscores
|
||||
variable-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
|
||||
# Argument names can be 2 to 31 characters long, with lowercase and underscores
|
||||
argument-rgx=[a-z_][a-z0-9_]{1,30}$
|
||||
|
||||
# Method names should be at least 3 characters long
|
||||
# and be lowecased with underscores
|
||||
method-rgx=[a-z_][a-z0-9_]{2,50}$
|
||||
|
||||
# Module names matching nova-* are ok (files in bin/)
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+)|(nova-[a-z0-9_-]+))$
|
||||
|
||||
# Don't require docstrings on tests.
|
||||
no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$
|
||||
|
||||
[Design]
|
||||
max-public-methods=100
|
||||
min-public-methods=0
|
||||
max-args=6
|
40
templates/getting_started.template
Normal file
40
templates/getting_started.template
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"Parameters" : {
|
||||
"KeyName" : {
|
||||
"Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instance",
|
||||
"Type" : "String"
|
||||
}
|
||||
},
|
||||
|
||||
"Mappings" : {
|
||||
"RegionMap" : {
|
||||
"us-east-1" : {
|
||||
"AMI" : "ami-76f0061f"
|
||||
},
|
||||
"us-west-1" : {
|
||||
"AMI" : "ami-655a0a20"
|
||||
},
|
||||
"eu-west-1" : {
|
||||
"AMI" : "ami-7fd4e10b"
|
||||
},
|
||||
"ap-southeast-1" : {
|
||||
"AMI" : "ami-72621c20"
|
||||
},
|
||||
"ap-northeast-1" : {
|
||||
"AMI" : "ami-8e08a38f"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"Resources" : {
|
||||
"Ec2Instance" : {
|
||||
"Type" : "AWS::EC2::Instance",
|
||||
"Properties" : {
|
||||
"KeyName" : { "Ref" : "KeyName" },
|
||||
"ImageId" : { "Fn::FindInMap" : [ "RegionMap", { "Ref" : "AWS::Region" }, "AMI" ]},
|
||||
"UserData" : { "Fn::Base64" : "80" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user