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/>.
28 from subprocess
import Popen
, PIPE
, STDOUT
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
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".
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
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
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
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
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
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.
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
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.
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
221 samba-tool user syncpasswords --cache-ldb-initialize \\
222 --attributes=virtualClearTextUTF8
223 samba-tool user syncpasswords
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
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
,
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,
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()
285 self
.samdb_url
= 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")
299 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
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")
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
)
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
= [
345 "supplementalCredentials",
348 dirsync_attrs
= dirsync_secret_attrs
+ [
352 "userAccountControl",
357 password_attrs
= None
359 if cache_ldb_initialize
:
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://"):
383 if not os
.path
.exists(cache_ldb
):
384 cache_ldb
= self
.lp
.private_path(cache_ldb
)
386 cache_ldb
= self
.lp
.private_path("user-syncpasswords-cache.ldb")
388 self
.lockfile
= "%s.pid" % cache_ldb
391 if self
.logfile
is not None:
393 if info
.st_nlink
== 0:
394 logfile
= self
.logfile
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)
402 log_msg("Reopened logfile[%s]\n" % (logfile
))
403 self
.logfile
= logfile
404 msg
= "%s: pid[%d]: %s" % (
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
,
429 self
.samdb_url
= str(res
[0]["samdbUrl"][0])
430 except KeyError as e
:
431 self
.samdb_url
= None
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" % (
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" % (
440 if self
.samdb_url
is None:
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"
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
)
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
483 self
.decrypt_samba_gpg
= False
484 if "syncCommand" in res
[0]:
485 self
.sync_command
= str(res
[0]["syncCommand"][0])
487 self
.sync_command
= None
488 if "currentPid" in res
[0]:
489 self
.current_pid
= int(res
[0]["currentPid"][0])
491 self
.current_pid
= None
492 log_msg("Using cache_ldb[%s]\n" % (cache_ldb
))
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
,
503 res
= sync_command_p
.poll()
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()
512 sync_command_p
.terminate()
513 res
= sync_command_p
.wait()
515 if reply
.startswith("DONE-EXIT: "):
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
))
530 for a
in list(dirsync_obj
.keys()):
531 for h
in dirsync_secret_attrs
:
532 if a
.lower() == h
.lower():
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
,
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
))
549 self
.outf
.write("# attrs=%s\n" % (sorted(obj
.keys())))
550 run_sync_command(obj
.dn
, ldif
)
552 def check_current_pid_conflict(terminate
):
558 self
.lockfd
= os
.open(self
.lockfile
, flags
, 0o600)
559 except IOError as e4
:
561 if err
== errno
.ENOENT
:
564 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
565 (self
.lockfile
, msg
, err
))
568 got_exclusive
= False
570 fcntl
.lockf(self
.lockfd
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
572 except IOError as e5
:
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
))
579 if not got_exclusive
:
580 buf
= os
.read(self
.lockfd
, 64)
581 self
.current_pid
= None
583 self
.current_pid
= int(buf
)
584 except ValueError as e
:
586 if self
.current_pid
is not None:
589 if got_exclusive
and terminate
:
591 os
.ftruncate(self
.lockfd
, 0)
592 except IOError as e2
:
594 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
595 (self
.lockfile
, msg
, err
))
597 os
.close(self
.lockfd
)
602 fcntl
.lockf(self
.lockfd
, fcntl
.LOCK_SH
)
603 except IOError as e6
:
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.
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):
617 fcntl
.lockf(self
.lockfd
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
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
))
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
))
640 os
.ftruncate(self
.lockfd
, 0)
642 os
.write(self
.lockfd
, get_bytes(buf
))
643 except IOError as e3
:
645 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
646 (self
.lockfile
, msg
, err
))
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
)
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
)
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
)
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
)),
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
)
703 lastCookie
= str(res_controls
[0])
705 self
.cache
.transaction_start()
707 res
= self
.cache
.search(base
=dn
, scope
=ldb
.SCOPE_BASE
,
708 expression
="(objectClass=*)",
709 attrs
=["lastCookie"])
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
)
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()
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
))
739 done
= check_object(r
, res
.controls
)
742 update_object(r
, res
.controls
)
744 update_cache(res
.controls
)
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
,
754 controls
=notify_controls
,
758 log_msg("Resuming monitoring\n")
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
)
769 for msg
in notify_handle
:
770 if not isinstance(msg
, ldb
.Message
):
771 self
.outf
.write("referral: %s\n" % msg
)
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
))
780 res
= notify_handle
.result()
785 orig_pid
= os
.getpid()
790 if pid
== 0: # Actual daemon
792 log_msg("Daemonized as pid %d (from %d)\n" % (pid
, orig_pid
))
797 if cache_ldb_initialize
:
799 self
.samdb
= self
.connect_system_samdb(url
=self
.samdb_url
,
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
):
822 log_msg("Attached to logfile[%s]\n" % (logfile
))
823 self
.logfile
= logfile
826 conflict
= check_current_pid_conflict(terminate
)
828 if self
.current_pid
is None:
829 log_msg("No process running.\n")
832 log_msg("Process %d is not running anymore.\n" % (
836 log_msg("Sending SIGTERM to process %d.\n" % (
838 os
.kill(self
.current_pid
, signal
.SIGTERM
)
841 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
842 os
.getpid(), self
.current_pid
))
846 update_pid(os
.getpid())
851 retry_sleep_max
= 600
856 retry_sleep
= retry_sleep_min
858 while self
.samdb
is None:
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
)
867 self
.samdb
= self
.connect_system_samdb(url
=self
.samdb_url
)
868 except Exception as msg
:
870 log_msg("Connect to samdb Exception => (%s)\n" % msg
)
876 except ldb
.LdbError
as e7
:
877 (enum
, estr
) = e7
.args
879 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum
, estr
))