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