summaryrefslogtreecommitdiff
path: root/mirrors
diff options
context:
space:
mode:
Diffstat (limited to 'mirrors')
-rw-r--r--mirrors/admin.py80
-rw-r--r--mirrors/fields.py45
-rw-r--r--mirrors/fixtures/mirrorprotocols.json29
-rw-r--r--mirrors/management/commands/mirrorcheck.py286
-rw-r--r--mirrors/management/commands/mirrorresolv.py65
-rw-r--r--mirrors/migrations/0001_initial.py130
-rw-r--r--mirrors/migrations/0002_mirrorurl_bandwidth.py20
-rw-r--r--mirrors/migrations/0002_rename_model_tables.py61
-rw-r--r--mirrors/migrations/0003_auto__add_mirrorlog.py72
-rw-r--r--mirrors/models.py165
-rw-r--r--mirrors/static/mirror_status.js193
-rw-r--r--mirrors/templatetags/jinja2.py53
-rw-r--r--mirrors/templatetags/mirror_status.py12
-rw-r--r--mirrors/urls.py18
-rw-r--r--mirrors/urls_mirrorlist.py11
-rw-r--r--mirrors/utils.py247
-rw-r--r--mirrors/views.py113
-rw-r--r--mirrors/views/__init__.py149
-rw-r--r--mirrors/views/api.py108
-rw-r--r--mirrors/views/mirrorlist.py129
20 files changed, 1525 insertions, 461 deletions
diff --git a/mirrors/admin.py b/mirrors/admin.py
index 394b3508..16a97ea2 100644
--- a/mirrors/admin.py
+++ b/mirrors/admin.py
@@ -1,61 +1,99 @@
-import re
+from datetime import datetime
+from urlparse import urlparse, urlunsplit
from django import forms
from django.contrib import admin
-from .models import Mirror, MirrorProtocol, MirrorUrl, MirrorRsync
+from .models import (Mirror, MirrorProtocol, MirrorUrl, MirrorRsync,
+ CheckLocation)
+
class MirrorUrlForm(forms.ModelForm):
class Meta:
model = MirrorUrl
+ fields = ('url', 'country', 'bandwidth', 'active')
+
def clean_url(self):
+ # is this a valid-looking URL?
+ url_parts = urlparse(self.cleaned_data["url"])
+ if not url_parts.scheme:
+ raise forms.ValidationError("No URL scheme (protocol) provided.")
+ if not url_parts.netloc:
+ raise forms.ValidationError("No URL host provided.")
+ if url_parts.params or url_parts.query or url_parts.fragment:
+ raise forms.ValidationError(
+ "URL parameters, query, and fragment elements are not supported.")
# ensure we always save the URL with a trailing slash
- url = self.cleaned_data["url"].strip()
- if url[-1] == '/':
- return url
- return url + '/'
+ path = url_parts.path
+ if not path.endswith('/'):
+ path += '/'
+ url = urlunsplit((url_parts.scheme, url_parts.netloc, path, '', ''))
+ return url
+
class MirrorUrlInlineAdmin(admin.TabularInline):
model = MirrorUrl
form = MirrorUrlForm
+ readonly_fields = ('protocol', 'has_ipv4', 'has_ipv6')
extra = 3
-# ripped off from django.forms.fields, adding netmask ability
-ipv4nm_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}(/(\d|[1-2]\d|3[0-2])){0,1}$')
-class IPAddressNetmaskField(forms.fields.RegexField):
- default_error_messages = {
- 'invalid': u'Enter a valid IPv4 address, possibly including netmask.',
- }
-
- def __init__(self, *args, **kwargs):
- super(IPAddressNetmaskField, self).__init__(ipv4nm_re, *args, **kwargs)
class MirrorRsyncForm(forms.ModelForm):
class Meta:
model = MirrorRsync
- ip = IPAddressNetmaskField(label='IP')
+ fields = ('ip',)
+
class MirrorRsyncInlineAdmin(admin.TabularInline):
model = MirrorRsync
form = MirrorRsyncForm
extra = 2
+
class MirrorAdminForm(forms.ModelForm):
class Meta:
model = Mirror
- upstream = forms.ModelChoiceField(queryset=Mirror.objects.filter(tier__gte=0, tier__lte=1), required=False)
+ fields = ('name', 'tier', 'upstream', 'admin_email', 'alternate_email',
+ 'public', 'active', 'isos', 'rsync_user', 'rsync_password',
+ 'bug', 'notes')
+
+ upstream = forms.ModelChoiceField(
+ queryset=Mirror.objects.filter(tier__gte=0, tier__lte=1),
+ required=False)
+
class MirrorAdmin(admin.ModelAdmin):
form = MirrorAdminForm
- list_display = ('name', 'tier', 'country', 'active', 'public', 'isos', 'admin_email', 'supported_protocols')
- list_filter = ('tier', 'country', 'active', 'public')
- search_fields = ('name',)
+ list_display = ('name', 'tier', 'active', 'public',
+ 'isos', 'admin_email', 'alternate_email')
+ list_filter = ('tier', 'active', 'public')
+ search_fields = ('name', 'admin_email', 'alternate_email')
+ readonly_fields = ('created', 'last_modified')
inlines = [
MirrorUrlInlineAdmin,
MirrorRsyncInlineAdmin,
]
+ def save_model(self, request, obj, form, change):
+ if '~~~' in obj.notes:
+ date = datetime.utcnow().strftime('%Y-%m-%d')
+ usertext = request.user.get_full_name()
+ obj.notes = obj.notes.replace('~~~', '%s (%s)' % (date, usertext))
+ obj.save()
+
+
+class MirrorProtocolAdmin(admin.ModelAdmin):
+ list_display = ('protocol', 'is_download', 'default')
+ list_filter = ('is_download', 'default')
+
+
+class CheckLocationAdmin(admin.ModelAdmin):
+ list_display = ('hostname', 'source_ip', 'country', 'created')
+ search_fields = ('hostname', 'source_ip')
+
+
admin.site.register(Mirror, MirrorAdmin)
-admin.site.register(MirrorProtocol)
+admin.site.register(MirrorProtocol, MirrorProtocolAdmin)
+admin.site.register(CheckLocation, CheckLocationAdmin)
# vim: set ts=4 sw=4 et:
diff --git a/mirrors/fields.py b/mirrors/fields.py
new file mode 100644
index 00000000..e8963edf
--- /dev/null
+++ b/mirrors/fields.py
@@ -0,0 +1,45 @@
+from IPy import IP
+
+from django import forms
+from django.core import validators
+from django.core.exceptions import ValidationError
+from django.db import models
+
+
+class IPNetworkFormField(forms.Field):
+ def to_python(self, value):
+ if value in validators.EMPTY_VALUES:
+ return None
+ try:
+ value = IP(value)
+ except ValueError as e:
+ raise ValidationError(str(e))
+ return value
+
+
+class IPNetworkField(models.Field):
+ __metaclass__ = models.SubfieldBase
+ description = "IPv4 or IPv6 address or subnet"
+
+ def __init__(self, *args, **kwargs):
+ kwargs['max_length'] = 44
+ super(IPNetworkField, self).__init__(*args, **kwargs)
+
+ def get_internal_type(self):
+ return "IPAddressField"
+
+ def to_python(self, value):
+ if not value:
+ return None
+ return IP(value)
+
+ def get_prep_value(self, value):
+ value = self.to_python(value)
+ if not value:
+ return None
+ return str(value)
+
+ def formfield(self, **kwargs):
+ defaults = {'form_class': IPNetworkFormField}
+ defaults.update(kwargs)
+ return super(IPNetworkField, self).formfield(**defaults)
diff --git a/mirrors/fixtures/mirrorprotocols.json b/mirrors/fixtures/mirrorprotocols.json
new file mode 100644
index 00000000..1a07510b
--- /dev/null
+++ b/mirrors/fixtures/mirrorprotocols.json
@@ -0,0 +1,29 @@
+[
+ {
+ "pk": 1,
+ "model": "mirrors.mirrorprotocol",
+ "fields": {
+ "is_download": true,
+ "default": true,
+ "protocol": "http"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "mirrors.mirrorprotocol",
+ "fields": {
+ "is_download": false,
+ "default": false,
+ "protocol": "rsync"
+ }
+ },
+ {
+ "pk": 5,
+ "model": "mirrors.mirrorprotocol",
+ "fields": {
+ "is_download": true,
+ "default": false,
+ "protocol": "https"
+ }
+ }
+]
diff --git a/mirrors/management/commands/mirrorcheck.py b/mirrors/management/commands/mirrorcheck.py
index 1662b15c..1a33073a 100644
--- a/mirrors/management/commands/mirrorcheck.py
+++ b/mirrors/management/commands/mirrorcheck.py
@@ -9,145 +9,255 @@ we encounter errors, record those as well.
Usage: ./manage.py mirrorcheck
"""
-from django.core.management.base import NoArgsCommand
-from django.db.models import Q
-
-from datetime import datetime, timedelta
+from collections import deque
+from datetime import datetime
+from httplib import HTTPException
import logging
+import os
+from optparse import make_option
+from pytz import utc
import re
import socket
+import ssl
+import subprocess
import sys
import time
-import thread
+import tempfile
from threading import Thread
+import types
from Queue import Queue, Empty
import urllib2
-from logging import ERROR, WARNING, INFO, DEBUG
+from django.core.management.base import NoArgsCommand
+from django.db import transaction
+from django.utils.timezone import now
+
+from mirrors.models import MirrorUrl, MirrorLog, CheckLocation
-from mirrors.models import Mirror, MirrorUrl, MirrorLog
logging.basicConfig(
- level=WARNING,
+ level=logging.WARNING,
format='%(asctime)s -> %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
stream=sys.stderr)
logger = logging.getLogger()
+
class Command(NoArgsCommand):
+ option_list = NoArgsCommand.option_list + (
+ make_option('-t', '--timeout', dest='timeout', type='float', default=10.0,
+ help='Timeout value for connecting to URL'),
+ make_option('-l', '--location', dest='location', type='int',
+ help='ID of CheckLocation object to use for this run'),
+ )
help = "Runs a check on all known mirror URLs to determine their up-to-date status."
def handle_noargs(self, **options):
v = int(options.get('verbosity', 0))
if v == 0:
- logger.level = ERROR
+ logger.level = logging.ERROR
elif v == 1:
- logger.level = WARNING
- elif v == 2:
- logger.level = DEBUG
-
- import signal, traceback
- handler = lambda sig, stack: traceback.print_stack(stack)
- signal.signal(signal.SIGQUIT, handler)
- signal.signal(signal.SIGUSR1, handler)
-
- return check_current_mirrors()
-
-def parse_rfc3339_datetime(time):
- # '2010-09-02 11:05:06+02:00'
- m = re.match('^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})([-+])(\d{2}):(\d{2})', time)
- if m:
- vals = m.groups()
- parsed = datetime(int(vals[0]), int(vals[1]), int(vals[2]),
- int(vals[3]), int(vals[4]), int(vals[5]))
- # now account for time zone offset
- sign = vals[6]
- offset = timedelta(hours=int(sign + vals[7]),
- minutes=int(sign + vals[8]))
- # subtract the offset, e.g. '-04:00' should be moved up 4 hours
- return parsed - offset
- return None
-
-def check_mirror_url(mirror_url):
+ logger.level = logging.WARNING
+ elif v >= 2:
+ logger.level = logging.DEBUG
+
+ timeout = options.get('timeout')
+
+ urls = MirrorUrl.objects.select_related('protocol').filter(
+ active=True, mirror__active=True, mirror__public=True)
+
+ location = options.get('location', None)
+ if location:
+ location = CheckLocation.objects.get(id=location)
+ family = location.family
+ monkeypatch_getaddrinfo(family)
+ if family == socket.AF_INET6:
+ urls = urls.filter(has_ipv6=True)
+ elif family == socket.AF_INET:
+ urls = urls.filter(has_ipv4=True)
+
+ pool = MirrorCheckPool(urls, location, timeout)
+ pool.run()
+ return 0
+
+
+def monkeypatch_getaddrinfo(force_family=socket.AF_INET):
+ '''Force the Python socket module to connect over the designated family;
+ e.g. socket.AF_INET or socket.AF_INET6.'''
+ orig = socket.getaddrinfo
+
+ def wrapper(host, port, family=0, socktype=0, proto=0, flags=0):
+ return orig(host, port, force_family, socktype, proto, flags)
+
+ socket.getaddrinfo = wrapper
+
+
+def parse_lastsync(log, data):
+ '''lastsync file should be an epoch value created by us.'''
+ try:
+ parsed_time = datetime.utcfromtimestamp(int(data))
+ log.last_sync = parsed_time.replace(tzinfo=utc)
+ except (TypeError, ValueError):
+ # it is bad news to try logging the lastsync value;
+ # sometimes we get a crazy-encoded web page.
+ # if we couldn't parse a time, this is a failure.
+ log.last_sync = None
+ log.error = "Could not parse time from lastsync"
+ log.is_success = False
+
+
+def check_mirror_url(mirror_url, location, timeout):
url = mirror_url.url + 'lastsync'
- logger.info("checking URL %s" % url)
- log = MirrorLog(url=mirror_url, check_time=datetime.utcnow())
+ logger.info("checking URL %s", url)
+ log = MirrorLog(url=mirror_url, check_time=now(), location=location)
+ headers = {'User-Agent': 'archweb/1.0'}
+ req = urllib2.Request(url, None, headers)
+ start = time.time()
try:
- start = time.time()
- result = urllib2.urlopen(url, timeout=10)
+ result = urllib2.urlopen(req, timeout=timeout)
data = result.read()
result.close()
end = time.time()
- # lastsync should be an epoch value, but some mirrors
- # are creating their own in RFC-3339 format:
- # '2010-09-02 11:05:06+02:00'
- try:
- parsed_time = datetime.utcfromtimestamp(int(data))
- except ValueError:
- # it is bad news to try logging the lastsync value;
- # sometimes we get a crazy-encoded web page.
- logger.info("attempting to parse generated lastsync file"
- " from mirror %s" % url)
- parsed_time = parse_rfc3339_datetime(data)
-
- log.last_sync = parsed_time
+ parse_lastsync(log, data)
log.duration = end - start
- logger.debug("success: %s, %.2f" % (url, log.duration))
- except urllib2.HTTPError, e:
+ logger.debug("success: %s, %.2f", url, log.duration)
+ except urllib2.HTTPError as e:
+ if e.code == 404:
+ # we have a duration, just not a success
+ end = time.time()
+ log.duration = end - start
+ log.is_success = False
+ log.error = str(e)
+ logger.debug("failed: %s, %s", url, log.error)
+ except urllib2.URLError as e:
log.is_success = False
- log.error =str(e)
- logger.debug("failed: %s, %s" % (url, log.error))
- except urllib2.URLError, e:
- log.is_success=False
log.error = e.reason
+ if isinstance(e.reason, types.StringTypes) and \
+ re.search(r'550.*No such file', e.reason):
+ # similar to 404 case above, still record duration
+ end = time.time()
+ log.duration = end - start
if isinstance(e.reason, socket.timeout):
log.error = "Connection timed out."
elif isinstance(e.reason, socket.error):
- log.error = e.reason.args[1]
- logger.debug("failed: %s, %s" % (url, log.error))
+ log.error = e.reason.args[-1]
+ logger.debug("failed: %s, %s", url, log.error)
+ except HTTPException:
+ # e.g., BadStatusLine
+ log.is_success = False
+ log.error = "Exception in processing HTTP request."
+ logger.debug("failed: %s, %s", url, log.error)
+ except ssl.CertificateError as e:
+ log.is_success = False
+ log.error = str(e)
+ logger.debug("failed: %s, %s", url, log.error)
+ except socket.timeout:
+ log.is_success = False
+ log.error = "Connection timed out."
+ logger.debug("failed: %s, %s", url, log.error)
+ except socket.error as e:
+ log.is_success = False
+ log.error = str(e)
+ logger.debug("failed: %s, %s", url, log.error)
- log.save()
return log
-def mirror_url_worker(queue):
+
+def check_rsync_url(mirror_url, location, timeout):
+ url = mirror_url.url + 'lastsync'
+ logger.info("checking URL %s", url)
+ log = MirrorLog(url=mirror_url, check_time=now(), location=location)
+
+ tempdir = tempfile.mkdtemp()
+ ipopt = ''
+ if location:
+ if location.family == socket.AF_INET6:
+ ipopt = '--ipv6'
+ elif location.family == socket.AF_INET:
+ ipopt = '--ipv4'
+ lastsync_path = os.path.join(tempdir, 'lastsync')
+ rsync_cmd = ["rsync", "--quiet", "--contimeout=%d" % timeout,
+ "--timeout=%d" % timeout]
+ if ipopt:
+ rsync_cmd.append(ipopt)
+ rsync_cmd.append(url)
+ rsync_cmd.append(lastsync_path)
+ try:
+ with open(os.devnull, 'w') as devnull:
+ if logger.isEnabledFor(logging.DEBUG):
+ logger.debug("rsync cmd: %s", ' '.join(rsync_cmd))
+ start = time.time()
+ proc = subprocess.Popen(rsync_cmd, stdout=devnull,
+ stderr=subprocess.PIPE)
+ _, errdata = proc.communicate()
+ end = time.time()
+ log.duration = end - start
+ if proc.returncode != 0:
+ logger.debug("error: %s, %s", url, errdata)
+ log.is_success = False
+ log.error = errdata.strip()
+ # look at rsync error code- if we had a command error or timed out,
+ # don't record a duration as it is misleading
+ if proc.returncode in (1, 30, 35):
+ log.duration = None
+ else:
+ logger.debug("success: %s, %.2f", url, log.duration)
+ if os.path.exists(lastsync_path):
+ with open(lastsync_path, 'r') as lastsync:
+ parse_lastsync(log, lastsync.read())
+ else:
+ parse_lastsync(log, None)
+ finally:
+ if os.path.exists(lastsync_path):
+ os.unlink(lastsync_path)
+ os.rmdir(tempdir)
+
+ return log
+
+
+def mirror_url_worker(work, output, location, timeout):
while True:
try:
- item = queue.get(block=False)
- check_mirror_url(item)
- queue.task_done()
+ url = work.get(block=False)
+ try:
+ if url.protocol.protocol == 'rsync':
+ log = check_rsync_url(url, location, timeout)
+ elif (url.protocol.protocol == 'ftp' and location and
+ location.family == socket.AF_INET6):
+ # IPv6 + FTP don't work; skip checking completely
+ log = None
+ else:
+ log = check_mirror_url(url, location, timeout)
+ if log:
+ output.append(log)
+ finally:
+ work.task_done()
except Empty:
return 0
+
class MirrorCheckPool(object):
- def __init__(self, work, num_threads=10):
+ def __init__(self, urls, location, timeout=10, num_threads=10):
self.tasks = Queue()
- for i in work:
- self.tasks.put(i)
+ self.logs = deque()
+ for url in list(urls):
+ self.tasks.put(url)
self.threads = []
- for i in range(num_threads):
- thread = Thread(target=mirror_url_worker, args=(self.tasks,))
+ for _ in range(num_threads):
+ thread = Thread(target=mirror_url_worker,
+ args=(self.tasks, self.logs, location, timeout))
thread.daemon = True
self.threads.append(thread)
- def run_and_join(self):
+ @transaction.atomic
+ def run(self):
logger.debug("starting threads")
- for t in self.threads:
- t.start()
+ for thread in self.threads:
+ thread.start()
logger.debug("joining on all threads")
self.tasks.join()
-
-def check_current_mirrors():
- urls = MirrorUrl.objects.filter(
- Q(protocol__protocol__iexact='HTTP') |
- Q(protocol__protocol__iexact='FTP'),
- mirror__active=True, mirror__public=True)
-
- pool = MirrorCheckPool(urls)
- pool.run_and_join()
- return 0
-
-# For lack of a better place to put it, here is a query to get latest check
-# result joined with mirror details:
-# SELECT mu.*, m.*, ml.* FROM mirrors_mirrorurl mu JOIN mirrors_mirror m ON mu.mirror_id = m.id JOIN mirrors_mirrorlog ml ON mu.id = ml.url_id LEFT JOIN mirrors_mirrorlog ml2 ON ml.url_id = ml2.url_id AND ml.id < ml2.id WHERE ml2.id IS NULL AND m.active = 1 AND m.public = 1;
+ logger.debug("processing %d log entries", len(self.logs))
+ MirrorLog.objects.bulk_create(self.logs)
+ logger.debug("log entries saved")
# vim: set ts=4 sw=4 et:
diff --git a/mirrors/management/commands/mirrorresolv.py b/mirrors/management/commands/mirrorresolv.py
new file mode 100644
index 00000000..0e71894b
--- /dev/null
+++ b/mirrors/management/commands/mirrorresolv.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+"""
+mirrorresolv command
+
+Poll all mirror URLs and determine whether they have IPv4 and/or IPv6 addresses
+available.
+
+Usage: ./manage.py mirrorresolv
+"""
+
+from django.core.management.base import NoArgsCommand
+
+import sys
+import logging
+import socket
+
+from mirrors.models import MirrorUrl
+
+logging.basicConfig(
+ level=logging.WARNING,
+ format='%(asctime)s -> %(levelname)s: %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S',
+ stream=sys.stderr)
+logger = logging.getLogger()
+
+class Command(NoArgsCommand):
+ help = "Runs a check on all active mirror URLs to determine if they are reachable via IPv4 and/or v6."
+
+ def handle_noargs(self, **options):
+ v = int(options.get('verbosity', 0))
+ if v == 0:
+ logger.level = logging.ERROR
+ elif v == 1:
+ logger.level = logging.WARNING
+ elif v >= 2:
+ logger.level = logging.DEBUG
+
+ return resolve_mirrors()
+
+def resolve_mirrors():
+ logger.debug("requesting list of mirror URLs")
+ for mirrorurl in MirrorUrl.objects.filter(active=True, mirror__active=True):
+ try:
+ # save old values, we can skip no-op updates this way
+ oldvals = (mirrorurl.has_ipv4, mirrorurl.has_ipv6)
+ logger.debug("resolving %3i (%s)", mirrorurl.id, mirrorurl.hostname)
+ families = mirrorurl.address_families()
+ mirrorurl.has_ipv4 = socket.AF_INET in families
+ mirrorurl.has_ipv6 = socket.AF_INET6 in families
+ logger.debug("%s: v4: %s v6: %s", mirrorurl.hostname,
+ mirrorurl.has_ipv4, mirrorurl.has_ipv6)
+ # now check new values, only update if new != old
+ newvals = (mirrorurl.has_ipv4, mirrorurl.has_ipv6)
+ if newvals != oldvals:
+ logger.debug("values changed for %s", mirrorurl)
+ mirrorurl.save(update_fields=('has_ipv4', 'has_ipv6'))
+ except socket.gaierror, e:
+ if e.errno == socket.EAI_NONAME:
+ logger.debug("gaierror resolving %s: %s", mirrorurl.hostname, e)
+ else:
+ logger.warn("gaierror resolving %s: %s", mirrorurl.hostname, e)
+ except socket.error, e:
+ logger.warn("error resolving %s: %s", mirrorurl.hostname, e)
+
+# vim: set ts=4 sw=4 et:
diff --git a/mirrors/migrations/0001_initial.py b/mirrors/migrations/0001_initial.py
index 4a3173c5..6f36c9eb 100644
--- a/mirrors/migrations/0001_initial.py
+++ b/mirrors/migrations/0001_initial.py
@@ -1,17 +1,123 @@
-# encoding: utf-8
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
-class Migration(SchemaMigration):
+from django.db import models, migrations
+import django_countries.fields
+import django.db.models.deletion
+import mirrors.fields
- def forwards(self, orm):
- pass
- def backwards(self, orm):
- pass
+class Migration(migrations.Migration):
- models = {}
+ dependencies = [
+ ]
- complete_apps = ['mirrors']
+ operations = [
+ migrations.CreateModel(
+ name='CheckLocation',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('hostname', models.CharField(max_length=255)),
+ ('source_ip', models.GenericIPAddressField(unique=True, verbose_name=b'source IP', unpack_ipv4=True)),
+ ('country', django_countries.fields.CountryField(max_length=2)),
+ ('created', models.DateTimeField(editable=False)),
+ ],
+ options={
+ 'ordering': ('hostname', 'source_ip'),
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Mirror',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(unique=True, max_length=255)),
+ ('tier', models.SmallIntegerField(default=2, choices=[(0, b'Tier 0'), (1, b'Tier 1'), (2, b'Tier 2'), (-1, b'Untiered')])),
+ ('admin_email', models.EmailField(max_length=255, blank=True)),
+ ('alternate_email', models.EmailField(max_length=255, blank=True)),
+ ('public', models.BooleanField(default=True)),
+ ('active', models.BooleanField(default=True)),
+ ('isos', models.BooleanField(default=True, verbose_name=b'ISOs')),
+ ('rsync_user', models.CharField(default=b'', max_length=50, blank=True)),
+ ('rsync_password', models.CharField(default=b'', max_length=50, blank=True)),
+ ('bug', models.PositiveIntegerField(null=True, verbose_name=b'Flyspray bug', blank=True)),
+ ('notes', models.TextField(blank=True)),
+ ('created', models.DateTimeField(editable=False)),
+ ('last_modified', models.DateTimeField(editable=False)),
+ ('upstream', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, to='mirrors.Mirror', null=True)),
+ ],
+ options={
+ 'ordering': ('name',),
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='MirrorLog',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('check_time', models.DateTimeField(db_index=True)),
+ ('last_sync', models.DateTimeField(null=True)),
+ ('duration', models.FloatField(null=True)),
+ ('is_success', models.BooleanField(default=True)),
+ ('error', models.TextField(default=b'', blank=True)),
+ ('location', models.ForeignKey(related_name=b'logs', to='mirrors.CheckLocation', null=True)),
+ ],
+ options={
+ 'get_latest_by': 'check_time',
+ 'verbose_name': 'mirror check log',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='MirrorProtocol',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('protocol', models.CharField(unique=True, max_length=10)),
+ ('is_download', models.BooleanField(default=True, help_text=b'Is protocol useful for end-users, e.g. HTTP')),
+ ('default', models.BooleanField(default=True, help_text=b'Included by default when building mirror list?')),
+ ('created', models.DateTimeField(editable=False)),
+ ],
+ options={
+ 'ordering': ('protocol',),
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='MirrorRsync',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('ip', mirrors.fields.IPNetworkField(max_length=44, verbose_name=b'IP')),
+ ('created', models.DateTimeField(editable=False)),
+ ('mirror', models.ForeignKey(related_name=b'rsync_ips', to='mirrors.Mirror')),
+ ],
+ options={
+ 'ordering': ('ip',),
+ 'verbose_name': 'mirror rsync IP',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='MirrorUrl',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('url', models.CharField(unique=True, max_length=255, verbose_name=b'URL')),
+ ('country', django_countries.fields.CountryField(blank=True, max_length=2, db_index=True)),
+ ('has_ipv4', models.BooleanField(default=True, verbose_name=b'IPv4 capable', editable=False)),
+ ('has_ipv6', models.BooleanField(default=False, verbose_name=b'IPv6 capable', editable=False)),
+ ('created', models.DateTimeField(editable=False)),
+ ('active', models.BooleanField(default=True)),
+ ('mirror', models.ForeignKey(related_name=b'urls', to='mirrors.Mirror')),
+ ('protocol', models.ForeignKey(related_name=b'urls', on_delete=django.db.models.deletion.PROTECT, editable=False, to='mirrors.MirrorProtocol')),
+ ],
+ options={
+ 'verbose_name': 'mirror URL',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.AddField(
+ model_name='mirrorlog',
+ name='url',
+ field=models.ForeignKey(related_name=b'logs', to='mirrors.MirrorUrl'),
+ preserve_default=True,
+ ),
+ ]
diff --git a/mirrors/migrations/0002_mirrorurl_bandwidth.py b/mirrors/migrations/0002_mirrorurl_bandwidth.py
new file mode 100644
index 00000000..f0118199
--- /dev/null
+++ b/mirrors/migrations/0002_mirrorurl_bandwidth.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('mirrors', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='mirrorurl',
+ name='bandwidth',
+ field=models.FloatField(null=True, verbose_name=b'bandwidth (mbits)', blank=True),
+ preserve_default=True,
+ ),
+ ]
diff --git a/mirrors/migrations/0002_rename_model_tables.py b/mirrors/migrations/0002_rename_model_tables.py
deleted file mode 100644
index d510bada..00000000
--- a/mirrors/migrations/0002_rename_model_tables.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# encoding: utf-8
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-class Migration(SchemaMigration):
-
- depends_on = (
- ('main', '0014_mirror_notes_rsync_optional'),
- )
-
- def forwards(self, orm):
- db.rename_table('main_mirror', 'mirrors_mirror')
- db.rename_table('main_mirrorurl', 'mirrors_mirrorurl')
- db.rename_table('main_mirrorrsync', 'mirrors_mirrorrsync')
- db.rename_table('main_mirrorprotocol', 'mirrors_mirrorprotocol')
-
- def backwards(self, orm):
- db.rename_table('mirrors_mirror', 'main_mirror')
- db.rename_table('mirrors_mirrorurl', 'main_mirrorurl')
- db.rename_table('mirrors_mirrorrsync', 'main_mirrorrsync')
- db.rename_table('mirrors_mirrorprotocol', 'main_mirrorprotocol')
-
- models = {
- 'mirrors.mirror': {
- 'Meta': {'ordering': "('country', 'name')", 'object_name': 'Mirror'},
- 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'admin_email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}),
- 'country': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'isos': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
- 'notes': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
- 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'rsync_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
- 'rsync_user': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
- 'tier': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
- 'upstream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['mirrors.Mirror']", 'null': 'True'})
- },
- 'mirrors.mirrorprotocol': {
- 'Meta': {'object_name': 'MirrorProtocol'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'protocol': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '10'})
- },
- 'mirrors.mirrorrsync': {
- 'Meta': {'object_name': 'MirrorRsync'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'ip': ('django.db.models.fields.CharField', [], {'max_length': '24'}),
- 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'rsync_ips'", 'to': "orm['mirrors.Mirror']"})
- },
- 'mirrors.mirrorurl': {
- 'Meta': {'object_name': 'MirrorUrl'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'to': "orm['mirrors.Mirror']"}),
- 'protocol': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'to': "orm['mirrors.MirrorProtocol']"}),
- 'url': ('django.db.models.fields.CharField', [], {'max_length': '255'})
- }
- }
-
- complete_apps = ['mirrors']
diff --git a/mirrors/migrations/0003_auto__add_mirrorlog.py b/mirrors/migrations/0003_auto__add_mirrorlog.py
deleted file mode 100644
index 5b4c225b..00000000
--- a/mirrors/migrations/0003_auto__add_mirrorlog.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# encoding: utf-8
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-class Migration(SchemaMigration):
-
- def forwards(self, orm):
- # Adding model 'MirrorLog'
- db.create_table('mirrors_mirrorlog', (
- ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
- ('url', self.gf('django.db.models.fields.related.ForeignKey')(related_name='logs', to=orm['mirrors.MirrorUrl'])),
- ('check_time', self.gf('django.db.models.fields.DateTimeField')(db_index=True)),
- ('last_sync', self.gf('django.db.models.fields.DateTimeField')(null=True)),
- ('duration', self.gf('django.db.models.fields.FloatField')(null=True)),
- ('is_success', self.gf('django.db.models.fields.BooleanField')(default=True)),
- ('error', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True)),
- ))
- db.send_create_signal('mirrors', ['MirrorLog'])
-
- def backwards(self, orm):
- # Deleting model 'MirrorLog'
- db.delete_table('mirrors_mirrorlog')
-
- models = {
- 'mirrors.mirror': {
- 'Meta': {'ordering': "('country', 'name')", 'object_name': 'Mirror'},
- 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'admin_email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}),
- 'country': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'isos': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
- 'notes': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
- 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'rsync_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
- 'rsync_user': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
- 'tier': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
- 'upstream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['mirrors.Mirror']", 'null': 'True'})
- },
- 'mirrors.mirrorlog': {
- 'Meta': {'object_name': 'MirrorLog'},
- 'check_time': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
- 'duration': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
- 'error': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'is_success': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
- 'url': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['mirrors.MirrorUrl']"})
- },
- 'mirrors.mirrorprotocol': {
- 'Meta': {'object_name': 'MirrorProtocol'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'protocol': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '10'})
- },
- 'mirrors.mirrorrsync': {
- 'Meta': {'object_name': 'MirrorRsync'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'ip': ('django.db.models.fields.CharField', [], {'max_length': '24'}),
- 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'rsync_ips'", 'to': "orm['mirrors.Mirror']"})
- },
- 'mirrors.mirrorurl': {
- 'Meta': {'object_name': 'MirrorUrl'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'to': "orm['mirrors.Mirror']"}),
- 'protocol': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'to': "orm['mirrors.MirrorProtocol']"}),
- 'url': ('django.db.models.fields.CharField', [], {'max_length': '255'})
- }
- }
-
- complete_apps = ['mirrors']
diff --git a/mirrors/models.py b/mirrors/models.py
index 85423303..9743d177 100644
--- a/mirrors/models.py
+++ b/mirrors/models.py
@@ -1,78 +1,189 @@
+from datetime import timedelta
+import socket
+from urlparse import urlparse
+
+from django.core.exceptions import ValidationError
from django.db import models
+from django.db.models.signals import pre_save
+from django_countries.fields import CountryField
+
+from .fields import IPNetworkField
+from main.utils import set_created_field
-TIER_CHOICES = (
- (0, 'Tier 0'),
- (1, 'Tier 1'),
- (2, 'Tier 2'),
- (-1, 'Untiered'),
-)
class Mirror(models.Model):
- name = models.CharField(max_length=255)
+ TIER_CHOICES = (
+ (0, 'Tier 0'),
+ (1, 'Tier 1'),
+ (2, 'Tier 2'),
+ (-1, 'Untiered'),
+ )
+
+ name = models.CharField(max_length=255, unique=True)
tier = models.SmallIntegerField(default=2, choices=TIER_CHOICES)
- upstream = models.ForeignKey('self', null=True)
- country = models.CharField(max_length=255, db_index=True)
+ upstream = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
admin_email = models.EmailField(max_length=255, blank=True)
+ alternate_email = models.EmailField(max_length=255, blank=True)
public = models.BooleanField(default=True)
active = models.BooleanField(default=True)
- isos = models.BooleanField(default=True)
+ isos = models.BooleanField("ISOs", default=True)
rsync_user = models.CharField(max_length=50, blank=True, default='')
rsync_password = models.CharField(max_length=50, blank=True, default='')
+ bug = models.PositiveIntegerField("Flyspray bug", null=True, blank=True)
notes = models.TextField(blank=True)
+ created = models.DateTimeField(editable=False)
+ last_modified = models.DateTimeField(editable=False)
class Meta:
- ordering = ('country', 'name')
+ ordering = ('name',)
def __unicode__(self):
return self.name
- def supported_protocols(self):
- protocols = MirrorProtocol.objects.filter(
- urls__mirror=self).order_by('protocol').distinct()
- return ", ".join([p.protocol for p in protocols])
-
def downstream(self):
return Mirror.objects.filter(upstream=self).order_by('name')
def get_absolute_url(self):
return '/mirrors/%s/' % self.name
+
class MirrorProtocol(models.Model):
protocol = models.CharField(max_length=10, unique=True)
+ is_download = models.BooleanField(default=True,
+ help_text="Is protocol useful for end-users, e.g. HTTP")
+ default = models.BooleanField(default=True,
+ help_text="Included by default when building mirror list?")
+ created = models.DateTimeField(editable=False)
+
def __unicode__(self):
return self.protocol
+
class Meta:
- verbose_name = 'Mirror Protocol'
+ ordering = ('protocol',)
+
class MirrorUrl(models.Model):
- url = models.CharField(max_length=255)
- protocol = models.ForeignKey(MirrorProtocol, related_name="urls")
+ url = models.CharField("URL", max_length=255, unique=True)
+ protocol = models.ForeignKey(MirrorProtocol, related_name="urls",
+ editable=False, on_delete=models.PROTECT)
mirror = models.ForeignKey(Mirror, related_name="urls")
+ country = CountryField(blank=True, db_index=True)
+ has_ipv4 = models.BooleanField("IPv4 capable", default=True,
+ editable=False)
+ has_ipv6 = models.BooleanField("IPv6 capable", default=False,
+ editable=False)
+ active = models.BooleanField(default=True)
+ bandwidth = models.FloatField("bandwidth (mbits)", null=True, blank=True)
+ created = models.DateTimeField(editable=False)
+
+ class Meta:
+ verbose_name = 'mirror URL'
+
def __unicode__(self):
return self.url
- class Meta:
- verbose_name = 'Mirror URL'
+
+ def address_families(self):
+ hostname = urlparse(self.url).hostname
+ info = socket.getaddrinfo(hostname, None, 0, socket.SOCK_STREAM)
+ families = [x[0] for x in info]
+ return families
+
+ @property
+ def hostname(self):
+ return urlparse(self.url).hostname
+
+ def clean(self):
+ try:
+ # Auto-map the protocol field by looking at the URL
+ protocol = urlparse(self.url).scheme
+ self.protocol = MirrorProtocol.objects.get(protocol=protocol)
+ except Exception as e:
+ raise ValidationError(e)
+ try:
+ families = self.address_families()
+ self.has_ipv4 = socket.AF_INET in families
+ self.has_ipv6 = socket.AF_INET6 in families
+ except socket.error:
+ # We don't fail in this case; we'll just set both to False
+ self.has_ipv4 = False
+ self.has_ipv6 = False
+
+ def get_absolute_url(self):
+ return '/mirrors/%s/%d/' % (self.mirror.name, self.pk)
+
class MirrorRsync(models.Model):
- ip = models.CharField(max_length=24)
+ # max length is 40 chars for full-form IPv6 addr + subnet
+ ip = IPNetworkField("IP")
mirror = models.ForeignKey(Mirror, related_name="rsync_ips")
+ created = models.DateTimeField(editable=False)
+
def __unicode__(self):
- return "%s" % (self.ip)
+ return unicode(self.ip)
+
+ class Meta:
+ verbose_name = 'mirror rsync IP'
+ ordering = ('ip',)
+
+
+class CheckLocation(models.Model):
+ hostname = models.CharField(max_length=255)
+ source_ip = models.GenericIPAddressField('source IP',
+ unpack_ipv4=True, unique=True)
+ country = CountryField()
+ created = models.DateTimeField(editable=False)
+
class Meta:
- verbose_name = 'Mirror Rsync IP'
+ ordering = ('hostname', 'source_ip')
+
+ def __unicode__(self):
+ return self.hostname
+
+ @property
+ def family(self):
+ info = socket.getaddrinfo(self.source_ip, None, 0, 0, 0,
+ socket.AI_NUMERICHOST)
+ families = [x[0] for x in info]
+ return families[0]
+
+ @property
+ def ip_version(self):
+ '''Returns integer '4' or '6'.'''
+ if self.family == socket.AF_INET6:
+ return 6
+ if self.family == socket.AF_INET:
+ return 4
+ return None
+
class MirrorLog(models.Model):
url = models.ForeignKey(MirrorUrl, related_name="logs")
+ location = models.ForeignKey(CheckLocation, related_name="logs", null=True)
check_time = models.DateTimeField(db_index=True)
last_sync = models.DateTimeField(null=True)
duration = models.FloatField(null=True)
is_success = models.BooleanField(default=True)
- error = models.CharField(max_length=255, blank=True, default='')
+ error = models.TextField(blank=True, default='')
+
+ @property
+ def delay(self):
+ if self.last_sync is None:
+ return None
+ # sanity check, this shouldn't happen
+ if self.check_time < self.last_sync:
+ return timedelta()
+ return self.check_time - self.last_sync
def __unicode__(self):
- return "Check of %s at %s" % (url.url, check_time)
+ return "Check of %s at %s" % (self.url.url, self.check_time)
class Meta:
- verbose_name = 'Mirror Check Log'
+ verbose_name = 'mirror check log'
+ get_latest_by = 'check_time'
+
+
+for model in (Mirror, MirrorProtocol, MirrorUrl, MirrorRsync, CheckLocation):
+ pre_save.connect(set_created_field, sender=model,
+ dispatch_uid="mirrors.models")
# vim: set ts=4 sw=4 et:
diff --git a/mirrors/static/mirror_status.js b/mirrors/static/mirror_status.js
new file mode 100644
index 00000000..44c81935
--- /dev/null
+++ b/mirrors/static/mirror_status.js
@@ -0,0 +1,193 @@
+/* mirror_status.js
+ * Homepage: https://projects.archlinux.org/archweb.git/
+ * Copyright: 2012-2013 The Archweb Team (Dan McGee)
+ * License: GPLv2
+ *
+ * This file is part of Archweb.
+ *
+ * Archweb is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * Archweb is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Archweb. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+function draw_graphs(location_url, log_url, container_id) {
+ jQuery.when(jQuery.getJSON(location_url), jQuery.getJSON(log_url))
+ .then(function(loc_data, log_data) {
+ /* use the same color selection for a given URL in every graph */
+ var color = d3.scale.category10();
+ jQuery.each(loc_data[0].locations, function(i, val) {
+ mirror_status(container_id, val, log_data[0], color);
+ });
+ });
+}
+
+function mirror_status(container_id, check_loc, log_data, color) {
+
+ var draw_graph = function(chart_id, data) {
+ var jq_div = jQuery(chart_id);
+ var margin = {top: 20, right: 20, bottom: 30, left: 40},
+ width = jq_div.width() - margin.left - margin.right,
+ height = jq_div.height() - margin.top - margin.bottom;
+
+ var x = d3.time.scale.utc().range([0, width]),
+ y = d3.scale.linear().range([height, 0]),
+ x_axis = d3.svg.axis().scale(x).orient("bottom"),
+ y_axis = d3.svg.axis().scale(y).orient("left");
+
+ /* remove any existing graph first if we are redrawing after resize */
+ d3.select(chart_id).select("svg").remove();
+ var svg = d3.select(chart_id).append("svg")
+ .attr("width", width + margin.left + margin.right)
+ .attr("height", height + margin.top + margin.bottom)
+ .append("g")
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+ x.domain([
+ d3.min(data, function(c) { return d3.min(c.logs, function(v) { return v.check_time; }); }),
+ d3.max(data, function(c) { return d3.max(c.logs, function(v) { return v.check_time; }); })
+ ]).nice(d3.time.hour);
+ y.domain([
+ 0,
+ d3.max(data, function(c) { return d3.max(c.logs, function(v) { return v.duration; }); })
+ ]).nice();
+
+ /* build the axis lines... */
+ svg.append("g")
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + height + ")")
+ .call(x_axis)
+ .append("text")
+ .attr("class", "label")
+ .attr("x", width)
+ .attr("y", -6)
+ .style("text-anchor", "end")
+ .text("Check Time (UTC)");
+
+ svg.append("g")
+ .attr("class", "y axis")
+ .call(y_axis)
+ .append("text")
+ .attr("class", "label")
+ .attr("transform", "rotate(-90)")
+ .attr("y", 6)
+ .attr("dy", ".71em")
+ .style("text-anchor", "end")
+ .text("Duration (seconds)");
+
+ var line = d3.svg.line()
+ .interpolate("basis")
+ .x(function(d) { return x(d.check_time); })
+ .y(function(d) { return y(d.duration); });
+
+ /* ...then the points and lines between them. */
+ var urls = svg.selectAll(".url")
+ .data(data)
+ .enter()
+ .append("g")
+ .attr("class", "url");
+
+ urls.append("path")
+ .attr("class", "url-line")
+ .attr("d", function(d) { return line(d.logs); })
+ .style("stroke", function(d) { return color(d.url); });
+
+ urls.selectAll("circle")
+ .data(function(u) {
+ return jQuery.map(u.logs, function(l, i) {
+ return {url: u.url, check_time: l.check_time, duration: l.duration};
+ });
+ })
+ .enter()
+ .append("circle")
+ .attr("class", "url-dot")
+ .attr("r", 3.5)
+ .attr("cx", function(d) { return x(d.check_time); })
+ .attr("cy", function(d) { return y(d.duration); })
+ .style("fill", function(d) { return color(d.url); })
+ .append("title")
+ .text(function(d) { return d.url + "\n" + d.duration.toFixed(3) + " secs\n" + d.check_time.toUTCString(); });
+
+ /* add a legend for good measure */
+ var active = jQuery.map(data, function(item, i) { return item.url; });
+ var legend = svg.selectAll(".legend")
+ .data(active)
+ .enter().append("g")
+ .attr("class", "legend")
+ .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
+
+ legend.append("rect")
+ .attr("x", width - 18)
+ .attr("width", 18)
+ .attr("height", 18)
+ .style("fill", color);
+
+ legend.append("text")
+ .attr("x", width - 24)
+ .attr("y", 9)
+ .attr("dy", ".35em")
+ .style("text-anchor", "end")
+ .text(function(d) { return d; });
+ };
+
+ var filter_data = function(json, location_id) {
+ return jQuery.map(json.urls, function(url, i) {
+ var logs = jQuery.map(url.logs, function(log, j) {
+ if (!log.is_success) {
+ return null;
+ }
+ /* screen by location ID if we were given one */
+ if (location_id && log.location_id !== location_id) {
+ return null;
+ }
+ return {
+ duration: log.duration,
+ check_time: new Date(log.check_time)
+ };
+ });
+ /* don't return URLs without any log info */
+ if (logs.length === 0) {
+ return null;
+ }
+ return {
+ url: url.url,
+ logs: logs
+ };
+ });
+ };
+
+ var cached_data = filter_data(log_data, check_loc.id);
+ /* we had a check location with no log data handed to us, skip graphing */
+ if (cached_data.length === 0) {
+ return;
+ }
+
+ /* create the containers, defer the actual graph drawing */
+ var chart_id = 'status-chart-' + check_loc.id;
+ jQuery(container_id).append('<h3><span class="fam-flag fam-flag-' + check_loc.country_code.toLowerCase() + '" title="' + check_loc.country + '"></span> ' + check_loc.country + ' (' + check_loc.source_ip + '), IPv' + check_loc.ip_version + '</h3>');
+ jQuery(container_id).append('<div id="' + chart_id + '" class="visualize-mirror visualize-chart"></div>');
+ jQuery(container_id).append('<br/>');
+ setTimeout(function() {
+ draw_graph('#' + chart_id, cached_data);
+ }, 0);
+
+ /* then hook up a resize handler to redraw if necessary */
+ var resize_timeout = null;
+ var real_resize = function() {
+ resize_timeout = null;
+ draw_graph('#' + chart_id, cached_data);
+ };
+ jQuery(window).resize(function() {
+ if (resize_timeout) {
+ clearTimeout(resize_timeout);
+ }
+ resize_timeout = setTimeout(real_resize, 200);
+ });
+}
diff --git a/mirrors/templatetags/jinja2.py b/mirrors/templatetags/jinja2.py
new file mode 100644
index 00000000..04e50238
--- /dev/null
+++ b/mirrors/templatetags/jinja2.py
@@ -0,0 +1,53 @@
+from datetime import timedelta
+from django_jinja import library
+from markupsafe import Markup
+
+
+@library.global_function
+def country_flag(country):
+ if not country:
+ return ''
+ html = '<span class="fam-flag fam-flag-%s" title="%s"></span> ' % (
+ unicode(country.code).lower(), unicode(country.name))
+ return Markup(html)
+
+
+@library.filter
+def duration(value):
+ if not value and type(value) != timedelta:
+ return u''
+ # does not take microseconds into account
+ total_secs = value.seconds + value.days * 24 * 3600
+ mins = total_secs // 60
+ hrs, mins = divmod(mins, 60)
+ return '%d:%02d' % (hrs, mins)
+
+
+@library.filter
+def hours(value):
+ if not value and type(value) != timedelta:
+ return u''
+ # does not take microseconds into account
+ total_secs = value.seconds + value.days * 24 * 3600
+ mins = total_secs // 60
+ hrs, mins = divmod(mins, 60)
+ if hrs == 1:
+ return '%d hour' % hrs
+ return '%d hours' % hrs
+
+
+@library.filter
+def floatvalue(value, arg=2):
+ if value is None:
+ return u''
+ return '%.*f' % (arg, value)
+
+
+@library.filter
+def percentage(value, arg=1):
+ if not value and type(value) != float:
+ return u''
+ new_val = value * 100.0
+ return '%.*f%%' % (arg, new_val)
+
+# vim: set ts=4 sw=4 et:
diff --git a/mirrors/templatetags/mirror_status.py b/mirrors/templatetags/mirror_status.py
index 0031d83b..c8004e4b 100644
--- a/mirrors/templatetags/mirror_status.py
+++ b/mirrors/templatetags/mirror_status.py
@@ -1,6 +1,5 @@
from datetime import timedelta
from django import template
-from django.template.defaultfilters import floatformat
register = template.Library()
@@ -10,7 +9,7 @@ def duration(value):
return u''
# does not take microseconds into account
total_secs = value.seconds + value.days * 24 * 3600
- mins, secs = divmod(total_secs, 60)
+ mins = total_secs // 60
hrs, mins = divmod(mins, 60)
return '%d:%02d' % (hrs, mins)
@@ -20,17 +19,16 @@ def hours(value):
return u''
# does not take microseconds into account
total_secs = value.seconds + value.days * 24 * 3600
- mins, secs = divmod(total_secs, 60)
+ mins = total_secs // 60
hrs, mins = divmod(mins, 60)
if hrs == 1:
return '%d hour' % hrs
return '%d hours' % hrs
@register.filter
-def percentage(value, arg=-1):
- if not value and type(value) != float:
+def floatvalue(value, arg=2):
+ if value is None:
return u''
- new_val = value * 100.0
- return floatformat(new_val, arg) + '%'
+ return '%.*f' % (arg, value)
# vim: set ts=4 sw=4 et:
diff --git a/mirrors/urls.py b/mirrors/urls.py
new file mode 100644
index 00000000..fc510fbb
--- /dev/null
+++ b/mirrors/urls.py
@@ -0,0 +1,18 @@
+from django.conf.urls import patterns
+
+from .views import mirrors, status, mirror_details, url_details
+from .views.api import status_json, mirror_details_json, locations_json
+
+urlpatterns = patterns('',
+ (r'^$', mirrors, {}, 'mirror-list'),
+ (r'^status/$', status, {}, 'mirror-status'),
+ (r'^status/json/$', status_json, {}, 'mirror-status-json'),
+ (r'^status/tier/(?P<tier>\d+)/$', status, {}, 'mirror-status-tier'),
+ (r'^status/tier/(?P<tier>\d+)/json/$', status_json, {}, 'mirror-status-tier-json'),
+ (r'^locations/json/$', locations_json, {}, 'mirror-locations-json'),
+ (r'^(?P<name>[\.\-\w]+)/$', mirror_details),
+ (r'^(?P<name>[\.\-\w]+)/json/$', mirror_details_json),
+ (r'^(?P<name>[\.\-\w]+)/(?P<url_id>\d+)/$', url_details),
+)
+
+# vim: set ts=4 sw=4 et:
diff --git a/mirrors/urls_mirrorlist.py b/mirrors/urls_mirrorlist.py
new file mode 100644
index 00000000..a64656a9
--- /dev/null
+++ b/mirrors/urls_mirrorlist.py
@@ -0,0 +1,11 @@
+from django.conf.urls import patterns
+
+
+urlpatterns = patterns('mirrors.views.mirrorlist',
+ (r'^$', 'generate_mirrorlist', {}, 'mirrorlist'),
+ (r'^all/$', 'find_mirrors', {'countries': ['all']}),
+ (r'^all/(?P<protocol>[A-z]+)/$', 'find_mirrors_simple',
+ {}, 'mirrorlist_simple')
+)
+
+# vim: set ts=4 sw=4 et:
diff --git a/mirrors/utils.py b/mirrors/utils.py
index 0463247a..7c2f5d17 100644
--- a/mirrors/utils.py
+++ b/mirrors/utils.py
@@ -1,64 +1,150 @@
-from django.db.models import Avg, Count, Max, Min, StdDev
-
-from main.utils import cache_function
-from .models import MirrorLog, MirrorProtocol, MirrorUrl
-
-import datetime
-
-default_cutoff = datetime.timedelta(hours=24)
-
-@cache_function(300)
-def get_mirror_statuses(cutoff=default_cutoff):
- cutoff_time = datetime.datetime.utcnow() - cutoff
- protocols = MirrorProtocol.objects.exclude(protocol__iexact='rsync')
- # I swear, this actually has decent performance...
- urls = MirrorUrl.objects.select_related('mirror', 'protocol').filter(
- mirror__active=True, mirror__public=True,
- protocol__in=protocols,
- logs__check_time__gte=cutoff_time).annotate(
- check_count=Count('logs'),
- success_count=Count('logs__duration'),
- last_sync=Max('logs__last_sync'),
- last_check=Max('logs__check_time'),
- duration_avg=Avg('logs__duration'),
- duration_stddev=StdDev('logs__duration')
- ).order_by('-last_sync', '-duration_avg')
-
- # The Django ORM makes it really hard to get actual average delay in the
- # above query, so run a seperate query for it and we will process the
- # results here.
- times = MirrorLog.objects.filter(is_success=True, last_sync__isnull=False,
- check_time__gte=cutoff_time)
- delays = {}
- for log in times:
- d = log.check_time - log.last_sync
- delays.setdefault(log.url_id, []).append(d)
+from datetime import timedelta
+
+from django.db import connection
+from django.db.models import Count, Max, Min
+from django.utils.dateparse import parse_datetime
+from django.utils.timezone import now
+
+from main.utils import cache_function, database_vendor
+from .models import MirrorLog, MirrorUrl
+
+
+DEFAULT_CUTOFF = timedelta(hours=24)
+
+
+def dictfetchall(cursor):
+ "Returns all rows from a cursor as a dict."
+ desc = cursor.description
+ return [
+ dict(zip([col[0] for col in desc], row))
+ for row in cursor.fetchall()
+ ]
+
+def status_data(cutoff_time, mirror_id=None):
+ if mirror_id is not None:
+ params = [cutoff_time, mirror_id]
+ mirror_where = 'AND u.mirror_id = %s'
+ else:
+ params = [cutoff_time]
+ mirror_where = ''
+
+ vendor = database_vendor(MirrorUrl)
+ if vendor == 'sqlite':
+ sql = """
+SELECT l.url_id, u.mirror_id,
+ COUNT(l.id) AS check_count,
+ COUNT(l.last_sync) AS success_count,
+ MAX(l.last_sync) AS last_sync,
+ MAX(l.check_time) AS last_check,
+ AVG(l.duration) AS duration_avg,
+ 0.0 AS duration_stddev,
+ AVG(STRFTIME('%%s', check_time) - STRFTIME('%%s', last_sync)) AS delay
+FROM mirrors_mirrorlog l
+JOIN mirrors_mirrorurl u ON u.id = l.url_id
+WHERE l.check_time >= %s
+""" + mirror_where + """
+GROUP BY l.url_id, u.mirror_id
+"""
+ else:
+ sql = """
+SELECT l.url_id, u.mirror_id,
+ COUNT(l.id) AS check_count,
+ COUNT(l.last_sync) AS success_count,
+ MAX(l.last_sync) AS last_sync,
+ MAX(l.check_time) AS last_check,
+ AVG(l.duration) AS duration_avg,
+ STDDEV(l.duration) AS duration_stddev,
+ AVG(check_time - last_sync) AS delay
+FROM mirrors_mirrorlog l
+JOIN mirrors_mirrorurl u ON u.id = l.url_id
+WHERE l.check_time >= %s
+""" + mirror_where + """
+GROUP BY l.url_id, u.mirror_id
+"""
+
+ cursor = connection.cursor()
+ cursor.execute(sql, params)
+ url_data = dictfetchall(cursor)
+
+ # sqlite loves to return less than ideal types
+ if vendor == 'sqlite':
+ for item in url_data:
+ if item['delay'] is not None:
+ item['delay'] = timedelta(seconds=item['delay'])
+ if item['last_sync'] is not None:
+ item['last_sync'] = parse_datetime(item['last_sync'])
+ item['last_check'] = parse_datetime(item['last_check'])
+
+ return {item['url_id']: item for item in url_data}
+
+
+def annotate_url(url, url_data):
+ '''Given a MirrorURL object, add a few more attributes to it regarding
+ status, including completion_pct, delay, and score.'''
+ # set up some sane default values in case we are missing anything
+ url.success_count = 0
+ url.check_count = 0
+ url.completion_pct = None
+ url.duration_avg = None
+ url.duration_stddev = None
+ url.last_check = None
+ url.last_sync = None
+ url.delay = None
+ url.score = None
+ for k, v in url_data.items():
+ if k not in ('url_id', 'mirror_id'):
+ setattr(url, k, v)
+
+ if url.check_count > 0:
+ url.completion_pct = float(url.success_count) / url.check_count
+
+ if url.delay is not None:
+ hours = url.delay.days * 24.0 + url.delay.seconds / 3600.0
+
+ if url.completion_pct > 0.0:
+ divisor = url.completion_pct
+ else:
+ # arbitrary small value
+ divisor = 0.005
+ stddev = url.duration_stddev or 0.0
+ url.score = (hours + url.duration_avg + stddev) / divisor
+
+ return url
+
+
+@cache_function(178)
+def get_mirror_statuses(cutoff=DEFAULT_CUTOFF, mirror_id=None, show_all=False):
+ cutoff_time = now() - cutoff
+
+ urls = MirrorUrl.objects.select_related(
+ 'mirror', 'protocol').order_by('mirror__id', 'url')
+ if mirror_id:
+ urls = urls.filter(mirror_id=mirror_id)
+ if not show_all:
+ urls = urls.filter(active=True, mirror__active=True,
+ mirror__public=True)
if urls:
- last_check = max([u.last_check for u in urls])
- num_checks = max([u.check_count for u in urls])
- check_info = MirrorLog.objects.filter(
- check_time__gte=cutoff_time).aggregate(
+ url_data = status_data(cutoff_time, mirror_id)
+ urls = [annotate_url(url, url_data.get(url.id, {})) for url in urls]
+ last_check = max([u.last_check for u in urls if u.last_check] or [None])
+ num_checks = max(u.check_count for u in urls)
+ check_info = MirrorLog.objects.filter(check_time__gte=cutoff_time)
+ if mirror_id:
+ check_info = check_info.filter(url__mirror_id=mirror_id)
+ check_info = check_info.aggregate(
mn=Min('check_time'), mx=Max('check_time'))
- check_frequency = (check_info['mx'] - check_info['mn']) / num_checks
+ if num_checks > 1:
+ check_frequency = (check_info['mx'] - check_info['mn']) \
+ / (num_checks - 1)
+ else:
+ check_frequency = None
else:
+ urls = []
last_check = None
num_checks = 0
check_frequency = None
- for url in urls:
- url.completion_pct = float(url.success_count) / num_checks
- if url.id in delays:
- url_delays = delays[url.id]
- d = sum(url_delays, datetime.timedelta()) / len(url_delays)
- url.delay = d
- hours = d.days * 24.0 + d.seconds / 3600.0
- url.score = hours + url.duration_avg + url.duration_stddev
- else:
- url.delay = None
- url.score = None
- url.completion = 0.0
-
return {
'cutoff': cutoff,
'last_check': last_check,
@@ -67,16 +153,57 @@ def get_mirror_statuses(cutoff=default_cutoff):
'urls': urls,
}
-@cache_function(300)
-def get_mirror_errors(cutoff=default_cutoff):
- cutoff_time = datetime.datetime.utcnow() - cutoff
+
+def get_mirror_errors(cutoff=DEFAULT_CUTOFF, mirror_id=None, show_all=False):
+ cutoff_time = now() - cutoff
errors = MirrorLog.objects.filter(
is_success=False, check_time__gte=cutoff_time,
- url__mirror__active=True, url__mirror__public=True).values(
- 'url__url', 'url__protocol__protocol', 'url__mirror__country',
- 'error').annotate(
+ url__mirror__public=True).values('url__id', 'error').annotate(
error_count=Count('error'), last_occurred=Max('check_time')
).order_by('-last_occurred', '-error_count')
- return list(errors)
+
+ if mirror_id:
+ errors = errors.filter(url__mirror_id=mirror_id)
+ if not show_all:
+ errors = errors.filter(url__active=True, url__mirror__active=True,
+ url__mirror__public=True)
+
+ errors = list(errors)
+ to_fetch = [err['url__id'] for err in errors]
+ urls = MirrorUrl.objects.select_related(
+ 'mirror', 'protocol').in_bulk(to_fetch)
+ for err in errors:
+ err['url'] = urls[err['url__id']]
+ return errors
+
+
+@cache_function(295)
+def get_mirror_url_for_download(cutoff=DEFAULT_CUTOFF):
+ '''Find a good mirror URL to use for package downloads. If we have mirror
+ status data available, it is used to determine a good choice by looking at
+ the last batch of status rows.'''
+ cutoff_time = now() - cutoff
+ log_data = MirrorLog.objects.filter(
+ check_time__gte=cutoff_time).aggregate(
+ Max('check_time'), Max('last_sync'))
+ if log_data['check_time__max'] is not None:
+ min_check_time = log_data['check_time__max'] - timedelta(minutes=5)
+ min_sync_time = log_data['last_sync__max'] - timedelta(minutes=20)
+ best_logs = MirrorLog.objects.select_related('url').filter(
+ is_success=True,
+ check_time__gte=min_check_time, last_sync__gte=min_sync_time,
+ url__active=True,
+ url__mirror__public=True, url__mirror__active=True,
+ url__protocol__default=True).order_by(
+ 'duration')[:1]
+ if best_logs:
+ return best_logs[0].url
+
+ mirror_urls = MirrorUrl.objects.filter(active=True,
+ mirror__public=True, mirror__active=True,
+ protocol__default=True)[:1]
+ if not mirror_urls:
+ return None
+ return mirror_urls[0]
# vim: set ts=4 sw=4 et:
diff --git a/mirrors/views.py b/mirrors/views.py
deleted file mode 100644
index 34385a98..00000000
--- a/mirrors/views.py
+++ /dev/null
@@ -1,113 +0,0 @@
-from django import forms
-from django.db.models import Avg, Count, Max, Min, StdDev
-from django.db.models import Q
-from django.http import Http404
-from django.shortcuts import get_object_or_404
-from django.views.decorators.csrf import csrf_exempt
-from django.views.generic.simple import direct_to_template
-
-from main.utils import make_choice
-from .models import Mirror, MirrorUrl, MirrorProtocol
-from .utils import get_mirror_statuses, get_mirror_errors
-
-import datetime
-from operator import attrgetter
-
-class MirrorlistForm(forms.Form):
- country = forms.MultipleChoiceField(required=False)
- protocol = forms.MultipleChoiceField(required=False)
- use_mirror_status = forms.BooleanField(required=False)
-
- def __init__(self, *args, **kwargs):
- super(MirrorlistForm, self).__init__(*args, **kwargs)
- mirrors = Mirror.objects.filter(active=True).values_list(
- 'country', flat=True).distinct().order_by('country')
- self.fields['country'].choices = make_choice(mirrors)
- self.fields['country'].initial = ['Any']
- protos = make_choice(
- MirrorProtocol.objects.exclude(protocol__iexact='rsync'))
- self.fields['protocol'].choices = protos
- self.fields['protocol'].initial = [t[0] for t in protos]
-
-@csrf_exempt
-def generate_mirrorlist(request):
- if request.REQUEST.get('country', ''):
- form = MirrorlistForm(data=request.REQUEST)
- if form.is_valid():
- countries = form.cleaned_data['country']
- protocols = form.cleaned_data['protocol']
- use_status = form.cleaned_data['use_mirror_status']
- return find_mirrors(request, countries, protocols, use_status)
- else:
- form = MirrorlistForm()
-
- return direct_to_template(request, 'mirrors/index.html', {'mirrorlist_form': form})
-
-def find_mirrors(request, countries=None, protocols=None, use_status=False):
- if not protocols:
- protocols = MirrorProtocol.objects.exclude(
- protocol__iexact='rsync').values_list('protocol', flat=True)
- qset = MirrorUrl.objects.select_related().filter(
- protocol__protocol__in=protocols,
- mirror__public=True, mirror__active=True, mirror__isos=True
- )
- if countries and 'all' not in countries:
- qset = qset.filter(mirror__country__in=countries)
- if not use_status:
- urls = qset.order_by('mirror__country', 'mirror__name', 'url')
- template = 'mirrors/mirrorlist.txt'
- else:
- status_info = get_mirror_statuses()
- scores = dict([(u.id, u.score) for u in status_info['urls']])
- urls = []
- for u in qset:
- u.score = scores[u.id]
- if u.score and u.score < 100.0:
- urls.append(u)
- urls = sorted(urls, key=attrgetter('score'))
- template = 'mirrors/mirrorlist_status.txt'
-
- return direct_to_template(request, template, {
- 'mirror_urls': urls,
- },
- mimetype='text/plain')
-
-def mirrors(request):
- mirrors = Mirror.objects.select_related().order_by('tier', 'country')
- if not request.user.is_authenticated():
- mirrors = mirrors.filter(public=True, active=True)
- return direct_to_template(request, 'mirrors/mirrors.html',
- {'mirror_list': mirrors})
-
-def mirror_details(request, name):
- mirror = get_object_or_404(Mirror, name=name)
- if not request.user.is_authenticated() and \
- (not mirror.public or not mirror.active):
- # TODO: maybe this should be 403? but that would leak existence
- raise Http404
- return direct_to_template(request, 'mirrors/mirror_details.html',
- {'mirror': mirror})
-
-def status(request):
- bad_timedelta = datetime.timedelta(days=3)
- status_info = get_mirror_statuses()
-
- urls = status_info['urls']
- good_urls = []
- bad_urls = []
- for url in urls:
- # split them into good and bad lists based on delay
- if not url.delay or url.delay > bad_timedelta:
- bad_urls.append(url)
- else:
- good_urls.append(url)
-
- context = status_info.copy()
- context.update({
- 'good_urls': good_urls,
- 'bad_urls': bad_urls,
- 'error_logs': get_mirror_errors(),
- })
- return direct_to_template(request, 'mirrors/status.html', context)
-
-# vim: set ts=4 sw=4 et:
diff --git a/mirrors/views/__init__.py b/mirrors/views/__init__.py
new file mode 100644
index 00000000..01e8519d
--- /dev/null
+++ b/mirrors/views/__init__.py
@@ -0,0 +1,149 @@
+from datetime import timedelta
+from itertools import groupby
+from operator import attrgetter, itemgetter
+
+from django.db import connection
+from django.http import Http404
+from django.shortcuts import get_object_or_404, render
+from django.utils.timezone import now
+from django.views.decorators.http import condition
+from django_countries.fields import Country
+
+from ..models import (Mirror, MirrorUrl, MirrorProtocol, MirrorLog,
+ CheckLocation)
+from ..utils import get_mirror_statuses, get_mirror_errors
+
+
+def mirrors(request):
+ mirror_list = Mirror.objects.select_related().order_by('tier', 'name')
+ protos = MirrorUrl.objects.values_list(
+ 'mirror_id', 'protocol__protocol').order_by(
+ 'mirror_id', 'protocol__protocol').distinct()
+ countries = MirrorUrl.objects.values_list(
+ 'mirror_id', 'country').order_by(
+ 'mirror_id', 'country').distinct()
+
+ if not request.user.is_authenticated():
+ mirror_list = mirror_list.filter(public=True, active=True)
+ protos = protos.filter(
+ mirror__public=True, mirror__active=True, active=True)
+ countries = countries.filter(
+ mirror__public=True, mirror__active=True, active=True)
+
+ protos = {k: list(v) for k, v in groupby(protos, key=itemgetter(0))}
+ countries = {k: list(v) for k, v in groupby(countries, key=itemgetter(0))}
+
+ for mirror in mirror_list:
+ item_protos = protos.get(mirror.id, [])
+ mirror.protocols = [item[1] for item in item_protos]
+ mirror.country = None
+ item_countries = countries.get(mirror.id, [])
+ if len(item_countries) == 1:
+ mirror.country = Country(item_countries[0][1])
+
+ return render(request, 'mirrors/mirrors.html',
+ {'mirror_list': mirror_list})
+
+
+def mirror_details(request, name):
+ mirror = get_object_or_404(Mirror, name=name)
+ authorized = request.user.is_authenticated()
+ if not authorized and \
+ (not mirror.public or not mirror.active):
+ raise Http404
+ error_cutoff = timedelta(days=7)
+
+ status_info = get_mirror_statuses(mirror_id=mirror.id,
+ show_all=authorized)
+ checked_urls = {url for url in status_info['urls'] \
+ if url.mirror_id == mirror.id}
+ all_urls = mirror.urls.select_related('protocol')
+ if not authorized:
+ all_urls = all_urls.filter(active=True)
+ all_urls = set(all_urls)
+ # Add dummy data for URLs that we haven't checked recently
+ other_urls = all_urls.difference(checked_urls)
+ for url in other_urls:
+ for attr in ('last_sync', 'completion_pct', 'delay', 'duration_avg',
+ 'duration_stddev', 'score'):
+ setattr(url, attr, None)
+ all_urls = sorted(checked_urls.union(other_urls), key=attrgetter('url'))
+
+ error_logs = get_mirror_errors(mirror_id=mirror.id, cutoff=error_cutoff,
+ show_all=True)
+
+ context = {
+ 'mirror': mirror,
+ 'urls': all_urls,
+ 'cutoff': error_cutoff,
+ 'error_logs': error_logs,
+ }
+ return render(request, 'mirrors/mirror_details.html', context)
+
+
+def url_details(request, name, url_id):
+ url = get_object_or_404(MirrorUrl.objects.select_related(),
+ id=url_id, mirror__name=name)
+ mirror = url.mirror
+ authorized = request.user.is_authenticated()
+ if not authorized and \
+ (not mirror.public or not mirror.active or not url.active):
+ raise Http404
+ error_cutoff = timedelta(days=7)
+ cutoff_time = now() - error_cutoff
+ logs = MirrorLog.objects.select_related('location').filter(
+ url=url, check_time__gte=cutoff_time).order_by('-check_time')
+
+ context = {
+ 'url': url,
+ 'logs': logs,
+ }
+ return render(request, 'mirrors/url_details.html', context)
+
+
+def status_last_modified(request, *args, **kwargs):
+ cursor = connection.cursor()
+ cursor.execute("SELECT MAX(check_time) FROM mirrors_mirrorlog")
+ return cursor.fetchone()[0]
+
+
+@condition(last_modified_func=status_last_modified)
+def status(request, tier=None):
+ if tier is not None:
+ tier = int(tier)
+ if tier not in [t[0] for t in Mirror.TIER_CHOICES]:
+ raise Http404
+ bad_timedelta = timedelta(days=3)
+ status_info = get_mirror_statuses()
+
+ urls = status_info['urls']
+ good_urls = []
+ bad_urls = []
+ for url in urls:
+ # screen by tier if we were asked to
+ if tier is not None and url.mirror.tier != tier:
+ continue
+ # split them into good and bad lists based on delay
+ if url.completion_pct is None:
+ # skip URLs that have never been checked
+ continue
+ elif not url.delay or url.delay > bad_timedelta:
+ bad_urls.append(url)
+ else:
+ good_urls.append(url)
+
+ error_logs = get_mirror_errors()
+ if tier is not None:
+ error_logs = [log for log in error_logs
+ if log['url'].mirror.tier == tier]
+
+ context = status_info.copy()
+ context.update({
+ 'good_urls': sorted(good_urls, key=attrgetter('score')),
+ 'bad_urls': sorted(bad_urls, key=lambda u: u.delay or timedelta.max),
+ 'error_logs': error_logs,
+ 'tier': tier,
+ })
+ return render(request, 'mirrors/status.html', context)
+
+# vim: set ts=4 sw=4 et:
diff --git a/mirrors/views/api.py b/mirrors/views/api.py
new file mode 100644
index 00000000..b72585e6
--- /dev/null
+++ b/mirrors/views/api.py
@@ -0,0 +1,108 @@
+from datetime import timedelta
+import json
+
+from django.core.serializers.json import DjangoJSONEncoder
+from django.http import Http404, HttpResponse
+from django.shortcuts import get_object_or_404
+from django.utils.timezone import now
+
+from ..models import (Mirror, MirrorUrl, MirrorProtocol, MirrorLog,
+ CheckLocation)
+from ..utils import get_mirror_statuses, DEFAULT_CUTOFF
+
+
+class MirrorStatusJSONEncoder(DjangoJSONEncoder):
+ '''Base JSONEncoder extended to handle datetime.timedelta and MirrorUrl
+ serialization. The base class takes care of datetime.datetime types.'''
+ url_attributes = ('url', 'protocol', 'last_sync', 'completion_pct',
+ 'delay', 'duration_avg', 'duration_stddev', 'score')
+
+ def default(self, obj):
+ if isinstance(obj, timedelta):
+ # always returned as integer seconds
+ return obj.days * 24 * 3600 + obj.seconds
+ if isinstance(obj, MirrorUrl):
+ data = {attr: getattr(obj, attr) for attr in self.url_attributes}
+ country = obj.country
+ data['country'] = unicode(country.name)
+ data['country_code'] = country.code
+ return data
+ if isinstance(obj, MirrorProtocol):
+ return unicode(obj)
+ return super(MirrorStatusJSONEncoder, self).default(obj)
+
+
+class ExtendedMirrorStatusJSONEncoder(MirrorStatusJSONEncoder):
+ '''Adds URL check history information.'''
+ log_attributes = ('check_time', 'last_sync', 'duration', 'is_success',
+ 'location_id')
+
+ def default(self, obj):
+ if isinstance(obj, MirrorUrl):
+ data = super(ExtendedMirrorStatusJSONEncoder, self).default(obj)
+ cutoff = now() - DEFAULT_CUTOFF
+ data['logs'] = list(obj.logs.filter(
+ check_time__gte=cutoff).order_by('check_time'))
+ return data
+ if isinstance(obj, MirrorLog):
+ data = {attr: getattr(obj, attr) for attr in self.log_attributes}
+ data['error'] = obj.error or None
+ return data
+ return super(ExtendedMirrorStatusJSONEncoder, self).default(obj)
+
+
+class LocationJSONEncoder(DjangoJSONEncoder):
+ '''Base JSONEncoder extended to handle CheckLocation objects.'''
+
+ def default(self, obj):
+ if isinstance(obj, CheckLocation):
+ return {
+ 'id': obj.pk,
+ 'hostname': obj.hostname,
+ 'source_ip': obj.source_ip,
+ 'country': unicode(obj.country.name),
+ 'country_code': obj.country.code,
+ 'ip_version': obj.ip_version,
+ }
+ return super(LocationJSONEncoder, self).default(obj)
+
+
+def status_json(request, tier=None):
+ if tier is not None:
+ tier = int(tier)
+ if tier not in [t[0] for t in Mirror.TIER_CHOICES]:
+ raise Http404
+ status_info = get_mirror_statuses()
+ data = status_info.copy()
+ if tier is not None:
+ data['urls'] = [url for url in data['urls'] if url.mirror.tier == tier]
+ data['version'] = 3
+ to_json = json.dumps(data, ensure_ascii=False, cls=MirrorStatusJSONEncoder)
+ response = HttpResponse(to_json, content_type='application/json')
+ return response
+
+
+def mirror_details_json(request, name):
+ authorized = request.user.is_authenticated()
+ mirror = get_object_or_404(Mirror, name=name)
+ if not authorized and (not mirror.public or not mirror.active):
+ raise Http404
+ status_info = get_mirror_statuses(mirror_id=mirror.id,
+ show_all=authorized)
+ data = status_info.copy()
+ data['version'] = 3
+ to_json = json.dumps(data, ensure_ascii=False,
+ cls=ExtendedMirrorStatusJSONEncoder)
+ response = HttpResponse(to_json, content_type='application/json')
+ return response
+
+
+def locations_json(request):
+ data = {}
+ data['version'] = 1
+ data['locations'] = list(CheckLocation.objects.all().order_by('pk'))
+ to_json = json.dumps(data, ensure_ascii=False, cls=LocationJSONEncoder)
+ response = HttpResponse(to_json, content_type='application/json')
+ return response
+
+# vim: set ts=4 sw=4 et:
diff --git a/mirrors/views/mirrorlist.py b/mirrors/views/mirrorlist.py
new file mode 100644
index 00000000..3c68d036
--- /dev/null
+++ b/mirrors/views/mirrorlist.py
@@ -0,0 +1,129 @@
+from operator import attrgetter, itemgetter
+
+from django import forms
+from django.db.models import Q
+from django.forms.widgets import SelectMultiple, CheckboxSelectMultiple
+from django.shortcuts import get_object_or_404, redirect, render
+from django.views.decorators.csrf import csrf_exempt
+from django_countries import countries
+
+from ..models import MirrorUrl, MirrorProtocol
+from ..utils import get_mirror_statuses
+
+
+class MirrorlistForm(forms.Form):
+ country = forms.MultipleChoiceField(required=False,
+ widget=SelectMultiple(attrs={'size': '12'}))
+ protocol = forms.MultipleChoiceField(required=False,
+ widget=CheckboxSelectMultiple)
+ ip_version = forms.MultipleChoiceField(required=False,
+ label="IP version", choices=(('4','IPv4'), ('6','IPv6')),
+ widget=CheckboxSelectMultiple)
+ use_mirror_status = forms.BooleanField(required=False)
+
+ def __init__(self, *args, **kwargs):
+ super(MirrorlistForm, self).__init__(*args, **kwargs)
+ fields = self.fields
+ fields['country'].choices = [('all','All')] + self.get_countries()
+ fields['country'].initial = ['all']
+ protos = [(p.protocol, p.protocol) for p in
+ MirrorProtocol.objects.filter(is_download=True)]
+ initial = MirrorProtocol.objects.filter(is_download=True, default=True)
+ fields['protocol'].choices = protos
+ fields['protocol'].initial = [p.protocol for p in initial]
+ fields['ip_version'].initial = ['4']
+
+ def get_countries(self):
+ country_codes = set()
+ country_codes.update(MirrorUrl.objects.filter(active=True,
+ mirror__active=True).exclude(country='').values_list(
+ 'country', flat=True).order_by().distinct())
+ code_list = [(code, countries.name(code)) for code in country_codes]
+ return sorted(code_list, key=itemgetter(1))
+
+ def as_div(self):
+ "Returns this form rendered as HTML <divs>s."
+ return self._html_output(
+ normal_row = u'<div%(html_class_attr)s>%(label)s %(field)s%(help_text)s</div>',
+ error_row = u'%s',
+ row_ender = '</div>',
+ help_text_html = u' <span class="helptext">%s</span>',
+ errors_on_separate_row = True)
+
+
+@csrf_exempt
+def generate_mirrorlist(request):
+ if request.method == 'POST' or len(request.GET) > 0:
+ form = MirrorlistForm(data=request.REQUEST)
+ if form.is_valid():
+ countries = form.cleaned_data['country']
+ protocols = form.cleaned_data['protocol']
+ use_status = form.cleaned_data['use_mirror_status']
+ ipv4 = '4' in form.cleaned_data['ip_version']
+ ipv6 = '6' in form.cleaned_data['ip_version']
+ return find_mirrors(request, countries, protocols,
+ use_status, ipv4, ipv6)
+ else:
+ form = MirrorlistForm()
+
+ return render(request, 'mirrors/mirrorlist_generate.html',
+ {'mirrorlist_form': form})
+
+
+def status_filter(original_urls):
+ status_info = get_mirror_statuses()
+ scores = {u.id: u.score for u in status_info['urls']}
+ urls = []
+ for u in original_urls:
+ u.score = scores.get(u.id, None)
+ # also include mirrors that don't have an up to date score
+ # (as opposed to those that have been set with no score)
+ if (u.id not in scores) or (u.score and u.score < 100.0):
+ urls.append(u)
+ # if a url doesn't have a score, treat it as the highest possible
+ return sorted(urls, key=lambda x: x.score or 100.0)
+
+
+def find_mirrors(request, countries=None, protocols=None, use_status=False,
+ ipv4_supported=True, ipv6_supported=True):
+ if not protocols:
+ protocols = MirrorProtocol.objects.filter(is_download=True)
+ elif hasattr(protocols, 'model') and protocols.model == MirrorProtocol:
+ # we already have a queryset, no need to query again
+ pass
+ else:
+ protocols = MirrorProtocol.objects.filter(protocol__in=protocols)
+ qset = MirrorUrl.objects.select_related().filter(
+ protocol__in=protocols, active=True,
+ mirror__public=True, mirror__active=True)
+ if countries and 'all' not in countries:
+ qset = qset.filter(country__in=countries)
+
+ ip_version = Q()
+ if ipv4_supported:
+ ip_version |= Q(has_ipv4=True)
+ if ipv6_supported:
+ ip_version |= Q(has_ipv6=True)
+ qset = qset.filter(ip_version)
+
+ if not use_status:
+ sort_key = attrgetter('country.name', 'mirror.name', 'url')
+ urls = sorted(qset, key=sort_key)
+ template = 'mirrors/mirrorlist.txt'
+ else:
+ urls = status_filter(qset)
+ template = 'mirrors/mirrorlist_status.txt'
+
+ context = {
+ 'mirror_urls': urls,
+ }
+ return render(request, template, context, content_type='text/plain')
+
+
+def find_mirrors_simple(request, protocol):
+ if protocol == 'smart':
+ return redirect('mirrorlist_simple', 'http', permanent=True)
+ proto = get_object_or_404(MirrorProtocol, protocol=protocol)
+ return find_mirrors(request, protocols=[proto])
+
+# vim: set ts=4 sw=4 et: