]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Split the resourses up into seperate files.
authorAngus Salkeld <asalkeld@redhat.com>
Thu, 3 May 2012 04:34:13 +0000 (14:34 +1000)
committerAngus Salkeld <asalkeld@redhat.com>
Thu, 3 May 2012 04:45:01 +0000 (14:45 +1000)
Signed-off-by: Angus Salkeld <asalkeld@redhat.com>
heat/engine/eip.py [new file with mode: 0644]
heat/engine/instance.py [new file with mode: 0644]
heat/engine/parser.py
heat/engine/resources.py
heat/engine/security_group.py [new file with mode: 0644]
heat/engine/volume.py [new file with mode: 0644]
heat/tests/test_resources.py

diff --git a/heat/engine/eip.py b/heat/engine/eip.py
new file mode 100644 (file)
index 0000000..f0b2b62
--- /dev/null
@@ -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 (file)
index 0000000..ad72b80
--- /dev/null
@@ -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)
index bef64283f2a31adfec457dd2cf5b9ca4b7674965..fb26e33e674fd34fa4dd9e2e2bdb4b15de648df3 100644 (file)
@@ -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,
index e0a8da7a7bedb1c10ce7e0914f1ee5b587df11b2..c1bac4147adda1d1142c9453378561b8d11840ca 100644 (file)
@@ -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 (file)
index 0000000..7e2fec2
--- /dev/null
@@ -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 (file)
index 0000000..eb5f0ca
--- /dev/null
@@ -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)
+
index f5a4fbaad526e6d66bc0687e8f3e70149685b1c9..3a4f0cdf794ae792add16f3a247509a3a54ccd3f 100644 (file)
@@ -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__':