gpo: Add a restore command (for backups) from XML
authorGarming Sam <garming@catalyst.net.nz>
Mon, 21 May 2018 05:30:40 +0000 (17:30 +1200)
committerAndrew Bartlett <abartlet@samba.org>
Thu, 16 Aug 2018 21:42:21 +0000 (23:42 +0200)
Currently because no parsers have been written, this just copies the old
files and puts them in their places.

Signed-off-by: Garming Sam <garming@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
python/samba/netcmd/gpo.py

index e7095fc0b864646d869abbb07beb5db044f54e3f..2f9e73292720d4a07449621d10a9c667ab0168a1 100644 (file)
@@ -23,6 +23,8 @@ import os
 import samba.getopt as options
 import ldb
 import re
+import xml.etree.ElementTree as ET
+import shutil
 
 from samba.auth import system_session
 from samba.netcmd import (
@@ -41,6 +43,7 @@ from samba.auth import AUTH_SESSION_INFO_DEFAULT_GROUPS, AUTH_SESSION_INFO_AUTHE
 from samba.netcmd.common import netcmd_finddc
 from samba import policy
 from samba import smb
+from samba import NTSTATUSError
 import uuid
 from samba.ntacls import dsacl2fsacl
 from samba.dcerpc import nbt
@@ -297,7 +300,8 @@ def copy_directory_remote_to_local(conn, remotedir, localdir):
                 open(l_name, 'w').write(data)
 
 
-def copy_directory_local_to_remote(conn, localdir, remotedir):
+def copy_directory_local_to_remote(conn, localdir, remotedir,
+                                   ignore_existing=False):
     if not conn.chkpath(remotedir):
         conn.mkdir(remotedir)
     l_dirs = [ localdir ]
@@ -315,7 +319,11 @@ def copy_directory_local_to_remote(conn, localdir, remotedir):
             if os.path.isdir(l_name):
                 l_dirs.append(l_name)
                 r_dirs.append(r_name)
-                conn.mkdir(r_name)
+                try:
+                    conn.mkdir(r_name)
+                except NTSTATUSError:
+                    if not ignore_existing:
+                        raise
             else:
                 data = open(l_name, 'r').read()
                 conn.savefile(r_name, data)
@@ -1037,6 +1045,9 @@ class cmd_create(Command):
         # Create new GUID
         guid  = str(uuid.uuid4())
         gpo = "{%s}" % guid.upper()
+
+        self.gpo_name = gpo
+
         realm = cldap_ret.dns_domain
         unc_path = "\\\\%s\\sysvol\\%s\\Policies\\%s" % (realm, realm, gpo)
 
@@ -1045,12 +1056,14 @@ class cmd_create(Command):
             tmpdir = "/tmp"
         if not os.path.isdir(tmpdir):
             raise CommandError("Temporary directory '%s' does not exist" % tmpdir)
+        self.tmpdir = tmpdir
 
         localdir = os.path.join(tmpdir, "policy")
         if not os.path.isdir(localdir):
             os.mkdir(localdir)
 
         gpodir = os.path.join(localdir, gpo)
+        self.gpodir = gpodir
         if os.path.isdir(gpodir):
             raise CommandError("GPO directory '%s' already exists, refusing to overwrite" % gpodir)
 
@@ -1065,11 +1078,14 @@ class cmd_create(Command):
 
         # Connect to DC over SMB
         [dom_name, service, sharepath] = parse_unc(unc_path)
+        self.sharepath = sharepath
         try:
             conn = smb.SMB(dc_hostname, service, lp=self.lp, creds=self.creds)
         except Exception as e:
             raise CommandError("Error connecting to '%s' using SMB" % dc_hostname, e)
 
+        self.conn = conn
+
         self.samdb.transaction_start()
         try:
             # Add cn=<guid>
@@ -1136,6 +1152,138 @@ class cmd_create(Command):
         self.outf.write("GPO '%s' created as %s\n" % (displayname, gpo))
 
 
+class cmd_restore(cmd_create):
+    """Restore a GPO to a new container."""
+
+    synopsis = "%prog <displayname> <backup location> [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+    }
+
+    takes_args = ['displayname', 'backup']
+
+    takes_options = [
+        Option("-H", help="LDB URL for database or target server", type=str),
+        Option("--tmpdir", help="Temporary directory for copying policy files", type=str),
+        Option("--entities", help="File defining XML entities to insert into DOCTYPE header", type=str)
+        ]
+
+    def restore_from_backup_to_local_dir(self, sourcedir, targetdir, dtd_header=''):
+        SUFFIX = '.SAMBABACKUP'
+
+        if not os.path.exists(targetdir):
+            os.mkdir(targetdir)
+
+        l_dirs = [ sourcedir ]
+        r_dirs = [ targetdir ]
+        while l_dirs:
+            l_dir = l_dirs.pop()
+            r_dir = r_dirs.pop()
+
+            dirlist = os.listdir(l_dir)
+            for e in dirlist:
+                l_name = os.path.join(l_dir, e)
+                r_name = os.path.join(r_dir, e)
+
+                if os.path.isdir(l_name):
+                    l_dirs.append(l_name)
+                    r_dirs.append(r_name)
+                    if not os.path.exists(r_name):
+                        os.mkdir(r_name)
+                else:
+                    if l_name.endswith('.xml'):
+                        # Restore the xml file if possible
+
+                        # Get the filename to find the parser
+                        to_parse = os.path.basename(l_name)[:-4]
+
+                        parser = find_parser(to_parse)
+                        try:
+                            with open(l_name, 'r') as ltemp:
+                                data = ltemp.read()
+                                # Load the XML file with the DTD (entity) header
+                                parser.load_xml(ET.fromstring(dtd_header + data))
+
+                                # Write out the substituted files in the output
+                                # location, ready to copy over.
+                                parser.write_binary(r_name[:-4])
+
+                        except GPNoParserException:
+                            # In the failure case, we fallback
+                            original_file = l_name[:-4] + SUFFIX
+                            shutil.copy2(original_file, r_name[:-4])
+
+                            self.outf.write('WARNING: No such parser for %s\n' % to_parse)
+                            self.outf.write('WARNING: Falling back to simple copy-restore.\n')
+                        except:
+                            import traceback
+                            traceback.print_exc()
+
+                            # In the failure case, we fallback
+                            original_file = l_name[:-4] + SUFFIX
+                            shutil.copy2(original_file, r_name[:-4])
+
+                            self.outf.write('WARNING: Error during parsing for %s\n' % l_name)
+                            self.outf.write('WARNING: Falling back to simple copy-restore.\n')
+
+    def run(self, displayname, backup, H=None, tmpdir=None, entities=None, sambaopts=None, credopts=None,
+            versionopts=None):
+
+        dtd_header = ''
+
+        if not os.path.exists(backup):
+            raise CommandError("Backup directory does not exist %s" % backup)
+
+        if entities is not None:
+            # DOCTYPE name is meant to match root element, but ElementTree does
+            # not seem to care, so this seems to be enough.
+
+            dtd_header = '<!DOCTYPE foobar [\n'
+
+            if not os.path.exists(entities):
+                raise CommandError("Entities file does not exist %s" %
+                                   entities)
+            with open(entities, 'r') as entities_file:
+                entities_content = entities_file.read()
+
+                # Do a basic regex test of the entities file format
+                if re.match('(\s*<!ENTITY\s*[a-zA-Z0-9_]+\s*.*?>)+\s*\Z',
+                            entities_content, flags=re.MULTILINE) is None:
+                    raise CommandError("Entities file does not appear to "
+                                       "conform to format\n"
+                                       'e.g. <!ENTITY entity "value">')
+                dtd_header += entities_content.strip()
+
+            dtd_header += '\n]>\n'
+
+        super(cmd_restore, self).run(displayname, H, tmpdir, sambaopts,
+                                    credopts, versionopts)
+
+        try:
+            # Iterate over backup files and restore with DTD
+            self.restore_from_backup_to_local_dir(backup, self.gpodir,
+                                                  dtd_header)
+
+            # Copy GPO files over SMB
+            copy_directory_local_to_remote(self.conn, self.gpodir,
+                                           self.sharepath,
+                                           ignore_existing=True)
+
+        except Exception as e:
+            import traceback
+            traceback.print_exc()
+            self.outf.write(str(e) + '\n')
+
+            self.outf.write("Failed to restore GPO -- deleting...\n")
+            cmd = cmd_del()
+            cmd.run(self.gpo_name, H, sambaopts, credopts, versionopts)
+
+            raise CommandError("Failed to restore: %s" % e)
+
+
 class cmd_del(Command):
     """Delete a GPO."""
 
@@ -1292,3 +1440,4 @@ class cmd_gpo(SuperCommand):
     subcommands["del"] = cmd_del()
     subcommands["aclcheck"] = cmd_aclcheck()
     subcommands["backup"] = cmd_backup()
+    subcommands["restore"] = cmd_restore()