-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy patheditdns
executable file
·381 lines (323 loc) · 14.1 KB
/
editdns
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
#!/bin/zsh
#
# Airstream Communications, LLC
# DNS support tools
#
# editdns: edit a BIND-style zone file with robust sanity checking.
# Usage: editdns zonesfile
#
# Note: this script uses ZSH-specific facilities (like loadable modules)
#
# Note: Except when called with the --list option, this script requires root privileges
#
zmodload zsh/stat
zmodload zsh/termcap
zmodload zsh/datetime
##############################################################################
#
# Function Definitions
#
##############################################################################
usage () {
cat <<-EOF
Usage: editdns [ --verbose ] [ --list ] [ --editor <editor> ] [ domain-name ]
Options:
--editor (-e) Specify an editor (vi, nano, pico, etc.)
--help (-h) This help
--list (-l) List all configured zones
--verbose (-v) Be chatty about what is going on
- When called with --list (-l) the 'zone-name' parameter is ignored and you do not need root credentials.
- The --editor (-e) named must exist as an executable in /bin or /usr/bin
- <domain-name> is the name of a DNS domain without the '.db' zone file extension
EOF
}
# sort a list of IP addresses
ipsort () {
sort --field-separator='.' --key=1n,1 --key=2n,2 --key=3n,3 --key=4n,4
}
# sort a list of in-addr.arpa names
in-addrsort () {
sort --field-separator='.' --key=3n,3 --key=2n,2 --key=1n,1
}
# return the full path to the zone file for the named zone (domain)
getZoneFile () {
local domainName=${1:?"Name a domain for which to retrieve a zone file."}
local returnValue=0
(( VERBOSE )) && echo "Getting zone filename for domain ${domainName}." >&2
# get the zone file
# BUG ALERT: this is totally dependent on the placement of the 'file' option inside 'zone' clauses
if zoneFile=$(egrep -A 3 "^zone[[:space:]]+\"${domainName}\"" ${BINDconf} | egrep '^[[:space:]]+file' | head -n 1 | cut -d'"' -f2)
then
if test -n "${zoneFile}"
then
(( VERBOSE )) && echo "Found zone file ${zoneFile}." >&2
# prepend dbDir if zoneFile is a relative pathname
if expr match ${zoneFile} '[^/]' > /dev/null
then
zoneFile=${dbDir}/${zoneFile}
(( VERBOSE )) && echo "Full path to zone file: ${zoneFile}." >&2
fi
if test -r ${zoneFile}
then
echo ${zoneFile}
else
echo "${0:t}: Error (3): Configuration file ${zoneFile} for ${domainName} not found in ${dbDir}." >&2
returnValue=3
fi
else
echo "${0:t}: Error (2): Unable to get a zone file name for domain ${domainName} from ${BINDconf}." >&2
returnValue=2
fi
else
echo "${0:t}: Error (1): Zone file configuration for ${domainName} not found in ${BINDconf}." >&2
returnValue=1
fi
return ${returnValue}
}
# set the SOA serial number field to a proper value
newSerialNumber() {
local zoneFileName=${1:?"Need a zone filename to proceed."}
today=$(strftime '%Y%m%d' ${EPOCHSECONDS})
# index is a two-digit, Zero-filled number
typeset -Z 2 index=1
# get the current serial number
# Note that this presumes all zone files have been touched since 1999
# This also strips any comments from the end of the line containing the serial number
serial=$(pcregrep -o '^\s+20[0-9][0-9][0-9][0-9][0-9][0-9][0-9]{1,2}' ${zoneFileName} | tr -d '[[:space:]]' | sed -r -e 's/[[:space:]]*;.*//')
(( VERBOSE )) && echo "Current serial number for ${zoneFileName}: ${BOLD}${serial}${NORMAL} \c" >&2
# lop off the two-digit index
serialDate=${serial%??}
if (( serialDate == today ))
then
(( newSerial = serial + 1 ))
else
newSerial=${today}${index}
fi
(( VERBOSE )) && echo "New serial number: ${BOLD}${newSerial}${NORMAL}" >&2
# replace the serial number in the zone file
# Note: this function exits with the exit value of the sed(1) command; errors are handled in the calling context
sed -i -r -e "s/^([[:space:]]+)20[0-9][0-9][0-9][0-9][0-9][0-9][0-9]{1,2}/\1${newSerial}/" ${zoneFileName}
}
# publish the changes and reload the named daemons
pushChanges() {
local domain=${1:?"Need a domain for which to push changes."}
local returnValue=0
# this file seems to be authoritative on whether we are clustered
if test -f ${ROOT:-/}var/cpanel/useclusteringdns
then
# push the zones to the slave servers
if test -x ${ROOT:-/}scripts/dnscluster && ${ROOT:-/}scripts/dnscluster synczone ${domain}
then
echo "Synchronized ${domain}"
else
echo "Could not synchronize ${domain} (${?}). Please contact sysadmin@wins.net." >&2
returnValue=1
fi
fi
# reload the name server
if ${ROOT:-/}usr/local/admin/dns/reload-dns.sh ${VERBOSE:+"--verbose"}
then
echo "Slave named instances successfully reloaded."
else
echo "Could not reload name service (${?}). Please contact sysadmin@wins.net." >&2
returnValue=2
fi
return $returnValue;
}
##############################################################################
#
# Global Variables
#
##############################################################################
# high-powered graphics, see zshmodules(1) and termcap(5) for details
BOLD="${termcap[md]}"
NORMAL="${termcap[me]}"
# default BIND configuration file is /etc/named.conf, exit if it is not readable
BINDconf=${BIND_CONF:-${ROOT:-/}etc/named.conf}
test -r "${BINDconf}" || { echo "Error (0): Cannot find a readable copy of ${BINDconf}. Got root?" ; exit ; }
# default zone file location from the BIND configuration
dbDir=$(awk '/^[[:space:]]+directory/{print $2}' ${BINDconf} | tr -d '[";]')
# set the defalt editor their preference, else vi
EDITOR=${VISUAL:-${EDITOR:-vi}}
# logger tag will contain this script name and the current PID
loggerTag="${0:t}[${$}]"
##############################################################################
#
# Script Main Line
#
##############################################################################
# use getopt to parse the command line args
TEMP=$(getopt -o e:hlv --long editor:,help,list-files,verbose -n ${0:t} -- "${@}")
if [ ${?} != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
# Note the quotes around '$TEMP': they are essential!
eval set -- "${TEMP}"
while :
do
case "${1}" in
-e|--edit*) editor=${2} ; shift 2 ;;
-[?h]|--help) usage ; exit ;;
-l|--list*) LISTZONES=1 ; shift ;;
-v|--verb*) VERBOSE=1 ; shift ;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
if test -n "${LISTZONES}"
then
# this does not need elevated privileges
for domain in $(awk '/^zone[[:space:]]/{print $2}' ${BINDconf} | tr -d '"' | ipsort | uniq)
do
zoneFile=$(grep -A 3 "${domain}" ${BINDconf} | awk '/^[[:space:]]+file[[:space:]]/{print $2}' | tr -d '[";]' | head -n 1)
stat -s -H zoneFileMeta ${zoneFile}
printf '%42s: %10s %8s %-8s %19s %s\n' ${domain} ${zoneFileMeta[mode]} ${zoneFileMeta[uid]} ${zoneFileMeta[gid]} ${zoneFileMeta[mtime]} ${zoneFile}
done
else
# from here to the end of the script requires elevated privileges
if (( UID == 0 )) && test "${USER}" = root
then
invokingUser=$(who am i | cut -d' ' -f1)
(( VERBOSE )) && echo "${invokingUser} seems to be running this instance of ${0:t}." >&2
DOMAIN=${1}
if test -n "${DOMAIN}"
then
# determine the full path to the chosen editor
if test -n "${editor}"
then
choices=( $(whence ${editor}) )
for choice in ${choices}
do
if test -x ${choice}
then
export EDITOR=${choice:t}
export VISUAL=${choice:t}
export SUDO_EDIT=${choice:t}
fi
done
fi
LOCKFILE=${TMP:-${ROOT:-/}tmp}/${DOMAIN}.lock
if test -r ${LOCKFILE}
then
echo "${BOLD}Found existing lock file. If you are certain nobody is editing this zone file, remove ${LOCKFILE} and try again.${NORMAL}"
logger -t "${loggerTag}" -- "${invokingUser}: ERROR: Found existing lock file for ${DOMAIN}."
else
# remove the lock file under most exit conditions
trap "rm -f ${LOCKFILE} ; exit ;" 0 1 2 15
logger -t "${loggerTag}" -- "${invokingUser}: wants to change zone file for ${DOMAIN}."
if zoneFile=$(getZoneFile ${DOMAIN})
then
# get current zone file meta info (owner, group, perms, etc.)
stat -o -H zoneFileMeta ${zoneFile}
if TMPZONEFILE=$(mktemp -t ${zoneFile:t}-XXXXXXX)
then
if cp -p ${zoneFile} ${TMPZONEFILE}
then
# clean up after ourself
trap "rm -f ${LOCKFILE} ${TMPZONEFILE}; exit;" 0 1 2 15
read ready\?"About to edit temporary zone file for ${DOMAIN}. Do NOT change the serial number ${BOLD}(press <ENTER> to continue)${NORMAL}. "
sudoedit ${TMPZONEFILE}
# cmp returns success if the files are identical
if ! cmp ${zoneFile} ${TMPZONEFILE} > /dev/null 2>&1
then
(( VERBOSE )) && echo "Updating the serial number in ${TMPZONEFILE}." >&2
if newSerialNumber ${TMPZONEFILE}
then
# perform some screen real estate calculations, then display the differences (paginated with less(1))
read ready\?"${BOLD}Press <Enter> to view differences between the old and new zone files:${NORMAL} "
(
# this is disgusting, but it enables us to display
# aligned column headers over the sdiff output
current="CURRENT CONFIGURATION"
new=" NEW CONFIGURATION "
cLen=${#current} # string length
nLen=${#new}
c=$(( ( COLUMNS / 4 ) - ( cLen / 2 ) ))
s=$(( COLUMNS / 2 - ( c + cLen ) ))
n=$(( ( COLUMNS / 4 * 3 - ( nLen / 2 )) - ( c + cLen + s ) ))
echo | perl -ne "print q( ) x ${c}, q(${current}), q( ) x ( ${s} + ${n} ), q(${new}), qq(\n);"
echo | perl -ne "print q(-) x ${COLUMNS}, qq(\n);"
# display the difference between the current and changed (temporary) zone files
sdiff -w${COLUMNS:-80} --suppress-common-lines --ignore-all-space ${zoneFile} ${TMPZONEFILE}
) | less -M -PM"- less (Press q to quit)- ?f%f\ ..?ltline\ %lt\ .?e(END)%t"
read ready\?"Press <Enter> to commit changes to ${zoneFile}. ${BOLD}Press <Ctrl-C> to abort this change:${NORMAL} "
if mv ${TMPZONEFILE} ${zoneFile}
then
# restore perms from the original zone file
# we have to strip off the left-most four octal digits else chmod gets confused
chmod ${zoneFileMeta[mode]#????} ${zoneFile}
chown ${zoneFileMeta[uid]}:${zoneFileMeta[gid]} ${zoneFile}
(( VERBOSE )) && echo "Publishing DNS changes..." >&2
if pushChanges ${DOMAIN}
then
# ignore some signals
trap '' 0 1 2 15
read comment\?"${BOLD}Please type a brief 1-line comment describing the change you just made:${NORMAL} "
logger -t "${loggerTag}" -- "${invokingUser}: ${zoneFile}: ${comment}"
cat <<EOF
Do you need to update one or more ${BOLD}reverse DNS${NORMAL} zone files?
Be sure to run this script with the relevant ${BOLD}in-addr.arpa${NORMAL} domain name
if any of your previous changes need to be reflected in a reverse lookup zone.
${BOLD}Please contact sysadmin@wins.net if you have any questions.${NORMAL}
EOF
# restore signals to clean up after we are done
trap "rm -f ${LOCKFILE} ${TMPZONEFILE}; exit;" 0 1 2 15
else
echo "${BOLD}Could not publish DNS changes (${?}). Please Contact sysadmin@wins.net.${NORMAL}"
logger -t "${loggerTag}" -- "${invokingUser}: Could not publish DNS changes: pushChanges() returned ${?}."
fi
else
echo "${BOLD}Could not replace ${zoneFile} with ${TMPZONEFILE} (${?}). Please contact sysadmin@wins.net.${NORMAL}"
logger -t "${loggerTag}" -- "${invokingUser}: Could not replace ${zoneFile} with ${TMPZONEFILE}: mv(1) returned ${?}."
fi
else
echo "${BOLD}Error updating the serial number in ${TMPZONEFILE} (${?}). Please edit this file again or contact sysadmin@wins.net.${NORMAL}"
logger -t "${loggerTag}" -- "${invokingUser}: Error updating the serial number in ${TMPZONEFILE}: newSerialNumber returned ${?}."
fi
else
echo "${BOLD}Did not detect any changes to ${zoneFile}. NOT pushing changes to name servers.${NORMAL}"
logger -t "${loggerTag}" -- "${invokingUser}: Did not detect any changes to ${zoneFile}. NOT pushing changes to name servers."
fi
else
echo "${BOLD}Could not copy ${zoneFile} to ${TMPZONEFILE} (${?}). Please contact sysadmin@wins.net.${NORMAL}"
logger -t "${loggerTag}" -- "${invokingUser}: Could not copy ${zoneFile} to ${TMPZONEFILE}: cp(1) returned ${?}."
fi
else
echo "${BOLD}Could not create temporary filename: mktemp failed (${?}). Please contact sysadmin@wins.net.${NORMAL}"
logger -t "${loggerTag}" -- "${invokingUser}: Could not create temporary filename: mktemp returned ${?}."
fi
else
echo "${BOLD}Could not determine the zone file for domain ${DOMAIN}. Please ensure you entered the correct domain name.${NORMAL}"
logger -t "${loggerTag}" -- "${invokingUser}: Could not determine the zone file for domain ${DOMAIN}."
fi
fi
else
echo "${BOLD}Please specify a domain name to edit.${NORMAL}"
usage
fi
else
echo "${BOLD}You must run this program with 'root' privileges. Try 'sudo ${0:t}' and see if that works better.${NORMAL}"
fi
fi
exit
# Local Variables: ***
# mode:shell-script ***
# indent-tabs-mode: f ***
# sh-indentation: 2 ***
# sh-basic-offset: 2 ***
# sh-indent-for-do: 0 ***
# sh-indent-after-do: + ***
# sh-indent-comment: t ***
# sh-indent-after-case: + ***
# sh-indent-after-done: 0 ***
# sh-indent-after-else: + ***
# sh-indent-after-if: + ***
# sh-indent-after-loop-construct: + ***
# sh-indent-after-open: + ***
# sh-indent-after-switch: + ***
# sh-indent-for-case-alt: + ***
# sh-indent-for-case-label: + ***
# sh-indent-for-continuation: + ***
# sh-indent-for-done: 0 ***
# sh-indent-for-else: 0 ***
# sh-indent-for-fi: 0 ***
# sh-indent-for-then: 0 ***
# End: ***