PEP8: fix E302: expected 2 blank lines, found 1
[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, set_gpos_acl, create_gpo_struct,
37                              provision, ProvisioningError,
38                              setsysvolacl, 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     hashDone = {}
491     if len(entry) == 0:
492         raise ProvisioningError("Unable to find msDs-KeyVersionNumber")
493     else:
494         for e in entry:
495             if str(e.dn).lower() in hashDns:
496                 val = e.get("msDs-KeyVersionNumber")
497                 if not val:
498                     val = "0"
499                 version = int(str(hashDns[str(e.dn).lower()]))
500                 if int(str(val)) < version:
501                     done = done + 1
502                     samdb.set_attribute_replmetadata_version(str(e.dn),
503                                                              "unicodePwd",
504                                                              version, True)
505
506
507 def delta_update_basesamdb(refsampath, sampath, creds, session, lp, message):
508     """Update the provision container db: sam.ldb
509     This function is aimed for alpha9 and newer;
510
511     :param refsampath: Path to the samdb in the reference provision
512     :param sampath: Path to the samdb in the upgraded provision
513     :param creds: Credential used for openning LDB files
514     :param session: Session to use for openning LDB files
515     :param lp: A loadparam object
516     :return: A msg_diff object with the difference between the @ATTRIBUTES
517              of the current provision and the reference provision
518     """
519
520     message(SIMPLE,
521             "Update base samdb by searching difference with reference one")
522     refsam = Ldb(refsampath, session_info=session, credentials=creds,
523                  lp=lp, options=["modules:"])
524     sam = Ldb(sampath, session_info=session, credentials=creds, lp=lp,
525               options=["modules:"])
526
527     empty = ldb.Message()
528     deltaattr = None
529     reference = refsam.search(expression="")
530
531     for refentry in reference:
532         entry = sam.search(expression="distinguishedName=%s" % refentry["dn"],
533                            scope=SCOPE_SUBTREE)
534         if not len(entry):
535             delta = sam.msg_diff(empty, refentry)
536             message(CHANGE, "Adding %s to sam db" % str(refentry.dn))
537             if str(refentry.dn) == "@PROVISION" and\
538                     delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE):
539                 delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE)
540             delta.dn = refentry.dn
541             sam.add(delta)
542         else:
543             delta = sam.msg_diff(entry[0], refentry)
544             if str(refentry.dn) == "@ATTRIBUTES":
545                 deltaattr = sam.msg_diff(refentry, entry[0])
546             if str(refentry.dn) == "@PROVISION" and\
547                     delta.get(samba.provision.LAST_PROVISION_USN_ATTRIBUTE):
548                 delta.remove(samba.provision.LAST_PROVISION_USN_ATTRIBUTE)
549             if len(delta.items()) > 1:
550                 delta.dn = refentry.dn
551                 sam.modify(delta)
552
553     return deltaattr
554
555
556 def construct_existor_expr(attrs):
557     """Construct a exists or LDAP search expression.
558
559     :param attrs: List of attribute on which we want to create the search
560         expression.
561     :return: A string representing the expression, if attrs is empty an
562         empty string is returned
563     """
564     expr = ""
565     if len(attrs) > 0:
566         expr = "(|"
567         for att in attrs:
568             expr = "%s(%s=*)" %(expr, att)
569         expr = "%s)" %expr
570     return expr
571
572
573 def update_machine_account_password(samdb, secrets_ldb, names):
574     """Update (change) the password of the current DC both in the SAM db and in
575        secret one
576
577     :param samdb: An LDB object related to the sam.ldb file of a given provision
578     :param secrets_ldb: An LDB object related to the secrets.ldb file of a given
579                         provision
580     :param names: List of key provision parameters"""
581
582     expression = "samAccountName=%s$" % names.netbiosname
583     secrets_msg = secrets_ldb.search(expression=expression,
584                                      attrs=["secureChannelType"])
585     if int(secrets_msg[0]["secureChannelType"][0]) == SEC_CHAN_BDC:
586         res = samdb.search(expression=expression, attrs=[])
587         assert(len(res) == 1)
588
589         msg = ldb.Message(res[0].dn)
590         machinepass = samba.generate_random_machine_password(128, 255)
591         mputf16 = machinepass.encode('utf-16-le')
592         msg["clearTextPassword"] = ldb.MessageElement(mputf16,
593                                                       ldb.FLAG_MOD_REPLACE,
594                                                       "clearTextPassword")
595         samdb.modify(msg)
596
597         res = samdb.search(expression=("samAccountName=%s$" % names.netbiosname),
598                            attrs=["msDs-keyVersionNumber"])
599         assert(len(res) == 1)
600         kvno = int(str(res[0]["msDs-keyVersionNumber"]))
601         secChanType = int(secrets_msg[0]["secureChannelType"][0])
602
603         secretsdb_self_join(secrets_ldb, domain=names.domain,
604                             realm=names.realm,
605                             domainsid=names.domainsid,
606                             dnsdomain=names.dnsdomain,
607                             netbiosname=names.netbiosname,
608                             machinepass=machinepass,
609                             key_version_number=kvno,
610                             secure_channel_type=secChanType)
611     else:
612         raise ProvisioningError("Unable to find a Secure Channel"
613                                 "of type SEC_CHAN_BDC")
614
615
616 def update_dns_account_password(samdb, secrets_ldb, names):
617     """Update (change) the password of the dns both in the SAM db and in
618        secret one
619
620     :param samdb: An LDB object related to the sam.ldb file of a given provision
621     :param secrets_ldb: An LDB object related to the secrets.ldb file of a given
622                         provision
623     :param names: List of key provision parameters"""
624
625     expression = "samAccountName=dns-%s" % names.netbiosname
626     secrets_msg = secrets_ldb.search(expression=expression)
627     if len(secrets_msg) == 1:
628         res = samdb.search(expression=expression, attrs=[])
629         assert(len(res) == 1)
630
631         msg = ldb.Message(res[0].dn)
632         machinepass = samba.generate_random_password(128, 255)
633         mputf16 = machinepass.encode('utf-16-le')
634         msg["clearTextPassword"] = ldb.MessageElement(mputf16,
635                                                       ldb.FLAG_MOD_REPLACE,
636                                                       "clearTextPassword")
637
638         samdb.modify(msg)
639
640         res = samdb.search(expression=expression,
641                            attrs=["msDs-keyVersionNumber"])
642         assert(len(res) == 1)
643         kvno = str(res[0]["msDs-keyVersionNumber"])
644
645         msg = ldb.Message(secrets_msg[0].dn)
646         msg["secret"] = ldb.MessageElement(machinepass,
647                                            ldb.FLAG_MOD_REPLACE,
648                                            "secret")
649         msg["msDS-KeyVersionNumber"] = ldb.MessageElement(kvno,
650                                                           ldb.FLAG_MOD_REPLACE,
651                                                           "msDS-KeyVersionNumber")
652
653         secrets_ldb.modify(msg)
654
655
656 def update_krbtgt_account_password(samdb):
657     """Update (change) the password of the krbtgt account
658
659     :param samdb: An LDB object related to the sam.ldb file of a given provision"""
660
661     expression = "samAccountName=krbtgt"
662     res = samdb.search(expression=expression, attrs=[])
663     assert(len(res) == 1)
664
665     msg = ldb.Message(res[0].dn)
666     machinepass = samba.generate_random_machine_password(128, 255)
667     mputf16 = machinepass.encode('utf-16-le')
668     msg["clearTextPassword"] = ldb.MessageElement(mputf16,
669                                                   ldb.FLAG_MOD_REPLACE,
670                                                   "clearTextPassword")
671
672     samdb.modify(msg)
673
674
675 def search_constructed_attrs_stored(samdb, rootdn, attrs):
676     """Search a given sam DB for calculated attributes that are
677     still stored in the db.
678
679     :param samdb: An LDB object pointing to the sam
680     :param rootdn: The base DN where the search should start
681     :param attrs: A list of attributes to be searched
682     :return: A hash with attributes as key and an array of
683              array. Each array contains the dn and the associated
684              values for this attribute as they are stored in the
685              sam."""
686
687     hashAtt = {}
688     expr = construct_existor_expr(attrs)
689     if expr == "":
690         return hashAtt
691     entry = samdb.search(expression=expr, base=ldb.Dn(samdb, str(rootdn)),
692                          scope=SCOPE_SUBTREE, attrs=attrs,
693                          controls=["search_options:1:2", "bypassoperational:0"])
694     if len(entry) == 0:
695         # Nothing anymore
696         return hashAtt
697
698     for ent in entry:
699         for att in attrs:
700             if ent.get(att):
701                 if att in hashAtt:
702                     hashAtt[att][str(ent.dn).lower()] = str(ent[att])
703                 else:
704                     hashAtt[att] = {}
705                     hashAtt[att][str(ent.dn).lower()] = str(ent[att])
706
707     return hashAtt
708
709
710 def findprovisionrange(samdb, basedn):
711     """ Find ranges of usn grouped by invocation id and then by timestamp
712         rouned at 1 minute
713
714         :param samdb: An LDB object pointing to the samdb
715         :param basedn: The DN of the forest
716
717         :return: A two level dictionary with invoication id as the
718                 first level, timestamp as the second one and then
719                 max, min, and number as subkeys, representing respectivily
720                 the maximum usn for the range, the minimum usn and the number
721                 of object with usn in this range.
722     """
723     nb_obj = 0
724     hash_id = {}
725
726     res = samdb.search(base=basedn, expression="objectClass=*",
727                        scope=ldb.SCOPE_SUBTREE,
728                        attrs=["replPropertyMetaData"],
729                        controls=["search_options:1:2"])
730
731     for e in res:
732         nb_obj = nb_obj + 1
733         obj = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
734                          str(e["replPropertyMetaData"])).ctr
735
736         for o in obj.array:
737             # like a timestamp but with the resolution of 1 minute
738             minutestamp = _glue.nttime2unix(o.originating_change_time) // 60
739             hash_ts = hash_id.get(str(o.originating_invocation_id))
740
741             if hash_ts is None:
742                 ob = {}
743                 ob["min"] = o.originating_usn
744                 ob["max"] = o.originating_usn
745                 ob["num"] = 1
746                 ob["list"] = [str(e.dn)]
747                 hash_ts = {}
748             else:
749                 ob = hash_ts.get(minutestamp)
750                 if ob is None:
751                     ob = {}
752                     ob["min"] = o.originating_usn
753                     ob["max"] = o.originating_usn
754                     ob["num"] = 1
755                     ob["list"] = [str(e.dn)]
756                 else:
757                     if ob["min"] > o.originating_usn:
758                         ob["min"] = o.originating_usn
759                     if ob["max"] < o.originating_usn:
760                         ob["max"] = o.originating_usn
761                     if not (str(e.dn) in ob["list"]):
762                         ob["num"] = ob["num"] + 1
763                         ob["list"].append(str(e.dn))
764             hash_ts[minutestamp] = ob
765             hash_id[str(o.originating_invocation_id)] = hash_ts
766
767     return (hash_id, nb_obj)
768
769
770 def print_provision_ranges(dic, limit_print, dest, samdb_path, invocationid):
771     """ print the differents ranges passed as parameter
772
773         :param dic: A dictionnary as returned by findprovisionrange
774         :param limit_print: minimum number of object in a range in order to print it
775         :param dest: Destination directory
776         :param samdb_path: Path to the sam.ldb file
777         :param invoicationid: Invocation ID for the current provision
778     """
779     ldif = ""
780
781     for id in dic:
782         hash_ts = dic[id]
783         sorted_keys = []
784         sorted_keys.extend(hash_ts.keys())
785         sorted_keys.sort()
786
787         kept_record = []
788         for k in sorted_keys:
789             obj = hash_ts[k]
790             if obj["num"] > limit_print:
791                 dt = _glue.nttime2string(_glue.unix2nttime(k * 60))
792                 print("%s # of modification: %d  \tmin: %d max: %d" % (dt, obj["num"],
793                                                                        obj["min"],
794                                                                        obj["max"]))
795             if hash_ts[k]["num"] > 600:
796                 kept_record.append(k)
797
798         # Let's try to concatenate consecutive block if they are in the almost same minutestamp
799         for i in range(0, len(kept_record)):
800             if i != 0:
801                 key1 = kept_record[i]
802                 key2 = kept_record[i - 1]
803                 if key1 - key2 == 1:
804                     # previous record is just 1 minute away from current
805                     if int(hash_ts[key1]["min"]) == int(hash_ts[key2]["max"]) + 1:
806                         # Copy the highest USN in the previous record
807                         # and mark the current as skipped
808                         hash_ts[key2]["max"] = hash_ts[key1]["max"]
809                         hash_ts[key1]["skipped"] = True
810
811         for k in kept_record:
812                 obj = hash_ts[k]
813                 if obj.get("skipped") is None:
814                     ldif = "%slastProvisionUSN: %d-%d;%s\n" % (ldif, obj["min"],
815                                                                obj["max"], id)
816
817     if ldif != "":
818         file = tempfile.mktemp(dir=dest, prefix="usnprov", suffix=".ldif")
819         print()
820         print("To track the USNs modified/created by provision and upgrade proivsion,")
821         print(" the following ranges are proposed to be added to your provision sam.ldb: \n%s" % ldif)
822         print("We recommend to review them, and if it's correct to integrate the following ldif: %s in your sam.ldb" % file)
823         print("You can load this file like this: ldbadd -H %s %s\n" %(str(samdb_path), file))
824         ldif = "dn: @PROVISION\nprovisionnerID: %s\n%s" % (invocationid, ldif)
825         open(file, 'w').write(ldif)
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
835     lvalue = long(value)
836     str = "%d-%d" % (lvalue &0xFFFFFFFF, lvalue >>32)
837     return str