Start separating the api and the implementation.
Signed-off-by: Angus Salkeld <asalkeld@redhat.com>
This commit is contained in:
parent
4abbc9393e
commit
6161c7aa85
4
bin/heat
4
bin/heat
@ -179,7 +179,9 @@ def stack_describe(options, arguments):
|
||||
try:
|
||||
parameters['StackName'] = arguments.pop(0)
|
||||
except IndexError:
|
||||
print "Describing all stacks"
|
||||
print "Please specify the stack name you wish to describe "
|
||||
print "as the first argument"
|
||||
return FAILURE
|
||||
|
||||
c = get_client(options)
|
||||
result = c.describe_stacks(**parameters)
|
||||
|
50
bin/heat-engine
Executable file
50
bin/heat-engine
Executable file
@ -0,0 +1,50 @@
|
||||
#!/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 Engine 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=config.DEFAULT_PORT+1)
|
||||
server.wait()
|
||||
except RuntimeError, e:
|
||||
sys.exit("ERROR: %s" % e)
|
@ -44,7 +44,7 @@ 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
|
||||
heat.app_factory = heat.api.v1:API
|
||||
|
||||
[filter:versionnegotiation]
|
||||
paste.filter_factory = heat.common.wsgi:filter_factory
|
||||
|
36
etc/heat-engine-paste.ini
Normal file
36
etc/heat-engine-paste.ini
Normal file
@ -0,0 +1,36 @@
|
||||
# Default minimal pipeline
|
||||
[pipeline:heat-engine]
|
||||
pipeline = context engineapp
|
||||
|
||||
# Use the following pipeline for keystone auth
|
||||
# i.e. in heat-engine.conf:
|
||||
# [paste_deploy]
|
||||
# flavor = keystone
|
||||
#
|
||||
[pipeline:heat-engine-keystone]
|
||||
pipeline = authtoken auth-context engineapp
|
||||
|
||||
[app:engineapp]
|
||||
paste.app_factory = heat.common.wsgi:app_factory
|
||||
heat.app_factory = heat.engine.api.v1:API
|
||||
|
||||
[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
|
25
etc/heat-engine.conf
Normal file
25
etc/heat-engine.conf
Normal file
@ -0,0 +1,25 @@
|
||||
[DEFAULT]
|
||||
# Show more verbose log output (sets INFO log level output)
|
||||
verbose = True
|
||||
|
||||
# Show debugging output in logs (sets DEBUG log level output)
|
||||
debug = True
|
||||
|
||||
# Address to bind the server to
|
||||
bind_host = 0.0.0.0
|
||||
|
||||
# Port the bind the server to
|
||||
bind_port = 8001
|
||||
|
||||
# Log to this file. Make sure the user running heat-api has
|
||||
# permissions to write to this file!
|
||||
log_file = /var/log/heat/engine.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
|
@ -13,7 +13,38 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
SUPPORTED_PARAMS = ('StackName', 'TemplateBody', 'TemplateUrl','NotificationARNs', 'Parameters',
|
||||
'Version', 'SignatureVersion', 'Timestamp', 'AWSAccessKeyId',
|
||||
'Signature')
|
||||
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 GetTemplate, 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="describe", conditions=dict(method=["GET"]))
|
||||
mapper.connect("/DeleteStack", controller=stacks_resource,
|
||||
action="delete", conditions=dict(method=["DELETE"]))
|
||||
mapper.connect("/UpdateStack", controller=stacks_resource,
|
||||
action="update", conditions=dict(method=["PUT"]))
|
||||
|
||||
super(API, self).__init__(mapper)
|
||||
|
@ -16,17 +16,10 @@
|
||||
"""
|
||||
/stack endpoint for heat v1 API
|
||||
"""
|
||||
import dbus
|
||||
import errno
|
||||
import eventlet
|
||||
from eventlet.green import socket
|
||||
import fcntl
|
||||
import httplib
|
||||
import json
|
||||
import libxml2
|
||||
import logging
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import urlparse
|
||||
|
||||
@ -35,283 +28,11 @@ from webob.exc import (HTTPNotFound,
|
||||
HTTPConflict,
|
||||
HTTPBadRequest)
|
||||
|
||||
from heat.common import exception
|
||||
from heat.common import utils
|
||||
from heat.common import wsgi
|
||||
from heat.engine import client as engine
|
||||
|
||||
logger = logging.getLogger('heat.api.v1.stacks')
|
||||
|
||||
stack_db = {}
|
||||
|
||||
|
||||
class Json2CapeXml:
|
||||
def __init__(self, template, stack_name):
|
||||
|
||||
self.t = template
|
||||
self.parms = self.t['Parameters']
|
||||
self.maps = self.t['Mappings']
|
||||
self.res = {}
|
||||
|
||||
self.parms['AWS::Region'] = {"Description" : "AWS Regions", "Type" : "String", "Default" : "ap-southeast-1",
|
||||
"AllowedValues" : ["us-east-1","us-west-1","us-west-2","sa-east-1","eu-west-1","ap-southeast-1","ap-northeast-1"],
|
||||
"ConstraintDescription" : "must be a valid EC2 instance type." }
|
||||
|
||||
# expected user parameters
|
||||
self.parms['AWS::StackName'] = {'Default': stack_name}
|
||||
self.parms['KeyName'] = {'Default': 'harry-45-5-34-5'}
|
||||
|
||||
for r in self.t['Resources']:
|
||||
# fake resource instance references
|
||||
self.parms[r] = {'Default': utils.generate_uuid()}
|
||||
|
||||
self.resolve_static_refs(self.t['Resources'])
|
||||
self.resolve_find_in_map(self.t['Resources'])
|
||||
#self.resolve_attributes(self.t['Resources'])
|
||||
self.resolve_joins(self.t['Resources'])
|
||||
self.resolve_base64(self.t['Resources'])
|
||||
#print json.dumps(self.t['Resources'], indent=2)
|
||||
|
||||
|
||||
def convert_and_write(self):
|
||||
|
||||
name = self.parms['AWS::StackName']['Default']
|
||||
|
||||
doc = libxml2.newDoc("1.0")
|
||||
dep = doc.newChild(None, "deployable", None)
|
||||
dep.setProp("name", name)
|
||||
dep.setProp("uuid", 'bogus')
|
||||
dep.setProp("monitor", 'active')
|
||||
dep.setProp("username", 'nobody-yet')
|
||||
n_asses = dep.newChild(None, "assemblies", None)
|
||||
|
||||
for r in self.t['Resources']:
|
||||
type = self.t['Resources'][r]['Type']
|
||||
if type != 'AWS::EC2::Instance':
|
||||
print 'ignoring Resource %s (%s)' % (r, type)
|
||||
continue
|
||||
|
||||
n_ass = n_asses.newChild(None, 'assembly', None)
|
||||
n_ass.setProp("name", r)
|
||||
n_ass.setProp("uuid", self.parms[r]['Default'])
|
||||
props = self.t['Resources'][r]['Properties']
|
||||
for p in props:
|
||||
if p == 'ImageId':
|
||||
n_ass.setProp("image_name", props[p])
|
||||
elif p == 'UserData':
|
||||
new_script = []
|
||||
script_lines = props[p].split('\n')
|
||||
for l in script_lines:
|
||||
if '#!/' in l:
|
||||
new_script.append(l)
|
||||
self.insert_package_and_services(self.t['Resources'][r], new_script)
|
||||
else:
|
||||
new_script.append(l)
|
||||
|
||||
startup = n_ass.newChild(None, 'startup', '\n'.join(new_script))
|
||||
|
||||
|
||||
try:
|
||||
con = self.t['Resources'][r]['Metadata']["AWS::CloudFormation::Init"]['config']
|
||||
n_services = n_ass.newChild(None, 'services', None)
|
||||
for st in con['services']:
|
||||
for s in con['services'][st]:
|
||||
n_service = n_services.newChild(None, 'service', None)
|
||||
n_service.setProp("name", '%s_%s' % (r, s))
|
||||
n_service.setProp("type", s)
|
||||
n_service.setProp("provider", 'pacemaker')
|
||||
n_service.setProp("class", 'lsb')
|
||||
n_service.setProp("monitor_interval", '30s')
|
||||
n_service.setProp("escalation_period", '1000')
|
||||
n_service.setProp("escalation_failures", '3')
|
||||
except KeyError as e:
|
||||
# if there is no config then no services.
|
||||
pass
|
||||
|
||||
try:
|
||||
filename = '/var/run/%s.xml' % name
|
||||
open(filename, 'w').write(doc.serialize(None, 1))
|
||||
doc.freeDoc()
|
||||
except IOError as e:
|
||||
logger.error('couldn\'t write to /var/run/ error %s' % e)
|
||||
|
||||
def insert_package_and_services(self, r, new_script):
|
||||
|
||||
try:
|
||||
con = r['Metadata']["AWS::CloudFormation::Init"]['config']
|
||||
except KeyError as e:
|
||||
return
|
||||
|
||||
for pt in con['packages']:
|
||||
if pt == 'yum':
|
||||
for p in con['packages']['yum']:
|
||||
new_script.append('yum install -y %s' % p)
|
||||
for st in con['services']:
|
||||
if st == 'systemd':
|
||||
for s in con['services']['systemd']:
|
||||
v = con['services']['systemd'][s]
|
||||
if v['enabled'] == 'true':
|
||||
new_script.append('systemctl enable %s.service' % s)
|
||||
if v['ensureRunning'] == 'true':
|
||||
new_script.append('systemctl start %s.service' % s)
|
||||
elif st == 'sysvinit':
|
||||
for s in con['services']['sysvinit']:
|
||||
v = con['services']['systemd'][s]
|
||||
if v['enabled'] == 'true':
|
||||
new_script.append('chkconfig %s on' % s)
|
||||
if v['ensureRunning'] == 'true':
|
||||
new_script.append('/etc/init.d/start %s' % s)
|
||||
|
||||
def resolve_static_refs(self, s):
|
||||
'''
|
||||
looking for { "Ref": "str" }
|
||||
'''
|
||||
if isinstance(s, dict):
|
||||
for i in s:
|
||||
if i == 'Ref' and isinstance(s[i], (basestring, unicode)) and \
|
||||
self.parms.has_key(s[i]):
|
||||
if self.parms[s[i]] == None:
|
||||
print 'None Ref: %s' % str(s[i])
|
||||
elif self.parms[s[i]].has_key('Default'):
|
||||
# note the "ref: values" are in a dict of
|
||||
# size one, so return is fine.
|
||||
#print 'Ref: %s == %s' % (s[i], self.parms[s[i]]['Default'])
|
||||
return self.parms[s[i]]['Default']
|
||||
else:
|
||||
print 'missing Ref: %s' % str(s[i])
|
||||
else:
|
||||
s[i] = self.resolve_static_refs(s[i])
|
||||
elif isinstance(s, list):
|
||||
for index, item in enumerate(s):
|
||||
#print 'resolve_static_refs %d %s' % (index, item)
|
||||
s[index] = self.resolve_static_refs(item)
|
||||
return s
|
||||
|
||||
def resolve_find_in_map(self, s):
|
||||
'''
|
||||
looking for { "Ref": "str" }
|
||||
'''
|
||||
if isinstance(s, dict):
|
||||
for i in s:
|
||||
if i == 'Fn::FindInMap':
|
||||
obj = self.maps
|
||||
if isinstance(s[i], list):
|
||||
#print 'map list: %s' % s[i]
|
||||
for index, item in enumerate(s[i]):
|
||||
if isinstance(item, dict):
|
||||
item = self.resolve_find_in_map(item)
|
||||
#print 'map item dict: %s' % (item)
|
||||
else:
|
||||
pass
|
||||
#print 'map item str: %s' % (item)
|
||||
obj = obj[item]
|
||||
else:
|
||||
obj = obj[s[i]]
|
||||
return obj
|
||||
else:
|
||||
s[i] = self.resolve_find_in_map(s[i])
|
||||
elif isinstance(s, list):
|
||||
for index, item in enumerate(s):
|
||||
s[index] = self.resolve_find_in_map(item)
|
||||
return s
|
||||
|
||||
|
||||
def resolve_joins(self, s):
|
||||
'''
|
||||
looking for { "Fn::join": [] }
|
||||
'''
|
||||
if isinstance(s, dict):
|
||||
for i in s:
|
||||
if i == 'Fn::Join':
|
||||
return s[i][0].join(s[i][1])
|
||||
else:
|
||||
s[i] = self.resolve_joins(s[i])
|
||||
elif isinstance(s, list):
|
||||
for index, item in enumerate(s):
|
||||
s[index] = self.resolve_joins(item)
|
||||
return s
|
||||
|
||||
|
||||
def resolve_base64(self, s):
|
||||
'''
|
||||
looking for { "Fn::join": [] }
|
||||
'''
|
||||
if isinstance(s, dict):
|
||||
for i in s:
|
||||
if i == 'Fn::Base64':
|
||||
return s[i]
|
||||
else:
|
||||
s[i] = self.resolve_base64(s[i])
|
||||
elif isinstance(s, list):
|
||||
for index, item in enumerate(s):
|
||||
s[index] = self.resolve_base64(item)
|
||||
return s
|
||||
|
||||
def systemctl(method, name, instance=None):
|
||||
|
||||
bus = dbus.SystemBus()
|
||||
|
||||
sysd = bus.get_object('org.freedesktop.systemd1',
|
||||
'/org/freedesktop/systemd1')
|
||||
|
||||
actual_method = ''
|
||||
if method == 'start':
|
||||
actual_method = 'StartUnit'
|
||||
elif method == 'stop':
|
||||
actual_method = 'StopUnit'
|
||||
else:
|
||||
raise
|
||||
|
||||
m = sysd.get_dbus_method(actual_method, 'org.freedesktop.systemd1.Manager')
|
||||
|
||||
if instance == None:
|
||||
service = '%s.service' % (name)
|
||||
else:
|
||||
service = '%s@%s.service' % (name, instance)
|
||||
|
||||
try:
|
||||
result = m(service, 'replace')
|
||||
except dbus.DBusException as e:
|
||||
logger.error('couldn\'t %s %s error: %s' % (method, name, e))
|
||||
return None
|
||||
return result
|
||||
|
||||
|
||||
class CapeEventListener:
|
||||
|
||||
def __init__(self):
|
||||
self.backlog = 50
|
||||
self.file = 'pacemaker-cloud-cped'
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
flags = fcntl.fcntl(sock, fcntl.F_GETFD)
|
||||
fcntl.fcntl(sock, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
st = os.stat(self.file)
|
||||
except OSError, err:
|
||||
if err.errno != errno.ENOENT:
|
||||
raise
|
||||
else:
|
||||
if stat.S_ISSOCK(st.st_mode):
|
||||
os.remove(self.file)
|
||||
else:
|
||||
raise ValueError("File %s exists and is not a socket", self.file)
|
||||
sock.bind(self.file)
|
||||
sock.listen(self.backlog)
|
||||
os.chmod(self.file, 0600)
|
||||
|
||||
eventlet.spawn_n(self.cape_event_listner, sock)
|
||||
|
||||
def cape_event_listner(self, sock):
|
||||
eventlet.serve(sock, self.cape_event_handle)
|
||||
|
||||
def cape_event_handle(self, sock, client_addr):
|
||||
while True:
|
||||
x = sock.recv(4096)
|
||||
# TODO(asalkeld) format this event "nicely"
|
||||
logger.info('%s' % x.strip('\n'))
|
||||
if not x: break
|
||||
|
||||
|
||||
class StackController(object):
|
||||
|
||||
@ -322,51 +43,34 @@ class StackController(object):
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = options
|
||||
self.stack_id = 1
|
||||
self.event_listener = CapeEventListener()
|
||||
engine.configure_engine_client(options)
|
||||
|
||||
def list(self, req):
|
||||
"""
|
||||
Returns the following information for all stacks:
|
||||
"""
|
||||
c = engine.get_engine_client(req.context)
|
||||
stack_list = c.get_stacks(**req.params)
|
||||
|
||||
res = {'ListStacksResponse': {'ListStacksResult': {'StackSummaries': [] } } }
|
||||
summaries = res['ListStacksResponse']['ListStacksResult']['StackSummaries']
|
||||
for s in stack_db:
|
||||
mem = {}
|
||||
mem['StackId'] = stack_db[s]['StackId']
|
||||
mem['StackName'] = s
|
||||
mem['CreationTime'] = 'now'
|
||||
try:
|
||||
mem['TemplateDescription'] = stack_db[s]['Description']
|
||||
mem['StackStatus'] = stack_db[s]['StackStatus']
|
||||
except:
|
||||
mem['TemplateDescription'] = 'No description'
|
||||
mem['StackStatus'] = 'unknown'
|
||||
summaries.append(mem)
|
||||
for s in stack_list:
|
||||
summaries.append(s)
|
||||
|
||||
return res
|
||||
|
||||
def describe(self, req):
|
||||
"""
|
||||
Returns the following information for all stacks:
|
||||
"""
|
||||
c = engine.get_engine_client(req.context)
|
||||
|
||||
stack_name = None
|
||||
if req.params.has_key('StackName'):
|
||||
stack_name = req.params['StackName']
|
||||
if not stack_db.has_key(stack_name):
|
||||
msg = _("Stack does not exist with that name.")
|
||||
return webob.exc.HTTPNotFound(msg)
|
||||
|
||||
stack_list = c.show_stack(req.params['StackName'])
|
||||
res = {'DescribeStacksResult': {'Stacks': [] } }
|
||||
summaries = res['DescribeStacksResult']['Stacks']
|
||||
for s in stack_db:
|
||||
if stack_name is None or s == stack_name:
|
||||
mem = {}
|
||||
mem['StackId'] = stack_db[s]['StackId']
|
||||
mem['StackStatus'] = stack_db[s]['StackStatus']
|
||||
mem['StackName'] = s
|
||||
mem['CreationTime'] = 'now'
|
||||
mem['DisableRollback'] = 'false'
|
||||
mem['Outputs'] = '[]'
|
||||
summaries.append(mem)
|
||||
stacks = res['DescribeStacksResult']['Stacks']
|
||||
for s in stack_list:
|
||||
mem = {'member': s}
|
||||
stacks.append(mem)
|
||||
|
||||
return res
|
||||
|
||||
@ -393,20 +97,12 @@ class StackController(object):
|
||||
|
||||
return None
|
||||
|
||||
def _apply_user_parameters(self, req, stack):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
def create(self, req):
|
||||
"""
|
||||
:param req: The WSGI/Webob Request object
|
||||
|
||||
:raises HttpBadRequest if not template is given
|
||||
:raises HttpConflict if object already exists
|
||||
Returns the following information for all stacks:
|
||||
"""
|
||||
if stack_db.has_key(req.params['StackName']):
|
||||
msg = _("Stack already exists with that name.")
|
||||
return webob.exc.HTTPConflict(msg)
|
||||
c = engine.get_engine_client(req.context)
|
||||
|
||||
templ = self._get_template(req)
|
||||
if templ is None:
|
||||
@ -414,66 +110,25 @@ class StackController(object):
|
||||
return webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
stack = json.loads(templ)
|
||||
my_id = '%s-%d' % (req.params['StackName'], self.stack_id)
|
||||
self.stack_id = self.stack_id + 1
|
||||
stack['StackId'] = my_id
|
||||
stack['StackStatus'] = 'CREATE_COMPLETE'
|
||||
self._apply_user_parameters(req, stack)
|
||||
stack_db[req.params['StackName']] = stack
|
||||
|
||||
cape_transformer = Json2CapeXml(stack, req.params['StackName'])
|
||||
cape_transformer.convert_and_write()
|
||||
|
||||
systemctl('start', 'pcloud-cape-sshd', req.params['StackName'])
|
||||
|
||||
return {'CreateStackResult': {'StackId': my_id}}
|
||||
|
||||
def update(self, req):
|
||||
"""
|
||||
:param req: The WSGI/Webob Request object
|
||||
|
||||
:raises HttpNotFound if object is not available
|
||||
"""
|
||||
if not stack_db.has_key(req.params['StackName']):
|
||||
msg = _("Stack does not exist with that name.")
|
||||
return webob.exc.HTTPNotFound(msg)
|
||||
|
||||
stack = stack_db[req.params['StackName']]
|
||||
my_id = stack['StackId']
|
||||
templ = self._get_template(req)
|
||||
if templ:
|
||||
stack = json.loads(templ)
|
||||
stack['StackId'] = my_id
|
||||
stack_db[req.params['StackName']] = stack
|
||||
|
||||
self._apply_user_parameters(req, stack)
|
||||
stack['StackStatus'] = 'UPDATE_COMPLETE'
|
||||
|
||||
return {'UpdateStackResult': {'StackId': my_id}}
|
||||
stack['StackName'] = req.params['StackName']
|
||||
|
||||
return c.create_stack(stack)
|
||||
|
||||
def delete(self, req):
|
||||
"""
|
||||
Deletes the object and all its resources
|
||||
|
||||
:param req: The WSGI/Webob Request object
|
||||
|
||||
:raises HttpBadRequest if the request is invalid
|
||||
:raises HttpNotFound if object is not available
|
||||
:raises HttpNotAuthorized if object is not
|
||||
deleteable by the requesting user
|
||||
Returns the following information for all stacks:
|
||||
"""
|
||||
logger.info('in delete %s ' % req.params['StackName'])
|
||||
if not stack_db.has_key(req.params['StackName']):
|
||||
msg = _("Stack does not exist with that name.")
|
||||
return webob.exc.HTTPNotFound(msg)
|
||||
logger.info('in api delete ')
|
||||
c = engine.get_engine_client(req.context)
|
||||
res = c.delete_stack(req.params['StackName'])
|
||||
if res.status == 200:
|
||||
return {'DeleteStackResult': ''}
|
||||
else:
|
||||
return webob.exc.HTTPNotFound()
|
||||
|
||||
del stack_db[req.params['StackName']]
|
||||
|
||||
systemctl('stop', 'pcloud-cape-sshd', req.params['StackName'])
|
||||
|
||||
def create_resource(options):
|
||||
"""Stacks resource factory method"""
|
||||
"""Stacks resource factory method."""
|
||||
deserializer = wsgi.JSONRequestDeserializer()
|
||||
serializer = wsgi.JSONResponseSerializer()
|
||||
return wsgi.Resource(StackController(options), deserializer, serializer)
|
||||
|
@ -17,21 +17,16 @@
|
||||
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
|
||||
|
||||
from heat.cloudformations import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
SUPPORTED_PARAMS = heat.api.v1.SUPPORTED_PARAMS
|
||||
|
||||
|
||||
class V1Client(base_client.BaseClient):
|
||||
@ -81,8 +76,9 @@ class V1Client(base_client.BaseClient):
|
||||
def delete_stack(self, **kwargs):
|
||||
params = self._extract_params(kwargs, SUPPORTED_PARAMS)
|
||||
self._insert_common_parameters(params)
|
||||
self.do_request("DELETE", "/DeleteStack", params=params)
|
||||
return True
|
||||
res = self.do_request("DELETE", "/DeleteStack", params=params)
|
||||
data = json.loads(res.read())
|
||||
return data
|
||||
|
||||
Client = V1Client
|
||||
|
||||
|
19
heat/cloudformations.py
Normal file
19
heat/cloudformations.py
Normal file
@ -0,0 +1,19 @@
|
||||
# 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.
|
||||
|
||||
SUPPORTED_PARAMS = ('StackName', 'TemplateBody', 'TemplateUrl','NotificationARNs', 'Parameters',
|
||||
'Version', 'SignatureVersion', 'Timestamp', 'AWSAccessKeyId',
|
||||
'Signature')
|
||||
|
17
heat/engine/__init__.py
Normal file
17
heat/engine/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
# 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.
|
||||
|
15
heat/engine/api/__init__.py
Normal file
15
heat/engine/api/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# 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.
|
||||
|
@ -13,39 +13,20 @@
|
||||
# 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__)
|
||||
from heat.engine.api.v1 import stacks
|
||||
|
||||
class API(wsgi.Router):
|
||||
|
||||
"""WSGI router for Heat v1 API requests."""
|
||||
#TODO GetTemplate, ValidateTemplate
|
||||
"""WSGI entry point for all stac requests."""
|
||||
|
||||
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="describe", conditions=dict(method=["GET"]))
|
||||
mapper.connect("/DeleteStack", controller=stacks_resource,
|
||||
action="delete", conditions=dict(method=["DELETE"]))
|
||||
mapper.connect("/UpdateStack", controller=stacks_resource,
|
||||
action="update", conditions=dict(method=["PUT"]))
|
||||
|
||||
super(API, self).__init__(mapper)
|
128
heat/engine/api/v1/stacks.py
Normal file
128
heat/engine/api/v1/stacks.py
Normal file
@ -0,0 +1,128 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Reference implementation stacks server WSGI controller
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
import webob
|
||||
from webob.exc import (HTTPNotFound,
|
||||
HTTPConflict,
|
||||
HTTPBadRequest)
|
||||
|
||||
from heat.common import exception
|
||||
from heat.common import wsgi
|
||||
|
||||
from heat.engine import capelistener
|
||||
from heat.engine import json2capexml
|
||||
from heat.engine import systemctl
|
||||
|
||||
|
||||
logger = logging.getLogger('heat.engine.api.v1.stacks')
|
||||
|
||||
stack_db = {}
|
||||
|
||||
class Controller(object):
|
||||
'''
|
||||
bla
|
||||
'''
|
||||
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
self.listener = capelistener.CapeEventListener()
|
||||
|
||||
|
||||
def index(self, req, format='json'):
|
||||
logger.info('format is %s' % format)
|
||||
res = {'stacks': [] }
|
||||
for s in stack_db:
|
||||
mem = {}
|
||||
mem['StackId'] = stack_db[s]['StackId']
|
||||
mem['StackName'] = s
|
||||
mem['CreationTime'] = 'now'
|
||||
try:
|
||||
mem['TemplateDescription'] = stack_db[s]['Description']
|
||||
mem['StackStatus'] = stack_db[s]['StackStatus']
|
||||
except:
|
||||
mem['TemplateDescription'] = 'No description'
|
||||
mem['StackStatus'] = 'unknown'
|
||||
res['stacks'].append(mem)
|
||||
|
||||
return res
|
||||
|
||||
def show(self, req, id):
|
||||
res = {'stacks': [] }
|
||||
if stack_db.has_key(id):
|
||||
mem = {}
|
||||
mem['StackId'] = stack_db[id]['StackId']
|
||||
mem['StackName'] = id
|
||||
mem['CreationTime'] = 'TODO'
|
||||
mem['LastUpdatedTime'] = 'TODO'
|
||||
mem['NotificationARNs'] = 'TODO'
|
||||
mem['Outputs'] = [{'Description': 'TODO', 'OutputKey': 'TODO', 'OutputValue': 'TODO' }]
|
||||
mem['Parameters'] = stack_db[id]['Parameters']
|
||||
mem['StackStatusReason'] = 'TODO'
|
||||
mem['TimeoutInMinutes'] = 'TODO'
|
||||
try:
|
||||
mem['TemplateDescription'] = stack_db[id]['Description']
|
||||
mem['StackStatus'] = stack_db[id]['StackStatus']
|
||||
except:
|
||||
mem['TemplateDescription'] = 'No description'
|
||||
mem['StackStatus'] = 'unknown'
|
||||
res['stacks'].append(mem)
|
||||
else:
|
||||
return webob.exc.HTTPNotFound('No stack by that name')
|
||||
|
||||
return res
|
||||
|
||||
def create(self, req, body=None):
|
||||
|
||||
if body is None:
|
||||
msg = _("TemplateBody or TemplateUrl were not given.")
|
||||
return webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
if stack_db.has_key(body['StackName']):
|
||||
msg = _("Stack already exists with that name.")
|
||||
return webob.exc.HTTPConflict(msg)
|
||||
|
||||
stack = body
|
||||
stack['StackId'] = body['StackName']
|
||||
stack['StackStatus'] = 'CREATE_COMPLETE'
|
||||
# TODO self._apply_user_parameters(req, stack)
|
||||
stack_db[body['StackName']] = stack
|
||||
|
||||
cape_transformer = json2capexml.Json2CapeXml(stack, body['StackName'])
|
||||
cape_transformer.convert_and_write()
|
||||
|
||||
systemctl.systemctl('start', 'pcloud-cape-sshd', body['StackName'])
|
||||
|
||||
return {'stack': {'id': body['StackName']}}
|
||||
|
||||
def delete(self, req, id):
|
||||
if not stack_db.has_key(id):
|
||||
return webob.exc.HTTPNotFound('No stack by that name')
|
||||
|
||||
logger.info('deleting stack %s' % id)
|
||||
systemctl.systemctl('stop', 'pcloud-cape-sshd', id)
|
||||
del stack_db[id]
|
||||
return None
|
||||
|
||||
def create_resource(conf):
|
||||
"""Stacks resource factory method."""
|
||||
deserializer = wsgi.JSONRequestDeserializer()
|
||||
serializer = wsgi.JSONResponseSerializer()
|
||||
return wsgi.Resource(Controller(conf), deserializer, serializer)
|
61
heat/engine/capelistener.py
Normal file
61
heat/engine/capelistener.py
Normal file
@ -0,0 +1,61 @@
|
||||
# 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 errno
|
||||
import eventlet
|
||||
from eventlet.green import socket
|
||||
import fcntl
|
||||
import logging
|
||||
import os
|
||||
import stat
|
||||
|
||||
class CapeEventListener:
|
||||
|
||||
def __init__(self):
|
||||
self.backlog = 50
|
||||
self.file = 'pacemaker-cloud-cped'
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
flags = fcntl.fcntl(sock, fcntl.F_GETFD)
|
||||
fcntl.fcntl(sock, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
st = os.stat(self.file)
|
||||
except OSError, err:
|
||||
if err.errno != errno.ENOENT:
|
||||
raise
|
||||
else:
|
||||
if stat.S_ISSOCK(st.st_mode):
|
||||
os.remove(self.file)
|
||||
else:
|
||||
raise ValueError("File %s exists and is not a socket", self.file)
|
||||
sock.bind(self.file)
|
||||
sock.listen(self.backlog)
|
||||
os.chmod(self.file, 0600)
|
||||
|
||||
eventlet.spawn_n(self.cape_event_listner, sock)
|
||||
|
||||
def cape_event_listner(self, sock):
|
||||
eventlet.serve(sock, self.cape_event_handle)
|
||||
|
||||
def cape_event_handle(self, sock, client_addr):
|
||||
while True:
|
||||
x = sock.recv(4096)
|
||||
# TODO(asalkeld) format this event "nicely"
|
||||
logger.info('%s' % x.strip('\n'))
|
||||
if not x: break
|
||||
|
||||
|
||||
|
167
heat/engine/client.py
Normal file
167
heat/engine/client.py
Normal file
@ -0,0 +1,167 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Simple client class to speak with any RESTful service that implements
|
||||
the heat Engine API
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from heat.common.client import BaseClient
|
||||
from heat.common import crypt
|
||||
from heat.common import config
|
||||
from openstack.common import cfg
|
||||
|
||||
from heat.cloudformations import *
|
||||
|
||||
_CLIENT_CREDS = None
|
||||
_CLIENT_HOST = None
|
||||
_CLIENT_PORT = None
|
||||
_CLIENT_KWARGS = {}
|
||||
# AES key used to encrypt 'location' metadata
|
||||
_METADATA_ENCRYPTION_KEY = None
|
||||
|
||||
|
||||
engine_addr_opts = [
|
||||
cfg.StrOpt('engine_host', default='0.0.0.0'),
|
||||
cfg.IntOpt('engine_port', default=8001),
|
||||
]
|
||||
engine_client_opts = [
|
||||
cfg.StrOpt('engine_client_protocol', default='http'),
|
||||
cfg.StrOpt('engine_client_key_file'),
|
||||
cfg.StrOpt('engine_client_cert_file'),
|
||||
cfg.StrOpt('engine_client_ca_file'),
|
||||
cfg.StrOpt('metadata_encryption_key'),
|
||||
]
|
||||
|
||||
class EngineClient(BaseClient):
|
||||
|
||||
"""A client for the Engine stack metadata service"""
|
||||
|
||||
DEFAULT_PORT = 8001
|
||||
|
||||
def __init__(self, host=None, port=None, metadata_encryption_key=None,
|
||||
**kwargs):
|
||||
"""
|
||||
:param metadata_encryption_key: Key used to encrypt 'location' metadata
|
||||
"""
|
||||
self.metadata_encryption_key = metadata_encryption_key
|
||||
# NOTE (dprince): by default base client overwrites host and port
|
||||
# settings when using keystone. configure_via_auth=False disables
|
||||
# this behaviour to ensure we still send requests to the Engine API
|
||||
BaseClient.__init__(self, host, port, configure_via_auth=False,
|
||||
**kwargs)
|
||||
|
||||
def get_stacks(self, **kwargs):
|
||||
"""
|
||||
Returns a list of stack id/name mappings from Engine
|
||||
|
||||
:param filters: dict of keys & expected values to filter results
|
||||
:param marker: stack id after which to start page
|
||||
:param limit: max number of stacks to return
|
||||
:param sort_key: results will be ordered by this stack attribute
|
||||
:param sort_dir: direction in which to to order results (asc, desc)
|
||||
"""
|
||||
params = self._extract_params(kwargs, SUPPORTED_PARAMS)
|
||||
res = self.do_request("GET", "/stacks", params=params)
|
||||
return json.loads(res.read())['stacks']
|
||||
|
||||
def show_stack(self, stack_id):
|
||||
"""Returns a mapping of stack metadata from Engine"""
|
||||
res = self.do_request("GET", "/stacks/%s" % stack_id)
|
||||
data = json.loads(res.read())['stacks']
|
||||
return data
|
||||
|
||||
|
||||
def create_stack(self, template):
|
||||
"""
|
||||
Tells engine about an stack's metadata
|
||||
"""
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
res = self.do_request("POST", "/stacks", json.dumps(template), headers=headers)
|
||||
data = json.loads(res.read())
|
||||
return data
|
||||
|
||||
def update_stack(self, stack_id, template):
|
||||
"""
|
||||
Updates Engine's information about an stack
|
||||
"""
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
res = self.do_request("PUT", "/stacks/%s" % (stack_id), json.dumps(template), headers)
|
||||
data = json.loads(res.read())
|
||||
stack = data['stack']
|
||||
return stack
|
||||
|
||||
def delete_stack(self, stack_name):
|
||||
"""
|
||||
Deletes Engine's information about an stack
|
||||
"""
|
||||
res = self.do_request("DELETE", "/stacks/%s" % stack_name)
|
||||
return res
|
||||
|
||||
def get_engine_addr(conf):
|
||||
conf.register_opts(engine_addr_opts)
|
||||
return (conf.engine_host, conf.engine_port)
|
||||
|
||||
|
||||
def configure_engine_client(conf):
|
||||
"""
|
||||
Sets up a engine client for use in engine lookups
|
||||
|
||||
:param conf: Configuration options coming from controller
|
||||
"""
|
||||
global _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT, _METADATA_ENCRYPTION_KEY
|
||||
try:
|
||||
host, port = get_engine_addr(conf)
|
||||
except cfg.ConfigFileValueError:
|
||||
msg = _("Configuration option was not valid")
|
||||
logger.error(msg)
|
||||
raise exception.BadEngineConnectionConfiguration(msg)
|
||||
except IndexError:
|
||||
msg = _("Could not find required configuration option")
|
||||
logger.error(msg)
|
||||
raise exception.BadEngineConnectionConfiguration(msg)
|
||||
|
||||
conf.register_opts(engine_client_opts)
|
||||
|
||||
_CLIENT_HOST = host
|
||||
_CLIENT_PORT = port
|
||||
_METADATA_ENCRYPTION_KEY = conf.metadata_encryption_key
|
||||
_CLIENT_KWARGS = {
|
||||
'use_ssl': conf.engine_client_protocol.lower() == 'https',
|
||||
'key_file': conf.engine_client_key_file,
|
||||
'cert_file': conf.engine_client_cert_file,
|
||||
'ca_file': conf.engine_client_ca_file
|
||||
}
|
||||
|
||||
|
||||
|
||||
def get_engine_client(cxt):
|
||||
global _CLIENT_CREDS, _CLIENT_KWARGS, _CLIENT_HOST, _CLIENT_PORT
|
||||
global _METADATA_ENCRYPTION_KEY
|
||||
kwargs = _CLIENT_KWARGS.copy()
|
||||
kwargs['auth_tok'] = cxt.auth_tok
|
||||
if _CLIENT_CREDS:
|
||||
kwargs['creds'] = _CLIENT_CREDS
|
||||
return EngineClient(_CLIENT_HOST, _CLIENT_PORT,
|
||||
_METADATA_ENCRYPTION_KEY, **kwargs)
|
||||
|
||||
|
234
heat/engine/json2capexml.py
Normal file
234
heat/engine/json2capexml.py
Normal file
@ -0,0 +1,234 @@
|
||||
# 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 libxml2
|
||||
import logging
|
||||
|
||||
from heat.common import utils
|
||||
|
||||
logger = logging.getLogger('heat.engine.json2capexml')
|
||||
|
||||
class Json2CapeXml:
|
||||
def __init__(self, template, stack_name):
|
||||
|
||||
self.t = template
|
||||
self.parms = self.t['Parameters']
|
||||
self.maps = self.t['Mappings']
|
||||
self.res = {}
|
||||
self.doc = None
|
||||
self.name = stack_name
|
||||
|
||||
self.parms['AWS::Region'] = {"Description" : "AWS Regions", "Type" : "String", "Default" : "ap-southeast-1",
|
||||
"AllowedValues" : ["us-east-1","us-west-1","us-west-2","sa-east-1","eu-west-1","ap-southeast-1","ap-northeast-1"],
|
||||
"ConstraintDescription" : "must be a valid EC2 instance type." }
|
||||
|
||||
# expected user parameters
|
||||
self.parms['AWS::StackName'] = {'Default': stack_name}
|
||||
self.parms['KeyName'] = {'Default': 'harry-45-5-34-5'}
|
||||
|
||||
for r in self.t['Resources']:
|
||||
# fake resource instance references
|
||||
self.parms[r] = {'Default': utils.generate_uuid()}
|
||||
|
||||
self.resolve_static_refs(self.t['Resources'])
|
||||
self.resolve_find_in_map(self.t['Resources'])
|
||||
#self.resolve_attributes(self.t['Resources'])
|
||||
self.resolve_joins(self.t['Resources'])
|
||||
self.resolve_base64(self.t['Resources'])
|
||||
#print json.dumps(self.t['Resources'], indent=2)
|
||||
|
||||
|
||||
def convert(self):
|
||||
|
||||
self.doc = libxml2.newDoc("1.0")
|
||||
dep = self.doc.newChild(None, "deployable", None)
|
||||
dep.setProp("name", self.name)
|
||||
dep.setProp("uuid", 'bogus')
|
||||
dep.setProp("username", 'nobody-yet')
|
||||
n_asses = dep.newChild(None, "assemblies", None)
|
||||
|
||||
for r in self.t['Resources']:
|
||||
type = self.t['Resources'][r]['Type']
|
||||
if type != 'AWS::EC2::Instance':
|
||||
print 'ignoring Resource %s (%s)' % (r, type)
|
||||
continue
|
||||
|
||||
n_ass = n_asses.newChild(None, 'assembly', None)
|
||||
n_ass.setProp("name", r)
|
||||
n_ass.setProp("uuid", self.parms[r]['Default'])
|
||||
props = self.t['Resources'][r]['Properties']
|
||||
for p in props:
|
||||
if p == 'ImageId':
|
||||
n_ass.setProp("image_name", props[p])
|
||||
elif p == 'UserData':
|
||||
new_script = []
|
||||
script_lines = props[p].split('\n')
|
||||
for l in script_lines:
|
||||
if '#!/' in l:
|
||||
new_script.append(l)
|
||||
self.insert_package_and_services(self.t['Resources'][r], new_script)
|
||||
else:
|
||||
new_script.append(l)
|
||||
|
||||
startup = n_ass.newChild(None, 'startup', '\n'.join(new_script))
|
||||
|
||||
|
||||
try:
|
||||
con = self.t['Resources'][r]['Metadata']["AWS::CloudFormation::Init"]['config']
|
||||
n_services = n_ass.newChild(None, 'services', None)
|
||||
for st in con['services']:
|
||||
for s in con['services'][st]:
|
||||
n_service = n_services.newChild(None, 'service', None)
|
||||
n_service.setProp("name", '%s_%s' % (r, s))
|
||||
n_service.setProp("type", s)
|
||||
n_service.setProp("provider", 'pacemaker')
|
||||
n_service.setProp("class", 'lsb')
|
||||
n_service.setProp("monitor_interval", '30s')
|
||||
n_service.setProp("escalation_period", '1000')
|
||||
n_service.setProp("escalation_failures", '3')
|
||||
except KeyError as e:
|
||||
# if there is no config then no services.
|
||||
pass
|
||||
|
||||
def get_xml(self):
|
||||
str = self.doc.serialize(None, 1)
|
||||
self.doc.freeDoc()
|
||||
self.doc = None
|
||||
return str
|
||||
|
||||
def convert_and_write(self):
|
||||
self.convert()
|
||||
try:
|
||||
filename = '/var/run/%s.xml' % self.name
|
||||
open(filename, 'w').write(self.doc.serialize(None, 1))
|
||||
self.doc.freeDoc()
|
||||
self.doc = None
|
||||
except IOError as e:
|
||||
logger.error('couldn\'t write to /var/run/ error %s' % e)
|
||||
|
||||
def insert_package_and_services(self, r, new_script):
|
||||
|
||||
try:
|
||||
con = r['Metadata']["AWS::CloudFormation::Init"]['config']
|
||||
except KeyError as e:
|
||||
return
|
||||
|
||||
for pt in con['packages']:
|
||||
if pt == 'yum':
|
||||
for p in con['packages']['yum']:
|
||||
new_script.append('yum install -y %s' % p)
|
||||
for st in con['services']:
|
||||
if st == 'systemd':
|
||||
for s in con['services']['systemd']:
|
||||
v = con['services']['systemd'][s]
|
||||
if v['enabled'] == 'true':
|
||||
new_script.append('systemctl enable %s.service' % s)
|
||||
if v['ensureRunning'] == 'true':
|
||||
new_script.append('systemctl start %s.service' % s)
|
||||
elif st == 'sysvinit':
|
||||
for s in con['services']['sysvinit']:
|
||||
v = con['services']['systemd'][s]
|
||||
if v['enabled'] == 'true':
|
||||
new_script.append('chkconfig %s on' % s)
|
||||
if v['ensureRunning'] == 'true':
|
||||
new_script.append('/etc/init.d/start %s' % s)
|
||||
|
||||
def resolve_static_refs(self, s):
|
||||
'''
|
||||
looking for { "Ref": "str" }
|
||||
'''
|
||||
if isinstance(s, dict):
|
||||
for i in s:
|
||||
if i == 'Ref' and isinstance(s[i], (basestring, unicode)) and \
|
||||
self.parms.has_key(s[i]):
|
||||
if self.parms[s[i]] == None:
|
||||
print 'None Ref: %s' % str(s[i])
|
||||
elif self.parms[s[i]].has_key('Default'):
|
||||
# note the "ref: values" are in a dict of
|
||||
# size one, so return is fine.
|
||||
#print 'Ref: %s == %s' % (s[i], self.parms[s[i]]['Default'])
|
||||
return self.parms[s[i]]['Default']
|
||||
else:
|
||||
print 'missing Ref: %s' % str(s[i])
|
||||
else:
|
||||
s[i] = self.resolve_static_refs(s[i])
|
||||
elif isinstance(s, list):
|
||||
for index, item in enumerate(s):
|
||||
#print 'resolve_static_refs %d %s' % (index, item)
|
||||
s[index] = self.resolve_static_refs(item)
|
||||
return s
|
||||
|
||||
def resolve_find_in_map(self, s):
|
||||
'''
|
||||
looking for { "Ref": "str" }
|
||||
'''
|
||||
if isinstance(s, dict):
|
||||
for i in s:
|
||||
if i == 'Fn::FindInMap':
|
||||
obj = self.maps
|
||||
if isinstance(s[i], list):
|
||||
#print 'map list: %s' % s[i]
|
||||
for index, item in enumerate(s[i]):
|
||||
if isinstance(item, dict):
|
||||
item = self.resolve_find_in_map(item)
|
||||
#print 'map item dict: %s' % (item)
|
||||
else:
|
||||
pass
|
||||
#print 'map item str: %s' % (item)
|
||||
obj = obj[item]
|
||||
else:
|
||||
obj = obj[s[i]]
|
||||
return obj
|
||||
else:
|
||||
s[i] = self.resolve_find_in_map(s[i])
|
||||
elif isinstance(s, list):
|
||||
for index, item in enumerate(s):
|
||||
s[index] = self.resolve_find_in_map(item)
|
||||
return s
|
||||
|
||||
|
||||
def resolve_joins(self, s):
|
||||
'''
|
||||
looking for { "Fn::join": [] }
|
||||
'''
|
||||
if isinstance(s, dict):
|
||||
for i in s:
|
||||
if i == 'Fn::Join':
|
||||
return s[i][0].join(s[i][1])
|
||||
else:
|
||||
s[i] = self.resolve_joins(s[i])
|
||||
elif isinstance(s, list):
|
||||
for index, item in enumerate(s):
|
||||
s[index] = self.resolve_joins(item)
|
||||
return s
|
||||
|
||||
|
||||
def resolve_base64(self, s):
|
||||
'''
|
||||
looking for { "Fn::join": [] }
|
||||
'''
|
||||
if isinstance(s, dict):
|
||||
for i in s:
|
||||
if i == 'Fn::Base64':
|
||||
return s[i]
|
||||
else:
|
||||
s[i] = self.resolve_base64(s[i])
|
||||
elif isinstance(s, list):
|
||||
for index, item in enumerate(s):
|
||||
s[index] = self.resolve_base64(item)
|
||||
return s
|
||||
|
||||
|
52
heat/engine/systemctl.py
Normal file
52
heat/engine/systemctl.py
Normal file