]> git.ipfire.org Git - pbs.git/commitdiff
mirrors: Refactor the selection process
authorMichael Tremer <michael.tremer@ipfire.org>
Thu, 13 Feb 2025 20:37:20 +0000 (20:37 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Thu, 13 Feb 2025 20:37:20 +0000 (20:37 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
src/buildservice/mirrors.py

index 692d9b4641eaac0e04dfc4c19a95c6e3cee73064..250bdc82ed2b07ceba9ce57379b9451dc1918955 100644 (file)
@@ -3,6 +3,7 @@
 import asyncio
 import datetime
 import functools
+import ipaddress
 import logging
 import os.path
 import random
@@ -83,20 +84,20 @@ class Mirrors(base.Object):
                        Returns all mirrors in random order with preferred mirrors first
                """
                # Lookup the client
-               network = self.location.lookup(address)
+               network = self.location.lookup("%s" % address)
 
                def __sort(mirror):
                        # Generate some random value for each mirror
                        r = random.random()
 
-                       # Put preferred mirrors first
-                       if network and mirror.is_preferred_for_network(network):
-                               r += 1
+                       # Add the preference score
+                       if network:
+                               r += mirror.is_preferred_for_network(network)
 
                        return r
 
                # Fetch all mirrors and shuffle them, but put preferred mirrors first
-               return sorted([mirror async for mirror in self], key=__sort)
+               return sorted([mirror async for mirror in self], key=__sort, reverse=True)
 
        @functools.cached_property
        def location(self):
@@ -263,6 +264,15 @@ class Mirror(database.Base, database.BackendMixin, database.SoftDeleteMixin):
 
                return False
 
+       # Addresses
+
+       @property
+       def addresses(self):
+               """
+                       All addresses of the mirror, regardless of family
+               """
+               return self.addresses_ipv6 + self.addresses_ipv4
+
        async def _update_country_code_and_asn(self):
                """
                        Updates the country code of this mirror
@@ -292,7 +302,7 @@ class Mirror(database.Base, database.BackendMixin, database.SoftDeleteMixin):
                ]
 
                # Lookup the country code and ASN
-               for address in self.addresses_ipv6 + self.addresses_ipv4:
+               for address in self.addresses:
                        network = self.backend.mirrors.location.lookup(address)
 
                        # Try the next IP address if we didn't find any data
@@ -309,24 +319,63 @@ class Mirror(database.Base, database.BackendMixin, database.SoftDeleteMixin):
                        break
 
        def is_preferred_for_address(self, address):
+               """
+                       Check if this mirror is preferred for this address
+               """
                # Lookup the client
                network = self.backend.mirrors.location.lookup("%s" % address)
 
+               # Check for the entire network
                return self.is_preferred_for_network(network)
 
        def is_preferred_for_network(self, network):
                """
-                       Returns True if this mirror is preferred for clients on the given network.
+                       Returns a score of how much the mirror is preferred for this network.
                """
+               first_address = ipaddress.ip_address(network.first_address)
+               last_address  = ipaddress.ip_address(network.last_address)
+
+               # Check if the mirror is on the same network
+               for address in self.addresses:
+                       # Skip incompatible families
+                       if isinstance(address, ipaddress.IPv6Address):
+                               if not network.family == socket.AF_INET6:
+                                       continue
+                       elif isinstance(address, ipaddress.IPv4Address):
+                               if not network.family == socket.AF_INET:
+                                       continue
+
+                       # Check if the address is within the network
+                       if first_address <= address <= last_address:
+                               return 4
+
                # If the AS matches, we will prefer this
                if self.asn and self.asn == network.asn:
-                       return True
+                       return 3
 
                # If the mirror and client are in the same country, we prefer this
                if self.country_code and self.country_code == network.country_code:
-                       return True
+                       return 2
 
-               return False
+               # Check if we are on the same continent
+               if self._continent_match(self.country_code, network.country_code):
+                       return 1
+
+               return 0
+
+       def _continent_match(self, cc1, cc2):
+               """
+                       Checks if the two given country codes are on the same continent
+               """
+               country1 = self.backend.mirrors.location.get_country(cc1)
+               country2 = self.backend.mirrors.location.get_country(cc2)
+
+               # If we are missing either country, we don't know
+               if not country1 or not country2:
+                       return False
+
+               # Return True if both countries are on the same continent
+               return country1.continent_code == country2.continent_code
 
        # Check