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