Added Zone class to handle ultradns zones

This commit is contained in:
Kush Bavishi 2019-07-22 11:47:48 -07:00
parent e37a7c775e
commit 0b52aa8c59
2 changed files with 114 additions and 15 deletions

View File

@ -1,6 +1,7 @@
import time import time
import requests import requests
import json import json
from .ultradns_zone import Zone
import dns import dns
import dns.exception import dns.exception
@ -11,10 +12,11 @@ import dns.resolver
from flask import current_app from flask import current_app
from lemur.extensions import metrics, sentry from lemur.extensions import metrics, sentry
use_http = False
def get_ultradns_token(): def get_ultradns_token():
# Function to call the UltraDNS Authorization API. Returns the Authorization access_token
# which is valid for 1 hour. Each request calls this function and we generate a new token
# every time.
path = "/v2/authorization/token" path = "/v2/authorization/token"
data = { data = {
"grant_type": "password", "grant_type": "password",
@ -27,6 +29,8 @@ def get_ultradns_token():
def _generate_header(): def _generate_header():
# Function to generate the header for a request. Contains the Authorization access_key
# obtained from the get_ultradns_token() function.
access_token = get_ultradns_token() access_token = get_ultradns_token()
return {"Authorization": "Bearer {}".format(access_token), "Content-Type": "application/json"} return {"Authorization": "Bearer {}".format(access_token), "Content-Type": "application/json"}
@ -34,8 +38,6 @@ def _generate_header():
def _paginate(path, key): def _paginate(path, key):
limit = 100 limit = 100
params = {"offset": 0, "limit": 1} params = {"offset": 0, "limit": 1}
# params["offset"] = 0
# params["limit"] = 1
resp = _get(path, params) resp = _get(path, params)
for index in range(0, resp["resultInfo"]["totalCount"], limit): for index in range(0, resp["resultInfo"]["totalCount"], limit):
params["offset"] = index params["offset"] = index
@ -45,6 +47,7 @@ def _paginate(path, key):
def _get(path, params=None): def _get(path, params=None):
# Function to execute a GET request on the given URL (base_uri + path) with given params
base_uri = current_app.config.get("ACME_ULTRADNS_DOMAIN", "") base_uri = current_app.config.get("ACME_ULTRADNS_DOMAIN", "")
resp = requests.get( resp = requests.get(
"{0}{1}".format(base_uri, path), "{0}{1}".format(base_uri, path),
@ -57,6 +60,7 @@ def _get(path, params=None):
def _delete(path): def _delete(path):
# Function to execute a DELETE request on the given URL
base_uri = current_app.config.get("ACME_ULTRADNS_DOMAIN", "") base_uri = current_app.config.get("ACME_ULTRADNS_DOMAIN", "")
resp = requests.delete( resp = requests.delete(
"{0}{1}".format(base_uri, path), "{0}{1}".format(base_uri, path),
@ -67,6 +71,7 @@ def _delete(path):
def _post(path, params): def _post(path, params):
# Executes a POST request on given URL. Body is sent in JSON format
base_uri = current_app.config.get("ACME_ULTRADNS_DOMAIN", "") base_uri = current_app.config.get("ACME_ULTRADNS_DOMAIN", "")
resp = requests.post( resp = requests.post(
"{0}{1}".format(base_uri, path), "{0}{1}".format(base_uri, path),
@ -78,6 +83,8 @@ def _post(path, params):
def _has_dns_propagated(name, token): def _has_dns_propagated(name, token):
# Check whether the DNS change made by Lemur have propagated to the public DNS or not.
# Invoked by wait_for_dns_change() function
txt_records = [] txt_records = []
try: try:
dns_resolver = dns.resolver.Resolver() dns_resolver = dns.resolver.Resolver()
@ -99,6 +106,7 @@ def _has_dns_propagated(name, token):
def wait_for_dns_change(change_id, account_number=None): def wait_for_dns_change(change_id, account_number=None):
# Waits and checks if the DNS changes have propagated or not.
fqdn, token = change_id fqdn, token = change_id
number_of_attempts = 20 number_of_attempts = 20
for attempts in range(0, number_of_attempts): for attempts in range(0, number_of_attempts):
@ -122,20 +130,26 @@ def wait_for_dns_change(change_id, account_number=None):
def get_zones(account_number): def get_zones(account_number):
# Get zones from the UltraDNS
path = "/v2/zones/" path = "/v2/zones/"
zones = [] zones = []
for page in _paginate(path, "zones"): for page in _paginate(path, "zones"):
for elem in page: for elem in page:
zones.append(elem["properties"]["name"][:-1]) # UltraDNS zone names end with a "." - Example - lemur.example.com.
# We pick out the names minus the "." at the end while returning the list
zone = Zone(elem)
# TODO : Check for active & Primary
# if elem["properties"]["type"] == "PRIMARY" and elem["properties"]["status"] == "ACTIVE":
if zone.authoritative_type == "PRIMARY" and zone.status == "ACTIVE":
zones.append(zone.name)
return zones return zones
def get_zone_name(domain, account_number): def get_zone_name(domain, account_number):
# Get the matching zone for the given domain
zones = get_zones(account_number) zones = get_zones(account_number)
zone_name = "" zone_name = ""
for z in zones: for z in zones:
if domain.endswith(z): if domain.endswith(z):
# Find the most specific zone possible for the domain # Find the most specific zone possible for the domain
@ -150,12 +164,20 @@ def get_zone_name(domain, account_number):
def create_txt_record(domain, token, account_number): def create_txt_record(domain, token, account_number):
# Create a TXT record for the given domain.
# The part of the domain that matches with the zone becomes the zone name.
# The remainder becomes the owner name (referred to as node name here)
# Example: Let's say we have a zone named "exmaple.com" in UltraDNS and we
# get a request to create a cert for lemur.example.com
# Domain - _acme-challenge.lemur.example.com
# Matching zone - example.com
# Owner name - _acme-challenge.lemur
zone_name = get_zone_name(domain, account_number) zone_name = get_zone_name(domain, account_number)
zone_parts = len(zone_name.split(".")) zone_parts = len(zone_name.split("."))
node_name = ".".join(domain.split(".")[:-zone_parts]) node_name = ".".join(domain.split(".")[:-zone_parts])
fqdn = "{0}.{1}".format(node_name, zone_name) fqdn = "{0}.{1}".format(node_name, zone_name)
path = "/v2/zones/{0}/rrsets/TXT/{1}".format(zone_name, node_name) path = "/v2/zones/{0}/rrsets/TXT/{1}".format(zone_name, node_name)
# zone = Zone(zone_name)
params = { params = {
"ttl": 300, "ttl": 300,
"rdata": [ "rdata": [
@ -180,7 +202,16 @@ def create_txt_record(domain, token, account_number):
def delete_txt_record(change_id, account_number, domain, token): def delete_txt_record(change_id, account_number, domain, token):
# client = get_ultradns_client() # Delete the TXT record that was created in the create_txt_record() function.
# UltraDNS handles records differently compared to Dyn. It creates an RRSet
# which is a set of records of the same type and owner. This means
# that while deleting the record, we cannot delete any individual record from
# the RRSet. Instead, we have to delete the entire RRSet. If multiple certs are
# being created for the same domain at the same time, the challenge TXT records
# that are created will be added under the same RRSet. If the RRSet had more
# than 1 record, then we create a new RRSet on UltraDNS minus the record that
# has to be deleted.
if not domain: if not domain:
current_app.logger.debug("delete_txt_record: No domain passed") current_app.logger.debug("delete_txt_record: No domain passed")
return return
@ -188,27 +219,26 @@ def delete_txt_record(change_id, account_number, domain, token):
zone_name = get_zone_name(domain, account_number) zone_name = get_zone_name(domain, account_number)
zone_parts = len(zone_name.split(".")) zone_parts = len(zone_name.split("."))
node_name = ".".join(domain.split(".")[:-zone_parts]) node_name = ".".join(domain.split(".")[:-zone_parts])
fqdn = "{0}.{1}".format(node_name, zone_name)
path = "/v2/zones/{}/rrsets/16/{}".format(zone_name, node_name) path = "/v2/zones/{}/rrsets/16/{}".format(zone_name, node_name)
try: try:
# rrsets = client.get_rrsets_by_type_owner(zone_name, "TXT", node_name)
rrsets = _get(path) rrsets = _get(path)
except Exception as e: except Exception as e:
metrics.send("delete_txt_record_geterror", "counter", 1) metrics.send("delete_txt_record_geterror", "counter", 1)
# No Text Records remain or host is not in the zone anymore because all records have been deleted. # No Text Records remain or host is not in the zone anymore because all records have been deleted.
return return
try: try:
# Remove the record from the RRSet locally
rrsets["rrSets"][0]["rdata"].remove("{}".format(token)) rrsets["rrSets"][0]["rdata"].remove("{}".format(token))
except ValueError: except ValueError:
current_app.logger.debug("Token not found") current_app.logger.debug("Token not found")
return return
#client.delete_rrset(zone_name, "TXT", node_name) # Delete the RRSet from UltraDNS
_delete(path) _delete(path)
# Check if the RRSet has more records. If yes, add the modified RRSet back to UltraDNS
if len(rrsets["rrSets"][0]["rdata"]) > 0: if len(rrsets["rrSets"][0]["rdata"]) > 0:
#client.create_rrset(zone_name, "TXT", node_name, 300, rrsets["rrSets"][0]["rdata"])
params = { params = {
"ttl": 300, "ttl": 300,
"rdata": rrsets["rrSets"][0]["rdata"], "rdata": rrsets["rrSets"][0]["rdata"],
@ -216,6 +246,42 @@ def delete_txt_record(change_id, account_number, domain, token):
_post(path, params) _post(path, params)
def delete_acme_txt_records(domain):
if not domain:
current_app.logger.debug("delete_acme_txt_records: No domain passed")
return
acme_challenge_string = "_acme-challenge"
if not domain.startswith(acme_challenge_string):
current_app.logger.debug(
"delete_acme_txt_records: Domain {} doesn't start with string {}. "
"Cowardly refusing to delete TXT records".format(
domain, acme_challenge_string
)
)
return
zone_name = get_zone_name(domain)
zone_parts = len(zone_name.split("."))
node_name = ".".join(domain.split(".")[:-zone_parts])
path = "/v2/zones/{}/rrsets/16/{}".format(zone_name, node_name)
_delete(path)
def get_authoritative_nameserver(domain): def get_authoritative_nameserver(domain):
# return "8.8.8.8" """
return "156.154.64.154" REMEMBER TO CHANGE THE RETURN VALUE
REMEMBER TO CHANGE THE RETURN VALUE
REMEMBER TO CHANGE THE RETURN VALUE
REMEMBER TO CHANGE THE RETURN VALUE
REMEMBER TO CHANGE THE RETURN VALUE
REMEMBER TO CHANGE THE RETURN VALUE
REMEMBER TO CHANGE THE RETURN VALUE
REMEMBER TO CHANGE THE RETURN VALUE
REMEMBER TO CHANGE THE RETURN VALUE
REMEMBER TO CHANGE THE RETURN VALUE
REMEMBER TO CHANGE THE RETURN VALUE
"""
return "8.8.8.8"
# return "156.154.64.154"

View File

@ -0,0 +1,33 @@
class Zone:
"""
This class implements an Ultra DNS zone.
"""
def __init__(self, _data, _client="Client"):
self._data = _data
self._client = _client
@property
def name(self):
"""
Zone name, has a trailing "." at the end, which we manually remove.
"""
return self._data["properties"]["name"][:-1]
@property
def authoritative_type(self):
"""
Indicates whether the zone is setup as a PRIMARY or SECONDARY
"""
return self._data["properties"]["type"]
@property
def record_count(self):
return self._data["properties"]["resourceRecordCount"]
@property
def status(self):
"""
Returns the status of the zone - ACTIVE, SUSPENDED, etc
"""
return self._data["properties"]["status"]