mdb_util: Better error message if lmdb-utils not installed
[amitay/samba.git] / python / samba / upgradehelpers.py
1 # Helpers for provision stuff
2 # Copyright (C) Matthieu Patou <mat@matws.net> 2009-2012
3 #
4 # Based on provision a Samba4 server by
5 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008
6 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008
7 #
8 #
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
22 from __future__ import print_function
23 from __future__ import division
24 """Helpers used for upgrading between different database formats."""
25
26 import os
27 import re
28 import shutil
29 import samba
30
31 from samba.compat import cmp_fn
32 from samba import Ldb, version, ntacls
33 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE
34 import ldb
35 from samba.provision import (provision_paths_from_lp,
36                              getpolicypath, create_gpo_struct,
37                              provision, ProvisioningError,
38                              secretsdb_self_join)
39 from samba.provision.common import FILL_FULL
40 from samba.dcerpc import xattr, drsblobs, security
41 from samba.dcerpc.misc import SEC_CHAN_BDC
42 from samba.ndr import ndr_unpack
43 from samba.samdb import SamDB
44 from samba import _glue
45 import tempfile
46
47 # All the ldb related to registry are commented because the path for them is
48 # relative in the provisionPath object
49 # And so opening them create a file in the current directory which is not what
50 # we want
51 # I still keep them commented because I plan soon to make more cleaner
52 ERROR = -1
53 SIMPLE = 0x00
54 CHANGE = 0x01
55 CHANGESD = 0x02
56 GUESS = 0x04
57 PROVISION = 0x08
58 CHANGEALL = 0xff
59
60 hashAttrNotCopied = set(["dn", "whenCreated", "whenChanged", "objectGUID",
61                          "uSNCreated", "replPropertyMetaData", "uSNChanged", "parentGUID",
62                          "objectCategory", "distinguishedName", "nTMixedDomain",
63                          "showInAdvancedViewOnly", "instanceType", "msDS-Behavior-Version",
64                          "nextRid", "cn", "versionNumber", "lmPwdHistory", "pwdLastSet",
65                          "ntPwdHistory", "unicodePwd", "dBCSPwd", "supplementalCredentials",
66                          "gPCUserExtensionNames", "gPCMachineExtensionNames", "maxPwdAge", "secret",
67                          "possibleInferiors", "privilege", "sAMAccountType"])
68
69
70 class ProvisionLDB(object):
71
72     def __init__(self):
73         self.sam = None
74         self.secrets = None
75         self.idmap = None
76         self.privilege = None
77         self.hkcr = None
78         self.hkcu = None
79         self.hku = None
80         self.hklm = None
81
82     def dbs(self):
83         return (self.sam, self.secrets, self.idmap, self.privilege)
84
85     def startTransactions(self):
86         for db in self.dbs():
87             db.transaction_start()
88 # TO BE DONE
89 #        self.hkcr.transaction_start()
90 #        self.hkcu.transaction_start()
91 #        self.hku.transaction_start()
92 #        self.hklm.transaction_start()
93
94     def groupedRollback(self):
95         ok = True
96         for db in self.dbs():
97             try:
98                 db.transaction_cancel()
99             except Exception:
100                 ok = False
101         return ok
102 # TO BE DONE
103 #        self.hkcr.transaction_cancel()
104 #        self.hkcu.transaction_cancel()
105 #        self.hku.transaction_cancel()
106 #        self.hklm.transaction_cancel()
107
108     def groupedCommit(self):
109         try:
110             for db in self.dbs():
111                 db.transaction_prepare_commit()
112         except Exception:
113             return self.groupedRollback()
114 # TO BE DONE
115 #        self.hkcr.transaction_prepare_commit()
116 #        self.hkcu.transaction_prepare_commit()
117 #        self.hku.transaction_prepare_commit()
118 #        self.hklm.transaction_prepare_commit()
119         try:
120             for db in self.dbs():
121                 db.transaction_commit()
122         except Exception:
123             return self.groupedRollback()
124
125 # TO BE DONE
126 #        self.hkcr.transaction_commit()
127 #        self.hkcu.transaction_commit()
128 #        self.hku.transaction_commit()
129 #        self.hklm.transaction_commit()
130         return True
131
132
133 def get_ldbs(paths, creds, session, lp):
134     """Return LDB object mapped on most important databases
135
136     :param paths: An object holding the different importants paths for provision object
137     :param creds: Credential used for openning LDB files
138     :param session: Session to use for openning LDB files
139     :param lp: A loadparam object
140     :return: A ProvisionLDB object that contains LDB object for the different LDB files of the provision"""
141
142     ldbs = ProvisionLDB()
143
144     ldbs.sam = SamDB(paths.samdb,
145                      session_info=session,
146                      credentials=creds,
147                      lp=lp,
148                      options=["modules:samba_dsdb"],
149                      flags=0)
150     ldbs.secrets = Ldb(paths.secrets, session_info=session, credentials=creds, lp=lp)
151     ldbs.idmap = Ldb(paths.idmapdb, session_info=session, credentials=creds, lp=lp)
152     ldbs.privilege = Ldb(paths.privilege, session_info=session, credentials=creds, lp=lp)
153 #    ldbs.hkcr = Ldb(paths.hkcr, session_info=session, credentials=creds, lp=lp)
154 #    ldbs.hkcu = Ldb(paths.hkcu, session_info=session, credentials=creds, lp=lp)
155 #    ldbs.hku = Ldb(paths.hku, session_info=session, credentials=creds, lp=lp)
156 #    ldbs.hklm = Ldb(paths.hklm, session_info=session, credentials=creds, lp=lp)
157
158     return ldbs
159
160
161 def usn_in_range(usn, range):
162     """Check if the usn is in one of the range provided.
163     To do so, the value is checked to be between the lower bound and
164     higher bound of a range
165
166     :param usn: A integer value corresponding to the usn that we want to update
167     :param range: A list of integer representing ranges, lower bounds are in
168                   the even indices, higher in odd indices
169     :return: True if the usn is in one of the range, False otherwise
170     """
171
172     idx = 0
173     cont = True
174     ok = False
175     while cont:
176         if idx == len(range):
177             cont = False
178             continue
179         if usn < int(range[idx]):
180             if idx % 2 == 1:
181                 ok = True
182             cont = False
183         if usn == int(range[idx]):
184             cont = False
185             ok = True
186         idx = idx + 1
187     return ok
188
189
190 def get_paths(param, targetdir=None, smbconf=None):
191     """Get paths to important provision objects (smb.conf, ldb files, ...)
192
193     :param param: Param object
194     :param targetdir: Directory where the provision is (or will be) stored
195     :param smbconf: Path to the smb.conf file
196     :return: A list with the path of important provision objects"""
197     if targetdir is not None:
198         if not os.path.exists(targetdir):
199             os.mkdir(targetdir)
200         etcdir = os.path.join(targetdir, "etc")
201         if not os.path.exists(etcdir):
202             os.makedirs(etcdir)
203         smbconf = os.path.join(etcdir, "smb.conf")
204     if smbconf is None:
205         smbconf = param.default_path()
206
207     if not os.path.exists(smbconf):
208         raise ProvisioningError("Unable to find smb.conf at %s" % smbconf)
209
210     lp = param.LoadParm()
211     lp.load(smbconf)
212     paths = provision_paths_from_lp(lp, lp.get("realm"))
213     return paths
214
215
216 def update_policyids(names, samdb):
217     """Update policy ids that could have changed after sam update
218
219     :param names: List of key provision parameters
220     :param samdb: An Ldb object conntected with the sam DB
221     """
222     # policy guid
223     res = samdb.search(expression="(displayName=Default Domain Policy)",
224                        base="CN=Policies,CN=System," + str(names.rootdn),
225                        scope=SCOPE_ONELEVEL, attrs=["cn", "displayName"])
226     names.policyid = str(res[0]["cn"]).replace("{", "").replace("}", "")
227     # dc policy guid
228     res2 = samdb.search(expression="(displayName=Default Domain Controllers"
229                                    " Policy)",
230                         base="CN=Policies,CN=System," + str(names.rootdn),
231                         scope=SCOPE_ONELEVEL, attrs=["cn", "displayName"])
232     if len(res2) == 1:
233         names.policyid_dc = str(res2[0]["cn"]).replace("{", "").replace("}", "")
234     else:
235         names.policyid_dc = None
236
237
238 def newprovision(names, session, smbconf, provdir, logger, base_schema=None):
239     """Create a new provision.
240
241     This provision will be the reference for knowing what has changed in the
242     since the latest upgrade in the current provision
243
244     :param names: List of provision parameters
245     :param creds: Credentials for the authentification
246     :param session: Session object
247     :param smbconf: Path to the smb.conf file
248     :param provdir: Directory where the provision will be stored
249     :param logger: A Logger
250     """
251     if os.path.isdir(provdir):
252         shutil.rmtree(provdir)
253     os.mkdir(provdir)
254     logger.info("Provision stored in %s", provdir)
255     return provision(logger, session, smbconf=smbconf,
256                      targetdir=provdir, samdb_fill=FILL_FULL, realm=names.realm,
257                      domain=names.domain, domainguid=names.domainguid,
258                      domainsid=names.domainsid, ntdsguid=names.ntdsguid,
259                      policyguid=names.policyid, policyguid_dc=names.policyid_dc,
260                      hostname=names.netbiosname.lower(), hostip=None, hostip6=None,
261                      invocationid=names.invocation, adminpass=names.adminpass,
262                      krbtgtpass=None, machinepass=None, dnspass=None, root=None,
263                      nobody=None, users=None,
264                      serverrole="domain controller",
265                      backend_type=None, ldapadminpass=None, ol_mmr_urls=None,
266                      slapd_path=None,
267                      dom_for_fun_level=names.domainlevel, dns_backend=names.dns_backend,
268                      useeadb=True, use_ntvfs=True, base_schema=base_schema)
269
270
271 def dn_sort(x, y):
272     """Sorts two DNs in the lexicographical order it and put higher level DN
273     before.
274
275     So given the dns cn=bar,cn=foo and cn=foo the later will be return as
276     smaller
277
278     :param x: First object to compare
279     :param y: Second object to compare
280     """
281     p = re.compile(r'(?<!\\), ?')
282     tab1 = p.split(str(x))
283     tab2 = p.split(str(y))
284     minimum = min(len(tab1), len(tab2))
285     len1 = len(tab1) - 1
286     len2 = len(tab2) - 1
287     # Note: python range go up to upper limit but do not include it
288     for i in range(0, minimum):
289         ret = cmp_fn(tab1[len1 - i], tab2[len2 - i])
290         if ret != 0:
291             return ret
292         else:
293             if i == minimum - 1:
294                 assert len1 != len2, "PB PB PB" + " ".join(tab1) + " / " + " ".join(tab2)
295                 if len1 > len2:
296                     return 1
297                 else:
298                     return -1
299     return ret
300
301
302 def identic_rename(ldbobj, dn):
303     """Perform a back and forth rename to trigger renaming on attribute that
304     can't be directly modified.
305
306     :param lbdobj: An Ldb Object
307     :param dn: DN of the object to manipulate
308     """
309     (before, after) = str(dn).split('=', 1)
310     # we need to use relax to avoid the subtree_rename constraints
311     ldbobj.rename(dn, ldb.Dn(ldbobj, "%s=foo%s" % (before, after)), ["relax:0"])
312     ldbobj.rename(ldb.Dn(ldbobj, "%s=foo%s" % (before, after)), dn, ["relax:0"])
313
314
315 def update_secrets(newsecrets_ldb, secrets_ldb, messagefunc):
316     """Update secrets.ldb
317
318     :param newsecrets_ldb: An LDB object that is connected to the secrets.ldb
319         of the reference provision
320     :param secrets_ldb: An LDB object that is connected to the secrets.ldb
321         of the updated provision
322     """
323
324     messagefunc(SIMPLE, "Update of secrets.ldb")
325     reference = newsecrets_ldb.search(base="@MODULES", scope=SCOPE_BASE)
326     current = secrets_ldb.search(base="@MODULES", scope=SCOPE_BASE)
327     assert reference, "Reference modules list can not be empty"
328     if len(current) == 0:
329         # No modules present
330         delta = secrets_ldb.msg_diff(ldb.Message(), reference[0])
331         delta.dn = reference[0].dn
332         secrets_ldb.add(reference[0])
333     else:
334         delta = secrets_ldb.msg_diff(current[0], reference[0])
335         delta.dn = current[0].dn
336         secrets_ldb.modify(delta)
337
338     reference = newsecrets_ldb.search(expression="objectClass=top", base="",
339                                       scope=SCOPE_SUBTREE, attrs=["dn"])
340     current = secrets_ldb.search(expression="objectClass=top", base="",
341                                  scope=SCOPE_SUBTREE, attrs=["dn"])
342     hash_new = {}
343     hash = {}
344     listMissing = []
345     listPresent = []
346
347     empty = ldb.Message()
348     for i in range(0, len(reference)):
349         hash_new[str(reference[i]["dn"]).lower()] = reference[i]["dn"]
350
351     # Create a hash for speeding the search of existing object in the
352     # current provision
353     for i in range(0, len(current)):
354         hash[str(current[i]["dn"]).lower()] = current[i]["dn"]
355
356     for k in hash_new.keys():
357         if k not in hash:
358             listMissing.append(hash_new[k])
359         else:
360             listPresent.append(hash_new[k])
361
362     for entry in listMissing:
363         reference = newsecrets_ldb.search(expression="distinguishedName=%s" % entry,
364                                           base="", scope=SCOPE_SUBTREE)
365         current = secrets_ldb.search(expression="distinguishedName=%s" % entry,
366                                      base="", scope=SCOPE_SUBTREE)
367         delta = secrets_ldb.msg_diff(empty, reference[0])
368         for att in hashAttrNotCopied:
369             delta.remove(att)
370         messagefunc(CHANGE, "Entry %s is missing from secrets.ldb" %
371                     reference[0].dn)
372         for att in delta:
373             messagefunc(CHANGE, " Adding attribute %s" % att)
374         delta.dn = reference[0].dn
375         secrets_ldb.add(delta)
376
377     for entry in listPresent:
378         reference = newsecrets_ldb.search(expression="distinguishedName=%s" % entry,
379                                           base="", scope=SCOPE_SUBTREE)
380         current = secrets_ldb.search(expression="distinguishedName=%s" % entry, base="",
381                                      scope=SCOPE_SUBTREE)
382         delta = secrets_ldb.msg_diff(current[0], reference[0])
383         for att in hashAttrNotCopied:
384             delta.remove(att)
385         for att in delta:
386             if att == "name":
387                 messagefunc(CHANGE, "Found attribute name on  %s,"
388                                     " must rename the DN" % (current[0].dn))
389                 identic_rename(secrets_ldb, reference[0].dn)
390             else:
391                 delta.remove(att)
392
393     for entry in listPresent:
394         reference = newsecrets_ldb.search(expression="distinguishedName=%s" % entry, base="",
395                                           scope=SCOPE_SUBTREE)
396         current = secrets_ldb.search(expression="distinguishedName=%s" % entry, base="",
397                                      scope=SCOPE_SUBTREE)
398         delta = secrets_ldb.msg_diff(current[0], reference[0])
399         for att in hashAttrNotCopied:
400             delta.remove(att)
401         for att in delta:
402             if att == "msDS-KeyVersionNumber":
403                 delta.remove(att)
404             if att != "dn":
405                 messagefunc(CHANGE,
406                             "Adding/Changing attribute %s to %s" %
407                             (att, current[0].dn))
408
409         delta.dn = current[0].dn
410         secrets_ldb.modify(delta)
411
412     res2 = secrets_ldb.search(expression="(samaccountname=dns)",
413                               scope=SCOPE_SUBTREE, attrs=["dn"])
414
415     if len(res2) == 1:
416             messagefunc(SIMPLE, "Remove old dns account")
417             secrets_ldb.delete(res2[0]["dn"])
418
419
420 def getOEMInfo(samdb, rootdn):
421     """Return OEM Information on the top level Samba4 use to store version
422     info in this field
423
424     :param samdb: An LDB object connect to sam.ldb
425     :param rootdn: Root DN of the domain
426     :return: The content of the field oEMInformation (if any)
427     """
428     res = samdb.search(expression="(objectClass=*)", base=str(rootdn),
429                        scope=SCOPE_BASE, attrs=["dn", "oEMInformation"])
430     if len(res) > 0 and res[0].get("oEMInformation"):
431         info = res[0]["oEMInformation"]
432         return info
433     else:
434         return ""
435
436
437 def updateOEMInfo(samdb, rootdn):
438     """Update the OEMinfo field to add information about upgrade
439
440     :param samdb: an LDB object connected to the sam DB
441     :param rootdn: The string representation of the root DN of
442         the provision (ie. DC=...,DC=...)
443     """
444     res = samdb.search(expression="(objectClass=*)", base=rootdn,
445                        scope=SCOPE_BASE, attrs=["dn", "oEMInformation"])
446     if len(res) > 0:
447         if res[0].get("oEMInformation"):
448             info = str(res[0]["oEMInformation"])
449         else:
450             info = ""
451         info = "%s, upgrade to %s" % (info, version)
452         delta = ldb.Message()
453         delta.dn = ldb.Dn(samdb, str(res[0]["dn"]))
454         delta["oEMInformation"] = ldb.MessageElement(info, ldb.FLAG_MOD_REPLACE,
455                                                      "oEMInformation")
456         samdb.modify(delta)
457
458
459 def update_gpo(paths, samdb, names, lp, message):
460     """Create missing GPO file object if needed
461     """
462     dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid)
463     if not os.path.isdir(dir):
464         create_gpo_struct(dir)
465
466     if names.policyid_dc is None:
467         raise ProvisioningError("Policy ID for Domain controller is missing")
468     dir = getpolicypath(paths.sysvol, names.dnsdomain, names.policyid_dc)
469     if not os.path.isdir(dir):
470         create_gpo_struct(dir)
471
472
473 def increment_calculated_keyversion_number(samdb, rootdn, hashDns):
474     """For a given hash associating dn and a number, this function will
475     update the replPropertyMetaData of each dn in the hash, so that the
476     calculated value of the msDs-KeyVersionNumber is equal or superior to the
477     one associated to the given dn.
478
479     :param samdb: An SamDB object pointing to the sam
480     :param rootdn: The base DN where we want to start
481     :param hashDns: A hash with dn as key and number representing the
482                  minimum value of msDs-KeyVersionNumber that we want to
483                  have
484     """
485     entry = samdb.search(expression='(objectClass=user)',
486                          base=ldb.Dn(samdb, str(rootdn)),
487                          scope=SCOPE_SUBTREE, attrs=["msDs-KeyVersionNumber"],
488                          controls=["search_options:1:2"])
489     done = 0
490     if len(entry) == 0:
491         raise ProvisioningError("Unable to find msDs-KeyVersionNumber")
492     else:
493         for e in entry:
494             if str(e.dn).lower() in hashDns:
495                 val = e.get("msDs-KeyVersionNumber")
496                 if not val:
497                     val = "0"
498                 version = int(str(hashDns[str(e.dn).lower()]))
499                 if int(str(val)) < version:
500                     done = done + 1
501                     samdb.set_attribute_replmetadata_version(str(e.dn),
502                                                              "unicodePwd",
503                                                              version, True)
504
505
506 def delta_update_basesamdb(refsampath, sampath, creds, session, lp, message):
507     """Update the provision container db: sam.ldb
508     This function is aimed for alpha9 and newer;
509
510     :param refsampath: Path to the samdb in the reference provision
511     :param sampath: Path to the samdb in the upgraded provision
512     :param creds: Credential used for openning LDB files
513     :param session: Session to use for openning LDB files
514     :param lp: A loadparam object
515     :return: A msg_diff object with the difference between the @ATTRIBUTES
516              of the current provision and the reference provision
517     """
518
519     message(SIMPLE,
520             "Update base samdb by searching difference with reference one")
521     refsam = Ldb(refsampath, session_info=session, credentials=creds,
522                  lp=lp, options=["modules:"])
523     sam = Ldb(sampath, session_info=session, credentials=creds, lp=lp,
524               options=["modules:"])
525
526     empty = ldb.Message()
527     deltaattr = None
528     reference = refsam.search(expression="")
529
530     for refentry in reference:
531         entry = sam.search(expression="distinguishedName=%s" % refentry["dn"],
532                            scope=SCOPE_SUBTREE)
533         if not len(entry):
534             delta = sam.msg_diff(empty, refentry)
535             message(CHANGE, "Adding %s to sam db" % str(refentry.dn))
536             if str(refentry.dn) == "@PROVISION" and\
537                     delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE):
538                 delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE)
539             delta.dn = refentry.dn
540             sam.add(delta)
541         else:
542             delta = sam.msg_diff(entry[0], refentry)
543             if str(refentry.dn) == "@ATTRIBUTES":
544                 deltaattr = sam.msg_diff(refentry, entry[0])
545             if str(refentry.dn) == "@PROVISION" and\
546                     delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE):
547                 delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE)
548             if len(delta.items()) > 1:
549                 delta.dn = refentry.dn
550                 sam.modify(delta)
551
552     return deltaattr
553
554
555 def construct_existor_expr(attrs):
556     """Construct a exists or LDAP search expression.
557
558     :param attrs: List of attribute on which we want to create the search
559         expression.
560     :return: A string representing the expression, if attrs is empty an
561         empty string is returned
562     """
563     expr = ""
564     if len(attrs) > 0:
565         expr = "(|"
566         for att in attrs:
567             expr = "%s(%s=*)" %(expr, att)
568         expr = "%s)" %expr
569     return expr
570
571
572 def update_machine_account_password(samdb, secrets_ldb, names):
573     """Update (change) the password of the current DC both in the SAM db and in
574        secret one
575
576     :param samdb: An LDB object related to the sam.ldb file of a given provision
577     :param secrets_ldb: An LDB object related to the secrets.ldb file of a given
578                         provision
579     :param names: List of key provision parameters"""
580
581     expression = "samAccountName=%s$" % names.netbiosname
582     secrets_msg = secrets_ldb.search(expression=expression,
583                                      attrs=["secureChannelType"])
584     if int(secrets_msg[0]["secureChannelType"][0]) == SEC_CHAN_BDC:
585         res = samdb.search(expression=expression, attrs=[])
586         assert(len(res) == 1)
587
588         msg = ldb.Message(res[0].dn)
589         machinepass = samba.generate_random_machine_password(128, 255)
590         mputf16 = machinepass.encode('utf-16-le')
591         msg["clearTextPassword"] = ldb.MessageElement(mputf16,
592                                                       ldb.FLAG_MOD_REPLACE,
593                                                       "clearTextPassword")
594         samdb.modify(msg)
595
596         res = samdb.search(expression=("samAccountName=%s$" % names.netbiosname),
597                            attrs=["msDs-keyVersionNumber"])
598         assert(len(res) == 1)
599         kvno = int(str(res[0]["msDs-keyVersionNumber"]))
600         secChanType = int(secrets_msg[0]["secureChannelType"][0])
601
602         secretsdb_self_join(secrets_ldb, domain=names.domain,
603                             realm=names.realm,
604                             domainsid=names.domainsid,
605                             dnsdomain=names.dnsdomain,
606                             netbiosname=names.netbiosname,
607                             machinepass=machinepass,
608                             key_version_number=kvno,
609                             secure_channel_type=secChanType)
610     else:
611         raise ProvisioningError("Unable to find a Secure Channel"
612                                 "of type SEC_CHAN_BDC")
613
614
615 def update_dns_account_password(samdb, secrets_ldb, names):
616     """Update (change) the password of the dns both in the SAM db and in
617        secret one
618
619     :param samdb: An LDB object related to the sam.ldb file of a given provision
620     :param secrets_ldb: An LDB object related to the secrets.ldb file of a given
621                         provision
622     :param names: List of key provision parameters"""
623
624     expression = "samAccountName=dns-%s" % names.netbiosname
625     secrets_msg = secrets_ldb.search(expression=expression)
626     if len(secrets_msg) == 1:
627         res = samdb.search(expression=expression, attrs=[])
628         assert(len(res) == 1)
629
630         msg = ldb.Message(res[0].dn)
631         machinepass = samba.generate_random_password(128, 255)
632         mputf16 = machinepass.encode('utf-16-le')
633         msg["clearTextPassword"] = ldb.MessageElement(mputf16,
634                                                       ldb.FLAG_MOD_REPLACE,
635                                                       "clearTextPassword")
636
637         samdb.modify(msg)
638
639         res = samdb.search(expression=expression,
640                            attrs=["msDs-keyVersionNumber"])
641         assert(len(res) == 1)
642         kvno = str(res[0]["msDs-keyVersionNumber"])
643
644         msg = ldb.Message(secrets_msg[0].dn)
645         msg["secret"] = ldb.MessageElement(machinepass,
646                                            ldb.FLAG_MOD_REPLACE,
647                                            "secret")
648         msg["msDS-KeyVersionNumber"] = ldb.MessageElement(kvno,
649                                                           ldb.FLAG_MOD_REPLACE,
650                                                           "msDS-KeyVersionNumber")
651
652         secrets_ldb.modify(msg)
653
654
655 def update_krbtgt_account_password(samdb):
656     """Update (change) the password of the krbtgt account
657
658     :param samdb: An LDB object related to the sam.ldb file of a given provision"""
659
660     expression = "samAccountName=krbtgt"
661     res = samdb.search(expression=expression, attrs=[])
662     assert(len(res) == 1)
663
664     msg = ldb.Message(res[0].dn)
665     machinepass = samba.generate_random_machine_password(128, 255)
666     mputf16 = machinepass.encode('utf-16-le')
667     msg["clearTextPassword"] = ldb.MessageElement(mputf16,
668                                                   ldb.FLAG_MOD_REPLACE,
669                                                   "clearTextPassword")
670
671     samdb.modify(msg)
672
673
674 def search_constructed_attrs_stored(samdb, rootdn, attrs):
675     """Search a given sam DB for calculated attributes that are
676     still stored in the db.
677
678     :param samdb: An LDB object pointing to the sam
679     :param rootdn: The base DN where the search should start
680     :param attrs: A list of attributes to be searched
681     :return: A hash with attributes as key and an array of
682              array. Each array contains the dn and the associated
683              values for this attribute as they are stored in the
684              sam."""
685
686     hashAtt = {}
687     expr = construct_existor_expr(attrs)
688     if expr == "":
689         return hashAtt
690     entry = samdb.search(expression=expr, base=ldb.Dn(samdb, str(rootdn)),
691                          scope=SCOPE_SUBTREE, attrs=attrs,
692                          controls=["search_options:1:2", "bypassoperational:0"])
693     if len(entry) == 0:
694         # Nothing anymore
695         return hashAtt
696
697     for ent in entry:
698         for att in attrs:
699             if ent.get(att):
700                 if att in hashAtt:
701                     hashAtt[att][str(ent.dn).lower()] = str(ent[att])
702                 else:
703                     hashAtt[att] = {}
704                     hashAtt[att][str(ent.dn).lower()] = str(ent[att])
705
706     return hashAtt
707
708
709 def findprovisionrange(samdb, basedn):
710     """ Find ranges of usn grouped by invocation id and then by timestamp
711         rouned at 1 minute
712
713         :param samdb: An LDB object pointing to the samdb
714         :param basedn: The DN of the forest
715
716         :return: A two level dictionary with invoication id as the
717                 first level, timestamp as the second one and then
718                 max, min, and number as subkeys, representing respectivily
719                 the maximum usn for the range, the minimum usn and the number
720                 of object with usn in this range.
721     """
722     nb_obj = 0
723     hash_id = {}
724
725     res = samdb.search(base=basedn, expression="objectClass=*",
726                        scope=ldb.SCOPE_SUBTREE,
727                        attrs=["replPropertyMetaData"],
728                        controls=["search_options:1:2"])
729
730     for e in res:
731         nb_obj = nb_obj + 1
732         obj = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
733                          str(e["replPropertyMetaData"])).ctr
734
735         for o in obj.array:
736             # like a timestamp but with the resolution of 1 minute
737             minutestamp = _glue.nttime2unix(o.originating_change_time) // 60
738             hash_ts = hash_id.get(str(o.originating_invocation_id))
739
740             if hash_ts is None:
741                 ob = {}
742                 ob["min"] = o.originating_usn
743                 ob["max"] = o.originating_usn
744                 ob["num"] = 1
745                 ob["list"] = [str(e.dn)]
746                 hash_ts = {}
747             else:
748                 ob = hash_ts.get(minutestamp)
749                 if ob is None:
750                     ob = {}
751                     ob["min"] = o.originating_usn
752                     ob["max"] = o.originating_usn
753                     ob["num"] = 1
754                     ob["list"] = [str(e.dn)]
755                 else:
756                     if ob["min"] > o.originating_usn:
757                         ob["min"] = o.originating_usn
758                     if ob["max"] < o.originating_usn:
759                         ob["max"] = o.originating_usn
760                     if not (str(e.dn) in ob["list"]):
761                         ob["num"] = ob["num"] + 1
762                         ob["list"].append(str(e.dn))
763             hash_ts[minutestamp] = ob
764             hash_id[str(o.originating_invocation_id)] = hash_ts
765
766     return (hash_id, nb_obj)
767
768
769 def print_provision_ranges(dic, limit_print, dest, samdb_path, invocationid):
770     """ print the differents ranges passed as parameter
771
772         :param dic: A dictionnary as returned by findprovisionrange
773         :param limit_print: minimum number of object in a range in order to print it
774         :param dest: Destination directory
775         :param samdb_path: Path to the sam.ldb file
776         :param invoicationid: Invocation ID for the current provision
777     """
778     ldif = ""
779
780     for id in dic:
781         hash_ts = dic[id]
782         sorted_keys = []
783         sorted_keys.extend(hash_ts.keys())
784         sorted_keys.sort()
785
786         kept_record = []
787         for k in sorted_keys:
788             obj = hash_ts[k]
789             if obj["num"] > limit_print:
790                 dt = _glue.nttime2string(_glue.unix2nttime(k * 60))
791                 print("%s # of modification: %d  \tmin: %d max: %d" % (dt, obj["num"],
792                                                                        obj["min"],
793                                                                        obj["max"]))
794             if hash_ts[k]["num"] > 600:
795                 kept_record.append(k)
796
797         # Let's try to concatenate consecutive block if they are in the almost same minutestamp
798         for i in range(0, len(kept_record)):
799             if i != 0:
800                 key1 = kept_record[i]
801                 key2 = kept_record[i - 1]
802                 if key1 - key2 == 1:
803                     # previous record is just 1 minute away from current
804                     if int(hash_ts[key1]["min"]) == int(hash_ts[key2]["max"]) + 1:
805                         # Copy the highest USN in the previous record
806                         # and mark the current as skipped
807                         hash_ts[key2]["max"] = hash_ts[key1]["max"]
808                         hash_ts[key1]["skipped"] = True
809
810         for k in kept_record:
811                 obj = hash_ts[k]
812                 if obj.get("skipped") is None:
813                     ldif = "%slastProvisionUSN: %d-%d;%s\n" % (ldif, obj["min"],
814                                                                obj["max"], id)
815
816     if ldif != "":
817         fd, file = tempfile.mkstemp(dir=dest, prefix="usnprov", suffix=".ldif")
818         print()
819         print("To track the USNs modified/created by provision and upgrade proivsion,")
820         print(" the following ranges are proposed to be added to your provision sam.ldb: \n%s" % ldif)
821         print("We recommend to review them, and if it's correct to integrate the following ldif: %s in your sam.ldb" % file)
822         print("You can load this file like this: ldbadd -H %s %s\n" %(str(samdb_path), file))
823         ldif = "dn: @PROVISION\nprovisionnerID: %s\n%s" % (invocationid, ldif)
824         os.write(fd, ldif)
825         os.close(fd)
826
827
828 def int64range2str(value):
829     """Display the int64 range stored in value as xxx-yyy
830
831     :param value: The int64 range
832     :return: A string of the representation of the range
833     """
834     lvalue = int(value)
835     str = "%d-%d" % (lvalue &0xFFFFFFFF, lvalue >>32)
836     return str