vpc_dns.py 10.5 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
#!/usr/bin/env python -u
#
# Updates DNS records for a stack
#
# Example usage:
#
#   # update route53 entries for ec2 and rds instances
#   # in the vpc with stack-name "stage-stack" and
#   # create DNS entries in the example.com hosted
#   # zone
#
#   python vpc_dns.py -s stage-stack -z example.com
#
#   # same thing but just print what will be done without
#   # making any changes
#
#   python vpc_dns.py -n -s stage-stack -z example.com
#
#   # Create a new zone "vpc.example.com", update the parent
#   # zone "example.com"
#
#   python vpc_dns.py -s stage-stack -z vpc.example.com
#

Feanil Patel committed
25
import argparse
26
import boto
27
import datetime
28
from vpcutil import vpc_for_stack_name
e0d committed
29
import xml.dom.minidom
30
import sys
31

32 33 34 35
# These are ELBs that we do not want to create dns entries
# for because the instances attached to them are also in
# other ELBs and we want the env-deploy-play tuple which makes
# up the dns name to be unique
Feanil Patel committed
36

37
ELB_BAN_LIST = [
38
    'Apros',
39
]
e0d committed
40

41 42 43 44 45 46 47 48 49 50
# If the ELB name has the key in its name these plays
# will be used for the DNS CNAME tuple.  This is used for
# commoncluster.

ELB_PLAY_MAPPINGS = {
    'RabbitMQ': 'rabbitmq',
    'Xqueue': 'xqueue',
    'Elastic': 'elasticsearch',
}

e0d committed
51

e0d committed
52
class DNSRecord():
Feanil Patel committed
53

e0d committed
54
    def __init__(self, zone, record_name, record_type,
55
                 record_ttl, record_values):
e0d committed
56 57 58 59 60 61
        self.zone = zone
        self.record_name = record_name
        self.record_type = record_type
        self.record_ttl = record_ttl
        self.record_values = record_values

62

e0d committed
63
def add_or_update_record(dns_records):
64 65 66 67
    """
    Creates or updates a DNS record in a hosted route53
    zone
    """
e0d committed
68
    change_set = boto.route53.record.ResourceRecordSets()
69
    record_names = set()
70

e0d committed
71
    for record in dns_records:
72

e0d committed
73 74 75 76 77 78 79 80 81
        status_msg = """
        record_name:   {}
        record_type:   {}
        record_ttl:    {}
        record_values: {}
                 """.format(record.record_name, record.record_type,
                            record.record_ttl, record.record_values)
        if args.noop:
            print("Would have updated DNS record:\n{}".format(status_msg))
82 83
        else:
            print("Updating DNS record:\n{}".format(status_msg))
Feanil Patel committed
84

85 86 87 88 89 90
        if record.record_name in record_names:
            print("Unable to create record for {} with value {} because one already exists!".format(
                record.record_values, record.record_name))
            sys.exit(1)
        record_names.add(record.record_name)

e0d committed
91
        zone_id = record.zone.Id.replace("/hostedzone/", "")
Feanil Patel committed
92

e0d committed
93
        records = r53.get_all_rrsets(zone_id)
Feanil Patel committed
94

e0d committed
95
        old_records = {r.name[:-1]: r for r in records}
Feanil Patel committed
96

e0d committed
97
        # If the record name already points to something.
98 99
        # Delete the existing connection. If the record has
        # the same type and name skip it.
e0d committed
100
        if record.record_name in old_records.keys():
101 102
            if record.record_name + "." == old_records[record.record_name].name and \
                    record.record_type == old_records[record.record_name].type:
103 104
                print("Record for {} already exists and is identical, skipping.\n".format(
                    record.record_name))
105 106
                continue

e0d committed
107 108 109 110 111 112 113 114 115 116 117
            if args.force:
                print("Deleting record:\n{}".format(status_msg))
                change = change_set.add_change(
                    'DELETE',
                    record.record_name,
                    record.record_type,
                    record.record_ttl)
            else:
                raise RuntimeError(
                    "DNS record exists for {} and force was not specified.".
                    format(record.record_name))
Feanil Patel committed
118

e0d committed
119 120
            for value in old_records[record.record_name].resource_records:
                change.add_value(value)
Feanil Patel committed
121

e0d committed
122 123 124 125 126
        change = change_set.add_change(
            'CREATE',
            record.record_name,
            record.record_type,
            record.record_ttl)
Feanil Patel committed
127

e0d committed
128 129
        for value in record.record_values:
            change.add_value(value)
Feanil Patel committed
130

e0d committed
131 132 133
    if args.noop:
        print("Would have submitted the following change set:\n")
    else:
134
        print("Submitting the following change set:\n")
135 136 137
    xml_doc = xml.dom.minidom.parseString(change_set.to_xml())
    print(xml_doc.toprettyxml(newl=''))  # newl='' to remove extra newlines
    if not args.noop:
138
        r53.change_rrsets(zone_id, change_set.to_xml())
Feanil Patel committed
139

140

141 142 143 144
def get_or_create_hosted_zone(zone_name):
    """
    Creates the zone and updates the parent
    with the NS information in the zone
145

146 147
    returns: created zone
    """
148

149
    zone = r53.get_hosted_zone_by_name(zone_name)
Feanil Patel committed
150 151
    parent_zone_name = ".".join(zone_name.split('.')[1:])
    parent_zone = r53.get_hosted_zone_by_name(parent_zone_name)
152

153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
    if args.noop:
        if parent_zone:
            print("Would have created/updated zone: {} parent: {}".format(
                zone_name, parent_zone_name))
        else:
            print("Would have created/updated zone: {}".format(
                zone_name, parent_zone_name))
        return zone

    if not zone:
        print("zone {} does not exist, creating".format(zone_name))
        ts = datetime.datetime.utcnow().strftime('%Y-%m-%d-%H:%M:%SUTC')
        zone = r53.create_hosted_zone(
            zone_name, comment="Created by vpc_dns script - {}".format(ts))

    if parent_zone:
        print("Updating parent zone {}".format(parent_zone_name))
e0d committed
170 171

        dns_records = set()
172
        dns_records.add(DNSRecord(parent_zone, zone_name, 'NS', 900, zone.NameServers))
e0d committed
173
        add_or_update_record(dns_records)
174 175 176

    return zone

177

e0d committed
178 179
def get_security_group_dns(group_name):
    # stage-edx-RabbitMQELBSecurityGroup-YB8ZKIZYN1EN
180 181
    environment, deployment, sec_group, salt = group_name.split('-')
    play = sec_group.replace("ELBSecurityGroup", "").lower()
e0d committed
182 183 184
    return environment, deployment, play


185
def get_dns_from_instances(elb):
e0d committed
186
    for inst in elb.instances:
187 188
        try:
            instance = ec2_con.get_all_instances(
e0d committed
189
                instance_ids=[inst.id])[0].instances[0]
190 191 192
        except IndexError:
            print("instance {} attached to elb {}".format(inst, elb))
            sys.exit(1)
e0d committed
193 194
        try:
            env_tag = instance.tags['environment']
195
            deployment_tag = instance.tags['deployment']
e0d committed
196 197 198 199 200 201 202 203
            if 'play' in instance.tags:
                play_tag = instance.tags['play']
            else:
                # deprecated, for backwards compatibility
                play_tag = instance.tags['role']
            break  # only need the first instance for tag info
        except KeyError:
            print("Instance {}, attached to elb {} does not "
204 205
                  "have a tag for environment, play or deployment".format(inst, elb))
            sys.exit(1)
e0d committed
206

207
    return env_tag, deployment_tag, play_tag
e0d committed
208

209 210 211 212 213 214 215 216 217

def update_elb_rds_dns(zone):
    """
    Creates elb and rds CNAME records
    in a zone for args.stack_name.
    Uses the tags of the instances attached
    to the ELBs to create the dns name
    """

e0d committed
218 219
    dns_records = set()

220
    vpc_id = vpc_for_stack_name(args.stack_name, args.aws_id, args.aws_secret)
221 222 223 224 225 226 227 228

    if not zone and args.noop:
        # use a placeholder for zone name
        # if it doesn't exist
        zone_name = "<zone name>"
    else:
        zone_name = zone.Name[:-1]

e0d committed
229 230 231
    stack_elbs = [elb for elb in elb_con.get_all_load_balancers()
                  if elb.vpc_id == vpc_id]
    for elb in stack_elbs:
232
        env_tag, deployment_tag, play_tag = get_dns_from_instances(elb)
e0d committed
233

234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
        # Override the play tag if a substring of the elb name
        # is in ELB_PLAY_MAPPINGS

        for key in ELB_PLAY_MAPPINGS.keys():
            if key in elb.name:
                play_tag = ELB_PLAY_MAPPINGS[key]
                break
        fqdn = "{}-{}-{}.{}".format(env_tag, deployment_tag, play_tag, zone_name)

        # Skip over ELBs if a substring of the ELB name is in
        # the ELB_BAN_LIST

        if any(name in elb.name for name in ELB_BAN_LIST):
            print("Skipping {} because it is on the ELB ban list".format(elb.name))
            continue

        dns_records.add(DNSRecord(zone, fqdn, 'CNAME', 600, [elb.dns_name]))
e0d committed
251

252 253 254 255
    stack_rdss = [rds for rds in rds_con.get_all_dbinstances()
                  if hasattr(rds.subnet_group, 'vpc_id') and
                  rds.subnet_group.vpc_id == vpc_id]

e0d committed
256
    # TODO the current version of the RDS API doesn't support
257 258 259 260
    # looking up RDS instance tags.  Hence, we are using the
    # env_tag and deployment_tag that was set via the loop over instances above.

    rds_endpoints = set()
e0d committed
261
    for rds in stack_rdss:
262 263 264 265 266 267
        endpoint = stack_rdss[0].endpoint[0]
        fqdn = "{}-{}-{}.{}".format(env_tag, deployment_tag, 'rds', zone_name)
        # filter out rds instances with the same endpoints (multi-AZ)
        if endpoint not in rds_endpoints:
            dns_records.add(DNSRecord(zone, fqdn, 'CNAME', 600, [endpoint]))
        rds_endpoints.add(endpoint)
268

e0d committed
269
    add_or_update_record(dns_records)
270

Feanil Patel committed
271
if __name__ == "__main__":
272 273 274 275 276 277 278 279
    description = """

    Give a cloudformation stack name, for an edx stack, setup
    DNS names for the ELBs in the stack

    DNS entries will be created with the following format

       <environment>-<deployment>-<play>.edx.org
Feanil Patel committed
280

281
    """
Feanil Patel committed
282
    parser = argparse.ArgumentParser(description=description)
283 284 285 286 287
    parser.add_argument('-s', '--stack-name', required=True,
                        help="The name of the cloudformation stack.")
    parser.add_argument('-n', '--noop',
                        help="Don't make any changes.", action="store_true",
                        default=False)
288
    parser.add_argument('-z', '--zone-name', default="edx.org",
289 290
                        help="The name of the zone under which to "
                             "create the dns entries.")
e0d committed
291 292
    parser.add_argument('-f', '--force',
                        help="Force reuse of an existing name in a zone",
293 294 295 296 297 298 299
                        action="store_true", default=False)
    parser.add_argument('--aws-id', default=None,
                        help="read only aws key for fetching instance information"
                             "the account you wish add entries for")
    parser.add_argument('--aws-secret', default=None,
                        help="read only aws id for fetching instance information for"
                             "the account you wish add entries for")
300

Feanil Patel committed
301
    args = parser.parse_args()
302 303 304 305 306 307 308 309
    # Connect to ec2 using the provided credentials on the commandline
    ec2_con = boto.connect_ec2(args.aws_id, args.aws_secret)
    elb_con = boto.connect_elb(args.aws_id, args.aws_secret)
    rds_con = boto.connect_rds(args.aws_id, args.aws_secret)

    # Connect to route53 using the user's .boto file
    r53 = boto.connect_route53()

310 311
    zone = get_or_create_hosted_zone(args.zone_name)
    update_elb_rds_dns(zone)