Initial commit of ELB policy management for boto_elb.

This commit is contained in:
Stephen Woodrow 2015-10-02 11:28:10 -07:00
parent 281e4fd966
commit 2f715803b2
2 changed files with 404 additions and 91 deletions

View file

@ -130,26 +130,48 @@ def get_elb_config(name, region=None, key=None, keyid=None, profile=None):
ret['availability_zones'] = lb.availability_zones
listeners = []
for _listener in lb.listeners:
# Making this a list makes our life easier and is also the only way
# to include the certificate.
complex_listener = list(_listener.get_complex_tuple())
# boto, you're *killing* me with this. boto doesn't include the
# certificate when calling get_complex_tuple, so you need to also
# get the certificate. So. Much. Hate.
listener_dict = {}
listener_dict['elb_port'] = _listener.load_balancer_port
listener_dict['elb_protocol'] = _listener.protocol
listener_dict['instance_port'] = _listener.instance_port
listener_dict['instance_protocol'] = _listener.instance_protocol
listener_dict['policies'] = _listener.policy_names
if _listener.ssl_certificate_id:
complex_listener.append(_listener.ssl_certificate_id)
listeners.append(complex_listener)
listener_dict['certificate'] = _listener.ssl_certificate_id
listeners.append(listener_dict)
ret['listeners'] = listeners
ret['subnets'] = lb.subnets
ret['security_groups'] = lb.security_groups
ret['scheme'] = lb.scheme
ret['dns_name'] = lb.dns_name
lb_policy_lists = [
lb.policies.app_cookie_stickiness_policies,
lb.policies.lb_cookie_stickiness_policies,
lb.policies.other_policies
]
policies = []
for policy_list in lb_policy_lists:
policies += [p.policy_name for p in policy_list]
ret['policies'] = policies
return ret
except boto.exception.BotoServerError as error:
log.debug(error)
return []
def listener_dict_to_tuple(listener):
# We define all listeners as complex listeners.
if 'instance_protocol' not in listener:
instance_protocol = listener['elb_protocol'].upper()
else:
instance_protocol = listener['instance_protocol'].upper()
listener_tuple = [listener['elb_port'], listener['instance_port'],
listener['elb_protocol'], instance_protocol]
if 'certificate' in listener:
listener_tuple.append(listener['certificate'])
return tuple(listener_tuple)
def create(name, availability_zones, listeners=None, subnets=None,
security_groups=None, scheme='internet-facing',
region=None, key=None, keyid=None,
@ -159,31 +181,20 @@ def create(name, availability_zones, listeners=None, subnets=None,
CLI example to create an ELB::
salt myminion boto_elb.create myelb '["us-east-1a", "us-east-1e"]' listeners='[[443, 80, "HTTPS", "HTTP", "arn:aws:iam::1111111:server-certificate/mycert"]]' region=us-east-1
salt myminion boto_elb.create myelb '["us-east-1a", "us-east-1e"]' listeners='{"elb_port": 443, "elb_protocol": "HTTPS", ...}' region=us-east-1
'''
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
if __salt__['boto_elb.exists'](name, region, key, keyid, profile):
if exists(name, region, key, keyid, profile):
return True
if isinstance(availability_zones, string_types):
availability_zones = json.loads(availability_zones)
if isinstance(listeners, string_types):
listeners = json.loads(listeners)
# Combining listeners and complex_listeners together makes our lives
# easier in some ways, especially since during introspection you can
# only get a combined set of listeners back from boto; however, boto
# requires us to send in separate listeners and complex listeners and
# the only real difference is the size. It feels like amazon/boto hate
# developers and wish to make us suffer.
_listeners = []
_complex_listeners = []
for listener in listeners:
if len(listener) <= 3:
_listeners.append(listener)
else:
_complex_listeners.append(listener)
_complex_listeners.append(listener_dict_to_tuple(listener))
try:
lb = conn.create_load_balancer(name, availability_zones, _listeners,
lb = conn.create_load_balancer(name, availability_zones, [],
subnets, security_groups, scheme,
_complex_listeners)
if lb:
@ -210,7 +221,7 @@ def delete(name, region=None, key=None, keyid=None, profile=None):
'''
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
if not __salt__['boto_elb.exists'](name, region, key, keyid, profile):
if not exists(name, region, key, keyid, profile):
return True
try:
conn.delete_load_balancer(name)
@ -237,22 +248,11 @@ def create_listeners(name, listeners=None, region=None, key=None, keyid=None,
if isinstance(listeners, string_types):
listeners = json.loads(listeners)
# Combining listeners and complex_listeners together makes our lives
# easier in some ways, especially since during introspection you can
# only get a combined set of listeners back from boto; however, boto
# requires us to send in separate listeners and complex listeners and
# the only real difference is the size. It feels like amazon/boto hate
# developers and wish to make us suffer.
_listeners = []
_complex_listeners = []
for listener in listeners:
if len(listener) <= 3:
_listeners.append(listener)
else:
_complex_listeners.append(listener)
_complex_listeners.append(listener_dict_to_tuple(listener))
try:
conn.create_load_balancer_listeners(name, _listeners,
_complex_listeners)
conn.create_load_balancer_listeners(name, [], _complex_listeners)
msg = 'Created ELB listeners on {0}'.format(name)
log.info(msg)
return True
@ -686,3 +686,60 @@ def get_instance_health(name, region=None, key=None, keyid=None, profile=None, i
except boto.exception.BotoServerError as error:
log.debug(error)
return []
def create_policy(name, policy_name, policy_type, policy, region=None,
key=None, keyid=None, profile=None):
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
if not exists(name, region, key, keyid, profile):
return False
try:
success = conn.create_lb_policy(name, policy_name, policy_type, policy)
if success:
log.info('Created policy {0} on ELB {1}'.format(policy_name, name))
return True
else:
msg = 'Failed to create policy {0} on ELB {1}'.format(policy_name, name)
log.error(msg)
return False
except boto.exception.BotoServerError as e:
log.debug(e)
msg = 'Failed to create policy {0} on ELB {1}: {2}'.format(policy_name, name, e.message)
log.error(msg)
return False
def delete_policy(name, policy_name, region=None, key=None, keyid=None,
profile=None):
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
if not exists(name, region, key, keyid, profile):
return True
try:
conn.delete_lb_policy(name, policy_name)
log.info('Deleted policy {0} on ELB {1}'.format(policy_name, name))
return True
except boto.exception.BotoServerError as e:
log.debug(e)
msg = 'Failed to delete policy {0} on ELB {1}: {2}'.format(policy_name, name, e.message)
log.error(msg)
return False
def set_listener_policy(name, port, policies=None, region=None, key=None,
keyid=None, profile=None):
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
if not exists(name, region, key, keyid, profile):
return True
if policies is None:
policies = []
try:
conn.set_lb_policies_of_listener(name, port, policies)
log.info('Set policies {0} on ELB {1} listener {2}'.format(policies, name, port))
except boto.exception.BotoServerError as e:
log.debug(e)
log.info('Failed to set policy {0} on ELB {1} listener {2}: {3}'.format(policies, name, port, e.message))
return False
return True

View file

@ -51,6 +51,9 @@ passed in as a dict, or as a string to pull from pillars or minion config:
elb_protocol: HTTPS
instance_protocol: HTTP
certificate: 'arn:aws:iam::1111111:server-certificate/mycert'
policies:
- my-ssl-policy
- cookie-policy
- elb_port: 8210
instance_port: 8210
elb_protocol: TCP
@ -70,6 +73,17 @@ passed in as a dict, or as a string to pull from pillars or minion config:
ttl: 60
- name: myothercname.example.com.
zone: example.com.
- policies:
- policy_name: my-ssl-policy
policy_type: SSLNegotiationPolicyType
policy:
Protocol-TLSv1.2: true
Protocol-SSLv3: false
Server-Defined-Cipher-Order: true
ECDHE-ECDSA-AES128-GCM-SHA256: true
- policy_name: cookie-policy
policy_type: LBCookieStickinessPolicyType
policy: {} # no policy means this is a session cookie
# Using a profile from pillars
Ensure myelb ELB exists:
@ -198,11 +212,12 @@ Overriding the alarm values on the resource:
from __future__ import absolute_import
# Import Salt Libs
import hashlib
import re
import salt.utils.dictupdate as dictupdate
from salt.exceptions import SaltInvocationError
import salt.ext.six as six
def __virtual__():
'''
Only load if boto is available.
@ -223,6 +238,8 @@ def present(
cnames=None,
alarms=None,
alarms_from_pillar="boto_elb_alarms",
policies=None,
policies_from_pillar="boto_elb_policies",
region=None,
key=None,
keyid=None,
@ -356,6 +373,14 @@ def present(
_ret = _alarms_present(name, alarms, alarms_from_pillar, region, key, keyid, profile)
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
if not _ret['result']:
ret['result'] = _ret['result']
if ret['result'] is False:
return ret
_ret = _policies_present(name, policies, policies_from_pillar, listeners, region, key,
keyid, profile)
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
if not _ret['result']:
ret['result'] = _ret['result']
if ret['result'] is False:
@ -425,6 +450,8 @@ def register_instances(name, instances, region=None, key=None, keyid=None,
return ret
DEFAULT_PILLAR_LISTENER_POLICY_KEY = 'boto_elb_listener_policies'
def _elb_present(
name,
availability_zones,
@ -445,33 +472,36 @@ def _elb_present(
' exclusive arguments.')
if not listeners:
listeners = []
_listeners = []
for listener in listeners:
if len(listener) < 3:
raise SaltInvocationError('Listeners must have at minimum port,'
' instance_port and protocol values in'
' the provided list.')
for config in ('elb_port', 'instance_port', 'elb_protocol'):
if not listener.get(config):
raise SaltInvocationError(
'{0} is a required value for listeners.'.format(config)
)
if 'elb_port' not in listener:
raise SaltInvocationError('elb_port is a required value for'
' listeners.')
if 'instance_port' not in listener:
raise SaltInvocationError('instance_port is a required value for'
' listeners.')
if 'elb_protocol' not in listener:
raise SaltInvocationError('elb_protocol is a required value for'
' listeners.')
listener['elb_protocol'] = listener['elb_protocol'].upper()
if listener['elb_protocol'] == 'HTTPS' and 'certificate' not in listener:
raise SaltInvocationError('certificate is a required value for'
' listeners if HTTPS is set for'
' elb_protocol.')
# We define all listeners as complex listeners.
if not listener.get('instance_protocol'):
listener['instance_protocol'] = listener['elb_protocol'].upper()
else:
listener['instance_protocol'] = listener['instance_protocol'].upper()
_listener = [listener['elb_port'], listener['instance_port'],
listener['elb_protocol'], listener['instance_protocol']]
if 'certificate' in listener:
_listener.append(listener['certificate'])
_listeners.append(_listener)
# best attempt at principle of least surprise here:
# only use the default pillar in cases where we don't explicitly
# define policies OR policies_from_pillar on a listener
policies = listener.setdefault('policies', [])
policies_pillar = listener.get('policies_from_pillar', None)
if not policies and policies_pillar is None:
policies_pillar = DEFAULT_PILLAR_LISTENER_POLICY_KEY
if policies_pillar:
policies += __salt__['pillar.get'](policies_pillar, {}).get(listener['elb_protocol'], [])
if subnets:
vpc_id = __salt__['boto_vpc.get_subnet_association'](
subnets, region, key, keyid, profile
@ -493,7 +523,7 @@ def _elb_present(
ret['result'] = None
return ret
created = __salt__['boto_elb.create'](name, availability_zones,
_listeners, subnets,
listeners, subnets,
security_groups, scheme, region,
key, keyid, profile)
if created:
@ -514,7 +544,7 @@ def _elb_present(
ret['result'] = _ret['result']
if ret['result'] is False:
return ret
_ret = _listeners_present(name, _listeners, region, key, keyid,
_ret = _listeners_present(name, listeners, region, key, keyid,
profile)
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
@ -557,45 +587,75 @@ def _listeners_present(
return ret
if not listeners:
listeners = []
expected_listeners_by_tuple = {}
for l in listeners:
key = __salt__['boto_elb.listener_dict_to_tuple'](l)
expected_listeners_by_tuple[key] = l
actual_listeners_by_tuple = {}
for l in lb['listeners']:
key = __salt__['boto_elb.listener_dict_to_tuple'](l)
actual_listeners_by_tuple[key] = l
to_delete = []
to_create = []
for listener in listeners:
if listener not in lb['listeners']:
to_create.append(listener)
for listener in lb['listeners']:
if listener not in listeners:
to_delete.append(listener[0])
if to_create or to_delete:
if __opts__['test']:
msg = 'ELB {0} set to have listeners modified.'.format(name)
for t, l in expected_listeners_by_tuple.iteritems():
if t not in actual_listeners_by_tuple:
to_create.append(l)
for t, l in actual_listeners_by_tuple.iteritems():
if t not in expected_listeners_by_tuple:
to_delete.append(l)
if __opts__['test']:
msg = []
if to_create or to_delete:
msg.append('ELB {0} set to have listeners modified:'.format(name))
for listener in to_create:
msg.append('Listener {} added.'.format(
__salt__['boto_elb.listener_dict_to_tuple'](listener)))
for listener in to_delete:
msg.append('Listener {} deleted.'.format(
__salt__['boto_elb.listener_dict_to_tuple'](listener)))
else:
msg.append('Listeners already set on ELB {0}.'.format(name))
ret['comment'] = ' '.join(msg)
ret['result'] = None
return ret
if to_delete:
ports = [l['elb_port'] for l in to_delete]
deleted = __salt__['boto_elb.delete_listeners'](name, ports,
region, key, keyid,
profile)
if deleted:
ret['comment'] = 'Deleted listeners on {0} ELB.'.format(name)
else:
msg = 'Failed to delete listeners on {0} ELB.'.format(name)
ret['comment'] = msg
ret['result'] = None
return ret
if to_delete:
deleted = __salt__['boto_elb.delete_listeners'](name, to_delete,
region, key, keyid,
profile)
if deleted:
ret['comment'] = 'Deleted listeners on {0} ELB.'.format(name)
else:
msg = 'Failed to delete listeners on {0} ELB.'.format(name)
ret['comment'] = msg
ret['result'] = False
if to_create:
created = __salt__['boto_elb.create_listeners'](name, to_create,
region, key, keyid,
profile)
if created:
msg = 'Created listeners on {0} ELB.'
ret['comment'] = ' '.join([ret['comment'], msg.format(name)])
else:
msg = 'Failed to create listeners on {0} ELB.'
ret['comment'] = ' '.join([ret['comment'], msg.format(name)])
ret['result'] = False
ret['changes']['old'] = {'listeners': lb['listeners']}
ret['result'] = False
if to_create:
created = __salt__['boto_elb.create_listeners'](name, to_create,
region, key, keyid,
profile)
if created:
msg = 'Created listeners on {0} ELB.'
ret['comment'] = ' '.join([ret['comment'], msg.format(name)])
else:
msg = 'Failed to create listeners on {0} ELB.'
ret['comment'] = ' '.join([ret['comment'], msg.format(name)])
ret['result'] = False
if to_create or to_delete:
ret['changes']['listeners'] = {}
ret['changes']['listeners']['old'] = lb['listeners']
lb = __salt__['boto_elb.get_elb_config'](name, region, key, keyid,
profile)
ret['changes']['new'] = {'listeners': lb['listeners']}
ret['changes']['listeners']['new'] = lb['listeners']
else:
ret['comment'] = 'Listeners already set on ELB {0}.'.format(name)
return ret
@ -919,6 +979,202 @@ def _alarms_present(name, alarms, alarms_from_pillar, region, key, keyid, profil
return merged_return_value
def _policies_present(
name,
policies,
policies_from_pillar,
listeners,
region,
key,
keyid,
profile):
'''helper method for present. ensure that ELB policies are set'''
if policies is None:
policies = []
pillar_policies = __salt__['config.option'](policies_from_pillar, [])
policies = policies + pillar_policies
# check for policy name uniqueness and correct type
policy_names = set()
for p in policies:
if 'policy_name' not in p:
raise SaltInvocationError('policy_name is a required value for '
'policies.')
if 'policy_type' not in p:
raise SaltInvocationError('policy_type is a required value for '
'policies.')
if 'policy' not in p:
raise SaltInvocationError('policy is a required value for '
'listeners.')
# check for unique policy names
if p['policy_name'] in policy_names:
raise SaltInvocationError('Policy names must be unique: policy {}'
' is declared twice.'.format(p['policy_name']))
policy_names.add(p['policy_name'])
# check that listeners refer to valid policy names
for l in listeners:
for p in l.get('policies', []):
if p not in policy_names:
raise SaltInvocationError('Listener {} on ELB {} refers to '
'undefined policy {}.'.format(l['elb_port'], name, p))
ret = {'result': True, 'comment': '', 'changes': {}}
lb = __salt__['boto_elb.get_elb_config'](name, region, key, keyid, profile)
if not lb:
msg = '{} ELB configuration could not be retrieved.'.format(name)
ret['comment'] = msg
ret['result'] = False
return ret
# Policies have two names:
# - a short name ('name') that's only the policy name (e.g. testpolicy)
# - a canonical name ('cname') that contains the policy type and hash
# (e.g. SSLNegotiationPolicy-testpolicy-14b32f668639cc8ea1391e062af98524)
policies_by_cname = {_policy_cname(p): p for p in policies}
cnames_by_name = {p['policy_name']: _policy_cname(p) for p in policies}
expected_policy_names = policies_by_cname.keys()
actual_policy_names = lb['policies']
# This is sadly a huge hack to get around the fact that AWS assigns a
# default SSLNegotiationPolicyType policy (with the naming scheme
# ELBSecurityPolicy-YYYY-MM) to all ELBs terminating SSL without an
# explicit policy set. If we don't keep track of the default policies and
# explicitly exclude them from deletion, orchestration will fail because we
# attempt to delete the default policy that's being used by listeners that
# were created with no explicit policy.
default_aws_policies = set()
expected_policies_by_listener = {}
for l in listeners:
expected_policies_by_listener[l['elb_port']] = set(
[cnames_by_name[p] for p in l.get('policies', [])])
actual_policies_by_listener = {}
for l in lb['listeners']:
listener_policies = set(l.get('policies', []))
actual_policies_by_listener[l['elb_port']] = listener_policies
# Determine if any actual listener policies look like default policies,
# so we can exclude them from deletion below (see note about this hack
# above).
for p in listener_policies:
if re.match(r'^ELBSecurityPolicy-\d{4}-\d{2}$', p):
default_aws_policies.add(p)
to_delete = []
to_create = []
for policy_name in expected_policy_names:
if policy_name not in actual_policy_names:
to_create.append(policy_name)
for policy_name in actual_policy_names:
if policy_name not in expected_policy_names:
if policy_name not in default_aws_policies:
to_delete.append(policy_name)
listeners_to_update = set()
for port, policies in expected_policies_by_listener.iteritems():
if policies != actual_policies_by_listener.get(port, set()):
listeners_to_update.add(port)
for port, policies in actual_policies_by_listener.iteritems():
if policies != expected_policies_by_listener.get(port, set()):
listeners_to_update.add(port)
if __opts__['test']:
msg = []
if to_create or to_delete:
msg.append('ELB {0} set to have policies modified:'.format(name))
for policy in to_create:
msg.append('Policy {} added.'.format(policy))
for policy in to_delete:
msg.append('Policy {} deleted.'.format(policy))
for listener in listeners_to_update:
msg.append('Listener {} policies updated.'.format(listener))
else:
msg.append('Policies already set on ELB {0}.'.format(name))
ret['comment'] = ' '.join(msg)
ret['result'] = None
return ret
if to_create:
for policy_name in to_create:
created = __salt__['boto_elb.create_policy'](
name=name,
policy_name=policy_name,
policy_type=policies_by_cname[policy_name]['policy_type'],
policy=policies_by_cname[policy_name]['policy'],
region=region,
key=key,
keyid=keyid,
profile=profile)
if created:
ret['changes'].setdefault(policy_name, {})['new'] = policy_name
comment = "Policy {} was created on ELB {}".format(
policy_name, name)
ret['comment'] = ' '.join([ret['comment'], comment])
ret['result'] = True
else:
ret['result'] = False
return ret
for port in listeners_to_update:
policy_set = __salt__['boto_elb.set_listener_policy'](
name=name,
port=port,
policies=list(expected_policies_by_listener.get(port, [])),
region=region,
key=key,
keyid=keyid,
profile=profile)
if policy_set:
policy_key = 'listener_{}_policy'.format(port)
ret['changes'][policy_key] = {
'old': list(actual_policies_by_listener.get(port, [])),
'new': list(expected_policies_by_listener.get(port, [])),
}
comment = "Policy {} was created on ELB {} listener {}".format(
expected_policies_by_listener[port], name, port)
ret['comment'] = ' '.join([ret['comment'], comment])
ret['result'] = True
else:
ret['result'] = False
return ret
if to_delete:
for policy_name in to_delete:
deleted = __salt__['boto_elb.delete_policy'](
name=name,
policy_name=policy_name,
region=region,
key=key,
keyid=keyid,
profile=profile)
if deleted:
ret['changes'].setdefault(policy_name, {})['old'] = policy_name
comment = "Policy {} was deleted from ELB {}".format(
policy_name, name)
ret['comment'] = ' '.join([ret['comment'], comment])
ret['result'] = True
else:
ret['result'] = False
return ret
return ret
def _policy_cname(policy_dict):
policy_name = policy_dict['policy_name']
policy_type = policy_dict['policy_type']
policy = policy_dict['policy']
canonical_policy_repr = str(sorted(list(policy.iteritems()), key=lambda x: str(x[0])))
policy_hash = hashlib.md5(str(canonical_policy_repr)).hexdigest()
if policy_type.endswith('Type'):
policy_type = policy_type[:-4]
return "{}-{}-{}".format(policy_type, policy_name, policy_hash)
def absent(
name,
region=None,