We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
geoip database is always downloaded, even if no URL configured/License key given
Motivation
----------
Currently, the replicas endpoint always tries to download the geoip database, even if no download URL is given, or no license key is given, which leads to the server always trying to download it from maxmindb and consequently failing, thus wasting time.

Modification
------------
If no URL or license key is set, this should just fallback to the exception and thus setting moon distances.
This needs to be backported to `1.26-LTS` as well.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `lib/rucio/core/replica_sorter.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 # Copyright 2020-2022 CERN
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 #    http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #
16 # Authors:
17 # - Benedikt Ziemons <benedikt.ziemons@cern.ch>, 2020-2021
18 # - Ilija Vukotic <ivukotic@cern.ch>, 2021
19 # - David Población Criado <david.poblacion.criado@cern.ch>, 2021
20 # - Martin Barisits <martin.barisits@cern.ch>, 2021
21 # - Radu Carpa <radu.carpa@cern.ch>, 2021-2022
22 
23 from __future__ import print_function, division
24 
25 import random
26 import shutil
27 import socket
28 import tarfile
29 from collections import OrderedDict
30 from datetime import datetime, timedelta
31 from math import asin, cos, radians, sin, sqrt
32 from pathlib import Path
33 from tempfile import TemporaryDirectory, TemporaryFile
34 from typing import TYPE_CHECKING
35 from urllib.parse import urlparse
36 
37 import geoip2.database
38 import requests
39 from dogpile.cache.api import NO_VALUE
40 
41 from rucio.common import utils
42 from rucio.common.cache import make_region_memcached
43 from rucio.common.config import config_get, config_get_bool, config_get_int
44 from rucio.common.exception import InvalidRSEExpression
45 from rucio.core.rse_expression_parser import parse_expression
46 
47 if TYPE_CHECKING:
48     from typing import Dict, List, Optional
49 
50 REGION = make_region_memcached(expiration_time=900, function_key_generator=utils.my_key_generator)
51 
52 # This product uses GeoLite data created by MaxMind,
53 # available from <a href="http://www.maxmind.com">http://www.maxmind.com</a>
54 GEOIP_DB_EDITION = 'GeoLite2-City'
55 
56 
57 def extract_file_from_tar_gz(archive_file_obj, file_name, destination):
58     """
59     Extract one file from the archive and put it at the destination
60 
61     archive_fileobj is supposed to be at position 0
62     """
63     with TemporaryDirectory(prefix=file_name) as tmp_dir:
64         tmp_dir = Path(tmp_dir)
65         with tarfile.open(fileobj=archive_file_obj, mode='r:gz') as tfile:
66             tfile.extractall(path=tmp_dir)
67             for entry in tfile:
68                 if entry.name.find(file_name) > -1:
69                     print('Will move %s to %s' % (tmp_dir / entry.name, destination))
70                     shutil.move(tmp_dir / entry.name, destination)
71 
72 
73 def __download_geoip_db(destination):
74     edition_id = GEOIP_DB_EDITION
75     download_url = config_get('core', 'geoip_download_url', raise_exception=False, default=None)
76     verify_tls = config_get_bool('core', 'geoip_download_verify_tls', raise_exception=False, default=True)
77     if not download_url:
78         licence_key = config_get('core', 'geoip_licence_key', raise_exception=False, default='NOLICENCE')
79         download_url = 'https://download.maxmind.com/app/geoip_download?edition_id=%s&license_key=%s&suffix=tar.gz' % (edition_id, licence_key)
80 
81     result = requests.get(download_url, stream=True, verify=verify_tls)
82     if result and result.status_code in [200, ]:
83         with TemporaryFile() as file_obj:
84             for chunk in result.iter_content(8192):
85                 file_obj.write(chunk)
86             file_obj.seek(0)
87 
88             extract_file_from_tar_gz(archive_file_obj=file_obj, file_name=f'{edition_id}.mmdb', destination=destination)
89     else:
90         raise Exception('Cannot download GeoIP database: %s, Code: %s, Error: %s' % (edition_id,
91                                                                                      result.status_code,
92                                                                                      result.text))
93 
94 
95 def __geoip_db():
96     db_path = Path(f'/tmp/{GEOIP_DB_EDITION}.mmdb')
97     db_expire_delay = timedelta(days=config_get_int('core', 'geoip_expire_delay', raise_exception=False, default=30))
98 
99     must_download = False
100     if not db_path.is_file():
101         print('%s does not exist. Downloading it.' % db_path)
102         must_download = True
103     elif db_expire_delay and datetime.fromtimestamp(db_path.stat().st_mtime) < datetime.now() - db_expire_delay:
104         print('%s is too old. Re-downloading it.' % db_path)
105         must_download = True
106 
107     if must_download:
108         __download_geoip_db(destination=db_path)
109 
110     return geoip2.database.Reader(str(db_path))
111 
112 
113 def __get_lat_long(se, gi):
114     """
115     Get the latitude and longitude on one host using the GeoLite DB
116     :param se  : A hostname or IP.
117     :param gi : A Reader object (geoip2 API).
118     """
119     try:
120         ip = socket.getaddrinfo(se, None)[0][4][0]
121         response = gi.city(ip)
122         return response.location.latitude, response.location.longitude
123     except socket.gaierror as error:
124         # Host definitively unknown
125         print(error)
126     return None, None
127 
128 
129 def __get_distance(se1, client_location, ignore_error):
130     """
131     Get the distance between 2 host using the GeoLite DB
132     :param se1 : A first hostname or IP.
133     :param client_location : contains {'ip', 'fqdn', 'site', 'latitude', 'longitude'}
134     :ignore_error: Ignore exception when the GeoLite DB cannot be retrieved
135     """
136     # does not cache ignore_error, str.lower on hostnames/ips is fine
137     canonical_parties = list(map(lambda x: str(x).lower(), [se1, client_location['ip'], client_location.get('latitude', ''), client_location.get('longitude', '')]))
138     canonical_parties.sort()
139     cache_key = f'replica_sorter:__get_distance|site_distance|{"".join(canonical_parties)}'
140     cache_val = REGION.get(cache_key)
141     if cache_val is NO_VALUE:
142         try:
143             gi = __geoip_db()
144 
145             lat1, long1 = __get_lat_long(se1, gi)
146             if client_location.get('latitude') and client_location.get('longitude'):
147                 lat2 = client_location['latitude']
148                 long2 = client_location['longitude']
149             else:
150                 lat2, long2 = __get_lat_long(client_location['ip'], gi)
151 
152             if lat1 and lat2:
153                 long1, lat1, long2, lat2 = map(radians, [long1, lat1, long2, lat2])
154                 dlon = long2 - long1
155                 dlat = lat2 - lat1
156                 cache_val = 6378 * 2 * asin(sqrt(sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2))
157                 REGION.set(cache_key, cache_val)
158                 return cache_val
159         except Exception as error:
160             if not ignore_error:
161                 raise error
162         # One host is on the Moon
163         cache_val = 360000
164         REGION.set(cache_key, cache_val)
165     return cache_val
166 
167 
168 def site_selector(replicas, site, vo):
169     """
170     Return a list of replicas located on one site.
171     :param replicas : A dict with RSEs as values and replicas as keys (URIs).
172     :param site : The name of the site
173     :param vo : The vo within which to search for RSEs
174     """
175     result = []
176     try:
177         rses = parse_expression("site=%s" % site, filter_={'vo': vo})
178     except InvalidRSEExpression:
179         return result
180     except Exception:
181         return result
182     rses = [i['rse'] for i in rses]
183     for replica in replicas:
184         if replicas[replica] in rses:
185             result.append(replica)
186     return result
187 
188 
189 def sort_replicas(dictreplica: "Dict", client_location: "Dict", selection: "Optional[str]" = None) -> "List":
190     """
191     General sorting method for a dictionary of replicas. Returns the List of replicas.
192 
193     :param dictreplica: A dict with replicas as keys (URIs).
194     :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}
195     :param selection: the selected sorting algorithm.
196     :param default: the default sorting algorithm (random, if not defined).
197     :returns: the keys of dictreplica in a sorted list.
198     """
199     if len(dictreplica) == 0:
200         return []
201     if not selection:
202         selection = 'geoip'
203 
204     items = [(key, value) for key, value in dictreplica.items()]
205     # safety check, TODO: remove if all dictreplica values are 4-tuple with priority as second item
206     if isinstance(items[0][1], tuple) and len(items[0][1]) == 4:
207         # sort by value[1], which is the priority
208         items.sort(key=lambda item: item[1][1])
209     dictreplica = OrderedDict(items)
210 
211     # all sorts must be stable to preserve the priority (the Python standard sorting functions always are stable)
212     if selection == 'geoip':
213         replicas = sort_geoip(dictreplica, client_location, ignore_error=True)
214     elif selection == 'closeness':
215         replicas = sort_closeness(dictreplica, client_location)
216     elif selection == 'dynamic':
217         replicas = sort_dynamic(dictreplica, client_location)
218     elif selection == 'ranking':
219         replicas = sort_ranking(dictreplica, client_location)
220     elif selection == 'random':
221         replicas = sort_random(dictreplica)
222     else:
223         replicas = list(dictreplica.keys())
224 
225     return replicas
226 
227 
228 def sort_random(dictreplica: "Dict") -> "List":
229     """
230     Return a list of replicas sorted randomly.
231     :param dictreplica: A dict with replicas as keys (URIs).
232     """
233 
234     list_replicas = list(dictreplica.keys())
235     random.shuffle(list_replicas)
236     return list_replicas
237 
238 
239 def sort_geoip(dictreplica: "Dict", client_location: "Dict", ignore_error: bool = False) -> "List":
240     """
241     Return a list of replicas sorted by geographical distance to the client IP.
242     :param dictreplica: A dict with replicas as keys (URIs).
243     :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}
244     :param ignore_error: Ignore exception when the GeoLite DB cannot be retrieved
245     """
246 
247     def distance(pfn):
248         return __get_distance(urlparse(pfn).hostname, client_location, ignore_error)
249 
250     return list(sorted(dictreplica, key=distance))
251 
252 
253 def sort_closeness(dictreplica: "Dict", client_location: "Dict") -> "List":
254     """
255     Return a list of replicas sorted by AGIS closeness. NOT IMPLEMENTED
256     :param dictreplica: A dict with replicas as keys (URIs).
257     :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}
258     """
259 
260     return list(dictreplica.keys())
261 
262 
263 def sort_ranking(dictreplica: "Dict", client_location: "Dict") -> "List":
264     """
265     Return a list of replicas sorted by ranking metric. NOT IMPLEMENTED
266     :param dictreplica: A dict with replicas as keys (URIs).
267     :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}
268     """
269 
270     return list(dictreplica.keys())
271 
272 
273 def sort_dynamic(dictreplica: "Dict", client_location: "Dict") -> "List":
274     """
275     Return a list of replicas sorted by dynamic network metrics. NOT IMPLEMENTED
276     :param dictreplica: A dict with replicas as keys (URIs).
277     :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}
278     """
279 
280     return list(dictreplica.keys())
281
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/lib/rucio/core/replica_sorter.py b/lib/rucio/core/replica_sorter.py
--- a/lib/rucio/core/replica_sorter.py
+++ b/lib/rucio/core/replica_sorter.py
@@ -75,7 +75,9 @@
     download_url = config_get('core', 'geoip_download_url', raise_exception=False, default=None)
     verify_tls = config_get_bool('core', 'geoip_download_verify_tls', raise_exception=False, default=True)
     if not download_url:
-        licence_key = config_get('core', 'geoip_licence_key', raise_exception=False, default='NOLICENCE')
+        licence_key = config_get('core', 'geoip_licence_key', raise_exception=False, default=None)
+        if not licence_key:
+            raise Exception('Cannot download GeoIP database: licence key not provided')
         download_url = 'https://download.maxmind.com/app/geoip_download?edition_id=%s&license_key=%s&suffix=tar.gz' % (edition_id, licence_key)
 
     result = requests.get(download_url, stream=True, verify=verify_tls)

{"golden_diff": "diff --git a/lib/rucio/core/replica_sorter.py b/lib/rucio/core/replica_sorter.py\n--- a/lib/rucio/core/replica_sorter.py\n+++ b/lib/rucio/core/replica_sorter.py\n@@ -75,7 +75,9 @@\n     download_url = config_get('core', 'geoip_download_url', raise_exception=False, default=None)\n     verify_tls = config_get_bool('core', 'geoip_download_verify_tls', raise_exception=False, default=True)\n     if not download_url:\n-        licence_key = config_get('core', 'geoip_licence_key', raise_exception=False, default='NOLICENCE')\n+        licence_key = config_get('core', 'geoip_licence_key', raise_exception=False, default=None)\n+        if not licence_key:\n+            raise Exception('Cannot download GeoIP database: licence key not provided')\n         download_url = 'https://download.maxmind.com/app/geoip_download?edition_id=%s&license_key=%s&suffix=tar.gz' % (edition_id, licence_key)\n \n     result = requests.get(download_url, stream=True, verify=verify_tls)\n", "issue": "geoip database is always downloaded, even if no URL configured/License key given\nMotivation\r\n----------\r\nCurrently, the replicas endpoint always tries to download the geoip database, even if no download URL is given, or no license key is given, which leads to the server always trying to download it from maxmindb and consequently failing, thus wasting time.\r\n\r\nModification\r\n------------\r\nIf no URL or license key is set, this should just fallback to the exception and thus setting moon distances.\r\nThis needs to be backported to `1.26-LTS` as well.\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n# Copyright 2020-2022 CERN\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# Authors:\n# - Benedikt Ziemons <benedikt.ziemons@cern.ch>, 2020-2021\n# - Ilija Vukotic <ivukotic@cern.ch>, 2021\n# - David Poblaci\u00f3n Criado <david.poblacion.criado@cern.ch>, 2021\n# - Martin Barisits <martin.barisits@cern.ch>, 2021\n# - Radu Carpa <radu.carpa@cern.ch>, 2021-2022\n\nfrom __future__ import print_function, division\n\nimport random\nimport shutil\nimport socket\nimport tarfile\nfrom collections import OrderedDict\nfrom datetime import datetime, timedelta\nfrom math import asin, cos, radians, sin, sqrt\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory, TemporaryFile\nfrom typing import TYPE_CHECKING\nfrom urllib.parse import urlparse\n\nimport geoip2.database\nimport requests\nfrom dogpile.cache.api import NO_VALUE\n\nfrom rucio.common import utils\nfrom rucio.common.cache import make_region_memcached\nfrom rucio.common.config import config_get, config_get_bool, config_get_int\nfrom rucio.common.exception import InvalidRSEExpression\nfrom rucio.core.rse_expression_parser import parse_expression\n\nif TYPE_CHECKING:\n    from typing import Dict, List, Optional\n\nREGION = make_region_memcached(expiration_time=900, function_key_generator=utils.my_key_generator)\n\n# This product uses GeoLite data created by MaxMind,\n# available from <a href=\"http://www.maxmind.com\">http://www.maxmind.com</a>\nGEOIP_DB_EDITION = 'GeoLite2-City'\n\n\ndef extract_file_from_tar_gz(archive_file_obj, file_name, destination):\n    \"\"\"\n    Extract one file from the archive and put it at the destination\n\n    archive_fileobj is supposed to be at position 0\n    \"\"\"\n    with TemporaryDirectory(prefix=file_name) as tmp_dir:\n        tmp_dir = Path(tmp_dir)\n        with tarfile.open(fileobj=archive_file_obj, mode='r:gz') as tfile:\n            tfile.extractall(path=tmp_dir)\n            for entry in tfile:\n                if entry.name.find(file_name) > -1:\n                    print('Will move %s to %s' % (tmp_dir / entry.name, destination))\n                    shutil.move(tmp_dir / entry.name, destination)\n\n\ndef __download_geoip_db(destination):\n    edition_id = GEOIP_DB_EDITION\n    download_url = config_get('core', 'geoip_download_url', raise_exception=False, default=None)\n    verify_tls = config_get_bool('core', 'geoip_download_verify_tls', raise_exception=False, default=True)\n    if not download_url:\n        licence_key = config_get('core', 'geoip_licence_key', raise_exception=False, default='NOLICENCE')\n        download_url = 'https://download.maxmind.com/app/geoip_download?edition_id=%s&license_key=%s&suffix=tar.gz' % (edition_id, licence_key)\n\n    result = requests.get(download_url, stream=True, verify=verify_tls)\n    if result and result.status_code in [200, ]:\n        with TemporaryFile() as file_obj:\n            for chunk in result.iter_content(8192):\n                file_obj.write(chunk)\n            file_obj.seek(0)\n\n            extract_file_from_tar_gz(archive_file_obj=file_obj, file_name=f'{edition_id}.mmdb', destination=destination)\n    else:\n        raise Exception('Cannot download GeoIP database: %s, Code: %s, Error: %s' % (edition_id,\n                                                                                     result.status_code,\n                                                                                     result.text))\n\n\ndef __geoip_db():\n    db_path = Path(f'/tmp/{GEOIP_DB_EDITION}.mmdb')\n    db_expire_delay = timedelta(days=config_get_int('core', 'geoip_expire_delay', raise_exception=False, default=30))\n\n    must_download = False\n    if not db_path.is_file():\n        print('%s does not exist. Downloading it.' % db_path)\n        must_download = True\n    elif db_expire_delay and datetime.fromtimestamp(db_path.stat().st_mtime) < datetime.now() - db_expire_delay:\n        print('%s is too old. Re-downloading it.' % db_path)\n        must_download = True\n\n    if must_download:\n        __download_geoip_db(destination=db_path)\n\n    return geoip2.database.Reader(str(db_path))\n\n\ndef __get_lat_long(se, gi):\n    \"\"\"\n    Get the latitude and longitude on one host using the GeoLite DB\n    :param se  : A hostname or IP.\n    :param gi : A Reader object (geoip2 API).\n    \"\"\"\n    try:\n        ip = socket.getaddrinfo(se, None)[0][4][0]\n        response = gi.city(ip)\n        return response.location.latitude, response.location.longitude\n    except socket.gaierror as error:\n        # Host definitively unknown\n        print(error)\n    return None, None\n\n\ndef __get_distance(se1, client_location, ignore_error):\n    \"\"\"\n    Get the distance between 2 host using the GeoLite DB\n    :param se1 : A first hostname or IP.\n    :param client_location : contains {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    :ignore_error: Ignore exception when the GeoLite DB cannot be retrieved\n    \"\"\"\n    # does not cache ignore_error, str.lower on hostnames/ips is fine\n    canonical_parties = list(map(lambda x: str(x).lower(), [se1, client_location['ip'], client_location.get('latitude', ''), client_location.get('longitude', '')]))\n    canonical_parties.sort()\n    cache_key = f'replica_sorter:__get_distance|site_distance|{\"\".join(canonical_parties)}'\n    cache_val = REGION.get(cache_key)\n    if cache_val is NO_VALUE:\n        try:\n            gi = __geoip_db()\n\n            lat1, long1 = __get_lat_long(se1, gi)\n            if client_location.get('latitude') and client_location.get('longitude'):\n                lat2 = client_location['latitude']\n                long2 = client_location['longitude']\n            else:\n                lat2, long2 = __get_lat_long(client_location['ip'], gi)\n\n            if lat1 and lat2:\n                long1, lat1, long2, lat2 = map(radians, [long1, lat1, long2, lat2])\n                dlon = long2 - long1\n                dlat = lat2 - lat1\n                cache_val = 6378 * 2 * asin(sqrt(sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2))\n                REGION.set(cache_key, cache_val)\n                return cache_val\n        except Exception as error:\n            if not ignore_error:\n                raise error\n        # One host is on the Moon\n        cache_val = 360000\n        REGION.set(cache_key, cache_val)\n    return cache_val\n\n\ndef site_selector(replicas, site, vo):\n    \"\"\"\n    Return a list of replicas located on one site.\n    :param replicas : A dict with RSEs as values and replicas as keys (URIs).\n    :param site : The name of the site\n    :param vo : The vo within which to search for RSEs\n    \"\"\"\n    result = []\n    try:\n        rses = parse_expression(\"site=%s\" % site, filter_={'vo': vo})\n    except InvalidRSEExpression:\n        return result\n    except Exception:\n        return result\n    rses = [i['rse'] for i in rses]\n    for replica in replicas:\n        if replicas[replica] in rses:\n            result.append(replica)\n    return result\n\n\ndef sort_replicas(dictreplica: \"Dict\", client_location: \"Dict\", selection: \"Optional[str]\" = None) -> \"List\":\n    \"\"\"\n    General sorting method for a dictionary of replicas. Returns the List of replicas.\n\n    :param dictreplica: A dict with replicas as keys (URIs).\n    :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    :param selection: the selected sorting algorithm.\n    :param default: the default sorting algorithm (random, if not defined).\n    :returns: the keys of dictreplica in a sorted list.\n    \"\"\"\n    if len(dictreplica) == 0:\n        return []\n    if not selection:\n        selection = 'geoip'\n\n    items = [(key, value) for key, value in dictreplica.items()]\n    # safety check, TODO: remove if all dictreplica values are 4-tuple with priority as second item\n    if isinstance(items[0][1], tuple) and len(items[0][1]) == 4:\n        # sort by value[1], which is the priority\n        items.sort(key=lambda item: item[1][1])\n    dictreplica = OrderedDict(items)\n\n    # all sorts must be stable to preserve the priority (the Python standard sorting functions always are stable)\n    if selection == 'geoip':\n        replicas = sort_geoip(dictreplica, client_location, ignore_error=True)\n    elif selection == 'closeness':\n        replicas = sort_closeness(dictreplica, client_location)\n    elif selection == 'dynamic':\n        replicas = sort_dynamic(dictreplica, client_location)\n    elif selection == 'ranking':\n        replicas = sort_ranking(dictreplica, client_location)\n    elif selection == 'random':\n        replicas = sort_random(dictreplica)\n    else:\n        replicas = list(dictreplica.keys())\n\n    return replicas\n\n\ndef sort_random(dictreplica: \"Dict\") -> \"List\":\n    \"\"\"\n    Return a list of replicas sorted randomly.\n    :param dictreplica: A dict with replicas as keys (URIs).\n    \"\"\"\n\n    list_replicas = list(dictreplica.keys())\n    random.shuffle(list_replicas)\n    return list_replicas\n\n\ndef sort_geoip(dictreplica: \"Dict\", client_location: \"Dict\", ignore_error: bool = False) -> \"List\":\n    \"\"\"\n    Return a list of replicas sorted by geographical distance to the client IP.\n    :param dictreplica: A dict with replicas as keys (URIs).\n    :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    :param ignore_error: Ignore exception when the GeoLite DB cannot be retrieved\n    \"\"\"\n\n    def distance(pfn):\n        return __get_distance(urlparse(pfn).hostname, client_location, ignore_error)\n\n    return list(sorted(dictreplica, key=distance))\n\n\ndef sort_closeness(dictreplica: \"Dict\", client_location: \"Dict\") -> \"List\":\n    \"\"\"\n    Return a list of replicas sorted by AGIS closeness. NOT IMPLEMENTED\n    :param dictreplica: A dict with replicas as keys (URIs).\n    :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    \"\"\"\n\n    return list(dictreplica.keys())\n\n\ndef sort_ranking(dictreplica: \"Dict\", client_location: \"Dict\") -> \"List\":\n    \"\"\"\n    Return a list of replicas sorted by ranking metric. NOT IMPLEMENTED\n    :param dictreplica: A dict with replicas as keys (URIs).\n    :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    \"\"\"\n\n    return list(dictreplica.keys())\n\n\ndef sort_dynamic(dictreplica: \"Dict\", client_location: \"Dict\") -> \"List\":\n    \"\"\"\n    Return a list of replicas sorted by dynamic network metrics. NOT IMPLEMENTED\n    :param dictreplica: A dict with replicas as keys (URIs).\n    :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    \"\"\"\n\n    return list(dictreplica.keys())\n", "path": "lib/rucio/core/replica_sorter.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n# Copyright 2020-2022 CERN\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# Authors:\n# - Benedikt Ziemons <benedikt.ziemons@cern.ch>, 2020-2021\n# - Ilija Vukotic <ivukotic@cern.ch>, 2021\n# - David Poblaci\u00f3n Criado <david.poblacion.criado@cern.ch>, 2021\n# - Martin Barisits <martin.barisits@cern.ch>, 2021\n# - Radu Carpa <radu.carpa@cern.ch>, 2021-2022\n\nfrom __future__ import print_function, division\n\nimport random\nimport shutil\nimport socket\nimport tarfile\nfrom collections import OrderedDict\nfrom datetime import datetime, timedelta\nfrom math import asin, cos, radians, sin, sqrt\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory, TemporaryFile\nfrom typing import TYPE_CHECKING\nfrom urllib.parse import urlparse\n\nimport geoip2.database\nimport requests\nfrom dogpile.cache.api import NO_VALUE\n\nfrom rucio.common import utils\nfrom rucio.common.cache import make_region_memcached\nfrom rucio.common.config import config_get, config_get_bool, config_get_int\nfrom rucio.common.exception import InvalidRSEExpression\nfrom rucio.core.rse_expression_parser import parse_expression\n\nif TYPE_CHECKING:\n    from typing import Dict, List, Optional\n\nREGION = make_region_memcached(expiration_time=900, function_key_generator=utils.my_key_generator)\n\n# This product uses GeoLite data created by MaxMind,\n# available from <a href=\"http://www.maxmind.com\">http://www.maxmind.com</a>\nGEOIP_DB_EDITION = 'GeoLite2-City'\n\n\ndef extract_file_from_tar_gz(archive_file_obj, file_name, destination):\n    \"\"\"\n    Extract one file from the archive and put it at the destination\n\n    archive_fileobj is supposed to be at position 0\n    \"\"\"\n    with TemporaryDirectory(prefix=file_name) as tmp_dir:\n        tmp_dir = Path(tmp_dir)\n        with tarfile.open(fileobj=archive_file_obj, mode='r:gz') as tfile:\n            tfile.extractall(path=tmp_dir)\n            for entry in tfile:\n                if entry.name.find(file_name) > -1:\n                    print('Will move %s to %s' % (tmp_dir / entry.name, destination))\n                    shutil.move(tmp_dir / entry.name, destination)\n\n\ndef __download_geoip_db(destination):\n    edition_id = GEOIP_DB_EDITION\n    download_url = config_get('core', 'geoip_download_url', raise_exception=False, default=None)\n    verify_tls = config_get_bool('core', 'geoip_download_verify_tls', raise_exception=False, default=True)\n    if not download_url:\n        licence_key = config_get('core', 'geoip_licence_key', raise_exception=False, default=None)\n        if not licence_key:\n            raise Exception('Cannot download GeoIP database: licence key not provided')\n        download_url = 'https://download.maxmind.com/app/geoip_download?edition_id=%s&license_key=%s&suffix=tar.gz' % (edition_id, licence_key)\n\n    result = requests.get(download_url, stream=True, verify=verify_tls)\n    if result and result.status_code in [200, ]:\n        with TemporaryFile() as file_obj:\n            for chunk in result.iter_content(8192):\n                file_obj.write(chunk)\n            file_obj.seek(0)\n\n            extract_file_from_tar_gz(archive_file_obj=file_obj, file_name=f'{edition_id}.mmdb', destination=destination)\n    else:\n        raise Exception('Cannot download GeoIP database: %s, Code: %s, Error: %s' % (edition_id,\n                                                                                     result.status_code,\n                                                                                     result.text))\n\n\ndef __geoip_db():\n    db_path = Path(f'/tmp/{GEOIP_DB_EDITION}.mmdb')\n    db_expire_delay = timedelta(days=config_get_int('core', 'geoip_expire_delay', raise_exception=False, default=30))\n\n    must_download = False\n    if not db_path.is_file():\n        print('%s does not exist. Downloading it.' % db_path)\n        must_download = True\n    elif db_expire_delay and datetime.fromtimestamp(db_path.stat().st_mtime) < datetime.now() - db_expire_delay:\n        print('%s is too old. Re-downloading it.' % db_path)\n        must_download = True\n\n    if must_download:\n        __download_geoip_db(destination=db_path)\n\n    return geoip2.database.Reader(str(db_path))\n\n\ndef __get_lat_long(se, gi):\n    \"\"\"\n    Get the latitude and longitude on one host using the GeoLite DB\n    :param se  : A hostname or IP.\n    :param gi : A Reader object (geoip2 API).\n    \"\"\"\n    try:\n        ip = socket.getaddrinfo(se, None)[0][4][0]\n        response = gi.city(ip)\n        return response.location.latitude, response.location.longitude\n    except socket.gaierror as error:\n        # Host definitively unknown\n        print(error)\n    return None, None\n\n\ndef __get_distance(se1, client_location, ignore_error):\n    \"\"\"\n    Get the distance between 2 host using the GeoLite DB\n    :param se1 : A first hostname or IP.\n    :param client_location : contains {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    :ignore_error: Ignore exception when the GeoLite DB cannot be retrieved\n    \"\"\"\n    # does not cache ignore_error, str.lower on hostnames/ips is fine\n    canonical_parties = list(map(lambda x: str(x).lower(), [se1, client_location['ip'], client_location.get('latitude', ''), client_location.get('longitude', '')]))\n    canonical_parties.sort()\n    cache_key = f'replica_sorter:__get_distance|site_distance|{\"\".join(canonical_parties)}'\n    cache_val = REGION.get(cache_key)\n    if cache_val is NO_VALUE:\n        try:\n            gi = __geoip_db()\n\n            lat1, long1 = __get_lat_long(se1, gi)\n            if client_location.get('latitude') and client_location.get('longitude'):\n                lat2 = client_location['latitude']\n                long2 = client_location['longitude']\n            else:\n                lat2, long2 = __get_lat_long(client_location['ip'], gi)\n\n            if lat1 and lat2:\n                long1, lat1, long2, lat2 = map(radians, [long1, lat1, long2, lat2])\n                dlon = long2 - long1\n                dlat = lat2 - lat1\n                cache_val = 6378 * 2 * asin(sqrt(sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2))\n                REGION.set(cache_key, cache_val)\n                return cache_val\n        except Exception as error:\n            if not ignore_error:\n                raise error\n        # One host is on the Moon\n        cache_val = 360000\n        REGION.set(cache_key, cache_val)\n    return cache_val\n\n\ndef site_selector(replicas, site, vo):\n    \"\"\"\n    Return a list of replicas located on one site.\n    :param replicas : A dict with RSEs as values and replicas as keys (URIs).\n    :param site : The name of the site\n    :param vo : The vo within which to search for RSEs\n    \"\"\"\n    result = []\n    try:\n        rses = parse_expression(\"site=%s\" % site, filter_={'vo': vo})\n    except InvalidRSEExpression:\n        return result\n    except Exception:\n        return result\n    rses = [i['rse'] for i in rses]\n    for replica in replicas:\n        if replicas[replica] in rses:\n            result.append(replica)\n    return result\n\n\ndef sort_replicas(dictreplica: \"Dict\", client_location: \"Dict\", selection: \"Optional[str]\" = None) -> \"List\":\n    \"\"\"\n    General sorting method for a dictionary of replicas. Returns the List of replicas.\n\n    :param dictreplica: A dict with replicas as keys (URIs).\n    :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    :param selection: the selected sorting algorithm.\n    :param default: the default sorting algorithm (random, if not defined).\n    :returns: the keys of dictreplica in a sorted list.\n    \"\"\"\n    if len(dictreplica) == 0:\n        return []\n    if not selection:\n        selection = 'geoip'\n\n    items = [(key, value) for key, value in dictreplica.items()]\n    # safety check, TODO: remove if all dictreplica values are 4-tuple with priority as second item\n    if isinstance(items[0][1], tuple) and len(items[0][1]) == 4:\n        # sort by value[1], which is the priority\n        items.sort(key=lambda item: item[1][1])\n    dictreplica = OrderedDict(items)\n\n    # all sorts must be stable to preserve the priority (the Python standard sorting functions always are stable)\n    if selection == 'geoip':\n        replicas = sort_geoip(dictreplica, client_location, ignore_error=True)\n    elif selection == 'closeness':\n        replicas = sort_closeness(dictreplica, client_location)\n    elif selection == 'dynamic':\n        replicas = sort_dynamic(dictreplica, client_location)\n    elif selection == 'ranking':\n        replicas = sort_ranking(dictreplica, client_location)\n    elif selection == 'random':\n        replicas = sort_random(dictreplica)\n    else:\n        replicas = list(dictreplica.keys())\n\n    return replicas\n\n\ndef sort_random(dictreplica: \"Dict\") -> \"List\":\n    \"\"\"\n    Return a list of replicas sorted randomly.\n    :param dictreplica: A dict with replicas as keys (URIs).\n    \"\"\"\n\n    list_replicas = list(dictreplica.keys())\n    random.shuffle(list_replicas)\n    return list_replicas\n\n\ndef sort_geoip(dictreplica: \"Dict\", client_location: \"Dict\", ignore_error: bool = False) -> \"List\":\n    \"\"\"\n    Return a list of replicas sorted by geographical distance to the client IP.\n    :param dictreplica: A dict with replicas as keys (URIs).\n    :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    :param ignore_error: Ignore exception when the GeoLite DB cannot be retrieved\n    \"\"\"\n\n    def distance(pfn):\n        return __get_distance(urlparse(pfn).hostname, client_location, ignore_error)\n\n    return list(sorted(dictreplica, key=distance))\n\n\ndef sort_closeness(dictreplica: \"Dict\", client_location: \"Dict\") -> \"List\":\n    \"\"\"\n    Return a list of replicas sorted by AGIS closeness. NOT IMPLEMENTED\n    :param dictreplica: A dict with replicas as keys (URIs).\n    :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    \"\"\"\n\n    return list(dictreplica.keys())\n\n\ndef sort_ranking(dictreplica: \"Dict\", client_location: \"Dict\") -> \"List\":\n    \"\"\"\n    Return a list of replicas sorted by ranking metric. NOT IMPLEMENTED\n    :param dictreplica: A dict with replicas as keys (URIs).\n    :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    \"\"\"\n\n    return list(dictreplica.keys())\n\n\ndef sort_dynamic(dictreplica: \"Dict\", client_location: \"Dict\") -> \"List\":\n    \"\"\"\n    Return a list of replicas sorted by dynamic network metrics. NOT IMPLEMENTED\n    :param dictreplica: A dict with replicas as keys (URIs).\n    :param client_location: Location dictionary containing {'ip', 'fqdn', 'site', 'latitude', 'longitude'}\n    \"\"\"\n\n    return list(dictreplica.keys())\n", "path": "lib/rucio/core/replica_sorter.py"}]}