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

Commit 97cecf3

Browse files
committed
initial commit
0 parents  commit 97cecf3

File tree

14 files changed

+510
-0
lines changed

14 files changed

+510
-0
lines changed

‎.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.swp
2+
__pycache__
3+
secrets.py
4+
environment
5+

‎Procfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
web: gunicorn app:app --preload --log-file=-
2+

‎README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Automatically Managed DNS
2+
=========================
3+
4+
A system to automate the management of DNS A records for a large number of servers.
5+
6+
Run
7+
---
8+
9+
To run locally: clone, set up a virtual environment with
10+
11+
python -m venv environment
12+
13+
and install dependencies using `environment/bin/pip install -r requirements.txt`.
14+
15+
Make a `secrets.py` file in `/app/dns/`. This should contain the API secrets as the constants `ACCESS_ID`, `ACCESS_SECRET`.
16+
17+
Then run with `./run.py` to serve the web interface on `localhost:5000`. In `dns.create_instances()` some toy data is created.
18+
19+
Currently deployed to https://lit-tundra-97584.herokuapp.com/.
20+

‎app/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from flask import Flask
2+
3+
app = Flask(__name__)
4+
from app import views
5+

‎app/dns/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

‎app/dns/dns.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!../../environment/bin/python3
2+
3+
import route53
4+
import boto
5+
from .secrets import ACCESS_ID, ACCESS_SECRET
6+
7+
class Cluster():
8+
"""A class for cluster objects"""
9+
instances = []
10+
records = []
11+
zone = None
12+
n = 4 # Number of unique clusters
13+
14+
def __init__(self, cluster_id):
15+
self.__class__.instances.append(self)
16+
self.cluster_id = cluster_id
17+
self.cluster_name = self._name()
18+
self.subdomain = self._subdomain()
19+
20+
def _name(self):
21+
name_dict = {1: "Los Angeles",
22+
2: "New York",
23+
3: "Frankfurt",
24+
4: "Hong Kong"}
25+
return name_dict[self.cluster_id]
26+
27+
def _subdomain(self):
28+
subdomain_dict = {"Los Angeles": "la",
29+
"New York": "nyc",
30+
"Frankfurt": "fra",
31+
"Hong Kong": "hk"}
32+
return subdomain_dict[self.cluster_name]
33+
34+
def create_server(self, server_name):
35+
return Server(self.cluster_id, server_name)
36+
37+
class Server(Cluster):
38+
"""Cluster child class"""
39+
instances = []
40+
41+
def __init__(self, cluster_id, friendly_name):
42+
super().__init__(cluster_id)
43+
self.server_id = len(__class__.instances)
44+
self.friendly_name = friendly_name
45+
self.ip_string = "0.0.0.0"
46+
self.dns = "NONE"
47+
48+
def add_to_rotation(self):
49+
"""Adds the server's IP to the cluster's subdomain."""
50+
fqdn = self.subdomain + "." + __class__.zone.name
51+
52+
# If records could simply be added & removed, this would make the
53+
# UI behave better with synchronous post requests.
54+
# Instead we have to rewrite the whole record.
55+
56+
ips = []
57+
for record in __class__.records:
58+
if fqdn == record.name:
59+
ips = record.records[:]
60+
break
61+
62+
ips.append(self.ip_string)
63+
64+
conn = boto.connect_route53(aws_access_key_id = ACCESS_ID,
65+
aws_secret_access_key = ACCESS_SECRET)
66+
67+
changes = boto.route53.record.ResourceRecordSets(conn, __class__.zone.id)
68+
change = changes.add_change("UPSERT", fqdn, "A")
69+
70+
for ip in set(ips):
71+
change.add_value(ip)
72+
73+
return changes.commit()
74+
75+
def remove_from_rotation(self):
76+
"""Removes the server's IP from the DNS record."""
77+
fqdn = self.subdomain + "." + __class__.zone.name
78+
79+
ips = []
80+
for record in __class__.records:
81+
if fqdn == record.name:
82+
ips = record.records[:]
83+
break
84+
85+
if self.ip_string not in ips:
86+
# Return values are not used
87+
return False
88+
89+
ips = list(filter(lambda x: x != self.ip_string, ips))
90+
91+
conn = boto.connect_route53(aws_access_key_id = ACCESS_ID,
92+
aws_secret_access_key = ACCESS_SECRET)
93+
94+
changes = boto.route53.record.ResourceRecordSets(conn, __class__.zone.id)
95+
96+
# This should be simple, but seemingly the API complains unless
97+
# it's updated in this arduous fashion.
98+
99+
if len(ips) > 0:
100+
change = changes.add_change("UPSERT", fqdn, "A")
101+
for ip in set(ips):
102+
change.add_value(ip)
103+
else:
104+
change = changes.add_change("DELETE", fqdn, "A")
105+
change.add_value(self.ip_string)
106+
107+
return changes.commit()
108+
109+
def dns():
110+
"""Grabs all A records for the hosted zone
111+
and assigns them to class variables"""
112+
print("Fetching DNS A records... ", end = "", flush = True)
113+
114+
conn = route53.connect(aws_access_key_id = ACCESS_ID,
115+
aws_secret_access_key = ACCESS_SECRET)
116+
117+
zone = list(conn.list_hosted_zones())[0]
118+
records = [record for record in zone.record_sets]
119+
records = list(filter(lambda x: x.rrset_type == "A", records))
120+
records = list(filter(lambda x: x.name != zone.name, records))
121+
122+
Cluster.zone = zone
123+
Cluster.records = records
124+
125+
print("done")
126+
127+
return records
128+
129+
def create_instances():
130+
"""Instantiates clusters and their servers"""
131+
for i in range(1, Cluster.n + 1):
132+
Cluster(i)
133+
134+
clusters = Cluster.instances
135+
136+
la1 = clusters[0].create_server("la1")
137+
ny1 = clusters[1].create_server("ny1")
138+
fr1 = clusters[2].create_server("fr1")
139+
hk1 = clusters[3].create_server("hk1")
140+
hk2 = clusters[3].create_server("hk2")
141+
142+
la1.ip_string = "2.4.6.8"
143+
ny1.ip_string = "1.1.1.1"
144+
fr1.ip_string = "5.6.7.8"
145+
hk1.ip_string = "4.3.2.1"
146+
hk2.ip_string = "1.2.3.4"
147+
148+
Server.instances.sort(key = lambda x: x.friendly_name)
149+
150+
def update_server_dns(dns_records):
151+
"""Assigns to each server instance their DNS record name"""
152+
for server in Server.instances:
153+
server.dns = "NONE"
154+
for record in dns_records:
155+
if server.ip_string in record.records:
156+
server.dns = record.name
157+
158+
def print_servers():
159+
"""ASCII analogy of the server UI."""
160+
print("\n\033[1mID\t Name\t Cluster\t DNS\t\t\t IP\033[0m")
161+
for server in Server.instances:
162+
print(server.server_id, "\t", server.friendly_name, "\t",
163+
server.cluster_name, "\t", server.dns, "\t", server.ip_string)
164+
165+
def print_dns(dns_records):
166+
"""ASCII analogy of the DNS UI."""
167+
print("\n\033[1mDomain\t\t\t IP(s)\t\t Server(s)\t Cluster\033[0m")
168+
for record in dns_records:
169+
matching_servers = []
170+
for server in Server.instances:
171+
if server.ip_string in record.records:
172+
matching_servers.append(server)
173+
print(record.name, "\t", record.records, "\t",
174+
[server.friendly_name for server in matching_servers], "\t",
175+
[server.cluster_name for server in matching_servers])
176+
177+
178+
if __name__ == "__main__":
179+
create_instances()
180+
dns_records = dns()
181+
update_server_dns(dns_records)
182+
183+
print_servers()
184+
print_dns(dns_records)
185+

‎app/templates/base.html

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<!doctype html>
2+
3+
<html>
4+
<head>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
8+
<!-- Latest compiled and minified CSS -->
9+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
10+
11+
<!-- Optional theme -->
12+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
13+
14+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
15+
16+
<!-- Latest compiled and minified JavaScript -->
17+
<!--script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script-->
18+
19+
<style>
20+
body {
21+
padding-top: 50px;
22+
}
23+
24+
@media (max-width: 768px) {
25+
body {
26+
padding-top: 130px;
27+
}
28+
}
29+
30+
.ip, .friendly_name {
31+
padding-right: 10pt;
32+
}
33+
34+
code {
35+
/*font-family: Inconsolata, Menlo, Monaco, Consolas, monospace;*/
36+
color: #f2f2f2 !important;
37+
background-color: #1d88a2;
38+
}
39+
</style>
40+
41+
<meta name="viewport" content="width=device-width, initial-scale=1">
42+
43+
{% if title -%}
44+
<title>{{ title }}</title>
45+
{%- else -%}
46+
<title>DNS Management</title>
47+
{%- endif %}
48+
49+
</head>
50+
51+
<body>
52+
53+
<nav class="navbar navbar-default navbar-fixed-top">
54+
<div class="container">
55+
<ul class="nav navbar-nav">
56+
<li {% if request.path == '/' %}class="active"{% endif %}><a href="/">Home</a></li>
57+
<li {% if request.path == '/dns' %}class="active"{% endif %}><a href="/dns">DNS</a></li>
58+
<li {% if request.path == '/servers' %}class="active"{% endif %}><a href="/servers">Servers</a></li>
59+
</ul>
60+
</div>
61+
</nav>
62+
63+
{% block content %}{% endblock -%}
64+
65+
</body>
66+
</html>
67+

‎app/templates/dns.html

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{% extends "base.html" %}
2+
{% block content %}
3+
4+
<main class="container" id="dns">
5+
6+
<h1>DNS Entries</h1>
7+
8+
<p>
9+
Showing all A records for subdomains on {{ zone.name[:-1] }} and matching them to our servers, if possible.
10+
</p>
11+
12+
<table class="table table-hover">
13+
<thead>
14+
<tr>
15+
<th>Domain String</th>
16+
<th>IP(s)</th>
17+
<th>Server Friendly Name(s)</th>
18+
<th>Cluster</th>
19+
</tr>
20+
</thead>
21+
<tbody>
22+
{% for record in dns_records -%}
23+
24+
{%- set highlight = {'yes': True} %}
25+
{%- for server in servers -%}
26+
{%- if server.ip_string in record.records -%}
27+
{%- if highlight.update({'yes': False}) -%} {%- endif -%}
28+
{%- endif -%}
29+
{%- endfor -%}
30+
31+
<tr
32+
{%- if highlight['yes'] -%}
33+
{{ "" }} class="danger"
34+
{%- endif -%}
35+
>
36+
<td>{{ record.name }}</td>
37+
<td>
38+
{%- for ip in record.records -%}
39+
<span class="ip"><code>{{ ip }}</code></span>
40+
{%- endfor -%}
41+
</td>
42+
<td>
43+
{%- for server in servers -%}
44+
{%- if server.ip_string in record.records -%}
45+
<span class="friendly_name"><code>{{ server.friendly_name }}</code></span>
46+
{%- endif -%}
47+
{%- endfor -%}
48+
</td>
49+
<td>
50+
{%- set clusterlist = [] -%}
51+
{%- for server in servers -%}
52+
{%- if server.ip_string in record.records -%}
53+
{%- if clusterlist.append(server.cluster_name) -%} {%- endif -%}
54+
{%- endif -%}
55+
{%- endfor -%}
56+
{%- if clusterlist -%}
57+
{{ clusterlist[0] }}
58+
{%- endif -%}
59+
</td>
60+
</tr>
61+
{% endfor %}
62+
</tbody>
63+
</table>
64+
65+
</main>
66+
67+
{% endblock %}
68+

‎app/templates/index.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{% extends "base.html" %}
2+
{% block content %}
3+
<main class="container" id="home">
4+
<h2>
5+
Options
6+
</h2>
7+
8+
<ul>
9+
{% for link in links %}
10+
<li><a href="{{ link.url }}">{{ link.title }}</a></li>
11+
{% endfor %}
12+
</ul>
13+
14+
</main>
15+
{% endblock %}
16+

0 commit comments

Comments
 (0)