diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 17:20:00 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 17:20:00 +0000 |
commit | 8daa83a594a2e98f39d764422bfbdbc62c9efd44 (patch) | |
tree | 4099e8021376c7d8c05bdf8503093d80e9c7bad0 /python/samba/netcmd/domain/schemaupgrade.py | |
parent | Initial commit. (diff) | |
download | samba-8daa83a594a2e98f39d764422bfbdbc62c9efd44.tar.xz samba-8daa83a594a2e98f39d764422bfbdbc62c9efd44.zip |
Adding upstream version 2:4.20.0+dfsg.upstream/2%4.20.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'python/samba/netcmd/domain/schemaupgrade.py')
-rw-r--r-- | python/samba/netcmd/domain/schemaupgrade.py | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/python/samba/netcmd/domain/schemaupgrade.py b/python/samba/netcmd/domain/schemaupgrade.py new file mode 100644 index 0000000..ff00a77 --- /dev/null +++ b/python/samba/netcmd/domain/schemaupgrade.py @@ -0,0 +1,350 @@ +# domain management - domain schemaupgrade +# +# Copyright Matthias Dieter Wallnoefer 2009 +# Copyright Andrew Kroeger 2009 +# Copyright Jelmer Vernooij 2007-2012 +# Copyright Giampaolo Lauria 2011 +# Copyright Matthieu Patou <mat@matws.net> 2011 +# Copyright Andrew Bartlett 2008-2015 +# Copyright Stefan Metzmacher 2012 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import os +import shutil +import subprocess +import tempfile + +import ldb +import samba.getopt as options +from samba.auth import system_session +from samba.netcmd import Command, CommandError, Option +from samba.netcmd.fsmo import get_fsmo_roleowner +from samba.provision import setup_path +from samba.samdb import SamDB + + +class ldif_schema_update: + """Helper class for applying LDIF schema updates""" + + def __init__(self): + self.is_defunct = False + self.unknown_oid = None + self.dn = None + self.ldif = "" + + def can_ignore_failure(self, error): + """Checks if we can safely ignore failure to apply an LDIF update""" + (num, errstr) = error.args + + # Microsoft has marked objects as defunct that Samba doesn't know about + if num == ldb.ERR_NO_SUCH_OBJECT and self.is_defunct: + print("Defunct object %s doesn't exist, skipping" % self.dn) + return True + elif self.unknown_oid is not None: + print("Skipping unknown OID %s for object %s" % (self.unknown_oid, self.dn)) + return True + + return False + + def apply(self, samdb): + """Applies a single LDIF update to the schema""" + + try: + try: + samdb.modify_ldif(self.ldif, controls=['relax:0']) + except ldb.LdbError as e: + if e.args[0] == ldb.ERR_INVALID_ATTRIBUTE_SYNTAX: + + # REFRESH after a failed change + + # Otherwise the OID-to-attribute mapping in + # _apply_updates_in_file() won't work, because it + # can't lookup the new OID in the schema + samdb.set_schema_update_now() + + samdb.modify_ldif(self.ldif, controls=['relax:0']) + else: + raise + except ldb.LdbError as e: + if self.can_ignore_failure(e): + return 0 + else: + print("Exception: %s" % e) + print("Encountered while trying to apply the following LDIF") + print("----------------------------------------------------") + print("%s" % self.ldif) + + raise + + return 1 + + +class cmd_domain_schema_upgrade(Command): + """Domain schema upgrading""" + + synopsis = "%prog [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("-q", "--quiet", help="Be quiet", action="store_true"), # unused + Option("-v", "--verbose", help="Be verbose", action="store_true"), + Option("--schema", type="choice", metavar="SCHEMA", + choices=["2012", "2012_R2", "2016", "2019"], + help="The schema file to upgrade to. Default is (Windows) 2019.", + default="2019"), + Option("--ldf-file", type=str, default=None, + help="Just apply the schema updates in the adprep/.LDF file(s) specified"), + Option("--base-dir", type=str, default=None, + help="Location of ldf files Default is ${SETUPDIR}/adprep.") + ] + + def _apply_updates_in_file(self, samdb, ldif_file): + """ + Applies a series of updates specified in an .LDIF file. The .LDIF file + is based on the adprep Schema updates provided by Microsoft. + """ + count = 0 + ldif_op = ldif_schema_update() + + # parse the file line by line and work out each update operation to apply + for line in ldif_file: + + line = line.rstrip() + + # the operations in the .LDIF file are separated by blank lines. If + # we hit a blank line, try to apply the update we've parsed so far + if line == '': + + # keep going if we haven't parsed anything yet + if ldif_op.ldif == '': + continue + + # Apply the individual change + count += ldif_op.apply(samdb) + + # start storing the next operation from scratch again + ldif_op = ldif_schema_update() + continue + + # replace the placeholder domain name in the .ldif file with the real domain + if line.upper().endswith('DC=X'): + line = line[:-len('DC=X')] + str(samdb.get_default_basedn()) + elif line.upper().endswith('CN=X'): + line = line[:-len('CN=X')] + str(samdb.get_default_basedn()) + + values = line.split(':') + + if values[0].lower() == 'dn': + ldif_op.dn = values[1].strip() + + # replace the Windows-specific operation with the Samba one + if values[0].lower() == 'changetype': + line = line.lower().replace(': ntdsschemaadd', + ': add') + line = line.lower().replace(': ntdsschemamodify', + ': modify') + line = line.lower().replace(': ntdsschemamodrdn', + ': modrdn') + line = line.lower().replace(': ntdsschemadelete', + ': delete') + + if values[0].lower() in ['rdnattid', 'subclassof', + 'systemposssuperiors', + 'systemmaycontain', + 'systemauxiliaryclass']: + _, value = values + + # The Microsoft updates contain some OIDs we don't recognize. + # Query the DB to see if we can work out the OID this update is + # referring to. If we find a match, then replace the OID with + # the ldapDisplayname + if '.' in value: + res = samdb.search(base=samdb.get_schema_basedn(), + expression="(|(attributeId=%s)(governsId=%s))" % + (value, value), + attrs=['ldapDisplayName']) + + if len(res) != 1: + ldif_op.unknown_oid = value + else: + display_name = str(res[0]['ldapDisplayName'][0]) + line = line.replace(value, ' ' + display_name) + + # Microsoft has marked objects as defunct that Samba doesn't know about + if values[0].lower() == 'isdefunct' and values[1].strip().lower() == 'true': + ldif_op.is_defunct = True + + # Samba has added the showInAdvancedViewOnly attribute to all objects, + # so rather than doing an add, we need to do a replace + if values[0].lower() == 'add' and values[1].strip().lower() == 'showinadvancedviewonly': + line = 'replace: showInAdvancedViewOnly' + + # Add the line to the current LDIF operation (including the newline + # we stripped off at the start of the loop) + ldif_op.ldif += line + '\n' + + return count + + def _apply_update(self, samdb, update_file, base_dir): + """Wrapper function for parsing an LDIF file and applying the updates""" + + print("Applying %s updates..." % update_file) + + ldif_file = None + try: + ldif_file = open(os.path.join(base_dir, update_file)) + + count = self._apply_updates_in_file(samdb, ldif_file) + + finally: + if ldif_file: + ldif_file.close() + + print("%u changes applied" % count) + + return count + + def run(self, **kwargs): + try: + from samba.ms_schema_markdown import read_ms_markdown + except ImportError as e: + self.outf.write("Exception in importing markdown: %s\n" % e) + raise CommandError('Failed to import module markdown') + from samba.schema import Schema + + updates_allowed_overridden = False + sambaopts = kwargs.get("sambaopts") + credopts = kwargs.get("credopts") + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + H = kwargs.get("H") + target_schema = kwargs.get("schema") + ldf_files = kwargs.get("ldf_file") + base_dir = kwargs.get("base_dir") + + temp_folder = None + + samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp) + + # we're not going to get far if the config doesn't allow schema updates + if lp.get("dsdb:schema update allowed") is None: + lp.set("dsdb:schema update allowed", "yes") + print("Temporarily overriding 'dsdb:schema update allowed' setting") + updates_allowed_overridden = True + + own_dn = ldb.Dn(samdb, samdb.get_dsServiceName()) + master = get_fsmo_roleowner(samdb, str(samdb.get_schema_basedn()), + 'schema') + if own_dn != master: + raise CommandError("This server is not the schema master.") + + # if specific LDIF files were specified, just apply them + if ldf_files: + schema_updates = ldf_files.split(",") + else: + schema_updates = [] + + # work out the version of the target schema we're upgrading to + end = Schema.get_version(target_schema) + + # work out the version of the schema we're currently using + res = samdb.search(base=samdb.get_schema_basedn(), + scope=ldb.SCOPE_BASE, attrs=['objectVersion']) + + if len(res) != 1: + raise CommandError('Could not determine current schema version') + start = int(res[0]['objectVersion'][0]) + 1 + + diff_dir = setup_path("adprep/WindowsServerDocs") + if base_dir is None: + # Read from the Schema-Updates.md file + temp_folder = tempfile.mkdtemp() + + update_file = setup_path("adprep/WindowsServerDocs/Schema-Updates.md") + + try: + read_ms_markdown(update_file, temp_folder) + except Exception as e: + print("Exception in markdown parsing: %s" % e) + shutil.rmtree(temp_folder) + raise CommandError('Failed to upgrade schema') + + base_dir = temp_folder + + for version in range(start, end + 1): + update = 'Sch%d.ldf' % version + schema_updates.append(update) + + # Apply patches if we parsed the Schema-Updates.md file + diff = os.path.abspath(os.path.join(diff_dir, update + '.diff')) + if temp_folder and os.path.exists(diff): + try: + p = subprocess.Popen(['patch', update, '-i', diff], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, cwd=temp_folder) + except (OSError, IOError): + shutil.rmtree(temp_folder) + raise CommandError("Failed to upgrade schema. " + "Is '/usr/bin/patch' missing?") + + stdout, stderr = p.communicate() + + if p.returncode: + print("Exception in patch: %s\n%s" % (stdout, stderr)) + shutil.rmtree(temp_folder) + raise CommandError('Failed to upgrade schema') + + print("Patched %s using %s" % (update, diff)) + + if base_dir is None: + base_dir = setup_path("adprep") + + samdb.transaction_start() + count = 0 + error_encountered = False + + try: + # Apply the schema updates needed to move to the new schema version + for ldif_file in schema_updates: + count += self._apply_update(samdb, ldif_file, base_dir) + + if count > 0: + samdb.transaction_commit() + print("Schema successfully updated") + else: + print("No changes applied to schema") + samdb.transaction_cancel() + except Exception as e: + print("Exception: %s" % e) + print("Error encountered, aborting schema upgrade") + samdb.transaction_cancel() + error_encountered = True + + if updates_allowed_overridden: + lp.set("dsdb:schema update allowed", "no") + + if temp_folder: + shutil.rmtree(temp_folder) + + if error_encountered: + raise CommandError('Failed to upgrade schema') |