#!/usr/bin/python2.6 # # Univention Migrate SBS # Migrates a SBS server to the local Samba 4 system # # Copyright 2012-2013 Univention GmbH # # http://www.univention.de/ # # All rights reserved. # # The source code of this program is made available # under the terms of the GNU Affero General Public License version 3 # (GNU AGPL V3) as published by the Free Software Foundation. # # Binary versions of this program provided by Univention to you as # well as other copyrighted, protected or trademarked materials like # Logos, graphics, fonts, specific documentations and configurations, # cryptographic keys etc. are subject to a license agreement between # you and Univention and not subject to the GNU AGPL V3. # # In the case you use this program under the terms of the GNU AGPL V3, # the program is provided in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public # License with the Debian GNU/Linux or Univention distribution in file # /usr/share/common-licenses/AGPL-3; if not, see # . from optparse import OptionParser, OptionValueError import samba.getopt import sys, os import subprocess import shutil from univention import config_registry import ldb import samba from samba.samdb import SamDB from samba.auth import system_session from samba.param import LoadParm import socket, time, struct import ldap import re from samba.ndr import ndr_pack, ndr_unpack from samba.dcerpc import security import univention.admin.uldap import univention.admin.uexceptions samba_dir = '/var/lib/samba' samba_private_dir = os.path.join(samba_dir, 'private') sysvol_path = os.path.join(samba_dir, 'sysvol') well_known_sids = { "Null Authority": "S-1-0", "Nobody": "S-1-0-0", "World Authority": "S-1-1", "Everyone": "S-1-1-0", "Local Authority": "S-1-2", "Local": "S-1-2-0", "Console Logon": "S-1-2-1", "Creator Authority": "S-1-3", "Creator Owner": "S-1-3-0", "Creator Group": "S-1-3-1", "Creator Owner Server": "S-1-3-2", "Creator Group Server": "S-1-3-3", "Owner Rights": "S-1-3-4", "All Services": "S-1-5-80-0", "Non-unique Authority": "S-1-4", "NT Authority": "S-1-5", "Dialup": "S-1-5-1", "Network": "S-1-5-2", "Batch": "S-1-5-3", "Interactive": "S-1-5-4", "Service": "S-1-5-6", "Anonymous": "S-1-5-7", "Proxy": "S-1-5-8", "Enterprise Domain Controllers": "S-1-5-9", "Principal Self": "S-1-5-10", "Authenticated Users": "S-1-5-11", "Restricted Code": "S-1-5-12", "Terminal Server Users": "S-1-5-13", "Remote Interactive Logon": "S-1-5-14", "This Organization": "S-1-5-15", "IUSR": "S-1-5-17", "Local System": "S-1-5-18", "NT Authority": "S-1-5-20", "Administrators": "S-1-5-32-544", "Users": "S-1-5-32-545", "Guests": "S-1-5-32-546", "Power Users": "S-1-5-32-547", "Account Operators": "S-1-5-32-548", "Server Operators": "S-1-5-32-549", "System Operators": "S-1-5-32-549", ## duplicate names "Print Operators": "S-1-5-32-550", "Backup Operators": "S-1-5-32-551", "Replicators": "S-1-5-32-552", } well_known_domain_rids = { "Administrator": 500, "Guest": 501, "KRBTGT": 502, "Domain Admins": 512, "Domain Users": 513, "Domain Guests": 514, "Domain Computers": 515, "Domain Controllers": 516, "Cert Publishers": 517, "Schema Admins": 518, "Enterprise Admins": 519, "Group Policy Creator Owners": 520, "RAS and IAS Servers": 553, ## Windows Server 2008 "Enterprise Read-only Domain Controllers": 498, "Read-Only Domain Controllers": 521, "Allowed RODC Password Replication Group": 571, "Denied RODC Password Replication Group": 572, ## Windows Server "8" "Cloneable Domain Controllers": 522, } def _connect_ucs(ucr, binddn=None, bindpwd=None): ''' Connect to OpenLDAP ''' if binddn and bindpwd: bindpw = bindpwd else: bindpw_file = ucr.get('connector/ldap/bindpw', '/etc/ldap.secret') binddn = ucr.get('connector/ldap/binddn', 'cn=admin,'+ucr['ldap/base']) bindpw=open(bindpw_file).read() if bindpw[-1] == '\n': bindpw=bindpw[0:-1] host = ucr.get('connector/ldap/server', ucr.get('ldap/master')) try: port = int(ucr.get('connector/ldap/port', ucr.get('ldap/master/port'))) except: port = 7389 lo = univention.admin.uldap.access(host=host, port=port, base=ucr['ldap/base'], binddn=binddn, bindpw=bindpw, start_tls=0, follow_referral=True) return lo def operatingSystem_attribute(ucr, samdb): msg = samdb.search(base=samdb.domain_dn(), scope=samba.ldb.SCOPE_SUBTREE, expression="(sAMAccountName=%s$)" % ucr["hostname"], attrs=["operatingSystem", "operatingSystemVersion"]) if msg: obj = msg[0] if not "operatingSystem" in obj: delta = ldb.Message() delta.dn = obj.dn delta["operatingSystem"] = ldb.MessageElement("Univention Corporate Server", ldb.FLAG_MOD_REPLACE, "operatingSystem") samdb.modify(delta) if not "operatingSystemVersion" in obj: delta = ldb.Message() delta.dn = obj.dn delta["operatingSystemVersion"] = ldb.MessageElement("3.0", ldb.FLAG_MOD_REPLACE, "operatingSystemVersion") samdb.modify(delta) def let_samba4_manage_etc_krb5_keytab(ucr, secretsdb): msg = secretsdb.search(base="cn=Primary Domains", scope=samba.ldb.SCOPE_SUBTREE, expression="(flatName=%s)" % ucr["windows/domain"], attrs=["krb5Keytab"]) if msg: obj = msg[0] if not "krb5Keytab" in obj or not "/etc/krb5.keytab" in obj["krb5Keytab"]: delta = ldb.Message() delta.dn = obj.dn delta["krb5Keytab"] = ldb.MessageElement("/etc/krb5.keytab", ldb.FLAG_MOD_ADD, "krb5Keytab") secretsdb.modify(delta) def add_servicePrincipals(ucr, secretsdb, spn_list): msg = secretsdb.search(base="cn=Primary Domains", scope=samba.ldb.SCOPE_SUBTREE, expression="(flatName=%s)" % ucr["windows/domain"], attrs=["servicePrincipalName"]) if msg: obj = msg[0] delta = ldb.Message() delta.dn = obj.dn for spn in spn_list: if not "servicePrincipalName" in obj or not spn in obj["servicePrincipalName"]: delta[spn] = ldb.MessageElement(spn, ldb.FLAG_MOD_ADD, "servicePrincipalName") secretsdb.modify(delta) def sync_time(server): ## source: http://code.activestate.com/recipes/117211-simple-very-sntp-client/ TIME1970 = 2208988800L # Thanks to F.Lundh client = socket.socket( socket.AF_INET, socket.SOCK_DGRAM ) data = '\x1b' + 47 * '\0' client.settimeout(15.0) try: client.sendto( data, ( server, 123 )) data, address = client.recvfrom( 1024 ) if data: print 'NTP Response received from server %s' % server t = struct.unpack( '!12I', data )[10] t -= TIME1970 if time.gmtime(t) >= time.gmtime(): print "Setting local time: ", p = subprocess.Popen(["/bin/date", "-s", time.ctime(t)], stdout=subprocess.PIPE) (stdout, stderr) = p.communicate() print stdout else: print "Error: time %s on server %s is earlier than" % (time.strftime("%a, %d %b %Y %H:%M:%S %Z", time.localtime(t)), server) print " time %s on this server!" % time.strftime("%a, %d %b %Y %H:%M:%S %Z", time.localtime()) print " Refusing to reset time on this server to avoid SSL certificate problems." print " Please check time on server %s" % server sys.exit(1) except socket.error: print "Warning: Could not retrive time from %s via NTP" % server def check_for_phase_II(ucr, lp, ad_server_ip): ## Check if we are in Phase II and the AD server is already switched off: if "hosts/static/%s" % ad_server_ip in ucr: ad_server_fqdn, ad_server_name = ucr["hosts/static/%s" % ad_server_ip].split() ## Check if the AD server is already in the local SAM db samdb = SamDB(os.path.join(samba_private_dir, "sam.ldb"), session_info=system_session(lp), lp=lp) msgs = samdb.search(base=ucr["samba4/ldap/base"], scope=samba.ldb.SCOPE_SUBTREE, expression="(sAMAccountName=%s$)" % ad_server_name, attrs=["objectSid"]) if msgs: return (1, ad_server_fqdn, ad_server_name) else: return (2, ad_server_fqdn, ad_server_name) return (0, None, None) def run_phaseI(opts, args): ad_server_name = None if len(args) > 0: ad_server_ip = args[0] if len(args) == 2: ad_server_name = args[1] elif len(args) != 1: parser.print_usage() sys.exit(1) devnull = open('/dev/null', 'w') ucr = config_registry.ConfigRegistry() ucr.load() local_fqdn = '.'.join((ucr["hostname"], ucr["domainname"])) # lp = LoadParm() # lp.load('/etc/samba/smb.conf') lp = sambaopts.get_loadparm() ### First plausibility checks ## 1.a Check that local domainname matches kerberos realm if ucr["domainname"].lower() != ucr["kerberos/realm"].lower(): print "Mismatching DNS domain and kerberos realm. Please reinstall the server with the same Domain as your AD" sys.exit(1) ## 1.b ping the given AD server IP print "Pinging AD IP %s: " % ad_server_ip, p1 = subprocess.Popen(["fping", ad_server_ip], stdout=devnull, stderr=devnull) rc= p1.poll() while rc is None: sys.stdout.write(".") sys.stdout.flush() time.sleep(1) rc= p1.poll() print if rc != 0: ## Check if we are in Phase II and the AD server is already switched off: (rc, tmp_fqdn, tmp_name) = check_for_phase_II(ucr, lp, ad_server_ip) if rc == 0: print "Error: Server IP %s not reachable" % ad_server_ip elif rc == 1: print "Note: The AD Server IP %s not reachable" % ad_server_ip print "Error: But found the AD DC %s account already in the Samba 4 SAM backend" % tmp_name print " Looks like it was switched of to finalize the migration?" print " If this is true, then restart this script with option --fsmo-takeover" elif rc == 2: print "Error: Server IP %s not reachable" % ad_server_ip print "Error: It seems that this script was run once already for the first migration step," print " but the server %s cannot be found in the local Samba SAM database." % tmp_name print " Don't know how to continue, giving up at this point." sys.exit(1) else: print "Ok, Server IP %s is online." % ad_server_ip ## 1.c Check, if a AD DNS domain is given and if it matches the local one ad_server_fqdn = None if ad_server_name: char_idx = ad_server_name.find(".") if char_idx == -1: ad_server_fqdn = "%s.%s" % (ad_server_name, ucr["domainname"]) else: ad_server_fqdn = ad_server_name else: try: p1 = subprocess.Popen(["dig", "@%s" % ad_server_ip, "SRV", "_kerberos._tcp.dc._msdcs.%s" % ucr["domainname"], "+short"], stdout=subprocess.PIPE) (stdout, stderr) = p1.communicate() except: print "Error: Failed to determine DC for IP %s" % ad_server_ip print "Please retry by passing the AD server name as additional command line argument" sys.exit(1) lines = stdout.rstrip('\n').split('\n') if len(lines) != 1: print "Warning: Multiple DCs registered for DNS SRV record _kerberos._tcp.dc._msdcs.%s" % ucr["domainname"] local_fqdn_found_in_AD_SRV = False for line in lines: tmp_fqdn = line.split()[3].rstrip('.') if tmp_fqdn == local_fqdn: print "Warning: This UCS server is already registered as DC at the DNS server %s" % ad_server_ip local_fqdn_found_in_AD_SRV = True continue else: p1 = subprocess.Popen(["dig", "@%s" % ad_server_ip, tmp_fqdn, "+short"], stdout=subprocess.PIPE) (stdout, stderr) = p1.communicate() if stdout.strip('\n') == ad_server_ip: if not ad_server_fqdn: ad_server_fqdn = tmp_fqdn else: print "Error: More than one of the registered DC FQDNs matches the given AD server IP:" print " %s" % ad_server_fqdn print " %s" % tmp_fqdn print "Error: Failed to determine DC for IP %s" % ad_server_ip print "Please retry by passing the AD server name as additional command line argument" sys.exit(1) if not ad_server_fqdn: print "Error: Failed to determine DC for IP %s" % ad_server_ip print "Please retry by passing the AD server name as additional command line argument" sys.exit(1) else: print "Sucessfull determined AD DC FQDN %s for given IP %s" % (ad_server_fqdn, ad_server_ip) if local_fqdn_found_in_AD_SRV and ad_server_fqdn: ## Check if we are in Phase II and the AD server is already switched off: (rc, tmp_fqdn, tmp_name) = check_for_phase_II(ucr, lp, ad_server_ip) if rc == 0: pass elif rc == 1: print "Error: Account for the AD DC %s is already the Samba 4 SAM backend." % tmp_name print " It seems that this script was run once already for the first migration step." print " If this is true, then go over to the AD DC to migrate the SYSVOL," print " and switch it off before restarting this script with option --fsmo-takeover" sys.exit(1) elif rc == 2: print "Error: It seems that this script was run once already for the first migration step," print " but the server %s cannot be found in the local Samba SAM database." % tmp_name print " Don't know how to continue, giving up at this point." sys.exit(1) else: ## OK, we have a unique match ad_server_fqdn = lines[0].split()[3] ad_server_fqdn = ad_server_fqdn.rstrip('.') print "Sucessfull determined AD DC FQDN %s for given IP %s" % (ad_server_fqdn, ad_server_ip) char_idx = ad_server_fqdn.find(".") if char_idx == -1: print "Error: AD server did not return FQDN for IP %s" % ad_server_ip print "Please retry by passing the AD server name as additional command line argument" sys.exit(1) elif not ad_server_fqdn[char_idx+1:] == ucr["domainname"]: print "Error: local DNS domain %s does not match AD server DNS domain." % ucr["domainname"] sys.exit(1) else: ad_server_name = ad_server_fqdn.split('.', 1)[0] ## 2. Check, if there is a DNS Server running at ad_server_ip which is ## able to resolve ad_server_fqdn p1 = subprocess.Popen(["dig", "@%s" % ad_server_ip, ad_server_fqdn, "+short"], stdout=subprocess.PIPE) (stdout, stderr) = p1.communicate() if not stdout.strip('\n') == ad_server_ip: print "Error: Cannot resolve DNS name %s using DNS server %s" % (ad_server_fqdn, ad_server_ip) print " Please check DNS name, IP or configuration." sys.exit(1) ## 3. Check, if the given AD Credentials work creds = credopts.get_credentials(lp) if creds.get_username() == "root": creds.set_username("Administrator") ad_join_user = creds.get_username() ad_join_password = creds.get_password() try: remote_samdb = SamDB("ldap://%s" % ad_server_ip, credentials=creds, session_info=system_session(lp), lp=lp) except: sys.exit(1) ## 4. Determine Site of given server, important for locale-dependend names like "Standardname-des-ersten-Standorts" sitename = None msgs = remote_samdb.search(base=ucr["samba4/ldap/base"], scope=samba.ldb.SCOPE_SUBTREE, expression="(sAMAccountName=%s$)" % ad_server_name, attrs=["serverReferenceBL"]) if msgs: obj = msgs[0] serverReferenceBL = obj["serverReferenceBL"][0] serverReferenceBL_RDNs = ldap.explode_dn(serverReferenceBL) serverReferenceBL_RDNs.reverse() config_partition_index = None site_container_index = None for i in xrange(len(serverReferenceBL_RDNs)): if site_container_index: sitename = serverReferenceBL_RDNs[i].split('=', 1)[1] break elif config_partition_index and serverReferenceBL_RDNs[i] == "CN=Sites": site_container_index = i elif not site_container_index and serverReferenceBL_RDNs[i] == "CN=Configuration": config_partition_index = i i = i+1 print "Located server %s site %s in AD SAM" % (ad_server_fqdn, sitename) ## OK, we are quite shure that we have the basics right, note the AD server IP and FQDN in UCR for phase II config_registry.handler_set(["hosts/static/%s=%s %s" % (ad_server_ip, ad_server_fqdn, ad_server_name) ]) ### Phase I.a: Join to AD ## Essential: Sync the time sync_time(ad_server_ip) ## Stop the S4 Connector for phase I p = subprocess.Popen(["/etc/init.d/univention-s4-connector", "stop"], stdout=devnull, stderr=devnull) p.wait() ## Stop Samba p = subprocess.Popen(["/etc/init.d/samba4", "stop"]) p.wait() ## Move current Samba directory out of the way if os.path.exists(samba_dir): backup_samba_dir = "%s.bak" % samba_private_dir if not os.path.exists(backup_samba_dir): os.rename(samba_private_dir, backup_samba_dir) os.makedirs(samba_private_dir) else: shutil.rmtree(samba_private_dir) os.mkdir(samba_private_dir) ## Adjust some UCR settings if "nameserver1/local" in ucr: nameserver1_orig = ucr["nameserver1/local"] else: nameserver1_orig = ucr["nameserver1"] config_registry.handler_set(["nameserver1/local=%s" % nameserver1_orig, "nameserver1=%s" % ad_server_ip]) config_registry.handler_set(["directory/manager/web/modules/users/user/properties/username/syntax=string"]) config_registry.handler_set(["dns/backend=ldap"]) ucr.load() ## Stop the NSCD p = subprocess.Popen(["/etc/init.d/nscd", "stop"]) p.wait() ## Restart bind9 to use the OpenLDAP backend, just to be sure p = subprocess.Popen(["/etc/init.d/bind9", "restart"]) p.wait() ## Get machine credentials try: machine_secret = open('/etc/machine.secret','r').read().strip() except IOError, e: univention.debug.debug(univention.debug.ADMIN, univention.debug.ERROR, 'Could not read machine credentials: %s', str(e)) sys.exit(1) ## Join into the domain if sitename: p = subprocess.Popen(["/usr/sbin/samba-tool", "domain", "join", ucr["domainname"], "DC", "-U%s%%%s" % (ad_join_user, ad_join_password), "--realm=%s" % ucr["kerberos/realm"], "--machinepass=%s" % machine_secret, "--server=%s" % ad_server_fqdn, "--site=%s" % sitename]) if p.wait() != 0: sys.exit(1) else: print "Error: Cannot determine site for server %s" % ad_server_fqdn sys.exit(1) ## Fix some attributes in local SamDB ad_domainsid = None samdb = SamDB(os.path.join(samba_private_dir, "sam.ldb"), session_info=system_session(lp), lp=lp) msgs = samdb.search(base=ucr["samba4/ldap/base"], scope=samba.ldb.SCOPE_BASE, expression="(objectClass=domain)", attrs=["objectSid"]) if msgs: obj = msgs[0] ad_domainsid = str(ndr_unpack(security.dom_sid, obj["objectSid"][0])) if not ad_domainsid: print "Error: Could not determine domain SID" sys.exit(1) old_domainsid = None lo = _connect_ucs(ucr) ldap_result = lo.search(filter="(&(objectClass=sambaDomain)(sambaDomainName=%s))" % ucr["windows/domain"], attr=["sambaSID"]) if len(ldap_result) == 1: ucs_object_dn = ldap_result[0][0] if os.path.exists("/var/tmp/old_sambasid"): f = open("/var/tmp/old_sambasid", 'r') old_domainsid = f.read() f.close() else: old_domainsid = ldap_result[0][1]["sambaSID"][0] f = open("/var/tmp/old_sambasid", 'w') f.write("%s" % old_domainsid) f.close() elif len(ldap_result) > 0: print 'ERROR: Found more than one sambaDomain object with sambaDomainName=%s' % ucr["windows/domain"] else: print 'ERROR: Did not find a sambaDomain object with sambaDomainName=%s' % ucr["windows/domain"] print "Replacing OLD sambaSID: %s" % old_domainsid if old_domainsid != ad_domainsid: ml = [("sambaSID", old_domainsid, ad_domainsid)] lo.modify(ucs_object_dn, ml) operatingSystem_attribute(ucr, samdb) ## Fix some attributes in SecretsDB secretsdb = samba.Ldb(os.path.join(samba_private_dir, "secrets.ldb"), session_info=system_session(lp), lp=lp) let_samba4_manage_etc_krb5_keytab(ucr, secretsdb) fqdn = "%s.%s" % (ucr['hostname'], ucr['domainname']) spn_list = ("host/%s" % fqdn, "ldap/%s" % fqdn) add_servicePrincipals(ucr, secretsdb, spn_list) ## Set Samba domain password settings. Note: rotation of passwords will only work with UCS 3.1, so max password age must be disabled for now. p = subprocess.Popen(["samba-tool", "domain", "passwordsettings", "set", "--history-length=3", "--min-pwd-age=0", "--max-pwd-age=0"]) p.wait() ## Start Samba p = subprocess.Popen(["/etc/init.d/samba4", "start"]) p.wait() ### Phase I.b: Pre-Map SIDs (locale adjustment etc.) ## construct locale mapping ucs_ad_name_map = { } for (name, rid) in well_known_domain_rids.items(): msgs = samdb.search(base=ucr["samba4/ldap/base"], scope=samba.ldb.SCOPE_SUBTREE, expression="(objectSid=%s-%s)" % (ad_domainsid, rid), attrs=["sAMAccountName"]) if msgs: obj = msgs[0] ad_object_name = obj["sAMAccountName"][0] ## Special Mapping for UCS if name == "Domain Computers": name ="Windows Hosts" if ad_object_name != name: ucs_ad_name_map[name] = ad_object_name config_registry.handler_set(["connector/s4/mapping/group/table/%s=%s" % (name, ad_object_name)]) ## construct dict of old UCS sambaSIDs old_sambaSID_dict = {} samba_sid_map = {} ## Users and Computers ldap_result = lo.search(filter="(&(objectClass=sambaSamAccount)(sambaSID=*))", attr=["uid", "sambaSID"]) for record in ldap_result: (ucs_object_dn, ucs_object_dict) = record old_sid = ucs_object_dict["sambaSID"][0] ucs_name = ucs_object_dict["uid"][0] if old_sid.startswith(old_domainsid): old_sambaSID_dict[old_sid] = ucs_name ## lookup new sid new_sid = None if ucs_name in ucs_ad_name_map: lookup_name = ucs_ad_name_map[ucs_name] else: lookup_name = ucs_name msgs = samdb.search(base=ucr["samba4/ldap/base"], scope=samba.ldb.SCOPE_SUBTREE, expression="(sAMAccountName=%s)" % lookup_name, attrs=["objectSid"]) if msgs: obj = msgs[0] new_sid = str(ndr_unpack(security.dom_sid, obj["objectSid"][0])) samba_sid_map[old_sid] = new_sid if new_sid: if opts.verbose: print "Rewriting user %s SID %s to %s" % (old_sambaSID_dict[old_sid], old_sid, new_sid) ml = [("sambaSID", old_sid, new_sid)] lo.modify(ucs_object_dn, ml) ## Groups ldap_result = lo.search(filter="(&(objectClass=sambaGroupMapping)(sambaSID=*))", attr=["cn", "sambaSID"]) for record in ldap_result: (ucs_object_dn, ucs_object_dict) = record old_sid = ucs_object_dict["sambaSID"][0] ucs_name = ucs_object_dict["cn"][0] if old_sid.startswith(old_domainsid): old_sambaSID_dict[old_sid] = ucs_name ## lookup new sid new_sid = None if ucs_name in ucs_ad_name_map: lookup_name = ucs_ad_name_map[ucs_name] else: lookup_name = ucs_name msgs = samdb.search(base=ucr["samba4/ldap/base"], scope=samba.ldb.SCOPE_SUBTREE, expression="(sAMAccountName=%s)" % lookup_name, attrs=["objectSid"]) if msgs: obj = msgs[0] new_sid = str(ndr_unpack(security.dom_sid, obj["objectSid"][0])) samba_sid_map[old_sid] = new_sid if new_sid: if opts.verbose: print "Rewriting group '%s' SID %s to %s" % (old_sambaSID_dict[old_sid], old_sid, new_sid) ml = [("sambaSID", old_sid, new_sid)] lo.modify(ucs_object_dn, ml) ldap_result = lo.search(filter="(sambaPrimaryGroupSID=*)", attr=["sambaPrimaryGroupSID"]) for record in ldap_result: (ucs_object_dn, ucs_object_dict) = record old_sid = ucs_object_dict["sambaPrimaryGroupSID"][0] if old_sid.startswith(old_domainsid): if old_sid in samba_sid_map: ml = [("sambaPrimaryGroupSID", old_sid, samba_sid_map[old_sid])] lo.modify(ucs_object_dn, ml) else: if old_sid in old_sambaSID_dict: print "Error: Could not find new sambaPrimaryGroupSID for %s" % old_sambaSID_dict[old_sid] else: print "Error: Unknown sambaPrimaryGroupSID %s" % old_sid ### Pre-Create mail domains for all mail and proxyAddresses: samdb = SamDB(os.path.join(samba_private_dir, "sam.ldb"), session_info=system_session(lp), lp=lp) msgs = samdb.search(base=ucr["samba4/ldap/base"], scope=samba.ldb.SCOPE_SUBTREE, expression="(|(mail=*)(proxyAddresses=*))", attrs=["mail", "proxyAddresses"]) maildomains = [] for msg in msgs: for attr in ("mail", "proxyAddresses"): if attr in msg: for address in msg[attr]: char_idx = address.find("@") if char_idx != -1: domainpart = address[char_idx+1:].lower() if not domainpart.endswith(".local"): if not domainpart in maildomains: maildomains.append(domainpart) for maildomain in maildomains: p = subprocess.Popen(["/usr/sbin/univention-directory-manager", "mail/domain", "create", "--ignore_exists", "--position", "cn=domain,cn=mail,%s" % ucr["ldap/base"], "--set" , "name=%s" % maildomain]) p.wait() ### Copy UCS AdministratorPassword to S4 Administrator object ### (Account is disabled in SBS 2008 by default and the password might be unknown) ## workaround for samba ndr parsing traceback p = subprocess.Popen(["samba-tool", "user", "setpassword", "Administrator", "--newpassword=DummyPW123"]) ## will be overwritten in the next step p.wait() p = subprocess.Popen(["/usr/sbin/univention-password_sync_ucs_to_s4", "Administrator"]) if p.wait() != 0: ## retry logic from 97univention-s4-connector.inst join script p = subprocess.Popen(["/etc/init.d/samba4", "restart"]) p.wait() time.sleep(3) p = subprocess.Popen(["/usr/sbin/univention-password_sync_ucs_to_s4", "Administrator"]) p.wait() ## TODO: fix proxyAddresses mapping config_registry.handler_set(["connector/s4/mapping/mailAlternativeAddress=False"]) ### Phase I.c: Run S4 Connector old_sleep = ucr.get("connector/s4/poll/sleep", "5") old_retry = ucr.get("connector/s4/retryrejected", "10") config_registry.handler_set(["connector/s4/poll/sleep=1", "connector/s4/retryrejected=2"]) p = subprocess.Popen(["/usr/share/univention-s4-connector/msgpo.py", "--write2ucs"]) p.wait() time.sleep(3) ## wait for listener ## Reset S4 Connector and handler state p = subprocess.Popen(["/etc/init.d/univention-directory-listener", "stop"]) p.wait() if os.path.exists("/var/lib/univention-directory-listener/handlers/s4-connector"): os.unlink("/var/lib/univention-directory-listener/handlers/s4-connector") # if os.path.exists("/var/lib/univention-directory-listener/handlers/samba4-idmap"): # os.unlink("/var/lib/univention-directory-listener/handlers/samba4-idmap") if os.path.exists("/etc/univention/connector/s4internal.sqlite"): os.unlink("/etc/univention/connector/s4internal.sqlite") for foldername in ("/var/lib/univention-connector/s4", "/var/lib/univention-connector/s4/tmp"): for entry in os.listdir(foldername): filename = os.path.join(foldername, entry) try: if os.path.isfile(filename): os.unlink(filename) except Exception, e: print "Error removing file: %s" % str(e) p = subprocess.Popen(["/etc/init.d/univention-directory-listener", "start"]) p.wait() time.sleep(10) ## wait for listener print "Starting S4 Connector" p = subprocess.Popen(["/etc/init.d/univention-s4-connector", "start"]) p.wait() ## Reset normal relication intervals config_registry.handler_set(["connector/s4/poll/sleep=%s" % old_sleep, "connector/s4/retryrejected=%s" % old_retry]) print "Waiting 30 sec for S4 Connector sync" time.sleep(30) ## rebuild idmap p = subprocess.Popen(["/usr/lib/univention-directory-listener/system/samba4-idmap.py", "--direct-resync"], stdout=devnull, stderr=devnull) p.wait() print "TODO: Determine replication status" ## Wait for Sync to finish # pattern = re.compile(r"(^.+\n-{38}\ntry to sync \d+ changes from UCS\ndone: .+\nChanges from UCS: (\d+) \((\d+) saved rejected\)\n-{38}\n-{38}\ntry to sync \d+ changes from S4\ndone: .+\nChanges from S4: (\d+) \((\d+) saved rejected\)\n-{38}\n)", re.MULTILINE) # changes_from_ucs = changes_from_s4 = 1 # while changes_from_ucs > 0 or changes_from_s4 > 0: # time.sleep(5) # with open("/var/log/univention/connector-s4-status.log", "r") as f: # status_log = f.read() # m = pattern.match(status_log) # changes_from_ucs = m.group(2) # rejects_from_ucs = m.group(3) # changes_from_s4 = m.group(4) # rejects_from_s4 = m.group(5) ### Phase II: AD-Side Sync print "TODO: Ask user to robocopy sysvol" print "TODO: Ask user to robocopy server side homedirs (?)" # print "TODO: Profile kopieren: z.B. nach /home/$user/windows-profiles/default.V2" print "TODO: Ask user to switch off AD server" sys.exit(1) def run_phaseIII(opts, args): ad_server_name = None if len(args) > 0: ad_server_ip = args[0] if len(args) == 2: ad_server_name = args[1] elif len(args) != 1: parser.print_usage() sys.exit(1) devnull = open('/dev/null', 'w') ucr = config_registry.ConfigRegistry() ucr.load() local_fqdn = '.'.join((ucr["hostname"], ucr["domainname"])) # lp = LoadParm() # lp.load('/etc/samba/smb.conf') lp = sambaopts.get_loadparm() ### First plausibility checks ## 1.a check if the given IP was mapped to a host name via UCR in Phase I (rc, phaseI_fqdn, phaseI_name) = check_for_phase_II(ucr, lp, ad_server_ip) if rc == 0: print "Error: given IP %s was not mapped to a hostname in phase I" print " Please complete phase I of the migration before initiating the FSMO takeover" sys.exit(1) elif rc == 1: print "OK, Found the AD DC %s account in the local Samba 4 SAM backend" % phaseI_name elif rc == 2: print "Error: It seems that this script was run once already for the first migration step," print " but the server %s cannot be found in the local Samba SAM database." % phaseI_name print " Don't know how to continue, giving up at this point." print " Maybe the steps needed for migration have been finished already?" sys.exit(1) ## 1.b Check, if a AD DNS domain is given and if it matches the one given before ad_server_fqdn = None if ad_server_name: char_idx = ad_server_name.find(".") if char_idx == -1: ad_server_fqdn = "%s.%s" % (ad_server_name, ucr["domainname"]) else: ad_server_fqdn = ad_server_name ad_server_name = ad_server_fqdn.split('.', 1)[0] if ad_server_name != phaseI_name: print "Error: Given AD server name %s does not match the one recorded for IP %s in phase I: %s" % (ad_server_name, ad_server_ip, phaseI_name) print " Consider not explicetely passing an AD server name." sys.exit(1) else: ad_server_fqdn = phaseI_fqdn ad_server_name = phaseI_name ## 1.c Check that local domainname matches kerberos realm if ucr["domainname"].lower() != ucr["kerberos/realm"].lower(): print "Mismatching DNS domain and kerberos realm. Please reinstall the server with the same Domain as your AD" sys.exit(1) ## 1.d ping the given AD server IP print "Pinging AD IP %s: " % ad_server_ip, p1 = subprocess.Popen(["fping", ad_server_ip], stdout=devnull, stderr=devnull) rc= p1.poll() while rc is None: sys.stdout.write(".") sys.stdout.flush() time.sleep(1) rc= p1.poll() print if rc == 0: print "Error: The server IP %s is still reachable" % ad_server_ip print " Return to the AD DC to migrate the SYSVOL, and then" print " switch it off before restarting this script with option --fsmo-takeover" sys.exit(1) else: print "Ok, Server IP %s unreachable." % ad_server_ip ### Phase III: Promote to FSMO master and DNS server ## 1. Determine Site of local server, important for locale-dependend names like "Standardname-des-ersten-Standorts" sitename = None samdb = SamDB(os.path.join(samba_private_dir, "sam.ldb"), session_info=system_session(lp), lp=lp) msgs = samdb.search(base=ucr["samba4/ldap/base"], scope=samba.ldb.SCOPE_SUBTREE, expression="(sAMAccountName=%s$)" % ucr["hostname"], attrs=["serverReferenceBL"]) if msgs: obj = msgs[0] serverReferenceBL = obj["serverReferenceBL"][0] serverReferenceBL_RDNs = ldap.explode_dn(serverReferenceBL) serverReferenceBL_RDNs.reverse() config_partition_index = None site_container_index = None for i in xrange(len(serverReferenceBL_RDNs)): if site_container_index: sitename = serverReferenceBL_RDNs[i].split('=', 1)[1] break elif config_partition_index and serverReferenceBL_RDNs[i] == "CN=Sites": site_container_index = i elif not site_container_index and serverReferenceBL_RDNs[i] == "CN=Configuration": config_partition_index = i i = i+1 print "Located server %s site %s in Samba4 SAM" % (ucr["hostname"], sitename) ## Re-Set Defaul NTACLs on sysvol p = subprocess.Popen(["/usr/share/univention-samba4/scripts/set_sysvol_ntacl.py", sysvol_path], stdout=devnull, stderr=devnull) p.wait() ## Re-set default fACLs so sysvol-sync can read files and directories p = subprocess.Popen(["setfacl", "-R", "-P", "-m", "g:Authenticated Users:r-x,d:g:Authenticated Users:r-x", sysvol_path]) if p.wait() != 0: print "Error: Could not set fACL for %s" % sysvol_path print "Warning: Continuing anyway. Please fix later:" print " setfacl -R -P -m 'g:Authenticated Users:r-x,d:g:Authenticated Users:r-x' %s" % sysvol_path ## Add DNS records to UDM: p = subprocess.Popen(["/usr/share/univention-samba4/scripts/setup-dns-in-ucsldap.sh", "--dc", "--pdc", "--gc", "--site=%s" % sitename]) p.wait() ## Replace DNS host record for AD Server name by DNS Alias p = subprocess.Popen(["univention-directory-manager", "dns/host_record", "delete", "", "--superordinate", "zoneName=%s,cn=dns,%s" % (ucr["domainname"], ucr["ldap/base"]), "--dn", "relativeDomainName=%s,zoneName=%s,cn=dns,%s" % (ad_server_name, ucr["domainname"], ucr["ldap/base"]) ], stdout=devnull, stderr=devnull) p.wait() p = subprocess.Popen(["univention-directory-manager", "dns/alias", "create", "--superordinate", "zoneName=%s,cn=dns,%s" % (ucr["domainname"], ucr["ldap/base"]), "--set", "name=%s" % ad_server_name, "--set", "cname=%s" % local_fqdn]) p.wait() ## Create NETBIOS Alias f = open('/etc/samba/local.conf', 'a') f.write('[global]\nnetbios aliases = "%s"\n' % ad_server_name) f.close() p = subprocess.Popen(["/usr/sbin/univention-config-registry", "commit", "/etc/samba/smb.conf"]) p.wait() ## Resolve against local Bind9 ## use OpenLDAP backend until the S4 Connector has run if "nameserver1/local" in ucr: nameserver1_orig = ucr["nameserver1/local"] config_registry.handler_set(["nameserver1=%s" % nameserver1_orig]) ## unset temporary variable config_registry.handler_unset(["nameserver1/local"]) else: print "Warning: Weird, unable to determine previous nameserver1..." print " Using localhost as fallback, probably that's the right thing to do." config_registry.handler_set(["nameserver1=127.0.0.1"]) ## Use Samba4 as DNS backend config_registry.handler_set(["dns/backend=samba4"]) p = subprocess.Popen(["/etc/init.d/samba4", "restart"]) p.wait() time.sleep(3) ## Restart Bind (get's restarted by samba4 if running) #p = subprocess.Popen(["/etc/init.d/bind9", "restart"]) #p.wait() ## Start NSCD again p = subprocess.Popen(["/etc/init.d/nscd", "start"]) p.wait() ## Claim FSMO roles print "Claiming FSMO roles" for fsmo_role in ('rid', 'pdc', 'infrastructure', 'schema', 'naming'): p = subprocess.Popen(["samba-tool", "fsmo", "seize", "--role=%s" % fsmo_role, "--force"]) p.wait() ## Restart Samba p = subprocess.Popen(["/etc/init.d/samba4", "restart"]) p.wait() ## re-create /etc/krb5.keytab ## https://forge.univention.org/bugzilla/show_bug.cgi?id=27426 p = subprocess.Popen(["/usr/share/univention-samba4/scripts/create-keytab.sh"]) p.wait() ## Cleanup necessary for NETBIOS print "Cleaning up:" print "Removing AD DC account from local Samba4 SAM database" p = subprocess.Popen(["/usr/bin/ldbdel", "-r", "-H", os.path.join(samba_private_dir, "sam.ldb"), "CN=%s,CN=Domain System Volume (SYSVOL share),CN=File Replication Service,CN=System,%s" % (ad_server_name, ucr["samba4/ldap/base"])], stdout=devnull, stderr=devnull) p.wait() p = subprocess.Popen(["/usr/bin/ldbdel", "-r", "-H", os.path.join(samba_private_dir, "sam.ldb"), "CN=%s,CN=Servers,CN=%s,CN=Sites,CN=Configuration,%s" % (ad_server_name, sitename, ucr["samba4/ldap/base"])]) p.wait() p = subprocess.Popen(["/usr/bin/ldbdel", "-r", "-H", os.path.join(samba_private_dir, "sam.ldb"), "CN=%s,OU=Domain Controllers,%s" % (ad_server_name, ucr["samba4/ldap/base"])]) p.wait() ## Finally, for consistency remove AD DC object from UDM print "Removing AD DC account from local Univention Directory Manager" p = subprocess.Popen(["univention-directory-manager", "computers/windows_domaincontroller", "delete", "--dn", "cn=%s,cn=dc,cn=computers,%s" % (ad_server_name, ucr["ldap/base"]) ]) p.wait() ## finally run KCC p = subprocess.Popen(["/usr/sbin/samba-tool", "drs", "kcc"]) p.wait() if __name__ == '__main__': parser = OptionParser("%prog [options] []") parser.add_option("-v", "--verbose", action="store_true") parser.add_option("--fsmo-takeover", action="store_true") sambaopts = samba.getopt.SambaOptions(parser) parser.add_option_group(sambaopts) parser.add_option_group(samba.getopt.VersionOptions(parser)) # use command line creds if available credopts = samba.getopt.CredentialsOptions(parser) parser.add_option_group(credopts) opts, args = parser.parse_args() if not opts.fsmo_takeover: run_phaseI(opts, args) else: run_phaseIII(opts, args)