~ubuntu-branches/ubuntu/vivid/samba/vivid

« back to all changes in this revision

Viewing changes to source4/scripting/bin/samba_dnsupdate

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2011-12-21 13:18:04 UTC
  • mfrom: (0.39.21 sid)
  • Revision ID: package-import@ubuntu.com-20111221131804-xtlr39wx6njehxxr
Tags: 2:3.6.1-3ubuntu1
* Merge from Debian testing.  Remaining changes:
  + debian/patches/VERSION.patch:
    - set SAMBA_VERSION_SUFFIX to Ubuntu.
  + debian/patches/error-trans.fix-276472:
    - Add the translation of Unix Error code -ENOTSUP to NT Error Code
    - NT_STATUS_NOT_SUPPORTED to prevent the Permission denied error.
  + debian/smb.conf:
    - add "(Samba, Ubuntu)" to server string.
    - comment out the default [homes] share, and add a comment about
      "valid users = %S" to show users how to restrict access to
      \\server\username to only username.
    - Set 'usershare allow guests', so that usershare admins are 
      allowed to create public shares in addition to authenticated
      ones.
    - add map to guest = Bad user, maps bad username to guest access.
  + debian/samba-common.config:
    - Do not change priority to high if dhclient3 is installed.
    - Use priority medium instead of high for the workgroup question.
  + debian/control:
    - Don't build against or suggest ctdb.
    - Add dependency on samba-common-bin to samba.
  + Add ufw integration:
    - Created debian/samba.ufw.profile
    - debian/rules, debian/samba.dirs, debian/samba.files: install
      profile
    - debian/control: have samba suggest ufw
  + Add apport hook:
    - Created debian/source_samba.py.
    - debian/rules, debian/samba.dirs, debian/samba-common-bin.files: install
  + Switch to upstart:
    - Add debian/samba.{nmbd,smbd}.upstart.
  + debian/samba.logrotate, debian/samba-common.dhcp, debian/samba.if-up:
    - Make them upstart compatible
  + debian/samba.postinst: 
    - Avoid scary pdbedit warnings on first import.
  + debian/samba-common.postinst: Add more informative error message for
    the case where smb.conf was manually deleted
  + debian/patches/fix-debuglevel-name-conflict.patch: don't use 'debug_level'
    as a global variable name in an NSS module 
  + Dropped:
    - debian/patches/error-trans.fix-276472
    - debian/patches/fix-debuglevel-name-conflict.patch

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
#
 
3
# update our DNS names using TSIG-GSS
 
4
#
 
5
# Copyright (C) Andrew Tridgell 2010
 
6
#
 
7
# This program is free software; you can redistribute it and/or modify
 
8
# it under the terms of the GNU General Public License as published by
 
9
# the Free Software Foundation; either version 3 of the License, or
 
10
# (at your option) any later version.
 
11
#
 
12
# This program is distributed in the hope that it will be useful,
 
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
15
# GNU General Public License for more details.
 
16
#
 
17
# You should have received a copy of the GNU General Public License
 
18
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
19
 
 
20
 
 
21
import os
 
22
import fcntl
 
23
import sys
 
24
import tempfile
 
25
import subprocess
 
26
 
 
27
# ensure we get messages out immediately, so they get in the samba logs,
 
28
# and don't get swallowed by a timeout
 
29
os.environ['PYTHONUNBUFFERED'] = '1'
 
30
 
 
31
# forcing GMT avoids a problem in some timezones with kerberos. Both MIT
 
32
# heimdal can get mutual authentication errors due to the 24 second difference
 
33
# between UTC and GMT when using some zone files (eg. the PDT zone from
 
34
# the US)
 
35
os.environ["TZ"] = "GMT"
 
36
 
 
37
# Find right directory when running from source tree
 
38
sys.path.insert(0, "bin/python")
 
39
 
 
40
import samba
 
41
import optparse
 
42
from samba import getopt as options
 
43
from ldb import SCOPE_BASE
 
44
from samba.auth import system_session
 
45
from samba.samdb import SamDB
 
46
from samba.dcerpc import netlogon, winbind
 
47
 
 
48
samba.ensure_external_module("dns", "dnspython")
 
49
import dns.resolver
 
50
import dns.exception
 
51
 
 
52
default_ttl = 900
 
53
am_rodc = False
 
54
error_count = 0
 
55
 
 
56
parser = optparse.OptionParser("samba_dnsupdate")
 
57
sambaopts = options.SambaOptions(parser)
 
58
parser.add_option_group(sambaopts)
 
59
parser.add_option_group(options.VersionOptions(parser))
 
60
parser.add_option("--verbose", action="store_true")
 
61
parser.add_option("--all-names", action="store_true")
 
62
parser.add_option("--all-interfaces", action="store_true")
 
63
parser.add_option("--use-file", type="string", help="Use a file, rather than real DNS calls")
 
64
parser.add_option("--update-list", type="string", help="Add DNS names from the given file")
 
65
parser.add_option("--fail-immediately", action='store_true', help="Exit on first failure")
 
66
 
 
67
creds = None
 
68
ccachename = None
 
69
 
 
70
opts, args = parser.parse_args()
 
71
 
 
72
if len(args) != 0:
 
73
    parser.print_usage()
 
74
    sys.exit(1)
 
75
 
 
76
lp = sambaopts.get_loadparm()
 
77
 
 
78
domain = lp.get("realm")
 
79
host = lp.get("netbios name")
 
80
if opts.all_interfaces:
 
81
    all_interfaces = True
 
82
else:
 
83
    all_interfaces = False
 
84
 
 
85
IPs = samba.interface_ips(lp, all_interfaces)
 
86
nsupdate_cmd = lp.get('nsupdate command')
 
87
 
 
88
if len(IPs) == 0:
 
89
    print "No IP interfaces - skipping DNS updates"
 
90
    sys.exit(0)
 
91
 
 
92
if opts.verbose:
 
93
    print "IPs: %s" % IPs
 
94
 
 
95
########################################################
 
96
# get credentials if we haven't got them already
 
97
def get_credentials(lp):
 
98
    from samba import credentials
 
99
    global ccachename, creds
 
100
    if creds is not None:
 
101
        return
 
102
    creds = credentials.Credentials()
 
103
    creds.guess(lp)
 
104
    creds.set_machine_account(lp)
 
105
    creds.set_krb_forwardable(credentials.NO_KRB_FORWARDABLE)
 
106
    (tmp_fd, ccachename) = tempfile.mkstemp()
 
107
    creds.get_named_ccache(lp, ccachename)
 
108
 
 
109
 
 
110
#############################################
 
111
# an object to hold a parsed DNS line
 
112
class dnsobj(object):
 
113
    def __init__(self, string_form):
 
114
        list = string_form.split()
 
115
        self.dest = None
 
116
        self.port = None
 
117
        self.ip = None
 
118
        self.existing_port = None
 
119
        self.existing_weight = None
 
120
        self.type = list[0]
 
121
        self.name = list[1].lower()
 
122
        if self.type == 'SRV':
 
123
            self.dest = list[2].lower()
 
124
            self.port = list[3]
 
125
        elif self.type == 'A':
 
126
            self.ip   = list[2] # usually $IP, which gets replaced
 
127
        elif self.type == 'CNAME':
 
128
            self.dest = list[2].lower()
 
129
        else:
 
130
            print "Received unexpected DNS reply of type %s" % self.type
 
131
            raise
 
132
 
 
133
    def __str__(self):
 
134
        if d.type == "A":     return "%s %s %s" % (self.type, self.name, self.ip)
 
135
        if d.type == "SRV":   return "%s %s %s %s" % (self.type, self.name, self.dest, self.port)
 
136
        if d.type == "CNAME": return "%s %s %s" % (self.type, self.name, self.dest)
 
137
 
 
138
 
 
139
################################################
 
140
# parse a DNS line from
 
141
def parse_dns_line(line, sub_vars):
 
142
    subline = samba.substitute_var(line, sub_vars)
 
143
    d = dnsobj(subline)
 
144
    return d
 
145
 
 
146
############################################
 
147
# see if two hostnames match
 
148
def hostname_match(h1, h2):
 
149
    h1 = str(h1)
 
150
    h2 = str(h2)
 
151
    return h1.lower().rstrip('.') == h2.lower().rstrip('.')
 
152
 
 
153
 
 
154
############################################
 
155
# check that a DNS entry exists
 
156
def check_dns_name(d):
 
157
    normalised_name = d.name.rstrip('.') + '.'
 
158
    if opts.verbose:
 
159
        print "Looking for DNS entry %s as %s" % (d, normalised_name)
 
160
 
 
161
    if opts.use_file is not None:
 
162
        try:
 
163
            dns_file = open(opts.use_file, "r")
 
164
        except IOError:
 
165
            return False
 
166
        
 
167
        for line in dns_file:
 
168
            line = line.strip()
 
169
            if line == '' or line[0] == "#":
 
170
                continue
 
171
            if line.lower() == str(d).lower():
 
172
                return True
 
173
        return False
 
174
 
 
175
    try:
 
176
        ans = dns.resolver.query(normalised_name, d.type)
 
177
    except dns.exception.DNSException:
 
178
        if opts.verbose:
 
179
            print "Failed to find DNS entry %s" % d
 
180
        return False
 
181
    if d.type == 'A':
 
182
        # we need to be sure that our IP is there
 
183
        for rdata in ans:
 
184
            if str(rdata) == str(d.ip):
 
185
                return True
 
186
    if d.type == 'CNAME':
 
187
        for i in range(len(ans)):
 
188
            if hostname_match(ans[i].target, d.dest):
 
189
                return True
 
190
    if d.type == 'SRV':
 
191
        for rdata in ans:
 
192
            if opts.verbose:
 
193
                print "Checking %s against %s" % (rdata, d)
 
194
            if hostname_match(rdata.target, d.dest):
 
195
                if str(rdata.port) == str(d.port):
 
196
                    return True
 
197
                else:
 
198
                    d.existing_port     = str(rdata.port)
 
199
                    d.existing_weight = str(rdata.weight)
 
200
 
 
201
    if opts.verbose:
 
202
        print "Failed to find matching DNS entry %s" % d
 
203
 
 
204
    return False
 
205
 
 
206
 
 
207
###########################################
 
208
# get the list of substitution vars
 
209
def get_subst_vars():
 
210
    global lp, am_rodc
 
211
    vars = {}
 
212
 
 
213
    samdb = SamDB(url=lp.get("sam database"), session_info=system_session(),
 
214
                  lp=lp)
 
215
 
 
216
    vars['DNSDOMAIN'] = lp.get('realm').lower()
 
217
    vars['DNSFOREST'] = lp.get('realm').lower()
 
218
    vars['HOSTNAME']  = lp.get('netbios name').lower() + "." + vars['DNSDOMAIN']
 
219
    vars['NTDSGUID']  = samdb.get_ntds_GUID()
 
220
    vars['SITE']      = samdb.server_site_name()
 
221
    res = samdb.search(base=None, scope=SCOPE_BASE, attrs=["objectGUID"])
 
222
    guid = samdb.schema_format_value("objectGUID", res[0]['objectGUID'][0])
 
223
    vars['DOMAINGUID'] = guid
 
224
    am_rodc = samdb.am_rodc()
 
225
 
 
226
    return vars
 
227
 
 
228
 
 
229
############################################
 
230
# call nsupdate for an entry
 
231
def call_nsupdate(d):
 
232
    global ccachename, nsupdate_cmd
 
233
 
 
234
    if opts.verbose:
 
235
        print "Calling nsupdate for %s" % d
 
236
 
 
237
    if opts.use_file is not None:
 
238
        wfile = open(opts.use_file, 'a')
 
239
        fcntl.lockf(wfile, fcntl.LOCK_EX)
 
240
        wfile.write(str(d)+"\n")
 
241
        fcntl.lockf(wfile, fcntl.LOCK_UN)
 
242
        return
 
243
 
 
244
    normalised_name = d.name.rstrip('.') + '.'
 
245
 
 
246
    (tmp_fd, tmpfile) = tempfile.mkstemp()
 
247
    f = os.fdopen(tmp_fd, 'w')
 
248
    if d.type == "A":
 
249
        f.write("update add %s %u A %s\n" % (normalised_name, default_ttl, d.ip))
 
250
    if d.type == "SRV":
 
251
        if d.existing_port is not None:
 
252
            f.write("update delete %s SRV 0 %s %s %s\n" % (normalised_name, d.existing_weight,
 
253
                                                           d.existing_port, d.dest))
 
254
        f.write("update add %s %u SRV 0 100 %s %s\n" % (normalised_name, default_ttl, d.port, d.dest))
 
255
    if d.type == "CNAME":
 
256
        f.write("update add %s %u CNAME %s\n" % (normalised_name, default_ttl, d.dest))
 
257
    if opts.verbose:
 
258
        f.write("show\n")
 
259
    f.write("send\n")
 
260
    f.close()
 
261
 
 
262
    os.environ["KRB5CCNAME"] = ccachename
 
263
    try:
 
264
        cmd = nsupdate_cmd[:]
 
265
        cmd.append(tmpfile)
 
266
        subprocess.check_call(cmd, shell=False)
 
267
    except Exception, estr:
 
268
        global error_count
 
269
        if opts.fail_immediately:
 
270
            sys.exit(1)
 
271
        error_count = error_count + 1
 
272
        if opts.verbose:
 
273
            print("Failed nsupdate: %s : %s" % (str(d), estr))
 
274
    os.unlink(tmpfile)
 
275
 
 
276
 
 
277
 
 
278
def rodc_dns_update(d, t):
 
279
    '''a single DNS update via the RODC netlogon call'''
 
280
    global sub_vars
 
281
 
 
282
    if opts.verbose:
 
283
        print "Calling netlogon RODC update for %s" % d
 
284
 
 
285
    typemap = {
 
286
        netlogon.NlDnsLdapAtSite       : netlogon.NlDnsInfoTypeNone,
 
287
        netlogon.NlDnsGcAtSite         : netlogon.NlDnsDomainNameAlias,
 
288
        netlogon.NlDnsDsaCname         : netlogon.NlDnsDomainNameAlias,
 
289
        netlogon.NlDnsKdcAtSite        : netlogon.NlDnsInfoTypeNone,
 
290
        netlogon.NlDnsDcAtSite         : netlogon.NlDnsInfoTypeNone,
 
291
        netlogon.NlDnsRfc1510KdcAtSite : netlogon.NlDnsInfoTypeNone,
 
292
        netlogon.NlDnsGenericGcAtSite  : netlogon.NlDnsDomainNameAlias
 
293
        }
 
294
 
 
295
    w = winbind.winbind("irpc:winbind_server", lp)
 
296
    dns_names = netlogon.NL_DNS_NAME_INFO_ARRAY()
 
297
    dns_names.count = 1
 
298
    name = netlogon.NL_DNS_NAME_INFO()
 
299
    name.type = t
 
300
    name.dns_domain_info_type = typemap[t]
 
301
    name.priority = 0
 
302
    name.weight   = 0
 
303
    if d.port is not None:
 
304
        name.port = int(d.port)
 
305
    name.dns_register = True
 
306
    dns_names.names = [ name ]
 
307
    site_name = sub_vars['SITE'].decode('utf-8')
 
308
 
 
309
    global error_count
 
310
 
 
311
    try:
 
312
        ret_names = w.DsrUpdateReadOnlyServerDnsRecords(site_name, default_ttl, dns_names)
 
313
        if ret_names.names[0].status != 0:
 
314
            print("Failed to set DNS entry: %s (status %u)" % (d, ret_names.names[0].status))
 
315
            error_count = error_count + 1
 
316
    except RuntimeError, reason:
 
317
        print("Error setting DNS entry of type %u: %s: %s" % (t, d, reason))
 
318
        error_count = error_count + 1
 
319
 
 
320
    if error_count != 0 and opts.fail_immediately:
 
321
        sys.exit(1)
 
322
 
 
323
 
 
324
def call_rodc_update(d):
 
325
    '''RODCs need to use the netlogon API for nsupdate'''
 
326
    global lp, sub_vars
 
327
 
 
328
    # we expect failure for 3268 if we aren't a GC
 
329
    if d.port is not None and int(d.port) == 3268:
 
330
        return
 
331
 
 
332
    # map the DNS request to a netlogon update type
 
333
    map = {
 
334
        netlogon.NlDnsLdapAtSite       : '_ldap._tcp.${SITE}._sites.${DNSDOMAIN}',
 
335
        netlogon.NlDnsGcAtSite         : '_ldap._tcp.${SITE}._sites.gc._msdcs.${DNSDOMAIN}',
 
336
        netlogon.NlDnsDsaCname         : '${NTDSGUID}._msdcs.${DNSFOREST}',
 
337
        netlogon.NlDnsKdcAtSite        : '_kerberos._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
 
338
        netlogon.NlDnsDcAtSite         : '_ldap._tcp.${SITE}._sites.dc._msdcs.${DNSDOMAIN}',
 
339
        netlogon.NlDnsRfc1510KdcAtSite : '_kerberos._tcp.${SITE}._sites.${DNSDOMAIN}',
 
340
        netlogon.NlDnsGenericGcAtSite  : '_gc._tcp.${SITE}._sites.${DNSFOREST}'
 
341
        }
 
342
 
 
343
    for t in map:
 
344
        subname = samba.substitute_var(map[t], sub_vars)
 
345
        if subname.lower() == d.name.lower():
 
346
            # found a match - do the update
 
347
            rodc_dns_update(d, t)
 
348
            return
 
349
    if opts.verbose:
 
350
        print("Unable to map to netlogon DNS update: %s" % d)
 
351
 
 
352
 
 
353
# get the list of DNS entries we should have
 
354
if opts.update_list:
 
355
    dns_update_list = opts.update_list
 
356
else:
 
357
    dns_update_list = lp.private_path('dns_update_list')
 
358
 
 
359
# use our private krb5.conf to avoid problems with the wrong domain
 
360
# bind9 nsupdate wants the default domain set
 
361
krb5conf = lp.private_path('krb5.conf')
 
362
os.environ['KRB5_CONFIG'] = krb5conf
 
363
 
 
364
file = open(dns_update_list, "r")
 
365
 
 
366
# get the substitution dictionary
 
367
sub_vars = get_subst_vars()
 
368
 
 
369
# build up a list of update commands to pass to nsupdate
 
370
update_list = []
 
371
dns_list = []
 
372
 
 
373
# read each line, and check that the DNS name exists
 
374
for line in file:
 
375
    line = line.strip()
 
376
    if line == '' or line[0] == "#":
 
377
        continue
 
378
    d = parse_dns_line(line, sub_vars)
 
379
    dns_list.append(d)
 
380
 
 
381
# now expand the entries, if any are A record with ip set to $IP
 
382
# then replace with multiple entries, one for each interface IP
 
383
for d in dns_list:
 
384
    if d.type == 'A' and d.ip == "$IP":
 
385
        d.ip = IPs[0]
 
386
        for i in range(len(IPs)-1):
 
387
            d2 = dnsobj(str(d))
 
388
            d2.ip = IPs[i+1]
 
389
            dns_list.append(d2)
 
390
 
 
391
# now check if the entries already exist on the DNS server
 
392
for d in dns_list:
 
393
    if opts.all_names or not check_dns_name(d):
 
394
        update_list.append(d)
 
395
 
 
396
if len(update_list) == 0:
 
397
    if opts.verbose:
 
398
        print "No DNS updates needed"
 
399
    sys.exit(0)
 
400
 
 
401
# get our krb5 creds
 
402
get_credentials(lp)
 
403
 
 
404
# ask nsupdate to add entries as needed
 
405
for d in update_list:
 
406
    if am_rodc:
 
407
        if d.name.lower() == domain.lower():
 
408
            continue
 
409
        if d.type != 'A':
 
410
            call_rodc_update(d)
 
411
        else:
 
412
            call_nsupdate(d)
 
413
    else:
 
414
        call_nsupdate(d)
 
415
 
 
416
# delete the ccache if we created it
 
417
if ccachename is not None:
 
418
    os.unlink(ccachename)
 
419
 
 
420
if error_count != 0:
 
421
    print("Failed update of %u entries" % error_count)
 
422
sys.exit(error_count)