e3ffab
From 5ec39e80a626e833d8fec24b1797b9c25c072784 Mon Sep 17 00:00:00 2001
e3ffab
From: Jan Cholasta <jcholast@redhat.com>
e3ffab
Date: Mon, 12 Jan 2015 12:44:21 +0000
e3ffab
Subject: [PATCH] Fix validation of ipa-restore options
e3ffab
e3ffab
Fix restore mode checks. Do some of the existing checks earlier to make them
e3ffab
effective. Check if --instance and --backend exist both in the filesystem and
e3ffab
in the backup.
e3ffab
e3ffab
Log backup type and restore mode before performing restore.
e3ffab
e3ffab
Update ipa-restore man page.
e3ffab
e3ffab
https://fedorahosted.org/freeipa/ticket/4797
e3ffab
e3ffab
Reviewed-By: Petr Vobornik <pvoborni@redhat.com>
e3ffab
Reviewed-By: Martin Kosek <mkosek@redhat.com>
e3ffab
---
e3ffab
 install/tools/man/ipa-restore.1  |  10 +--
e3ffab
 ipaserver/install/ipa_restore.py | 175 +++++++++++++++++++++++----------------
e3ffab
 2 files changed, 108 insertions(+), 77 deletions(-)
e3ffab
e3ffab
diff --git a/install/tools/man/ipa-restore.1 b/install/tools/man/ipa-restore.1
e3ffab
index 31734b259524e4b07312a4009184e725aafc3728..98242e246ab888f326af5ccfa6420fc080261891 100644
e3ffab
--- a/install/tools/man/ipa-restore.1
e3ffab
+++ b/install/tools/man/ipa-restore.1
e3ffab
@@ -64,17 +64,17 @@ Restore the data only. The default is to restore everything in the backup.
e3ffab
 The full path to a GPG keyring. The keyring consists of two files, a public and a private key (.sec and .pub respectively). Specify the path without an extension.
e3ffab
 .TP
e3ffab
 \fB\-\-no\-logs\fR
e3ffab
-Exclude the IPA service log files in the backup (if they were backed up). Applicable only with a full backup.
e3ffab
+Exclude the IPA service log files in the backup (if they were backed up).
e3ffab
 .TP
e3ffab
 \fB\-\-online\fR
e3ffab
-Perform the restore on\-line. Requires the \-\-data option.
e3ffab
+Perform the restore on\-line. Requires data\-only backup or the \-\-data option.
e3ffab
 .TP
e3ffab
 \fB\-\-instance\fR=\fIINSTANCE\fR
e3ffab
-The backend to restore within an instance or instances.
e3ffab
-.TP
e3ffab
-Restore only the databases in this 389\-ds instance. The default is to restore all found (at most this is the IPA REALM instance and the PKI\-IPA instance).
e3ffab
+Restore only the databases in this 389\-ds instance. The default is to restore all found (at most this is the IPA REALM instance and the PKI\-IPA instance). Requires data\-only backup or the \-\-data option.
e3ffab
 .TP
e3ffab
 \fB\-\-backend\fR=\fIBACKEND\fR
e3ffab
+The backend to restore within an instance or instances. Requires data\-only backup or the \-\-data option.
e3ffab
+.TP
e3ffab
 \fB\-\-v\fR, \fB\-\-verbose\fR
e3ffab
 Print debugging information
e3ffab
 .TP
e3ffab
diff --git a/ipaserver/install/ipa_restore.py b/ipaserver/install/ipa_restore.py
e3ffab
index cd98d07f5f7c7b2ea1b1fef9a272229475efcdc9..be487166d9b2319aeee5fcb54bf4779afcac5afa 100644
e3ffab
--- a/ipaserver/install/ipa_restore.py
e3ffab
+++ b/ipaserver/install/ipa_restore.py
e3ffab
@@ -25,6 +25,7 @@ import time
e3ffab
 import pwd
e3ffab
 from ConfigParser import SafeConfigParser
e3ffab
 import ldif
e3ffab
+import itertools
e3ffab
 
e3ffab
 from ipalib import api, errors
e3ffab
 from ipapython import version, ipautil, certdb, dogtag
e3ffab
@@ -161,32 +162,25 @@ class Restore(admintool.AdminTool):
e3ffab
 
e3ffab
 
e3ffab
     def validate_options(self):
e3ffab
+        parser = self.option_parser
e3ffab
         options = self.options
e3ffab
         super(Restore, self).validate_options(needs_root=True)
e3ffab
-        if options.data_only:
e3ffab
-            installutils.check_server_configuration()
e3ffab
 
e3ffab
         if len(self.args) < 1:
e3ffab
-            self.option_parser.error(
e3ffab
-                "must provide the backup to restore")
e3ffab
+            parser.error("must provide the backup to restore")
e3ffab
         elif len(self.args) > 1:
e3ffab
-            self.option_parser.error(
e3ffab
-                "must provide exactly one name for the backup")
e3ffab
+            parser.error("must provide exactly one name for the backup")
e3ffab
 
e3ffab
         dirname = self.args[0]
e3ffab
         if not os.path.isabs(dirname):
e3ffab
-            self.backup_dir = os.path.join(BACKUP_DIR, dirname)
e3ffab
-        else:
e3ffab
-            self.backup_dir = dirname
e3ffab
-
e3ffab
+            dirname = os.path.join(BACKUP_DIR, dirname)
e3ffab
         if not os.path.isdir(dirname):
e3ffab
-            raise self.option_parser.error("must provide path to backup directory")
e3ffab
+            parser.error("must provide path to backup directory")
e3ffab
 
e3ffab
         if options.gpg_keyring:
e3ffab
             if (not os.path.exists(options.gpg_keyring + '.pub') or
e3ffab
-               not os.path.exists(options.gpg_keyring + '.sec')):
e3ffab
-                raise admintool.ScriptError('No such key %s' %
e3ffab
-                    options.gpg_keyring)
e3ffab
+                    not os.path.exists(options.gpg_keyring + '.sec')):
e3ffab
+                parser.error("no such key %s" % options.gpg_keyring)
e3ffab
 
e3ffab
 
e3ffab
     def ask_for_options(self):
e3ffab
@@ -212,38 +206,88 @@ class Restore(admintool.AdminTool):
e3ffab
         api.bootstrap(in_server=False, context='restore')
e3ffab
         api.finalize()
e3ffab
 
e3ffab
+        self.backup_dir = self.args[0]
e3ffab
+        if not os.path.isabs(self.backup_dir):
e3ffab
+            self.backup_dir = os.path.join(BACKUP_DIR, self.backup_dir)
e3ffab
+
e3ffab
         self.log.info("Preparing restore from %s on %s",
e3ffab
-            self.backup_dir, api.env.host)
e3ffab
+                      self.backup_dir, api.env.host)
e3ffab
 
e3ffab
-        if options.instance and options.backend:
e3ffab
-            database = (options.instance, options.backend)
e3ffab
-            if not os.path.exists(paths.SLAPD_INSTANCE_DB_DIR_TEMPLATE %
e3ffab
-                                  database):
e3ffab
-                raise admintool.ScriptError(
e3ffab
-                    "Instance %s with backend %s does not exist" % database)
e3ffab
+        self.header = os.path.join(self.backup_dir, 'header')
e3ffab
+
e3ffab
+        try:
e3ffab
+            self.read_header()
e3ffab
+        except IOError as e:
e3ffab
+            raise admintool.ScriptError("Cannot read backup metadata: %s" % e)
e3ffab
+
e3ffab
+        if options.data_only:
e3ffab
+            restore_type = 'DATA'
e3ffab
+        else:
e3ffab
+            restore_type = self.backup_type
e3ffab
+
e3ffab
+        instances = [realm_to_serverid(api.env.realm), 'PKI-IPA']
e3ffab
+        backends = ['userRoot', 'ipaca']
e3ffab
 
e3ffab
-            databases = [database]
e3ffab
+        # These checks would normally be in the validate method but
e3ffab
+        # we need to know the type of backup we're dealing with.
e3ffab
+        if restore_type == 'FULL':
e3ffab
+            if options.online:
e3ffab
+                raise admintool.ScriptError(
e3ffab
+                    "File restoration cannot be done online")
e3ffab
+            if options.instance or options.backend:
e3ffab
+                raise admintool.ScriptError(
e3ffab
+                    "Restore must be in data-only mode when restoring a "
e3ffab
+                    "specific instance or backend")
e3ffab
         else:
e3ffab
+            installutils.check_server_configuration()
e3ffab
+
e3ffab
             if options.instance:
e3ffab
+                instance_dir = (paths.VAR_LIB_SLAPD_INSTANCE_DIR_TEMPLATE %
e3ffab
+                                options.instance)
e3ffab
+                if not os.path.exists(instance_dir):
e3ffab
+                    raise admintool.ScriptError(
e3ffab
+                        "Instance %s does not exist" % options.instance)
e3ffab
+
e3ffab
                 instances = [options.instance]
e3ffab
-            else:
e3ffab
-                instances = [realm_to_serverid(api.env.realm), 'PKI-IPA']
e3ffab
 
e3ffab
             if options.backend:
e3ffab
+                for instance in instances:
e3ffab
+                    db_dir = (paths.SLAPD_INSTANCE_DB_DIR_TEMPLATE %
e3ffab
+                              (instance, options.backend))
e3ffab
+                    if os.path.exists(db_dir):
e3ffab
+                        break
e3ffab
+                else:
e3ffab
+                    raise admintool.ScriptError(
e3ffab
+                        "Backend %s does not exist" % options.backend)
e3ffab
+
e3ffab
                 backends = [options.backend]
e3ffab
+
e3ffab
+            for instance, backend in itertools.product(instances, backends):
e3ffab
+                db_dir = (paths.SLAPD_INSTANCE_DB_DIR_TEMPLATE %
e3ffab
+                          (instance, backend))
e3ffab
+                if os.path.exists(db_dir):
e3ffab
+                    break
e3ffab
             else:
e3ffab
-                backends = ['userRoot', 'ipaca']
e3ffab
+                raise admintool.ScriptError(
e3ffab
+                    "Cannot restore a data backup into an empty system")
e3ffab
 
e3ffab
-            databases = []
e3ffab
-            for instance in instances:
e3ffab
-                for backend in backends:
e3ffab
-                    database = (instance, backend)
e3ffab
-                    if os.path.exists(paths.SLAPD_INSTANCE_DB_DIR_TEMPLATE %
e3ffab
-                                      database):
e3ffab
-                        databases.append(database)
e3ffab
+        self.log.info("Performing %s restore from %s backup" %
e3ffab
+                      (restore_type, self.backup_type))
e3ffab
 
e3ffab
-            if options.data_only and not databases:
e3ffab
-                raise admintool.ScriptError('No instances to restore to')
e3ffab
+        if self.backup_host != api.env.host:
e3ffab
+            raise admintool.ScriptError(
e3ffab
+                "Host name %s does not match backup name %s" %
e3ffab
+                (api.env.host, self.backup_host))
e3ffab
+
e3ffab
+        if self.backup_ipa_version != str(version.VERSION):
e3ffab
+            self.log.warning(
e3ffab
+                "Restoring data from a different release of IPA.\n"
e3ffab
+                "Data is version %s.\n"
e3ffab
+                "Server is running %s." %
e3ffab
+                (self.backup_ipa_version, str(version.VERSION)))
e3ffab
+            if (not options.unattended and
e3ffab
+                    not user_input("Continue to restore?", False)):
e3ffab
+                raise admintool.ScriptError("Aborted")
e3ffab
 
e3ffab
         create_ds_user()
e3ffab
         pent = pwd.getpwnam(DS_USER)
e3ffab
@@ -257,46 +301,35 @@ class Restore(admintool.AdminTool):
e3ffab
 
e3ffab
         os.chown(self.dir, pent.pw_uid, pent.pw_gid)
e3ffab
 
e3ffab
-        self.header = os.path.join(self.backup_dir, 'header')
e3ffab
-
e3ffab
         cwd = os.getcwd()
e3ffab
         try:
e3ffab
             dirsrv = services.knownservices.dirsrv
e3ffab
 
e3ffab
-            try:
e3ffab
-                self.read_header()
e3ffab
-            except IOError as e:
e3ffab
-                raise admintool.ScriptError('Cannot read backup metadata: %s' % e)
e3ffab
-            # These two checks would normally be in the validate method but
e3ffab
-            # we need to know the type of backup we're dealing with.
e3ffab
-            if (self.backup_type != 'FULL' and not options.data_only and
e3ffab
-                not databases):
e3ffab
-                raise admintool.ScriptError('Cannot restore a data backup into an empty system')
e3ffab
-            if (self.backup_type == 'FULL' and not options.data_only and
e3ffab
-                (options.instance or options.backend)):
e3ffab
-                raise admintool.ScriptError('Restore must be in data-only mode when restoring a specific instance or backend.')
e3ffab
-            if self.backup_host != api.env.host:
e3ffab
-                raise admintool.ScriptError(
e3ffab
-                    'Host name %s does not match backup name %s' %
e3ffab
-                    (api.env.host, self.backup_host))
e3ffab
-            if self.backup_ipa_version != str(version.VERSION):
e3ffab
-                self.log.warning(
e3ffab
-                    "Restoring data from a different release of IPA.\n"
e3ffab
-                    "Data is version %s.\n"
e3ffab
-                    "Server is running %s." %
e3ffab
-                    (self.backup_ipa_version, str(version.VERSION)))
e3ffab
-                if (not options.unattended and
e3ffab
-                    not user_input("Continue to restore?", False)):
e3ffab
-                    raise admintool.ScriptError("Aborted")
e3ffab
-
e3ffab
             self.extract_backup(options.gpg_keyring)
e3ffab
 
e3ffab
-            for database in databases:
e3ffab
-                ldifname = '%s-%s.ldif' % database
e3ffab
-                ldiffile = os.path.join(self.dir, ldifname)
e3ffab
-                if not os.path.exists(ldiffile):
e3ffab
+            databases = []
e3ffab
+            for instance in instances:
e3ffab
+                for backend in backends:
e3ffab
+                    database = (instance, backend)
e3ffab
+                    ldiffile = os.path.join(self.dir, '%s-%s.ldif' % database)
e3ffab
+                    if os.path.exists(ldiffile):
e3ffab
+                        databases.append(database)
e3ffab
+
e3ffab
+            if options.instance:
e3ffab
+                for instance, backend in databases:
e3ffab
+                    if instance == options.instance:
e3ffab
+                        break
e3ffab
+                else:
e3ffab
+                    raise admintool.ScriptError(
e3ffab
+                        "Instance %s not found in backup" % options.instance)
e3ffab
+
e3ffab
+            if options.backend:
e3ffab
+                for instance, backend in databases:
e3ffab
+                    if backend == options.backend:
e3ffab
+                        break
e3ffab
+                else:
e3ffab
                     raise admintool.ScriptError(
e3ffab
-                        "Instance %s with backend %s not in backup" % database)
e3ffab
+                        "Backend %s not found in backup" % options.backend)
e3ffab
 
e3ffab
             # Big fat warning
e3ffab
             if  (not options.unattended and
e3ffab
@@ -315,7 +348,7 @@ class Restore(admintool.AdminTool):
e3ffab
             self.log.info("Disabling all replication.")
e3ffab
             self.disable_agreements()
e3ffab
 
e3ffab
-            if options.data_only:
e3ffab
+            if restore_type != 'FULL':
e3ffab
                 if not options.online:
e3ffab
                     self.log.info('Stopping Directory Server')
e3ffab
                     dirsrv.stop(capture_output=False)
e3ffab
@@ -332,11 +365,9 @@ class Restore(admintool.AdminTool):
e3ffab
 
e3ffab
 
e3ffab
             # We do either a full file restore or we restore data.
e3ffab
-            if self.backup_type == 'FULL' and not options.data_only:
e3ffab
+            if restore_type == 'FULL':
e3ffab
                 if 'CA' in self.backup_services:
e3ffab
                     create_ca_user()
e3ffab
-                if options.online:
e3ffab
-                    raise admintool.ScriptError('File restoration cannot be done online.')
e3ffab
                 self.cert_restore_prepare()
e3ffab
                 self.file_restore(options.no_logs)
e3ffab
                 self.cert_restore()
e3ffab
@@ -351,7 +382,7 @@ class Restore(admintool.AdminTool):
e3ffab
             for instance, backend in databases:
e3ffab
                 self.ldif2db(instance, backend, online=options.online)
e3ffab
 
e3ffab
-            if options.data_only:
e3ffab
+            if restore_type != 'FULL':
e3ffab
                 if not options.online:
e3ffab
                     self.log.info('Starting Directory Server')
e3ffab
                     dirsrv.start(capture_output=False)
e3ffab
-- 
e3ffab
2.1.0
e3ffab