Repository: rucio/rucio
Issue: Site attribute check for lan/wan selection needs to be optional

Motivation
----------
Currently list_replicas checks the `site` attribute of the RSE and matches it with the `client_location`. However, if the site attribute is not set the method crashes. This should be changed and just the wan replica should be returned.

Changed files:
lib/rucio/core/rse.py, lib/rucio/core/replica.py, lib/rucio/tests/test_replica.py

diff --git a/lib/rucio/core/replica.py b/lib/rucio/core/replica.py
index 0835f5f87..539d1b464 100644
--- a/lib/rucio/core/replica.py
+++ b/lib/rucio/core/replica.py
@@ -900,8 +900,16 @@ def _list_replicas(dataset_clause, file_clause, state_clause, show_pfns,
                         # cannot be pushed into protocols because we need to lookup rse attributes.
                         # ultra-conservative implementation.
                         if domain == 'wan' and protocol.attributes['scheme'] == 'root' and client_location:
+
                             if 'site' in client_location and client_location['site']:
-                                replica_site = get_rse_attribute('site', rse_info[rse]['id'], session=session)[0]
+
+                                # is the RSE site-configured?
+                                rse_site_attr = get_rse_attribute('site', rse_info[rse]['id'], session=session)
+                                replica_site = ['']
+                                if isinstance(rse_site_attr, list) and len(rse_site_attr):
+                                    replica_site = rse_site_attr[0]
+
+                                # does it match with the client?
                                 if client_location['site'] != replica_site:
                                     root_proxy_internal = get_rses_with_attribute_value('site', client_location['site'],
                                                                                         'root-proxy-internal',
diff --git a/lib/rucio/core/rse.py b/lib/rucio/core/rse.py
index f783873c2..a23f22c7b 100644
--- a/lib/rucio/core/rse.py
+++ b/lib/rucio/core/rse.py
@@ -404,7 +404,7 @@ def get_rses_with_attribute_value(key, value, lookup_key, session=None):
     :returns: List of rse dictionaries with the rse_id and lookup_key/value pair
     """
 
-    result = REGION.get('%s-%s-%s' % (key, value, lookup_key))
+    result = REGION.get('av-%s-%s-%s' % (key, value, lookup_key))
     if result is NO_VALUE:
 
         rse_list = []
@@ -427,7 +427,7 @@ def get_rses_with_attribute_value(key, value, lookup_key, session=None):
                              'key': row[1],
                              'value': row[2]})
 
-        REGION.set('%s-%s-%s' % (key, value, lookup_key), rse_list)
+        REGION.set('av-%s-%s-%s' % (key, value, lookup_key), rse_list)
         return rse_list
 
     return result
@@ -445,18 +445,26 @@ def get_rse_attribute(key, rse_id=None, value=None, session=None):
 
     :returns: A list with RSE attribute values for a Key.
     """
-    rse_attrs = []
-    if rse_id:
-        query = session.query(models.RSEAttrAssociation.value).filter_by(rse_id=rse_id, key=key).distinct()
-        if value:
-            query = session.query(models.RSEAttrAssociation.value).filter_by(rse_id=rse_id, key=key, value=value).distinct()
-    else:
-        query = session.query(models.RSEAttrAssociation.value).filter_by(key=key).distinct()
-        if value:
-            query = session.query(models.RSEAttrAssociation.value).filter_by(key=key, value=value).distinct()
-    for attr_value in query:
-        rse_attrs.append(attr_value[0])
-    return rse_attrs
+
+    result = REGION.get('%s-%s-%s' % (key, rse_id, value))
+    if result is NO_VALUE:
+
+        rse_attrs = []
+        if rse_id:
+            query = session.query(models.RSEAttrAssociation.value).filter_by(rse_id=rse_id, key=key).distinct()
+            if value:
+                query = session.query(models.RSEAttrAssociation.value).filter_by(rse_id=rse_id, key=key, value=value).distinct()
+        else:
+            query = session.query(models.RSEAttrAssociation.value).filter_by(key=key).distinct()
+            if value:
+                query = session.query(models.RSEAttrAssociation.value).filter_by(key=key, value=value).distinct()
+        for attr_value in query:
+            rse_attrs.append(attr_value[0])
+
+        REGION.set('%s-%s-%s' % (key, rse_id, value), rse_attrs)
+        return rse_attrs
+
+    return result
 
 
 @transactional_session
diff --git a/lib/rucio/tests/test_replica.py b/lib/rucio/tests/test_replica.py
index e282df851..483e73685 100644
--- a/lib/rucio/tests/test_replica.py
+++ b/lib/rucio/tests/test_replica.py
@@ -44,7 +44,7 @@ from rucio.core.replica import (add_replica, add_replicas, delete_replicas,
                                 declare_bad_file_replicas, list_bad_replicas,
                                 update_replicas_paths, update_replica_state,
                                 get_replica_atime, touch_replica)
-from rucio.core.rse import add_rse, add_protocol
+from rucio.core.rse import add_rse, add_protocol, add_rse_attribute, del_rse_attribute
 from rucio.daemons.necromancer import run
 from rucio.rse import rsemanager as rsemgr
 from rucio.tests.common import execute, rse_name_generator
@@ -341,6 +341,45 @@ class TestReplicaCore:
             assert_in('/i/prefer/the/wan', stdout)
             assert_in('/i/prefer/the/lan', stdout)
 
+    def test_replica_no_site(self):
+        """ REPLICA (CORE): Test listing replicas without site attribute """
+
+        rc = ReplicaClient()
+
+        rse = 'APERTURE_%s' % rse_name_generator()
+        add_rse(rse)
+
+        add_protocol(rse, {'scheme': 'root',
+                           'hostname': 'root.aperture.com',
+                           'port': 1409,
+                           'prefix': '//test/chamber/',
+                           'impl': 'rucio.rse.protocols.xrootd.Default',
+                           'domains': {
+                               'lan': {'read': 1, 'write': 1, 'delete': 1},
+                               'wan': {'read': 1, 'write': 1, 'delete': 1}}})
+
+        add_rse_attribute(rse=rse, key='site', value='APERTURE')
+
+        files = [{'scope': 'mock', 'name': 'element_%s' % generate_uuid(),
+                  'bytes': 1234, 'adler32': 'deadbeef'}]
+        add_replicas(rse=rse, files=files, account='root')
+
+        replicas = [r for r in rc.list_replicas(dids=[{'scope': 'mock', 'name': f['name']} for f in files])]
+        assert_in('root://', replicas[0]['pfns'].keys()[0])
+
+        replicas = [r for r in rc.list_replicas(dids=[{'scope': 'mock', 'name': f['name']} for f in files],
+                                                client_location={'site': 'SOMEWHERE'})]
+        assert_in('root://', replicas[0]['pfns'].keys()[0])
+
+        del_rse_attribute(rse=rse, key='site')
+
+        replicas = [r for r in rc.list_replicas(dids=[{'scope': 'mock', 'name': f['name']} for f in files])]
+        assert_in('root://', replicas[0]['pfns'].keys()[0])
+
+        replicas = [r for r in rc.list_replicas(dids=[{'scope': 'mock', 'name': f['name']} for f in files],
+                                                client_location={'site': 'SOMEWHERE'})]
+        assert_in('root://', replicas[0]['pfns'].keys()[0])
+
 
 class TestReplicaClients: