From: Angus Salkeld Date: Thu, 3 May 2012 04:34:13 +0000 (+1000) Subject: Split the resourses up into seperate files. X-Git-Tag: 2014.1~1896 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=20fc3c76a2f2e577af5eefc2640eb97a586593b1;p=openstack-build%2Fheat-build.git Split the resourses up into seperate files. Signed-off-by: Angus Salkeld --- diff --git a/heat/engine/eip.py b/heat/engine/eip.py new file mode 100644 index 00000000..f0b2b624 --- /dev/null +++ b/heat/engine/eip.py @@ -0,0 +1,126 @@ +# 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 eventlet +import logging +import os + +from heat.common import exception +from heat.engine.resources import Resource + +logger = logging.getLogger(__file__) + + +class ElasticIp(Resource): + def __init__(self, name, json_snippet, stack): + super(ElasticIp, self).__init__(name, json_snippet, stack) + self.ipaddress = '' + + if 'Domain' in self.t['Properties']: + logger.warn('*** can\'t support Domain %s yet' % \ + (self.t['Properties']['Domain'])) + + def create(self): + """Allocate a floating IP for the current tenant.""" + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + super(ElasticIp, self).create() + + ips = self.nova().floating_ips.create() + logger.info('ElasticIp create %s' % str(ips)) + self.ipaddress = ips.ip + self.instance_id_set(ips.id) + self.state_set(self.CREATE_COMPLETE) + + def reload(self): + ''' + get the ipaddress here + ''' + if self.instance_id != None: + try: + ips = self.nova().floating_ips.get(self.instance_id) + self.ipaddress = ips.ip + except Exception as ex: + logger.warn("Error getting floating IPs: %s" % str(ex)) + + Resource.reload(self) + + def delete(self): + """De-allocate a floating IP.""" + if self.state == self.DELETE_IN_PROGRESS or \ + self.state == self.DELETE_COMPLETE: + return + + self.state_set(self.DELETE_IN_PROGRESS) + Resource.delete(self) + + if self.instance_id != None: + self.nova().floating_ips.delete(self.instance_id) + + self.state_set(self.DELETE_COMPLETE) + + def FnGetRefId(self): + return unicode(self.ipaddress) + + def FnGetAtt(self, key): + if key == 'AllocationId': + return unicode(self.instance_id) + else: + raise exception.InvalidTemplateAttribute(resource=self.name, + key=key) + + +class ElasticIpAssociation(Resource): + def __init__(self, name, json_snippet, stack): + super(ElasticIpAssociation, self).__init__(name, json_snippet, stack) + + def FnGetRefId(self): + if not 'EIP' in self.t['Properties']: + return unicode('0.0.0.0') + else: + return unicode(self.t['Properties']['EIP']) + + def create(self): + """Add a floating IP address to a server.""" + + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + super(ElasticIpAssociation, self).create() + logger.debug('ElasticIpAssociation %s.add_floating_ip(%s)' % \ + (self.t['Properties']['InstanceId'], + self.t['Properties']['EIP'])) + + server = self.nova().servers.get(self.t['Properties']['InstanceId']) + server.add_floating_ip(self.t['Properties']['EIP']) + self.instance_id_set(self.t['Properties']['EIP']) + self.state_set(self.CREATE_COMPLETE) + + def delete(self): + """Remove a floating IP address from a server.""" + if self.state == self.DELETE_IN_PROGRESS or \ + self.state == self.DELETE_COMPLETE: + return + + self.state_set(self.DELETE_IN_PROGRESS) + Resource.delete(self) + + server = self.nova().servers.get(self.t['Properties']['InstanceId']) + server.remove_floating_ip(self.t['Properties']['EIP']) + + self.state_set(self.DELETE_COMPLETE) + + diff --git a/heat/engine/instance.py b/heat/engine/instance.py new file mode 100644 index 00000000..ad72b80d --- /dev/null +++ b/heat/engine/instance.py @@ -0,0 +1,208 @@ +# 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 base64 +import eventlet +import logging +import os +import string +import json +import sys +from email import encoders +from email.message import Message +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from novaclient.exceptions import NotFound + +from heat.engine.resources import Resource +from heat.common import exception + +logger = logging.getLogger(__file__) +# 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) + cloudinit_path = '%s/heat/%s/' % (possible_topdir, "cloudinit") +else: + for p in sys.path: + if 'heat' in p: + cloudinit_path = '%s/heat/%s/' % (p, "cloudinit") + break + + + +class Instance(Resource): + + def __init__(self, name, json_snippet, stack): + super(Instance, self).__init__(name, json_snippet, stack) + self.ipaddress = '0.0.0.0' + + if not 'AvailabilityZone' in self.t['Properties']: + self.t['Properties']['AvailabilityZone'] = 'nova' + self.itype_oflavor = {'t1.micro': 'm1.tiny', + 'm1.small': 'm1.small', + 'm1.medium': 'm1.medium', + 'm1.large': 'm1.large', + 'm1.xlarge': 'm1.tiny', # TODO(sdake) + 'm2.xlarge': 'm1.xlarge', + '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): + + res = None + if key == 'AvailabilityZone': + res = self.t['Properties']['AvailabilityZone'] + elif key == 'PublicIp': + res = self.ipaddress + else: + raise exception.InvalidTemplateAttribute(resource=self.name, + key=key) + + # TODO(asalkeld) PrivateDnsName, PublicDnsName & PrivateIp + + logger.info('%s.GetAtt(%s) == %s' % (self.name, key, res)) + return unicode(res) + + def _build_userdata(self, userdata): + # Build mime multipart data blob for cloudinit userdata + mime_blob = MIMEMultipart() + fp = open('%s/%s' % (cloudinit_path, 'config'), 'r') + msg = MIMEText(fp.read(), _subtype='cloud-config') + fp.close() + msg.add_header('Content-Disposition', 'attachment', + filename='cloud-config') + mime_blob.attach(msg) + + fp = open('%s/%s' % (cloudinit_path, 'part-handler.py'), 'r') + msg = MIMEText(fp.read(), _subtype='part-handler') + fp.close() + msg.add_header('Content-Disposition', 'attachment', + filename='part-handler.py') + mime_blob.attach(msg) + + msg = MIMEText(json.dumps(self.t['Metadata']), + _subtype='x-cfninitdata') + msg.add_header('Content-Disposition', 'attachment', + filename='cfn-init-data') + mime_blob.attach(msg) + + msg = MIMEText(userdata, _subtype='x-shellscript') + msg.add_header('Content-Disposition', 'attachment', filename='startup') + mime_blob.attach(msg) + return mime_blob.as_string() + + def create(self): + def _null_callback(p, n, out): + """ + Method to silence the default M2Crypto.RSA.gen_key output. + """ + pass + + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + Resource.create(self) + props = self.t['Properties'] + if not 'KeyName' in props: + raise exception.UserParameterMissing(key='KeyName') + if not 'InstanceType' in props: + raise exception.UserParameterMissing(key='InstanceType') + if not 'ImageId' in props: + raise exception.UserParameterMissing(key='ImageId') + + security_groups = props.get('SecurityGroups') + + userdata = self.t['Properties']['UserData'] + + flavor = self.itype_oflavor[self.t['Properties']['InstanceType']] + key_name = self.t['Properties']['KeyName'] + + keypairs = self.nova().keypairs.list() + key_exists = False + for k in keypairs: + if k.name == key_name: + # cool it exists + key_exists = True + break + if not key_exists: + raise exception.UserKeyPairMissing(key_name=key_name) + + image_name = self.t['Properties']['ImageId'] + image_id = None + image_list = self.nova().images.list() + for o in image_list: + if o.name == image_name: + image_id = o.id + + if image_id is None: + logger.info("Image %s was not found in glance" % image_name) + raise exception.ImageNotFound(image_name=image_name) + + flavor_list = self.nova().flavors.list() + for o in flavor_list: + if o.name == flavor: + flavor_id = o.id + + server_userdata = self._build_userdata(userdata) + server = self.nova().servers.create(name=self.name, image=image_id, + flavor=flavor_id, + key_name=key_name, + security_groups=security_groups, + userdata=server_userdata) + while server.status == 'BUILD': + server.get() + eventlet.sleep(1) + if server.status == 'ACTIVE': + self.instance_id_set(server.id) + self.state_set(self.CREATE_COMPLETE) + # just record the first ipaddress + for n in server.networks: + self.ipaddress = server.networks[n][0] + break + else: + self.state_set(self.CREATE_FAILED) + + def reload(self): + ''' + re-read the server's ipaddress so FnGetAtt works. + ''' + try: + server = self.nova().servers.get(self.instance_id) + for n in server.networks: + self.ipaddress = server.networks[n][0] + except NotFound: + self.ipaddress = '0.0.0.0' + + Resource.reload(self) + + def delete(self): + if self.state == self.DELETE_IN_PROGRESS or \ + self.state == self.DELETE_COMPLETE: + return + self.state_set(self.DELETE_IN_PROGRESS) + Resource.delete(self) + server = self.nova().servers.get(self.instance_id) + server.delete() + self.instance_id = None + self.state_set(self.DELETE_COMPLETE) diff --git a/heat/engine/parser.py b/heat/engine/parser.py index bef64283..fb26e33e 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -19,9 +19,15 @@ import logging from heat.common import exception from heat.engine import resources +from heat.engine import instance +from heat.engine import volume +from heat.engine import eip +from heat.engine import security_group +from heat.engine import wait_condition + from heat.db import api as db_api -logger = logging.getLogger('heat.engine.parser') +logger = logging.getLogger(__file__) class Stack(object): @@ -42,6 +48,7 @@ class Stack(object): self.doc = None self.name = stack_name self.parsed_template_id = 0 + self.metadata_server = 'http://10.0.0.1' self.parms['AWS::StackName'] = {"Description": "AWS StackName", "Type": "String", @@ -66,22 +73,23 @@ class Stack(object): for r in self.t['Resources']: type = self.t['Resources'][r]['Type'] if type == 'AWS::EC2::Instance': - self.resources[r] = resources.Instance(r, + self.resources[r] = instance.Instance(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::Volume': - self.resources[r] = resources.Volume(r, + self.resources[r] = volume.Volume(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::VolumeAttachment': - self.resources[r] = resources.VolumeAttachment(r, + self.resources[r] = volume.VolumeAttachment(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::EIP': - self.resources[r] = resources.ElasticIp(r, + self.resources[r] = eip.ElasticIp(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::EIPAssociation': - self.resources[r] = resources.ElasticIpAssociation(r, + self.resources[r] = eip.ElasticIpAssociation(r, self.t['Resources'][r], self) elif type == 'AWS::EC2::SecurityGroup': - self.resources[r] = resources.SecurityGroup(r, + self.resources[r] = security_group.SecurityGroup(r, + self.t['Resources'][r], self) self.t['Resources'][r], self) else: self.resources[r] = resources.GenericResource(r, diff --git a/heat/engine/resources.py b/heat/engine/resources.py index e0a8da7a..c1bac414 100644 --- a/heat/engine/resources.py +++ b/heat/engine/resources.py @@ -20,11 +20,6 @@ import os import string import json import sys -from email import encoders -from email.message import Message -from email.mime.base import MIMEBase -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText from novaclient.v1_1 import client from novaclient.exceptions import BadRequest @@ -34,7 +29,7 @@ from heat.common import exception from heat.db import api as db_api from heat.common.config import HeatEngineConfigOpts -logger = logging.getLogger('heat.engine.resources') +logger = logging.getLogger(__file__) # 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]), @@ -203,442 +198,3 @@ class GenericResource(Resource): super(GenericResource, self).create() logger.info('creating GenericResource %s' % self.name) self.state_set(self.CREATE_COMPLETE) - - -class SecurityGroup(Resource): - - def __init__(self, name, json_snippet, stack): - super(SecurityGroup, self).__init__(name, json_snippet, stack) - self.instance_id = '' - - if 'GroupDescription' in self.t['Properties']: - self.description = self.t['Properties']['GroupDescription'] - else: - self.description = '' - - def create(self): - if self.state != None: - return - self.state_set(self.CREATE_IN_PROGRESS) - Resource.create(self) - sec = None - - groups = self.nova().security_groups.list() - for group in groups: - if group.name == self.name: - sec = group - break - - if not sec: - sec = self.nova().security_groups.create(self.name, - self.description) - - self.instance_id_set(sec.id) - - if 'SecurityGroupIngress' in self.t['Properties']: - rules_client = self.nova().security_group_rules - for i in self.t['Properties']['SecurityGroupIngress']: - try: - rule = rules_client.create(sec.id, - i['IpProtocol'], - i['FromPort'], - i['ToPort'], - i['CidrIp']) - except BadRequest as ex: - if ex.message.find('already exists') >= 0: - # no worries, the rule is already there - pass - else: - # unexpected error - raise - - self.state_set(self.CREATE_COMPLETE) - - def delete(self): - if self.state == self.DELETE_IN_PROGRESS or \ - self.state == self.DELETE_COMPLETE: - return - - self.state_set(self.DELETE_IN_PROGRESS) - Resource.delete(self) - - if self.instance_id != None: - sec = self.nova().security_groups.get(self.instance_id) - - for rule in sec.rules: - self.nova().security_group_rules.delete(rule['id']) - - self.nova().security_groups.delete(sec) - self.instance_id = None - - self.state_set(self.DELETE_COMPLETE) - - def FnGetRefId(self): - return unicode(self.name) - - -class ElasticIp(Resource): - def __init__(self, name, json_snippet, stack): - super(ElasticIp, self).__init__(name, json_snippet, stack) - self.ipaddress = '' - - if 'Domain' in self.t['Properties']: - logger.warn('*** can\'t support Domain %s yet' % \ - (self.t['Properties']['Domain'])) - - def create(self): - """Allocate a floating IP for the current tenant.""" - if self.state != None: - return - self.state_set(self.CREATE_IN_PROGRESS) - super(ElasticIp, self).create() - - ips = self.nova().floating_ips.create() - logger.info('ElasticIp create %s' % str(ips)) - self.ipaddress = ips.ip - self.instance_id_set(ips.id) - self.state_set(self.CREATE_COMPLETE) - - def reload(self): - ''' - get the ipaddress here - ''' - if self.instance_id != None: - try: - ips = self.nova().floating_ips.get(self.instance_id) - self.ipaddress = ips.ip - except Exception as ex: - logger.warn("Error getting floating IPs: %s" % str(ex)) - - Resource.reload(self) - - def delete(self): - """De-allocate a floating IP.""" - if self.state == self.DELETE_IN_PROGRESS or \ - self.state == self.DELETE_COMPLETE: - return - - self.state_set(self.DELETE_IN_PROGRESS) - Resource.delete(self) - - if self.instance_id != None: - self.nova().floating_ips.delete(self.instance_id) - - self.state_set(self.DELETE_COMPLETE) - - def FnGetRefId(self): - return unicode(self.ipaddress) - - def FnGetAtt(self, key): - if key == 'AllocationId': - return unicode(self.instance_id) - else: - raise exception.InvalidTemplateAttribute(resource=self.name, - key=key) - - -class ElasticIpAssociation(Resource): - def __init__(self, name, json_snippet, stack): - super(ElasticIpAssociation, self).__init__(name, json_snippet, stack) - - def FnGetRefId(self): - if not 'EIP' in self.t['Properties']: - return unicode('0.0.0.0') - else: - return unicode(self.t['Properties']['EIP']) - - def create(self): - """Add a floating IP address to a server.""" - - if self.state != None: - return - self.state_set(self.CREATE_IN_PROGRESS) - super(ElasticIpAssociation, self).create() - logger.debug('ElasticIpAssociation %s.add_floating_ip(%s)' % \ - (self.t['Properties']['InstanceId'], - self.t['Properties']['EIP'])) - - server = self.nova().servers.get(self.t['Properties']['InstanceId']) - server.add_floating_ip(self.t['Properties']['EIP']) - self.instance_id_set(self.t['Properties']['EIP']) - self.state_set(self.CREATE_COMPLETE) - - def delete(self): - """Remove a floating IP address from a server.""" - if self.state == self.DELETE_IN_PROGRESS or \ - self.state == self.DELETE_COMPLETE: - return - - self.state_set(self.DELETE_IN_PROGRESS) - Resource.delete(self) - - server = self.nova().servers.get(self.t['Properties']['InstanceId']) - server.remove_floating_ip(self.t['Properties']['EIP']) - - self.state_set(self.DELETE_COMPLETE) - - -class Volume(Resource): - def __init__(self, name, json_snippet, stack): - super(Volume, self).__init__(name, json_snippet, stack) - - def create(self): - if self.state != None: - return - self.state_set(self.CREATE_IN_PROGRESS) - super(Volume, self).create() - - vol = self.nova('volume').volumes.create(self.t['Properties']['Size'], - display_name=self.name, - display_description=self.name) - - while vol.status == 'creating': - eventlet.sleep(1) - vol.get() - if vol.status == 'available': - self.instance_id_set(vol.id) - self.state_set(self.CREATE_COMPLETE) - else: - self.state_set(self.CREATE_FAILED) - - def delete(self): - if self.state == self.DELETE_IN_PROGRESS or \ - self.state == self.DELETE_COMPLETE: - return - - if self.instance_id != None: - vol = self.nova('volume').volumes.get(self.instance_id) - if vol.status == 'in-use': - logger.warn('cant delete volume when in-use') - return - - self.state_set(self.DELETE_IN_PROGRESS) - Resource.delete(self) - - if self.instance_id != None: - self.nova('volume').volumes.delete(self.instance_id) - self.state_set(self.DELETE_COMPLETE) - - -class VolumeAttachment(Resource): - def __init__(self, name, json_snippet, stack): - super(VolumeAttachment, self).__init__(name, json_snippet, stack) - - def create(self): - - if self.state != None: - return - self.state_set(self.CREATE_IN_PROGRESS) - super(VolumeAttachment, self).create() - - server_id = self.t['Properties']['InstanceId'] - volume_id = self.t['Properties']['VolumeId'] - logger.warn('Attaching InstanceId %s VolumeId %s Device %s' % - (server_id, volume_id, self.t['Properties']['Device'])) - volapi = self.nova().volumes - va = volapi.create_server_volume(server_id=server_id, - volume_id=volume_id, - device=self.t['Properties']['Device']) - - vol = self.nova('volume').volumes.get(va.id) - while vol.status == 'available' or vol.status == 'attaching': - eventlet.sleep(1) - vol.get() - if vol.status == 'in-use': - self.instance_id_set(va.id) - self.state_set(self.CREATE_COMPLETE) - else: - self.state_set(self.CREATE_FAILED) - - def delete(self): - if self.state == self.DELETE_IN_PROGRESS or \ - self.state == self.DELETE_COMPLETE: - return - self.state_set(self.DELETE_IN_PROGRESS) - Resource.delete(self) - - server_id = self.t['Properties']['InstanceId'] - volume_id = self.t['Properties']['VolumeId'] - logger.info('VolumeAttachment un-attaching %s %s' % \ - (server_id, volume_id)) - - volapi = self.nova().volumes - volapi.delete_server_volume(server_id, - volume_id) - - vol = self.nova('volume').volumes.get(volume_id) - logger.info('un-attaching %s, status %s' % (volume_id, vol.status)) - while vol.status == 'in-use': - logger.info('trying to un-attach %s, but still %s' % - (volume_id, vol.status)) - eventlet.sleep(1) - try: - volapi.delete_server_volume(server_id, - volume_id) - except Exception: - pass - vol.get() - - self.state_set(self.DELETE_COMPLETE) - - -class Instance(Resource): - - def __init__(self, name, json_snippet, stack): - super(Instance, self).__init__(name, json_snippet, stack) - self.ipaddress = '0.0.0.0' - - if not 'AvailabilityZone' in self.t['Properties']: - self.t['Properties']['AvailabilityZone'] = 'nova' - self.itype_oflavor = {'t1.micro': 'm1.tiny', - 'm1.small': 'm1.small', - 'm1.medium': 'm1.medium', - 'm1.large': 'm1.large', - 'm1.xlarge': 'm1.tiny', # TODO(sdake) - 'm2.xlarge': 'm1.xlarge', - '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): - - res = None - if key == 'AvailabilityZone': - res = self.t['Properties']['AvailabilityZone'] - elif key == 'PublicIp': - res = self.ipaddress - else: - raise exception.InvalidTemplateAttribute(resource=self.name, - key=key) - - # TODO(asalkeld) PrivateDnsName, PublicDnsName & PrivateIp - - logger.info('%s.GetAtt(%s) == %s' % (self.name, key, res)) - return unicode(res) - - def _build_userdata(self, userdata): - # Build mime multipart data blob for cloudinit userdata - mime_blob = MIMEMultipart() - fp = open('%s/%s' % (cloudinit_path, 'config'), 'r') - msg = MIMEText(fp.read(), _subtype='cloud-config') - fp.close() - msg.add_header('Content-Disposition', 'attachment', - filename='cloud-config') - mime_blob.attach(msg) - - fp = open('%s/%s' % (cloudinit_path, 'part-handler.py'), 'r') - msg = MIMEText(fp.read(), _subtype='part-handler') - fp.close() - msg.add_header('Content-Disposition', 'attachment', - filename='part-handler.py') - mime_blob.attach(msg) - - msg = MIMEText(json.dumps(self.t['Metadata']), - _subtype='x-cfninitdata') - msg.add_header('Content-Disposition', 'attachment', - filename='cfn-init-data') - mime_blob.attach(msg) - - msg = MIMEText(userdata, _subtype='x-shellscript') - msg.add_header('Content-Disposition', 'attachment', filename='startup') - mime_blob.attach(msg) - return mime_blob.as_string() - - def create(self): - def _null_callback(p, n, out): - """ - Method to silence the default M2Crypto.RSA.gen_key output. - """ - pass - - if self.state != None: - return - self.state_set(self.CREATE_IN_PROGRESS) - Resource.create(self) - props = self.t['Properties'] - if not 'KeyName' in props: - raise exception.UserParameterMissing(key='KeyName') - if not 'InstanceType' in props: - raise exception.UserParameterMissing(key='InstanceType') - if not 'ImageId' in props: - raise exception.UserParameterMissing(key='ImageId') - - security_groups = props.get('SecurityGroups') - - userdata = self.t['Properties']['UserData'] - - flavor = self.itype_oflavor[self.t['Properties']['InstanceType']] - distro_name = self.stack.parameter_get('LinuxDistribution') - key_name = self.t['Properties']['KeyName'] - - keypairs = self.nova().keypairs.list() - key_exists = False - for k in keypairs: - if k.name == key_name: - # cool it exists - key_exists = True - break - if not key_exists: - raise exception.UserKeyPairMissing(key_name=key_name) - - image_name = self.t['Properties']['ImageId'] - image_id = None - image_list = self.nova().images.list() - for o in image_list: - if o.name == image_name: - image_id = o.id - - if image_id is None: - logger.info("Image %s was not found in glance" % image_name) - raise exception.ImageNotFound(image_name=image_name) - - flavor_list = self.nova().flavors.list() - for o in flavor_list: - if o.name == flavor: - flavor_id = o.id - - server_userdata = self._build_userdata(userdata) - server = self.nova().servers.create(name=self.name, image=image_id, - flavor=flavor_id, - key_name=key_name, - security_groups=security_groups, - userdata=server_userdata) - while server.status == 'BUILD': - server.get() - eventlet.sleep(1) - if server.status == 'ACTIVE': - self.instance_id_set(server.id) - self.state_set(self.CREATE_COMPLETE) - # just record the first ipaddress - for n in server.networks: - self.ipaddress = server.networks[n][0] - break - else: - self.state_set(self.CREATE_FAILED) - - def reload(self): - ''' - re-read the server's ipaddress so FnGetAtt works. - ''' - try: - server = self.nova().servers.get(self.instance_id) - for n in server.networks: - self.ipaddress = server.networks[n][0] - except NotFound: - self.ipaddress = '0.0.0.0' - - Resource.reload(self) - - def delete(self): - if self.state == self.DELETE_IN_PROGRESS or \ - self.state == self.DELETE_COMPLETE: - return - self.state_set(self.DELETE_IN_PROGRESS) - Resource.delete(self) - server = self.nova().servers.get(self.instance_id) - server.delete() - self.instance_id = None - self.state_set(self.DELETE_COMPLETE) diff --git a/heat/engine/security_group.py b/heat/engine/security_group.py new file mode 100644 index 00000000..7e2fec23 --- /dev/null +++ b/heat/engine/security_group.py @@ -0,0 +1,98 @@ +# 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 eventlet +import logging +import os + +from novaclient.exceptions import BadRequest +from heat.common import exception +from heat.engine.resources import Resource + +logger = logging.getLogger(__file__) + + +class SecurityGroup(Resource): + + def __init__(self, name, json_snippet, stack): + super(SecurityGroup, self).__init__(name, json_snippet, stack) + self.instance_id = '' + + if 'GroupDescription' in self.t['Properties']: + self.description = self.t['Properties']['GroupDescription'] + else: + self.description = '' + + def create(self): + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + Resource.create(self) + sec = None + + groups = self.nova().security_groups.list() + for group in groups: + if group.name == self.name: + sec = group + break + + if not sec: + sec = self.nova().security_groups.create(self.name, + self.description) + + self.instance_id_set(sec.id) + + if 'SecurityGroupIngress' in self.t['Properties']: + rules_client = self.nova().security_group_rules + for i in self.t['Properties']['SecurityGroupIngress']: + try: + rule = rules_client.create(sec.id, + i['IpProtocol'], + i['FromPort'], + i['ToPort'], + i['CidrIp']) + except BadRequest as ex: + if ex.message.find('already exists') >= 0: + # no worries, the rule is already there + pass + else: + # unexpected error + raise + + self.state_set(self.CREATE_COMPLETE) + + def delete(self): + if self.state == self.DELETE_IN_PROGRESS or \ + self.state == self.DELETE_COMPLETE: + return + + self.state_set(self.DELETE_IN_PROGRESS) + Resource.delete(self) + + if self.instance_id != None: + sec = self.nova().security_groups.get(self.instance_id) + + for rule in sec.rules: + self.nova().security_group_rules.delete(rule['id']) + + self.nova().security_groups.delete(sec) + self.instance_id = None + + self.state_set(self.DELETE_COMPLETE) + + def FnGetRefId(self): + return unicode(self.name) + + diff --git a/heat/engine/volume.py b/heat/engine/volume.py new file mode 100644 index 00000000..eb5f0cac --- /dev/null +++ b/heat/engine/volume.py @@ -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. + +import eventlet +import logging +import os + +from heat.common import exception +from heat.engine.resources import Resource + +logger = logging.getLogger(__file__) + + +class Volume(Resource): + def __init__(self, name, json_snippet, stack): + super(Volume, self).__init__(name, json_snippet, stack) + + def create(self): + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + super(Volume, self).create() + + vol = self.nova('volume').volumes.create(self.t['Properties']['Size'], + display_name=self.name, + display_description=self.name) + + while vol.status == 'creating': + eventlet.sleep(1) + vol.get() + if vol.status == 'available': + self.instance_id_set(vol.id) + self.state_set(self.CREATE_COMPLETE) + else: + self.state_set(self.CREATE_FAILED) + + def delete(self): + if self.state == self.DELETE_IN_PROGRESS or \ + self.state == self.DELETE_COMPLETE: + return + + if self.instance_id != None: + vol = self.nova('volume').volumes.get(self.instance_id) + if vol.status == 'in-use': + logger.warn('cant delete volume when in-use') + return + + self.state_set(self.DELETE_IN_PROGRESS) + Resource.delete(self) + + if self.instance_id != None: + self.nova('volume').volumes.delete(self.instance_id) + self.state_set(self.DELETE_COMPLETE) + + +class VolumeAttachment(Resource): + def __init__(self, name, json_snippet, stack): + super(VolumeAttachment, self).__init__(name, json_snippet, stack) + + def create(self): + + if self.state != None: + return + self.state_set(self.CREATE_IN_PROGRESS) + super(VolumeAttachment, self).create() + + server_id = self.t['Properties']['InstanceId'] + volume_id = self.t['Properties']['VolumeId'] + logger.warn('Attaching InstanceId %s VolumeId %s Device %s' % + (server_id, volume_id, self.t['Properties']['Device'])) + volapi = self.nova().volumes + va = volapi.create_server_volume(server_id=server_id, + volume_id=volume_id, + device=self.t['Properties']['Device']) + + vol = self.nova('volume').volumes.get(va.id) + while vol.status == 'available' or vol.status == 'attaching': + eventlet.sleep(1) + vol.get() + if vol.status == 'in-use': + self.instance_id_set(va.id) + self.state_set(self.CREATE_COMPLETE) + else: + self.state_set(self.CREATE_FAILED) + + def delete(self): + if self.state == self.DELETE_IN_PROGRESS or \ + self.state == self.DELETE_COMPLETE: + return + self.state_set(self.DELETE_IN_PROGRESS) + Resource.delete(self) + + server_id = self.t['Properties']['InstanceId'] + volume_id = self.t['Properties']['VolumeId'] + logger.info('VolumeAttachment un-attaching %s %s' % \ + (server_id, volume_id)) + + volapi = self.nova().volumes + volapi.delete_server_volume(server_id, + volume_id) + + vol = self.nova('volume').volumes.get(volume_id) + logger.info('un-attaching %s, status %s' % (volume_id, vol.status)) + while vol.status == 'in-use': + logger.info('trying to un-attach %s, but still %s' % + (volume_id, vol.status)) + eventlet.sleep(1) + try: + volapi.delete_server_volume(server_id, + volume_id) + except Exception: + pass + vol.get() + + self.state_set(self.DELETE_COMPLETE) + diff --git a/heat/tests/test_resources.py b/heat/tests/test_resources.py index f5a4fbaa..3a4f0cdf 100644 --- a/heat/tests/test_resources.py +++ b/heat/tests/test_resources.py @@ -12,6 +12,7 @@ from nose import with_setup from heat.tests.v1_1 import fakes from heat.engine import resources +from heat.engine import instance import heat.db as db_api from heat.engine import parser @@ -42,11 +43,11 @@ class ResourcesTest(unittest.TestCase): db_api.resource_get_by_name_and_stack(None, 'test_resource_name',\ stack).AndReturn(None) - self.m.StubOutWithMock(resources.Instance, 'nova') - resources.Instance.nova().AndReturn(self.fc) - resources.Instance.nova().AndReturn(self.fc) - resources.Instance.nova().AndReturn(self.fc) - resources.Instance.nova().AndReturn(self.fc) + self.m.StubOutWithMock(instance.Instance, 'nova') + instance.Instance.nova().AndReturn(self.fc) + instance.Instance.nova().AndReturn(self.fc) + instance.Instance.nova().AndReturn(self.fc) + instance.Instance.nova().AndReturn(self.fc) #Need to find an easier way userdata = t['Resources']['WebServer']['Properties']['UserData'] @@ -56,10 +57,10 @@ class ResourcesTest(unittest.TestCase): t['Resources']['WebServer']['Properties']['ImageId'] = 'CentOS 5.2' t['Resources']['WebServer']['Properties']['InstanceType'] = \ '256 MB Server' - instance = resources.Instance('test_resource_name',\ - t['Resources']['WebServer'], stack) + inst = instance.Instance('test_resource_name',\ + t['Resources']['WebServer'], stack) - server_userdata = instance._build_userdata(json.dumps(userdata)) + server_userdata = inst._build_userdata(json.dumps(userdata)) self.m.StubOutWithMock(self.fc.servers, 'create') self.fc.servers.create(image=1, flavor=1, key_name='test',\ name='test_resource_name', security_groups=None,\ @@ -67,16 +68,16 @@ class ResourcesTest(unittest.TestCase): AndReturn(self.fc.servers.list()[1]) self.m.ReplayAll() - instance.itype_oflavor['256 MB Server'] = '256 MB Server' - instance.create() + inst.itype_oflavor['256 MB Server'] = '256 MB Server' + inst.create() self.m.ReplayAll() - instance.itype_oflavor['256 MB Server'] = '256 MB Server' - instance.create() + inst.itype_oflavor['256 MB Server'] = '256 MB Server' + inst.create() # this makes sure the auto increment worked on instance creation - assert(instance.id > 0) + assert(inst.id > 0) def test_initialize_instance_from_template_and_delete(self): f = open('../../templates/WordPress_Single_Instance_gold.template') @@ -93,11 +94,11 @@ class ResourcesTest(unittest.TestCase): db_api.resource_get_by_name_and_stack(None, 'test_resource_name',\ stack).AndReturn(None) - self.m.StubOutWithMock(resources.Instance, 'nova') - resources.Instance.nova().AndReturn(self.fc) - resources.Instance.nova().AndReturn(self.fc) - resources.Instance.nova().AndReturn(self.fc) - resources.Instance.nova().AndReturn(self.fc) + self.m.StubOutWithMock(instance.Instance, 'nova') + instance.Instance.nova().AndReturn(self.fc) + instance.Instance.nova().AndReturn(self.fc) + instance.Instance.nova().AndReturn(self.fc) + instance.Instance.nova().AndReturn(self.fc) #Need to find an easier way userdata = t['Resources']['WebServer']['Properties']['UserData'] @@ -107,10 +108,10 @@ class ResourcesTest(unittest.TestCase): t['Resources']['WebServer']['Properties']['ImageId'] = 'CentOS 5.2' t['Resources']['WebServer']['Properties']['InstanceType'] = \ '256 MB Server' - instance = resources.Instance('test_resource_name',\ - t['Resources']['WebServer'], stack) + inst = instance.Instance('test_resource_name',\ + t['Resources']['WebServer'], stack) - server_userdata = instance._build_userdata(json.dumps(userdata)) + server_userdata = inst._build_userdata(json.dumps(userdata)) self.m.StubOutWithMock(self.fc.servers, 'create') self.fc.servers.create(image=1, flavor=1, key_name='test',\ name='test_resource_name', security_groups=None,\ @@ -118,18 +119,18 @@ class ResourcesTest(unittest.TestCase): AndReturn(self.fc.servers.list()[1]) self.m.ReplayAll() - instance.itype_oflavor['256 MB Server'] = '256 MB Server' - instance.create() + inst.itype_oflavor['256 MB Server'] = '256 MB Server' + inst.create() self.m.ReplayAll() - instance.instance_id = 1234 - instance.itype_oflavor['256 MB Server'] = '256 MB Server' - instance.create() + inst.instance_id = 1234 + inst.itype_oflavor['256 MB Server'] = '256 MB Server' + inst.create() - instance.delete() - assert(instance.instance_id == None) - assert(instance.state == instance.DELETE_COMPLETE) + inst.delete() + assert(inst.instance_id == None) + assert(inst.state == inst.DELETE_COMPLETE) # allows testing of the test directly, shown below if __name__ == '__main__':