Merge branch 'master' of github.com:ngoduykhanh/PowerDNS-Admin

This commit is contained in:
Khanh Ngo 2019-12-16 17:23:54 +07:00
commit 4a3f121b02
11 changed files with 498 additions and 524 deletions

View file

@ -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": (" ", " "),

View file

@ -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'

View file

@ -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 +

View file

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

View file

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

View file

@ -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:

View file

@ -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, '&amp;').replace(/\\"/g, '&quot;') .replace(/&/g, '&amp;').replace(/\\"/g, '&quot;')
.replace(/</g, '&lt;').replace(/>/g, '&gt;') .replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(jsonLine, json_library.replacer); .replace(jsonLine, json_library.replacer);
} }
}; };

View file

@ -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&nbsp;<i class="fa fa-info"></i> Info&nbsp;<i class="fa fa-info"></i>
</button> </button>
</td> </td>

View file

@ -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');
}) })

View file

@ -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');
}) })

View file

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