Blob Blame Raw
From 43f8a317bcd9040874b27cad905347a9e6bc8a6f Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Wed, 9 Dec 2020 22:42:59 +0000
Subject: [PATCH 4/6] Issue 4419 - Warn users of skipped entries during ldif2db
 online import (#4476)

Bug Description:  During an online ldif2db import entries that do not
                  conform to various constraints will be skipped and
                  not imported. On completition of an import with skipped
                  entries, the server responds with a success message
                  and logs the skipped entry detail to the error logs.
                  The success messgae could lead the user to believe
                  that all entries were successfully imported.

Fix Description:  If a skipped entry occurs during import, the import
                  will continue and a warning message will be displayed.
                  The schema is extended with a nsTaskWarning attribute
                  which is used to capture and retrieve any task
                  warnings.

                  CLI tools for online import updated.

                  Test added to generate an incorrect ldif entry and perform an
                  online import.

Fixes: https://github.com/389ds/389-ds-base/issues/4419

Reviewed by: tbordaz, mreynolds389, droideck, Firstyear (Thanks)
---
 .../tests/suites/import/import_test.py        | 39 +++++++++++++++++--
 ldap/schema/02common.ldif                     |  3 +-
 .../back-ldbm/db-bdb/bdb_import_threads.c     |  5 +++
 ldap/servers/slapd/slap.h                     |  1 +
 ldap/servers/slapd/slapi-plugin.h             | 11 ++++++
 ldap/servers/slapd/slapi-private.h            |  8 ----
 ldap/servers/slapd/task.c                     | 29 +++++++++++++-
 src/lib389/lib389/cli_conf/backend.py         |  6 ++-
 src/lib389/lib389/tasks.py                    | 23 +++++++++--
 9 files changed, 108 insertions(+), 17 deletions(-)

diff --git a/dirsrvtests/tests/suites/import/import_test.py b/dirsrvtests/tests/suites/import/import_test.py
index b47db96ed..77c915026 100644
--- a/dirsrvtests/tests/suites/import/import_test.py
+++ b/dirsrvtests/tests/suites/import/import_test.py
@@ -65,6 +65,9 @@ def _import_clean(request, topo):
         import_ldif = ldif_dir + '/basic_import.ldif'
         if os.path.exists(import_ldif):
             os.remove(import_ldif)
+        syntax_err_ldif = ldif_dir + '/syntax_err.dif'
+        if os.path.exists(syntax_err_ldif):
+            os.remove(syntax_err_ldif)
 
     request.addfinalizer(finofaci)
 
@@ -141,17 +144,19 @@ def _create_bogus_ldif(topo):
 
 def _create_syntax_err_ldif(topo):
     """
-    Create an incorrect ldif entry that violates syntax check
+    Create an ldif file, which contains an entry that violates syntax check
     """
     ldif_dir = topo.standalone.get_ldif_dir()
     line1 = """dn: dc=example,dc=com
 objectClass: top
 objectClass: domain
 dc: example
+
 dn: ou=groups,dc=example,dc=com
 objectClass: top
 objectClass: organizationalUnit
 ou: groups
+
 dn: uid=JHunt,ou=groups,dc=example,dc=com
 objectClass: top
 objectClass: person
@@ -201,6 +206,34 @@ def test_import_with_index(topo, _import_clean):
     assert f'{place}/userRoot/roomNumber.db' in glob.glob(f'{place}/userRoot/*.db', recursive=True)
 
 
+def test_online_import_with_warning(topo, _import_clean):
+    """
+    Import an ldif file with syntax errors, verify skipped entry warning code
+
+    :id: 5bf75c47-a283-430e-a65c-3c5fd8dbadb8
+    :setup: Standalone Instance
+    :steps:
+        1. Create standalone Instance
+        2. Create an ldif file with an entry that violates syntax check (empty givenname)
+        3. Online import of troublesome ldif file
+    :expected results:
+        1. Successful import with skipped entry warning
+        """
+    topo.standalone.restart()
+
+    import_task = ImportTask(topo.standalone)
+    import_ldif1 = _create_syntax_err_ldif(topo)
+
+    # Importing  the offending ldif file - online
+    import_task.import_suffix_from_ldif(ldiffile=import_ldif1, suffix=DEFAULT_SUFFIX)
+
+    # There is just  a single entry in this ldif
+    import_task.wait(5)
+
+    # Check for the task nsTaskWarning attr, make sure its set to skipped entry code
+    assert import_task.present('nstaskwarning')
+    assert TaskWarning.WARN_SKIPPED_IMPORT_ENTRY == import_task.get_task_warn()
+
 def test_crash_on_ldif2db(topo, _import_clean):
     """
     Delete the cn=monitor entry for an LDBM backend instance. Doing this will
@@ -246,7 +279,7 @@ def test_ldif2db_allows_entries_without_a_parent_to_be_imported(topo, _import_cl
     topo.standalone.start()
 
 
-def test_ldif2db_syntax_check(topo):
+def test_ldif2db_syntax_check(topo, _import_clean):
     """ldif2db should return a warning when a skipped entry has occured.
     :id: 85e75670-42c5-4062-9edc-7f117c97a06f
     :setup:
@@ -261,7 +294,7 @@ def test_ldif2db_syntax_check(topo):
     import_ldif1 = _create_syntax_err_ldif(topo)
     # Import the offending LDIF data - offline
     topo.standalone.stop()
-    ret = topo.standalone.ldif2db('userRoot', None, None, None, import_ldif1)
+    ret = topo.standalone.ldif2db('userRoot', None, None, None, import_ldif1, None)
     assert ret == TaskWarning.WARN_SKIPPED_IMPORT_ENTRY
     topo.standalone.start()
 
diff --git a/ldap/schema/02common.ldif b/ldap/schema/02common.ldif
index c6dc074db..821640d03 100644
--- a/ldap/schema/02common.ldif
+++ b/ldap/schema/02common.ldif
@@ -145,6 +145,7 @@ attributeTypes: ( 2.16.840.1.113730.3.1.2356 NAME 'nsTaskExitCode' DESC 'Slapi T
 attributeTypes: ( 2.16.840.1.113730.3.1.2357 NAME 'nsTaskCurrentItem' DESC 'Slapi Task item' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN '389 Directory Server' )
 attributeTypes: ( 2.16.840.1.113730.3.1.2358 NAME 'nsTaskTotalItems' DESC 'Slapi Task total items' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN '389 Directory Server' )
 attributeTypes: ( 2.16.840.1.113730.3.1.2359 NAME 'nsTaskCreated' DESC 'Slapi Task creation date' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE X-ORIGIN '389 Directory Server' )
+attributeTypes: ( 2.16.840.1.113730.3.1.2375 NAME 'nsTaskWarning' DESC 'Slapi Task warning code' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN '389 Directory Server' )
 #
 # objectclasses:
 #
@@ -177,5 +178,5 @@ objectClasses: ( 2.16.840.1.113730.3.2.503 NAME 'nsDSWindowsReplicationAgreement
 objectClasses: ( 2.16.840.1.113730.3.2.128 NAME 'costemplate' DESC 'Netscape defined objectclass' SUP top MAY ( cn $ cospriority ) X-ORIGIN 'Netscape Directory Server' )
 objectClasses: ( 2.16.840.1.113730.3.2.304 NAME 'nsView' DESC 'Netscape defined objectclass' SUP top AUXILIARY MAY ( nsViewFilter $ description ) X-ORIGIN 'Netscape Directory Server' )
 objectClasses: ( 2.16.840.1.113730.3.2.316 NAME 'nsAttributeEncryption' DESC 'Netscape defined objectclass' SUP top MUST ( cn $ nsEncryptionAlgorithm ) X-ORIGIN 'Netscape Directory Server' )
-objectClasses: ( 2.16.840.1.113730.3.2.335 NAME 'nsSlapiTask' DESC 'Slapi_Task objectclass' SUP top MUST ( cn ) MAY ( ttl $ nsTaskLog $ nsTaskStatus $ nsTaskExitCode $ nsTaskCurrentItem $ nsTaskTotalItems $ nsTaskCreated ) X-ORIGIN '389 Directory Server' )
+objectClasses: ( 2.16.840.1.113730.3.2.335 NAME 'nsSlapiTask' DESC 'Slapi_Task objectclass' SUP top MUST ( cn ) MAY ( ttl $ nsTaskLog $ nsTaskStatus $ nsTaskExitCode $ nsTaskCurrentItem $ nsTaskTotalItems $ nsTaskCreated $ nsTaskWarning ) X-ORIGIN '389 Directory Server' )
 
diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c
index 310893884..5c7d9c8f7 100644
--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c
+++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c
@@ -747,6 +747,11 @@ import_producer(void *param)
         }
     }
 
+    /* capture skipped entry warnings for this task */
+    if((job) && (job->skipped)) {
+        slapi_task_set_warning(job->task, WARN_SKIPPED_IMPORT_ENTRY);
+    }
+
     slapi_value_free(&(job->usn_value));
     import_free_ldif(&c);
     info->state = FINISHED;
diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h
index 53c9161d1..be4d38739 100644
--- a/ldap/servers/slapd/slap.h
+++ b/ldap/servers/slapd/slap.h
@@ -1753,6 +1753,7 @@ typedef struct slapi_task
     int task_progress;         /* number between 0 and task_work */
     int task_work;             /* "units" of work to be done */
     int task_flags;            /* (see above) */
+    task_warning task_warn;    /* task warning */
     char *task_status;         /* transient status info */
     char *task_log;            /* appended warnings, etc */
     char task_date[SLAPI_TIMESTAMP_BUFSIZE]; /* Date/time when task was created */
diff --git a/ldap/servers/slapd/slapi-plugin.h b/ldap/servers/slapd/slapi-plugin.h
index 96313ef2c..ddb11bc7c 100644
--- a/ldap/servers/slapd/slapi-plugin.h
+++ b/ldap/servers/slapd/slapi-plugin.h
@@ -6638,6 +6638,15 @@ int slapi_config_remove_callback(int operation, int flags, const char *base, int
 /* task flags (set by the task-control code) */
 #define SLAPI_TASK_DESTROYING 0x01 /* queued event for destruction */
 
+/* task warnings */
+typedef enum task_warning_t{
+    WARN_UPGARDE_DN_FORMAT_ALL    = (1 << 0),
+    WARN_UPGRADE_DN_FORMAT        = (1 << 1),
+    WARN_UPGRADE_DN_FORMAT_SPACE  = (1 << 2),
+    WARN_SKIPPED_IMPORT_ENTRY     = (1 << 3)
+} task_warning;
+
+
 int slapi_task_register_handler(const char *name, dseCallbackFn func);
 int slapi_plugin_task_register_handler(const char *name, dseCallbackFn func, Slapi_PBlock *plugin_pb);
 int slapi_plugin_task_unregister_handler(const char *name, dseCallbackFn func);
@@ -6654,6 +6663,8 @@ int slapi_task_get_refcount(Slapi_Task *task);
 void slapi_task_set_destructor_fn(Slapi_Task *task, TaskCallbackFn func);
 void slapi_task_set_cancel_fn(Slapi_Task *task, TaskCallbackFn func);
 void slapi_task_status_changed(Slapi_Task *task);
+void slapi_task_set_warning(Slapi_Task *task, task_warning warn);
+int slapi_task_get_warning(Slapi_Task *task);
 void slapi_task_log_status(Slapi_Task *task, char *format, ...)
 #ifdef __GNUC__
     __attribute__((format(printf, 2, 3)));
diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h
index d5abe8ac1..b956ebe63 100644
--- a/ldap/servers/slapd/slapi-private.h
+++ b/ldap/servers/slapd/slapi-private.h
@@ -1465,14 +1465,6 @@ void slapi_pblock_set_operation_notes(Slapi_PBlock *pb, uint32_t opnotes);
 void slapi_pblock_set_flag_operation_notes(Slapi_PBlock *pb, uint32_t opflag);
 void slapi_pblock_set_result_text_if_empty(Slapi_PBlock *pb, char *text);
 
-/* task warnings */
-typedef enum task_warning_t{
-    WARN_UPGARDE_DN_FORMAT_ALL    = (1 << 0),
-    WARN_UPGRADE_DN_FORMAT        = (1 << 1),
-    WARN_UPGRADE_DN_FORMAT_SPACE  = (1 << 2),
-    WARN_SKIPPED_IMPORT_ENTRY     = (1 << 3)
-} task_warning;
-
 int32_t slapi_pblock_get_task_warning(Slapi_PBlock *pb);
 void slapi_pblock_set_task_warning(Slapi_PBlock *pb, task_warning warn);
 
diff --git a/ldap/servers/slapd/task.c b/ldap/servers/slapd/task.c
index 936c64920..806077a16 100644
--- a/ldap/servers/slapd/task.c
+++ b/ldap/servers/slapd/task.c
@@ -46,6 +46,7 @@ static uint64_t shutting_down = 0;
 #define TASK_PROGRESS_NAME "nsTaskCurrentItem"
 #define TASK_WORK_NAME "nsTaskTotalItems"
 #define TASK_DATE_NAME "nsTaskCreated"
+#define TASK_WARNING_NAME "nsTaskWarning"
 
 #define DEFAULT_TTL "3600"                        /* seconds */
 #define TASK_SYSCONFIG_FILE_ATTR "sysconfigfile" /* sysconfig reload task file attr */
@@ -332,7 +333,7 @@ slapi_task_status_changed(Slapi_Task *task)
     LDAPMod modlist[20];
     LDAPMod *mod[20];
     int cur = 0, i;
-    char s1[20], s2[20], s3[20];
+    char s1[20], s2[20], s3[20], s4[20];
 
     if (shutting_down) {
         /* don't care about task status updates anymore */
@@ -346,9 +347,11 @@ slapi_task_status_changed(Slapi_Task *task)
     sprintf(s1, "%d", task->task_exitcode);
     sprintf(s2, "%d", task->task_progress);
     sprintf(s3, "%d", task->task_work);
+    sprintf(s4, "%d", task->task_warn);
     NEXTMOD(TASK_PROGRESS_NAME, s2);
     NEXTMOD(TASK_WORK_NAME, s3);
     NEXTMOD(TASK_DATE_NAME, task->task_date);
+    NEXTMOD(TASK_WARNING_NAME, s4);
     /* only add the exit code when the job is done */
     if ((task->task_state == SLAPI_TASK_FINISHED) ||
         (task->task_state == SLAPI_TASK_CANCELLED)) {
@@ -452,6 +455,30 @@ slapi_task_get_refcount(Slapi_Task *task)
     return 0; /* return value not currently used */
 }
 
+/*
+ * Return task warning
+ */
+int
+slapi_task_get_warning(Slapi_Task *task)
+{
+    if (task) {
+        return task->task_warn;
+    }
+
+    return 0; /* return value not currently used */
+}
+
+/*
+ * Set task warning
+ */
+void
+slapi_task_set_warning(Slapi_Task *task, task_warning warn)
+{
+    if (task) {
+        return task->task_warn |= warn;
+    }
+}
+
 int
 slapi_plugin_task_unregister_handler(const char *name, dseCallbackFn func)
 {
diff --git a/src/lib389/lib389/cli_conf/backend.py b/src/lib389/lib389/cli_conf/backend.py
index d7a6e670c..6bfbcb036 100644
--- a/src/lib389/lib389/cli_conf/backend.py
+++ b/src/lib389/lib389/cli_conf/backend.py
@@ -243,9 +243,13 @@ def backend_import(inst, basedn, log, args):
                           exclude_suffixes=args.exclude_suffixes)
     task.wait(timeout=None)
     result = task.get_exit_code()
+    warning = task.get_task_warn()
 
     if task.is_complete() and result == 0:
-        log.info("The import task has finished successfully")
+        if warning is None or (warning == 0):
+            log.info("The import task has finished successfully")
+        else:
+            log.info("The import task has finished successfully, with warning code {}, check the logs for more detail".format(warning))
     else:
         raise ValueError("Import task failed\n-------------------------\n{}".format(ensure_str(task.get_task_log())))
 
diff --git a/src/lib389/lib389/tasks.py b/src/lib389/lib389/tasks.py
index dc7bb9206..bf20d1e61 100644
--- a/src/lib389/lib389/tasks.py
+++ b/src/lib389/lib389/tasks.py
@@ -38,6 +38,7 @@ class Task(DSLdapObject):
         self._protected = False
         self._exit_code = None
         self._task_log = ""
+        self._task_warn = None
 
     def status(self):
         """Return the decoded status of the task
@@ -49,6 +50,7 @@ class Task(DSLdapObject):
 
         self._exit_code = self.get_attr_val_utf8("nsTaskExitCode")
         self._task_log = self.get_attr_val_utf8("nsTaskLog")
+        self._task_warn = self.get_attr_val_utf8("nsTaskWarning")
         if not self.exists():
             self._log.debug("complete: task has self cleaned ...")
             # The task cleaned it self up.
@@ -77,6 +79,15 @@ class Task(DSLdapObject):
                 return None
         return None
 
+    def get_task_warn(self):
+        """Return task's warning code if task is complete, else None."""
+        if self.is_complete():
+            try:
+                return int(self._task_warn)
+            except TypeError:
+                return None
+        return None
+
     def wait(self, timeout=120):
         """Wait until task is complete."""
 
@@ -390,14 +401,17 @@ class Tasks(object):
         running, true if done - if true, second is the exit code - if dowait
         is True, this function will block until the task is complete'''
         attrlist = ['nsTaskLog', 'nsTaskStatus', 'nsTaskExitCode',
-                    'nsTaskCurrentItem', 'nsTaskTotalItems']
+                    'nsTaskCurrentItem', 'nsTaskTotalItems', 'nsTaskWarning']
         done = False
         exitCode = 0
+        warningCode = 0
         dn = entry.dn
         while not done:
             entry = self.conn.getEntry(dn, attrlist=attrlist)
             self.log.debug("task entry %r", entry)
 
+            if entry.nsTaskWarning:
+                warningCode = int(entry.nsTaskWarning)
             if entry.nsTaskExitCode:
                 exitCode = int(entry.nsTaskExitCode)
                 done = True
@@ -405,7 +419,7 @@ class Tasks(object):
                 time.sleep(1)
             else:
                 break
-        return (done, exitCode)
+        return (done, exitCode, warningCode)
 
     def importLDIF(self, suffix=None, benamebase=None, input_file=None,
                    args=None):
@@ -461,8 +475,9 @@ class Tasks(object):
         self.conn.add_s(entry)
 
         exitCode = 0
+        warningCode = 0
         if args and args.get(TASK_WAIT, False):
-            (done, exitCode) = self.conn.tasks.checkTask(entry, True)
+            (done, exitCode, warningCode) = self.conn.tasks.checkTask(entry, True)
 
         if exitCode:
             self.log.error("Error: import task %s for file %s exited with %d",
@@ -470,6 +485,8 @@ class Tasks(object):
         else:
             self.log.info("Import task %s for file %s completed successfully",
                           cn, input_file)
+            if warningCode:
+                self.log.info("with warning code %d", warningCode)
         self.dn = dn
         self.entry = entry
         return exitCode
-- 
2.26.2