summaryrefslogtreecommitdiff
path: root/library/cloud/ec2_vpc
blob: 0f3c1bef37e3af0dde85f66b80da6042fea8fe41 (plain)
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
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
#!/usr/bin/python
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

DOCUMENTATION = '''
---
module: ec2_vpc 
short_description: configure AWS virtual private clouds
description:
    - Create or terminates AWS virtual private clouds.  This module has a dependency on python-boto.
version_added: "1.4"
options:
  cidr_block:
    description:
      - "The cidr block representing the VPC, e.g. 10.0.0.0/16"
    required: false, unless state=present
  dns_support:
    description:
      - toggles the "Enable DNS resolution" flag
    required: false
    default: "yes"
    choices: [ "yes", "no" ]
  dns_hostnames:
    description:
      - toggles the "Enable DNS hostname support for instances" flag
    required: false
    default: "yes"
    choices: [ "yes", "no" ]
  subnets:
    description:
      - "A dictionary array of subnets to add of the form: { cidr: ..., az: ... }. Where az is the desired availability zone of the subnet, but it is not required. All VPC subnets not in this list will be removed."
    required: false
    default: null
    aliases: []
  vpc_id:
    description:
      - A VPC id to terminate when state=absent
    required: false
    default: null
    aliases: []
  internet_gateway:
    description:
      - Toggle whether there should be an Internet gateway attached to the VPC
    required: false
    default: "no"
    choices: [ "yes", "no" ]
    aliases: []
  route_tables:
    description:
      - "A dictionary array of route tables to add of the form: { subnets: [172.22.2.0/24, 172.22.3.0/24,], routes: [{ dest: 0.0.0.0/0, gw: igw},] }. Where the subnets list is those subnets the route table should be associated with, and the routes list is a list of routes to be in the table.  The special keyword for the gw of igw specifies that you should the route should go through the internet gateway attached to the VPC. gw also accepts instance-ids in addition igw. This module is currently unable to affect the 'main' route table due to some limitations in boto, so you must explicitly define the associated subnets or they will be attached to the main table implicitly."
    required: false
    default: null
    aliases: []
  wait:
    description:
      - wait for the VPC to be in state 'available' before returning
    required: false
    default: "no"
    choices: [ "yes", "no" ]
    aliases: []
  wait_timeout:
    description:
      - how long before wait gives up, in seconds
    default: 300
    aliases: []
  state:
    description:
      - Create or terminate the VPC
    required: true
    default: present
    aliases: []
  region:
    description:
      - region in which the resource exists. 
    required: false
    default: null
    aliases: ['aws_region', 'ec2_region']
  aws_secret_key:
    description:
      - AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used. 
    required: false
    default: None
    aliases: ['ec2_secret_key', 'secret_key' ]
  aws_access_key:
    description:
      - AWS access key. If not set then the value of the AWS_ACCESS_KEY environment variable is used.
    required: false
    default: None
    aliases: ['ec2_access_key', 'access_key' ]
requirements: [ "boto" ]
author: Carson Gee
'''

EXAMPLES = '''
# Note: None of these examples set aws_access_key, aws_secret_key, or region.
# It is assumed that their matching environment variables are set.

# Basic creation example:
      local_action:
        module: ec2_vpc
        state: present
        cidr_block: 172.23.0.0/16
        region: us-west-2
# Full creation example with subnets and optional availability zones.
# The absence or presense of subnets deletes or creates them respectively.
      local_action:
        module: ec2_vpc
        state: present
        cidr_block: 172.22.0.0/16
        subnets: 
          - cidr: 172.22.1.0/24
            az: us-west-2c
          - cidr: 172.22.2.0/24
            az: us-west-2b
          - cidr: 172.22.3.0/24
            az: us-west-2a
        internet_gateway: True
        route_tables:
          - subnets: 
              - 172.22.2.0/24
              - 172.22.3.0/24
            routes: 
              - dest: 0.0.0.0/0
                gw: igw
          - subnets:
              - 172.22.1.0/24
            routes:
              - dest: 0.0.0.0/0
                gw: igw
        region: us-west-2
      register: vpc

# Removal of a VPC by id
      local_action:
        module: ec2_vpc
        state: absent
        vpc_id: vpc-aaaaaaa 
        region: us-west-2  
If you have added elements not managed by this module, e.g. instances, NATs, etc then
the delete will fail until those dependencies are removed.
'''


import sys
import time

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

AWS_REGIONS = ['ap-northeast-1',
               'ap-southeast-1',
               'ap-southeast-2',
               'eu-west-1',
               'sa-east-1',
               'us-east-1',
               'us-west-1',
               'us-west-2']

def get_vpc_info(vpc):
    """
    Retrieves vpc information from an instance
    ID and returns it as a dictionary
    """

    return({
        'id': vpc.id,
        'cidr_block': vpc.cidr_block,
        'dhcp_options_id': vpc.dhcp_options_id,
        'region': vpc.region.name,
        'state': vpc.state,
    })

def create_vpc(module, vpc_conn):
    """
    Creates a new VPC

    module : AnsibleModule object
    vpc_conn: authenticated VPCConnection connection object

    Returns:
        A dictionary with information
        about the VPC and subnets that were launched 
    """
    
    id = module.params.get('id')
    cidr_block = module.params.get('cidr_block')
    dns_support = module.params.get('dns_support')
    dns_hostnames = module.params.get('dns_hostnames')
    subnets = module.params.get('subnets')
    internet_gateway = module.params.get('internet_gateway')
    route_tables = module.params.get('route_tables')
    wait = module.params.get('wait')
    wait_timeout = int(module.params.get('wait_timeout'))
    changed = False

    # Check for existing VPC by cidr_block or id
    if id != None:
        filter_dict = {'vpc-id':id, 'state': 'available',}
        previous_vpcs = vpc_conn.get_all_vpcs(None, filter_dict)
    else:
        filter_dict = {'cidr': cidr_block, 'state': 'available'}
        previous_vpcs = vpc_conn.get_all_vpcs(None, filter_dict)

    if len(previous_vpcs) > 1:
        module.fail_json(msg='EC2 returned more than one VPC, aborting')

    if len(previous_vpcs) == 1:
        changed = False
        vpc = previous_vpcs[0]
    else:
        changed = True
        try:
            vpc = vpc_conn.create_vpc(cidr_block)
            # wait here until the vpc is available
            pending = True
            wait_timeout = time.time() + wait_timeout
            while wait and wait_timeout > time.time() and pending:
                pvpc = vpc_conn.get_all_vpcs(vpc.id)
                if pvpc.state == "available":
                    pending = False
                time.sleep(5)
            if wait and wait_timeout <= time.time():
                # waiting took too long
                module.fail_json(msg = "wait for vpc availability timeout on %s" % time.asctime())

        except boto.exception.BotoServerError, e:
            module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))

    # Done with base VPC, now change to attributes and features.
    

    # boto doesn't appear to have a way to determine the existing
    # value of the dns attributes, so we just set them.
    # It also must be done one at a time.
    vpc_conn.modify_vpc_attribute(vpc.id, enable_dns_support=dns_support)
    vpc_conn.modify_vpc_attribute(vpc.id, enable_dns_hostnames=dns_hostnames)


    # Process all subnet properties
    if not isinstance(subnets, list):
        module.fail_json(msg='subnets needs to be a list of cidr blocks')
    
    current_subnets = vpc_conn.get_all_subnets(filters={ 'vpc_id': vpc.id })
    # First add all new subnets
    for subnet in subnets:
        add_subnet = True
        for csn in current_subnets:
            if subnet['cidr'] == csn.cidr_block:
                add_subnet = False
        if add_subnet:
            try:
                vpc_conn.create_subnet(vpc.id, subnet['cidr'], subnet.get('az', None))
                changed = True
            except EC2ResponseError as e:
                module.fail_json(msg='Unable to create subnet {0}, error: {1}'.format(subnet['cidr'], e))
    # Now delete all absent subnets
    for csubnet in current_subnets:
        delete_subnet = True
        for subnet in subnets:
            if csubnet.cidr_block == subnet['cidr']:
                delete_subnet = False
        if delete_subnet:
            try:
                vpc_conn.delete_subnet(csubnet.id)
                changed = True
            except EC2ResponseError as e:
                module.fail_json(msg='Unable to delete subnet {0}, error: {1}'.format(csubnet.cidr_block, e))

    # Handle Internet gateway (create/delete igw)
    igw = None
    igws = vpc_conn.get_all_internet_gateways(filters={'attachment.vpc-id': vpc.id})
    if len(igws) > 1: 
        module.fail_json(msg='EC2 returned more than one Internet Gateway for id %s, aborting' % vpc.id)
    if internet_gateway:
        if len(igws) != 1:
            try:
                igw = vpc_conn.create_internet_gateway()
                vpc_conn.attach_internet_gateway(igw.id, vpc.id)
                changed = True
            except EC2ResponseError as e:
                module.fail_json(msg='Unable to create Internet Gateway, error: {0}'.format(e))
        else:
            # Set igw variable to the current igw instance for use in route tables.
            igw = igws[0]
    else:
        if len(igws) > 0:
            try:
                vpc_conn.detach_internet_gateway(igws[0].id, vpc.id)
                vpc_conn.delete_internet_gateway(igws[0].id)
                changed = True
            except EC2ResponseError as e:
                module.fail_json(msg='Unable to delete Internet Gateway, error: {0}'.format(e))

    # Handle route tables - this may be worth splitting into a
    # different module but should work fine here. The strategy to stay
    # indempotent is to basically build all the route tables as
    # defined, track the route table ids, and then run through the
    # remote list of route tables and delete any that we didn't
    # create.  This shouldn't interupt traffic in theory, but is the
    # only way to really work with route tables over time that I can
    # think of without using painful aws ids.  Hopefully boto will add
    # the replace-route-table API to make this smoother and
    # allow control of the 'main' routing table.
    if not isinstance(route_tables, list):
        module.fail_json(msg='route tables need to be a list of dictionaries')
    
    # Work through each route table and update/create to match dictionary array
    all_route_tables = []
    for rt in route_tables:
        try:
            new_rt = vpc_conn.create_route_table(vpc.id)
            for route in rt['routes']:
                r_gateway = route['gw']
                if r_gateway == 'igw':
                    if not internet_gateway:
                        module.fail_json(
                            msg='You asked for an Internet Gateway ' \
                            '(igw) route, but you have no Internet Gateway'
                        )
                    r_gateway = igw.id
                vpc_conn.create_route(new_rt.id, route['dest'], r_gateway)

            # Associate with subnets
            for sn in rt['subnets']:
                rsn = vpc_conn.get_all_subnets(filters={'cidr': sn})
                if len(rsn) != 1:
                    module.fail_json(
                        msg='The subnet {0} to associate with route_table {1} ' \
                        'does not exist, aborting'.format(sn, rt)
                    )
                rsn = rsn[0]

                # Disassociate then associate since we don't have replace
                old_rt = vpc_conn.get_all_route_tables(
                    filters={'association.subnet_id': rsn.id}
                )
                if len(old_rt) == 1:
                    old_rt = old_rt[0]
                    association_id = None
                    for a in old_rt.associations:
                        if a.subnet_id == rsn.id:
                            association_id = a.id
                    vpc_conn.disassociate_route_table(association_id)

                vpc_conn.associate_route_table(new_rt.id, rsn.id)

            all_route_tables.append(new_rt)
        except EC2ResponseError as e:
            module.fail_json(
                msg='Unable to create and associate route table {0}, error: ' \
                '{1}'.format(rt, e)
            )
        

    # Now that we are good to go on our new route tables, delete the
    # old ones except the 'main' route table as boto can't set the main
    # table yet.
    all_rts = vpc_conn.get_all_route_tables(filters={'vpc-id': vpc.id})
    for rt in all_rts:
        delete_rt = True
        for newrt in all_route_tables:
            if newrt.id == rt.id:
                delete_rt = False
        if delete_rt:
            rta = rt.associations
            is_main = False
            for a in rta:
                if a.main:
                    is_main = True
            try:
                if not is_main:
                    vpc_conn.delete_route_table(rt.id)
            except EC2ResponseError as e:
                module.fail_json(msg='Unable to delete old route table {0}, error: {1}'.format(rt.id, e))

    vpc_dict = get_vpc_info(vpc)
    created_vpc_id = vpc.id
    returned_subnets = []
    current_subnets = vpc_conn.get_all_subnets(filters={ 'vpc_id': vpc.id })
    for sn in current_subnets:
        returned_subnets.append({
            'cidr': sn.cidr_block, 
            'az': sn.availability_zone,
            'id': sn.id,
        })


    return (vpc_dict, created_vpc_id, returned_subnets, changed)

def terminate_vpc(module, vpc_conn, vpc_id=None, cidr=None):
    """
    Terminates a VPC

    module: Ansible module object
    vpc_conn: authenticated VPCConnection connection object
    vpc_id: a vpc id to terminate
    cidr: The cidr block of the VPC - can be used in lieu of an ID

    Returns a dictionary of VPC information
    about the VPC terminated.

    If the VPC to be terminated is available
    "changed" will be set to True.

    """
    vpc_dict = {}
    terminated_vpc_id = ''
    changed = False

    if vpc_id == None and cidr == None:
        module.fail_json(
            msg='You must either specify a vpc id or a cidr '\
            'block to terminate a VPC, aborting'
        )
    if vpc_id is not None:         
        vpc_rs = vpc_conn.get_all_vpcs(vpc_id)
    else:
        vpc_rs = vpc_conn.get_all_vpcs(filters={'cidr': cidr})
    if len(vpc_rs) > 1:
        module.fail_json(
            msg='EC2 returned more than one VPC for id {0} ' \
            'or cidr {1}, aborting'.format(vpc_id,vidr)
        )
    if len(vpc_rs) == 1:
        vpc = vpc_rs[0]
        if vpc.state == 'available':
            terminated_vpc_id=vpc.id
            vpc_dict=get_vpc_info(vpc)
            try:
                subnets = vpc_conn.get_all_subnets(filters={'vpc_id': vpc.id})
                for sn in subnets:
                    vpc_conn.delete_subnet(sn.id)

                igws = vpc_conn.get_all_internet_gateways(
                    filters={'attachment.vpc-id': vpc.id}
                )
                for igw in igws:
                    vpc_conn.detach_internet_gateway(igw.id, vpc.id)
                    vpc_conn.delete_internet_gateway(igw.id)

                rts = vpc_conn.get_all_route_tables(filters={'vpc_id': vpc.id})
                for rt in rts:
                    rta = rt.associations
                    is_main = False
                    for a in rta:
                        if a.main:
                            is_main = True
                    if not is_main:
                        vpc_conn.delete_route_table(rt.id)

                vpc_conn.delete_vpc(vpc.id)
            except EC2ResponseError as e:
                module.fail_json(
                    msg='Unable to delete VPC {0}, error: {1}'.format(vpc.id, e)
                )
            changed = True

    return (changed, vpc_dict, terminated_vpc_id)


def main():
    module = AnsibleModule(
        argument_spec = dict(
            cidr_block = dict(),
            wait = dict(choices=BOOLEANS, default=False),
            wait_timeout = dict(default=300),
            dns_support = dict(choices=BOOLEANS, default=True),
            dns_hostnames = dict(choices=BOOLEANS, default=True),
            subnets = dict(type='list'),
            vpc_id = dict(),
            internet_gateway = dict(choices=BOOLEANS, default=False),
            route_tables = dict(type='list'),
            region = dict(aliases=['aws_region', 'ec2_region'], choices=AWS_REGIONS),
            state = dict(choices=['present', 'absent'], default='present'),
            aws_secret_key = dict(aliases=['ec2_secret_key', 'secret_key'], no_log=True),
            aws_access_key = dict(aliases=['ec2_access_key', 'access_key']),
        )
    )

    state = module.params.get('state')

    ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
   
    # If we have a region specified, connect to its endpoint.
    if region: 
        try:
            vpc_conn = boto.vpc.connect_to_region(
                region, 
                aws_access_key_id=aws_access_key,
                aws_secret_access_key=aws_secret_key
            )
        except boto.exception.NoAuthHandlerFound, e:
            module.fail_json(msg = str(e))
    else:
        module.fail_json(msg="region must be specified")
    
    if module.params.get('state') == 'absent':
        vpc_id = module.params.get('vpc_id')
        cidr = module.params.get('cidr_block')
        if vpc_id == None and cidr == None:
            module.fail_json(
                msg='You must either specify a vpc id or a cidr '\
                'block to terminate a VPC, aborting'
            )
        (changed, vpc_dict, new_vpc_id) = terminate_vpc(module, vpc_conn, vpc_id, cidr)
        subnets_changed = None
    elif module.params.get('state') == 'present':
        # Changed is always set to true when provisioning a new VPC
        (vpc_dict, new_vpc_id, subnets_changed, changed) = create_vpc(module, vpc_conn)

    module.exit_json(changed=changed, vpc_id=new_vpc_id, vpc=vpc_dict, subnets=subnets_changed)

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

main()