mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-12-30 15:05:39 +00:00
Merge branch 'master' of github.com:ngoduykhanh/PowerDNS-Admin
This commit is contained in:
commit
4a3f121b02
@ -201,6 +201,10 @@ def validate_ipaddress(address):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_json(data):
|
||||||
|
return json.dumps(data, sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
|
||||||
class customBoxes:
|
class customBoxes:
|
||||||
boxes = {
|
boxes = {
|
||||||
"reverse": (" ", " "),
|
"reverse": (" ", " "),
|
||||||
|
@ -3,7 +3,6 @@ import traceback
|
|||||||
from flask import current_app
|
from flask import current_app
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
from distutils.version import StrictVersion
|
|
||||||
|
|
||||||
from ..lib import utils
|
from ..lib import utils
|
||||||
from .base import db, domain_apikey
|
from .base import db, domain_apikey
|
||||||
@ -57,11 +56,6 @@ class Domain(db.Model):
|
|||||||
self.PDNS_VERSION = Setting().get('pdns_version')
|
self.PDNS_VERSION = Setting().get('pdns_version')
|
||||||
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
|
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
|
||||||
|
|
||||||
if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'):
|
|
||||||
self.NEW_SCHEMA = True
|
|
||||||
else:
|
|
||||||
self.NEW_SCHEMA = False
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Domain {0}>'.format(self.name)
|
return '<Domain {0}>'.format(self.name)
|
||||||
|
|
||||||
@ -214,9 +208,8 @@ class Domain(db.Model):
|
|||||||
headers = {}
|
headers = {}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||||
|
|
||||||
if self.NEW_SCHEMA:
|
domain_name = domain_name + '.'
|
||||||
domain_name = domain_name + '.'
|
domain_ns = [ns + '.' for ns in domain_ns]
|
||||||
domain_ns = [ns + '.' for ns in domain_ns]
|
|
||||||
|
|
||||||
if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]:
|
if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]:
|
||||||
soa_edit_api = 'DEFAULT'
|
soa_edit_api = 'DEFAULT'
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
import itertools
|
|
||||||
import dns.reversename
|
import dns.reversename
|
||||||
import dns.inet
|
import dns.inet
|
||||||
import dns.name
|
import dns.name
|
||||||
from distutils.version import StrictVersion
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
from .. import utils
|
from .. import utils
|
||||||
from .base import db
|
from .base import db
|
||||||
@ -40,14 +40,9 @@ class Record(object):
|
|||||||
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
|
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
|
||||||
self.PRETTY_IPV6_PTR = Setting().get('pretty_ipv6_ptr')
|
self.PRETTY_IPV6_PTR = Setting().get('pretty_ipv6_ptr')
|
||||||
|
|
||||||
if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'):
|
def get_rrsets(self, domain):
|
||||||
self.NEW_SCHEMA = True
|
|
||||||
else:
|
|
||||||
self.NEW_SCHEMA = False
|
|
||||||
|
|
||||||
def get_record_data(self, domain):
|
|
||||||
"""
|
"""
|
||||||
Query domain's DNS records via API
|
Query domain's rrsets via PDNS API
|
||||||
"""
|
"""
|
||||||
headers = {}
|
headers = {}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||||
@ -62,41 +57,24 @@ class Record(object):
|
|||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
"Cannot fetch domain's record data from remote powerdns api. DETAIL: {0}"
|
"Cannot fetch domain's record data from remote powerdns api. DETAIL: {0}"
|
||||||
.format(e))
|
.format(e))
|
||||||
return False
|
return []
|
||||||
|
|
||||||
if self.NEW_SCHEMA:
|
return jdata['rrsets']
|
||||||
rrsets = jdata['rrsets']
|
|
||||||
for rrset in rrsets:
|
|
||||||
if rrset['records']:
|
|
||||||
r_name = rrset['name'].rstrip('.')
|
|
||||||
if self.PRETTY_IPV6_PTR: # only if activated
|
|
||||||
if rrset['type'] == 'PTR': # only ptr
|
|
||||||
if 'ip6.arpa' in r_name: # only if v6-ptr
|
|
||||||
r_name = dns.reversename.to_address(
|
|
||||||
dns.name.from_text(r_name))
|
|
||||||
|
|
||||||
rrset['name'] = r_name
|
def add(self, domain_name, rrset):
|
||||||
rrset['content'] = rrset['records'][0]['content']
|
|
||||||
rrset['disabled'] = rrset['records'][0]['disabled']
|
|
||||||
|
|
||||||
# Get the record's comment. PDNS support multiple comments
|
|
||||||
# per record. However, we are only interested in the 1st
|
|
||||||
# one, for now.
|
|
||||||
rrset['comment_data'] = {"content": "", "account": ""}
|
|
||||||
if rrset['comments']:
|
|
||||||
rrset['comment_data'] = rrset['comments'][0]
|
|
||||||
return {'records': rrsets}
|
|
||||||
|
|
||||||
return jdata
|
|
||||||
|
|
||||||
def add(self, domain):
|
|
||||||
"""
|
"""
|
||||||
Add a record to domain
|
Add a record to a domain (Used by auto_ptr and DynDNS)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain_name(str): The zone name
|
||||||
|
rrset(dict): The record in PDNS rrset format
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(dict): A dict contains status code and message
|
||||||
"""
|
"""
|
||||||
# validate record first
|
# Validate record first
|
||||||
r = self.get_record_data(domain)
|
rrsets = self.get_rrsets(domain_name)
|
||||||
records = r['records']
|
check = list(filter(lambda check: check['name'] == self.name, rrsets))
|
||||||
check = list(filter(lambda check: check['name'] == self.name, records))
|
|
||||||
if check:
|
if check:
|
||||||
r = check[0]
|
r = check[0]
|
||||||
if r['type'] in ('A', 'AAAA', 'CNAME'):
|
if r['type'] in ('A', 'AAAA', 'CNAME'):
|
||||||
@ -106,315 +84,233 @@ class Record(object):
|
|||||||
'Record already exists with type "A", "AAAA" or "CNAME"'
|
'Record already exists with type "A", "AAAA" or "CNAME"'
|
||||||
}
|
}
|
||||||
|
|
||||||
# continue if the record is ready to be added
|
# Continue if the record is ready to be added
|
||||||
headers = {}
|
headers = {}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||||
|
|
||||||
if self.NEW_SCHEMA:
|
|
||||||
data = {
|
|
||||||
"rrsets": [{
|
|
||||||
"name":
|
|
||||||
self.name.rstrip('.') + '.',
|
|
||||||
"type":
|
|
||||||
self.type,
|
|
||||||
"changetype":
|
|
||||||
"REPLACE",
|
|
||||||
"ttl":
|
|
||||||
self.ttl,
|
|
||||||
"records": [{
|
|
||||||
"content": self.data,
|
|
||||||
"disabled": self.status,
|
|
||||||
}],
|
|
||||||
"comments":
|
|
||||||
[self.comment_data] if self.comment_data else []
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
data = {
|
|
||||||
"rrsets": [{
|
|
||||||
"name":
|
|
||||||
self.name,
|
|
||||||
"type":
|
|
||||||
self.type,
|
|
||||||
"changetype":
|
|
||||||
"REPLACE",
|
|
||||||
"records": [{
|
|
||||||
"content": self.data,
|
|
||||||
"disabled": self.status,
|
|
||||||
"name": self.name,
|
|
||||||
"ttl": self.ttl,
|
|
||||||
"type": self.type
|
|
||||||
}],
|
|
||||||
"comments":
|
|
||||||
[self.comment_data] if self.comment_data else []
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jdata = utils.fetch_json(urljoin(
|
jdata = utils.fetch_json(urljoin(
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||||
'/servers/localhost/zones/{0}'.format(domain)),
|
'/servers/localhost/zones/{0}'.format(domain_name)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(
|
timeout=int(
|
||||||
Setting().get('pdns_api_timeout')),
|
Setting().get('pdns_api_timeout')),
|
||||||
method='PATCH',
|
method='PATCH',
|
||||||
data=data)
|
data=rrset)
|
||||||
current_app.logger.debug(jdata)
|
current_app.logger.debug(jdata)
|
||||||
return {'status': 'ok', 'msg': 'Record was added successfully'}
|
return {'status': 'ok', 'msg': 'Record was added successfully'}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
"Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}".
|
"Cannot add record to domain {}. Error: {}".format(
|
||||||
format(self.name, self.type, self.data, domain, e))
|
domain_name, e))
|
||||||
|
current_app.logger.debug("Submitted record rrset: \n{}".format(
|
||||||
|
utils.pretty_json(rrset)))
|
||||||
return {
|
return {
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'msg':
|
'msg':
|
||||||
'There was something wrong, please contact administrator'
|
'There was something wrong, please contact administrator'
|
||||||
}
|
}
|
||||||
|
|
||||||
def compare(self, domain_name, new_records):
|
def merge_rrsets(self, rrsets):
|
||||||
"""
|
"""
|
||||||
Compare new records with current powerdns record data
|
Merge the rrsets that has same "name" and
|
||||||
Input is a list of hashes (records)
|
"type".
|
||||||
|
Return: a new rrest which has multiple "records"
|
||||||
|
and "comments"
|
||||||
"""
|
"""
|
||||||
# get list of current records we have in powerdns
|
if not rrsets:
|
||||||
current_records = self.get_record_data(domain_name)['records']
|
raise Exception("Empty rrsets to merge")
|
||||||
|
elif len(rrsets) == 1:
|
||||||
|
# It is unique rrest already
|
||||||
|
return rrsets[0]
|
||||||
|
else:
|
||||||
|
# Merge rrsets into one
|
||||||
|
rrest = rrsets[0]
|
||||||
|
for r in rrsets[1:]:
|
||||||
|
rrest['records'] = rrest['records'] + r['records']
|
||||||
|
rrest['comments'] = rrest['comments'] + r['comments']
|
||||||
|
return rrest
|
||||||
|
|
||||||
# convert them to list of list (just has [name, type]) instead of list of hash
|
def build_rrsets(self, domain_name, submitted_records):
|
||||||
# to compare easier
|
|
||||||
list_current_records = [[x['name'], x['type']]
|
|
||||||
for x in current_records]
|
|
||||||
list_new_records = [[x['name'], x['type']] for x in new_records]
|
|
||||||
|
|
||||||
# get list of deleted records
|
|
||||||
# they are the records which exist in list_current_records but not in list_new_records
|
|
||||||
list_deleted_records = [
|
|
||||||
x for x in list_current_records if x not in list_new_records
|
|
||||||
]
|
|
||||||
|
|
||||||
# convert back to list of hash
|
|
||||||
deleted_records = [
|
|
||||||
x for x in current_records
|
|
||||||
if [x['name'], x['type']] in list_deleted_records and (
|
|
||||||
x['type'] in Setting().get_records_allow_to_edit()
|
|
||||||
and x['type'] != 'SOA')
|
|
||||||
]
|
|
||||||
|
|
||||||
# return a tuple
|
|
||||||
return deleted_records, new_records
|
|
||||||
|
|
||||||
def apply(self, domain, post_records):
|
|
||||||
"""
|
"""
|
||||||
Apply record changes to domain
|
Build rrsets from the datatable's records
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain_name(str): The zone name
|
||||||
|
submitted_records(list): List of records submitted from PDA datatable
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
transformed_rrsets(list): List of rrests converted from PDA datatable
|
||||||
"""
|
"""
|
||||||
records = []
|
rrsets = []
|
||||||
for r in post_records:
|
for record in submitted_records:
|
||||||
r_name = domain if r['record_name'] in [
|
# Format the record name
|
||||||
'@', ''
|
#
|
||||||
] else r['record_name'] + '.' + domain
|
# If it is ipv6 reverse zone and PRETTY_IPV6_PTR is enabled,
|
||||||
r_type = r['record_type']
|
# We convert ipv6 address back to reverse record format
|
||||||
if self.PRETTY_IPV6_PTR: # only if activated
|
# before submitting to PDNS API.
|
||||||
if self.NEW_SCHEMA: # only if new schema
|
if self.PRETTY_IPV6_PTR and re.search(r'ip6\.arpa', domain_name):
|
||||||
if r_type == 'PTR': # only ptr
|
if record['record_type'] == 'PTR' and ':' in record[
|
||||||
if ':' in r['record_name']: # dirty ipv6 check
|
'record_name']:
|
||||||
r_name = r['record_name']
|
record_name = dns.reversename.from_address(
|
||||||
|
record['record_name']).to_text()
|
||||||
|
|
||||||
r_data = domain if r_type == 'CNAME' and r['record_data'] in [
|
# Else, it is forward zone, then record name should be
|
||||||
'@', ''
|
# in format "<name>.<domain>.". If it is root
|
||||||
] else r['record_data']
|
# domain name (name == '@' or ''), the name should
|
||||||
|
# be in format "<domain>."
|
||||||
|
else:
|
||||||
|
record_name = "{}.{}.".format(
|
||||||
|
record["record_name"],
|
||||||
|
domain_name) if record["record_name"] not in [
|
||||||
|
'@', ''
|
||||||
|
] else domain_name + '.'
|
||||||
|
|
||||||
record = {
|
# Format the record content, it musts end
|
||||||
"name": r_name,
|
# with a dot character if in following types
|
||||||
"type": r_type,
|
if record["record_type"] in [
|
||||||
"content": r_data,
|
'MX', 'CNAME', 'SRV', 'NS'
|
||||||
|
] and record["record_data"].strip()[-1:] != '.':
|
||||||
|
record["record_data"] += '.'
|
||||||
|
|
||||||
|
record_conntent = {
|
||||||
|
"content": record["record_data"],
|
||||||
"disabled":
|
"disabled":
|
||||||
True if r['record_status'] == 'Disabled' else False,
|
False if record['record_status'] == 'Active' else True
|
||||||
"ttl": int(r['record_ttl']) if r['record_ttl'] else 3600,
|
|
||||||
"comment_data": r['comment_data']
|
|
||||||
}
|
}
|
||||||
records.append(record)
|
|
||||||
|
|
||||||
deleted_records, new_records = self.compare(domain, records)
|
# Format the comment
|
||||||
|
record_comments = [{
|
||||||
|
"content": record["record_comment"],
|
||||||
|
"account": ""
|
||||||
|
}] if record.get("record_comment") else []
|
||||||
|
|
||||||
records = []
|
# Add the formatted record to rrsets list
|
||||||
for r in deleted_records:
|
rrsets.append({
|
||||||
r_name = r['name'].rstrip(
|
"name": record_name,
|
||||||
'.') + '.' if self.NEW_SCHEMA else r['name']
|
"type": record["record_type"],
|
||||||
r_type = r['type']
|
"ttl": int(record["record_ttl"]),
|
||||||
if self.PRETTY_IPV6_PTR: # only if activated
|
"records": [record_conntent],
|
||||||
if self.NEW_SCHEMA: # only if new schema
|
"comments": record_comments
|
||||||
if r_type == 'PTR': # only ptr
|
})
|
||||||
if ':' in r['name']: # dirty ipv6 check
|
|
||||||
r_name = dns.reversename.from_address(
|
|
||||||
r['name']).to_text()
|
|
||||||
|
|
||||||
record = {
|
# Group the records which has the same name and type.
|
||||||
"name": r_name,
|
# The rrest then has multiple records inside.
|
||||||
"type": r_type,
|
transformed_rrsets = []
|
||||||
"changetype": "DELETE",
|
|
||||||
"records": []
|
|
||||||
}
|
|
||||||
records.append(record)
|
|
||||||
|
|
||||||
postdata_for_delete = {"rrsets": records}
|
# Sort the list before using groupby
|
||||||
|
rrsets = sorted(rrsets, key=lambda r: (r['name'], r['type']))
|
||||||
|
groups = groupby(rrsets, key=lambda r: (r['name'], r['type']))
|
||||||
|
for _k, v in groups:
|
||||||
|
group = list(v)
|
||||||
|
transformed_rrsets.append(self.merge_rrsets(group))
|
||||||
|
|
||||||
records = []
|
return transformed_rrsets
|
||||||
for r in new_records:
|
|
||||||
if self.NEW_SCHEMA:
|
|
||||||
r_name = r['name'].rstrip('.') + '.'
|
|
||||||
r_type = r['type']
|
|
||||||
if self.PRETTY_IPV6_PTR: # only if activated
|
|
||||||
if r_type == 'PTR': # only ptr
|
|
||||||
if ':' in r['name']: # dirty ipv6 check
|
|
||||||
r_name = r['name']
|
|
||||||
|
|
||||||
record = {
|
def compare(self, domain_name, submitted_records):
|
||||||
"name":
|
"""
|
||||||
r_name,
|
Compare the submitted records with PDNS's actual data
|
||||||
"type":
|
|
||||||
r_type,
|
|
||||||
"changetype":
|
|
||||||
"REPLACE",
|
|
||||||
"ttl":
|
|
||||||
r['ttl'],
|
|
||||||
"records": [{
|
|
||||||
"content": r['content'],
|
|
||||||
"disabled": r['disabled']
|
|
||||||
}],
|
|
||||||
"comments":
|
|
||||||
r['comment_data']
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
record = {
|
|
||||||
"name":
|
|
||||||
r['name'],
|
|
||||||
"type":
|
|
||||||
r['type'],
|
|
||||||
"changetype":
|
|
||||||
"REPLACE",
|
|
||||||
"records": [{
|
|
||||||
"content": r['content'],
|
|
||||||
"disabled": r['disabled'],
|
|
||||||
"name": r['name'],
|
|
||||||
"ttl": r['ttl'],
|
|
||||||
"type": r['type'],
|
|
||||||
"priority":
|
|
||||||
10, # priority field for pdns 3.4.1. https://doc.powerdns.com/md/authoritative/upgrading/
|
|
||||||
}],
|
|
||||||
"comments":
|
|
||||||
r['comment_data']
|
|
||||||
}
|
|
||||||
|
|
||||||
records.append(record)
|
Args:
|
||||||
|
domain_name(str): The zone name
|
||||||
|
submitted_records(list): List of records submitted from PDA datatable
|
||||||
|
|
||||||
# Adjustment to add multiple records which described in
|
Returns:
|
||||||
# https://github.com/ngoduykhanh/PowerDNS-Admin/issues/5#issuecomment-181637576
|
new_rrsets(list): List of rrests to be added
|
||||||
final_records = []
|
del_rrsets(list): List of rrests to be deleted
|
||||||
records = sorted(records,
|
"""
|
||||||
key=lambda item:
|
# Create submitted rrsets from submitted records
|
||||||
(item["name"], item["type"], item["changetype"]))
|
submitted_rrsets = self.build_rrsets(domain_name, submitted_records)
|
||||||
for key, group in itertools.groupby(
|
|
||||||
records, lambda item:
|
|
||||||
(item["name"], item["type"], item["changetype"])):
|
|
||||||
if self.NEW_SCHEMA:
|
|
||||||
r_name = key[0]
|
|
||||||
r_type = key[1]
|
|
||||||
r_changetype = key[2]
|
|
||||||
|
|
||||||
if self.PRETTY_IPV6_PTR: # only if activated
|
|
||||||
if r_type == 'PTR': # only ptr
|
|
||||||
if ':' in r_name: # dirty ipv6 check
|
|
||||||
r_name = dns.reversename.from_address(
|
|
||||||
r_name).to_text()
|
|
||||||
|
|
||||||
new_record = {
|
|
||||||
"name": r_name,
|
|
||||||
"type": r_type,
|
|
||||||
"changetype": r_changetype,
|
|
||||||
"ttl": None,
|
|
||||||
"records": []
|
|
||||||
}
|
|
||||||
for item in group:
|
|
||||||
temp_content = item['records'][0]['content']
|
|
||||||
temp_disabled = item['records'][0]['disabled']
|
|
||||||
if key[1] in ['MX', 'CNAME', 'SRV', 'NS']:
|
|
||||||
if temp_content.strip()[-1:] != '.':
|
|
||||||
temp_content += '.'
|
|
||||||
|
|
||||||
if new_record['ttl'] is None:
|
|
||||||
new_record['ttl'] = item['ttl']
|
|
||||||
new_record['records'].append({
|
|
||||||
"content": temp_content,
|
|
||||||
"disabled": temp_disabled
|
|
||||||
})
|
|
||||||
new_record['comments'] = item['comments']
|
|
||||||
final_records.append(new_record)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
final_records.append({
|
|
||||||
"name":
|
|
||||||
key[0],
|
|
||||||
"type":
|
|
||||||
key[1],
|
|
||||||
"changetype":
|
|
||||||
key[2],
|
|
||||||
"records": [{
|
|
||||||
"content": item['records'][0]['content'],
|
|
||||||
"disabled": item['records'][0]['disabled'],
|
|
||||||
"name": key[0],
|
|
||||||
"ttl": item['records'][0]['ttl'],
|
|
||||||
"type": key[1],
|
|
||||||
"priority": 10,
|
|
||||||
} for item in group]
|
|
||||||
})
|
|
||||||
|
|
||||||
postdata_for_new = {"rrsets": final_records}
|
|
||||||
current_app.logger.debug(
|
current_app.logger.debug(
|
||||||
"postdata_for_new: {}".format(postdata_for_new))
|
"submitted_rrsets_data: \n{}".format(utils.pretty_json(submitted_rrsets)))
|
||||||
|
|
||||||
|
# Current domain's rrsets in PDNS
|
||||||
|
current_rrsets = self.get_rrsets(domain_name)
|
||||||
|
current_app.logger.debug("current_rrsets_data: \n{}".format(
|
||||||
|
utils.pretty_json(current_rrsets)))
|
||||||
|
|
||||||
|
# Remove comment's 'modified_at' key
|
||||||
|
# PDNS API always return the comments with modified_at
|
||||||
|
# info, we have to remove it to be able to do the dict
|
||||||
|
# comparison between current and submitted rrsets
|
||||||
|
for r in current_rrsets:
|
||||||
|
for comment in r['comments']:
|
||||||
|
del comment['modified_at']
|
||||||
|
|
||||||
|
# List of rrsets to be added
|
||||||
|
new_rrsets = {"rrsets": []}
|
||||||
|
for r in submitted_rrsets:
|
||||||
|
if r not in current_rrsets and r['type'] in Setting(
|
||||||
|
).get_records_allow_to_edit():
|
||||||
|
r['changetype'] = 'REPLACE'
|
||||||
|
new_rrsets["rrsets"].append(r)
|
||||||
|
|
||||||
|
# List of rrsets to be removed
|
||||||
|
del_rrsets = {"rrsets": []}
|
||||||
|
for r in current_rrsets:
|
||||||
|
if r not in submitted_rrsets and r['type'] in Setting(
|
||||||
|
).get_records_allow_to_edit():
|
||||||
|
r['changetype'] = 'DELETE'
|
||||||
|
del_rrsets["rrsets"].append(r)
|
||||||
|
|
||||||
|
current_app.logger.debug("new_rrsets: \n{}".format(utils.pretty_json(new_rrsets)))
|
||||||
|
current_app.logger.debug("del_rrsets: \n{}".format(utils.pretty_json(del_rrsets)))
|
||||||
|
|
||||||
|
return new_rrsets, del_rrsets
|
||||||
|
|
||||||
|
def apply(self, domain_name, submitted_records):
|
||||||
|
"""
|
||||||
|
Apply record changes to a domain. This function
|
||||||
|
will make 2 calls to the PDNS API to DELETE and
|
||||||
|
REPLACE records (rrests)
|
||||||
|
"""
|
||||||
current_app.logger.debug(
|
current_app.logger.debug(
|
||||||
"postdata_for_delete: {}".format(postdata_for_delete))
|
"submitted_records: {}".format(submitted_records))
|
||||||
current_app.logger.info(
|
|
||||||
urljoin(
|
# Get the list of rrsets to be added and deleted
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
new_rrsets, del_rrsets = self.compare(domain_name, submitted_records)
|
||||||
'/servers/localhost/zones/{0}'.format(domain)))
|
|
||||||
|
# Submit the changes to PDNS API
|
||||||
try:
|
try:
|
||||||
headers = {}
|
headers = {}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||||
jdata1 = utils.fetch_json(urljoin(
|
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
|
||||||
'/servers/localhost/zones/{0}'.format(domain)),
|
|
||||||
headers=headers,
|
|
||||||
method='PATCH',
|
|
||||||
data=postdata_for_delete)
|
|
||||||
jdata2 = utils.fetch_json(urljoin(
|
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
|
||||||
'/servers/localhost/zones/{0}'.format(domain)),
|
|
||||||
headers=headers,
|
|
||||||
timeout=int(
|
|
||||||
Setting().get('pdns_api_timeout')),
|
|
||||||
method='PATCH',
|
|
||||||
data=postdata_for_new)
|
|
||||||
|
|
||||||
if 'error' in jdata1.keys():
|
if del_rrsets["rrsets"]:
|
||||||
current_app.logger.error('Cannot apply record changes.')
|
jdata1 = utils.fetch_json(urljoin(
|
||||||
current_app.logger.debug(jdata1['error'])
|
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||||
return {'status': 'error', 'msg': jdata1['error']}
|
'/servers/localhost/zones/{0}'.format(domain_name)),
|
||||||
elif 'error' in jdata2.keys():
|
headers=headers,
|
||||||
current_app.logger.error('Cannot apply record changes.')
|
method='PATCH',
|
||||||
current_app.logger.debug(jdata2['error'])
|
data=del_rrsets)
|
||||||
return {'status': 'error', 'msg': jdata2['error']}
|
if 'error' in jdata1.keys():
|
||||||
else:
|
current_app.logger.error(
|
||||||
self.auto_ptr(domain, new_records, deleted_records)
|
'Cannot apply record changes with deleting rrsets step. PDNS error: {}'
|
||||||
self.update_db_serial(domain)
|
.format(jdata1['error']))
|
||||||
current_app.logger.info('Record was applied successfully.')
|
return {'status': 'error', 'msg': jdata1['error']}
|
||||||
return {
|
|
||||||
'status': 'ok',
|
if new_rrsets["rrsets"]:
|
||||||
'msg': 'Record was applied successfully'
|
jdata2 = utils.fetch_json(
|
||||||
}
|
urljoin(
|
||||||
|
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||||
|
'/servers/localhost/zones/{0}'.format(domain_name)),
|
||||||
|
headers=headers,
|
||||||
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
|
method='PATCH',
|
||||||
|
data=new_rrsets)
|
||||||
|
if 'error' in jdata2.keys():
|
||||||
|
current_app.logger.error(
|
||||||
|
'Cannot apply record changes with adding rrsets step. PDNS error: {}'
|
||||||
|
.format(jdata2['error']))
|
||||||
|
return {'status': 'error', 'msg': jdata2['error']}
|
||||||
|
|
||||||
|
self.auto_ptr(domain_name, new_rrsets, del_rrsets)
|
||||||
|
self.update_db_serial(domain_name)
|
||||||
|
current_app.logger.info('Record was applied successfully.')
|
||||||
|
return {'status': 'ok', 'msg': 'Record was applied successfully', 'data': (new_rrsets, del_rrsets)}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
"Cannot apply record changes to domain {0}. Error: {1}".format(
|
"Cannot apply record changes to domain {0}. Error: {1}".format(
|
||||||
domain, e))
|
domain_name, e))
|
||||||
current_app.logger.debug(traceback.format_exc())
|
current_app.logger.debug(traceback.format_exc())
|
||||||
return {
|
return {
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
@ -422,48 +318,93 @@ class Record(object):
|
|||||||
'There was something wrong, please contact administrator'
|
'There was something wrong, please contact administrator'
|
||||||
}
|
}
|
||||||
|
|
||||||
def auto_ptr(self, domain, new_records, deleted_records):
|
def auto_ptr(self, domain_name, new_rrsets, del_rrsets):
|
||||||
"""
|
"""
|
||||||
Add auto-ptr records
|
Add auto-ptr records
|
||||||
"""
|
"""
|
||||||
domain_obj = Domain.query.filter(Domain.name == domain).first()
|
# Check if auto_ptr is enabled for this domain
|
||||||
domain_auto_ptr = DomainSetting.query.filter(
|
auto_ptr_enabled = False
|
||||||
DomainSetting.domain == domain_obj).filter(
|
if Setting().get('auto_ptr'):
|
||||||
DomainSetting.setting == 'auto_ptr').first()
|
auto_ptr_enabled = True
|
||||||
domain_auto_ptr = strtobool(
|
else:
|
||||||
domain_auto_ptr.value) if domain_auto_ptr else False
|
domain_obj = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
|
domain_setting = DomainSetting.query.filter(
|
||||||
|
DomainSetting.domain == domain_obj).filter(
|
||||||
|
DomainSetting.setting == 'auto_ptr').first()
|
||||||
|
auto_ptr_enabled = strtobool(
|
||||||
|
domain_setting.value) if domain_setting else False
|
||||||
|
|
||||||
system_auto_ptr = Setting().get('auto_ptr')
|
# If it is enabled, we create/delete the PTR records automatically
|
||||||
|
if auto_ptr_enabled:
|
||||||
if system_auto_ptr or domain_auto_ptr:
|
|
||||||
try:
|
try:
|
||||||
|
RECORD_TYPE_TO_PTR = ['A', 'AAAA']
|
||||||
|
new_rrsets = new_rrsets['rrsets']
|
||||||
|
del_rrsets = del_rrsets['rrsets']
|
||||||
|
|
||||||
|
if not new_rrsets and not del_rrsets:
|
||||||
|
msg = 'No changes detected. Skipping auto ptr...'
|
||||||
|
current_app.logger.info(msg)
|
||||||
|
return {'status': 'ok', 'msg': msg}
|
||||||
|
|
||||||
|
new_rrsets = [
|
||||||
|
r for r in new_rrsets if r['type'] in RECORD_TYPE_TO_PTR
|
||||||
|
]
|
||||||
|
del_rrsets = [
|
||||||
|
r for r in del_rrsets if r['type'] in RECORD_TYPE_TO_PTR
|
||||||
|
]
|
||||||
|
|
||||||
d = Domain()
|
d = Domain()
|
||||||
for r in new_records:
|
for r in new_rrsets:
|
||||||
if r['type'] in ['A', 'AAAA']:
|
for record in r['records']:
|
||||||
r_name = r['name'] + '.'
|
# Format the reverse record name
|
||||||
r_content = r['content']
|
# It is the reverse of forward record's content.
|
||||||
reverse_host_address = dns.reversename.from_address(
|
reverse_host_address = dns.reversename.from_address(
|
||||||
r_content).to_text()
|
record['content']).to_text()
|
||||||
|
|
||||||
|
# Create the reverse domain name in PDNS
|
||||||
domain_reverse_name = d.get_reverse_domain_name(
|
domain_reverse_name = d.get_reverse_domain_name(
|
||||||
reverse_host_address)
|
reverse_host_address)
|
||||||
d.create_reverse_domain(domain, domain_reverse_name)
|
d.create_reverse_domain(domain_name,
|
||||||
self.name = dns.reversename.from_address(
|
domain_reverse_name)
|
||||||
r_content).to_text().rstrip('.')
|
|
||||||
self.type = 'PTR'
|
# Build the rrset for reverse zone updating
|
||||||
self.status = r['disabled']
|
rrset_data = [{
|
||||||
self.ttl = r['ttl']
|
"changetype":
|
||||||
self.data = r_name
|
"REPLACE",
|
||||||
self.add(domain_reverse_name)
|
"name":
|
||||||
for r in deleted_records:
|
reverse_host_address,
|
||||||
if r['type'] in ['A', 'AAAA']:
|
"ttl":
|
||||||
r_content = r['content']
|
r['ttl'],
|
||||||
|
"type":
|
||||||
|
"PTR",
|
||||||
|
"records": [{
|
||||||
|
"content": r['name'],
|
||||||
|
"disabled": record['disabled']
|
||||||
|
}],
|
||||||
|
"comments": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
# Format the rrset
|
||||||
|
rrset = {"rrsets": rrset_data}
|
||||||
|
self.add(domain_reverse_name, rrset)
|
||||||
|
|
||||||
|
for r in del_rrsets:
|
||||||
|
for record in r['records']:
|
||||||
|
# Format the reverse record name
|
||||||
|
# It is the reverse of forward record's content.
|
||||||
reverse_host_address = dns.reversename.from_address(
|
reverse_host_address = dns.reversename.from_address(
|
||||||
r_content).to_text()
|
record['content']).to_text()
|
||||||
|
|
||||||
|
# Create the reverse domain name in PDNS
|
||||||
domain_reverse_name = d.get_reverse_domain_name(
|
domain_reverse_name = d.get_reverse_domain_name(
|
||||||
reverse_host_address)
|
reverse_host_address)
|
||||||
|
d.create_reverse_domain(domain_name,
|
||||||
|
domain_reverse_name)
|
||||||
|
|
||||||
|
# Delete the reverse zone
|
||||||
self.name = reverse_host_address
|
self.name = reverse_host_address
|
||||||
self.type = 'PTR'
|
self.type = 'PTR'
|
||||||
self.data = r_content
|
self.data = record['content']
|
||||||
self.delete(domain_reverse_name)
|
self.delete(domain_reverse_name)
|
||||||
return {
|
return {
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
@ -472,7 +413,7 @@ class Record(object):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
"Cannot update auto-ptr record changes to domain {0}. Error: {1}"
|
"Cannot update auto-ptr record changes to domain {0}. Error: {1}"
|
||||||
.format(domain, e))
|
.format(domain_name, e))
|
||||||
current_app.logger.debug(traceback.format_exc())
|
current_app.logger.debug(traceback.format_exc())
|
||||||
return {
|
return {
|
||||||
'status':
|
'status':
|
||||||
@ -533,17 +474,13 @@ class Record(object):
|
|||||||
"""
|
"""
|
||||||
Check if record is present within domain records, and if it's present set self to found record
|
Check if record is present within domain records, and if it's present set self to found record
|
||||||
"""
|
"""
|
||||||
jdata = self.get_record_data(domain)
|
rrsets = self.get_rrsets(domain)
|
||||||
jrecords = jdata['records']
|
for r in rrsets:
|
||||||
|
if r['name'].rstrip('.') == self.name and r['type'] == self.type and r['records']:
|
||||||
for jr in jrecords:
|
self.type = r['type']
|
||||||
if jr['name'] == self.name and jr['type'] == self.type:
|
self.status = r['records'][0]['disabled']
|
||||||
self.name = jr['name']
|
self.ttl = r['ttl']
|
||||||
self.type = jr['type']
|
self.data = r['records'][0]['content']
|
||||||
self.status = jr['disabled']
|
|
||||||
self.ttl = jr['ttl']
|
|
||||||
self.data = jr['content']
|
|
||||||
self.priority = 10
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -554,42 +491,23 @@ class Record(object):
|
|||||||
headers = {}
|
headers = {}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||||
|
|
||||||
if self.NEW_SCHEMA:
|
data = {
|
||||||
data = {
|
"rrsets": [{
|
||||||
"rrsets": [{
|
"name":
|
||||||
"name":
|
self.name + '.',
|
||||||
self.name + '.',
|
"type":
|
||||||
"type":
|
self.type,
|
||||||
self.type,
|
"ttl":
|
||||||
"ttl":
|
self.ttl,
|
||||||
self.ttl,
|
"changetype":
|
||||||
"changetype":
|
"REPLACE",
|
||||||
"REPLACE",
|
"records": [{
|
||||||
"records": [{
|
"content": content,
|
||||||
"content": content,
|
"disabled": self.status,
|
||||||
"disabled": self.status,
|
|
||||||
}]
|
|
||||||
}]
|
}]
|
||||||
}
|
}]
|
||||||
else:
|
}
|
||||||
data = {
|
|
||||||
"rrsets": [{
|
|
||||||
"name":
|
|
||||||
self.name,
|
|
||||||
"type":
|
|
||||||
self.type,
|
|
||||||
"changetype":
|
|
||||||
"REPLACE",
|
|
||||||
"records": [{
|
|
||||||
"content": content,
|
|
||||||
"disabled": self.status,
|
|
||||||
"name": self.name,
|
|
||||||
"ttl": self.ttl,
|
|
||||||
"type": self.type,
|
|
||||||
"priority": 10
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
utils.fetch_json(urljoin(
|
utils.fetch_json(urljoin(
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import re
|
|
||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from distutils.version import StrictVersion
|
from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, jsonify, abort, flash
|
||||||
from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, jsonify, abort
|
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
from ..decorators import operator_role_required, admin_role_required
|
from ..decorators import operator_role_required, admin_role_required
|
||||||
@ -183,7 +181,7 @@ def manage_user():
|
|||||||
# Then delete the user
|
# Then delete the user
|
||||||
result = user.delete()
|
result = user.delete()
|
||||||
if result:
|
if result:
|
||||||
history = History(msg='Delete username {0}'.format(data),
|
history = History(msg='Delete user {0}'.format(data),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username)
|
||||||
history.add()
|
history.add()
|
||||||
return make_response(
|
return make_response(
|
||||||
@ -829,59 +827,42 @@ def create_template_from_zone():
|
|||||||
created_by=current_user.username)
|
created_by=current_user.username)
|
||||||
history.add()
|
history.add()
|
||||||
|
|
||||||
|
# After creating the domain in Domain Template in the,
|
||||||
|
# local DB. We add records into it Record Template.
|
||||||
records = []
|
records = []
|
||||||
r = Record()
|
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if domain:
|
if domain:
|
||||||
# query domain info from PowerDNS API
|
# Query zone's rrsets from PowerDNS API
|
||||||
zone_info = r.get_record_data(domain.name)
|
rrsets = Record().get_rrsets(domain.name)
|
||||||
if zone_info:
|
if rrsets:
|
||||||
jrecords = zone_info['records']
|
for r in rrsets:
|
||||||
|
name = '@' if r['name'] == domain_name + '.' else r[
|
||||||
if StrictVersion(Setting().get(
|
'name'].replace('.{}.'.format(domain_name), '')
|
||||||
'pdns_version')) >= StrictVersion('4.0.0'):
|
for record in r['records']:
|
||||||
for jr in jrecords:
|
t_record = DomainTemplateRecord(
|
||||||
if jr['type'] in Setting().get_records_allow_to_edit():
|
|
||||||
name = '@' if jr['name'] == domain_name else re.sub(
|
|
||||||
'\.{}$'.format(domain_name), '', jr['name'])
|
|
||||||
for subrecord in jr['records']:
|
|
||||||
record = DomainTemplateRecord(
|
|
||||||
name=name,
|
|
||||||
type=jr['type'],
|
|
||||||
status=True
|
|
||||||
if subrecord['disabled'] else False,
|
|
||||||
ttl=jr['ttl'],
|
|
||||||
data=subrecord['content'],
|
|
||||||
comment=jr['comment_data']['content'])
|
|
||||||
records.append(record)
|
|
||||||
else:
|
|
||||||
for jr in jrecords:
|
|
||||||
if jr['type'] in Setting().get_records_allow_to_edit():
|
|
||||||
name = '@' if jr['name'] == domain_name else re.sub(
|
|
||||||
'\.{}$'.format(domain_name), '', jr['name'])
|
|
||||||
record = DomainTemplateRecord(
|
|
||||||
name=name,
|
name=name,
|
||||||
type=jr['type'],
|
type=r['type'],
|
||||||
status=True if jr['disabled'] else False,
|
status=False if record['disabled'] else True,
|
||||||
ttl=jr['ttl'],
|
ttl=r['ttl'],
|
||||||
data=jr['content'],
|
data=record['content'])
|
||||||
comment=jr['comment_data']['content'])
|
records.append(t_record)
|
||||||
records.append(record)
|
|
||||||
|
|
||||||
result_records = t.replace_records(records)
|
result = t.replace_records(records)
|
||||||
|
|
||||||
if result_records['status'] == 'ok':
|
if result['status'] == 'ok':
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({
|
jsonify({
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
'msg': result['msg']
|
'msg': result['msg']
|
||||||
}), 200)
|
}), 200)
|
||||||
else:
|
else:
|
||||||
|
# Revert the domain template (remove it)
|
||||||
|
# ff we cannot add records.
|
||||||
t.delete_template()
|
t.delete_template()
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({
|
jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'msg': result_records['msg']
|
'msg': result['msg']
|
||||||
}), 500)
|
}), 500)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -918,7 +899,7 @@ def edit_template(template):
|
|||||||
record = DomainTemplateRecord(
|
record = DomainTemplateRecord(
|
||||||
name=jr.name,
|
name=jr.name,
|
||||||
type=jr.type,
|
type=jr.type,
|
||||||
status='Disabled' if jr.status else 'Active',
|
status='Active' if jr.status else 'Disabled',
|
||||||
ttl=jr.ttl,
|
ttl=jr.ttl,
|
||||||
data=jr.data,
|
data=jr.data,
|
||||||
comment=jr.comment if jr.comment else '')
|
comment=jr.comment if jr.comment else '')
|
||||||
@ -952,14 +933,14 @@ def apply_records(template):
|
|||||||
type = j['record_type']
|
type = j['record_type']
|
||||||
data = j['record_data']
|
data = j['record_data']
|
||||||
comment = j['record_comment']
|
comment = j['record_comment']
|
||||||
disabled = True if j['record_status'] == 'Disabled' else False
|
status = 0 if j['record_status'] == 'Disabled' else 1
|
||||||
ttl = int(j['record_ttl']) if j['record_ttl'] else 3600
|
ttl = int(j['record_ttl']) if j['record_ttl'] else 3600
|
||||||
|
|
||||||
dtr = DomainTemplateRecord(name=name,
|
dtr = DomainTemplateRecord(name=name,
|
||||||
type=type,
|
type=type,
|
||||||
data=data,
|
data=data,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
status=disabled,
|
status=status,
|
||||||
ttl=ttl)
|
ttl=ttl)
|
||||||
records.append(dtr)
|
records.append(dtr)
|
||||||
|
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
|
import dns.name
|
||||||
|
import dns.reversename
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, abort, jsonify
|
from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, abort, jsonify
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
|
from ..lib.utils import pretty_json
|
||||||
from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec
|
from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
from ..models.account import Account
|
from ..models.account import Account
|
||||||
@ -27,17 +30,17 @@ domain_bp = Blueprint('domain',
|
|||||||
@login_required
|
@login_required
|
||||||
@can_access_domain
|
@can_access_domain
|
||||||
def domain(domain_name):
|
def domain(domain_name):
|
||||||
r = Record()
|
# Validate the domain existing in the local DB
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if not domain:
|
if not domain:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
# query domain info from PowerDNS API
|
# Query domain's rrsets from PowerDNS API
|
||||||
zone_info = r.get_record_data(domain.name)
|
rrsets = Record().get_rrsets(domain.name)
|
||||||
if zone_info:
|
current_app.logger.debug("Fetched rrests: \n{}".format(pretty_json(rrsets)))
|
||||||
jrecords = zone_info['records']
|
|
||||||
else:
|
# API server might be down, misconfigured
|
||||||
# can not get any record, API server might be down
|
if not rrsets:
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
quick_edit = Setting().get('record_quick_edit')
|
quick_edit = Setting().get('record_quick_edit')
|
||||||
@ -49,45 +52,52 @@ def domain(domain_name):
|
|||||||
ttl_options = Setting().get_ttl_options()
|
ttl_options = Setting().get_ttl_options()
|
||||||
records = []
|
records = []
|
||||||
|
|
||||||
|
# Render the "records" to display in HTML datatable
|
||||||
|
#
|
||||||
|
# BUG: If we have multiple records with the same name
|
||||||
|
# and each record has its own comment, the display of
|
||||||
|
# [record-comment] may not consistent because PDNS API
|
||||||
|
# returns the rrsets (records, comments) has different
|
||||||
|
# order than its database records.
|
||||||
|
# TODO:
|
||||||
|
# - Find a way to make it consistent, or
|
||||||
|
# - Only allow one comment for that case
|
||||||
if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'):
|
if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'):
|
||||||
for jr in jrecords:
|
for r in rrsets:
|
||||||
if jr['type'] in records_allow_to_edit:
|
if r['type'] in records_allow_to_edit:
|
||||||
for subrecord in jr['records']:
|
r_name = r['name'].rstrip('.')
|
||||||
record = RecordEntry(name=jr['name'],
|
|
||||||
type=jr['type'],
|
# If it is reverse zone and pretty_ipv6_ptr setting
|
||||||
status='Disabled' if
|
# is enabled, we reformat the name for ipv6 records.
|
||||||
subrecord['disabled'] else 'Active',
|
if Setting().get('pretty_ipv6_ptr') and r[
|
||||||
ttl=jr['ttl'],
|
'type'] == 'PTR' and 'ip6.arpa' in r_name:
|
||||||
data=subrecord['content'],
|
r_name = dns.reversename.to_address(
|
||||||
comment=jr['comment_data']['content'],
|
dns.name.from_text(r_name))
|
||||||
is_allowed_edit=True)
|
|
||||||
records.append(record)
|
# Create the list of records in format that
|
||||||
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
|
# PDA jinja2 template can understand.
|
||||||
editable_records = forward_records_allow_to_edit
|
index = 0
|
||||||
else:
|
for record in r['records']:
|
||||||
editable_records = reverse_records_allow_to_edit
|
record_entry = RecordEntry(
|
||||||
return render_template('domain.html',
|
name=r_name,
|
||||||
domain=domain,
|
type=r['type'],
|
||||||
records=records,
|
status='Disabled' if record['disabled'] else 'Active',
|
||||||
editable_records=editable_records,
|
ttl=r['ttl'],
|
||||||
quick_edit=quick_edit,
|
data=record['content'],
|
||||||
ttl_options=ttl_options)
|
comment=r['comments'][index]['content']
|
||||||
|
if r['comments'] else '',
|
||||||
|
is_allowed_edit=True)
|
||||||
|
index += 1
|
||||||
|
records.append(record_entry)
|
||||||
else:
|
else:
|
||||||
for jr in jrecords:
|
# Unsupported version
|
||||||
if jr['type'] in records_allow_to_edit:
|
abort(500)
|
||||||
record = RecordEntry(
|
|
||||||
name=jr['name'],
|
if not re.search(r'ip6\.arpa|in-addr\.arpa$', domain_name):
|
||||||
type=jr['type'],
|
|
||||||
status='Disabled' if jr['disabled'] else 'Active',
|
|
||||||
ttl=jr['ttl'],
|
|
||||||
data=jr['content'],
|
|
||||||
comment=jr['comment_data']['content'],
|
|
||||||
is_allowed_edit=True)
|
|
||||||
records.append(record)
|
|
||||||
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
|
|
||||||
editable_records = forward_records_allow_to_edit
|
editable_records = forward_records_allow_to_edit
|
||||||
else:
|
else:
|
||||||
editable_records = reverse_records_allow_to_edit
|
editable_records = reverse_records_allow_to_edit
|
||||||
|
|
||||||
return render_template('domain.html',
|
return render_template('domain.html',
|
||||||
domain=domain,
|
domain=domain,
|
||||||
records=records,
|
records=records,
|
||||||
@ -158,7 +168,7 @@ def add():
|
|||||||
record_row = {
|
record_row = {
|
||||||
'record_data': template_record.data,
|
'record_data': template_record.data,
|
||||||
'record_name': template_record.name,
|
'record_name': template_record.name,
|
||||||
'record_status': template_record.status,
|
'record_status': 'Active' if template_record.status else 'Disabled',
|
||||||
'record_ttl': template_record.ttl,
|
'record_ttl': template_record.ttl,
|
||||||
'record_type': template_record.type,
|
'record_type': template_record.type,
|
||||||
'comment_data': [{'content': template_record.comment, 'account': ''}]
|
'comment_data': [{'content': template_record.comment, 'account': ''}]
|
||||||
@ -276,11 +286,11 @@ def change_type(domain_name):
|
|||||||
#TODO: Validate ip addresses input
|
#TODO: Validate ip addresses input
|
||||||
domain_master_ips = []
|
domain_master_ips = []
|
||||||
if domain_type == 'slave' and request.form.getlist('domain_master_address'):
|
if domain_type == 'slave' and request.form.getlist('domain_master_address'):
|
||||||
domain_master_string = request.form.getlist(
|
domain_master_string = request.form.getlist(
|
||||||
'domain_master_address')[0]
|
'domain_master_address')[0]
|
||||||
domain_master_string = domain_master_string.replace(
|
domain_master_string = domain_master_string.replace(
|
||||||
' ', '')
|
' ', '')
|
||||||
domain_master_ips = domain_master_string.split(',')
|
domain_master_ips = domain_master_string.split(',')
|
||||||
|
|
||||||
d = Domain()
|
d = Domain()
|
||||||
status = d.update_kind(domain_name=domain_name,
|
status = d.update_kind(domain_name=domain_name,
|
||||||
@ -354,19 +364,16 @@ def change_account(domain_name):
|
|||||||
@login_required
|
@login_required
|
||||||
@can_access_domain
|
@can_access_domain
|
||||||
def record_apply(domain_name):
|
def record_apply(domain_name):
|
||||||
#TODO: filter removed records / name modified records.
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jdata = request.json
|
jdata = request.json
|
||||||
submitted_serial = jdata['serial']
|
submitted_serial = jdata['serial']
|
||||||
submitted_record = jdata['record']
|
submitted_record = jdata['record']
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
current_app.logger.debug(
|
|
||||||
'Your submitted serial: {0}'.format(submitted_serial))
|
|
||||||
current_app.logger.debug('Current domain serial: {0}'.format(
|
|
||||||
domain.serial))
|
|
||||||
|
|
||||||
if domain:
|
if domain:
|
||||||
|
current_app.logger.debug('Current domain serial: {0}'.format(
|
||||||
|
domain.serial))
|
||||||
|
|
||||||
if int(submitted_serial) != domain.serial:
|
if int(submitted_serial) != domain.serial:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({
|
jsonify({
|
||||||
@ -384,26 +391,17 @@ def record_apply(domain_name):
|
|||||||
'Domain name {0} does not exist'.format(domain_name)
|
'Domain name {0} does not exist'.format(domain_name)
|
||||||
}), 404)
|
}), 404)
|
||||||
|
|
||||||
# Modify the record's comment data. We append
|
|
||||||
# the "current_user" into account field as it
|
|
||||||
# a field with user-defined meaning
|
|
||||||
for sr in submitted_record:
|
|
||||||
if sr.get('record_comment'):
|
|
||||||
sr['comment_data'] = [{
|
|
||||||
'content': sr['record_comment'],
|
|
||||||
'account': current_user.username
|
|
||||||
}]
|
|
||||||
else:
|
|
||||||
sr['comment_data'] = []
|
|
||||||
|
|
||||||
r = Record()
|
r = Record()
|
||||||
result = r.apply(domain_name, submitted_record)
|
result = r.apply(domain_name, submitted_record)
|
||||||
if result['status'] == 'ok':
|
if result['status'] == 'ok':
|
||||||
jdata.pop('_csrf_token',
|
|
||||||
None) # don't store csrf token in the history.
|
|
||||||
history = History(
|
history = History(
|
||||||
msg='Apply record changes to domain {0}'.format(domain_name),
|
msg='Apply record changes to domain {0}'.format(domain_name),
|
||||||
detail=str(json.dumps(jdata)),
|
detail=str(
|
||||||
|
json.dumps({
|
||||||
|
"domain": domain_name,
|
||||||
|
"add_rrests": result['data'][0]['rrsets'],
|
||||||
|
"del_rrests": result['data'][1]['rrsets']
|
||||||
|
})),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username)
|
||||||
history.add()
|
history.add()
|
||||||
return make_response(jsonify(result), 200)
|
return make_response(jsonify(result), 200)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
import datetime
|
import datetime
|
||||||
@ -161,6 +162,7 @@ def login():
|
|||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
login_user(user, remember=False)
|
login_user(user, remember=False)
|
||||||
session['authentication_type'] = 'OAuth'
|
session['authentication_type'] = 'OAuth'
|
||||||
|
signin_history(user.username, 'Google OAuth', True)
|
||||||
return redirect(url_for('index.index'))
|
return redirect(url_for('index.index'))
|
||||||
|
|
||||||
if 'github_token' in session:
|
if 'github_token' in session:
|
||||||
@ -187,6 +189,7 @@ def login():
|
|||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
session['authentication_type'] = 'OAuth'
|
session['authentication_type'] = 'OAuth'
|
||||||
login_user(user, remember=False)
|
login_user(user, remember=False)
|
||||||
|
signin_history(user.username, 'Github OAuth', True)
|
||||||
return redirect(url_for('index.index'))
|
return redirect(url_for('index.index'))
|
||||||
|
|
||||||
if 'azure_token' in session:
|
if 'azure_token' in session:
|
||||||
@ -225,6 +228,7 @@ def login():
|
|||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
session['authentication_type'] = 'OAuth'
|
session['authentication_type'] = 'OAuth'
|
||||||
login_user(user, remember=False)
|
login_user(user, remember=False)
|
||||||
|
signin_history(user.username, 'Azure OAuth', True)
|
||||||
return redirect(url_for('index.index'))
|
return redirect(url_for('index.index'))
|
||||||
|
|
||||||
if 'oidc_token' in session:
|
if 'oidc_token' in session:
|
||||||
@ -250,6 +254,7 @@ def login():
|
|||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
session['authentication_type'] = 'OAuth'
|
session['authentication_type'] = 'OAuth'
|
||||||
login_user(user, remember=False)
|
login_user(user, remember=False)
|
||||||
|
signin_history(user.username, 'OIDC OAuth', True)
|
||||||
return redirect(url_for('index.index'))
|
return redirect(url_for('index.index'))
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
@ -272,6 +277,7 @@ def login():
|
|||||||
auth = user.is_validate(method=auth_method,
|
auth = user.is_validate(method=auth_method,
|
||||||
src_ip=request.remote_addr)
|
src_ip=request.remote_addr)
|
||||||
if auth == False:
|
if auth == False:
|
||||||
|
signin_history(user.username, 'LOCAL', False)
|
||||||
return render_template('login.html',
|
return render_template('login.html',
|
||||||
saml_enabled=SAML_ENABLED,
|
saml_enabled=SAML_ENABLED,
|
||||||
error='Invalid credentials')
|
error='Invalid credentials')
|
||||||
@ -288,6 +294,7 @@ def login():
|
|||||||
if otp_token and otp_token.isdigit():
|
if otp_token and otp_token.isdigit():
|
||||||
good_token = user.verify_totp(otp_token)
|
good_token = user.verify_totp(otp_token)
|
||||||
if not good_token:
|
if not good_token:
|
||||||
|
signin_history(user.username, 'LOCAL', False)
|
||||||
return render_template('login.html',
|
return render_template('login.html',
|
||||||
saml_enabled=SAML_ENABLED,
|
saml_enabled=SAML_ENABLED,
|
||||||
error='Invalid credentials')
|
error='Invalid credentials')
|
||||||
@ -297,6 +304,7 @@ def login():
|
|||||||
error='Token required')
|
error='Token required')
|
||||||
|
|
||||||
login_user(user, remember=remember_me)
|
login_user(user, remember=remember_me)
|
||||||
|
signin_history(user.username, 'LOCAL', True)
|
||||||
return redirect(session.get('next', url_for('index.index')))
|
return redirect(session.get('next', url_for('index.index')))
|
||||||
|
|
||||||
|
|
||||||
@ -309,6 +317,37 @@ def clear_session():
|
|||||||
logout_user()
|
logout_user()
|
||||||
|
|
||||||
|
|
||||||
|
def signin_history(username, authenticator, success):
|
||||||
|
# Get user ip address
|
||||||
|
if request.headers.getlist("X-Forwarded-For"):
|
||||||
|
request_ip = request.headers.getlist("X-Forwarded-For")[0]
|
||||||
|
request_ip = request_ip.split(',')[0]
|
||||||
|
else:
|
||||||
|
request_ip = request.remote_addr
|
||||||
|
|
||||||
|
# Write log
|
||||||
|
if success:
|
||||||
|
str_success = 'succeeded'
|
||||||
|
current_app.logger.info(
|
||||||
|
"User {} authenticated successfully via {} from {}".format(
|
||||||
|
username, authenticator, request_ip))
|
||||||
|
else:
|
||||||
|
str_success = 'failed'
|
||||||
|
current_app.logger.warning(
|
||||||
|
"User {} failed to authenticate via {} from {}".format(
|
||||||
|
username, authenticator, request_ip))
|
||||||
|
|
||||||
|
# Write history
|
||||||
|
History(msg='User {} authentication {}'.format(username, str_success),
|
||||||
|
detail=str({
|
||||||
|
"username": username,
|
||||||
|
"authenticator": authenticator,
|
||||||
|
"ip_address": request_ip,
|
||||||
|
"success": 1 if success else 0
|
||||||
|
}),
|
||||||
|
created_by='System').add()
|
||||||
|
|
||||||
|
|
||||||
@index_bp.route('/logout')
|
@index_bp.route('/logout')
|
||||||
def logout():
|
def logout():
|
||||||
if current_app.config.get(
|
if current_app.config.get(
|
||||||
@ -427,7 +466,7 @@ def dyndns_update():
|
|||||||
|
|
||||||
domain = None
|
domain = None
|
||||||
domain_segments = hostname.split('.')
|
domain_segments = hostname.split('.')
|
||||||
for index in range(len(domain_segments)):
|
for _index in range(len(domain_segments)):
|
||||||
full_domain = '.'.join(domain_segments)
|
full_domain = '.'.join(domain_segments)
|
||||||
potential_domain = Domain.query.filter(
|
potential_domain = Domain.query.filter(
|
||||||
Domain.name == full_domain).first()
|
Domain.name == full_domain).first()
|
||||||
@ -468,7 +507,7 @@ def dyndns_update():
|
|||||||
# Record content did not change, return 'nochg'
|
# Record content did not change, return 'nochg'
|
||||||
history = History(
|
history = History(
|
||||||
msg=
|
msg=
|
||||||
"DynDNS update: attempted update of {0} but record did not change"
|
"DynDNS update: attempted update of {0} but record already up-to-date"
|
||||||
.format(hostname),
|
.format(hostname),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username)
|
||||||
history.add()
|
history.add()
|
||||||
@ -477,10 +516,14 @@ def dyndns_update():
|
|||||||
result = r.update(domain.name, str(ip))
|
result = r.update(domain.name, str(ip))
|
||||||
if result['status'] == 'ok':
|
if result['status'] == 'ok':
|
||||||
history = History(
|
history = History(
|
||||||
msg=
|
msg='DynDNS update: updated {} successfully'.format(hostname),
|
||||||
'DynDNS update: updated {0} record {1} in zone {2}, it changed from {3} to {4}'
|
detail=str({
|
||||||
.format(rtype, hostname, domain.name, oldip, str(ip)),
|
"domain": domain.name,
|
||||||
detail=str(result),
|
"record": hostname,
|
||||||
|
"type": rtype,
|
||||||
|
"old_value": oldip,
|
||||||
|
"new_value": str(ip)
|
||||||
|
}),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username)
|
||||||
history.add()
|
history.add()
|
||||||
response = 'good'
|
response = 'good'
|
||||||
@ -493,18 +536,33 @@ def dyndns_update():
|
|||||||
DomainSetting.setting == 'create_via_dyndns').first()
|
DomainSetting.setting == 'create_via_dyndns').first()
|
||||||
if (ondemand_creation is not None) and (strtobool(
|
if (ondemand_creation is not None) and (strtobool(
|
||||||
ondemand_creation.value) == True):
|
ondemand_creation.value) == True):
|
||||||
record = Record(name=hostname,
|
|
||||||
type=rtype,
|
# Build the rrset
|
||||||
data=str(ip),
|
rrset_data = [{
|
||||||
status=False,
|
"changetype": "REPLACE",
|
||||||
ttl=3600)
|
"name": hostname + '.',
|
||||||
result = record.add(domain.name)
|
"ttl": 3600,
|
||||||
|
"type": rtype,
|
||||||
|
"records": [{
|
||||||
|
"content": str(ip),
|
||||||
|
"disabled": False
|
||||||
|
}],
|
||||||
|
"comments": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
# Format the rrset
|
||||||
|
rrset = {"rrsets": rrset_data}
|
||||||
|
result = Record().add(domain.name, rrset)
|
||||||
if result['status'] == 'ok':
|
if result['status'] == 'ok':
|
||||||
history = History(
|
history = History(
|
||||||
msg=
|
msg=
|
||||||
'DynDNS update: created record {0} in zone {1}, it now represents {2}'
|
'DynDNS update: created record {0} in zone {1} successfully'
|
||||||
.format(hostname, domain.name, str(ip)),
|
.format(hostname, domain.name, str(ip)),
|
||||||
detail=str(result),
|
detail=str({
|
||||||
|
"domain": domain.name,
|
||||||
|
"record": hostname,
|
||||||
|
"value": str(ip)
|
||||||
|
}),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username)
|
||||||
history.add()
|
history.add()
|
||||||
response = 'good'
|
response = 'good'
|
||||||
@ -663,6 +721,7 @@ def saml_authorized():
|
|||||||
user.update_profile()
|
user.update_profile()
|
||||||
session['authentication_type'] = 'SAML'
|
session['authentication_type'] = 'SAML'
|
||||||
login_user(user, remember=False)
|
login_user(user, remember=False)
|
||||||
|
signin_history(user.username, 'SAML', True)
|
||||||
return redirect(url_for('index.login'))
|
return redirect(url_for('index.login'))
|
||||||
else:
|
else:
|
||||||
return render_template('errors/SAML.html', errors=errors)
|
return render_template('errors/SAML.html', errors=errors)
|
||||||
@ -683,7 +742,7 @@ def handle_account(account_name):
|
|||||||
clean_name = ''.join(c for c in account_name.lower()
|
clean_name = ''.join(c for c in account_name.lower()
|
||||||
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
|
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
|
||||||
if len(clean_name) > Account.name.type.length:
|
if len(clean_name) > Account.name.type.length:
|
||||||
logging.error(
|
current_app.logger.error(
|
||||||
"Account name {0} too long. Truncated.".format(clean_name))
|
"Account name {0} too long. Truncated.".format(clean_name))
|
||||||
account = Account.query.filter_by(name=clean_name).first()
|
account = Account.query.filter_by(name=clean_name).first()
|
||||||
if not account:
|
if not account:
|
||||||
|
@ -241,26 +241,30 @@ function reload_domains(url) {
|
|||||||
|
|
||||||
// pretty JSON
|
// pretty JSON
|
||||||
json_library = {
|
json_library = {
|
||||||
replacer: function(match, pIndent, pKey, pVal, pEnd) {
|
replacer: function (match, pIndent, pKey, pVal, pEnd) {
|
||||||
var key = '<span class=json-key>';
|
var key = '<span class=json-key>';
|
||||||
var val = '<span class=json-value>';
|
var val = '<span class=json-value>';
|
||||||
var str = '<span class=json-string>';
|
var str = '<span class=json-string>';
|
||||||
var r = pIndent || '';
|
var r = pIndent || '';
|
||||||
if (pKey){
|
if (pKey) {
|
||||||
r = r + key + pKey.replace(/[": ]/g, '') + '</span>: ';
|
// r = r + key + pKey.replace(/[": ]/g, '') + '</span>: ';
|
||||||
|
// Keep the quote in the key
|
||||||
|
r = r + key + pKey.replace(/":/, '"') + '</span>: ';
|
||||||
}
|
}
|
||||||
if (pVal){
|
if (pVal) {
|
||||||
r = r + (pVal[0] == '"' ? str : val) + pVal + '</span>';
|
r = r + (pVal[0] == '"' ? str : val) + pVal + '</span>';
|
||||||
}
|
}
|
||||||
return r + (pEnd || '');
|
return r + (pEnd || '');
|
||||||
},
|
},
|
||||||
prettyPrint: function(obj) {
|
prettyPrint: function (obj) {
|
||||||
obj = obj.replace(/u'/g, "\'").replace(/'/g, "\"").replace(/(False|None)/g, "\"$1\"");
|
obj = obj.replace(/u'/g, "\'").replace(/'/g, "\"").replace(/(False|None)/g, "\"$1\"");
|
||||||
var jsonData = JSON.parse(obj);
|
var jsonData = JSON.parse(obj);
|
||||||
var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
|
// var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
|
||||||
return JSON.stringify(jsonData, null, 3)
|
// The new regex to handle case value is an empty list [] or dict {}
|
||||||
|
var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?/mg;
|
||||||
|
return JSON.stringify(jsonData, null, 3)
|
||||||
.replace(/&/g, '&').replace(/\\"/g, '"')
|
.replace(/&/g, '&').replace(/\\"/g, '"')
|
||||||
.replace(/</g, '<').replace(/>/g, '>')
|
.replace(/</g, '<').replace(/>/g, '>')
|
||||||
.replace(jsonLine, json_library.replacer);
|
.replace(jsonLine, json_library.replacer);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -107,7 +107,7 @@
|
|||||||
<td>{{ history.msg }}</td>
|
<td>{{ history.msg }}</td>
|
||||||
<td>{{ history.created_on }}</td>
|
<td>{{ history.created_on }}</td>
|
||||||
<td width="6%">
|
<td width="6%">
|
||||||
<button type="button" class="btn btn-flat btn-primary history-info-button" value='{{ history.detail|replace("[]","None") }}'>
|
<button type="button" class="btn btn-flat btn-primary history-info-button" value='{{ history.detail }}'>
|
||||||
Info <i class="fa fa-info"></i>
|
Info <i class="fa fa-info"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
@ -335,6 +335,9 @@
|
|||||||
mx_server = modal.find('#mx_server').val();
|
mx_server = modal.find('#mx_server').val();
|
||||||
mx_priority = modal.find('#mx_priority').val();
|
mx_priority = modal.find('#mx_priority').val();
|
||||||
data = mx_priority + " " + mx_server;
|
data = mx_priority + " " + mx_server;
|
||||||
|
if (data && !data.endsWith('.')) {
|
||||||
|
data = data + '.'
|
||||||
|
}
|
||||||
record_data.val(data);
|
record_data.val(data);
|
||||||
modal.modal('hide');
|
modal.modal('hide');
|
||||||
})
|
})
|
||||||
@ -370,6 +373,9 @@
|
|||||||
srv_port = modal.find('#srv_port').val();
|
srv_port = modal.find('#srv_port').val();
|
||||||
srv_target = modal.find('#srv_target').val();
|
srv_target = modal.find('#srv_target').val();
|
||||||
data = srv_priority + " " + srv_weight + " " + srv_port + " " + srv_target;
|
data = srv_priority + " " + srv_weight + " " + srv_port + " " + srv_target;
|
||||||
|
if (data && !data.endsWith('.')) {
|
||||||
|
data = data + '.'
|
||||||
|
}
|
||||||
record_data.val(data);
|
record_data.val(data);
|
||||||
modal.modal('hide');
|
modal.modal('hide');
|
||||||
})
|
})
|
||||||
|
@ -313,6 +313,9 @@
|
|||||||
mx_server = modal.find('#mx_server').val();
|
mx_server = modal.find('#mx_server').val();
|
||||||
mx_priority = modal.find('#mx_priority').val();
|
mx_priority = modal.find('#mx_priority').val();
|
||||||
data = mx_priority + " " + mx_server;
|
data = mx_priority + " " + mx_server;
|
||||||
|
if (data && !data.endsWith('.')) {
|
||||||
|
data = data + '.'
|
||||||
|
}
|
||||||
record_data.val(data);
|
record_data.val(data);
|
||||||
modal.modal('hide');
|
modal.modal('hide');
|
||||||
})
|
})
|
||||||
@ -348,6 +351,9 @@
|
|||||||
srv_port = modal.find('#srv_port').val();
|
srv_port = modal.find('#srv_port').val();
|
||||||
srv_target = modal.find('#srv_target').val();
|
srv_target = modal.find('#srv_target').val();
|
||||||
data = srv_priority + " " + srv_weight + " " + srv_port + " " + srv_target;
|
data = srv_priority + " " + srv_weight + " " + srv_port + " " + srv_target;
|
||||||
|
if (data && !data.endsWith('.')) {
|
||||||
|
data = data + '.'
|
||||||
|
}
|
||||||
record_data.val(data);
|
record_data.val(data);
|
||||||
modal.modal('hide');
|
modal.modal('hide');
|
||||||
})
|
})
|
||||||
|
@ -417,6 +417,11 @@ datatables.net-bs@^1.10.19:
|
|||||||
datatables.net "1.10.19"
|
datatables.net "1.10.19"
|
||||||
jquery ">=1.7"
|
jquery ">=1.7"
|
||||||
|
|
||||||
|
datatables.net-plugins@^1.10.19:
|
||||||
|
version "1.10.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/datatables.net-plugins/-/datatables.net-plugins-1.10.20.tgz#c89f6bed3fa7e6605cbeaa35d60f223659e84c8c"
|
||||||
|
integrity sha512-rnhNmRHe9UEzvM7gtjBay1QodkWUmxLUhHNbmJMYhhUggjtm+BRSlE0PRilkeUkwckpNWzq+0fPd7/i0fpQgzA==
|
||||||
|
|
||||||
datatables.net@1.10.19, datatables.net@^1.10.19:
|
datatables.net@1.10.19, datatables.net@^1.10.19:
|
||||||
version "1.10.19"
|
version "1.10.19"
|
||||||
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.10.19.tgz#97a1ed41c85e62d61040603481b59790a172dd1f"
|
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.10.19.tgz#97a1ed41c85e62d61040603481b59790a172dd1f"
|
||||||
|
Loading…
Reference in New Issue
Block a user