--- /dev/null
+*.pyc
+*.swp
+*.log
+build
+dist
+heat/vcsversion.py
--- /dev/null
+
+ 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.
+
--- /dev/null
+#!/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()
--- /dev/null
+#!/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)
--- /dev/null
+[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
--- /dev/null
+# 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
--- /dev/null
+# 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)
--- /dev/null
+# 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.
--- /dev/null
+# 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.
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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')
+
--- /dev/null
+# 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)
--- /dev/null
+# 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)
--- /dev/null
+# 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
--- /dev/null
+# 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)
--- /dev/null
+# 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.
--- /dev/null
+# 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)
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Red Hat, Inc.
+#
+# 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.
+
+r"""
+Configuration options which may be set on the command line or in config files.
+
+The schema for each option is defined using the Opt sub-classes e.g.
+
+ common_opts = [
+ cfg.StrOpt('bind_host',
+ default='0.0.0.0',
+ help='IP address to listen on'),
+ cfg.IntOpt('bind_port',
+ default=9292,
+ help='Port number to listen on')
+ ]
+
+Options can be strings, integers, floats, booleans, lists or 'multi strings':
+
+ enabled_apis_opt = \
+ cfg.ListOpt('enabled_apis',
+ default=['ec2', 'osapi'],
+ help='List of APIs to enable by default')
+
+ DEFAULT_EXTENSIONS = [
+ 'nova.api.openstack.contrib.standard_extensions'
+ ]
+ osapi_extension_opt = \
+ cfg.MultiStrOpt('osapi_extension',
+ default=DEFAULT_EXTENSIONS)
+
+Option schemas are registered with with the config manager at runtime, but
+before the option is referenced:
+
+ class ExtensionManager(object):
+
+ enabled_apis_opt = cfg.ListOpt(...)
+
+ def __init__(self, conf):
+ self.conf = conf
+ self.conf.register_opt(enabled_apis_opt)
+ ...
+
+ def _load_extensions(self):
+ for ext_factory in self.conf.osapi_extension:
+ ....
+
+A common usage pattern is for each option schema to be defined in the module or
+class which uses the option:
+
+ opts = ...
+
+ def add_common_opts(conf):
+ conf.register_opts(opts)
+
+ def get_bind_host(conf):
+ return conf.bind_host
+
+ def get_bind_port(conf):
+ return conf.bind_port
+
+An option may optionally be made available via the command line. Such options
+must registered with the config manager before the command line is parsed (for
+the purposes of --help and CLI arg validation):
+
+ cli_opts = [
+ cfg.BoolOpt('verbose',
+ short='v',
+ default=False,
+ help='Print more verbose output'),
+ cfg.BoolOpt('debug',
+ short='d',
+ default=False,
+ help='Print debugging output'),
+ ]
+
+ def add_common_opts(conf):
+ conf.register_cli_opts(cli_opts)
+
+The config manager has a single CLI option defined by default, --config-file:
+
+ class ConfigOpts(object):
+
+ config_file_opt = \
+ MultiStrOpt('config-file',
+ ...
+
+ def __init__(self, ...):
+ ...
+ self.register_cli_opt(self.config_file_opt)
+
+Option values are parsed from any supplied config files using SafeConfigParser.
+If none are specified, a default set is used e.g. glance-api.conf and
+glance-common.conf:
+
+ glance-api.conf:
+ [DEFAULT]
+ bind_port = 9292
+
+ glance-common.conf:
+ [DEFAULT]
+ bind_host = 0.0.0.0
+
+Option values in config files override those on the command line. Config files
+are parsed in order, with values in later files overriding those in earlier
+files.
+
+The parsing of CLI args and config files is initiated by invoking the config
+manager e.g.
+
+ conf = ConfigOpts()
+ conf.register_opt(BoolOpt('verbose', ...))
+ conf(sys.argv[1:])
+ if conf.verbose:
+ ...
+
+Options can be registered as belonging to a group:
+
+ rabbit_group = cfg.OptionGroup(name='rabbit',
+ title='RabbitMQ options')
+
+ rabbit_host_opt = \
+ cfg.StrOpt('host',
+ group='rabbit',
+ default='localhost',
+ help='IP/hostname to listen on'),
+ rabbit_port_opt = \
+ cfg.IntOpt('port',
+ default=5672,
+ help='Port number to listen on')
+ rabbit_ssl_opt = \
+ conf.BoolOpt('use_ssl',
+ default=False,
+ help='Whether to support SSL connections')
+
+ def register_rabbit_opts(conf):
+ conf.register_group(rabbit_group)
+ # options can be registered under a group in any of these ways:
+ conf.register_opt(rabbit_host_opt)
+ conf.register_opt(rabbit_port_opt, group='rabbit')
+ conf.register_opt(rabbit_ssl_opt, group=rabbit_group)
+
+If no group is specified, options belong to the 'DEFAULT' section of config
+files:
+
+ glance-api.conf:
+ [DEFAULT]
+ bind_port = 9292
+ ...
+
+ [rabbit]
+ host = localhost
+ port = 5672
+ use_ssl = False
+ userid = guest
+ password = guest
+ virtual_host = /
+
+Command-line options in a group are automatically prefixed with the group name:
+
+ --rabbit-host localhost --rabbit-use-ssl False
+
+Option values in the default group are referenced as attributes/properties on
+the config manager; groups are also attributes on the config manager, with
+attributes for each of the options associated with the group:
+
+ server.start(app, conf.bind_port, conf.bind_host, conf)
+
+ self.connection = kombu.connection.BrokerConnection(
+ hostname=conf.rabbit.host,
+ port=conf.rabbit.port,
+ ...)
+
+Option values may reference other values using PEP 292 string substitution:
+
+ opts = [
+ cfg.StrOpt('state_path',
+ default=os.path.join(os.path.dirname(__file__), '../'),
+ help='Top-level directory for maintaining nova state'),
+ cfg.StrOpt('sqlite_db',
+ default='nova.sqlite',
+ help='file name for sqlite'),
+ cfg.StrOpt('sql_connection',
+ default='sqlite:///$state_path/$sqlite_db',
+ help='connection string for sql database'),
+ ]
+
+Note that interpolation can be avoided by using '$$'.
+"""
+
+import sys
+import ConfigParser
+import copy
+import optparse
+import os
+import string
+
+
+class Error(Exception):
+ """Base class for cfg exceptions."""
+
+ def __init__(self, msg=None):
+ self.msg = msg
+
+ def __str__(self):
+ return self.msg
+
+
+class ArgsAlreadyParsedError(Error):
+ """Raised if a CLI opt is registered after parsing."""
+
+ def __str__(self):
+ ret = "arguments already parsed"
+ if self.msg:
+ ret += ": " + self.msg
+ return ret
+
+
+class NoSuchOptError(Error):
+ """Raised if an opt which doesn't exist is referenced."""
+
+ def __init__(self, opt_name, group=None):
+ self.opt_name = opt_name
+ self.group = group
+
+ def __str__(self):
+ if self.group is None:
+ return "no such option: %s" % self.opt_name
+ else:
+ return "no such option in group %s: %s" % (self.group.name,
+ self.opt_name)
+
+
+class NoSuchGroupError(Error):
+ """Raised if a group which doesn't exist is referenced."""
+
+ def __init__(self, group_name):
+ self.group_name = group_name
+
+ def __str__(self):
+ return "no such group: %s" % self.group_name
+
+
+class DuplicateOptError(Error):
+ """Raised if multiple opts with the same name are registered."""
+
+ def __init__(self, opt_name):
+ self.opt_name = opt_name
+
+ def __str__(self):
+ return "duplicate option: %s" % self.opt_name
+
+
+class TemplateSubstitutionError(Error):
+ """Raised if an error occurs substituting a variable in an opt value."""
+
+ def __str__(self):
+ return "template substitution error: %s" % self.msg
+
+
+class ConfigFilesNotFoundError(Error):
+ """Raised if one or more config files are not found."""
+
+ def __init__(self, config_files):
+ self.config_files = config_files
+
+ def __str__(self):
+ return 'Failed to read some config files: %s' % \
+ string.join(self.config_files, ',')
+
+
+class ConfigFileParseError(Error):
+ """Raised if there is an error parsing a config file."""
+
+ def __init__(self, config_file, msg):
+ self.config_file = config_file
+ self.msg = msg
+
+ def __str__(self):
+ return 'Failed to parse %s: %s' % (self.config_file, self.msg)
+
+
+class ConfigFileValueError(Error):
+ """Raised if a config file value does not match its opt type."""
+ pass
+
+
+def find_config_files(project=None, prog=None, filetype="conf"):
+ """Return a list of default configuration files.
+
+ We default to two config files: [${project}.conf, ${prog}.conf]
+
+ And we look for those config files in the following directories:
+
+ ~/.${project}/
+ ~/
+ /etc/${project}/
+ /etc/
+
+ We return an absolute path for (at most) one of each the default config
+ files, for the topmost directory it exists in.
+
+ For example, if project=foo, prog=bar and /etc/foo/foo.conf, /etc/bar.conf
+ and ~/.foo/bar.conf all exist, then we return ['/etc/foo/foo.conf',
+ '~/.foo/bar.conf']
+
+ If no project name is supplied, we only look for ${prog.conf}.
+
+ :param project: an optional project name
+ :param prog: the program name, defaulting to the basename of sys.argv[0]
+ """
+ if prog is None:
+ prog = os.path.basename(sys.argv[0])
+
+ fix_path = lambda p: os.path.abspath(os.path.expanduser(p))
+
+ cfg_dirs = [
+ fix_path(os.path.join('~', '.' + project)) if project else None,
+ fix_path('~'),
+ os.path.join('/etc', project) if project else None,
+ '/etc',
+ 'etc',
+ ]
+ cfg_dirs = filter(bool, cfg_dirs)
+
+ def search_dirs(dirs, basename):
+ for d in dirs:
+ path = os.path.join(d, basename)
+ if os.path.exists(path):
+ return path
+
+ config_files = []
+
+ if project:
+ project_config = search_dirs(cfg_dirs, '%s.%s' % (project, filetype))
+ config_files.append(project_config)
+
+ config_files.append(search_dirs(cfg_dirs, '%s.%s' % (prog, filetype)))
+
+ return filter(bool, config_files)
+
+
+def _is_opt_registered(opts, opt):
+ """Check whether an opt with the same name is already registered.
+
+ The same opt may be registered multiple times, with only the first
+ registration having any effect. However, it is an error to attempt
+ to register a different opt with the same name.
+
+ :param opts: the set of opts already registered
+ :param opt: the opt to be registered
+ :returns: True if the opt was previously registered, False otherwise
+ :raises: DuplicateOptError if a naming conflict is detected
+ """
+ if opt.dest in opts:
+ if opts[opt.dest]['opt'] is not opt:
+ raise DuplicateOptError(opt.name)
+ return True
+ else:
+ return False
+
+
+class Opt(object):
+
+ """Base class for all configuration options.
+
+ An Opt object has no public methods, but has a number of public string
+ properties:
+
+ name:
+ the name of the option, which may include hyphens
+ dest:
+ the (hyphen-less) ConfigOpts property which contains the option value
+ short:
+ a single character CLI option name
+ default:
+ the default value of the option
+ metavar:
+ the name shown as the argument to a CLI option in --help output
+ help:
+ an string explaining how the options value is used
+ """
+
+ def __init__(self, name, dest=None, short=None,
+ default=None, metavar=None, help=None):
+ """Construct an Opt object.
+
+ The only required parameter is the option's name. However, it is
+ common to also supply a default and help string for all options.
+
+ :param name: the option's name
+ :param dest: the name of the corresponding ConfigOpts property
+ :param short: a single character CLI option name
+ :param default: the default value of the option
+ :param metavar: the option argument to show in --help
+ :param help: an explanation of how the option is used
+ """
+ self.name = name
+ if dest is None:
+ self.dest = self.name.replace('-', '_')
+ else:
+ self.dest = dest
+ self.short = short
+ self.default = default
+ self.metavar = metavar
+ self.help = help
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieves the option value from a ConfigParser object.
+
+ This is the method ConfigOpts uses to look up the option value from
+ config files. Most opt types override this method in order to perform
+ type appropriate conversion of the returned value.
+
+ :param cparser: a ConfigParser object
+ :param section: a section name
+ """
+ return cparser.get(section, self.dest)
+
+ def _add_to_cli(self, parser, group=None):
+ """Makes the option available in the command line interface.
+
+ This is the method ConfigOpts uses to add the opt to the CLI interface
+ as appropriate for the opt type. Some opt types may extend this method,
+ others may just extend the helper methods it uses.
+
+ :param parser: the CLI option parser
+ :param group: an optional OptGroup object
+ """
+ container = self._get_optparse_container(parser, group)
+ kwargs = self._get_optparse_kwargs(group)
+ prefix = self._get_optparse_prefix('', group)
+ self._add_to_optparse(container, self.name, self.short, kwargs, prefix)
+
+ def _add_to_optparse(self, container, name, short, kwargs, prefix=''):
+ """Add an option to an optparse parser or group.
+
+ :param container: an optparse.OptionContainer object
+ :param name: the opt name
+ :param short: the short opt name
+ :param kwargs: the keyword arguments for add_option()
+ :param prefix: an optional prefix to prepend to the opt name
+ :raises: DuplicateOptError if a naming confict is detected
+ """
+ args = ['--' + prefix + name]
+ if short:
+ args += ['-' + short]
+ for a in args:
+ if container.has_option(a):
+ raise DuplicateOptError(a)
+ container.add_option(*args, **kwargs)
+
+ def _get_optparse_container(self, parser, group):
+ """Returns an optparse.OptionContainer.
+
+ :param parser: an optparse.OptionParser
+ :param group: an (optional) OptGroup object
+ :returns: an optparse.OptionGroup if a group is given, else the parser
+ """
+ if group is not None:
+ return group._get_optparse_group(parser)
+ else:
+ return parser
+
+ def _get_optparse_kwargs(self, group, **kwargs):
+ """Build a dict of keyword arguments for optparse's add_option().
+
+ Most opt types extend this method to customize the behaviour of the
+ options added to optparse.
+
+ :param group: an optional group
+ :param kwargs: optional keyword arguments to add to
+ :returns: a dict of keyword arguments
+ """
+ dest = self.dest
+ if group is not None:
+ dest = group.name + '_' + dest
+ kwargs.update({
+ 'dest': dest,
+ 'metavar': self.metavar,
+ 'help': self.help,
+ })
+ return kwargs
+
+ def _get_optparse_prefix(self, prefix, group):
+ """Build a prefix for the CLI option name, if required.
+
+ CLI options in a group are prefixed with the group's name in order
+ to avoid conflicts between similarly named options in different
+ groups.
+
+ :param prefix: an existing prefix to append to (e.g. 'no' or '')
+ :param group: an optional OptGroup object
+ :returns: a CLI option prefix including the group name, if appropriate
+ """
+ if group is not None:
+ return group.name + '-' + prefix
+ else:
+ return prefix
+
+
+class StrOpt(Opt):
+ """
+ String opts do not have their values transformed and are returned as
+ str objects.
+ """
+ pass
+
+
+class BoolOpt(Opt):
+
+ """
+ Bool opts are set to True or False on the command line using --optname or
+ --noopttname respectively.
+
+ In config files, boolean values are case insensitive and can be set using
+ 1/0, yes/no, true/false or on/off.
+ """
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieve the opt value as a boolean from ConfigParser."""
+ return cparser.getboolean(section, self.dest)
+
+ def _add_to_cli(self, parser, group=None):
+ """Extends the base class method to add the --nooptname option."""
+ super(BoolOpt, self)._add_to_cli(parser, group)
+ self._add_inverse_to_optparse(parser, group)
+
+ def _add_inverse_to_optparse(self, parser, group):
+ """Add the --nooptname option to the option parser."""
+ container = self._get_optparse_container(parser, group)
+ kwargs = self._get_optparse_kwargs(group, action='store_false')
+ prefix = self._get_optparse_prefix('no', group)
+ kwargs["help"] = "The inverse of --" + self.name
+ self._add_to_optparse(container, self.name, None, kwargs, prefix)
+
+ def _get_optparse_kwargs(self, group, action='store_true', **kwargs):
+ """Extends the base optparse keyword dict for boolean options."""
+ return super(BoolOpt,
+ self)._get_optparse_kwargs(group, action=action, **kwargs)
+
+
+class IntOpt(Opt):
+
+ """Int opt values are converted to integers using the int() builtin."""
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieve the opt value as a integer from ConfigParser."""
+ return cparser.getint(section, self.dest)
+
+ def _get_optparse_kwargs(self, group, **kwargs):
+ """Extends the base optparse keyword dict for integer options."""
+ return super(IntOpt,
+ self)._get_optparse_kwargs(group, type='int', **kwargs)
+
+
+class FloatOpt(Opt):
+
+ """Float opt values are converted to floats using the float() builtin."""
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieve the opt value as a float from ConfigParser."""
+ return cparser.getfloat(section, self.dest)
+
+ def _get_optparse_kwargs(self, group, **kwargs):
+ """Extends the base optparse keyword dict for float options."""
+ return super(FloatOpt,
+ self)._get_optparse_kwargs(group, type='float', **kwargs)
+
+
+class ListOpt(Opt):
+
+ """
+ List opt values are simple string values separated by commas. The opt value
+ is a list containing these strings.
+ """
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieve the opt value as a list from ConfigParser."""
+ return cparser.get(section, self.dest).split(',')
+
+ def _get_optparse_kwargs(self, group, **kwargs):
+ """Extends the base optparse keyword dict for list options."""
+ return super(ListOpt,
+ self)._get_optparse_kwargs(group,
+ type='string',
+ action='callback',
+ callback=self._parse_list,
+ **kwargs)
+
+ def _parse_list(self, option, opt, value, parser):
+ """An optparse callback for parsing an option value into a list."""
+ setattr(parser.values, self.dest, value.split(','))
+
+
+class MultiStrOpt(Opt):
+
+ """
+ Multistr opt values are string opts which may be specified multiple times.
+ The opt value is a list containing all the string values specified.
+ """
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieve the opt value as a multistr from ConfigParser."""
+ # FIXME(markmc): values spread across the CLI and multiple
+ # config files should be appended
+ value = \
+ super(MultiStrOpt, self)._get_from_config_parser(cparser, section)
+ return value if value is None else [value]
+
+ def _get_optparse_kwargs(self, group, **kwargs):
+ """Extends the base optparse keyword dict for multi str options."""
+ return super(MultiStrOpt,
+ self)._get_optparse_kwargs(group, action='append')
+
+
+class OptGroup(object):
+
+ """
+ Represents a group of opts.
+
+ CLI opts in the group are automatically prefixed with the group name.
+
+ Each group corresponds to a section in config files.
+
+ An OptGroup object has no public methods, but has a number of public string
+ properties:
+
+ name:
+ the name of the group
+ title:
+ the group title as displayed in --help
+ help:
+ the group description as displayed in --help
+ """
+
+ def __init__(self, name, title=None, help=None):
+ """Constructs an OptGroup object.
+
+ :param name: the group name
+ :param title: the group title for --help
+ :param help: the group description for --help
+ """
+ self.name = name
+ if title is None:
+ self.title = "%s options" % title
+ else:
+ self.title = title
+ self.help = help
+
+ self._opts = {} # dict of dicts of {opt:, override:, default:)
+ self._optparse_group = None
+
+ def _register_opt(self, opt):
+ """Add an opt to this group.
+
+ :param opt: an Opt object
+ :returns: False if previously registered, True otherwise
+ :raises: DuplicateOptError if a naming conflict is detected
+ """
+ if _is_opt_registered(self._opts, opt):
+ return False
+
+ self._opts[opt.dest] = {'opt': opt, 'override': None, 'default': None}
+
+ return True
+
+ def _get_optparse_group(self, parser):
+ """Build an optparse.OptionGroup for this group."""
+ if self._optparse_group is None:
+ self._optparse_group = \
+ optparse.OptionGroup(parser, self.title, self.help)
+ return self._optparse_group
+
+
+class ConfigOpts(object):
+
+ """
+ Config options which may be set on the command line or in config files.
+
+ ConfigOpts is a configuration option manager with APIs for registering
+ option schemas, grouping options, parsing option values and retrieving
+ the values of options.
+ """
+
+ def __init__(self,
+ project=None,
+ prog=None,
+ version=None,
+ usage=None,
+ default_config_files=None):
+ """Construct a ConfigOpts object.
+
+ Automatically registers the --config-file option with either a supplied
+ list of default config files, or a list from find_config_files().
+
+ :param project: the toplevel project name, used to locate config files
+ :param prog: the name of the program (defaults to sys.argv[0] basename)
+ :param version: the program version (for --version)
+ :param usage: a usage string (%prog will be expanded)
+ :param default_config_files: config files to use by default
+ """
+ if prog is None:
+ prog = os.path.basename(sys.argv[0])
+
+ if default_config_files is None:
+ default_config_files = find_config_files(project, prog)
+
+ self.project = project
+ self.prog = prog
+ self.version = version
+ self.usage = usage
+ self.default_config_files = default_config_files
+
+ self._opts = {} # dict of dicts of (opt:, override:, default:)
+ self._groups = {}
+
+ self._args = None
+ self._cli_values = {}
+
+ self._oparser = optparse.OptionParser(prog=self.prog,
+ version=self.version,
+ usage=self.usage)
+ self._cparser = None
+
+ self.register_cli_opt(\
+ MultiStrOpt('config-file',
+ default=self.default_config_files,
+ metavar='PATH',
+ help='Path to a config file to use. Multiple config '
+ 'files can be specified, with values in later '
+ 'files taking precedence. The default files used '
+ 'are: %s' % (self.default_config_files, )))
+
+ def __call__(self, args=None):
+ """Parse command line arguments and config files.
+
+ Calling a ConfigOpts object causes the supplied command line arguments
+ and config files to be parsed, causing opt values to be made available
+ as attributes of the object.
+
+ The object may be called multiple times, each time causing the previous
+ set of values to be overwritten.
+
+ :params args: command line arguments (defaults to sys.argv[1:])
+ :returns: the list of arguments left over after parsing options
+ :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError
+ """
+ self.reset()
+
+ self._args = args
+
+ (values, args) = self._oparser.parse_args(self._args)
+
+ self._cli_values = vars(values)
+
+ if self.config_file:
+ self._parse_config_files(self.config_file)
+
+ return args
+
+ def __getattr__(self, name):
+ """Look up an option value and perform string substitution.
+
+ :param name: the opt name (or 'dest', more precisely)
+ :returns: the option value (after string subsititution) or a GroupAttr
+ :raises: NoSuchOptError,ConfigFileValueError,TemplateSubstitutionError
+ """
+ return self._substitute(self._get(name))
+
+ def reset(self):
+ """Reset the state of the object to before it was called."""
+ self._args = None
+ self._cli_values = None
+ self._cparser = None
+
+ def register_opt(self, opt, group=None):
+ """Register an option schema.
+
+ Registering an option schema makes any option value which is previously
+ or subsequently parsed from the command line or config files available
+ as an attribute of this object.
+
+ :param opt: an instance of an Opt sub-class
+ :param group: an optional OptGroup object or group name
+ :return: False if the opt was already register, True otherwise
+ :raises: DuplicateOptError
+ """
+ if group is not None:
+ return self._get_group(group)._register_opt(opt)
+
+ if _is_opt_registered(self._opts, opt):
+ return False
+
+ self._opts[opt.dest] = {'opt': opt, 'override': None, 'default': None}
+
+ return True
+
+ def register_opts(self, opts, group=None):
+ """Register multiple option schemas at once."""
+ for opt in opts:
+ self.register_opt(opt, group)
+
+ def register_cli_opt(self, opt, group=None):
+ """Register a CLI option schema.
+
+ CLI option schemas must be registered before the command line and
+ config files are parsed. This is to ensure that all CLI options are
+ show in --help and option validation works as expected.
+
+ :param opt: an instance of an Opt sub-class
+ :param group: an optional OptGroup object or group name
+ :return: False if the opt was already register, True otherwise
+ :raises: DuplicateOptError, ArgsAlreadyParsedError
+ """
+ if self._args != None:
+ raise ArgsAlreadyParsedError("cannot register CLI option")
+
+ if not self.register_opt(opt, group):
+ return False
+
+ if group is not None:
+ group = self._get_group(group)
+
+ opt._add_to_cli(self._oparser, group)
+
+ return True
+
+ def register_cli_opts(self, opts, group=None):
+ """Register multiple CLI option schemas at once."""
+ for opt in opts:
+ self.register_cli_opt(opt, group)
+
+ def register_group(self, group):
+ """Register an option group.
+
+ An option group must be registered before options can be registered
+ with the group.
+
+ :param group: an OptGroup object
+ """
+ if group.name in self._groups:
+ return
+
+ self._groups[group.name] = copy.copy(group)
+
+ def set_override(self, name, override, group=None):
+ """Override an opt value.
+
+ Override the command line, config file and default values of a
+ given option.
+
+ :param name: the name/dest of the opt
+ :param override: the override value
+ :param group: an option OptGroup object or group name
+ :raises: NoSuchOptError, NoSuchGroupError
+ """
+ opt_info = self._get_opt_info(name, group)
+ opt_info['override'] = override
+
+ def set_default(self, name, default, group=None):
+ """Override an opt's default value.
+
+ Override the default value of given option. A command line or
+ config file value will still take precedence over this default.
+
+ :param name: the name/dest of the opt
+ :param default: the default value
+ :param group: an option OptGroup object or group name
+ :raises: NoSuchOptError, NoSuchGroupError
+ """
+ opt_info = self._get_opt_info(name, group)
+ opt_info['default'] = default
+
+ def log_opt_values(self, logger, lvl):
+ """Log the value of all registered opts.
+
+ It's often useful for an app to log its configuration to a log file at
+ startup for debugging. This method dumps to the entire config state to
+ the supplied logger at a given log level.
+
+ :param logger: a logging.Logger object
+ :param lvl: the log level (e.g. logging.DEBUG) arg to logger.log()
+ """
+ logger.log(lvl, "*" * 80)
+ logger.log(lvl, "Configuration options gathered from:")
+ logger.log(lvl, "command line args: %s", self._args)
+ logger.log(lvl, "config files: %s", self.config_file)
+ logger.log(lvl, "=" * 80)
+
+ for opt_name in sorted(self._opts):
+ logger.log(lvl, "%-30s = %s", opt_name, getattr(self, opt_name))
+
+ for group_name in self._groups:
+ group_attr = self.GroupAttr(self, group_name)
+ for opt_name in sorted(self._groups[group_name]._opts):
+ logger.log(lvl, "%-30s = %s",
+ "%s.%s" % (group_name, opt_name),
+ getattr(group_attr, opt_name))
+
+ logger.log(lvl, "*" * 80)
+
+ def print_usage(self, file=None):
+ """Print the usage message for the current program."""
+ self._oparser.print_usage(file)
+
+ def _get(self, name, group=None):
+ """Look up an option value.
+
+ :param name: the opt name (or 'dest', more precisely)
+ :param group: an option OptGroup
+ :returns: the option value, or a GroupAttr object
+ :raises: NoSuchOptError, NoSuchGroupError, ConfigFileValueError,
+ TemplateSubstitutionError
+ """
+ if group is None and name in self._groups:
+ return self.GroupAttr(self, name)
+
+ if group is not None:
+ group = self._get_group(group)
+
+ info = self._get_opt_info(name, group)
+ default, opt, override = map(lambda k: info[k], sorted(info.keys()))
+
+ if override is not None:
+ return override
+
+ if self._cparser is not None:
+ section = group.name if group is not None else 'DEFAULT'
+ try:
+ return opt._get_from_config_parser(self._cparser, section)
+ except (ConfigParser.NoOptionError,
+ ConfigParser.NoSectionError):
+ pass
+ except ValueError, ve:
+ raise ConfigFileValueError(str(ve))
+
+ name = name if group is None else group.name + '_' + name
+ value = self._cli_values.get(name, None)
+ if value is not None:
+ return value
+
+ if default is not None:
+ return default
+
+ return opt.default
+
+ def _substitute(self, value):
+ """Perform string template substitution.
+
+ Substititue any template variables (e.g. $foo, ${bar}) in the supplied
+ string value(s) with opt values.
+
+ :param value: the string value, or list of string values
+ :returns: the substituted string(s)
+ """
+ if isinstance(value, list):
+ return [self._substitute(i) for i in value]
+ elif isinstance(value, str):
+ tmpl = string.Template(value)
+ return tmpl.safe_substitute(self.StrSubWrapper(self))
+ else:
+ return value
+
+ def _get_group(self, group_or_name):
+ """Looks up a OptGroup object.
+
+ Helper function to return an OptGroup given a parameter which can
+ either be the group's name or an OptGroup object.
+
+ The OptGroup object returned is from the internal dict of OptGroup
+ objects, which will be a copy of any OptGroup object that users of
+ the API have access to.
+
+ :param group_or_name: the group's name or the OptGroup object itself
+ :raises: NoSuchGroupError
+ """
+ if isinstance(group_or_name, OptGroup):
+ group_name = group_or_name.name
+ else:
+ group_name = group_or_name
+
+ if not group_name in self._groups:
+ raise NoSuchGroupError(group_name)
+
+ return self._groups[group_name]
+
+ def _get_opt_info(self, opt_name, group=None):
+ """Return the (opt, override, default) dict for an opt.
+
+ :param opt_name: an opt name/dest
+ :param group: an optional group name or OptGroup object
+ :raises: NoSuchOptError, NoSuchGroupError
+ """
+ if group is None:
+ opts = self._opts
+ else:
+ group = self._get_group(group)
+ opts = group._opts
+
+ if not opt_name in opts:
+ raise NoSuchOptError(opt_name, group)
+
+ return opts[opt_name]
+
+ def _parse_config_files(self, config_files):
+ """Parse the supplied configuration files.
+
+ :raises: ConfigFilesNotFoundError, ConfigFileParseError
+ """
+ self._cparser = ConfigParser.SafeConfigParser()
+
+ try:
+ read_ok = self._cparser.read(config_files)
+ except ConfigParser.ParsingError, cpe:
+ raise ConfigFileParseError(cpe.filename, cpe.message)
+
+ if read_ok != config_files:
+ not_read_ok = filter(lambda f: f not in read_ok, config_files)
+ raise ConfigFilesNotFoundError(not_read_ok)
+
+ class GroupAttr(object):
+
+ """
+ A helper class representing the option values of a group as attributes.
+ """
+
+ def __init__(self, conf, group):
+ """Construct a GroupAttr object.
+
+ :param conf: a ConfigOpts object
+ :param group: a group name or OptGroup object
+ """
+ self.conf = conf
+ self.group = group
+
+ def __getattr__(self, name):
+ """Look up an option value and perform template substitution."""
+ return self.conf._substitute(self.conf._get(name, self.group))
+
+ class StrSubWrapper(object):
+
+ """
+ A helper class exposing opt values as a dict for string substitution.
+ """
+
+ def __init__(self, conf):
+ """Construct a StrSubWrapper object.
+
+ :param conf: a ConfigOpts object
+ """
+ self.conf = conf
+
+ def __getitem__(self, key):
+ """Look up an opt value from the ConfigOpts object.
+
+ :param key: an opt name
+ :returns: an opt value
+ :raises: TemplateSubstitutionError if attribute is a group
+ """
+ value = getattr(self.conf, key)
+ if isinstance(value, self.conf.GroupAttr):
+ raise TemplateSubstitutionError(
+ 'substituting group %s not supported' % key)
+ return value
+
+
+class CommonConfigOpts(ConfigOpts):
+
+ DEFAULT_LOG_FORMAT = ('%(asctime)s %(process)d %(levelname)8s '
+ '[%(name)s] %(message)s')
+ DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+ common_cli_opts = [
+ BoolOpt('debug',
+ short='d',
+ default=False,
+ help='Print debugging output'),
+ BoolOpt('verbose',
+ short='v',
+ default=False,
+ help='Print more verbose output'),
+ ]
+
+ logging_cli_opts = [
+ StrOpt('log-config',
+ metavar='PATH',
+ help='If this option is specified, the logging configuration '
+ 'file specified is used and overrides any other logging '
+ 'options specified. Please see the Python logging module '
+ 'documentation for details on logging configuration '
+ 'files.'),
+ StrOpt('log-format',
+ default=DEFAULT_LOG_FORMAT,
+ metavar='FORMAT',
+ help='A logging.Formatter log message format string which may '
+ 'use any of the available logging.LogRecord attributes. '
+ 'Default: %default'),
+ StrOpt('log-date-format',
+ default=DEFAULT_LOG_DATE_FORMAT,
+ metavar='DATE_FORMAT',
+ help='Format string for %(asctime)s in log records. '
+ 'Default: %default'),
+ StrOpt('log-file',
+ metavar='PATH',
+ help='(Optional) Name of log file to output to. '
+ 'If not set, logging will go to stdout.'),
+ StrOpt('log-dir',
+ help='(Optional) The directory to keep log files in '
+ '(will be prepended to --logfile)'),
+ BoolOpt('use-syslog',
+ default=False,
+ help='Use syslog for logging.'),
+ StrOpt('syslog-log-facility',
+ default='LOG_USER',
+ help='syslog facility to receive log lines')
+ ]
+
+ def __init__(self, **kwargs):
+ super(CommonConfigOpts, self).__init__(**kwargs)
+ self.register_cli_opts(self.common_cli_opts)
+ self.register_cli_opts(self.logging_cli_opts)
--- /dev/null
+# 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
--- /dev/null
+#!/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())
--- /dev/null
+# 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)
--- /dev/null
+#!/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))]
--- /dev/null
+# 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.")
--- /dev/null
+# 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
--- /dev/null
+# 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]
--- /dev/null
+# 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()
--- /dev/null
+# 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())
--- /dev/null
+[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
--- /dev/null
+{
+ "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" }
+ }
+ }
+ }
+}
+