Add the beginnings of an OpenStack ReST API
So far only access to stacks (not resources and events) is implemented. Change-Id: I9655e9441087ef60c06e67e2d6ae68ec4a3b2d11 Signed-off-by: Zane Bitter <zbitter@redhat.com>
This commit is contained in:
parent
f26b218831
commit
9e237f5a66
57
bin/heat-api
Executable file
57
bin/heat-api
Executable file
@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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. An OpenStack ReST API to Heat.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
from heat.openstack.common import cfg
|
||||
from heat.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger('heat.api')
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
cfg.CONF(project='heat', prog='heat-api')
|
||||
config.setup_logging()
|
||||
config.register_api_opts()
|
||||
|
||||
app = config.load_paste_app()
|
||||
|
||||
port = cfg.CONF.bind_port
|
||||
host = cfg.CONF.bind_host
|
||||
LOG.info('Starting Heat ReST API on %s:%s' % (host, port))
|
||||
server = wsgi.Server()
|
||||
server.start(app, cfg.CONF, default_port=port)
|
||||
server.wait()
|
||||
except RuntimeError, e:
|
||||
sys.exit("ERROR: %s" % e)
|
83
etc/heat/heat-api-paste.ini
Normal file
83
etc/heat/heat-api-paste.ini
Normal file
@ -0,0 +1,83 @@
|
||||
|
||||
# Default pipeline
|
||||
[pipeline:heat-api]
|
||||
pipeline = versionnegotiation authtoken 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 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 authtoken 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 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 authtoken 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 auth-context cache cachemanage apiv1app
|
||||
|
||||
[app:apiv1app]
|
||||
paste.app_factory = heat.common.wsgi:app_factory
|
||||
heat.app_factory = heat.api.openstack.v1:API
|
||||
|
||||
[filter:versionnegotiation]
|
||||
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||
heat.filter_factory = heat.api.openstack:version_negotiation_filter
|
||||
|
||||
[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.context:ContextMiddleware_filter_factory
|
||||
|
||||
[filter:authtoken]
|
||||
paste.filter_factory = heat.common.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/v2.0
|
||||
|
||||
# These must be set to your local values in order for the token
|
||||
# authentication to work.
|
||||
admin_tenant_name = admin
|
||||
admin_user = admin
|
||||
admin_password = verybadpass
|
||||
|
||||
[filter:auth-context]
|
||||
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||
heat.filter_factory = keystone.middleware.heat_auth_token:KeystoneContextMiddleware
|
27
etc/heat/heat-api.conf
Normal file
27
etc/heat/heat-api.conf
Normal file
@ -0,0 +1,27 @@
|
||||
[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 = 8004
|
||||
|
||||
# Log to this file. Make sure the user running heat-api-cfn 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
|
||||
|
||||
rpc_backend=heat.openstack.common.rpc.impl_qpid
|
22
heat/api/openstack/__init__.py
Normal file
22
heat/api/openstack/__init__.py
Normal file
@ -0,0 +1,22 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
#
|
||||
# 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.api.middleware.version_negotiation import VersionNegotiationFilter
|
||||
from heat.api.openstack import versions
|
||||
|
||||
|
||||
def version_negotiation_filter(app, conf, **local_conf):
|
||||
return VersionNegotiationFilter(versions.Controller, app,
|
||||
conf, **local_conf)
|
81
heat/api/openstack/v1/__init__.py
Normal file
81
heat/api/openstack/v1/__init__.py
Normal file
@ -0,0 +1,81 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
#
|
||||
# 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 json
|
||||
import urlparse
|
||||
import httplib
|
||||
import routes
|
||||
import gettext
|
||||
|
||||
gettext.install('heat', unicode=1)
|
||||
|
||||
from heat.api.openstack.v1 import stacks
|
||||
from heat.common import wsgi
|
||||
|
||||
from webob import Request
|
||||
import webob
|
||||
from heat import utils
|
||||
from heat.common import context
|
||||
|
||||
from heat.openstack.common import log as logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class API(wsgi.Router):
|
||||
|
||||
"""
|
||||
WSGI router for Heat v1 ReST API requests.
|
||||
"""
|
||||
|
||||
def __init__(self, conf, **local_conf):
|
||||
self.conf = conf
|
||||
mapper = routes.Mapper()
|
||||
|
||||
stacks_resource = stacks.create_resource(conf)
|
||||
|
||||
# Stack collection
|
||||
mapper.connect("stack", "/{tenant_id}/stacks",
|
||||
controller=stacks_resource, action="index",
|
||||
conditions={'method': 'GET'})
|
||||
mapper.connect("stack", "/{tenant_id}/stacks",
|
||||
controller=stacks_resource, action="create",
|
||||
conditions={'method': 'POST'})
|
||||
|
||||
# Stack data
|
||||
mapper.connect("stack", "/{tenant_id}/stacks/{stack_name}",
|
||||
controller=stacks_resource, action="lookup")
|
||||
mapper.connect("stack", "/{tenant_id}/stacks/{stack_name}/{stack_id}",
|
||||
controller=stacks_resource, action="show",
|
||||
conditions={'method': 'GET'})
|
||||
mapper.connect("stack",
|
||||
"/{tenant_id}/stacks/{stack_name}/{stack_id}/template",
|
||||
controller=stacks_resource, action="template",
|
||||
conditions={'method': 'GET'})
|
||||
|
||||
# Stack update/delete
|
||||
mapper.connect("stack", "/{tenant_id}/stacks/{stack_name}/{stack_id}",
|
||||
controller=stacks_resource, action="update",
|
||||
conditions={'method': 'PUT'})
|
||||
mapper.connect("stack", "/{tenant_id}/stacks/{stack_name}/{stack_id}",
|
||||
controller=stacks_resource, action="delete",
|
||||
conditions={'method': 'DELETE'})
|
||||
|
||||
# Template handling
|
||||
mapper.connect("stack", "/validate",
|
||||
controller=stacks_resource, action="validate",
|
||||
conditions={'method': 'POST'})
|
||||
|
||||
super(API, self).__init__(mapper)
|
341
heat/api/openstack/v1/stacks.py
Normal file
341
heat/api/openstack/v1/stacks.py
Normal file
@ -0,0 +1,341 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
#
|
||||
# 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 ReST API.
|
||||
"""
|
||||
|
||||
import httplib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import re
|
||||
import urlparse
|
||||
import webob
|
||||
from webob import exc
|
||||
from functools import wraps
|
||||
|
||||
from heat.common import wsgi
|
||||
from heat.common import config
|
||||
from heat.common import context
|
||||
from heat.common import exception
|
||||
from heat import utils
|
||||
from heat.engine import api as engine_api
|
||||
from heat.engine import identifier
|
||||
from heat.engine import rpcapi as engine_rpcapi
|
||||
|
||||
from heat.openstack.common import rpc
|
||||
import heat.openstack.common.rpc.common as rpc_common
|
||||
from heat.openstack.common import log as logging
|
||||
|
||||
logger = logging.getLogger('heat.api.openstack.v1.stacks')
|
||||
|
||||
CREATE_PARAMS = (
|
||||
PARAM_STACK_NAME,
|
||||
PARAM_TEMPLATE,
|
||||
PARAM_TEMPLATE_URL,
|
||||
PARAM_USER_PARAMS,
|
||||
) = (
|
||||
'stack_name',
|
||||
'template',
|
||||
'template_url',
|
||||
'parameters',
|
||||
)
|
||||
|
||||
|
||||
def json_parse(self, data, data_type):
|
||||
try:
|
||||
return json.loads(data)
|
||||
except ValueError:
|
||||
err_reason = "%s not in valid JSON format" % data_type
|
||||
raise exc.HTTPBadRequest(explanation=err_reason)
|
||||
|
||||
|
||||
def get_template(req):
|
||||
"""
|
||||
Get template file contents, either from local file or URL, in JSON format
|
||||
"""
|
||||
if PARAM_TEMPLATE in req.params:
|
||||
return json_parse(req.params[PARAM_TEMPLATE], 'Template')
|
||||
elif PARAM_TEMPLATE_URL in req.params:
|
||||
logger.debug('Template URL %s' % req.params[PARAM_TEMPLATE_URL])
|
||||
url = urlparse.urlparse(req.params[PARAM_TEMPLATE_URL])
|
||||
err_reason = _("Could not retrieve template")
|
||||
|
||||
try:
|
||||
ConnType = (url.scheme == 'https' and httplib.HTTPSConnection
|
||||
or httplib.HTTPConnection)
|
||||
conn = ConnType(url.netloc)
|
||||
|
||||
try:
|
||||
conn.request("GET", url.path)
|
||||
resp = conn.getresponse()
|
||||
logger.info('status %d' % r1.status)
|
||||
|
||||
if resp.status != 200:
|
||||
raise exc.HTTPBadRequest(explanation=err_reason)
|
||||
|
||||
return json_parse(resp.read(), 'Template')
|
||||
finally:
|
||||
conn.close()
|
||||
except socket.gaierror:
|
||||
raise exc.HTTPBadRequest(explanation=err_reason)
|
||||
|
||||
raise exc.HTTPBadRequest(explanation=_("No template specified"))
|
||||
|
||||
|
||||
def get_user_params(req):
|
||||
"""
|
||||
Get the user-supplied parameters for the stack in JSON format
|
||||
"""
|
||||
if PARAM_USER_PARAMS not in req.params:
|
||||
return {}
|
||||
|
||||
return json_parse(req.params[PARAM_USER_PARAMS], 'User Parameters')
|
||||
|
||||
|
||||
def get_args(req):
|
||||
params = req.params.items()
|
||||
return dict((k, v) for k, v in params if k not in CREATE_PARAMS)
|
||||
|
||||
|
||||
def tenant_local(handler):
|
||||
@wraps(handler)
|
||||
def handle_stack_method(controller, req, tenant_id, **kwargs):
|
||||
req.context.tenant = tenant_id
|
||||
return handler(controller, req, **kwargs)
|
||||
|
||||
return handle_stack_method
|
||||
|
||||
|
||||
def identified_stack(handler):
|
||||
@tenant_local
|
||||
@wraps(handler)
|
||||
def handle_stack_method(controller, req, stack_name, stack_id, **kwargs):
|
||||
stack_identity = identifier.HeatIdentifier(req.context.tenant,
|
||||
stack_name,
|
||||
stack_id)
|
||||
return handler(controller, req, dict(stack_identity), **kwargs)
|
||||
|
||||
return handle_stack_method
|
||||
|
||||
|
||||
def stack_url(req, identity):
|
||||
try:
|
||||
stack_identity = identifier.HeatIdentifier(**identity)
|
||||
except ValueError:
|
||||
err_reason = _("Invalid Stack address")
|
||||
raise exc.HTTPInternalServerError(explanation=err_reason)
|
||||
|
||||
return req.relative_url(stack_identity.url_path(), True)
|
||||
|
||||
|
||||
def format_stack(req, stack, keys=[]):
|
||||
include_key = lambda k: k in keys if keys else True
|
||||
|
||||
def transform(key, value):
|
||||
if key == engine_api.STACK_ID:
|
||||
return 'URL', stack_url(req, value)
|
||||
elif key == engine_api.STACK_PARAMETERS:
|
||||
return key, json.dumps(value)
|
||||
|
||||
return key, value
|
||||
|
||||
return dict(transform(k, v) for k, v in stack.items() if include_key(k))
|
||||
|
||||
|
||||
class StackController(object):
|
||||
"""
|
||||
WSGI controller for stacks resource in Heat v1 API
|
||||
Implements the API actions
|
||||
"""
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = options
|
||||
self.engine_rpcapi = engine_rpcapi.EngineAPI()
|
||||
|
||||
def _remote_error(self, ex):
|
||||
"""
|
||||
Map rpc_common.RemoteError exceptions returned by the engine
|
||||
to webob exceptions which can be used to return
|
||||
properly formatted error responses.
|
||||
"""
|
||||
raise exc.HTTPBadRequest(explanation=str(ex))
|
||||
|
||||
def default(self, req, **args):
|
||||
raise exc.HTTPNotFound()
|
||||
|
||||
@tenant_local
|
||||
def index(self, req):
|
||||
"""
|
||||
Lists summary information for all stacks
|
||||
"""
|
||||
|
||||
try:
|
||||
# Note show_stack returns details for all stacks when called with
|
||||
# no stack_name, we only use a subset of the result here though
|
||||
stack_list = self.engine_rpcapi.show_stack(req.context, None)
|
||||
except rpc_common.RemoteError as ex:
|
||||
return self._remote_error(ex)
|
||||
|
||||
summary_keys = (engine_api.STACK_ID,
|
||||
engine_api.STACK_NAME,
|
||||
engine_api.STACK_DESCRIPTION,
|
||||
engine_api.STACK_STATUS,
|
||||
engine_api.STACK_STATUS_DATA,
|
||||
engine_api.STACK_CREATION_TIME,
|
||||
engine_api.STACK_DELETION_TIME,
|
||||
engine_api.STACK_UPDATED_TIME)
|
||||
|
||||
stacks = stack_list['stacks']
|
||||
|
||||
return {'stacks': [format_stack(req, s, summary_keys) for s in stacks]}
|
||||
|
||||
@tenant_local
|
||||
def create(self, req):
|
||||
"""
|
||||
Create a new stack
|
||||
"""
|
||||
if PARAM_STACK_NAME not in req.params:
|
||||
raise exc.HTTPBadRequest(explanation=_("No stack name specified"))
|
||||
stack_name = req.params[PARAMS_STACK_NAME]
|
||||
|
||||
stack_params = get_user_params(req)
|
||||
template = get_template(req)
|
||||
args = get_args(req)
|
||||
|
||||
try:
|
||||
identity = self.engine_rpcapi.create_stack(req.context,
|
||||
stack_name,
|
||||
template,
|
||||
stack_params,
|
||||
args)
|
||||
except rpc_common.RemoteError as ex:
|
||||
return self._remote_error(ex)
|
||||
|
||||
raise exc.HTTPCreated(location=stack_url(req, identity))
|
||||
|
||||
@tenant_local
|
||||
def lookup(self, req, stack_name):
|
||||
"""
|
||||
Redirect to the canonical URL for a stack
|
||||
"""
|
||||
|
||||
try:
|
||||
identity = self.engine_rpcapi.identify_stack(req.context,
|
||||
stack_name)
|
||||
except rpc_common.RemoteError as ex:
|
||||
return self._remote_error(ex)
|
||||
|
||||
raise exc.HTTPFound(location=stack_url(req, identity))
|
||||
|
||||
@identified_stack
|
||||
def show(self, req, identity):
|
||||
"""
|
||||
Gets detailed information for a stack
|
||||
"""
|
||||
|
||||
try:
|
||||
stack_list = self.engine_rpcapi.show_stack(req.context,
|
||||
identity)
|
||||
except rpc_common.RemoteError as ex:
|
||||
return self._remote_error(ex)
|
||||
|
||||
if not stack_list['stacks']:
|
||||
raise exc.HTTPNotFound()
|
||||
|
||||
stack = stack_list['stacks'][0]
|
||||
|
||||
return {'stack': format_stack(req, stack)}
|
||||
|
||||
@identified_stack
|
||||
def template(self, req, identity):
|
||||
"""
|
||||
Get the template body for an existing stack
|
||||
"""
|
||||
|
||||
try:
|
||||
templ = self.engine_rpcapi.get_template(req.context,
|
||||
identity)
|
||||
except rpc_common.RemoteError as ex:
|
||||
return self._remote_error(ex)
|
||||
|
||||
if templ is None:
|
||||
raise exc.HTTPNotFound()
|
||||
|
||||
# TODO(zaneb): always set Content-type to application/json
|
||||
return json.dumps(templ)
|
||||
|
||||
@identified_stack
|
||||
def update(self, req, identity):
|
||||
"""
|
||||
Update an existing stack with a new template and/or parameters
|
||||
"""
|
||||
stack_params = get_user_params(req)
|
||||
template = get_template(req)
|
||||
args = get_args(req)
|
||||
|
||||
try:
|
||||
res = self.engine_rpcapi.update_stack(req.context,
|
||||
identity,
|
||||
template,
|
||||
stack_params, args)
|
||||
except rpc_common.RemoteError as ex:
|
||||
return self._remote_error(ex)
|
||||
|
||||
raise exc.HTTPAccepted()
|
||||
|
||||
@identified_stack
|
||||
def delete(self, req, identity):
|
||||
"""
|
||||
Delete the specified stack
|
||||
"""
|
||||
|
||||
try:
|
||||
res = self.engine_rpcapi.delete_stack(req.context,
|
||||
identity,
|
||||
cast=False)
|
||||
|
||||
except rpc_common.RemoteError as ex:
|
||||
return self._remote_error(ex)
|
||||
|
||||
if res is not None:
|
||||
raise exc.HTTPBadRequest(explanation=res['Error'])
|
||||
|
||||
raise exc.HTTPNoContent()
|
||||
|
||||
def validate_template(self, req):
|
||||
"""
|
||||
Implements the ValidateTemplate API action
|
||||
Validates the specified template
|
||||
"""
|
||||
|
||||
template = get_template(req)
|
||||
stack_params = get_user_params(req)
|
||||
|
||||
try:
|
||||
return self.engine_rpcapi.validate_template(req.context,
|
||||
template, params)
|
||||
except rpc_common.RemoteError as ex:
|
||||
return self._remote_error(ex)
|
||||
|
||||
|
||||
def create_resource(options):
|
||||
"""
|
||||
Stacks resource factory method.
|
||||
"""
|
||||
deserializer = wsgi.JSONRequestDeserializer()
|
||||
return wsgi.Resource(StackController(options), deserializer)
|
59
heat/api/openstack/versions.py
Normal file
59
heat/api/openstack/versions.py
Normal file
@ -0,0 +1,59 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
#
|
||||
# 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
|
||||
|
||||
|
||||
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.0",
|
||||
"status": "CURRENT",
|
||||
"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
|
@ -73,9 +73,9 @@ class HeatIdentifier(collections.Mapping):
|
||||
def url_path(self):
|
||||
'''
|
||||
Return a URL-encoded path segment of a URL in the form:
|
||||
/<tenant>/stacks/<stack_name>/<stack_id><path>
|
||||
<tenant>/stacks/<stack_name>/<stack_id><path>
|
||||
'''
|
||||
return '/%s/%s' % (urllib.quote(self.tenant, ''), self._tenant_path())
|
||||
return '/'.join((urllib.quote(self.tenant, ''), self._tenant_path()))
|
||||
|
||||
def _tenant_path(self):
|
||||
'''
|
||||
|
@ -130,34 +130,34 @@ class IdentifierTest(unittest.TestCase):
|
||||
|
||||
def test_url_path(self):
|
||||
hi = identifier.HeatIdentifier('t', 's', 'i', 'p')
|
||||
self.assertEqual(hi.url_path(), '/t/stacks/s/i/p')
|
||||
self.assertEqual(hi.url_path(), 't/stacks/s/i/p')
|
||||
|
||||
def test_url_path_default(self):
|
||||
hi = identifier.HeatIdentifier('t', 's', 'i')
|
||||
self.assertEqual(hi.url_path(), '/t/stacks/s/i')
|
||||
self.assertEqual(hi.url_path(), 't/stacks/s/i')
|
||||
|
||||
def test_tenant_escape(self):
|
||||
hi = identifier.HeatIdentifier(':/', 's', 'i')
|
||||
self.assertEqual(hi.tenant, ':/')
|
||||
self.assertEqual(hi.url_path(), '/%3A%2F/stacks/s/i')
|
||||
self.assertEqual(hi.url_path(), '%3A%2F/stacks/s/i')
|
||||
self.assertEqual(hi.arn(), 'arn:openstack:heat::%3A%2F:stacks/s/i')
|
||||
|
||||
def test_name_escape(self):
|
||||
hi = identifier.HeatIdentifier('t', ':/', 'i')
|
||||
self.assertEqual(hi.stack_name, ':/')
|
||||
self.assertEqual(hi.url_path(), '/t/stacks/%3A%2F/i')
|
||||
self.assertEqual(hi.url_path(), 't/stacks/%3A%2F/i')
|
||||
self.assertEqual(hi.arn(), 'arn:openstack:heat::t:stacks/%3A%2F/i')
|
||||
|
||||
def test_id_escape(self):
|
||||
hi = identifier.HeatIdentifier('t', 's', ':/')
|
||||
self.assertEqual(hi.stack_id, ':/')
|
||||
self.assertEqual(hi.url_path(), '/t/stacks/s/%3A%2F')
|
||||
self.assertEqual(hi.url_path(), 't/stacks/s/%3A%2F')
|
||||
self.assertEqual(hi.arn(), 'arn:openstack:heat::t:stacks/s/%3A%2F')
|
||||
|
||||
def test_path_escape(self):
|
||||
hi = identifier.HeatIdentifier('t', 's', 'i', ':/')
|
||||
self.assertEqual(hi.path, '/:/')
|
||||
self.assertEqual(hi.url_path(), '/t/stacks/s/i/%3A/')
|
||||
self.assertEqual(hi.url_path(), 't/stacks/s/i/%3A/')
|
||||
self.assertEqual(hi.arn(), 'arn:openstack:heat::t:stacks/s/i/%3A/')
|
||||
|
||||
def test_tenant_decode(self):
|
||||
|
16
install.sh
16
install.sh
@ -14,22 +14,6 @@ LOG_DIR=/var/log/heat
|
||||
install -d $LOG_DIR
|
||||
|
||||
|
||||
archive_file() {
|
||||
local f=$1
|
||||
|
||||
if [ -e $CONF_PREFIX/$f ]; then
|
||||
echo "Archiving configuration file $CONF_PREFIX/$f" >&2
|
||||
mv $CONF_PREFIX/$f $CONF_PREFIX/$f.bak
|
||||
fi
|
||||
}
|
||||
|
||||
# Archive existing heat-api* config files in preparation
|
||||
# for change to heat-api-cfn*, and future use of heat-api*
|
||||
# for the OpenStack API.
|
||||
archive_file etc/heat/heat-api.conf
|
||||
archive_file etc/heat/heat-api-paste.ini
|
||||
|
||||
|
||||
install_dir() {
|
||||
local dir=$1
|
||||
local prefix=$2
|
||||
|
@ -46,7 +46,7 @@ function run_tests {
|
||||
function run_pep8 {
|
||||
echo "Running pep8..."
|
||||
PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat"
|
||||
PEP8_INCLUDE="bin/heat bin/heat-boto bin/heat-api-cfn bin/heat-engine heat tools setup.py heat/testing/runner.py"
|
||||
PEP8_INCLUDE="bin/heat bin/heat-boto bin/heat-api-cfn bin/heat-api bin/heat-engine heat tools setup.py heat/testing/runner.py"
|
||||
${wrapper} pep8 $PEP8_OPTIONS $PEP8_INCLUDE
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user