Initial commit (basics copied from glance)

Signed-off-by: Angus Salkeld <asalkeld@redhat.com>
This commit is contained in:
Angus Salkeld 2012-03-13 21:48:07 +11:00
commit 3b9c41fb6c
30 changed files with 5288 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*.pyc
*.swp
*.log
build
dist
heat/vcsversion.py

176
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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.

View 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.

View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

593
heat/common/client.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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" }
}
}
}
}