Repository: rucio/rucio

Issue:
Misleading warnings when submitting a Lifetime Model exception with an input file containing duplicate entries
### Description

If the user is submitting a Lifetime Model exception request using the CLI and the input file contains some dataset multiple times, then they will see some warning about datasets not existing.

Instead, it should show a warning that the entry is being removed for being a duplicate (so that, when reviewing the summary, one can easily understand the difference between total and successfully-submitted DIDs).

### Steps to reproduce

```console
$ cat covfefe
mc16_13TeV.364677.Sherpa_CT10_CT14nnlo_CSShower_2to2jets_JZ1W.deriv.DAOD_STDM11.e6997_e5984_s3126_r10201_r10210_p4615_tid26084487_00
mc16_13TeV.364677.Sherpa_CT10_CT14nnlo_CSShower_2to2jets_JZ1W.deriv.DAOD_STDM11.e6997_e5984_s3126_r10201_r10210_p4615_tid26084487_00
$ rucio add-lifetime-exception --inputfile covfefe --reason 'For testing purposed; please ignore.' --expiration 2024-01-01
2023-03-23 08:09:13,033	INFO	mc16_13TeV:mc16_13TeV.364677.Sherpa_CT10_CT14nnlo_CSShower_2to2jets_JZ1W.deriv.DAOD_STDM11.e6997_e5984_s3126_r10201_r10210_p4615_tid26084487_00 will be declared
2023-03-23 08:09:13,033	WARNING	mc16_13TeV:mc16_13TeV.364677.Sherpa_CT10_CT14nnlo_CSShower_2to2jets_JZ1W.deriv.DAOD_STDM11.e6997_e5984_s3126_r10201_r10210_p4615_tid26084487_00 does not exist
2023-03-23 08:09:13,066	INFO	Exception successfully submitted. Summary below
Total DIDs                                                                                                2
DID not submitted because it is a file                                                                    0
DID not submitted because it is a container                                                               0
DID not submitted because it is not part of the lifetime campaign                                         0
DID successfully submitted                                                                                1
```

### Rucio Version

_No response_

### Additional Information

_No response_


Discussion:

Resolving patch:
diff --git a/lib/rucio/core/did_meta_plugins/did_column_meta.py b/lib/rucio/core/did_meta_plugins/did_column_meta.py
--- a/lib/rucio/core/did_meta_plugins/did_column_meta.py
+++ b/lib/rucio/core/did_meta_plugins/did_column_meta.py
@@ -75,7 +75,16 @@ def set_metadata_bulk(self, scope, name, metadata, recursive=False, *, session:
 
         remainder = {}
         for key, value in metadata.items():
-            if key == 'lifetime':
+            if key == 'eol_at' and isinstance(value, str):
+                try:
+                    eol_at = datetime.strptime(value, '%Y-%M-%d')
+                    rowcount = did_query.update({'eol_at': eol_at}, synchronize_session='fetch')
+                except TypeError as error:
+                    raise exception.InvalidValueForKey(error)
+                if not rowcount:
+                    # check for did presence
+                    raise exception.UnsupportedOperation('%s for %s:%s cannot be updated' % (key, scope, name))
+            elif key == 'lifetime':
                 try:
                     expired_at = None
                     if value is not None:


Test patch:
diff --git a/lib/rucio/tests/test_bin_rucio.py b/lib/rucio/tests/test_bin_rucio.py
--- a/lib/rucio/tests/test_bin_rucio.py
+++ b/lib/rucio/tests/test_bin_rucio.py
@@ -16,6 +16,7 @@
 import os
 import random
 import re
+import tempfile
 from datetime import datetime, timedelta
 from os import remove, unlink, listdir, rmdir, stat, path, environ
 
@@ -26,6 +27,7 @@
 from rucio.client.replicaclient import ReplicaClient
 from rucio.client.rseclient import RSEClient
 from rucio.client.ruleclient import RuleClient
+from rucio.client.configclient import ConfigClient
 from rucio.common.config import config_get, config_get_bool
 from rucio.common.types import InternalScope, InternalAccount
 from rucio.common.utils import generate_uuid, get_tmp_dir, md5, render_json
@@ -67,6 +69,7 @@ def setup_obj(self, vo, function_scope_prefix):
         self.did_client = DIDClient()
         self.replica_client = ReplicaClient()
         self.rule_client = RuleClient()
+        self.config_client = ConfigClient()
         self.account_client = AccountLimitClient()
         rse_factory = None
         if environ.get('SUITE', 'remote_dbs') != 'client':
@@ -2327,3 +2330,18 @@ def test_admin_rse_update_unsupported_option(self):
         print(out, err)
         assert exitcode == 0
         assert not err
+
+    @pytest.mark.noparallel(reason='Modify config')
+    def test_lifetime_cli(self):
+        """ CLIENT(USER): Check CLI to declare lifetime exceptions """
+        # Setup data for CLI check
+        tmp_dsn_name = 'container' + rse_name_generator()
+        tmp_dsn_did = self.user + ':' + tmp_dsn_name
+        self.did_client.add_did(scope=self.user, name=tmp_dsn_name, did_type='DATASET')
+        self.did_client.set_metadata(scope=self.user, name=tmp_dsn_name, key='eol_at', value=(datetime.utcnow() - timedelta(days=1)).strftime('%Y-%m-%d'))
+        self.config_client.set_config_option(section='lifetime_model', option='cutoff_date', value=(datetime.utcnow() + timedelta(days=1)).strftime('%Y-%m-%d'))
+        with tempfile.NamedTemporaryFile(mode="w+") as fp:
+            fp.write(f'{tmp_dsn_did}\n' * 2)
+            fp.seek(0)
+            exitcode, out, err = execute("rucio add-lifetime-exception --inputfile %s --reason 'For testing purpose; please ignore.' --expiration 2124-01-01" % fp.name)
+            assert 'does not exist' not in err