summaryrefslogtreecommitdiff
path: root/cloud/google/gcdns_record.py
blob: 7c209c5cbad23e63e0ad1bb75f5a3685c3425f29 (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
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 CallFire Inc.
#
# This file is part of Ansible.
#
# This program 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.


################################################################################
# Documentation
################################################################################

ANSIBLE_METADATA = {'status': ['preview'],
                    'supported_by': 'community',
                    'version': '1.0'}

DOCUMENTATION = '''
---
module: gcdns_record
short_description: Creates or removes resource records in Google Cloud DNS
description:
    - Creates or removes resource records in Google Cloud DNS.
version_added: "2.2"
author: "William Albert (@walbert947)"
requirements:
    - "python >= 2.6"
    - "apache-libcloud >= 0.19.0"
options:
    state:
        description:
            - Whether the given resource record should or should not be present.
        required: false
        choices: ["present", "absent"]
        default: "present"
    record:
        description:
            - The fully-qualified domain name of the resource record.
        required: true
        aliases: ['name']
    zone:
        description:
            - The DNS domain name of the zone (e.g., example.com).
            - One of either I(zone) or I(zone_id) must be specified as an
              option, or the module will fail.
            - If both I(zone) and I(zone_id) are specifed, I(zone_id) will be
              used.
        required: false
    zone_id:
        description:
            - The Google Cloud ID of the zone (e.g., example-com).
            - One of either I(zone) or I(zone_id) must be specified as an
              option, or the module will fail.
            - These usually take the form of domain names with the dots replaced
              with dashes. A zone ID will never have any dots in it.
            - I(zone_id) can be faster than I(zone) in projects with a large
              number of zones.
            - If both I(zone) and I(zone_id) are specifed, I(zone_id) will be
              used.
        required: false
    type:
        description:
            - The type of resource record to add.
        required: true
        choices: [ 'A', 'AAAA', 'CNAME', 'SRV', 'TXT', 'SOA', 'NS', 'MX', 'SPF', 'PTR' ]
    record_data:
        description:
            - The record_data to use for the resource record.
            - I(record_data) must be specified if I(state) is C(present) or
              I(overwrite) is C(True), or the module will fail.
            - Valid record_data vary based on the record's I(type). In addition,
              resource records that contain a DNS domain name in the value
              field (e.g., CNAME, PTR, SRV, .etc) MUST include a trailing dot
              in the value.
            - Individual string record_data for TXT records must be enclosed in
              double quotes.
            - For resource records that have the same name but different
              record_data (e.g., multiple A records), they must be defined as
              multiple list entries in a single record.
        required: false
        aliases: ['value']
    ttl:
        description:
            - The amount of time in seconds that a resource record will remain
              cached by a caching resolver.
        required: false
        default: 300
    overwrite:
        description:
            - Whether an attempt to overwrite an existing record should succeed
              or fail. The behavior of this option depends on I(state).
            - If I(state) is C(present) and I(overwrite) is C(True), this
              module will replace an existing resource record of the same name
              with the provided I(record_data). If I(state) is C(present) and
              I(overwrite) is C(False), this module will fail if there is an
              existing resource record with the same name and type, but
              different resource data.
            - If I(state) is C(absent) and I(overwrite) is C(True), this
              module will remove the given resource record unconditionally.
              If I(state) is C(absent) and I(overwrite) is C(False), this
              module will fail if the provided record_data do not match exactly
              with the existing resource record's record_data.
        required: false
        choices: [True, False]
        default: False
    service_account_email:
        description:
            - The e-mail address for a service account with access to Google
              Cloud DNS.
        required: false
        default: null
    pem_file:
        description:
            - The path to the PEM file associated with the service account
              email.
            - This option is deprecated and may be removed in a future release.
              Use I(credentials_file) instead.
        required: false
        default: null
    credentials_file:
        description:
            - The path to the JSON file associated with the service account
              email.
        required: false
        default: null
    project_id:
        description:
            - The Google Cloud Platform project ID to use.
        required: false
        default: null
notes:
    - See also M(gcdns_zone).
    - This modules's underlying library does not support in-place updates for
      DNS resource records. Instead, resource records are quickly deleted and
      recreated. 
    - SOA records are technically supported, but their functionality is limited
      to verifying that a zone's existing SOA record matches a pre-determined
      value. The SOA record cannot be updated.
    - Root NS records cannot be updated.
    - NAPTR records are not supported.
'''

EXAMPLES = '''
# Create an A record.
- gcdns_record:
    record: 'www1.example.com'
    zone: 'example.com'
    type: A
    value: '1.2.3.4'

# Update an existing record.
- gcdns_record:
    record: 'www1.example.com'
    zone: 'example.com'
    type: A
    overwrite: true
    value: '5.6.7.8'

# Remove an A record.
- gcdns_record:
    record: 'www1.example.com'
    zone_id: 'example-com'
    state: absent
    type: A
    value: '5.6.7.8'

# Create a CNAME record.
- gcdns_record:
    record: 'www.example.com'
    zone_id: 'example-com'
    type: CNAME
    value: 'www.example.com.'    # Note the trailing dot

# Create an MX record with a custom TTL.
- gcdns_record:
    record: 'example.com'
    zone: 'example.com'
    type: MX
    ttl: 3600
    value: '10 mail.example.com.'    # Note the trailing dot

# Create multiple A records with the same name.
- gcdns_record:
    record: 'api.example.com'
    zone_id: 'example-com'
    type: A
    record_data:
      - '192.0.2.23'
      - '10.4.5.6'
      - '198.51.100.5'
      - '203.0.113.10'

# Change the value of an existing record with multiple record_data.
- gcdns_record:
    record: 'api.example.com'
    zone: 'example.com'
    type: A
    overwrite: true
    record_data:           # WARNING: All values in a record will be replaced
      - '192.0.2.23'
      - '192.0.2.42'    # The changed record
      - '198.51.100.5'
      - '203.0.113.10'

# Safely remove a multi-line record.
- gcdns_record:
    record: 'api.example.com'
    zone_id: 'example-com'
    state: absent
    type: A
    record_data:           # NOTE: All of the values must match exactly
      - '192.0.2.23'
      - '192.0.2.42'
      - '198.51.100.5'
      - '203.0.113.10'

# Unconditionally remove a record.
- gcdns_record:
    record: 'api.example.com'
    zone_id: 'example-com'
    state: absent
    overwrite: true   # overwrite is true, so no values are needed
    type: A

# Create an AAAA record
- gcdns_record:
    record: 'www1.example.com'
    zone: 'example.com'
    type: AAAA
    value: 'fd00:db8::1'

# Create a PTR record
- gcdns_record:
    record: '10.5.168.192.in-addr.arpa'
    zone: '5.168.192.in-addr.arpa'
    type: PTR
    value: 'api.example.com.'    # Note the trailing dot.

# Create an NS record
- gcdns_record:
    record: 'subdomain.example.com'
    zone: 'example.com'
    type: NS
    ttl: 21600
    record_data:
      - 'ns-cloud-d1.googledomains.com.'    # Note the trailing dots on values
      - 'ns-cloud-d2.googledomains.com.'
      - 'ns-cloud-d3.googledomains.com.'
      - 'ns-cloud-d4.googledomains.com.'

# Create a TXT record
- gcdns_record:
    record: 'example.com'
    zone_id: 'example-com'
    type: TXT
    record_data:
      - '"v=spf1 include:_spf.google.com -all"'   # A single-string TXT value
      - '"hello " "world"'    # A multi-string TXT value
'''

RETURN = '''
overwrite:
    description: Whether to the module was allowed to overwrite the record
    returned: success
    type: boolean
    sample: True
record:
    description: Fully-qualified domain name of the resource record
    returned: success
    type: string
    sample: mail.example.com.
state:
    description: Whether the record is present or absent
    returned: success
    type: string
    sample: present
ttl:
    description: The time-to-live of the resource record
    returned: success
    type: int
    sample: 300
type:
    description: The type of the resource record
    returned: success
    type: string
    sample: A
record_data:
    description: The resource record values
    returned: success
    type: list
    sample: ['5.6.7.8', '9.10.11.12']
zone:
    description: The dns name of the zone
    returned: success
    type: string
    sample: example.com.
zone_id:
    description: The Google Cloud DNS ID of the zone
    returned: success
    type: string
    sample: example-com
'''


################################################################################
# Imports
################################################################################

import socket
from distutils.version import LooseVersion

try:
    from libcloud import __version__ as LIBCLOUD_VERSION
    from libcloud.common.google import InvalidRequestError
    from libcloud.common.types import LibcloudError
    from libcloud.dns.types import Provider
    from libcloud.dns.types import RecordDoesNotExistError
    from libcloud.dns.types import ZoneDoesNotExistError
    HAS_LIBCLOUD = True
except ImportError:
    HAS_LIBCLOUD = False


################################################################################
# Constants
################################################################################

# Apache libcloud 0.19.0 was the first to contain the non-beta Google Cloud DNS
# v1 API. Earlier versions contained the beta v1 API, which has since been
# deprecated and decommissioned.
MINIMUM_LIBCLOUD_VERSION = '0.19.0'

# The libcloud Google Cloud DNS provider.
PROVIDER = Provider.GOOGLE

# The records that libcloud's Google Cloud DNS provider supports.
#
# Libcloud has a RECORD_TYPE_MAP dictionary in the provider that also contains
# this information and is the authoritative source on which records are
# supported, but accessing the dictionary requires creating a Google Cloud DNS
# driver object, which is done in a helper module.
#
# I'm hard-coding the supported record types here, because they (hopefully!)
# shouldn't change much, and it allows me to use it as a "choices" parameter
# in an AnsibleModule argument_spec.
SUPPORTED_RECORD_TYPES = [ 'A', 'AAAA', 'CNAME', 'SRV', 'TXT', 'SOA', 'NS', 'MX', 'SPF', 'PTR' ]


################################################################################
# Functions
################################################################################

def create_record(module, gcdns, zone, record):
    """Creates or overwrites a resource record."""

    overwrite = module.boolean(module.params['overwrite'])
    record_name = module.params['record']
    record_type = module.params['type']
    ttl = module.params['ttl']
    record_data = module.params['record_data']
    data = dict(ttl=ttl, rrdatas=record_data)

    # Google Cloud DNS wants the trailing dot on all DNS names.
    if record_name[-1] != '.':
        record_name = record_name + '.'

    # If we found a record, we need to check if the values match.
    if record is not None:
        # If the record matches, we obviously don't have to change anything.
        if _records_match(record.data['ttl'], record.data['rrdatas'], ttl, record_data):
            return False

        # The record doesn't match, so we need to check if we can overwrite it.
        if not overwrite:
            module.fail_json(
                msg     = 'cannot overwrite existing record, overwrite protection enabled',
                changed = False
            )

    # The record either doesn't exist, or it exists and we can overwrite it.
    if record is None and not module.check_mode:
        # There's no existing record, so we'll just create it.
        try:
            gcdns.create_record(record_name, zone, record_type, data)
        except InvalidRequestError as error:
            if error.code == 'invalid':
                # The resource record name and type are valid by themselves, but
                # not when combined (e.g., an 'A' record with "www.example.com"
                # as its value).
                module.fail_json(
                    msg     = 'value is invalid for the given type: ' +
                              "%s, got value: %s" % (record_type, record_data),
                    changed = False
                )

            elif error.code == 'cnameResourceRecordSetConflict':
                # We're attempting to create a CNAME resource record when we
                # already have another type of resource record with the name
                # domain name.
                module.fail_json(
                    msg     = "non-CNAME resource record already exists: %s" % record_name,
                    changed = False
                )

            else:
                # The error is something else that we don't know how to handle,
                # so we'll just re-raise the exception.
                raise

    elif record is not None and not module.check_mode:
        # The Google provider in libcloud doesn't support updating a record in
        # place, so if the record already exists, we need to delete it and
        # recreate it using the new information.
        gcdns.delete_record(record)

        try:
            gcdns.create_record(record_name, zone, record_type, data)
        except InvalidRequestError:
            # Something blew up when creating the record. This will usually be a
            # result of invalid value data in the new record. Unfortunately, we
            # already changed the state of the record by deleting the old one,
            # so we'll try to roll back before failing out.
            try:
                gcdns.create_record(record.name, record.zone, record.type, record.data)
                module.fail_json(
                    msg     = 'error updating record, the original record was restored',
                    changed = False
                )
            except LibcloudError:
                # We deleted the old record, couldn't create the new record, and
                # couldn't roll back. That really sucks. We'll dump the original
                # record to the failure output so the user can resore it if
                # necessary.
                module.fail_json(
                    msg     = 'error updating record, and could not restore original record, ' +
                              "original name: %s " % record.name +
                              "original zone: %s " % record.zone +
                              "original type: %s " % record.type +
                              "original data: %s" % record.data,
                    changed = True)

    return True


def remove_record(module, gcdns, record):
    """Remove a resource record."""

    overwrite   = module.boolean(module.params['overwrite'])
    ttl         = module.params['ttl']
    record_data = module.params['record_data']

    # If there is no record, we're obviously done.
    if record is None:
        return False

    # If there is an existing record, do our values match the values of the
    # existing record?
    if not overwrite:
        if not _records_match(record.data['ttl'], record.data['rrdatas'], ttl, record_data):
            module.fail_json(
                msg     = 'cannot delete due to non-matching ttl or record_data: ' +
                          "ttl: %d, record_data: %s " % (ttl, record_data) +
                          "original ttl: %d, original record_data: %s" % (record.data['ttl'], record.data['rrdatas']),
                changed = False
            )

    # If we got to this point, we're okay to delete the record.
    if not module.check_mode:
        gcdns.delete_record(record)

    return True


def _get_record(gcdns, zone, record_type, record_name):
    """Gets the record object for a given FQDN."""

    # The record ID is a combination of its type and FQDN. For example, the
    # ID of an A record for www.example.com would be 'A:www.example.com.'
    record_id = "%s:%s" % (record_type, record_name)

    try:
        return gcdns.get_record(zone.id, record_id)
    except RecordDoesNotExistError:
        return None


def _get_zone(gcdns, zone_name, zone_id):
    """Gets the zone object for a given domain name."""

    if zone_id is not None:
        try:
            return gcdns.get_zone(zone_id)
        except ZoneDoesNotExistError:
            return None

    # To create a zone, we need to supply a domain name. However, to delete a
    # zone, we need to supply a zone ID. Zone ID's are often based on domain
    # names, but that's not guaranteed, so we'll iterate through the list of
    # zones to see if we can find a matching domain name.
    available_zones = gcdns.iterate_zones()
    found_zone = None

    for zone in available_zones:
        if zone.domain == zone_name:
            found_zone = zone
            break

    return found_zone


def _records_match(old_ttl, old_record_data, new_ttl, new_record_data):
    """Checks to see if original and new TTL and values match."""

    matches = True

    if old_ttl != new_ttl:
        matches = False
    if old_record_data != new_record_data:
        matches = False

    return matches


def _sanity_check(module):
    """Run sanity checks that don't depend on info from the zone/record."""

    overwrite   = module.params['overwrite']
    record_name = module.params['record']
    record_type = module.params['type']
    state       = module.params['state']
    ttl         = module.params['ttl']
    record_data = module.params['record_data']

    # Apache libcloud needs to be installed and at least the minimum version.
    if not HAS_LIBCLOUD:
        module.fail_json(
            msg = 'This module requires Apache libcloud %s or greater' % MINIMUM_LIBCLOUD_VERSION,
            changed = False
        )
    elif LooseVersion(LIBCLOUD_VERSION) < MINIMUM_LIBCLOUD_VERSION:
        module.fail_json(
            msg = 'This module requires Apache libcloud %s or greater' % MINIMUM_LIBCLOUD_VERSION,
            changed = False
        )

    # A negative TTL is not permitted (how would they even work?!).
    if ttl < 0:
        module.fail_json(
            msg     = 'TTL cannot be less than zero, got: %d' % ttl,
            changed = False
        )

    # Deleting SOA records is not permitted.
    if record_type == 'SOA' and state == 'absent':
        module.fail_json(msg='cannot delete SOA records', changed=False)

    # Updating SOA records is not permitted.
    if record_type == 'SOA' and state == 'present' and overwrite:
        module.fail_json(msg='cannot update SOA records', changed=False)

    # Some sanity checks depend on what value was supplied.
    if record_data is not None and (state == 'present' or not overwrite):
        # A records must contain valid IPv4 addresses.
        if record_type == 'A':
            for value in record_data:
                try:
                    socket.inet_aton(value)
                except socket.error:
                    module.fail_json(
                        msg     = 'invalid A record value, got: %s' % value,
                        changed = False
                    )

        # AAAA records must contain valid IPv6 addresses.
        if record_type == 'AAAA':
            for value in record_data:
                try:
                    socket.inet_pton(socket.AF_INET6, value)
                except socket.error:
                    module.fail_json(
                        msg     = 'invalid AAAA record value, got: %s' % value,
                        changed = False
                    )

        # CNAME and SOA records can't have multiple values.
        if record_type in ['CNAME', 'SOA'] and len(record_data) > 1:
            module.fail_json(
                msg     = 'CNAME or SOA records cannot have more than one value, ' +
                          "got: %s" % record_data,
                changed = False
            )

        # Google Cloud DNS does not support wildcard NS records.
        if record_type == 'NS' and record_name[0] == '*':
            module.fail_json(
                msg     = "wildcard NS records not allowed, got: %s" % record_name,
                changed = False
            )

        # Values for txt records must begin and end with a double quote.
        if record_type == 'TXT':
            for value in record_data:
                if value[0] != '"' and value[-1] != '"':
                    module.fail_json(
                        msg     = 'TXT record_data must be enclosed in double quotes, ' +
                                  'got: %s' % value,
                        changed = False
                    )


def _additional_sanity_checks(module, zone):
    """Run input sanity checks that depend on info from the zone/record."""

    overwrite   = module.params['overwrite']
    record_name = module.params['record']
    record_type = module.params['type']
    state       = module.params['state']

    # CNAME records are not allowed to have the same name as the root domain.
    if record_type == 'CNAME' and record_name == zone.domain:
        module.fail_json(
            msg     = 'CNAME records cannot match the zone name',
            changed = False
        )

    # The root domain must always have an NS record.
    if record_type == 'NS' and record_name == zone.domain and state == 'absent':
        module.fail_json(
            msg     = 'cannot delete root NS records',
            changed = False
        )

    # Updating NS records with the name as the root domain is not allowed
    # because libcloud does not support in-place updates and root domain NS
    # records cannot be removed.
    if record_type == 'NS' and record_name == zone.domain and overwrite:
        module.fail_json(
            msg     = 'cannot update existing root NS records',
            changed = False
        )

    # SOA records with names that don't match the root domain are not permitted
    # (and wouldn't make sense anyway).
    if record_type == 'SOA' and record_name != zone.domain:
        module.fail_json(
            msg     = 'non-root SOA records are not permitted, got: %s' % record_name,
            changed = False
        )


################################################################################
# Main
################################################################################

def main():
    """Main function"""

    module = AnsibleModule(
        argument_spec = dict(
            state                 = dict(default='present', choices=['present', 'absent'], type='str'),
            record                = dict(required=True, aliases=['name'], type='str'),
            zone                  = dict(type='str'),
            zone_id               = dict(type='str'),
            type                  = dict(required=True, choices=SUPPORTED_RECORD_TYPES, type='str'),
            record_data           = dict(aliases=['value'], type='list'),
            ttl                   = dict(default=300, type='int'),
            overwrite             = dict(default=False, type='bool'),
            service_account_email = dict(type='str'),
            pem_file              = dict(type='path'),
            credentials_file      = dict(type='path'),
            project_id            = dict(type='str')
        ),
        required_if = [
            ('state', 'present', ['record_data']),
            ('overwrite', False, ['record_data'])
        ],
        required_one_of     = [['zone', 'zone_id']],
        supports_check_mode = True
    )

    _sanity_check(module)

    record_name = module.params['record']
    record_type = module.params['type']
    state       = module.params['state']
    ttl         = module.params['ttl']
    zone_name   = module.params['zone']
    zone_id     = module.params['zone_id']

    json_output = dict(
        state       = state,
        record      = record_name,
        zone        = zone_name,
        zone_id     = zone_id,
        type        = record_type,
        record_data = module.params['record_data'],
        ttl         = ttl,
        overwrite   = module.boolean(module.params['overwrite'])
    )

    # Google Cloud DNS wants the trailing dot on all DNS names.
    if zone_name is not None and zone_name[-1] != '.':
        zone_name = zone_name + '.'
    if record_name[-1] != '.':
        record_name = record_name + '.'

    # Build a connection object that we can use to connect with Google Cloud
    # DNS.
    gcdns = gcdns_connect(module, provider=PROVIDER)

    # We need to check that the zone we're creating a record for actually
    # exists.
    zone = _get_zone(gcdns, zone_name, zone_id)
    if zone is None and zone_name is not None:
        module.fail_json(
            msg     = 'zone name was not found: %s' % zone_name,
            changed = False
        )
    elif zone is None and zone_id is not None:
        module.fail_json(
            msg     = 'zone id was not found: %s' % zone_id,
            changed = False
        )

    # Populate the returns with the actual zone information.
    json_output['zone'] = zone.domain
    json_output['zone_id'] = zone.id

    # We also need to check if the record we want to create or remove actually
    # exists.
    try:
        record = _get_record(gcdns, zone, record_type, record_name)
    except InvalidRequestError:
        # We gave Google Cloud DNS an invalid DNS record name.
        module.fail_json(
            msg     = 'record name is invalid: %s' % record_name,
            changed = False
        )

    _additional_sanity_checks(module, zone)

    diff = dict()

    # Build the 'before' diff
    if record is None:
        diff['before'] = ''
        diff['before_header'] = '<absent>'
    else:
        diff['before'] = dict(
            record      = record.data['name'],
            type        = record.data['type'],
            record_data = record.data['rrdatas'],
            ttl         = record.data['ttl']
        )
        diff['before_header'] = "%s:%s" % (record_type, record_name)

    # Create, remove, or modify the record.
    if state == 'present':
        diff['after'] = dict(
            record      = record_name,
            type        = record_type,
            record_data = module.params['record_data'],
            ttl         = ttl
        )
        diff['after_header'] = "%s:%s" % (record_type, record_name)

        changed = create_record(module, gcdns, zone, record)

    elif state == 'absent':
        diff['after'] = ''
        diff['after_header'] = '<absent>'

        changed = remove_record(module, gcdns, record)

    module.exit_json(changed=changed, diff=diff, **json_output)


from ansible.module_utils.basic import *
from ansible.module_utils.gcdns import *

if __name__ == '__main__':
    main()