2018-11-14 06:16:56 -05:00
#!/usr/bin/python3
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# requirements:
# sudo apt install libmysqlclient-dev
# pip3 install sqlobject configparser mysqlclient argparse
from sqlobject import *
import configparser
import argparse
import os
import string
import sys
import time
import re
from subprocess import DEVNULL , PIPE , Popen
# globals
2018-11-14 08:29:34 -05:00
connection = None
2018-11-14 06:16:56 -05:00
soa_serial = int ( time . time ( ) )
config = configparser . ConfigParser ( )
fix_absolute = False
storage = os . getcwd ( )
knotc_binary = " knotc "
knotc_socket = None
2018-11-14 10:09:18 -05:00
slave_mode = False
2018-11-21 08:58:09 -05:00
conf_txn_open = False
2019-01-15 08:01:53 -05:00
knotc_zone_reload = [ ]
2019-01-15 10:12:15 -05:00
knotc_timeout = 10
2018-11-14 06:16:56 -05:00
class Domains ( SQLObject ) :
# id = IntCol() # implicitly there
name = StringCol ( )
master = StringCol ( )
2018-11-16 04:51:23 -05:00
last_check = IntCol ( )
2018-11-14 06:16:56 -05:00
type = StringCol ( )
2018-11-16 04:51:23 -05:00
notified_serial = IntCol ( )
2018-11-14 06:16:56 -05:00
account = StringCol ( )
class Records ( SQLObject ) :
# id = IntCol() # implicitly there
domain = ForeignKey ( ' Domains ' )
name = StringCol ( )
type = StringCol ( )
content = StringCol ( )
ttl = IntCol ( )
prio = IntCol ( )
2018-11-16 04:51:23 -05:00
change_date = IntCol ( )
2018-11-14 06:16:56 -05:00
ordername = StringCol ( )
2018-11-16 04:51:23 -05:00
auth = IntCol ( )
2018-11-14 06:16:56 -05:00
class Changes ( SQLObject ) :
# id = IntCol() # implicitly there
domain = ForeignKey ( ' Domains ' )
type = IntCol ( ) # -1 .. zone removed; 0 .. zone modified; 1 .. zone added
def remove_dot ( s ) :
return s [ : - 1 ] if s [ - 1 ] == ' . ' else s
def fix_abs ( name ) :
return remove_dot ( name ) + ' . ' if fix_absolute else name
2018-11-14 08:29:34 -05:00
def domain_get_records ( domain , txn ) :
2018-11-14 06:16:56 -05:00
if str ( domain ) . isdigit ( ) :
2018-11-14 08:29:34 -05:00
return Records . select ( Records . q . domain == domain , connection = txn )
2018-11-14 06:16:56 -05:00
else :
dn = remove_dot ( domain )
2018-11-14 08:29:34 -05:00
return Records . select ( AND ( Domains . q . id == Records . q . domain , Domains . q . name == dn ) , connection = txn )
2018-11-14 06:16:56 -05:00
2018-11-14 08:29:34 -05:00
def domain_id2name ( domain , txn ) :
return Domains . select ( Domains . q . id == domain , connection = txn ) [ 0 ] . name
2018-11-14 06:16:56 -05:00
def get_config ( key , default_val ) :
global config
return int ( config [ ' DEFAULT ' ] [ key ] ) if key in config [ ' DEFAULT ' ] and config [ ' DEFAULT ' ] [ key ] is not None else default_val
def get_soa_params ( ) :
refresh = get_config ( " soa-refresh-default " , 10800 )
retry = get_config ( " soa-retry-default " , 3600 )
expire = get_config ( " soa-expire-default " , 604800 )
minttl = get_config ( " soa-minimum-ttl " , 3600 )
return ( refresh , retry , expire , minttl )
def soa_content ( db_content ) :
global soa_serial
( nameserver , contact , fake_serial ) = db_content . split ( )
ns = fix_abs ( nameserver )
co = fix_abs ( contact . replace ( " @ " , " . " ) )
( refresh , retry , expire , minttl ) = get_soa_params ( )
return ( " %s %s %d %d %d %d %d " % ( ns , co , soa_serial , refresh , retry , expire , minttl ) )
def zone_storage ( zone ) :
global storage
return os . path . join ( storage , " %s .zone " % remove_dot ( zone ) )
def knotc_single ( * args ) :
global knotc_binary
global knotc_socket
2019-01-15 10:12:15 -05:00
global knotc_timeout
2018-11-14 06:16:56 -05:00
2019-01-15 10:12:15 -05:00
cmd = [ knotc_binary , " -s " , knotc_socket , " -t " , str ( knotc_timeout ) ] + list ( args )
2018-11-14 06:16:56 -05:00
p = Popen ( cmd , stdout = PIPE , stderr = PIPE , universal_newlines = True )
( stdout , stderr ) = p . communicate ( )
if p . returncode != 0 :
raise Exception ( " error: knotc %s failed: ' %s ' " % ( str ( args ) , stderr ) )
2018-11-14 08:48:23 -05:00
def zone_template ( zone ) :
# this function is intended to be patched by user's bussiness logic
return None
2019-01-15 08:07:00 -05:00
# param type:
# -1 ... remove this zone
# 0 ... mark this zone as modified (fallback to 2 if not exists)
# 1 ... add zone (fallback to 0 if exists already)
# 2 ... add zone no fallback (ignore if exists already)
2018-11-14 06:16:56 -05:00
def knotc_send ( type , zone ) :
2018-11-14 10:09:18 -05:00
global slave_mode
2018-11-21 08:58:09 -05:00
global conf_txn_open
2019-01-15 08:01:53 -05:00
global knotc_zone_reload
2018-11-14 06:16:56 -05:00
if type == 0 :
2018-11-16 04:49:05 -05:00
try :
2019-01-15 08:01:53 -05:00
# ensure by zone-status that the zone exists
knotc_single ( " zone-status " , zone )
knotc_zone_reload . append ( zone )
2018-11-16 04:49:05 -05:00
except :
2019-01-15 08:07:00 -05:00
knotc_send ( 2 , zone )
2018-11-14 06:16:56 -05:00
else :
try :
2018-11-21 08:58:09 -05:00
if not conf_txn_open :
knotc_single ( " conf-begin " )
conf_txn_open = True
2018-11-14 06:16:56 -05:00
if type > 0 :
2019-01-15 08:07:00 -05:00
try :
knotc_single ( " conf-set " , " zone[ %s ] " % zone )
knotc_single ( " conf-set " , " zone[ %s ].file " % zone , zone_storage ( zone ) )
template = zone_template ( remove_dot ( zone ) )
if template is not None :
knotc_single ( " conf-set " , " zone[ %s ].template " % zone , template )
except :
if type > 1 :
pass
else :
knotc_send ( 0 , zone )
2018-11-14 06:16:56 -05:00
else :
knotc_single ( " conf-unset " , " zone[ %s ] " % zone )
2019-01-15 08:01:53 -05:00
try :
knotc_zone_reload . remove ( zone )
except :
pass
2018-11-14 06:16:56 -05:00
except :
knotc_single ( " conf-abort " )
2018-11-21 08:58:09 -05:00
conf_txn_open = False
2018-11-14 06:16:56 -05:00
raise
def print_record ( record , outfile ) :
t = record . type . upper ( )
if t == ' SOA ' :
content = soa_content ( record . content )
elif t == ' MX ' or t == ' SRV ' :
content = " %d %s " % ( record . prio , record . content )
2018-12-22 15:31:42 -05:00
elif t == ' TXT ' or t == ' SPF ' :
content = " \" %s \" " % record . content
2018-11-14 06:16:56 -05:00
else :
content = record . content
if t in ( ' NS ' , ' MX ' , ' CNAME ' , ' DNAME ' , ' SRV ' , ' PTR ' ) :
content = fix_abs ( content )
record = ( " %s . %d %s %s \n " % ( record . name , record . ttl , t , content ) )
outfile . write ( record )
2018-11-14 08:29:34 -05:00
def print_domain ( domain , change_type = 0 , txn = None ) :
2018-11-14 06:16:56 -05:00
global knotc_socket
2018-11-14 10:09:18 -05:00
global slave_mode
2018-11-14 08:29:34 -05:00
dn = domain_id2name ( domain , txn ) if str ( domain ) . isdigit ( ) else domain
2018-11-14 10:09:18 -05:00
if not slave_mode :
2018-12-21 10:19:56 -05:00
file_name = zone_storage ( dn )
tmp_name = " %s .tmp " % file_name
f = open ( tmp_name , " w " )
2018-11-14 10:09:18 -05:00
for r in domain_get_records ( domain , txn ) :
print_record ( r , f )
f . close ( )
2018-12-21 10:19:56 -05:00
os . rename ( tmp_name , file_name )
2018-11-14 06:16:56 -05:00
if knotc_socket is not None :
knotc_send ( change_type , dn )
2018-11-23 06:28:11 -05:00
print ( " Updated zone %s " % dn )
2018-11-14 06:16:56 -05:00
2018-11-14 08:29:34 -05:00
def domain_from_change ( change , txn ) :
2018-11-14 06:16:56 -05:00
global knotc_socket
if change . type > = 0 :
2018-11-14 08:29:34 -05:00
print_domain ( change . domain . name , change . type , txn )
2018-11-14 06:16:56 -05:00
else :
dn = change . domain . name
try :
os . remove ( zone_storage ( dn ) )
except :
2018-11-23 06:28:11 -05:00
print ( " Warning: failed to delete zonefile for %s " % dn , file = sys . stderr )
2018-11-14 06:16:56 -05:00
if knotc_socket is not None :
knotc_send ( change . type , dn )
else :
print ( " Warning: removed zone ' %s ' , but unspecified knotc socket. " % dn , file = sys . stderr )
def process_changes ( startwith ) :
2018-11-14 08:29:34 -05:00
global connection
2018-11-14 06:16:56 -05:00
processed = [ ]
try :
2018-11-14 08:29:34 -05:00
txn = connection . transaction ( )
for ch in Changes . select ( Changes . q . id > startwith , connection = txn ) :
domain_from_change ( ch , txn )
2018-11-14 06:16:56 -05:00
processed . append ( ch . id )
2018-11-14 08:29:34 -05:00
txn . commit ( )
2018-11-14 06:16:56 -05:00
finally :
if len ( processed ) > 0 :
2018-11-23 06:28:11 -05:00
print ( " Processed up to change_id %d " % processed [ - 1 ] )
2018-11-14 06:16:56 -05:00
2018-11-14 08:29:34 -05:00
def process_all ( ) :
global connection
txn = connection . transaction ( )
for d in Domains . select ( connection = txn ) :
print_domain ( d . id , txn = txn )
txn . commit ( )
2018-11-14 06:16:56 -05:00
def read_config_file ( filename ) :
global config
with open ( filename , ' r ' ) as f :
fcontent = ' [DEFAULT] \n ' + f . read ( )
config . read_string ( fcontent )
def main ( ) :
global storage
global knotc_socket
global soa_serial
global fix_absolute
2018-11-14 08:29:34 -05:00
global connection
2018-11-14 10:09:18 -05:00
global slave_mode
2018-11-21 08:58:09 -05:00
global conf_txn_open
2019-01-15 08:01:53 -05:00
global knotc_zone_reload
2019-01-15 10:12:15 -05:00
global knotc_timeout
2018-11-14 06:16:56 -05:00
2018-11-23 06:28:11 -05:00
argp = argparse . ArgumentParser ( prog = ' dns_sql2zf ' , description = " Export DNS records from Mysql or Postgres DB into zonefile. " , epilog = " (C) CZ.NIC, GPLv3 " )
2018-11-14 06:16:56 -05:00
argp . add_argument ( dest = ' domains ' , metavar = ' zone ' , nargs = ' * ' , help = ' Zone to be exported. ' )
argp . add_argument ( ' --db ' , dest = ' dburi ' , metavar = ' DB_URI ' , nargs = 1 , required = True , help = ' URI of database to export from (example: mysql://user:password@127.0.0.1/powerdns_db). ' )
argp . add_argument ( ' --storage ' , dest = ' storage ' , metavar = ' path ' , nargs = 1 , help = ' Storage for the generated zonefile (otherwise current dir). ' )
argp . add_argument ( ' --all ' , dest = ' all ' , action = ' store_true ' , help = " Export all zones. " )
argp . add_argument ( ' --confile ' , dest = ' confile ' , metavar = ' file ' , nargs = 1 , help = ' PowerDNS configfile to obtain SOA parameters (otherwise defaults). ' )
argp . add_argument ( ' --serial ' , dest = ' soa_serial ' , type = int , metavar = ' uint32 ' , nargs = 1 , help = ' SOA serial number (otherwise UNIX timestamp). ' )
argp . add_argument ( ' --absolute-names ' , dest = ' fix_absolute ' , action = ' store_true ' , help = " Interpret names in records ' contents (e.g. CNAME, NS...) as absolute even if w/o trailing dot. " )
2018-12-18 04:58:58 -05:00
argp . add_argument ( ' --from-changes ' , dest = ' from_changes ' , metavar = " from_id " , nargs = ' ? ' , const = 0 , help = " Export zones listed in extra ' changes ' table. " )
2018-11-14 06:16:56 -05:00
argp . add_argument ( ' --knotc ' , dest = ' knotc_socket ' , metavar = ' knot_socket ' , nargs = 1 , help = " Notify Knot DNS about changes (requires: $PATH/knotc). " )
2019-01-15 10:12:15 -05:00
argp . add_argument ( ' --knotc-timeout ' , dest = ' knotc_timeout ' , type = int , metavar = ' uint32 ' , nargs = 1 , help = ' Timeout for knotc commands (default 10). ' )
2018-11-14 10:09:18 -05:00
argp . add_argument ( ' --slave ' , dest = ' slave ' , action = ' store_true ' , help = " Don ' t generate zonefiles, use ' knotc zone-refresh ' instead of zone-reload. " )
2018-11-14 06:16:56 -05:00
argp . add_argument ( ' --version ' , action = ' version ' , version = ' dns_sql2zf 0.1 ' )
args = argp . parse_args ( )
if args . soa_serial is not None :
soa_serial = args . soa_serial [ 0 ]
if args . confile is not None :
read_config_file ( args . confile [ 0 ] )
if args . storage is not None :
storage = args . storage [ 0 ]
if args . fix_absolute :
fix_absolute = True
if args . knotc_socket is not None :
knotc_socket = args . knotc_socket [ 0 ]
2019-01-15 10:12:15 -05:00
if args . knotc_timeout is not None :
knotc_timeout = args . knotc_timeout [ 0 ]
2018-11-14 10:09:18 -05:00
if args . slave :
slave_mode = True
2018-11-14 06:16:56 -05:00
connection = connectionForURI ( args . dburi [ 0 ] )
sqlhub . processConnection = connection
for domain in args . domains :
print_domain ( domain )
if args . all :
2018-11-14 08:29:34 -05:00
process_all ( )
2018-11-14 06:16:56 -05:00
if args . from_changes is not None :
2018-12-18 04:58:58 -05:00
process_changes ( args . from_changes )
2018-11-14 06:16:56 -05:00
2018-11-21 08:58:09 -05:00
if conf_txn_open :
knotc_single ( " conf-commit " )
2019-01-15 08:01:53 -05:00
for zone in knotc_zone_reload :
knotc_single ( " zone-reload " if not slave_mode else " zone-refresh " , zone )
2018-11-14 06:16:56 -05:00
if __name__ == " __main__ " :
main ( )