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