heat/heat/engine/resources/loadbalancer.py

352 lines
11 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.
from heat.engine import clients
from heat.common import exception
from heat.common import template_format
from heat.engine.resources import stack
from heat.openstack.common import log as logging
logger = logging.getLogger(__name__)
lb_template = '''
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Built in HAProxy server",
"Parameters" : {
"KeyName" : {
"Type" : "String"
}
},
"Resources": {
"latency_watcher": {
"Type": "AWS::CloudWatch::Alarm",
"Properties": {
"MetricName": "Latency",
"Namespace": "AWS/ELB",
"Statistic": "Average",
"Period": "60",
"EvaluationPeriods": "1",
"Threshold": "2",
"AlarmActions": [],
"ComparisonOperator": "GreaterThanThreshold"
}
},
"LB_instance": {
"Type": "AWS::EC2::Instance",
"Metadata": {
"AWS::CloudFormation::Init": {
"config": {
"packages": {
"yum": {
"cronie" : [],
"haproxy" : [],
"socat" : [],
"python-psutil" : []
}
},
"services": {
"systemd": {
"crond" : { "enabled" : "true", "ensureRunning" : "true" },
"haproxy" : { "enabled": "true", "ensureRunning": "true" }
}
},
"files": {
"/etc/cfn/cfn-hup.conf" : {
"content" : { "Fn::Join" : ["", [
"[main]\\n",
"stack=", { "Ref" : "AWS::StackName" }, "\\n",
"credential-file=/etc/cfn/cfn-credentials\\n",
"region=", { "Ref" : "AWS::Region" }, "\\n",
"interval=60\\n"
]]},
"mode" : "000400",
"owner" : "root",
"group" : "root"
},
"/etc/cfn/hooks.conf" : {
"content": { "Fn::Join" : ["", [
"[cfn-init]\\n",
"triggers=post.update\\n",
"path=Resources.LB_instance.Metadata\\n",
"action=/opt/aws/bin/cfn-init -s ",
{ "Ref": "AWS::StackName" },
" -r LB_instance ",
" --region ", { "Ref": "AWS::Region" }, "\\n",
"runas=root\\n",
"\\n",
"[reload]\\n",
"triggers=post.update\\n",
"path=Resources.LB_instance.Metadata\\n",
"action=systemctl reload haproxy.service\\n",
"runas=root\\n"
]]},
"mode" : "000400",
"owner" : "root",
"group" : "root"
},
"/etc/haproxy/haproxy.cfg": {
"content": "",
"mode": "000644",
"owner": "root",
"group": "root"
},
"/tmp/cfn-hup-crontab.txt" : {
"content" : { "Fn::Join" : ["", [
"MAIL=\\"\\"\\n",
"\\n",
"* * * * * /opt/aws/bin/cfn-hup -f\\n",
"* * * * * /opt/aws/bin/cfn-push-stats ",
" --watch latency_watcher --haproxy\\n"
]]},
"mode" : "000600",
"owner" : "root",
"group" : "root"
}
}
}
}
},
"Properties": {
"ImageId": "F17-x86_64-cfntools",
"InstanceType": "m1.small",
"KeyName": { "Ref": "KeyName" },
"UserData": { "Fn::Base64": { "Fn::Join": ["", [
"#!/bin/bash -v\\n",
"/opt/aws/bin/cfn-init -s ",
{ "Ref": "AWS::StackName" },
" -r LB_instance ",
" --region ", { "Ref": "AWS::Region" }, "\\n",
"touch /etc/cfn/cfn-credentials\\n",
"# install cfn-hup crontab\\n",
"crontab /tmp/cfn-hup-crontab.txt\\n"
]]}}
}
}
},
"Outputs": {
"PublicIp": {
"Value": { "Fn::GetAtt": [ "LB_instance", "PublicIp" ] },
"Description": "instance IP"
}
}
}
'''
#
# TODO(asalkeld) the above inline template _could_ be placed in an external
# file at the moment this is because we will probably need to implement a
# LoadBalancer based on keepalived as well (for for ssl support).
#
class LoadBalancer(stack.NestedStack):
listeners_schema = {
'InstancePort': {'Type': 'Number',
'Required': True},
'LoadBalancerPort': {'Type': 'Number',
'Required': True},
'Protocol': {'Type': 'String',
'Required': True,
'AllowedValues': ['TCP', 'HTTP']},
'SSLCertificateId': {'Type': 'String',
'Implemented': False},
'PolicyNames': {'Type': 'List',
'Implemented': False}
}
healthcheck_schema = {
'HealthyThreshold': {'Type': 'Number',
'Required': True},
'Interval': {'Type': 'Number',
'Required': True},
'Target': {'Type': 'String',
'Required': True},
'Timeout': {'Type': 'Number',
'Required': True},
'UnhealthyThreshold': {'Type': 'Number',
'Required': True},
}
properties_schema = {
'AvailabilityZones': {'Type': 'List',
'Required': True},
'HealthCheck': {'Type': 'Map',
'Schema': healthcheck_schema},
'Instances': {'Type': 'List'},
'Listeners': {'Type': 'List',
'Schema': {'Type': 'Map',
'Schema': listeners_schema}},
'AppCookieStickinessPolicy': {'Type': 'String',
'Implemented': False},
'LBCookieStickinessPolicy': {'Type': 'String',
'Implemented': False},
'SecurityGroups': {'Type': 'String',
'Implemented': False},
'Subnets': {'Type': 'List',
'Implemented': False}
}
def _instance_to_ipaddress(self, inst):
'''
Return the server's IP address, fetching it from Nova
'''
try:
server = self.nova().servers.get(inst)
except clients.novaclient.exceptions.NotFound as ex:
logger.warn('Instance (%s) not found: %s' % (inst, str(ex)))
else:
for n in server.networks:
return server.networks[n][0]
return '0.0.0.0'
def _haproxy_config(self, templ, instances):
# initial simplifications:
# - only one Listener
# - only http (no tcp or ssl)
#
# option httpchk HEAD /check.txt HTTP/1.0
gl = '''
global
daemon
maxconn 256
stats socket /tmp/.haproxy-stats
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
'''
listener = self.properties['Listeners'][0]
lb_port = listener['LoadBalancerPort']
inst_port = listener['InstancePort']
spaces = ' '
frontend = '''
frontend http
bind *:%s
''' % (lb_port)
health_chk = self.properties['HealthCheck']
if health_chk:
check = 'check inter %ss fall %s rise %s' % (
health_chk['Interval'],
health_chk['UnhealthyThreshold'],
health_chk['HealthyThreshold'])
timeout_check = 'timeout check %ds' % int(health_chk['Timeout'])
else:
check = ''
timeout_check = ''
backend = '''
default_backend servers
backend servers
balance roundrobin
option http-server-close
option forwardfor
option httpchk
%s
''' % timeout_check
servers = []
n = 1
for i in instances:
ip = self._instance_to_ipaddress(i)
logger.debug('haproxy server:%s' % ip)
servers.append('%sserver server%d %s:%s %s' % (spaces, n,
ip, inst_port,
check))
n = n + 1
return '%s%s%s%s\n' % (gl, frontend, backend, '\n'.join(servers))
def handle_create(self):
templ = template_format.parse(lb_template)
if self.properties['Instances']:
md = templ['Resources']['LB_instance']['Metadata']
files = md['AWS::CloudFormation::Init']['config']['files']
cfg = self._haproxy_config(templ, self.properties['Instances'])
files['/etc/haproxy/haproxy.cfg']['content'] = cfg
# total hack - probably need an admin key here.
param = self.stack.resolve_static_data({'KeyName': {'Ref': 'KeyName'}})
self.create_with_template(templ, param)
def validate(self):
'''
Validate any of the provided params
'''
res = super(LoadBalancer, self).validate()
if res:
return res
health_chk = self.properties['HealthCheck']
if health_chk:
if float(health_chk['Interval']) < float(health_chk['Timeout']):
return {'Error':
'Interval must be larger than Timeout'}
def reload(self, inst_list):
'''
re-generate the Metadata
save it to the db.
rely on the cfn-hup to reconfigure HAProxy
'''
templ = template_format.parse(lb_template)
cfg = self._haproxy_config(templ, inst_list)
md = self.nested()['LB_instance'].metadata
files = md['AWS::CloudFormation::Init']['config']['files']
files['/etc/haproxy/haproxy.cfg']['content'] = cfg
self.nested()['LB_instance'].metadata = md
def FnGetRefId(self):
return unicode(self.name)
def handle_update(self):
return self.UPDATE_REPLACE
def FnGetAtt(self, key):
'''
We don't really support any of these yet.
'''
allow = ('CanonicalHostedZoneName',
'CanonicalHostedZoneNameID',
'DNSName',
'SourceSecurityGroupName',
'SourceSecurityGroupOwnerAlias')
if not key in allow:
raise exception.InvalidTemplateAttribute(resource=self.name,
key=key)
if key == 'DNSName':
return self.get_output('PublicIp')
else:
return ''
def resource_mapping():
return {
'AWS::ElasticLoadBalancing::LoadBalancer': LoadBalancer,
}