Blame SOURCES/0023-Issue-4419-Warn-users-of-skipped-entries-during-ldif.patch

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