From 91ee085a0dd65ae408d29bb494d0208530435203 Mon Sep 17 00:00:00 2001 From: Angus Salkeld Date: Mon, 26 Mar 2012 16:45:06 +1100 Subject: [PATCH] Begin the change to a python only implementation. - Don't start pacemaker-cloud cape, instead start the resources (soon to be implemented). - kill off systemctl, capelistener and json2capexml Signed-off-by: Angus Salkeld --- heat/engine/api/v1/events.py | 4 +- heat/engine/api/v1/stacks.py | 19 +-- heat/engine/capelistener.py | 103 ------------ heat/engine/json2capexml.py | 234 -------------------------- heat/engine/parser.py | 307 ++-------------------------------- heat/engine/resources.py | 308 +++++++++++++++++++++++++++++++++++ heat/engine/simpledb.py | 4 + heat/engine/systemctl.py | 52 ------ 8 files changed, 333 insertions(+), 698 deletions(-) delete mode 100644 heat/engine/capelistener.py delete mode 100644 heat/engine/json2capexml.py create mode 100644 heat/engine/resources.py delete mode 100644 heat/engine/systemctl.py diff --git a/heat/engine/api/v1/events.py b/heat/engine/api/v1/events.py index 994398e3..941fa744 100644 --- a/heat/engine/api/v1/events.py +++ b/heat/engine/api/v1/events.py @@ -27,7 +27,7 @@ from webob.exc import (HTTPNotFound, from heat.common import exception from heat.common import wsgi -from heat.engine import capelistener +from heat.engine import parser from heat.engine import simpledb logger = logging.getLogger('heat.engine.api.v1.events') @@ -41,8 +41,6 @@ class EventsController(object): def __init__(self, conf): self.conf = conf - self.event_db = {} - self.listener = capelistener.CapeEventListener() def index(self, req, stack_id): return simpledb.events_get(stack_id) diff --git a/heat/engine/api/v1/stacks.py b/heat/engine/api/v1/stacks.py index ba3af0c4..698435f7 100644 --- a/heat/engine/api/v1/stacks.py +++ b/heat/engine/api/v1/stacks.py @@ -27,9 +27,7 @@ from webob.exc import (HTTPNotFound, 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 +from heat.engine import parser logger = logging.getLogger('heat.engine.api.v1.stacks') @@ -43,8 +41,6 @@ class StacksController(object): def __init__(self, conf): self.conf = conf - self.listener = capelistener.CapeEventListener() - def index(self, req, format='json'): logger.info('format is %s' % format) @@ -99,16 +95,8 @@ class StacksController(object): 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']) + stack_db[body['StackName']] = parser.Stack(body['StackName'], body) + stack_db[body['StackName']].start() return {'stack': {'id': body['StackName']}} @@ -117,7 +105,6 @@ class StacksController(object): 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 diff --git a/heat/engine/capelistener.py b/heat/engine/capelistener.py deleted file mode 100644 index 4752574b..00000000 --- a/heat/engine/capelistener.py +++ /dev/null @@ -1,103 +0,0 @@ -# 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 libxml2 -import logging -import os -import stat -from heat.engine import simpledb - - -logger = logging.getLogger('heat.engine.capelistener') - -class CapeEventListener(object): - - 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 store(self, xml_event): - - try: - doc = libxml2.parseDoc(xml_event) - except: - return - - event = {'EventId': ''} - root = doc.getRootElement() - child = root.children - while child is not None: - if child.type != "element": - child = child.next - elif child.name == 'event': - child = child.children - elif child.name == 'application': - event['StackId'] = child.prop('name') - event['StackName'] = child.prop('name') - child = child.children - elif child.name == 'node': - event['ResourceType'] = 'AWS::EC2::Instance' - event['LogicalResourceId'] = child.prop('name') - child = child.children - elif child.name == 'resource': - event['ResourceType'] = 'ORG::HA::Service' - event['LogicalResourceId'] = child.prop('name') - child = child.children - elif child.name == 'state': - event['ResourceStatus'] = child.content - child = child.next - elif child.name == 'reason': - event['ResourceStatusReason'] = child.content - child = child.next - else: - child = child.next - - simpledb.event_append(event) - doc.freeDoc() - - def cape_event_handle(self, sock, client_addr): - while True: - x = sock.recv(4096) - self.store(x.strip('\n')) - if not x: break - diff --git a/heat/engine/json2capexml.py b/heat/engine/json2capexml.py deleted file mode 100644 index 96503a4b..00000000 --- a/heat/engine/json2capexml.py +++ /dev/null @@ -1,234 +0,0 @@ -# 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 - - diff --git a/heat/engine/parser.py b/heat/engine/parser.py index 6cbc894f..90e3176d 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -16,290 +16,13 @@ import json import logging -logger = logging.getLogger('heat.engine.parser') - -parse_debug = False -#parse_debug = True - - -class Resource(object): - CREATE_IN_PROGRESS = 'CREATE_IN_PROGRESS' - CREATE_FAILED = 'CREATE_FAILED' - CREATE_COMPLETE = 'CREATE_COMPLETE' - DELETE_IN_PROGRESS = 'DELETE_IN_PROGRESS' - DELETE_FAILED = 'DELETE_FAILED' - DELETE_COMPLETE = 'DELETE_COMPLETE' - UPDATE_IN_PROGRESS = 'UPDATE_IN_PROGRESS' - UPDATE_FAILED = 'UPDATE_FAILED' - UPDATE_COMPLETE = 'UPDATE_COMPLETE' - - def __init__(self, name, json_snippet, stack): - self.t = json_snippet - self.depends_on = [] - self.references = [] - self.references_resolved = False - self.state = None - self.stack = stack - self.name = name - self.instance_id = None - - - - stack.resolve_static_refs(self.t) - stack.resolve_find_in_map(self.t) - - def start(self): - for c in self.depends_on: - #print '%s->%s.start()' % (self.name, self.stack.resources[c].name) - self.stack.resources[c].start() +from heat.engine import resources - self.stack.resolve_attributes(self.t) - self.stack.resolve_joins(self.t) - self.stack.resolve_base64(self.t) - - - def stop(self): - pass - - def reload(self): - pass - - def FnGetRefId(self): - ''' -http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html - ''' - if self.instance_id != None: - return unicode(self.instance_id) - else: - return unicode(self.name) - - def FnGetAtt(self, key): - ''' -http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html - ''' - print '%s.GetAtt(%s)' % (self.name, key) - return unicode('not-this-surely') - -class GenericResource(Resource): - def __init__(self, name, json_snippet, stack): - super(GenericResource, self).__init__(name, json_snippet, stack) - - def start(self): - if self.state != None: - return - self.state = self.CREATE_IN_PROGRESS - super(GenericResource, self).start() - print 'Starting GenericResource %s' % self.name - - -class ElasticIp(Resource): - def __init__(self, name, json_snippet, stack): - super(ElasticIp, self).__init__(name, json_snippet, stack) - self.instance_id = '' - - if self.t.has_key('Properties') and self.t['Properties'].has_key('Domain'): - print '*** can\'t support Domain %s yet' % (self.t['Properties']['Domain']) - - def start(self): - if self.state != None: - return - self.state = Resource.CREATE_IN_PROGRESS - super(ElasticIp, self).start() - self.instance_id = 'eip-000003' - - def FnGetRefId(self): - return unicode('0.0.0.0') - - def FnGetAtt(self, key): - return unicode(self.instance_id) - -class ElasticIpAssociation(Resource): - def __init__(self, name, json_snippet, stack): - super(ElasticIpAssociation, self).__init__(name, json_snippet, stack) - - # note we only support already assigned ipaddress - # - # Done with: - # nova-manage floating create 172.31.0.224/28 - # euca-allocate-address - # - - if not self.t['Properties'].has_key('EIP'): - print '*** can\'t support this yet' - if self.t['Properties'].has_key('AllocationId'): - print '*** can\'t support AllocationId %s yet' % (self.t['Properties']['AllocationId']) - - def FnGetRefId(self): - if not self.t['Properties'].has_key('EIP'): - return unicode('0.0.0.0') - else: - return unicode(self.t['Properties']['EIP']) - - def start(self): - - if self.state != None: - return - self.state = Resource.CREATE_IN_PROGRESS - super(ElasticIpAssociation, self).start() - print '$ euca-associate-address -i %s %s' % (self.t['Properties']['InstanceId'], - self.t['Properties']['EIP']) - -class Volume(Resource): - def __init__(self, name, json_snippet, stack): - super(Volume, self).__init__(name, json_snippet, stack) - - def start(self): - - if self.state != None: - return - self.state = Resource.CREATE_IN_PROGRESS - super(Volume, self).start() - # TODO start the volume here - # of size -> self.t['Properties']['Size'] - # and set self.instance_id to the volume id - print '$ euca-create-volume -s %s -z nova' % self.t['Properties']['Size'] - self.instance_id = 'vol-4509854' - -class VolumeAttachment(Resource): - def __init__(self, name, json_snippet, stack): - super(VolumeAttachment, self).__init__(name, json_snippet, stack) - - def start(self): - - if self.state != None: - return - self.state = Resource.CREATE_IN_PROGRESS - super(VolumeAttachment, self).start() - # TODO attach the volume with an id of: - # self.t['Properties']['VolumeId'] - # to the vm of instance: - # self.t['Properties']['InstanceId'] - # and make sure that the mountpoint is: - # self.t['Properties']['Device'] - print '$ euca-attach-volume %s -i %s -d %s' % (self.t['Properties']['VolumeId'], - self.t['Properties']['InstanceId'], - self.t['Properties']['Device']) - -class Instance(Resource): - - def __init__(self, name, json_snippet, stack): - super(Instance, self).__init__(name, json_snippet, stack) - - if not self.t['Properties'].has_key('AvailabilityZone'): - self.t['Properties']['AvailabilityZone'] = 'nova' - self.itype_oflavor = {'t1.micro': 'm1.tiny', - 'm1.small': 'm1.small', - 'm1.medium': 'm1.medium', - 'm1.large': 'm1.large', - 'm2.xlarge': 'm1.large', - 'm2.2xlarge': 'm1.large', - 'm2.4xlarge': 'm1.large', - 'c1.medium': 'm1.medium', - 'c1.4xlarge': 'm1.large', - 'cc2.8xlarge': 'm1.large', - 'cg1.4xlarge': 'm1.large'} - - def FnGetAtt(self, key): - print '%s.GetAtt(%s)' % (self.name, key) - - if key == 'AvailabilityZone': - return unicode(self.t['Properties']['AvailabilityZone']) - else: - # TODO PrivateDnsName, PublicDnsName, PrivateIp, PublicIp - return unicode('not-this-surely') - - - def start(self): - - if self.state != None: - return - self.state = Resource.CREATE_IN_PROGRESS - Resource.start(self) - - props = self.t['Properties'] - if not props.has_key('KeyName'): - props['KeyName'] = 'default-key-name' - if not props.has_key('InstanceType'): - props['InstanceType'] = 's1.large' - if not props.has_key('ImageId'): - props['ImageId'] = 'F16-x86_64' - - for p in props: - if 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, new_script) - else: - new_script.append(l) - - if parse_debug: - print '----------------------' - try: - print '\n'.join(new_script) - except: - print str(new_script) - raise - print '----------------------' - - try: - con = self.t['Metadata']["AWS::CloudFormation::Init"]['config'] - for st in con['services']: - for s in con['services'][st]: - print 'service start %s_%s' % (self.name, s) - except KeyError as e: - # if there is no config then no services. - pass - - - # TODO start the instance here. - # and set self.instance_id - print '$ euca-run-instances -k %s -t %s %s' % (self.t['Properties']['KeyName'], - self.t['Properties']['InstanceType'], - self.t['Properties']['ImageId']) - - # Convert AWS instance type to OpenStack flavor - # TODO(sdake) - # heat API should take care of these conversions and feed them into - # heat engine in an openstack specific json format - flavor = self.itype_oflavor[self.t['Properties']['InstanceType']] - self.instance_id = 'i-734509008' - - def insert_package_and_services(self, r, new_script): - - try: - con = r['Metadata']["AWS::CloudFormation::Init"]['config'] - except KeyError as e: - return - - if con.has_key('packages'): - for pt in con['packages']: - if pt == 'yum': - for p in con['packages']['yum']: - new_script.append('yum install -y %s' % p) +logger = logging.getLogger('heat.engine.parser') - if con.has_key('services'): - 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']['sysvinit'][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) class Stack: - def __init__(self, template, stack_name): + def __init__(self, stack_name, template): self.t = template if self.t.has_key('Parameters'): @@ -318,27 +41,31 @@ class Stack: "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." } + +###### +# stack['StackId'] = body['StackName'] +# stack['StackStatus'] = 'CREATE_COMPLETE' +# # TODO self._apply_user_parameters(req, stack) +# stack_db[body['StackName']] = stack +###### + self.resources = {} for r in self.t['Resources']: type = self.t['Resources'][r]['Type'] if type == 'AWS::EC2::Instance': - self.resources[r] = Instance(r, self.t['Resources'][r], self) + self.resources[r] = resources.Instance(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::Volume': - self.resources[r] = Volume(r, self.t['Resources'][r], self) + self.resources[r] = resources.Volume(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::VolumeAttachment': - self.resources[r] = VolumeAttachment(r, self.t['Resources'][r], self) + self.resources[r] = resources.VolumeAttachment(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::EIP': - self.resources[r] = ElasticIp(r, self.t['Resources'][r], self) + self.resources[r] = resources.ElasticIp(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::EIPAssociation': - self.resources[r] = ElasticIpAssociation(r, self.t['Resources'][r], self) + self.resources[r] = resources.ElasticIpAssociation(r, self.t['Resources'][r], self) else: - self.resources[r] = GenericResource(r, self.t['Resources'][r], self) + self.resources[r] = resources.GenericResource(r, self.t['Resources'][r], self) self.calulate_dependancies(self.t['Resources'][r], self.resources[r]) - #print json.dumps(self.t['Resources'], indent=2) - if parse_debug: - for r in self.t['Resources']: - print '%s -> %s' % (r, self.resources[r].depends_on) def start(self): # start Volumes first. diff --git a/heat/engine/resources.py b/heat/engine/resources.py new file mode 100644 index 00000000..58016eab --- /dev/null +++ b/heat/engine/resources.py @@ -0,0 +1,308 @@ +# 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 logging + +from heat.engine import simpledb + +logger = logging.getLogger('heat.engine.resources') + +class Resource(object): + CREATE_IN_PROGRESS = 'CREATE_IN_PROGRESS' + CREATE_FAILED = 'CREATE_FAILED' + CREATE_COMPLETE = 'CREATE_COMPLETE' + DELETE_IN_PROGRESS = 'DELETE_IN_PROGRESS' + DELETE_FAILED = 'DELETE_FAILED' + DELETE_COMPLETE = 'DELETE_COMPLETE' + UPDATE_IN_PROGRESS = 'UPDATE_IN_PROGRESS' + UPDATE_FAILED = 'UPDATE_FAILED' + UPDATE_COMPLETE = 'UPDATE_COMPLETE' + + def __init__(self, name, json_snippet, stack): + self.t = json_snippet + self.depends_on = [] + self.references = [] + self.references_resolved = False + self.state = None + self.stack = stack + self.name = name + self.instance_id = None + + stack.resolve_static_refs(self.t) + stack.resolve_find_in_map(self.t) + + def start(self): + for c in self.depends_on: + #print '%s->%s.start()' % (self.name, self.stack.resources[c].name) + self.stack.resources[c].start() + + self.stack.resolve_attributes(self.t) + self.stack.resolve_joins(self.t) + self.stack.resolve_base64(self.t) + + + def state_set(self, new_state, reason="state changed"): + if new_state != self.state: + ev = {} + ev['LogicalResourceId'] = self.name + ev['PhysicalResourceId'] = self.name + ev['StackId'] = self.stack.name + ev['StackName'] = self.stack.name + ev['ResourceStatus'] = new_state + ev['ResourceStatusReason'] = reason + ev['ResourceType'] = self.t['Type'] + ev['ResourceProperties'] = self.t['Properties'] + + simpledb.event_append(ev) + self.state = new_state + + def stop(self): + pass + + def reload(self): + pass + + def FnGetRefId(self): + ''' +http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html + ''' + if self.instance_id != None: + return unicode(self.instance_id) + else: + return unicode(self.name) + + def FnGetAtt(self, key): + ''' +http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html + ''' + print '%s.GetAtt(%s)' % (self.name, key) + return unicode('not-this-surely') + +class GenericResource(Resource): + def __init__(self, name, json_snippet, stack): + super(GenericResource, self).__init__(name, json_snippet, stack) + + def start(self): + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + super(GenericResource, self).start() + print 'Starting GenericResource %s' % self.name + + +class ElasticIp(Resource): + def __init__(self, name, json_snippet, stack): + super(ElasticIp, self).__init__(name, json_snippet, stack) + self.instance_id = '' + + if self.t.has_key('Properties') and self.t['Properties'].has_key('Domain'): + logger.warn('*** can\'t support Domain %s yet' % (self.t['Properties']['Domain'])) + + def start(self): + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + super(ElasticIp, self).start() + self.instance_id = 'eip-000003' + + def FnGetRefId(self): + return unicode('0.0.0.0') + + def FnGetAtt(self, key): + return unicode(self.instance_id) + +class ElasticIpAssociation(Resource): + def __init__(self, name, json_snippet, stack): + super(ElasticIpAssociation, self).__init__(name, json_snippet, stack) + + # note we only support already assigned ipaddress + # + # Done with: + # nova-manage floating create 172.31.0.224/28 + # euca-allocate-address + # + + if not self.t['Properties'].has_key('EIP'): + logger.warn('*** can\'t support this yet') + if self.t['Properties'].has_key('AllocationId'): + logger.warn('*** can\'t support AllocationId %s yet' % (self.t['Properties']['AllocationId'])) + + def FnGetRefId(self): + if not self.t['Properties'].has_key('EIP'): + return unicode('0.0.0.0') + else: + return unicode(self.t['Properties']['EIP']) + + def start(self): + + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + super(ElasticIpAssociation, self).start() + logger.info('$ euca-associate-address -i %s %s' % (self.t['Properties']['InstanceId'], + self.t['Properties']['EIP'])) + +class Volume(Resource): + def __init__(self, name, json_snippet, stack): + super(Volume, self).__init__(name, json_snippet, stack) + + def start(self): + + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + super(Volume, self).start() + # TODO start the volume here + # of size -> self.t['Properties']['Size'] + # and set self.instance_id to the volume id + logger.info('$ euca-create-volume -s %s -z nova' % self.t['Properties']['Size']) + self.instance_id = 'vol-4509854' + +class VolumeAttachment(Resource): + def __init__(self, name, json_snippet, stack): + super(VolumeAttachment, self).__init__(name, json_snippet, stack) + + def start(self): + + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + super(VolumeAttachment, self).start() + # TODO attach the volume with an id of: + # self.t['Properties']['VolumeId'] + # to the vm of instance: + # self.t['Properties']['InstanceId'] + # and make sure that the mountpoint is: + # self.t['Properties']['Device'] + logger.info('$ euca-attach-volume %s -i %s -d %s' % (self.t['Properties']['VolumeId'], + self.t['Properties']['InstanceId'], + self.t['Properties']['Device'])) + +class Instance(Resource): + + def __init__(self, name, json_snippet, stack): + super(Instance, self).__init__(name, json_snippet, stack) + + if not self.t['Properties'].has_key('AvailabilityZone'): + self.t['Properties']['AvailabilityZone'] = 'nova' + self.itype_oflavor = {'t1.micro': 'm1.tiny', + 'm1.small': 'm1.small', + 'm1.medium': 'm1.medium', + 'm1.large': 'm1.large', + 'm2.xlarge': 'm1.large', + 'm2.2xlarge': 'm1.large', + 'm2.4xlarge': 'm1.large', + 'c1.medium': 'm1.medium', + 'c1.4xlarge': 'm1.large', + 'cc2.8xlarge': 'm1.large', + 'cg1.4xlarge': 'm1.large'} + + def FnGetAtt(self, key): + print '%s.GetAtt(%s)' % (self.name, key) + + if key == 'AvailabilityZone': + return unicode(self.t['Properties']['AvailabilityZone']) + else: + # TODO PrivateDnsName, PublicDnsName, PrivateIp, PublicIp + return unicode('not-this-surely') + + + def start(self): + + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + Resource.start(self) + + props = self.t['Properties'] + if not props.has_key('KeyName'): + props['KeyName'] = 'default-key-name' + if not props.has_key('InstanceType'): + props['InstanceType'] = 's1.large' + if not props.has_key('ImageId'): + props['ImageId'] = 'F16-x86_64' + + for p in props: + if 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, new_script) + else: + new_script.append(l) + + print '----------------------' + try: + print '\n'.join(new_script) + except: + print str(new_script) + raise + print '----------------------' + + try: + con = self.t['Metadata']["AWS::CloudFormation::Init"]['config'] + for st in con['services']: + for s in con['services'][st]: + print 'service start %s_%s' % (self.name, s) + except KeyError as e: + # if there is no config then no services. + pass + + + # TODO start the instance here. + # and set self.instance_id + logger.info('$ euca-run-instances -k %s -t %s %s' % (self.t['Properties']['KeyName'], + self.t['Properties']['InstanceType'], + self.t['Properties']['ImageId'])) + + # Convert AWS instance type to OpenStack flavor + # TODO(sdake) + # heat API should take care of these conversions and feed them into + # heat engine in an openstack specific json format + flavor = self.itype_oflavor[self.t['Properties']['InstanceType']] + self.instance_id = 'i-734509008' + + def insert_package_and_services(self, r, new_script): + + try: + con = r['Metadata']["AWS::CloudFormation::Init"]['config'] + except KeyError as e: + return + + if con.has_key('packages'): + for pt in con['packages']: + if pt == 'yum': + for p in con['packages']['yum']: + new_script.append('yum install -y %s' % p) + + if con.has_key('services'): + 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']['sysvinit'][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) diff --git a/heat/engine/simpledb.py b/heat/engine/simpledb.py index 7921ab65..a3907991 100644 --- a/heat/engine/simpledb.py +++ b/heat/engine/simpledb.py @@ -17,6 +17,10 @@ import anydbm import json def event_append(event): + ''' + EventId The unique ID of this event. + Timestamp Time the status was updated. + ''' name = event['StackName'] d = anydbm.open('/var/lib/heat/%s.events.db' % name, 'c') if d.has_key('lastid'): diff --git a/heat/engine/systemctl.py b/heat/engine/systemctl.py deleted file mode 100644 index 693dc401..00000000 --- a/heat/engine/systemctl.py +++ /dev/null @@ -1,52 +0,0 @@ -# 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. - -""" -Start and Stop systemd services -""" -import dbus -import logging - -logger = logging.getLogger('heat.engine.systemctl') - -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 - -- 2.45.2