s4-python: Start using standard python logging infrastructure rather
[mat/samba.git] / source4 / scripting / bin / upgradeprovision
1 #!/usr/bin/env python
2 # vim: expandtab
3 #
4 # Copyright (C) Matthieu Patou <mat@matws.net> 2009
5 #
6 # Based on provision a Samba4 server by
7 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2008
8 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008
9 #
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
24
25 import shutil
26 import optparse
27 import os
28 import sys
29 import tempfile
30 # Allow to run from s4 source directory (without installing samba)
31 sys.path.insert(0, "bin/python")
32
33 import samba
34 import samba.getopt as options
35 from samba.credentials import DONT_USE_KERBEROS
36 from samba.auth import system_session, admin_session
37 from samba import Ldb, version
38 from ldb import (SCOPE_SUBTREE, SCOPE_BASE, FLAG_MOD_REPLACE,
39     FLAG_MOD_ADD, FLAG_MOD_DELETE, MessageElement, Message, Dn)
40 from samba import param
41 from samba.misc import messageEltFlagToString
42 from samba.provision import (find_setup_dir, get_domain_descriptor,
43     get_config_descriptor, secretsdb_self_join, set_gpo_acl, 
44     getpolicypath, create_gpo_struct, ProvisioningError)
45 from samba.schema import get_linked_attributes, Schema, get_schema_descriptor
46 from samba.dcerpc import security
47 from samba.ndr import ndr_unpack
48 from samba.dcerpc.misc import SEC_CHAN_BDC
49 from samba.upgradehelpers import dn_sort, get_paths, newprovision, find_provision_key_parameters
50
51 never=0
52 replace=2^FLAG_MOD_REPLACE
53 add=2^FLAG_MOD_ADD
54 delete=2^FLAG_MOD_DELETE
55
56 #Errors are always logged
57 ERROR =     -1
58 SIMPLE =     0x00
59 CHANGE =     0x01
60 CHANGESD =     0x02
61 GUESS =     0x04
62 PROVISION =    0x08
63 CHANGEALL =    0xff
64
65 __docformat__ = "restructuredText"
66
67 # Attributes that are never copied from the reference provision (even if they
68 # do not exist in the destination object).
69 # This is most probably because they are populated automatcally when object is
70 # created
71 # This also apply to imported object from reference provision
72 hashAttrNotCopied = {     "dn": 1, "whenCreated": 1, "whenChanged": 1, "objectGUID": 1, "replPropertyMetaData": 1, "uSNChanged": 1,
73                         "uSNCreated": 1, "parentGUID": 1, "objectCategory": 1, "distinguishedName": 1,
74                         "showInAdvancedViewOnly": 1, "instanceType": 1, "cn": 1, "msDS-Behavior-Version":1, "nextRid":1,
75                         "nTMixedDomain": 1, "versionNumber":1, "lmPwdHistory":1, "pwdLastSet": 1, "ntPwdHistory":1, "unicodePwd":1,
76                         "dBCSPwd":1, "supplementalCredentials":1, "gPCUserExtensionNames":1, "gPCMachineExtensionNames":1,
77                         "maxPwdAge":1, "mail":1, "secret":1, "possibleInferiors":1, "sAMAccountType":1}
78
79 # Usually for an object that already exists we do not overwrite attributes as
80 # they might have been changed for good reasons. Anyway for a few of them it's
81 # mandatory to replace them otherwise the provision will be broken somehow.
82 hashOverwrittenAtt = {    "prefixMap": replace, "systemMayContain": replace, "systemOnly":replace, "searchFlags":replace,
83                         "mayContain":replace,  "systemFlags":replace, "description":replace,
84                         "oEMInformation":never, "operatingSystemVersion":replace, "adminPropertyPages":replace,
85                         "defaultSecurityDescriptor": replace, "wellKnownObjects":replace, "privilege":delete, "groupType":replace,
86                         "rIDAvailablePool": never}
87
88
89 backlinked = []
90 dn_syntax_att = []
91 def define_what_to_log(opts):
92     what = 0
93     if opts.debugchange:
94         what = what | CHANGE
95     if opts.debugchangesd:
96         what = what | CHANGESD
97     if opts.debugguess:
98         what = what | GUESS
99     if opts.debugprovision:
100         what = what | PROVISION
101     if opts.debugall:
102         what = what | CHANGEALL
103     return what
104
105
106 parser = optparse.OptionParser("provision [options]")
107 sambaopts = options.SambaOptions(parser)
108 parser.add_option_group(sambaopts)
109 parser.add_option_group(options.VersionOptions(parser))
110 credopts = options.CredentialsOptions(parser)
111 parser.add_option_group(credopts)
112 parser.add_option("--setupdir", type="string", metavar="DIR",
113                     help="directory with setup files")
114 parser.add_option("--debugprovision", help="Debug provision", action="store_true")
115 parser.add_option("--debugguess", help="Print information on what is different but won't be changed", action="store_true")
116 parser.add_option("--debugchange", help="Print information on what is different but won't be changed", action="store_true")
117 parser.add_option("--debugchangesd", help="Print information security descriptors differences", action="store_true")
118 parser.add_option("--debugall", help="Print all available information (very verbose)", action="store_true")
119 parser.add_option("--full", help="Perform full upgrade of the samdb (schema, configuration, new objects, ...", action="store_true")
120
121 opts = parser.parse_args()[0]
122
123 whatToLog = define_what_to_log(opts)
124
125 def messageprovision(text):
126     """Print a message if quiet is not set
127
128     :param text: Message to print """
129     if opts.debugprovision or opts.debugall:
130         print text
131
132 def message(what,text):
133     """Print a message if this message type has been selected to be printed
134
135     :param what: Category of the message
136     :param text: Message to print """
137     if (whatToLog & what) or what <= 0:
138         print text
139
140 if len(sys.argv) == 1:
141     opts.interactive = True
142 lp = sambaopts.get_loadparm()
143 smbconf = lp.configfile
144
145 creds = credopts.get_credentials(lp)
146 creds.set_kerberos_state(DONT_USE_KERBEROS)
147 setup_dir = opts.setupdir
148 if setup_dir is None:
149     setup_dir = find_setup_dir()
150
151 session = system_session()
152
153 def identic_rename(ldbobj,dn):
154     """Perform a back and forth rename to trigger renaming on attribute that can't be directly modified.
155
156     :param lbdobj: An Ldb Object
157     :param dn: DN of the object to manipulate """
158     (before,sep,after)=str(dn).partition('=')
159     ldbobj.rename(dn,Dn(ldbobj,"%s=foo%s"%(before,after)))
160     ldbobj.rename(Dn(ldbobj,"%s=foo%s"%(before,after)),dn)
161
162
163 def populate_backlink(newpaths,creds,session,schemadn):
164     """Populate an array with all the back linked attributes
165
166     This attributes that are modified automaticaly when
167     front attibutes are changed
168
169     :param newpaths: a list of paths for different provision objects
170     :param creds: credential for the authentification
171     :param session: session for connexion
172     :param schemadn: DN of the schema for the partition"""
173     newsam_ldb = Ldb(newpaths.samdb, session_info=session, credentials=creds,lp=lp)
174     linkedAttHash = get_linked_attributes(Dn(newsam_ldb,str(schemadn)),newsam_ldb)
175     backlinked.extend(linkedAttHash.values())
176
177 def populate_dnsyntax(newpaths,creds,session,schemadn):
178     """Populate an array with all the attributes that have DN synthax (oid 2.5.5.1)
179
180     :param newpaths: a list of paths for different provision objects
181     :param creds: credential for the authentification
182     :param session: session for connexion
183     :param schemadn: DN of the schema for the partition"""
184     newsam_ldb = Ldb(newpaths.samdb, session_info=session, credentials=creds,lp=lp)
185     res = newsam_ldb.search(expression="(attributeSyntax=2.5.5.1)",base=Dn(newsam_ldb,str(schemadn)),
186                             scope=SCOPE_SUBTREE, attrs=["lDAPDisplayName"])
187     for elem in res:
188         dn_syntax_att.append(elem["lDAPDisplayName"])
189
190
191 def sanitychecks(credentials,session_info,names,paths):
192     """Populate an array with all the attributes that have DN synthax (oid 2.5.5.1)
193
194     :param creds: credential for the authentification
195     :param session_info: session for connexion
196     :param names: list of key provision parameters
197     :param paths: list of path to provision object
198     :return: Status of check (1 for Ok, 0 for not Ok) """
199     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp,options=["modules:samba_dsdb"])
200
201     sam_ldb.set_session_info(session)
202     res = sam_ldb.search(expression="objectClass=ntdsdsa",base=str(names.configdn),
203                          scope=SCOPE_SUBTREE,attrs=["dn"],controls=["search_options:1:2"])
204     if len(res) == 0:
205         print "No DC found, your provision is most probalby hardly broken !"
206         return False
207     elif len(res) != 1:
208         print "Found %d domain controllers, for the moment upgradeprovision is not able to handle upgrade on \
209 domain with more than one DC, please demote the other(s) DC(s) before upgrading"%len(res)
210         return False
211     else:
212         return True
213
214
215 def print_provision_key_parameters(names):
216     """Do a a pretty print of provision parameters
217
218     :param names: list of key provision parameters """
219     message(GUESS, "rootdn      :"+str(names.rootdn))
220     message(GUESS, "configdn    :"+str(names.configdn))
221     message(GUESS, "schemadn    :"+str(names.schemadn))
222     message(GUESS, "serverdn    :"+str(names.serverdn))
223     message(GUESS, "netbiosname :"+names.netbiosname)
224     message(GUESS, "defaultsite :"+names.sitename)
225     message(GUESS, "dnsdomain   :"+names.dnsdomain)
226     message(GUESS, "hostname    :"+names.hostname)
227     message(GUESS, "domain      :"+names.domain)
228     message(GUESS, "realm       :"+names.realm)
229     message(GUESS, "invocationid:"+names.invocation)
230     message(GUESS, "policyguid  :"+names.policyid)
231     message(GUESS, "policyguiddc:"+str(names.policyid_dc))
232     message(GUESS, "domainsid   :"+str(names.domainsid))
233     message(GUESS, "domainguid  :"+names.domainguid)
234     message(GUESS, "ntdsguid    :"+names.ntdsguid)
235     message(GUESS, "domainlevel :"+str(names.domainlevel))
236
237
238 def handle_security_desc(ischema, att, msgElt, hashallSD, old, new):
239     """Check if the security descriptor has been modified.
240
241     This function also populate a hash used for the upgrade process.
242     :param ischema: Boolean that indicate if it's the schema that is updated
243     :param att: Name of the attribute
244     :param msgElt: MessageElement object
245     :param hashallSD: Hash table with DN as key and the old SD as value
246     :param old: The updated LDAP object
247     :param new: The reference LDAP object
248     :return: 1 to indicate that the attribute should be kept, 0 for discarding it
249     """
250     if ischema == 1 and att == "defaultSecurityDescriptor"  and msgElt.flags() == FLAG_MOD_REPLACE:
251         hashSD = {}
252         hashSD["oldSD"] = old[0][att]
253         hashSD["newSD"] = new[0][att]
254         hashallSD[str(old[0].dn)] = hashSD
255         return True
256     if att == "nTSecurityDescriptor"  and msgElt.flags() == FLAG_MOD_REPLACE:
257         if ischema == 0:
258             hashSD = {}
259             hashSD["oldSD"] = ndr_unpack(security.descriptor, str(old[0][att]))
260             hashSD["newSD"] = ndr_unpack(security.descriptor, str(new[0][att]))
261             hashallSD[str(old[0].dn)] = hashSD
262         return False
263     return False
264
265
266 def handle_special_case(att, delta, new, old, ischema):
267     """Define more complicate update rules for some attributes
268
269     :param att: The attribute to be updated
270     :param delta: A messageElement object that correspond to the difference between the updated object and the reference one
271     :param new: The reference object
272     :param old: The Updated object
273     :param ischema: A boolean that indicate that the attribute is part of a schema object
274     :return: Tru to indicate that the attribute should be kept, False for discarding it
275     """
276     flag = delta.get(att).flags()
277     if (att == "gPLink" or att == "gPCFileSysPath") and \
278         flag == FLAG_MOD_REPLACE and str(new[0].dn).lower() == str(old[0].dn).lower():
279         delta.remove(att)
280         return True
281     if att == "forceLogoff":
282         ref=0x8000000000000000
283         oldval=int(old[0][att][0])
284         newval=int(new[0][att][0])
285         ref == old and ref == abs(new)
286         return True
287     if (att == "adminDisplayName" or att == "adminDescription") and ischema:
288         return True
289
290     if (str(old[0].dn) == "CN=Samba4-Local-Domain,%s" % (str(names.schemadn))\
291         and att == "defaultObjectCategory" and flag == FLAG_MOD_REPLACE):
292         return True
293
294     if (str(old[0].dn) == "CN=Title,%s"%(str(names.schemadn)) and att == "rangeUpper" and flag == FLAG_MOD_REPLACE):
295         return True
296
297     if ((att == "member" or att == "servicePrincipalName") and flag == FLAG_MOD_REPLACE):
298         hash = {}
299         newval = []
300         changeDelta=0
301         for elem in old[0][att]:
302             hash[str(elem)]=1
303             newval.append(str(elem))
304
305         for elem in new[0][att]:
306             if not hash.has_key(str(elem)):
307                 changeDelta=1
308                 newval.append(str(elem))
309         if changeDelta == 1:
310             delta[att] = MessageElement(newval, FLAG_MOD_REPLACE, att)
311         else:
312             delta.remove(att)
313         return True
314
315     if (str(old[0].dn) == "%s"%(str(names.rootdn)) and att == "subRefs" and flag == FLAG_MOD_REPLACE):
316         return True
317     if str(delta.dn).endswith("CN=DisplaySpecifiers,%s"%names.configdn):
318         return True
319     return False
320
321 def update_secrets(newpaths, paths, creds, session):
322     """Update secrets.ldb
323
324     :param newpaths: a list of paths for different provision objects from the
325         reference provision
326     :param paths: a list of paths for different provision objects from the
327         upgraded provision
328     :param creds: credential for the authentification
329     :param session: session for connexion"""
330
331     message(SIMPLE,"update secrets.ldb")
332     newsecrets_ldb = Ldb(newpaths.secrets, session_info=session,
333         credentials=creds,lp=lp)
334     secrets_ldb = Ldb(paths.secrets, session_info=session,
335         credentials=creds,lp=lp, options=["modules:samba_secrets"])
336     reference = newsecrets_ldb.search(expression="dn=@MODULES",base="",
337         scope=SCOPE_SUBTREE)
338     current = secrets_ldb.search(expression="dn=@MODULES",base="",
339         scope=SCOPE_SUBTREE)
340     delta = secrets_ldb.msg_diff(current[0],reference[0])
341     delta.dn = current[0].dn
342     secrets_ldb.modify(delta)
343
344     newsecrets_ldb = Ldb(newpaths.secrets, session_info=session, credentials=creds,lp=lp)
345     secrets_ldb = Ldb(paths.secrets, session_info=session, credentials=creds,lp=lp)
346     reference = newsecrets_ldb.search(expression="objectClass=top",base="", scope=SCOPE_SUBTREE,attrs=["dn"])
347     current = secrets_ldb.search(expression="objectClass=top",base="", scope=SCOPE_SUBTREE,attrs=["dn"])
348     hash_new = {}
349     hash = {}
350     listMissing = []
351     listPresent = []
352
353     empty = Message()
354     for i in range(0,len(reference)):
355         hash_new[str(reference[i]["dn"]).lower()] = reference[i]["dn"]
356
357     # Create a hash for speeding the search of existing object in the
358     # current provision
359     for i in range(0,len(current)):
360         hash[str(current[i]["dn"]).lower()] = current[i]["dn"]
361
362     for k in hash_new.keys():
363         if not hash.has_key(k):
364             listMissing.append(hash_new[k])
365         else:
366             listPresent.append(hash_new[k])
367
368     for entry in listMissing:
369         reference = newsecrets_ldb.search(expression="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
370         current = secrets_ldb.search(expression="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
371         delta = secrets_ldb.msg_diff(empty,reference[0])
372         for att in hashAttrNotCopied.keys():
373             delta.remove(att)
374         message(CHANGE,"Entry %s is missing from secrets.ldb"%reference[0].dn)
375         for att in delta:
376             message(CHANGE," Adding attribute %s"%att)
377         delta.dn = reference[0].dn
378         secrets_ldb.add(delta)
379
380     for entry in listPresent:
381         reference = newsecrets_ldb.search(expression="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
382         current = secrets_ldb.search(expression="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
383         delta = secrets_ldb.msg_diff(current[0],reference[0])
384         for att in hashAttrNotCopied.keys():
385             delta.remove(att)
386         for att in delta:
387             if att == "name":
388                 message(CHANGE,"Found attribute name on  %s, 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="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
395         current = secrets_ldb.search(expression="dn=%s"%entry,base="", scope=SCOPE_SUBTREE)
396         delta = secrets_ldb.msg_diff(current[0],reference[0])
397         for att in hashAttrNotCopied.keys():
398             delta.remove(att)
399         for att in delta:
400             if att != "dn":
401                 message(CHANGE," Adding/Changing attribute %s to %s"%(att,current[0].dn))
402
403         delta.dn = current[0].dn
404         secrets_ldb.modify(delta)
405
406
407 def dump_denied_change(dn,att,flagtxt,current,reference):
408     """Print detailed information about why a changed is denied
409
410     :param dn: DN of the object which attribute is denied
411     :param att: Attribute that was supposed to be upgraded
412     :param flagtxt: Type of the update that should be performed (add, change, remove, ...)
413     :param current: Value(s) of the current attribute
414     :param reference: Value(s) of the reference attribute"""
415
416     message(CHANGE, "dn= "+str(dn)+" "+att+" with flag "+flagtxt+" is not allowed to be changed/removed, I discard this change ...")
417     if att != "objectSid" :
418         i = 0
419         for e in range(0,len(current)):
420             message(CHANGE,"old %d : %s"%(i,str(current[e])))
421             i+=1
422         if reference != None:
423             i = 0
424             for e in range(0,len(reference)):
425                 message(CHANGE,"new %d : %s"%(i,str(reference[e])))
426                 i+=1
427     else:
428         message(CHANGE,"old : %s"%str(ndr_unpack( security.dom_sid,current[0])))
429         message(CHANGE,"new : %s"%str(ndr_unpack( security.dom_sid,reference[0])))
430
431
432 def handle_special_add(sam_ldb,dn,names):
433     """Handle special operation (like remove) on some object needed during upgrade
434
435     This is mostly due to wrong creation of the object in previous provision.
436     :param sam_ldb: An Ldb object representing the SAM database
437     :param dn: DN of the object to inspect
438     :param names: list of key provision parameters"""
439     dntoremove = None
440     if str(dn).lower() == ("CN=Certificate Service DCOM Access,CN=Builtin,%s"%names.rootdn).lower():
441         #This entry was misplaced lets remove it if it exists
442         dntoremove = "CN=Certificate Service DCOM Access,CN=Users,%s"%names.rootdn
443
444     if str(dn).lower() == ("CN=Cryptographic Operators,CN=Builtin,%s"%names.rootdn).lower():
445         #This entry was misplaced lets remove it if it exists
446         dntoremove = "CN=Cryptographic Operators,CN=Users,%s"%names.rootdn
447
448     if str(dn).lower() == ("CN=Event Log Readers,CN=Builtin,%s"%names.rootdn).lower():
449         #This entry was misplaced lets remove it if it exists
450         dntoremove = "CN=Event Log Readers,CN=Users,%s"%names.rootdn
451
452     if dntoremove != None:
453         res = sam_ldb.search(expression="objectClass=*",base=dntoremove, scope=SCOPE_BASE,attrs=["dn"],controls=["search_options:1:2"])
454         if len(res) > 0:
455             message(CHANGE,"Existing object %s must be replaced by %s, removing old object"%(dntoremove,str(dn)))
456             sam_ldb.delete(res[0]["dn"])
457
458
459 def check_dn_nottobecreated(hash, index, listdn):
460     """Check if one of the DN present in the list has a creation order greater than the current.
461
462     Hash is indexed by dn to be created, with each key is associated the creation order
463     First dn to be created has the creation order 0, second has 1, ...
464     Index contain the current creation order
465
466     :param hash: Hash holding the different DN of the object to be created as key
467     :param index: Current creation order
468     :param listdn: List of DNs on which the current DN depends on
469     :return: None if the current object do not depend on other object or if all object have been
470     created before."""
471     if listdn == None:
472         return None
473     for dn in listdn:
474         key = str(dn).lower()
475         if hash.has_key(key) and hash[key] > index:
476             return str(dn)
477     return None
478
479
480 def add_missing_object(newsam_ldb, sam_ldb, dn, names, basedn, hash, index):
481     """Add a new object if the dependencies are satisfied
482
483     The function add the object if the object on which it depends are already created
484     :param newsam_ldb: Ldb object representing the SAM db of the reference provision
485     :param sam_ldb: Ldb object representing the SAM db of the upgraded provision
486     :param dn: DN of the object to be added
487     :param names: List of key provision parameters
488     :param basedn: DN of the partition to be updated
489     :param hash: Hash holding the different DN of the object to be created as key
490     :param index: Current creation order
491     :return: True if the object was created False otherwise"""
492     handle_special_add(sam_ldb,dn,names)
493     reference = newsam_ldb.search(expression="dn=%s"%(str(dn)),base=basedn,
494                     scope=SCOPE_SUBTREE,controls=["search_options:1:2"])
495     empty = Message()
496     delta = sam_ldb.msg_diff(empty,reference[0])
497     for att in hashAttrNotCopied.keys():
498         delta.remove(att)
499     for att in backlinked:
500         delta.remove(att)
501     depend_on_yettobecreated = None
502     for att in dn_syntax_att:
503         depend_on_yet_tobecreated = check_dn_nottobecreated(hash,index,delta.get(str(att)))
504         if depend_on_yet_tobecreated != None:
505             message(CHANGE,"Object %s depends on %s in attribute %s, delaying the creation"
506                             %(str(dn),depend_on_yet_tobecreated,str(att)))
507             return False
508     delta.dn = dn
509     message(CHANGE,"Object %s will be added"%dn)
510     sam_ldb.add(delta,["relax:0"])
511     return True
512
513
514 def gen_dn_index_hash(listMissing):
515     """Generate a hash associating the DN to its creation order
516
517     :param listMissing: List of DN
518     :return: Hash with DN as keys and creation order as values"""
519     hash = {}
520     for i in range(0,len(listMissing)):
521         hash[str(listMissing[i]).lower()] = i
522     return hash
523
524
525 def add_missing_entries(newsam_ldb, sam_ldb, names, basedn,list):
526     """Add the missing object whose DN is the list
527
528     The function add the object if the object on which it depends are already created
529     :param newsam_ldb: Ldb object representing the SAM db of the reference provision
530     :param sam_ldb: Ldb object representing the SAM db of the upgraded provision
531     :param dn: DN of the object to be added
532     :param names: List of key provision parameters
533     :param basedn: DN of the partition to be updated
534     :param list: List of DN to be added in the upgraded provision"""
535     listMissing = []
536     listDefered = list
537
538     while(len(listDefered) != len(listMissing) and len(listDefered) > 0):
539         index = 0
540         listMissing = listDefered
541         listDefered = []
542         hashMissing = gen_dn_index_hash(listMissing)
543         for dn in listMissing:
544             ret = add_missing_object(newsam_ldb,sam_ldb,dn,names,basedn,hashMissing,index)
545             index = index + 1
546             if ret == 0:
547                 #DN can't be created because it depends on some other DN in the list
548                 listDefered.append(dn)
549     if len(listDefered) != 0:
550         raise ProvisioningError("Unable to insert missing elements: circular references")
551
552
553 def check_diff_name(newpaths, paths, creds, session, basedn, names, ischema):
554     """Check differences between the reference provision and the upgraded one.
555
556     It looks for all objects which base DN is name. If ischema is "false" then
557     the scan is done in cross partition mode.
558     If "ischema" is true, then special handling is done for dealing with schema
559
560     This function will also add the missing object and update existing object to add
561     or remove attributes that were missing.
562     :param newpaths: List of paths for different provision objects from the reference provision
563     :param paths: List of paths for different provision objects from the upgraded provision
564     :param creds: Credential for the authentification
565     :param session: Session for connexion
566     :param basedn: DN of the partition to update
567     :param names: List of key provision parameters
568     :param ischema: Boolean indicating if the update is about the schema only
569     :return: Hash of security descriptor to update"""
570
571     hash_new = {}
572     hash = {}
573     hashallSD = {}
574     listMissing = []
575     listPresent = []
576     reference = []
577     current = []
578     # Connect to the reference provision and get all the attribute in the
579     # partition referred by name
580     newsam_ldb = Ldb(newpaths.samdb, session_info=session, credentials=creds,lp=lp)
581     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp, options=["modules:samba_dsdb"])
582     sam_ldb.transaction_start()
583     if ischema:
584         reference = newsam_ldb.search(expression="objectClass=*",base=basedn, scope=SCOPE_SUBTREE,attrs=["dn"])
585         current = sam_ldb.search(expression="objectClass=*",base=basedn, scope=SCOPE_SUBTREE,attrs=["dn"])
586     else:
587         reference = newsam_ldb.search(expression="objectClass=*",base=basedn, scope=SCOPE_SUBTREE,attrs=["dn"],controls=["search_options:1:2"])
588         current = sam_ldb.search(expression="objectClass=*",base=basedn, scope=SCOPE_SUBTREE,attrs=["dn"],controls=["search_options:1:2"])
589
590     sam_ldb.transaction_commit()
591     # Create a hash for speeding the search of new object
592     for i in range(0,len(reference)):
593         hash_new[str(reference[i]["dn"]).lower()] = reference[i]["dn"]
594
595     # Create a hash for speeding the search of existing object in the
596     # current provision
597     for i in range(0,len(current)):
598         hash[str(current[i]["dn"]).lower()] = current[i]["dn"]
599
600     for k in hash_new.keys():
601         if not hash.has_key(k):
602             print hash_new[k]
603             listMissing.append(hash_new[k])
604         else:
605             listPresent.append(hash_new[k])
606
607     # Sort the missing object in order to have object of the lowest level
608     # first (which can be containers for higher level objects)
609     listMissing.sort(dn_sort)
610     listPresent.sort(dn_sort)
611
612     if ischema:
613         # The following lines (up to the for loop) is to load the up to
614         # date schema into our current LDB
615         # a complete schema is needed as the insertion of attributes
616         # and class is done against it
617         # and the schema is self validated
618         # The double ldb open and schema validation is taken from the
619         # initial provision script
620         # it's not certain that it is really needed ....
621         sam_ldb = Ldb(session_info=session, credentials=creds, lp=lp)
622         schema = Schema(setup_path, names.domainsid, schemadn=basedn, serverdn=str(names.serverdn))
623         # Load the schema from the one we computed earlier
624         sam_ldb.set_schema_from_ldb(schema.ldb)
625         # And now we can connect to the DB - the schema won't be loaded
626         # from the DB
627         sam_ldb.connect(paths.samdb)
628     else:
629         sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp, options=["modules:samba_dsdb"])
630
631     sam_ldb.transaction_start()
632     # XXX: This needs to be wrapped in try/except so we
633     # abort on exceptions.
634     message(SIMPLE,"There are %d missing objects"%(len(listMissing)))
635     add_missing_entries(newsam_ldb,sam_ldb,names,basedn,listMissing)
636     changed = 0
637     for dn in listPresent:
638         reference = newsam_ldb.search(expression="dn=%s"%(str(dn)),base=basedn, scope=SCOPE_SUBTREE,controls=["search_options:1:2"])
639         current = sam_ldb.search(expression="dn=%s"%(str(dn)),base=basedn, scope=SCOPE_SUBTREE,controls=["search_options:1:2"])
640         if ((str(current[0].dn) != str(reference[0].dn)) and (str(current[0].dn).upper() == str(reference[0].dn).upper())):
641             message(CHANGE,"Name are the same but case change, let's rename %s to %s"%(str(current[0].dn),str(reference[0].dn)))
642             identic_rename(sam_ldb,reference[0].dn)
643             current = sam_ldb.search(expression="dn=%s"%(str(dn)),base=basedn, scope=SCOPE_SUBTREE,controls=["search_options:1:2"])
644
645         delta = sam_ldb.msg_diff(current[0],reference[0])
646         for att in hashAttrNotCopied.keys():
647             delta.remove(att)
648         for att in backlinked:
649             delta.remove(att)
650         delta.remove("parentGUID")
651         nb = 0
652         
653         for att in delta:
654             msgElt = delta.get(att)
655             if att == "dn":
656                 continue
657             if att == "name":
658                 delta.remove(att)
659                 continue
660             if not handle_security_desc(ischema,att,msgElt,hashallSD,current,reference):
661                 delta.remove(att)
662                 continue
663             if (not hashOverwrittenAtt.has_key(att) or not (hashOverwrittenAtt.get(att)&2^msgElt.flags())):
664                 if  hashOverwrittenAtt.has_key(att) and hashOverwrittenAtt.get(att)==never:
665                     delta.remove(att)
666                     continue
667                 if not handle_special_case(att,delta,reference,current,ischema) and msgElt.flags()!=FLAG_MOD_ADD:
668                     if opts.debugchange or opts.debugall:
669                         try:
670                             dump_denied_change(dn,att,messageEltFlagToString(msgElt.flags()),current[0][att],reference[0][att])
671                         except:
672                             # FIXME: Should catch an explicit exception here
673                             dump_denied_change(dn,att,messageEltFlagToString(msgElt.flags()),current[0][att],None)
674                     delta.remove(att)
675         delta.dn = dn
676         if len(delta.items()) >1:
677             attributes=",".join(delta.keys())
678             message(CHANGE,"%s is different from the reference one, changed attributes: %s"%(dn,attributes))
679             changed = changed + 1
680             sam_ldb.modify(delta)
681
682     sam_ldb.transaction_commit()
683     message(SIMPLE,"There are %d changed objects"%(changed))
684     return hashallSD
685
686
687 def check_updated_sd(newpaths, paths, creds, session, names):
688     """Check if the security descriptor in the upgraded provision are the same as the reference
689
690     :param newpaths: List of paths for different provision objects from the reference provision
691     :param paths: List of paths for different provision objects from the upgraded provision
692     :param creds: Credential for the authentification
693     :param session: Session for connexion
694     :param basedn: DN of the partition to update
695     :param names: List of key provision parameters"""
696     newsam_ldb = Ldb(newpaths.samdb, session_info=session, credentials=creds,lp=lp)
697     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp)
698     reference = newsam_ldb.search(expression="objectClass=*",base=str(names.rootdn), scope=SCOPE_SUBTREE,attrs=["dn","nTSecurityDescriptor"],controls=["search_options:1:2"])
699     current = sam_ldb.search(expression="objectClass=*",base=str(names.rootdn), scope=SCOPE_SUBTREE,attrs=["dn","nTSecurityDescriptor"],controls=["search_options:1:2"])
700     hash_new = {}
701     for i in range(0,len(reference)):
702         hash_new[str(reference[i]["dn"]).lower()] = ndr_unpack(security.descriptor,str(reference[i]["nTSecurityDescriptor"])).as_sddl(names.domainsid)
703
704     for i in range(0,len(current)):
705         key = str(current[i]["dn"]).lower()
706         if hash_new.has_key(key):
707             sddl = ndr_unpack(security.descriptor,str(current[i]["nTSecurityDescriptor"])).as_sddl(names.domainsid)
708             if sddl != hash_new[key]:
709                 print "%s new sddl/sddl in ref"%key
710                 print "%s\n%s"%(sddl,hash_new[key])
711
712
713 def update_sd(paths, creds, session, names):
714     """Update security descriptor of the current provision
715
716     During the different pre release of samba4 security descriptors (SD) were notarly broken (up to alpha11 included)
717     This function allow to get them back in order, this function make the assumption that nobody has modified manualy an SD
718     and so SD can be safely recalculated from scratch to get them right.
719
720     :param paths: List of paths for different provision objects from the upgraded provision
721     :param creds: Credential for the authentification
722     :param session: Session for connexion
723     :param names: List of key provision parameters"""
724
725     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp,options=["modules:samba_dsdb"])
726     sam_ldb.transaction_start()
727     # First update the SD for the rootdn
728     sam_ldb.set_session_info(session)
729     res = sam_ldb.search(expression="objectClass=*", base=str(names.rootdn), scope=SCOPE_BASE,\
730                          attrs=["dn", "whenCreated"], controls=["search_options:1:2"])
731     delta = Message()
732     delta.dn = Dn(sam_ldb,str(res[0]["dn"]))
733     descr = get_domain_descriptor(names.domainsid)
734     delta["nTSecurityDescriptor"] = MessageElement(descr, FLAG_MOD_REPLACE, "nTSecurityDescriptor")
735     sam_ldb.modify(delta,["recalculate_sd:0"])
736     # Then the config dn
737     res = sam_ldb.search(expression="objectClass=*",base=str(names.configdn), scope=SCOPE_BASE,attrs=["dn","whenCreated"],controls=["search_options:1:2"])
738     delta = Message()
739     delta.dn = Dn(sam_ldb,str(res[0]["dn"]))
740     descr = get_config_descriptor(names.domainsid)
741     delta["nTSecurityDescriptor"] = MessageElement(descr, FLAG_MOD_REPLACE, "nTSecurityDescriptor" )
742     sam_ldb.modify(delta,["recalculate_sd:0"])
743     # Then the schema dn
744     res = sam_ldb.search(expression="objectClass=*",base=str(names.schemadn), scope=SCOPE_BASE,attrs=["dn","whenCreated"],controls=["search_options:1:2"])
745     delta = Message()
746     delta.dn = Dn(sam_ldb,str(res[0]["dn"]))
747     descr = get_schema_descriptor(names.domainsid)
748     delta["nTSecurityDescriptor"] = MessageElement(descr, FLAG_MOD_REPLACE, "nTSecurityDescriptor" )
749     sam_ldb.modify(delta,["recalculate_sd:0"])
750
751     # Then the rest
752     hash = {}
753     res = sam_ldb.search(expression="objectClass=*",base=str(names.rootdn), scope=SCOPE_SUBTREE,attrs=["dn","whenCreated"],controls=["search_options:1:2"])
754     for obj in res:
755         if not (str(obj["dn"]) == str(names.rootdn) or
756             str(obj["dn"]) == str(names.configdn) or \
757             str(obj["dn"]) == str(names.schemadn)):
758             hash[str(obj["dn"])] = obj["whenCreated"]
759
760     listkeys = hash.keys()
761     listkeys.sort(dn_sort)
762
763     for key in listkeys:
764         try:
765             delta = Message()
766             delta.dn = Dn(sam_ldb,key)
767             delta["whenCreated"] = MessageElement(hash[key], FLAG_MOD_REPLACE, "whenCreated" )
768             sam_ldb.modify(delta,["recalculate_sd:0"])
769         except:
770             # XXX: We should always catch an explicit exception.
771             # What could go wrong here?
772             sam_ldb.transaction_cancel()
773             res = sam_ldb.search(expression="objectClass=*", base=str(names.rootdn), scope=SCOPE_SUBTREE,\
774                                  attrs=["dn","nTSecurityDescriptor"], controls=["search_options:1:2"])
775             print "bad stuff" +ndr_unpack(security.descriptor,str(res[0]["nTSecurityDescriptor"])).as_sddl(names.domainsid)
776             return
777     sam_ldb.transaction_commit()
778
779
780 def update_basesamdb(newpaths, paths, names):
781     """Update the provision container db: sam.ldb
782
783     :param newpaths: List of paths for different provision objects from the reference provision
784     :param paths: List of paths for different provision objects from the upgraded provision
785     :param names: List of key provision parameters"""
786
787     message(SIMPLE,"Copy samdb")
788     shutil.copy(newpaths.samdb,paths.samdb)
789
790     message(SIMPLE,"Update partitions filename if needed")
791     schemaldb = os.path.join(paths.private_dir, "schema.ldb")
792     configldb = os.path.join(paths.private_dir, "configuration.ldb")
793     usersldb = os.path.join(paths.private_dir, "users.ldb")
794     samldbdir = os.path.join(paths.private_dir, "sam.ldb.d")
795
796     if not os.path.isdir(samldbdir):
797         os.mkdir(samldbdir)
798         os.chmod(samldbdir,0700)
799     if os.path.isfile(schemaldb):
800         shutil.copy(schemaldb, os.path.join(samldbdir, "%s.ldb" % str(names.schemadn).upper()))
801         os.remove(schemaldb)
802     if os.path.isfile(usersldb):
803         shutil.copy(usersldb, os.path.join(samldbdir, "%s.ldb" % str(names.rootdn).upper()))
804         os.remove(usersldb)
805     if os.path.isfile(configldb):
806         shutil.copy(configldb, os.path.join(samldbdir, "%s.ldb" % str(names.configdn).upper()))
807         os.remove(configldb)
808
809
810 def update_privilege(newpaths, paths):
811     """Update the privilege database
812
813     :param newpaths: List of paths for different provision objects from the reference provision
814     :param paths: List of paths for different provision objects from the upgraded provision"""
815     message(SIMPLE, "Copy privilege")
816     shutil.copy(os.path.join(newpaths.private_dir, "privilege.ldb"), 
817                 os.path.join(paths.private_dir, "privilege.ldb"))
818
819
820 def update_samdb(newpaths, paths, creds, session, names):
821     """Upgrade the SAM DB contents for all the provision
822
823     :param newpaths: List of paths for different provision objects from the reference provision
824     :param paths: List of paths for different provision objects from the upgraded provision
825     :param creds: Credential for the authentification
826     :param session: Session for connexion
827     :param names: List of key provision parameters"""
828
829     message(SIMPLE, "Doing schema update")
830     hashdef = check_diff_name(newpaths,paths,creds,session,str(names.schemadn),names,1)
831     message(SIMPLE,"Done with schema update")
832     message(SIMPLE,"Scanning whole provision for updates and additions")
833     hashSD = check_diff_name(newpaths,paths,creds,session,str(names.rootdn),names,0)
834     message(SIMPLE,"Done with scanning")
835
836
837 def update_machine_account_password(paths, creds, session, names):
838     """Update (change) the password of the current DC both in the SAM db and in secret one
839
840     :param paths: List of paths for different provision objects from the upgraded provision
841     :param creds: Credential for the authentification
842     :param session: Session for connexion
843     :param names: List of key provision parameters"""
844
845     secrets_ldb = Ldb(paths.secrets, session_info=session,
846         credentials=creds,lp=lp)
847     secrets_ldb.transaction_start()
848     secrets_msg = secrets_ldb.search(expression=("samAccountName=%s$" % names.netbiosname), attrs=["secureChannelType"])
849     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp)
850     sam_ldb.transaction_start()
851     if int(secrets_msg[0]["secureChannelType"][0]) == SEC_CHAN_BDC:
852         res = sam_ldb.search(expression=("samAccountName=%s$" % names.netbiosname), attrs=[])
853         assert(len(res) == 1)
854
855         msg = Message(res[0].dn)
856         machinepass = samba.generate_random_password(128, 255)
857         msg["userPassword"] = MessageElement(machinepass, FLAG_MOD_REPLACE, "userPassword")
858         sam_ldb.modify(msg)
859
860         res = sam_ldb.search(expression=("samAccountName=%s$" % names.netbiosname),
861                      attrs=["msDs-keyVersionNumber"])
862         assert(len(res) == 1)
863         kvno = int(str(res[0]["msDs-keyVersionNumber"]))
864
865         secretsdb_self_join(secrets_ldb, domain=names.domain,
866                     realm=names.realm or sambaopts._lp.get('realm'),
867                     domainsid=names.domainsid,
868                     dnsdomain=names.dnsdomain,
869                     netbiosname=names.netbiosname,
870                     machinepass=machinepass,
871                     key_version_number=kvno,
872                     secure_channel_type=int(secrets_msg[0]["secureChannelType"][0]))
873         sam_ldb.transaction_prepare_commit()
874         secrets_ldb.transaction_prepare_commit()
875         sam_ldb.transaction_commit()
876         secrets_ldb.transaction_commit()
877     else:
878         secrets_ldb.transaction_cancel()
879
880
881 def update_gpo(paths,creds,session,names):
882     """Create missing GPO file object if needed
883
884     Set ACL correctly also.
885     """
886     dir = getpolicypath(paths.sysvol,names.dnsdomain,names.policyid)
887     if not os.path.isdir(dir):
888         create_gpo_struct(dir)
889
890     dir = getpolicypath(paths.sysvol,names.dnsdomain,names.policyid_dc)
891     if not os.path.isdir(dir):
892         create_gpo_struct(dir)
893     samdb = Ldb(paths.samdb, session_info=session, credentials=creds,lp=lp)
894     set_gpo_acl(paths.sysvol, names.dnsdomain, names.domainsid,
895         names.domaindn, samdb, lp)
896
897
898 def updateOEMInfo(paths, creds, session,names):
899     sam_ldb = Ldb(paths.samdb, session_info=session, credentials=creds, lp=lp,
900         options=["modules:samba_dsdb"])
901     res = sam_ldb.search(expression="(objectClass=*)",base=str(names.rootdn),
902                             scope=SCOPE_BASE, attrs=["dn","oEMInformation"])
903     if len(res) > 0:
904         info = res[0]["oEMInformation"]
905         info = "%s, upgrade to %s"%(info,version)
906         delta = Message()
907         delta.dn = Dn(sam_ldb,str(res[0]["dn"]))
908         descr = get_schema_descriptor(names.domainsid)
909         delta["oEMInformation"] = MessageElement(info, FLAG_MOD_REPLACE,
910             "oEMInformation" )
911         sam_ldb.modify(delta)
912
913
914 def setup_path(file):
915     return os.path.join(setup_dir, file)
916
917
918 if __name__ == '__main__':
919     # From here start the big steps of the program
920     # First get files paths
921     paths=get_paths(param,smbconf=smbconf)
922     paths.setup = setup_dir
923     # Guess all the needed names (variables in fact) from the current
924     # provision.
925
926     names = find_provision_key_parameters(param, creds, session, paths, smbconf)
927     if not sanitychecks(creds,session,names,paths):
928         message(SIMPLE,"Sanity checks for the upgrade fails, checks messages and correct it before rerunning upgradeprovision")
929         sys.exit(1)
930     # Let's see them
931     print_provision_key_parameters(names)
932     # With all this information let's create a fresh new provision used as reference
933     message(SIMPLE,"Creating a reference provision")
934     provisiondir = tempfile.mkdtemp(dir=paths.private_dir, prefix="referenceprovision")
935     newprovision(names, setup_dir, creds, session, smbconf, provisiondir, messageprovision)
936     # Get file paths of this new provision
937     newpaths = get_paths(param, targetdir=provisiondir)
938     populate_backlink(newpaths, creds, session,names.schemadn)
939     populate_dnsyntax(newpaths, creds, session,names.schemadn)
940     # Check the difference
941     update_basesamdb(newpaths, paths, names)
942
943     if opts.full:
944         update_samdb(newpaths, paths, creds, session, names)
945     update_secrets(newpaths, paths, creds, session)
946     update_privilege(newpaths, paths)
947     update_machine_account_password(paths, creds, session, names)
948     # SD should be created with admin but as some previous acl were so wrong that admin can't modify them we have first
949     # to recreate them with the good form but with system account and then give the ownership to admin ...
950     admin_session_info = admin_session(lp, str(names.domainsid))
951     message(SIMPLE, "Updating SD")
952     update_sd(paths, creds, session,names)
953     update_sd(paths, creds, admin_session_info, names)
954     check_updated_sd(newpaths, paths, creds, session, names)
955     updateOEMInfo(paths,creds,session,names)
956     message(SIMPLE, "Upgrade finished !")
957     # remove reference provision now that everything is done !
958     shutil.rmtree(provisiondir)