235 lines
8.4 KiB
Python
235 lines
8.4 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
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'
|
|
self.mime_string = None
|
|
|
|
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):
|
|
if not self.mime_string:
|
|
# 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)
|
|
|
|
if self.stack.metadata_server:
|
|
msg = MIMEText(self.stack.metadata_server,
|
|
_subtype='x-cfninitdata')
|
|
msg.add_header('Content-Disposition', 'attachment',
|
|
filename='cfn-metadata-server')
|
|
mime_blob.attach(msg)
|
|
|
|
msg = MIMEText(userdata, _subtype='x-shellscript')
|
|
msg.add_header('Content-Disposition', 'attachment', filename='startup')
|
|
mime_blob.attach(msg)
|
|
self.mime_string = mime_blob.as_string()
|
|
|
|
return self.mime_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 validate(self):
|
|
'''
|
|
Validate any of the provided params
|
|
'''
|
|
#check validity of key
|
|
if self.stack.parms['KeyName']:
|
|
keypairs = self.nova().keypairs.list()
|
|
valid_key = False
|
|
for k in keypairs:
|
|
if k.name == self.stack.parms['KeyName'].get('Value'):
|
|
valid_key = True
|
|
if not valid_key:
|
|
return {'Error': \
|
|
'Provided KeyName is not registered with nova'}
|
|
return None
|
|
|
|
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)
|