]> git.ipfire.org Git - people/shoehn/ipfire.org.git/commitdiff
mirrors: New functions for geo load-balancing.
authorMichael Tremer <michael.tremer@ipfire.org>
Tue, 18 Jan 2011 22:07:32 +0000 (23:07 +0100)
committerMichael Tremer <michael.tremer@ipfire.org>
Tue, 18 Jan 2011 22:07:32 +0000 (23:07 +0100)
www/static/css/style.css
www/templates/mirrors-item.html
www/templates/mirrors.html
www/templates/modules/mirrors-table.html [new file with mode: 0644]
www/translations/de_DE.csv
www/webapp/__init__.py
www/webapp/backend/geoip.py
www/webapp/backend/mirrors.py
www/webapp/handlers_download.py
www/webapp/handlers_mirrors.py
www/webapp/ui_modules.py

index a2660d98dc0d42e511e5319e8d9ec08fd9c8dbd3..21f096f7de8db5eff08b9aa768afaa862219aab4 100644 (file)
@@ -886,6 +886,7 @@ table.mirrors td {
 table.mirrors td.hostname {
        text-align: right;
        padding-left: 2em;
+       width: 10em;
 }
 
 table.mirrors td.down {
index f81ed7c5fc4e7a419b7ffc1d3c81303c052712c1..81cbaa4e5ed8c9c3fe589ef664a417a2bb0c31ac 100644 (file)
                        <td>{{ _("Owner") }}</td>
                        <td>{{ item.owner }}</td>
                </tr>
+               {% if item.prefer_for_countries %}
+                       <tr>
+                               <td>{{ _("Preferred for") }}:</td>
+                               <td>{{ locale.list(item.prefer_for_countries_names) }}</td>
+                       </tr>
+               {% end %}
        </table>
 
        <p class="links">
@@ -43,7 +49,7 @@
                {{ _("The mirror <em>%s</em> is located in %s.") % (item.hostname, item.location_str) }}
                <br class="clear" />
                <img class="map"
-                       src="http://maps.google.com/maps/api/staticmap?center={{ item.coordiantes }}&size=640x280&zoom=6&markers=color:blue|label:.|{{ item.coordiantes }}&sensor=false"
+                       src="http://maps.google.com/maps/api/staticmap?center={{ item.coordiante_str }}&size=640x280&zoom=6&markers=color:blue|label:.|{{ item.coordiante_str }}&sensor=false"
                        alt="{{ _("Location of the server") }}" />
        </p>
 {% end block %}
index 0ec796a8762b3808068ecfc75d36505bb94a8d47..fadaf44d4f1b6bccdf81c19905e21db666aa8ecf 100644 (file)
 
        <br class="clear" />
 
-       <h3>{{ _("List of servers") }}</h3>
-       <table class="mirrors">
-               {% for mirror in mirrors %}
-                       <tr>
-                               <td class="hostname {{ mirror.state.lower() }}">
-                                       <a href="/mirror/{{ mirror.id }}">{{ mirror.hostname }}</a>
-                               </td>
-                               {% if not mirror.state == "UP" %}
-                                       <td>{{ _("Last update") }}: {{ locale.format_date(mirror.last_update) }}.</td>
-                               {% else %}
-                                       <td>&nbsp;</td>
-                               {% end %}
-                       </tr>
-               {% end %}
-       </table>
+       {% if other_mirrors %}
+               <h3>{{ _("Mirror servers nearby") }}</h3>
+               {{ modules.MirrorsTable(preferred_mirrors) }}
+
+               <h3>{{ _("Worldwide mirror servers") }}</h3>
+               {{ modules.MirrorsTable(other_mirrors) }}
+       {% else %}
+               <h3>{{ _("Worldwide mirror servers") }}</h3>
+               {{ modules.MirrorsTable(preferred_mirrors) }}
+       {% end %}
 {% end block %}
 
 {% block sidebar %}
diff --git a/www/templates/modules/mirrors-table.html b/www/templates/modules/mirrors-table.html
new file mode 100644 (file)
index 0000000..ce44617
--- /dev/null
@@ -0,0 +1,15 @@
+<table class="mirrors">
+       {% for mirror in mirrors %}
+               <tr>
+                       <td class="hostname {{ mirror.state.lower() }}">
+                               <a href="/mirror/{{ mirror.id }}">{{ mirror.hostname }}</a>
+                       </td>
+                       <td>
+                               <img src="{{ static_url("images/flags/%s.png") % mirror.country_code }}" alt="{{ mirror.country_code }}" />
+                               {{ mirror.location_str }}
+                       </td>
+               </tr>
+       {% end %}
+</table>
+<br class="clear" />
+
index c0396a0934691790a41771684161716be171a778..6741200fed19c1c59497535da29ba9fb1bcfd073 100644 (file)
 "Go to the wiki","Zum Wiki"
 "Documentation","Dokumentation"
 "Configuration","Konfiguration"
+"Mirror servers nearby","Mirror-Server in der Nähe"
+"Worldwide mirror servers","Weltweite Mirror-Server"
+"Preferred for","Bevorzugt für"
index 7274e109acb44b124595873a7439d2ee4362a032..fb1bca7aa7a2476fd3afb4551d65886f6d106df1 100644 (file)
@@ -21,13 +21,14 @@ class Application(tornado.web.Application):
        def __init__(self):
                settings = dict(
                        cookie_secret = "aXBmaXJlY29va2llc2VjcmV0Cg==",
-                       debug = False,
+                       debug = True,
                        gzip = True,
                        login_url = "/login",
                        template_path = os.path.join(BASEDIR, "templates"),
                        ui_modules = {
                                "Menu"           : MenuModule,
                                "MirrorItem"     : MirrorItemModule,
+                               "MirrorsTable"   : MirrorsTableModule,
                                "NewsItem"       : NewsItemModule,
                                "NewsLine"       : NewsLineModule,
                                "PlanetEntry"    : PlanetEntryModule,
index 9a68fe39fd810b7e1a6f0c8ade00e03c7d28ed4e..123bc772a82ea37f518cb79a30b76adfc0b305a0 100644 (file)
@@ -3,6 +3,7 @@
 import re
 
 from databases import Databases
+from memcached import Memcached
 from misc import Singleton
 
 class GeoIP(object):
@@ -15,6 +16,10 @@ class GeoIP(object):
        def db(self):
                return Databases().geoip
 
+       @property
+       def memcached(self):
+               return Memcached()
+
        def __encode_ip(self, addr):
                # We get a tuple if there were proxy headers.
                addr = addr.split(", ")
@@ -27,15 +32,37 @@ class GeoIP(object):
                return int(((int(a1) * 256 + int(a2)) * 256 + int(a3)) * 256 + int(a4) + 100)
 
        def get_country(self, addr):
-               return self.db.get("SELECT * FROM ip_group_country WHERE ip_start <= %s \
-                       ORDER BY ip_start DESC LIMIT 1;", self.__encode_ip(addr)).country_code.lower()
+               addr = self.__encode_ip(addr)
+
+               mem_id = "geoip-country-%s" % addr
+               ret = self.memcached.get(mem_id)
+
+               if not ret:
+                       ret = self.db.get("SELECT * FROM ip_group_country WHERE ip_start <= %s \
+                               ORDER BY ip_start DESC LIMIT 1;", addr).country_code.lower()
+                       self.memcached.set(mem_id, ret, 3600)
+
+               return ret
 
        def get_all(self, addr):
-               # XXX should be done with a join
-               location = self.db.get("SELECT location FROM ip_group_city WHERE ip_start <= %s \
-                       ORDER BY ip_start DESC LIMIT 1;", self.__encode_ip(addr)).location
-                       
-               return self.db.get("SELECT * FROM locations WHERE id = %s", int(location))
+               addr = self.__encode_ip(addr)
+
+               mem_id = "geoip-all-%s" % addr
+               ret = self.memcached.get(mem_id)
+
+               if not ret:
+                       # XXX should be done with a join
+                       location = self.db.get("SELECT location FROM ip_group_city WHERE ip_start <= %s \
+                               ORDER BY ip_start DESC LIMIT 1;", addr).location
+
+                       ret = self.db.get("SELECT * FROM locations WHERE id = %s", int(location))
+                       self.memcached.set(mem_id, ret, 3600)
+
+               # If location was not determinable
+               if ret.latitude == 0 and ret.longitude == 0:
+                       return None
+
+               return ret
 
        def get_country_name(self, code):
                name = "Unknown"
index 1de463124f85da838bdbf530881963ec39d09f04..cc5af681a43e6f5d39a73313bf06003999056685 100644 (file)
@@ -1,13 +1,16 @@
 #!/usr/bin/python
 
 import logging
+import math
 import os.path
+import random
 import socket
 import time
 import tornado.httpclient
 
 from databases import Databases
 from geoip import GeoIP
+from memcached import Memcached
 from misc import Singleton
 
 class Mirrors(object):
@@ -17,6 +20,10 @@ class Mirrors(object):
        def db(self):
                return Databases().webapp
 
+       @property
+       def memcached(self):
+               return Memcached()
+
        def list(self):
                return [Mirror(m.id) for m in self.db.query("SELECT id FROM mirrors ORDER BY state")]
 
@@ -27,6 +34,9 @@ class Mirrors(object):
        def get(self, id):
                return Mirror(id)
 
+       def get_all(self):
+               return MirrorSet(self.list())
+
        def get_by_hostname(self, hostname):
                mirror = self.db.get("SELECT id FROM mirrors WHERE hostname=%s", hostname)
 
@@ -60,6 +70,22 @@ class Mirrors(object):
                for mirror in mirrors:
                        yield self.get(mirror.id)
 
+       def get_for_location(self, addr):
+               distance = 10
+
+               mirrors = []
+               all_mirrors = self.list()
+
+               while all_mirrors and len(mirrors) <= 2 and distance <= 270:
+                       for mirror in all_mirrors:
+                               if mirror.distance_to(addr) <= distance:
+                                       mirrors.append(mirror)
+                                       all_mirrors.remove(mirror)
+
+                       distance *= 1.2
+
+               return mirrors
+
        def get_all_files(self):
                files = []
 
@@ -74,6 +100,97 @@ class Mirrors(object):
                return files
 
 
+class MirrorSet(object):
+       def __init__(self, mirrors):
+               self._mirrors = mirrors
+
+       def __add__(self, other):
+               mirrors = []
+
+               for mirror in self._mirrors + other._mirrors:
+                       if mirror in mirrors:
+                               continue
+
+                       mirrors.append(mirror)
+
+               return MirrorSet(mirrors)
+
+       def __sub__(self, other):
+               mirrors = self._mirrors[:]
+
+               for mirror in other._mirrors:
+                       if mirror in mirrors:
+                               mirrors.remove(mirror)
+
+               return MirrorSet(mirrors)
+
+       def __iter__(self):
+               return iter(self._mirrors)
+
+       def __len__(self):
+               return len(self._mirrors)
+
+       def __str__(self):
+               return "<MirrorSet %s>" % ", ".join([m.hostname for m in self._mirrors])
+
+       @property
+       def db(self):
+               return Mirrors().db
+
+       def get_with_file(self, filename):
+               with_file = [m.mirror for m in self.db.query("SELECT mirror FROM mirror_files WHERE filename=%s", filename)]
+
+               mirrors = []
+               for mirror in self._mirrors:
+                       if mirror.id in with_file:
+                               mirrors.append(mirror)
+
+               return MirrorSet(mirrors)
+
+       def get_random(self):
+               mirrors = []
+               for mirror in self._mirrors:
+                       for i in range(0, mirror.priority + 1):
+                               mirrors.append(mirror)
+
+               return random.choice(mirrors)
+
+       def get_for_country(self, country):
+               mirrors = []
+
+               for mirror in self._mirrors:
+                       if country in mirror.prefer_for_countries:
+                               mirrors.append(mirror)
+
+               return MirrorSet(mirrors)
+
+       def get_for_location(self, addr):
+               distance = 10
+
+               mirrors = []
+
+               while len(mirrors) <= 2 and distance <= 270:
+                       for mirror in self._mirrors:
+                               if mirror in mirrors:
+                                       continue
+
+                               if mirror.distance_to(addr) <= distance:
+                                       mirrors.append(mirror)
+
+                       distance *= 1.2
+
+               return MirrorSet(mirrors)
+
+       def get_with_state(self, state):
+               mirrors = []
+
+               for mirror in self._mirrors:
+                       if mirror.state == state:
+                               mirrors.append(mirror)
+
+               return MirrorSet(mirrors)
+
+
 class Mirror(object):
        def __init__(self, id):
                self.id = id
@@ -91,8 +208,15 @@ class Mirror(object):
                return Databases().webapp
 
        def reload(self):
-               self._info = self.db.get("SELECT * FROM mirrors WHERE id=%s", self.id)
-               self._info["url"] = self.generate_url()
+               memcached = Memcached()
+               mem_id = "mirror-%s" % self.id
+
+               self._info = memcached.get(mem_id)
+               if not self._info:
+                       self._info = self.db.get("SELECT * FROM mirrors WHERE id=%s", self.id)
+                       self._info["url"] = self.generate_url()
+
+                       memcached.set(mem_id, self._info, 60)
 
        def generate_url(self):
                url = "http://%s" % self.hostname
@@ -113,10 +237,57 @@ class Mirror(object):
        def address(self):
                return socket.gethostbyname(self.hostname)
 
+       @property
+       def location(self):
+               if not hasattr(self, "__location"):
+                       self.__location = GeoIP().get_all(self.address)
+
+               return self.__location
+
+       @property
+       def latitude(self):
+               return self.location.latitude
+
+       @property
+       def longitude(self):
+               return self.location.longitude
+
+       @property
+       def coordinates(self):
+               return (self.latitude, self.longitude)
+
+       @property
+       def coordiante_str(self):
+               coordinates = []
+
+               for i in self.coordinates:
+                       coordinates.append("%s" % i)
+
+               return ",".join(coordinates)
+
        @property
        def country_code(self):
                return GeoIP().get_country(self.address).lower() or "unknown"
 
+       @property
+       def country_name(self):
+               return GeoIP().get_country_name(self.country_code)
+
+       @property
+       def city(self):
+               if self._info["city"]:
+                       return self._info["city"]
+
+               return self.location.city
+
+       @property
+       def location_str(self):
+               s = self.country_name
+               if self.city:
+                       s = "%s, %s" % (self.city, s)
+
+               return s
+
        @property
        def filelist(self):
                filelist = self.db.query("SELECT filename FROM mirror_files WHERE mirror=%s ORDER BY filename", self.id)
@@ -227,12 +398,53 @@ class Mirror(object):
 
        @property
        def prefer_for_countries(self):
-               return self._info.get("prefer_for_countries", "").split()
+               countries = self._info.get("prefer_for_countries", "")
+               if countries:
+                       return sorted(countries.split(", "))
 
+               return []
+
+       @property
+       def prefer_for_countries_names(self):
+               return sorted([GeoIP().get_country_name(c) for c in self.prefer_for_countries])
 
+       def distance_to(self, addr):
+               location = GeoIP().get_all(addr)
+               if not location:
+                       return 0
 
-if __name__ == "__main__":
-       m = Mirrors()
+               if location.country_code.lower() in self.prefer_for_countries:
+                       return 0
+
+               distance_vector = (
+                       self.latitude - location.latitude,
+                       self.longitude - location.longitude
+               )
+
+               distance = 0
+               for i in distance_vector:
+                       distance += i**2
+
+               return math.sqrt(distance)
+
+       def traffic(self, since):
+               # XXX needs to be done better
+
+               files = {}
+               for entry in self.db.query("SELECT filename, filesize FROM files"):
+                       files[entry.filename] = entry.filesize
+
+               query = "SELECT COUNT(filename) as count, filename FROM log_download WHERE mirror = %s"
+               query += " AND date >= %s GROUP BY filename"
+
+               traffic = 0
+               for entry in self.db.query(query, self.id, since):
+                       if files.has_key(entry.filename):
+                               traffic += entry.count * files[entry.filename]
+
+               return traffic
+
+       @property
+       def priority(self):
+               return self._info.get("priority", 1) * 10
 
-       for mirror in m.list():
-               print mirror.hostname, mirror.country_code
index d8b1dc3de6a44950cf9f5a0981ce13c9cf83aeaf..84874c34fe13a9ed770cdaecdcd1c75859727125 100644 (file)
@@ -68,19 +68,25 @@ class DownloadDevelopmentHandler(BaseHandler):
 
 class DownloadFileHandler(BaseHandler):
        def get(self, filename):
-               country_code = self.geoip.get_country(self.request.remote_ip)
-
                self.set_header("Pragma", "no-cache")
-               self.set_header("X-Mirror-Client-Country", country_code)
 
-               mirrors = self.mirrors.get_with_file(filename, country=country_code)
-               if not mirrors:
-                       self.mirrors.get_with_file(filename)
+               # Get all mirrors...
+               mirrors = self.mirrors.get_all()
+               mirrors = mirrors.get_with_file(filename)
+               mirrors = mirrors.get_with_state("UP")
 
                if not mirrors:
                        raise tornado.web.HTTPError(404, "File not found: %s" % filename)
 
-               mirror = random.choice(mirrors)
+               # Find mirrors located near to the user.
+               # If we have not found any, we use all.
+               if len(mirrors) <= 3:
+                       #mirrors_nearby = mirrors.get_for_location(self.request.remote_ip)
+                       mirrors_nearby = mirrors.get_for_location("193.59.194.101")
+                       if mirrors_nearby:
+                               mirrors = mirrors_nearby
+
+               mirror = mirrors.get_random()
 
                self.redirect(mirror.url + filename[len(mirror.prefix):])
 
index 86d2e8ad526e756c1b7b0a3a93c094ad4a0f1fb5..8ae911df546aa2c665071afc1c920149b4506fa7 100644 (file)
@@ -7,30 +7,29 @@ from handlers_base import *
 
 class MirrorIndexHandler(BaseHandler):
        def get(self):
-               mirrors = self.mirrors.list()
+               ip_addr = self.get_argument("addr", self.request.remote_ip)
 
-               self.render("mirrors.html", mirrors=mirrors)
+               # Get a list of all mirrors.
+               all_mirrors = self.mirrors.get_all()
+
+               # Choose the preferred ones by their location.
+               preferred_mirrors = all_mirrors.get_for_location(ip_addr)
+
+               # Remove the preferred ones from the list of the rest.
+               other_mirrors = all_mirrors - preferred_mirrors
+
+               self.render("mirrors.html",
+                       preferred_mirrors=preferred_mirrors, other_mirrors=other_mirrors)
 
 
 class MirrorItemHandler(BaseHandler):
        def get(self, id):
+               _ = self.locale.translate
+
                mirror = self.mirrors.get(id)
                if not mirror:
                        raise tornado.web.HTTPError(404)
 
-               ip = socket.gethostbyname(mirror.hostname)
-               mirror.location = self.geoip.get_all(ip)
-
-               # Shortcut for coordiantes
-               mirror.coordiantes = "%s,%s" % \
-                       (mirror.location.latitude, mirror.location.longitude)
-
-               # Nice string for the user
-               mirror.location_str = mirror.location.country_code
-               if mirror.location.city:
-                       mirror.location_str = "%s, %s" % \
-                               (mirror.location.city, mirror.location_str)
-
                self.render("mirrors-item.html", item=mirror)
 
 
index ba8360879cf4789cb803905437fc971978e069de..01b9c86e04cf47f97c0e14e605a3634045c44526 100644 (file)
@@ -224,3 +224,8 @@ class StasyGeoTableModule(UIModule):
                        countries.append(country)
 
                return self.render_string("modules/stasy-table-geo.html", countries=countries)
+
+
+class MirrorsTableModule(UIModule):
+       def render(self, mirrors):
+               return self.render_string("modules/mirrors-table.html", mirrors=mirrors)