netcmd: user: readpasswords: move getpassword command to readpasswords
[Samba.git] / python / samba / netcmd / user / readpasswords / __init__.py
blobc282d351aa8332224717c0eb462e0b3f9cd2c444
1 # user management
3 # user readpasswords commands
5 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
6 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 import base64
23 import errno
24 import fcntl
25 import os
26 import signal
27 import time
28 from subprocess import Popen, PIPE, STDOUT
30 import ldb
31 import samba.getopt as options
32 from samba import Ldb, dsdb
33 from samba.dcerpc import misc, security
34 from samba.ndr import ndr_unpack
35 from samba.common import get_bytes
36 from samba.netcmd import CommandError, Option
38 from .common import (
39 GetPasswordCommand,
40 gpg_decrypt,
41 decrypt_samba_gpg_help,
42 virtual_attributes_help
44 from .getpassword import cmd_user_getpassword
45 from .show import cmd_user_show
48 class cmd_user_syncpasswords(GetPasswordCommand):
49 """Sync the password of user accounts.
51 This syncs logon passwords for user accounts.
53 Note that this command should run on a single domain controller only
54 (typically the PDC-emulator). However the "password hash gpg key ids"
55 option should to be configured on all domain controllers.
57 The command must be run from the root user id or another authorized user id.
58 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
59 local path. By default, ldapi:// is used with the default path to the
60 privileged ldapi socket.
62 This command has three modes: "Cache Initialization", "Sync Loop Run" and
63 "Sync Loop Terminate".
66 Cache Initialization
67 ====================
69 The first time, this command needs to be called with
70 '--cache-ldb-initialize' in order to initialize its cache.
72 The cache initialization requires '--attributes' and allows the following
73 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
74 '-H/--URL'.
76 The '--attributes' parameter takes a comma separated list of attributes,
77 which will be printed or given to the script specified by '--script'. If a
78 specified attribute is not available on an object it will be silently omitted.
79 All attributes defined in the schema (e.g. the unicodePwd attribute holds
80 the NTHASH) and the following virtual attributes are possible (see '--help'
81 for supported virtual attributes in your environment):
83 virtualClearTextUTF16: The raw cleartext as stored in the
84 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
85 with '--decrypt-samba-gpg') buffer inside of the
86 supplementalCredentials attribute. This typically
87 contains valid UTF-16-LE, but may contain random
88 bytes, e.g. for computer accounts.
90 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
91 (only from valid UTF-16-LE).
93 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
94 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
96 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
97 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
98 with a $5$... salt, see crypt(3) on modern systems.
99 The number of rounds used to calculate the hash can
100 also be specified. By appending ";rounds=x" to the
101 attribute name i.e. virtualCryptSHA256;rounds=10000
102 will calculate a SHA256 hash with 10,000 rounds.
103 Non numeric values for rounds are silently ignored.
104 The value is calculated as follows:
105 1) If a value exists in 'Primary:userPassword' with
106 the specified number of rounds it is returned.
107 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG' with
108 '--decrypt-samba-gpg'. Calculate a hash with
109 the specified number of rounds
110 3) Return the first CryptSHA256 value in
111 'Primary:userPassword'.
113 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
114 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
115 with a $6$... salt, see crypt(3) on modern systems.
116 The number of rounds used to calculate the hash can
117 also be specified. By appending ";rounds=x" to the
118 attribute name i.e. virtualCryptSHA512;rounds=10000
119 will calculate a SHA512 hash with 10,000 rounds.
120 Non numeric values for rounds are silently ignored.
121 The value is calculated as follows:
122 1) If a value exists in 'Primary:userPassword' with
123 the specified number of rounds it is returned.
124 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG' with
125 '--decrypt-samba-gpg'. Calculate a hash with
126 the specified number of rounds.
127 3) Return the first CryptSHA512 value in
128 'Primary:userPassword'.
130 virtualWDigestNN: The individual hash values stored in
131 'Primary:WDigest' where NN is the hash number in
132 the range 01 to 29.
133 NOTE: As at 22-05-2017 the documentation:
134 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
135 https://msdn.microsoft.com/en-us/library/cc245680.aspx
136 is incorrect.
138 virtualKerberosSalt: This results the salt string that is used to compute
139 Kerberos keys from a UTF-8 cleartext password.
141 virtualSambaGPG: The raw cleartext as stored in the
142 'Primary:SambaGPG' buffer inside of the
143 supplementalCredentials attribute.
144 See the 'password hash gpg key ids' option in
145 smb.conf.
147 The '--decrypt-samba-gpg' option triggers decryption of the
148 Primary:SambaGPG buffer. Check with '--help' if this feature is available
149 in your environment or not (the python-gpgme package is required). Please
150 note that you might need to set the GNUPGHOME environment variable. If the
151 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
152 environment variable has been set correctly and the passphrase is already
153 known by the gpg-agent.
155 The '--script' option specifies a custom script that is called whenever any
156 of the dirsyncAttributes (see below) was changed. The script is called
157 without any arguments. It gets the LDIF for exactly one object on STDIN.
158 If the script processed the object successfully it has to respond with a
159 single line starting with 'DONE-EXIT: ' followed by an optional message.
161 Note that the script might be called without any password change, e.g. if
162 the account was disabled (a userAccountControl change) or the
163 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
164 are always returned as unique identifier of the account. It might be useful
165 to also ask for non-password attributes like: objectSid, sAMAccountName,
166 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
167 Depending on the object, some attributes may not be present/available,
168 but you always get the current state (and not a diff).
170 If no '--script' option is specified, the LDIF will be printed on STDOUT or
171 into the logfile.
173 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
174 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
175 (!(sAMAccountName=krbtgt*)))
176 This means only normal (non-krbtgt) user
177 accounts are monitored. The '--filter' can modify that, e.g. if it's
178 required to also sync computer accounts.
181 Sync Loop Run
182 =============
184 This (default) mode runs in an endless loop waiting for password related
185 changes in the active directory database. It makes use of the
186 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
187 get changes in a reliable fashion. Objects are monitored for changes of the
188 following dirsyncAttributes:
190 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
191 userPrincipalName and userAccountControl.
193 It recovers from LDAP disconnects and updates the cache in conservative way
194 (in single steps after each successfully processed change). An error from
195 the script (specified by '--script') will result in fatal error and this
196 command will exit. But the cache state should be still valid and can be
197 resumed in the next "Sync Loop Run".
199 The '--logfile' option specifies an optional (required if '--daemon' is
200 specified) logfile that takes all output of the command. The logfile is
201 automatically reopened if fstat returns st_nlink == 0.
203 The optional '--daemon' option will put the command into the background.
205 You can stop the command without the '--daemon' option, also by hitting
206 strg+c.
208 If you specify the '--no-wait' option the command skips the
209 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
210 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
212 Sync Loop Terminate
213 ===================
215 In order to terminate an already running command (likely as daemon) the
216 '--terminate' option can be used. This also requires the '--logfile' option
217 to be specified.
220 Example1:
221 samba-tool user syncpasswords --cache-ldb-initialize \\
222 --attributes=virtualClearTextUTF8
223 samba-tool user syncpasswords
225 Example2:
226 samba-tool user syncpasswords --cache-ldb-initialize \\
227 --attributes=objectGUID,objectSID,sAMAccountName,\\
228 userPrincipalName,userAccountControl,pwdLastSet,\\
229 msDS-KeyVersionNumber,virtualCryptSHA512 \\
230 --script=/path/to/my-custom-syncpasswords-script.py
231 samba-tool user syncpasswords --daemon \\
232 --logfile=/var/log/samba/user-syncpasswords.log
233 samba-tool user syncpasswords --terminate \\
234 --logfile=/var/log/samba/user-syncpasswords.log
237 def __init__(self):
238 super(cmd_user_syncpasswords, self).__init__()
240 synopsis = "%prog [--cache-ldb-initialize] [options]"
242 takes_optiongroups = {
243 "sambaopts": options.SambaOptions,
244 "versionopts": options.VersionOptions,
247 takes_options = [
248 Option("--cache-ldb-initialize",
249 help="Initialize the cache for the first time",
250 dest="cache_ldb_initialize", action="store_true"),
251 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
252 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
253 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
254 metavar="URL", dest="H"),
255 Option("--filter", help="optional LDAP filter to set password on", type=str,
256 metavar="LDAP-SEARCH-FILTER", dest="filter"),
257 Option("--attributes", type=str,
258 help=virtual_attributes_help,
259 metavar="ATTRIBUTELIST", dest="attributes"),
260 Option("--decrypt-samba-gpg",
261 help=decrypt_samba_gpg_help,
262 action="store_true", default=False, dest="decrypt_samba_gpg"),
263 Option("--script", help="Script that is called for each password change", type=str,
264 metavar="/path/to/syncpasswords.script", dest="script"),
265 Option("--no-wait", help="Don't block waiting for changes",
266 action="store_true", default=False, dest="nowait"),
267 Option("--logfile", type=str,
268 help="The logfile to use (required in --daemon mode).",
269 metavar="/path/to/syncpasswords.log", dest="logfile"),
270 Option("--daemon", help="daemonize after initial setup",
271 action="store_true", default=False, dest="daemon"),
272 Option("--terminate",
273 help="Send a SIGTERM to an already running (daemon) process",
274 action="store_true", default=False, dest="terminate"),
277 def run(self, cache_ldb_initialize=False, cache_ldb=None,
278 H=None, filter=None,
279 attributes=None, decrypt_samba_gpg=None,
280 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
281 sambaopts=None, versionopts=None):
283 self.lp = sambaopts.get_loadparm()
284 self.logfile = None
285 self.samdb_url = None
286 self.samdb = None
287 self.cache = None
289 if not cache_ldb_initialize:
290 if attributes is not None:
291 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
292 if decrypt_samba_gpg:
293 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
294 if script is not None:
295 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
296 if filter is not None:
297 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
298 if H is not None:
299 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
300 else:
301 if nowait is not False:
302 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
303 if logfile is not None:
304 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
305 if daemon is not False:
306 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
307 if terminate is not False:
308 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
310 if nowait is True:
311 if daemon is True:
312 raise CommandError("--daemon is not allowed together with --no-wait")
313 if terminate is not False:
314 raise CommandError("--terminate is not allowed together with --no-wait")
316 if terminate is True and daemon is True:
317 raise CommandError("--terminate is not allowed together with --daemon")
319 if daemon is True and logfile is None:
320 raise CommandError("--daemon is only allowed together with --logfile")
322 if terminate is True and logfile is None:
323 raise CommandError("--terminate is only allowed together with --logfile")
325 if script is not None:
326 if not os.path.exists(script):
327 raise CommandError("script[%s] does not exist!" % script)
329 sync_command = "%s" % os.path.abspath(script)
330 else:
331 sync_command = None
333 dirsync_filter = filter
334 if dirsync_filter is None:
335 dirsync_filter = "(&" + \
336 "(objectClass=user)" + \
337 "(userAccountControl:%s:=%u)" % (
338 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
339 "(!(sAMAccountName=krbtgt*))" + \
342 dirsync_secret_attrs = [
343 "unicodePwd",
344 "dBCSPwd",
345 "supplementalCredentials",
348 dirsync_attrs = dirsync_secret_attrs + [
349 "pwdLastSet",
350 "sAMAccountName",
351 "userPrincipalName",
352 "userAccountControl",
353 "isDeleted",
354 "isRecycled",
357 password_attrs = None
359 if cache_ldb_initialize:
360 if H is None:
361 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
363 if decrypt_samba_gpg and not gpg_decrypt:
364 raise CommandError(decrypt_samba_gpg_help)
366 password_attrs = self.parse_attributes(attributes)
367 lower_attrs = [x.lower() for x in password_attrs]
368 # We always return these in order to track deletions
369 for a in ["objectGUID", "isDeleted", "isRecycled"]:
370 if a.lower() not in lower_attrs:
371 password_attrs += [a]
373 if cache_ldb is not None:
374 if cache_ldb.lower().startswith("ldapi://"):
375 raise CommandError("--cache_ldb ldapi:// is not supported")
376 elif cache_ldb.lower().startswith("ldap://"):
377 raise CommandError("--cache_ldb ldap:// is not supported")
378 elif cache_ldb.lower().startswith("ldaps://"):
379 raise CommandError("--cache_ldb ldaps:// is not supported")
380 elif cache_ldb.lower().startswith("tdb://"):
381 pass
382 else:
383 if not os.path.exists(cache_ldb):
384 cache_ldb = self.lp.private_path(cache_ldb)
385 else:
386 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
388 self.lockfile = "%s.pid" % cache_ldb
390 def log_msg(msg):
391 if self.logfile is not None:
392 info = os.fstat(0)
393 if info.st_nlink == 0:
394 logfile = self.logfile
395 self.logfile = None
396 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
397 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
398 os.dup2(logfd, 0)
399 os.dup2(logfd, 1)
400 os.dup2(logfd, 2)
401 os.close(logfd)
402 log_msg("Reopened logfile[%s]\n" % (logfile))
403 self.logfile = logfile
404 msg = "%s: pid[%d]: %s" % (
405 time.ctime(),
406 os.getpid(),
407 msg)
408 self.outf.write(msg)
409 return
411 def load_cache():
412 cache_attrs = [
413 "samdbUrl",
414 "dirsyncFilter",
415 "dirsyncAttribute",
416 "dirsyncControl",
417 "passwordAttribute",
418 "decryptSambaGPG",
419 "syncCommand",
420 "currentPid",
423 self.cache = Ldb(cache_ldb)
424 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
425 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
426 attrs=cache_attrs)
427 if len(res) == 1:
428 try:
429 self.samdb_url = str(res[0]["samdbUrl"][0])
430 except KeyError as e:
431 self.samdb_url = None
432 else:
433 self.samdb_url = None
434 if self.samdb_url is None and not cache_ldb_initialize:
435 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
436 cache_ldb))
437 if self.samdb_url is not None and cache_ldb_initialize:
438 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
439 cache_ldb))
440 if self.samdb_url is None:
441 self.samdb_url = H
442 self.dirsync_filter = dirsync_filter
443 self.dirsync_attrs = dirsync_attrs
444 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"]
445 self.password_attrs = password_attrs
446 self.decrypt_samba_gpg = decrypt_samba_gpg
447 self.sync_command = sync_command
448 add_ldif = "dn: %s\n" % self.cache_dn +\
449 "objectClass: userSyncPasswords\n" +\
450 "samdbUrl:: %s\n" % base64.b64encode(get_bytes(self.samdb_url)).decode('utf8') +\
451 "dirsyncFilter:: %s\n" % base64.b64encode(get_bytes(self.dirsync_filter)).decode('utf8') +\
452 "".join("dirsyncAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.dirsync_attrs) +\
453 "dirsyncControl: %s\n" % self.dirsync_controls[0] +\
454 "".join("passwordAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.password_attrs)
455 if self.decrypt_samba_gpg:
456 add_ldif += "decryptSambaGPG: TRUE\n"
457 else:
458 add_ldif += "decryptSambaGPG: FALSE\n"
459 if self.sync_command is not None:
460 add_ldif += "syncCommand: %s\n" % self.sync_command
461 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
462 self.cache.add_ldif(add_ldif)
463 self.current_pid = None
464 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
465 msgs = self.cache.parse_ldif(add_ldif)
466 changetype, msg = next(msgs)
467 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
468 self.outf.write("%s" % ldif)
469 else:
470 self.dirsync_filter = str(res[0]["dirsyncFilter"][0])
471 self.dirsync_attrs = []
472 for a in res[0]["dirsyncAttribute"]:
473 self.dirsync_attrs.append(str(a))
474 self.dirsync_controls = [str(res[0]["dirsyncControl"][0]), "extended_dn:1:0"]
475 self.password_attrs = []
476 for a in res[0]["passwordAttribute"]:
477 self.password_attrs.append(str(a))
478 decrypt_string = str(res[0]["decryptSambaGPG"][0])
479 assert(decrypt_string in ["TRUE", "FALSE"])
480 if decrypt_string == "TRUE":
481 self.decrypt_samba_gpg = True
482 else:
483 self.decrypt_samba_gpg = False
484 if "syncCommand" in res[0]:
485 self.sync_command = str(res[0]["syncCommand"][0])
486 else:
487 self.sync_command = None
488 if "currentPid" in res[0]:
489 self.current_pid = int(res[0]["currentPid"][0])
490 else:
491 self.current_pid = None
492 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
494 return
496 def run_sync_command(dn, ldif):
497 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
498 sync_command_p = Popen(self.sync_command,
499 stdin=PIPE,
500 stdout=PIPE,
501 stderr=STDOUT)
503 res = sync_command_p.poll()
504 assert res is None
506 input = "%s" % (ldif)
507 reply = sync_command_p.communicate(
508 input.encode('utf-8'))[0].decode('utf-8')
509 log_msg("%s\n" % (reply))
510 res = sync_command_p.poll()
511 if res is None:
512 sync_command_p.terminate()
513 res = sync_command_p.wait()
515 if reply.startswith("DONE-EXIT: "):
516 return
518 log_msg("RESULT: %s\n" % (res))
519 raise Exception("ERROR: %s - %s\n" % (res, reply))
521 def handle_object(idx, dirsync_obj):
522 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
523 guid = ndr_unpack(misc.GUID, binary_guid)
524 binary_sid = dirsync_obj.dn.get_extended_component("SID")
525 sid = ndr_unpack(security.dom_sid, binary_sid)
526 domain_sid, rid = sid.split()
527 if rid == security.DOMAIN_RID_KRBTGT:
528 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
529 return
530 for a in list(dirsync_obj.keys()):
531 for h in dirsync_secret_attrs:
532 if a.lower() == h.lower():
533 del dirsync_obj[a]
534 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
535 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
536 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
537 obj = self.get_account_attributes(self.samdb,
538 username="%s" % sid,
539 basedn="<GUID=%s>" % guid,
540 filter="(objectClass=user)",
541 scope=ldb.SCOPE_BASE,
542 attrs=self.password_attrs,
543 decrypt=self.decrypt_samba_gpg)
544 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
545 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
546 if self.sync_command is None:
547 self.outf.write("%s" % (ldif))
548 return
549 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
550 run_sync_command(obj.dn, ldif)
552 def check_current_pid_conflict(terminate):
553 flags = os.O_RDWR
554 if not terminate:
555 flags |= os.O_CREAT
557 try:
558 self.lockfd = os.open(self.lockfile, flags, 0o600)
559 except IOError as e4:
560 (err, msg) = e4.args
561 if err == errno.ENOENT:
562 if terminate:
563 return False
564 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
565 (self.lockfile, msg, err))
566 raise
568 got_exclusive = False
569 try:
570 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
571 got_exclusive = True
572 except IOError as e5:
573 (err, msg) = e5.args
574 if err != errno.EACCES and err != errno.EAGAIN:
575 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
576 (self.lockfile, msg, err))
577 raise
579 if not got_exclusive:
580 buf = os.read(self.lockfd, 64)
581 self.current_pid = None
582 try:
583 self.current_pid = int(buf)
584 except ValueError as e:
585 pass
586 if self.current_pid is not None:
587 return True
589 if got_exclusive and terminate:
590 try:
591 os.ftruncate(self.lockfd, 0)
592 except IOError as e2:
593 (err, msg) = e2.args
594 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
595 (self.lockfile, msg, err))
596 raise
597 os.close(self.lockfd)
598 self.lockfd = -1
599 return False
601 try:
602 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
603 except IOError as e6:
604 (err, msg) = e6.args
605 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
606 (self.lockfile, msg, err))
608 # We leave the function with the shared lock.
609 return False
611 def update_pid(pid):
612 if self.lockfd != -1:
613 got_exclusive = False
614 # Try 5 times to get the exclusive lock.
615 for i in range(0, 5):
616 try:
617 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
618 got_exclusive = True
619 except IOError as e:
620 (err, msg) = e.args
621 if err != errno.EACCES and err != errno.EAGAIN:
622 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
623 (pid, self.lockfile, msg, err))
624 raise
625 if got_exclusive:
626 break
627 time.sleep(1)
628 if not got_exclusive:
629 log_msg("update_pid(%r): failed to get exclusive lock[%s]" %
630 (pid, self.lockfile))
631 raise CommandError("update_pid(%r): failed to get "
632 "exclusive lock[%s] after 5 seconds" %
633 (pid, self.lockfile))
635 if pid is not None:
636 buf = "%d\n" % pid
637 else:
638 buf = None
639 try:
640 os.ftruncate(self.lockfd, 0)
641 if buf is not None:
642 os.write(self.lockfd, get_bytes(buf))
643 except IOError as e3:
644 (err, msg) = e3.args
645 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
646 (self.lockfile, msg, err))
647 raise
648 self.current_pid = pid
649 if self.current_pid is not None:
650 log_msg("currentPid: %d\n" % self.current_pid)
652 modify_ldif = "dn: %s\n" % (self.cache_dn) +\
653 "changetype: modify\n" +\
654 "replace: currentPid\n"
655 if self.current_pid is not None:
656 modify_ldif += "currentPid: %d\n" % (self.current_pid)
657 modify_ldif += "replace: currentTime\n" +\
658 "currentTime: %s\n" % ldb.timestring(int(time.time()))
659 self.cache.modify_ldif(modify_ldif)
660 return
662 def update_cache(res_controls):
663 assert len(res_controls) > 0
664 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
665 res_controls[0].critical = True
666 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
667 # This cookie can be extremely long
668 # log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
670 modify_ldif = "dn: %s\n" % (self.cache_dn) +\
671 "changetype: modify\n" +\
672 "replace: dirsyncControl\n" +\
673 "dirsyncControl: %s\n" % (self.dirsync_controls[0]) +\
674 "replace: currentTime\n" +\
675 "currentTime: %s\n" % ldb.timestring(int(time.time()))
676 self.cache.modify_ldif(modify_ldif)
677 return
679 def check_object(dirsync_obj, res_controls):
680 assert len(res_controls) > 0
681 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
683 binary_sid = dirsync_obj.dn.get_extended_component("SID")
684 sid = ndr_unpack(security.dom_sid, binary_sid)
685 dn = "KEY=%s" % sid
686 lastCookie = str(res_controls[0])
688 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
689 expression="(lastCookie=%s)" % (
690 ldb.binary_encode(lastCookie)),
691 attrs=[])
692 if len(res) == 1:
693 return True
694 return False
696 def update_object(dirsync_obj, res_controls):
697 assert len(res_controls) > 0
698 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
700 binary_sid = dirsync_obj.dn.get_extended_component("SID")
701 sid = ndr_unpack(security.dom_sid, binary_sid)
702 dn = "KEY=%s" % sid
703 lastCookie = str(res_controls[0])
705 self.cache.transaction_start()
706 try:
707 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
708 expression="(objectClass=*)",
709 attrs=["lastCookie"])
710 if len(res) == 0:
711 add_ldif = "dn: %s\n" % (dn) +\
712 "objectClass: userCookie\n" +\
713 "lastCookie: %s\n" % (lastCookie) +\
714 "currentTime: %s\n" % ldb.timestring(int(time.time()))
715 self.cache.add_ldif(add_ldif)
716 else:
717 modify_ldif = "dn: %s\n" % (dn) +\
718 "changetype: modify\n" +\
719 "replace: lastCookie\n" +\
720 "lastCookie: %s\n" % (lastCookie) +\
721 "replace: currentTime\n" +\
722 "currentTime: %s\n" % ldb.timestring(int(time.time()))
723 self.cache.modify_ldif(modify_ldif)
724 self.cache.transaction_commit()
725 except Exception as e:
726 self.cache.transaction_cancel()
728 return
730 def dirsync_loop():
731 while True:
732 res = self.samdb.search(expression=str(self.dirsync_filter),
733 scope=ldb.SCOPE_SUBTREE,
734 attrs=self.dirsync_attrs,
735 controls=self.dirsync_controls)
736 log_msg("dirsync_loop(): results %d\n" % len(res))
737 ri = 0
738 for r in res:
739 done = check_object(r, res.controls)
740 if not done:
741 handle_object(ri, r)
742 update_object(r, res.controls)
743 ri += 1
744 update_cache(res.controls)
745 if len(res) == 0:
746 break
748 def sync_loop(wait):
749 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
750 notify_controls = ["notification:1", "show_recycled:1"]
751 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
752 scope=ldb.SCOPE_SUBTREE,
753 attrs=notify_attrs,
754 controls=notify_controls,
755 timeout=-1)
757 if wait is True:
758 log_msg("Resuming monitoring\n")
759 else:
760 log_msg("Getting changes\n")
761 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
762 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
763 self.outf.write("syncCommand: %s\n" % self.sync_command)
764 dirsync_loop()
766 if wait is not True:
767 return
769 for msg in notify_handle:
770 if not isinstance(msg, ldb.Message):
771 self.outf.write("referral: %s\n" % msg)
772 continue
773 created = msg.get("uSNCreated")[0]
774 changed = msg.get("uSNChanged")[0]
775 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
776 (msg.dn, created, changed))
778 dirsync_loop()
780 res = notify_handle.result()
782 def daemonize():
783 self.samdb = None
784 self.cache = None
785 orig_pid = os.getpid()
786 pid = os.fork()
787 if pid == 0:
788 os.setsid()
789 pid = os.fork()
790 if pid == 0: # Actual daemon
791 pid = os.getpid()
792 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
793 load_cache()
794 return
795 os._exit(0)
797 if cache_ldb_initialize:
798 self.samdb_url = H
799 self.samdb = self.connect_system_samdb(url=self.samdb_url,
800 verbose=True)
801 load_cache()
802 return
804 if logfile is not None:
805 import resource # Resource usage information.
806 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
807 if maxfd == resource.RLIM_INFINITY:
808 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
809 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
810 self.outf.write("Using logfile[%s]\n" % logfile)
811 for fd in range(0, maxfd):
812 if fd == logfd:
813 continue
814 try:
815 os.close(fd)
816 except OSError:
817 pass
818 os.dup2(logfd, 0)
819 os.dup2(logfd, 1)
820 os.dup2(logfd, 2)
821 os.close(logfd)
822 log_msg("Attached to logfile[%s]\n" % (logfile))
823 self.logfile = logfile
825 load_cache()
826 conflict = check_current_pid_conflict(terminate)
827 if terminate:
828 if self.current_pid is None:
829 log_msg("No process running.\n")
830 return
831 if not conflict:
832 log_msg("Process %d is not running anymore.\n" % (
833 self.current_pid))
834 update_pid(None)
835 return
836 log_msg("Sending SIGTERM to process %d.\n" % (
837 self.current_pid))
838 os.kill(self.current_pid, signal.SIGTERM)
839 return
840 if conflict:
841 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
842 os.getpid(), self.current_pid))
844 if daemon is True:
845 daemonize()
846 update_pid(os.getpid())
848 wait = True
849 while wait is True:
850 retry_sleep_min = 1
851 retry_sleep_max = 600
852 if nowait is True:
853 wait = False
854 retry_sleep = 0
855 else:
856 retry_sleep = retry_sleep_min
858 while self.samdb is None:
859 if retry_sleep != 0:
860 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
861 time.sleep(retry_sleep)
862 retry_sleep = retry_sleep * 2
863 if retry_sleep >= retry_sleep_max:
864 retry_sleep = retry_sleep_max
865 log_msg("Connecting to '%s'\n" % self.samdb_url)
866 try:
867 self.samdb = self.connect_system_samdb(url=self.samdb_url)
868 except Exception as msg:
869 self.samdb = None
870 log_msg("Connect to samdb Exception => (%s)\n" % msg)
871 if wait is not True:
872 raise
874 try:
875 sync_loop(wait)
876 except ldb.LdbError as e7:
877 (enum, estr) = e7.args
878 self.samdb = None
879 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
881 update_pid(None)
882 return