Skip to content
This repository was archived by the owner on Jun 3, 2023. It is now read-only.

Commit 7acab4d

Browse files
committed
Fix Cluster and Server object model
* Update documentation * Use ip_address objects * Rename environment variables to be consistent with AWS * Add named-tuple helpers * Add a global dns.CLUSTER_MAP and remove that data from Clusters * Break the Cluster > Server inheritance model * Remove the instances pattern, so objects are unaware of each other * Introduce a Zone class to manage DNS zone task * Add more clusters and servers * Remove redundant records assignments * Rename server.friendly_name -> server.name * Add "Server IP Address" to /servers * In views, import dns objects inside their namespace * Add views.INFRASTRUCTURE and views.ZONE globals * Improve formatting in views.rotate * Remove views.main
1 parent 0a09e12 commit 7acab4d

File tree

5 files changed

+165
-157
lines changed

5 files changed

+165
-157
lines changed

‎README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
# Automatically Managed DNS
1+
# Managed DNS
22

3-
A system to automate the management of DNS A records for an arbitrary number of servers.
3+
A system to ease the management of DNS A records for an arbitrary number of servers.
44

55
## Run
66

77
### Locally
88

99
Clone, set up a virtual environment with `make venv` and enter it with `source venv/bin/activate`.
1010

11-
The application expects AWS API secrets as the environment variables `AWS_ACCESS_ID`, `AWS_ACCESS_SECRET` and a pseudo-random `SECRET_KEY`.
11+
The application expects AWS API secrets as the environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and a pseudo-random `SECRET_KEY` for Flask.
1212

13-
Run with `./run.py` or `flask run` to serve the web interface in development mode on `localhost:5000` with Flask. To serve with `gunicorn` on `localhost:8000`, run the command in `Procfile`. Simulate a deployment with `heroku local`.
13+
Run with `./run.py` or `flask run` to serve the web interface in development mode on `localhost:5000` with Flask. To serve with `gunicorn` on `localhost:8000`, run the command in the `Procfile`.
1414

15-
Some toy data is created by `dns.create_instances`.
15+
Some toy data is created by `app.server_admin.create_infrastructure`.
1616

1717
### Deploy
1818

‎app/server_admin/dns.py

Lines changed: 127 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,95 @@
22
"""DNS logic"""
33

44
import os
5-
from typing import List
5+
import ipaddress
6+
from typing import List, NamedTuple
67

78
import boto
89
import route53
910

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")}
1225

1326
# 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"""
2029

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:
2334
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
2640

2741
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)
2945

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"""
4752

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] = []
5159

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})>"
5862

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:
6092
"""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
6294

6395
# If records could simply be added & removed, this would make the
6496
# UI behave better with synchronous post requests.
@@ -70,38 +102,33 @@ def add_to_rotation(self) -> None:
70102
ips = record.records[:]
71103
break
72104

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)
77106

78-
changes = boto.route53.record.ResourceRecordSets(conn, self.zone.id)
107+
changes = boto.route53.record.ResourceRecordSets(self._conn, self.zone.id)
79108
change = changes.add_change("UPSERT", fqdn, "A")
80109

81110
for ip_address in set(ips):
82111
change.add_value(ip_address)
83112

84113
changes.commit()
114+
server.dns = fqdn
85115

86-
def remove_from_rotation(self) -> None:
116+
def remove_server(self, server: Server) -> None:
87117
"""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
89119

90120
ips: list = []
91121
for record in self.records:
92122
if fqdn == record.name:
93-
ips = record.records[:]
123+
ips = [ipaddress.ip_address(r) for r in record.records[:]]
94124
break
95125

96-
if self.ip_string not in ips:
126+
if server.ip_address not in ips:
97127
return
98128

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))
103130

104-
changes = boto.route53.record.ResourceRecordSets(conn, self.zone.id)
131+
changes = boto.route53.record.ResourceRecordSets(self._conn, self.zone.id)
105132

106133
# This should be simple, but seemingly the API complains unless
107134
# it's updated in this arduous fashion.
@@ -112,86 +139,72 @@ def remove_from_rotation(self) -> None:
112139
change.add_value(ip_address)
113140
else:
114141
change = changes.add_change("DELETE", fqdn, "A")
115-
change.add_value(self.ip_string)
142+
change.add_value(server.ip_address)
116143

117144
changes.commit()
145+
server.dns = None
118146

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:
141148
"""Instantiates clusters and their servers"""
142149
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))
145152

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"))
151162

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)
157167

158-
Server.instances.sort(key=lambda x: x.friendly_name)
168+
servers.sort(key=lambda x: x.name)
159169

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)
167171

168-
def print_servers() -> None:
172+
def print_servers(server_instances: List[Server]) -> None:
169173
"""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)
174179

175-
def print_dns(dns_records: list) -> None:
180+
def print_dns(dns_records: list, server_instances: List[Server]) -> None:
176181
"""ASCII analogy of the DNS UI"""
177182
print("\n\033[1mDomain\t\t\t\t\t IP(s)\t\t Server(s)\t Cluster\033[0m")
178183
for record in dns_records:
179184
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:
182187
matching_servers.append(server)
183188
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
186199

187200
def main() -> None:
188201
"""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)
192205

193-
print_servers()
194-
print_dns(records)
206+
print_servers(infrastructure.servers)
207+
print_dns(zone.records, infrastructure.servers)
195208

196209
if __name__ == "__main__":
197210
main()

‎app/templates/dns.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
<h1>DNS Entries</h1>
77

88
<p>
9-
Showing all A records for subdomains on {{ zone.name[:-1] }} and matching them to our servers, if possible.
9+
Showing all A records on {{ zone_name[:-1] }} and matching them to our servers, if possible.
1010
</p>
1111

1212
<table class="table table-hover">
1313
<thead>
1414
<tr>
1515
<th>Domain String</th>
16-
<th>IP(s)</th>
16+
<th>IP Address Record(s)</th>
1717
<th>Server Friendly Name(s)</th>
1818
<th>Cluster</th>
1919
</tr>
@@ -23,7 +23,7 @@ <h1>DNS Entries</h1>
2323

2424
{%- set highlight = {'yes': True} %}
2525
{%- for server in servers -%}
26-
{%- if server.ip_string in record.records -%}
26+
{%- if server.ip_address.exploded in record.records -%}
2727
{%- if highlight.update({'yes': False}) -%} {%- endif -%}
2828
{%- endif -%}
2929
{%- endfor -%}
@@ -41,15 +41,15 @@ <h1>DNS Entries</h1>
4141
</td>
4242
<td>
4343
{%- for server in servers -%}
44-
{%- if server.ip_string in record.records -%}
45-
<span class="friendly_name"><code>{{ server.friendly_name }}</code></span>
44+
{%- if server.ip_address.exploded in record.records -%}
45+
<span class="friendly_name"><code>{{ server.name }}</code></span>
4646
{%- endif -%}
4747
{%- endfor -%}
4848
</td>
4949
<td>
5050
{%- set clusterlist = [] -%}
5151
{%- for server in servers -%}
52-
{%- if server.ip_string in record.records -%}
52+
{%- if server.ip_address.exploded in record.records -%}
5353
{%- if clusterlist.append(server.cluster_name) -%} {%- endif -%}
5454
{%- endif -%}
5555
{%- endfor -%}

0 commit comments

Comments
 (0)