2
2
"""DNS logic"""
3
3
4
4
import os
5
- from typing import List
5
+ import ipaddress
6
+ from typing import List , NamedTuple
6
7
7
8
import boto
8
9
import route53
9
10
10
- AWS_ACCESS_ID = os .environ ["AWS_ACCESS_ID" ]
11
- AWS_ACCESS_SECRET = os .environ ["AWS_ACCESS_SECRET" ]
11
+ AWS_ACCESS_KEY_ID = os .environ ["AWS_ACCESS_KEY_ID" ]
12
+ AWS_SECRET_ACCESS_KEY = os .environ ["AWS_SECRET_ACCESS_KEY" ]
13
+
14
+ NameDomain = NamedTuple ("NameSubdomain" , [("name" , str ), ("subdomain" , str )])
15
+
16
+ Infrastructure = NamedTuple ("Infrastructure" , [("clusters" , List ["Cluster" ]),
17
+ ("servers" , List ["Server" ])])
18
+
19
+ CLUSTER_MAP = {1 : NameDomain ("Los Angeles" , "la" ),
20
+ 2 : NameDomain ("New York" , "nyc" ),
21
+ 3 : NameDomain ("Frankfurt" , "fra" ),
22
+ 4 : NameDomain ("Hong Kong" , "hk" ),
23
+ 5 : NameDomain ("Tokyo" , "tyo" ),
24
+ 6 : NameDomain ("Dublin" , "dub" )}
12
25
13
26
# pylint: disable=too-few-public-methods
14
- class Cluster ():
15
- """A class for cluster objects"""
16
- instances : List ["Cluster" ] = []
17
- records : List ["route53.resource_record_set.AResourceRecordSet" ] = []
18
- zone : "route53.hosted_zone.HostedZone" = None
19
- n = 4 # Number of unique clusters
27
+ class Server :
28
+ """Server dataclass"""
20
29
21
- def __init__ (self , cluster_id : int ) -> None :
22
- self .instances .append (self )
30
+ # pylint: disable=too-many-arguments
31
+ def __init__ (self , server_id : int , name : str ,
32
+ cluster_id : int , cluster_name : str ,
33
+ ip : ipaddress .IPv4Address ) -> None :
23
34
self .cluster_id = cluster_id
24
- self .cluster_name = self ._name ()
25
- self .subdomain = self ._subdomain ()
35
+ self .cluster_name = cluster_name
36
+ self .server_id = server_id
37
+ self .name = name
38
+ self .ip_address = ip
39
+ self .dns = None
26
40
27
41
def __repr__ (self ) -> str :
28
- return f"<{ type (self ).__name__ } (cluster_id={ self .cluster_id } )>"
42
+ return "<{}(server_id={}, name={}, cluster_id={}, cluster_name={}, ip={})>" \
43
+ .format (type (self ).__name__ , self .server_id , self .name ,
44
+ self .cluster_id , self .cluster_name , self .ip_address )
29
45
30
- def _name (self ) -> str :
31
- name_dict = {1 : "Los Angeles" ,
32
- 2 : "New York" ,
33
- 3 : "Frankfurt" ,
34
- 4 : "Hong Kong" }
35
- return name_dict [self .cluster_id ]
36
-
37
- def _subdomain (self ) -> str :
38
- subdomain_dict = {"Los Angeles" : "la" ,
39
- "New York" : "nyc" ,
40
- "Frankfurt" : "fra" ,
41
- "Hong Kong" : "hk" }
42
- return subdomain_dict [self .cluster_name ]
43
-
44
- def create_server (self , server_name : str ) -> "Server" :
45
- """Factory method for server creation"""
46
- return Server (self .cluster_id , server_name )
46
+ def __str__ (self ) -> str :
47
+ return f"<{ self .cluster_name } ({ self .server_id } , { self .name } )"
48
+
49
+ # pylint: disable=too-few-public-methods
50
+ class Cluster :
51
+ """A class for cluster objects"""
47
52
48
- class Server (Cluster ):
49
- """Cluster child class"""
50
- instances : list = []
53
+ def __init__ (self , cluster_id : int , cluster_name : str ,
54
+ subdomain : str ) -> None :
55
+ self .cluster_id = cluster_id
56
+ self .cluster_name = cluster_name
57
+ self .subdomain = subdomain
58
+ self .server_instances : List [Server ] = []
51
59
52
- def __init__ (self , cluster_id : int , friendly_name : str ) -> None :
53
- super ().__init__ (cluster_id )
54
- self .server_id = len (self .instances )
55
- self .friendly_name = friendly_name
56
- self .ip_string = "0.0.0.0"
57
- self .dns = "NONE"
60
+ def __repr__ (self ) -> str :
61
+ return f"<{ type (self ).__name__ } (cluster_id={ self .cluster_id } )>"
58
62
59
- def add_to_rotation (self ) -> None :
63
+ def create_server (self , server_id : int , server_name : str ,
64
+ server_ip : ipaddress .IPv4Address ) -> Server :
65
+ """Factory method for server creation"""
66
+ server = Server (server_id = server_id ,
67
+ name = server_name ,
68
+ cluster_id = self .cluster_id ,
69
+ cluster_name = self .cluster_name ,
70
+ ip = server_ip )
71
+ self .server_instances .append (server )
72
+ return server
73
+
74
+ class Zone :
75
+ """Zone container"""
76
+
77
+ def __init__ (self ) -> None :
78
+ # Gotta consolidate these route53 APIs.
79
+ aws_credentials = {"aws_access_key_id" : AWS_ACCESS_KEY_ID ,
80
+ "aws_secret_access_key" : AWS_SECRET_ACCESS_KEY }
81
+ _r53_conn = route53 .connect (** aws_credentials )
82
+ self ._conn = boto .connect_route53 (** aws_credentials )
83
+ self .zone = list (_r53_conn .list_hosted_zones ())[0 ]
84
+
85
+ @property
86
+ def records (self ) -> list :
87
+ """Flexible record property"""
88
+ return [r for r in self .zone .record_sets \
89
+ if (r .rrset_type == "A" and r .name != self .zone .name )]
90
+
91
+ def add_server (self , server : Server ) -> None :
60
92
"""Adds the server's IP to the cluster's subdomain."""
61
- fqdn = self .subdomain + "." + self .zone .name
93
+ fqdn = CLUSTER_MAP [ server . cluster_id ] .subdomain + "." + self .zone .name
62
94
63
95
# If records could simply be added & removed, this would make the
64
96
# UI behave better with synchronous post requests.
@@ -70,38 +102,33 @@ def add_to_rotation(self) -> None:
70
102
ips = record .records [:]
71
103
break
72
104
73
- ips .append (self .ip_string )
74
-
75
- conn = boto .connect_route53 (aws_access_key_id = AWS_ACCESS_ID ,
76
- aws_secret_access_key = AWS_ACCESS_SECRET )
105
+ ips .append (server .ip_address )
77
106
78
- changes = boto .route53 .record .ResourceRecordSets (conn , self .zone .id )
107
+ changes = boto .route53 .record .ResourceRecordSets (self . _conn , self .zone .id )
79
108
change = changes .add_change ("UPSERT" , fqdn , "A" )
80
109
81
110
for ip_address in set (ips ):
82
111
change .add_value (ip_address )
83
112
84
113
changes .commit ()
114
+ server .dns = fqdn
85
115
86
- def remove_from_rotation (self ) -> None :
116
+ def remove_server (self , server : Server ) -> None :
87
117
"""Removes the server's IP from the DNS record."""
88
- fqdn = self .subdomain + "." + self .zone .name
118
+ fqdn = CLUSTER_MAP [ server . cluster_id ] .subdomain + "." + self .zone .name
89
119
90
120
ips : list = []
91
121
for record in self .records :
92
122
if fqdn == record .name :
93
- ips = record .records [:]
123
+ ips = [ ipaddress . ip_address ( r ) for r in record .records [:] ]
94
124
break
95
125
96
- if self . ip_string not in ips :
126
+ if server . ip_address not in ips :
97
127
return
98
128
99
- ips = list (filter (lambda x : x != self .ip_string , ips ))
100
-
101
- conn = boto .connect_route53 (aws_access_key_id = AWS_ACCESS_ID ,
102
- aws_secret_access_key = AWS_ACCESS_SECRET )
129
+ ips = list (filter (lambda x : x != server .ip_address , ips ))
103
130
104
- changes = boto .route53 .record .ResourceRecordSets (conn , self .zone .id )
131
+ changes = boto .route53 .record .ResourceRecordSets (self . _conn , self .zone .id )
105
132
106
133
# This should be simple, but seemingly the API complains unless
107
134
# it's updated in this arduous fashion.
@@ -112,86 +139,72 @@ def remove_from_rotation(self) -> None:
112
139
change .add_value (ip_address )
113
140
else :
114
141
change = changes .add_change ("DELETE" , fqdn , "A" )
115
- change .add_value (self . ip_string )
142
+ change .add_value (server . ip_address )
116
143
117
144
changes .commit ()
145
+ server .dns = None
118
146
119
- def assign_dns () -> list :
120
- """Grabs all A records for the hosted zone
121
- and assigns them to class variables"""
122
- print ("Fetching DNS A records... " , end = "" , flush = True )
123
-
124
- conn = route53 .connect (aws_access_key_id = AWS_ACCESS_ID ,
125
- aws_secret_access_key = AWS_ACCESS_SECRET )
126
-
127
- zone = list (conn .list_hosted_zones ())[0 ]
128
- records = [record for record in zone .record_sets ]
129
-
130
- records = [r for r in zone .record_sets \
131
- if (r .rrset_type == "A" and r .name != zone .name )]
132
-
133
- Cluster .zone = zone
134
- Cluster .records = records
135
-
136
- print ("done" )
137
-
138
- return records
139
-
140
- def create_instances () -> None :
147
+ def create_infrastructure () -> Infrastructure :
141
148
"""Instantiates clusters and their servers"""
142
149
clusters = []
143
- for i in range ( 1 , Cluster . n + 1 ):
144
- clusters .append (Cluster (i ))
150
+ for cluster_id , ident in CLUSTER_MAP . items ( ):
151
+ clusters .append (Cluster (cluster_id , ident . name , ident . subdomain ))
145
152
146
- la1 = clusters [0 ].create_server ("la1" )
147
- ny1 = clusters [1 ].create_server ("ny1" )
148
- fr1 = clusters [2 ].create_server ("fr1" )
149
- hk1 = clusters [3 ].create_server ("hk1" )
150
- hk2 = clusters [3 ].create_server ("hk2" )
153
+ clusters [0 ].create_server (1 , "la-1" , ipaddress .ip_address ("2.4.6.8" ))
154
+ clusters [1 ].create_server (2 , "nyc-1" , ipaddress .ip_address ("1.0.1.1" ))
155
+ clusters [2 ].create_server (3 , "fra-1" , ipaddress .ip_address ("5.6.7.8" ))
156
+ clusters [3 ].create_server (4 , "hk-1" , ipaddress .ip_address ("4.3.2.1" ))
157
+ clusters [3 ].create_server (5 , "hk-2" , ipaddress .ip_address ("1.2.3.4" ))
158
+ clusters [3 ].create_server (6 , "hk-3" , ipaddress .ip_address ("1.2.3.5" ))
159
+ clusters [3 ].create_server (7 , "hk-4" , ipaddress .ip_address ("1.2.3.6" ))
160
+ clusters [4 ].create_server (8 , "tyo-1" , ipaddress .ip_address ("8.1.1.1" ))
161
+ clusters [5 ].create_server (9 , "dub-1" , ipaddress .ip_address ("9.1.1.1" ))
151
162
152
- la1 .ip_string = "2.4.6.8"
153
- ny1 .ip_string = "1.1.1.1"
154
- fr1 .ip_string = "5.6.7.8"
155
- hk1 .ip_string = "4.3.2.1"
156
- hk2 .ip_string = "1.2.3.4"
163
+ servers = []
164
+ for cluster in clusters :
165
+ for server in cluster .server_instances :
166
+ servers .append (server )
157
167
158
- Server . instances . sort (key = lambda x : x .friendly_name )
168
+ servers . sort (key = lambda x : x .name )
159
169
160
- def update_server_dns (dns_records : list ) -> None :
161
- """Assigns to each server instance their DNS record name"""
162
- for server in Server .instances :
163
- server .dns = "NONE"
164
- for record in dns_records :
165
- if server .ip_string in record .records :
166
- server .dns = record .name
170
+ return Infrastructure (clusters , servers )
167
171
168
- def print_servers () -> None :
172
+ def print_servers (server_instances : List [ Server ] ) -> None :
169
173
"""ASCII analogy of the server UI"""
170
- print ("\n \033 [1mID\t Name\t Cluster\t DNS\t \t \t IP\033 [0m" )
171
- for server in Server .instances :
172
- print (server .server_id , "\t " , server .friendly_name , "\t " ,
173
- server .cluster_name , "\t " , server .dns , "\t " , server .ip_string )
174
+ print ("\n \033 [1mID\t Name\t Cluster\t IP\t \t DNS\033 [0m" )
175
+ for server in server_instances :
176
+ print (server .server_id , "\t " , server .name , "\t " ,
177
+ CLUSTER_MAP [server .cluster_id ].name , "\t " ,
178
+ server .ip_address , server .dns )
174
179
175
- def print_dns (dns_records : list ) -> None :
180
+ def print_dns (dns_records : list , server_instances : List [ Server ] ) -> None :
176
181
"""ASCII analogy of the DNS UI"""
177
182
print ("\n \033 [1mDomain\t \t \t \t \t IP(s)\t \t Server(s)\t Cluster\033 [0m" )
178
183
for record in dns_records :
179
184
matching_servers = []
180
- for server in Server . instances :
181
- if server .ip_string in record .records :
185
+ for server in server_instances :
186
+ if server .ip_address . exploded in record .records :
182
187
matching_servers .append (server )
183
188
print (record .name , "\t " , record .records , "\t " ,
184
- [server .friendly_name for server in matching_servers ], "\t " ,
185
- [server .cluster_name for server in matching_servers ])
189
+ [server .name for server in matching_servers ], "\t " ,
190
+ [CLUSTER_MAP [server .cluster_id ].name for server in matching_servers ])
191
+
192
+ def update_servers (records : list , servers : List [Server ]) -> None :
193
+ """Assigns to each server instance their DNS record name"""
194
+ for server in servers :
195
+ server .dns = None
196
+ for record in records :
197
+ if server .ip_address .exploded in record .records :
198
+ server .dns = record .name
186
199
187
200
def main () -> None :
188
201
"""Entry point"""
189
- create_instances ()
190
- records = assign_dns ()
191
- update_server_dns ( records )
202
+ zone = Zone ()
203
+ infrastructure = create_infrastructure ()
204
+ update_servers ( zone . records , infrastructure . servers )
192
205
193
- print_servers ()
194
- print_dns (records )
206
+ print_servers (infrastructure . servers )
207
+ print_dns (zone . records , infrastructure . servers )
195
208
196
209
if __name__ == "__main__" :
197
210
main ()
0 commit comments