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):
|
def url_path(self):
|
||||||
'''
|
'''
|
||||||
Return a URL-encoded path segment of a URL in the form:
|
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):
|
def _tenant_path(self):
|
||||||
'''
|
'''
|
||||||
|
@ -130,34 +130,34 @@ class IdentifierTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_url_path(self):
|
def test_url_path(self):
|
||||||
hi = identifier.HeatIdentifier('t', 's', 'i', 'p')
|
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):
|
def test_url_path_default(self):
|
||||||
hi = identifier.HeatIdentifier('t', 's', 'i')
|
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):
|
def test_tenant_escape(self):
|
||||||
hi = identifier.HeatIdentifier(':/', 's', 'i')
|
hi = identifier.HeatIdentifier(':/', 's', 'i')
|
||||||
self.assertEqual(hi.tenant, ':/')
|
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')
|
self.assertEqual(hi.arn(), 'arn:openstack:heat::%3A%2F:stacks/s/i')
|
||||||
|
|
||||||
def test_name_escape(self):
|
def test_name_escape(self):
|
||||||
hi = identifier.HeatIdentifier('t', ':/', 'i')
|
hi = identifier.HeatIdentifier('t', ':/', 'i')
|
||||||
self.assertEqual(hi.stack_name, ':/')
|
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')
|
self.assertEqual(hi.arn(), 'arn:openstack:heat::t:stacks/%3A%2F/i')
|
||||||
|
|
||||||
def test_id_escape(self):
|
def test_id_escape(self):
|
||||||
hi = identifier.HeatIdentifier('t', 's', ':/')
|
hi = identifier.HeatIdentifier('t', 's', ':/')
|
||||||
self.assertEqual(hi.stack_id, ':/')
|
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')
|
self.assertEqual(hi.arn(), 'arn:openstack:heat::t:stacks/s/%3A%2F')
|
||||||
|
|
||||||
def test_path_escape(self):
|
def test_path_escape(self):
|
||||||
hi = identifier.HeatIdentifier('t', 's', 'i', ':/')
|
hi = identifier.HeatIdentifier('t', 's', 'i', ':/')
|
||||||
self.assertEqual(hi.path, '/:/')
|
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/')
|
self.assertEqual(hi.arn(), 'arn:openstack:heat::t:stacks/s/i/%3A/')
|
||||||
|
|
||||||
def test_tenant_decode(self):
|
def test_tenant_decode(self):
|
||||||
|
16
install.sh
16
install.sh
@ -14,22 +14,6 @@ LOG_DIR=/var/log/heat
|
|||||||
install -d $LOG_DIR
|
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() {
|
install_dir() {
|
||||||
local dir=$1
|
local dir=$1
|
||||||
local prefix=$2
|
local prefix=$2
|
||||||
|
@ -46,7 +46,7 @@ function run_tests {
|
|||||||
function run_pep8 {
|
function run_pep8 {
|
||||||
echo "Running pep8..."
|
echo "Running pep8..."
|
||||||
PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat"
|
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
|
${wrapper} pep8 $PEP8_OPTIONS $PEP8_INCLUDE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
setup.py
1
setup.py
@ -44,6 +44,7 @@ setuptools.setup(
|
|||||||
'Environment :: No Input/Output (Daemon)',
|
'Environment :: No Input/Output (Daemon)',
|
||||||
],
|
],
|
||||||
scripts=['bin/heat',
|
scripts=['bin/heat',
|
||||||
|
'bin/heat-api',
|
||||||
'bin/heat-api-cfn',
|
'bin/heat-api-cfn',
|
||||||
'bin/heat-api-cloudwatch',
|
'bin/heat-api-cloudwatch',
|
||||||
'bin/heat-boto',
|
'bin/heat-boto',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user