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