Repository: rucio/rucio

Issue:
Per-RSE checksum specification misbehaving for MD5-only
Motivation
----------
I'm trying to use an admittedly ancient gridftp server which reports the MD5 value instead of Adler32.  I thought the solution/workaround was to use:
```
rucio-admin rse set-attribute --rse LIGO-CIT --key supported_checksums --value md5
```
However, I notice that this actually completely disables all FTS checksums.

Modification
------------
If i check the supported checksums with `get_rse_supported_checksums_from_attributes`, the result is:
```
['m']
```
And `checksum_validation_strategy` returns:
```
('destination', ['m'])
```
but I believe the correct result should instead be:
```
('both', ['md5'])
```
As far as I can tell, it looks like the problem is in [`rse.py#L641`](https://github.com/rucio/rucio/blob/master/lib/rucio/core/rse.py#L641):
```
checksum_attribute[0].split(',')
```
I'm not sure what useful purpose that `[0]` serves?



Discussion:
Ciao @astroclark ,
I think the `[0]` is something which might have worked once but now might be un-necessary and in fact might broke the whole behaviour.
Can you try editing the code removing the `[0]`? If that's the case I can manage to fix it with a PR.
Cheers!
Hm, removing the `[0]` ultimately gives:
```
  File "/usr/local/lib/python3.6/site-packages/rucio/core/rse.py", line 641, in parse_checksum_support_attribute
    supported_checksum_list = checksum_attribute.split(',')
AttributeError: 'list' object has no attribute 'split'
```
So I guess `checksum_attribute` is already a list of length 1 *unless* it's getting those values from the RSE attributes, in which case it's a string.  In fact, if i set:
```
$ rucio-admin rse set-attribute --rse LIGO-CIT --key supported_checksums --value md5,alder32
```
The supported checksums are still returned as:
```
supported by RSE settings: ['md5', 'alder32']
supported by RSE attrs: ['m']
```
using `get_rse_supported_checksums` and `get_rse_supported_checksums_from_attributes`, respectively.

I'm a little short of time now but might be able to take a more careful look tomorrow.

Resolving patch:
diff --git a/lib/rucio/core/rse.py b/lib/rucio/core/rse.py
--- a/lib/rucio/core/rse.py
+++ b/lib/rucio/core/rse.py
@@ -19,7 +19,7 @@
 import traceback
 from io import StringIO
 from re import match
-from typing import Any, Dict, Optional, Sequence, Union
+from typing import Any, Dict, List, Optional, Sequence, Union
 
 import sqlalchemy
 from dogpile.cache.api import NO_VALUE
@@ -762,15 +762,16 @@ def get_rse_attributes(rse_id, session=None):
     return result
 
 
-def get_rse_supported_checksums_from_attributes(rse_attributes):
+def get_rse_supported_checksums_from_attributes(rse_attributes: Dict[str, Any]) -> List[str]:
     """
     Parse the RSE attribute defining the checksum supported by the RSE
     :param rse_attributes: attributes retrieved using list_rse_attributes
+    :returns: A list of the names of supported checksums indicated by the specified attributes.
     """
-    return parse_checksum_support_attribute(rse_attributes.get(CHECKSUM_KEY))
+    return parse_checksum_support_attribute(rse_attributes.get(CHECKSUM_KEY, ''))
 
 
-def parse_checksum_support_attribute(checksum_attribute):
+def parse_checksum_support_attribute(checksum_attribute: str) -> List[str]:
     """
     Parse the checksum support RSE attribute.
     :param checksum_attribute: The value of the RSE attribute storing the checksum value
@@ -782,10 +783,12 @@ def parse_checksum_support_attribute(checksum_attribute):
 
     if not checksum_attribute:
         return GLOBALLY_SUPPORTED_CHECKSUMS
+
+    supported_checksum_list = [c.strip() for c in checksum_attribute.split(',') if c.strip()]
+
+    if 'none' in supported_checksum_list:
+        return []
     else:
-        supported_checksum_list = [c.strip() for c in checksum_attribute.split(',') if c.strip()]
-        if 'none' in supported_checksum_list:
-            return []
         return supported_checksum_list
 
 


Test patch:
diff --git a/lib/rucio/tests/conftest.py b/lib/rucio/tests/conftest.py
--- a/lib/rucio/tests/conftest.py
+++ b/lib/rucio/tests/conftest.py
@@ -415,14 +415,17 @@ def caches_mock(request):
     from dogpile.cache import make_region
 
     caches_to_mock = []
+    expiration_time = 600
+
     params = __get_fixture_param(request)
     if params:
         caches_to_mock = params.get("caches_to_mock", caches_to_mock)
+        expiration_time = params.get("expiration_time", expiration_time)
 
     with ExitStack() as stack:
         mocked_caches = []
         for module in caches_to_mock:
-            region = make_region().configure('dogpile.cache.memory', expiration_time=600)
+            region = make_region().configure('dogpile.cache.memory', expiration_time=expiration_time)
             stack.enter_context(mock.patch(module, new=region))
             mocked_caches.append(region)
 
diff --git a/lib/rucio/tests/test_rse.py b/lib/rucio/tests/test_rse.py
--- a/lib/rucio/tests/test_rse.py
+++ b/lib/rucio/tests/test_rse.py
@@ -25,12 +25,14 @@
                                     InvalidObject, ResourceTemporaryUnavailable,
                                     RSEAttributeNotFound, RSEOperationNotSupported,
                                     InputValidationError)
-from rucio.common.utils import generate_uuid
+from rucio.common.utils import generate_uuid, GLOBALLY_SUPPORTED_CHECKSUMS, CHECKSUM_KEY
 from rucio.core.rse import (add_rse, get_rse_id, del_rse, restore_rse, list_rses,
                             rse_exists, add_rse_attribute, list_rse_attributes,
                             set_rse_transfer_limits, get_rse_transfer_limits,
                             delete_rse_transfer_limits, get_rse_protocols,
                             del_rse_attribute, get_rse_attribute, get_rse, rse_is_empty,
+                            parse_checksum_support_attribute,
+                            get_rse_supported_checksums_from_attributes,
                             update_rse)
 from rucio.db.sqla import session, models
 from rucio.db.sqla.constants import RSEType
@@ -217,6 +219,40 @@ def test_empty_rse(self):
         assert not rse_is_empty(rse_id=rse_id)
 
 
+def test_parse_checksum_support_attribute():
+    assert parse_checksum_support_attribute('') == GLOBALLY_SUPPORTED_CHECKSUMS
+    assert parse_checksum_support_attribute('none') == []
+    assert parse_checksum_support_attribute('none,md5') == []
+    assert parse_checksum_support_attribute('md5') == ['md5']
+    assert parse_checksum_support_attribute('md5,adler32') == ['md5', 'adler32']
+
+
+@pytest.mark.parametrize("caches_mock", [{
+    "caches_to_mock": ['rucio.core.rse.REGION'],
+    'expiration_time': 0
+}], indirect=True)
+def test_rse_get_supported_checksums_from_attributes(vo, caches_mock):
+    rse_name = rse_name_generator()
+    rse_id = add_rse(rse_name, vo=vo)
+
+    attrs = list_rse_attributes(rse_id)
+    assert get_rse_supported_checksums_from_attributes(attrs) == GLOBALLY_SUPPORTED_CHECKSUMS
+
+    add_rse_attribute(rse_id, CHECKSUM_KEY, 'none')
+    attrs = list_rse_attributes(rse_id)
+    assert get_rse_supported_checksums_from_attributes(attrs) == []
+
+    add_rse_attribute(rse_id, CHECKSUM_KEY, 'md5')
+    attrs = list_rse_attributes(rse_id)
+    assert get_rse_supported_checksums_from_attributes(attrs) == ['md5']
+
+    add_rse_attribute(rse_id, CHECKSUM_KEY, 'md5,adler32')
+    attrs = list_rse_attributes(rse_id)
+    assert get_rse_supported_checksums_from_attributes(attrs) == ['md5', 'adler32']
+
+    del_rse(rse_id)
+
+
 def test_create_rse_success(vo, rest_client, auth_token):
     """ RSE (REST): send a POST to create a new RSE """
     rse_name = rse_name_generator()