ec2_group_local 13.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
#!/usr/bin/env python
# -*- coding: utf-8 -*-


DOCUMENTATION = '''
---
module: ec2_group
version_added: "1.3"
short_description: maintain an ec2 VPC security group.
description:
    - maintains ec2 security groups. This module has a dependency on python-boto >= 2.5
options:
  name:
    description:
      - Name of the security group.
    required: true
  description:
    description:
      - Description of the security group.
    required: true
  vpc_id:
    description:
      - ID of the VPC to create the group in.
    required: false
  rules:
    description:
      - List of firewall inbound rules to enforce in this group (see example).
    required: false
  rules_egress:
    description:
      - List of firewall outbound rules to enforce in this group (see example).
    required: false
    version_added: "1.6"
  tags:
    description:
      - List of tags to apply to this security group
    required: false
    version_added: "1.8"
  region:
    description:
      - the EC2 region to use
    required: false
    default: null
    aliases: []
  state:
    version_added: "1.4"
    description:
      - create or delete security group
    required: false
    default: 'present'
    aliases: []

extends_documentation_fragment: aws

notes:
  - If a rule declares a group_name and that group doesn't exist, it will be
    automatically created. In that case, group_desc should be provided as well.
    The module will refuse to create a depended-on group without a description.
'''

EXAMPLES = '''
- name: example ec2 group
  local_action:
    module: ec2_group
    name: example
    description: an example EC2 group
    vpc_id: 12345
    region: eu-west-1a
    aws_secret_key: SECRET
    aws_access_key: ACCESS
    rules:
      - proto: tcp
        from_port: 80
        to_port: 80
        cidr_ip: 0.0.0.0/0
      - proto: tcp
        from_port: 22
        to_port: 22
        cidr_ip: 10.0.0.0/8
      - proto: udp
        from_port: 10050
        to_port: 10050
        cidr_ip: 10.0.0.0/8
      - proto: udp
        from_port: 10051
        to_port: 10051
        group_id: sg-12345678
      - proto: all
        # the containing group name may be specified here
        group_name: example
    rules_egress:
      - proto: tcp
        from_port: 80
        to_port: 80
        group_name: example-other
        # description to use if example-other needs to be created
        group_desc: other example EC2 group
    tags:
      - key: environment
        value: production
'''

try:
    import boto.ec2
except ImportError:
    print "failed=True msg='boto required for this module'"
    sys.exit(1)


def addRulesToLookup(rules, prefix, dict):
    for rule in rules:
        for grant in rule.grants:
            dict["%s-%s-%s-%s-%s-%s" % (prefix, rule.ip_protocol, rule.from_port, rule.to_port,
                                        grant.group_id, grant.cidr_ip)] = rule


def get_target_from_rule(module, rule, name, group, groups):
    """
    Returns tuple of (group_id, ip) after validating rule params.

    rule: Dict describing a rule.
    name: Name of the security group being managed.
    groups: Dict of all available security groups.

    AWS accepts an ip range or a security group as target of a rule. This
    function validate the rule specification and return either a non-None
    group_id or a non-None ip range.
    """

    group_id = None
    group_name = None
    ip = None
    target_group_created = False
    if 'group_id' in rule and 'cidr_ip' in rule:
        module.fail_json(msg="Specify group_id OR cidr_ip, not both")
    elif 'group_name' in rule and 'cidr_ip' in rule:
        module.fail_json(msg="Specify group_name OR cidr_ip, not both")
    elif 'group_id' in rule and 'group_name' in rule:
        module.fail_json(msg="Specify group_id OR group_name, not both")
    elif 'group_id' in rule:
        group_id = rule['group_id']
    elif 'group_name' in rule:
        group_name = rule['group_name']
        if group_name in groups:
            group_id = groups[group_name].id
        elif group_name == name:
            group_id = group.id
            groups[group_id] = group
            groups[group_name] = group
        else:
            if not rule.get('group_desc', '').strip():
                module.fail_json(msg="group %s will be automatically created by rule %s and no description was provided" % (group_name, rule))
            if not module.check_mode:
                auto_group = ec2.create_security_group(group_name, rule['group_desc'], vpc_id=vpc_id)
                group_id = auto_group.id
                groups[group_id] = auto_group
                groups[group_name] = auto_group
            target_group_created = True
    elif 'cidr_ip' in rule:
        ip = rule['cidr_ip']

    return group_id, ip, target_group_created

## can be removed if https://github.com/ansible/ansible/pull/9113 is merged upstream
def is_taggable(object):

    from boto.ec2.ec2object import TaggedEC2Object
    if not object or not issubclass(object.__class__, TaggedEC2Object):
        return False

    return True

def do_tags(module, object, tags):
    """
    General function for adding tags to objects that are subclasses
    of boto.ec2.ec2object.TaggedEC2Object.  Currently updates
    existing tags, as the API overwrites them, but does not remove
    orphans.
    :param module:
    :param object:
    :param tags:
    """
    dry_run = True if module.check_mode else False

    if (is_taggable(object)):

        tag_dict = {}

        for tag in tags:
            tag_dict[tag['key']] = tag['value']

        object.add_tags(tag_dict, dry_run)
    else:
        module.fail_json(msg="Security group object is not a subclass of TaggedEC2Object")
## end can be removed

def main():
    argument_spec = ec2_argument_spec()
    argument_spec.update(dict(
            name=dict(required=True),
            description=dict(required=True),
            vpc_id=dict(),
            rules=dict(),
            rules_egress=dict(),
            tags=dict(type='list', default=[]),
            state = dict(default='present', choices=['present', 'absent']),
        )
    )
    module = AnsibleModule(
        argument_spec=argument_spec,
        supports_check_mode=True,
    )

    name = module.params['name']
    description = module.params['description']
    vpc_id = module.params['vpc_id']
    rules = module.params['rules']
    rules_egress = module.params['rules_egress']
    tags = module.params['tags']
    state = module.params.get('state')

    changed = False

    ec2 = ec2_connect(module)

    # find the group if present
    group = None
    groups = {}
    for curGroup in ec2.get_all_security_groups():
        groups[curGroup.id] = curGroup
        groups[curGroup.name] = curGroup

        if curGroup.name == name and (vpc_id is None or curGroup.vpc_id == vpc_id):
            group = curGroup

    # Ensure requested group is absent
    if state == 'absent':
        if group:
            '''found a match, delete it'''
            try:
                group.delete()
            except Exception, e:
                module.fail_json(msg="Unable to delete security group '%s' - %s" % (group, e))
            else:
                group = None
                changed = True
        else:
            '''no match found, no changes required'''

    # Ensure requested group is present
    elif state == 'present':
        if group:
            '''existing group found'''
            # check the group parameters are correct
            group_in_use = False
            rs = ec2.get_all_instances()
            for r in rs:
                for i in r.instances:
                    group_in_use |= reduce(lambda x, y: x | (y.name == 'public-ssh'), i.groups, False)

            if group.description != description:
                if group_in_use:
                    module.fail_json(msg="Group description does not match, but it is in use so cannot be changed.")

        # if the group doesn't exist, create it now
        else:
            '''no match found, create it'''
            if not module.check_mode:
                group = ec2.create_security_group(name, description, vpc_id=vpc_id)

                # When a group is created, an egress_rule ALLOW ALL
                # to 0.0.0.0/0 is added automatically but it's not
                # reflected in the object returned by the AWS API
                # call. We re-read the group for getting an updated object
                # amazon sometimes takes a couple seconds to update the security group so wait till it exists
                while len(ec2.get_all_security_groups(filters={ 'group_id': group.id, })) == 0:
                    time.sleep(0.1)

                group = ec2.get_all_security_groups(group_ids=(group.id,))[0]
            changed = True

        # tag the security group, function imported from ansible.module_utils.ec2
        do_tags(module, group, tags)
    else:
        module.fail_json(msg="Unsupported state requested: %s" % state)

    # create a lookup for all existing rules on the group
    if group:

        # Manage ingress rules
        groupRules = {}
        addRulesToLookup(group.rules, 'in', groupRules)

        # Now, go through all provided rules and ensure they are there.
        if rules:
            for rule in rules:
                group_id, ip, target_group_created = get_target_from_rule(module, rule, name, group, groups)
                if target_group_created:
                    changed = True

                if rule['proto'] == 'all':
                    rule['proto'] = -1
                    rule['from_port'] = None
                    rule['to_port'] = None

                # If rule already exists, don't later delete it
                ruleId = "%s-%s-%s-%s-%s-%s" % ('in', rule['proto'], rule['from_port'], rule['to_port'], group_id, ip)
                if ruleId in groupRules:
                    del groupRules[ruleId]
                # Otherwise, add new rule
                else:
                    grantGroup = None
                    if group_id:
                        grantGroup = groups[group_id]

                    if not module.check_mode:
                        group.authorize(rule['proto'], rule['from_port'], rule['to_port'], ip, grantGroup)
                    changed = True

        # Finally, remove anything left in the groupRules -- these will be defunct rules
        for rule in groupRules.itervalues():
            for grant in rule.grants:
                grantGroup = None
                if grant.group_id:
                    grantGroup = groups[grant.group_id]
                if not module.check_mode:
                    group.revoke(rule.ip_protocol, rule.from_port, rule.to_port, grant.cidr_ip, grantGroup)
                changed = True

        # Manage egress rules
        groupRules = {}
        addRulesToLookup(group.rules_egress, 'out', groupRules)

        # Now, go through all provided rules and ensure they are there.
        if rules_egress:
            for rule in rules_egress:
                group_id, ip, target_group_created = get_target_from_rule(module, rule, name, group, groups)
                if target_group_created:
                    changed = True

                if rule['proto'] == 'all':
                    rule['proto'] = -1
                    rule['from_port'] = None
                    rule['to_port'] = None

                # If rule already exists, don't later delete it
                ruleId = "%s-%s-%s-%s-%s-%s" % ('out', rule['proto'], rule['from_port'], rule['to_port'], group_id, ip)
                if ruleId in groupRules:
                    del groupRules[ruleId]
                # Otherwise, add new rule
                else:
                    grantGroup = None
                    if group_id:
                        grantGroup = groups[group_id].id

                    if not module.check_mode:
                        ec2.authorize_security_group_egress(
                                group_id=group.id,
                                ip_protocol=rule['proto'],
                                from_port=rule['from_port'],
                                to_port=rule['to_port'],
                                src_group_id=grantGroup,
                                cidr_ip=ip)
                    changed = True
        elif vpc_id and not module.check_mode:
            # when using a vpc, but no egress rules are specified, 
            # we add in a default allow all out rule, which was the
            # default behavior before egress rules were added
            default_egress_rule = 'out--1-None-None-None-0.0.0.0/0'
            if default_egress_rule not in groupRules:
                ec2.authorize_security_group_egress(
                    group_id=group.id,
                    ip_protocol=-1,
                    from_port=None,
                    to_port=None,
                    src_group_id=None,
                    cidr_ip='0.0.0.0/0'
                )
                changed = True
            else:
                # make sure the default egress rule is not removed
                del groupRules[default_egress_rule]

        # Finally, remove anything left in the groupRules -- these will be defunct rules
        for rule in groupRules.itervalues():
            for grant in rule.grants:
                grantGroup = None
                if grant.group_id:
                    grantGroup = groups[grant.group_id].id
                if not module.check_mode:
                    ec2.revoke_security_group_egress(
                            group_id=group.id,
                            ip_protocol=rule.ip_protocol,
                            from_port=rule.from_port,
                            to_port=rule.to_port,
                            src_group_id=grantGroup,
                            cidr_ip=grant.cidr_ip)
                changed = True

    if group:
        module.exit_json(changed=changed, group_id=group.id)
    else:
        module.exit_json(changed=changed, group_id=None)

# import module snippets
from ansible.module_utils.basic import *
from ansible.module_utils.ec2 import *

main()