summaryrefslogtreecommitdiff
path: root/releng
diff options
context:
space:
mode:
Diffstat (limited to 'releng')
-rw-r--r--releng/__init__.py0
-rw-r--r--releng/admin.py34
-rw-r--r--releng/fixtures/architecture.json30
-rw-r--r--releng/fixtures/bootloaders.json23
-rw-r--r--releng/fixtures/boottype.json23
-rw-r--r--releng/fixtures/clockchoices.json72
-rw-r--r--releng/fixtures/filesystems.json23
-rw-r--r--releng/fixtures/hardware.json44
-rw-r--r--releng/fixtures/installtype.json30
-rw-r--r--releng/fixtures/isotypes.json16
-rw-r--r--releng/fixtures/modules.json86
-rw-r--r--releng/fixtures/source.json23
-rw-r--r--releng/management/__init__.py0
-rw-r--r--releng/management/commands/__init__.py0
-rw-r--r--releng/management/commands/syncisos.py61
-rw-r--r--releng/migrations/0001_initial.py185
-rw-r--r--releng/migrations/0002_release_last_modified.py22
-rw-r--r--releng/migrations/0003_release_populate_last_modified.py21
-rw-r--r--releng/migrations/__init__.py0
-rw-r--r--releng/models.py193
-rw-r--r--releng/urls.py30
-rw-r--r--releng/views.py283
22 files changed, 1199 insertions, 0 deletions
diff --git a/releng/__init__.py b/releng/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/releng/__init__.py
diff --git a/releng/admin.py b/releng/admin.py
new file mode 100644
index 00000000..9c93c4be
--- /dev/null
+++ b/releng/admin.py
@@ -0,0 +1,34 @@
+from django.contrib import admin
+
+from .models import (Architecture, BootType, Bootloader, ClockChoice,
+ Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source,
+ Test, Release)
+
+class IsoAdmin(admin.ModelAdmin):
+ list_display = ('name', 'created', 'active', 'removed')
+ list_filter = ('active', 'created')
+ date_hierarchy = 'created'
+
+class TestAdmin(admin.ModelAdmin):
+ list_display = ('user_name', 'user_email', 'created', 'ip_address',
+ 'iso', 'success')
+ list_filter = ('success', 'iso')
+
+class ReleaseAdmin(admin.ModelAdmin):
+ list_display = ('version', 'release_date', 'kernel_version', 'available',
+ 'created')
+ list_filter = ('available', 'release_date')
+ readonly_fields = ('created', 'last_modified')
+
+
+SIMPLE_MODELS = (Architecture, BootType, Bootloader, ClockChoice, Filesystem,
+ HardwareType, InstallType, IsoType, Module, Source)
+
+for model in SIMPLE_MODELS:
+ admin.site.register(model)
+
+admin.site.register(Iso, IsoAdmin)
+admin.site.register(Test, TestAdmin)
+admin.site.register(Release, ReleaseAdmin)
+
+# vim: set ts=4 sw=4 et:
diff --git a/releng/fixtures/architecture.json b/releng/fixtures/architecture.json
new file mode 100644
index 00000000..0bf9b8bf
--- /dev/null
+++ b/releng/fixtures/architecture.json
@@ -0,0 +1,30 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.architecture",
+ "fields": {
+ "name": "dual, option i686"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.architecture",
+ "fields": {
+ "name": "dual, option x86_64"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.architecture",
+ "fields": {
+ "name": "i686"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "releng.architecture",
+ "fields": {
+ "name": "x86_64"
+ }
+ }
+]
diff --git a/releng/fixtures/bootloaders.json b/releng/fixtures/bootloaders.json
new file mode 100644
index 00000000..bee02f2b
--- /dev/null
+++ b/releng/fixtures/bootloaders.json
@@ -0,0 +1,23 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.bootloader",
+ "fields": {
+ "name": "grub"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.bootloader",
+ "fields": {
+ "name": "syslinux"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.bootloader",
+ "fields": {
+ "name": "other/manual"
+ }
+ }
+]
diff --git a/releng/fixtures/boottype.json b/releng/fixtures/boottype.json
new file mode 100644
index 00000000..ed4636eb
--- /dev/null
+++ b/releng/fixtures/boottype.json
@@ -0,0 +1,23 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.boottype",
+ "fields": {
+ "name": "optical"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.boottype",
+ "fields": {
+ "name": "usb"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.boottype",
+ "fields": {
+ "name": "pxe"
+ }
+ }
+]
diff --git a/releng/fixtures/clockchoices.json b/releng/fixtures/clockchoices.json
new file mode 100644
index 00000000..d2d4eb80
--- /dev/null
+++ b/releng/fixtures/clockchoices.json
@@ -0,0 +1,72 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "default region/timezone, keep clock"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "default region/timezone, change clock manually (UTC)"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "default region/timezone, change clock with NTP (UTC)"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "default region/timezone, change clock manually (localtime)"
+ }
+ },
+ {
+ "pk": 5,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "default region/timezone, change clock with NTP (localtime)"
+ }
+ },
+ {
+ "pk": 6,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "update region/timezone, keep clock"
+ }
+ },
+ {
+ "pk": 7,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "update region/timezone, change clock manually (UTC)"
+ }
+ },
+ {
+ "pk": 8,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "update region/timezone, change clock with NTP (UTC)"
+ }
+ },
+ {
+ "pk": 9,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "update region/timezone, change clock manually (localtime)"
+ }
+ },
+ {
+ "pk": 10,
+ "model": "releng.clockchoice",
+ "fields": {
+ "name": "update region/timezone, change clock with NTP (localtime)"
+ }
+ }
+]
diff --git a/releng/fixtures/filesystems.json b/releng/fixtures/filesystems.json
new file mode 100644
index 00000000..208f5c73
--- /dev/null
+++ b/releng/fixtures/filesystems.json
@@ -0,0 +1,23 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.filesystem",
+ "fields": {
+ "name": "autoprepare"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.filesystem",
+ "fields": {
+ "name": "manual"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.filesystem",
+ "fields": {
+ "name": "from config file"
+ }
+ }
+]
diff --git a/releng/fixtures/hardware.json b/releng/fixtures/hardware.json
new file mode 100644
index 00000000..a2bb9ec0
--- /dev/null
+++ b/releng/fixtures/hardware.json
@@ -0,0 +1,44 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "virtualbox"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "qemu"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "intel i686"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "intel x86_64"
+ }
+ },
+ {
+ "pk": 5,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "amd i686"
+ }
+ },
+ {
+ "pk": 6,
+ "model": "releng.hardwaretype",
+ "fields": {
+ "name": "amd x86_64"
+ }
+ }
+]
diff --git a/releng/fixtures/installtype.json b/releng/fixtures/installtype.json
new file mode 100644
index 00000000..07d17f28
--- /dev/null
+++ b/releng/fixtures/installtype.json
@@ -0,0 +1,30 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.installtype",
+ "fields": {
+ "name": "interactive install"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.installtype",
+ "fields": {
+ "name": "automatic install generic example"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.installtype",
+ "fields": {
+ "name": "automatic install fancy example"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "releng.installtype",
+ "fields": {
+ "name": "automatic install custom config (if special, specify in comments)"
+ }
+ }
+]
diff --git a/releng/fixtures/isotypes.json b/releng/fixtures/isotypes.json
new file mode 100644
index 00000000..a529b181
--- /dev/null
+++ b/releng/fixtures/isotypes.json
@@ -0,0 +1,16 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.isotype",
+ "fields": {
+ "name": "core"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.isotype",
+ "fields": {
+ "name": "net"
+ }
+ }
+]
diff --git a/releng/fixtures/modules.json b/releng/fixtures/modules.json
new file mode 100644
index 00000000..9cdf1a8d
--- /dev/null
+++ b/releng/fixtures/modules.json
@@ -0,0 +1,86 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.module",
+ "fields": {
+ "name": "lvm2"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.module",
+ "fields": {
+ "name": "dm_crypt"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.module",
+ "fields": {
+ "name": "softraid"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "releng.module",
+ "fields": {
+ "name": "nilfs2"
+ }
+ },
+ {
+ "pk": 5,
+ "model": "releng.module",
+ "fields": {
+ "name": "btrfs"
+ }
+ },
+ {
+ "pk": 6,
+ "model": "releng.module",
+ "fields": {
+ "name": "ext2"
+ }
+ },
+ {
+ "pk": 7,
+ "model": "releng.module",
+ "fields": {
+ "name": "ext3"
+ }
+ },
+ {
+ "pk": 8,
+ "model": "releng.module",
+ "fields": {
+ "name": "ext4"
+ }
+ },
+ {
+ "pk": 9,
+ "model": "releng.module",
+ "fields": {
+ "name": "swap"
+ }
+ },
+ {
+ "pk": 10,
+ "model": "releng.module",
+ "fields": {
+ "name": "xfs"
+ }
+ },
+ {
+ "pk": 11,
+ "model": "releng.module",
+ "fields": {
+ "name": "jfs"
+ }
+ },
+ {
+ "pk": 12,
+ "model": "releng.module",
+ "fields": {
+ "name": "reiserFS"
+ }
+ }
+]
diff --git a/releng/fixtures/source.json b/releng/fixtures/source.json
new file mode 100644
index 00000000..9d1950a5
--- /dev/null
+++ b/releng/fixtures/source.json
@@ -0,0 +1,23 @@
+[
+ {
+ "pk": 1,
+ "model": "releng.source",
+ "fields": {
+ "name": "net install manual networking config (verify network, rc.conf, resolv.conf, mirrorlist)"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "releng.source",
+ "fields": {
+ "name": "net install dhcp (verify network, rc.conf, mirrorlist)"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "releng.source",
+ "fields": {
+ "name": "core"
+ }
+ }
+]
diff --git a/releng/management/__init__.py b/releng/management/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/releng/management/__init__.py
diff --git a/releng/management/commands/__init__.py b/releng/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/releng/management/commands/__init__.py
diff --git a/releng/management/commands/syncisos.py b/releng/management/commands/syncisos.py
new file mode 100644
index 00000000..f182cc33
--- /dev/null
+++ b/releng/management/commands/syncisos.py
@@ -0,0 +1,61 @@
+import re
+import urllib
+from HTMLParser import HTMLParser, HTMLParseError
+
+from django.conf import settings
+from django.core.management.base import BaseCommand, CommandError
+from django.utils.timezone import now
+
+from releng.models import Iso
+
+
+class IsoListParser(HTMLParser):
+ def __init__(self):
+ HTMLParser.__init__(self)
+
+ self.hyperlinks = []
+ self.url_re = re.compile('(?!\.{2})/$')
+
+ def handle_starttag(self, tag, attrs):
+ if tag == 'a':
+ for name, value in attrs:
+ if name == "href":
+ if value != '../' and self.url_re.search(value) is not None:
+ self.hyperlinks.append(value[:-1])
+
+ def parse(self, url):
+ try:
+ remote_file = urllib.urlopen(url)
+ data = remote_file.read()
+ remote_file.close()
+ self.feed(data)
+ self.close()
+ return self.hyperlinks
+ except HTMLParseError:
+ raise CommandError('Couldn\'t parse "%s"' % url)
+
+class Command(BaseCommand):
+ help = 'Gets new ISOs from %s' % settings.ISO_LIST_URL
+
+ def handle(self, *args, **options):
+ parser = IsoListParser()
+ isonames = Iso.objects.values_list('name', flat=True)
+ active_isos = parser.parse(settings.ISO_LIST_URL)
+
+ for iso in active_isos:
+ # create any names that don't already exist
+ if iso not in isonames:
+ new = Iso(name=iso, active=True)
+ new.save()
+ # update those that do if they were marked inactive
+ else:
+ existing = Iso.objects.get(name=iso)
+ if not existing.active:
+ existing.active = True
+ existing.removed = None
+ existing.save(update_fields=('active', 'removed'))
+ # and then mark all other names as no longer active
+ Iso.objects.filter(active=True).exclude(name__in=active_isos).update(
+ active=False, removed=now())
+
+# vim: set ts=4 sw=4 et:
diff --git a/releng/migrations/0001_initial.py b/releng/migrations/0001_initial.py
new file mode 100644
index 00000000..b56f389d
--- /dev/null
+++ b/releng/migrations/0001_initial.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Architecture',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=200)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Bootloader',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=200)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='BootType',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=200)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='ClockChoice',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=200)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Filesystem',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=200)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='HardwareType',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=200)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='InstallType',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=200)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Iso',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=255)),
+ ('created', models.DateTimeField(editable=False)),
+ ('removed', models.DateTimeField(default=None, null=True, blank=True)),
+ ('active', models.BooleanField(default=True)),
+ ],
+ options={
+ 'verbose_name': 'ISO',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='IsoType',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=200)),
+ ],
+ options={
+ 'verbose_name': 'ISO type',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Module',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=200)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Release',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('release_date', models.DateField(db_index=True)),
+ ('version', models.CharField(unique=True, max_length=50)),
+ ('kernel_version', models.CharField(max_length=50, blank=True)),
+ ('md5_sum', models.CharField(max_length=32, verbose_name=b'MD5 digest', blank=True)),
+ ('sha1_sum', models.CharField(max_length=40, verbose_name=b'SHA1 digest', blank=True)),
+ ('created', models.DateTimeField(editable=False)),
+ ('available', models.BooleanField(default=True)),
+ ('info', models.TextField(verbose_name=b'Public information', blank=True)),
+ ('torrent_data', models.TextField(help_text=b'base64-encoded torrent file', blank=True)),
+ ],
+ options={
+ 'ordering': ('-release_date', '-version'),
+ 'get_latest_by': 'release_date',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Source',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('name', models.CharField(max_length=200)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='Test',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('user_name', models.CharField(max_length=500)),
+ ('user_email', models.EmailField(max_length=75, verbose_name=b'email address')),
+ ('ip_address', models.GenericIPAddressField(verbose_name=b'IP address', unpack_ipv4=True)),
+ ('created', models.DateTimeField(editable=False)),
+ ('success', models.BooleanField(default=True)),
+ ('comments', models.TextField(null=True, blank=True)),
+ ('architecture', models.ForeignKey(to='releng.Architecture')),
+ ('boot_type', models.ForeignKey(to='releng.BootType')),
+ ('bootloader', models.ForeignKey(to='releng.Bootloader')),
+ ('clock_choice', models.ForeignKey(to='releng.ClockChoice')),
+ ('filesystem', models.ForeignKey(to='releng.Filesystem')),
+ ('hardware_type', models.ForeignKey(to='releng.HardwareType')),
+ ('install_type', models.ForeignKey(to='releng.InstallType')),
+ ('iso', models.ForeignKey(to='releng.Iso')),
+ ('iso_type', models.ForeignKey(to='releng.IsoType')),
+ ('modules', models.ManyToManyField(to='releng.Module', null=True, blank=True)),
+ ('rollback_filesystem', models.ForeignKey(related_name=b'rollback_test_set', blank=True, to='releng.Filesystem', null=True)),
+ ('rollback_modules', models.ManyToManyField(related_name=b'rollback_test_set', null=True, to='releng.Module', blank=True)),
+ ('source', models.ForeignKey(to='releng.Source')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ ]
diff --git a/releng/migrations/0002_release_last_modified.py b/releng/migrations/0002_release_last_modified.py
new file mode 100644
index 00000000..58502452
--- /dev/null
+++ b/releng/migrations/0002_release_last_modified.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import datetime
+from django.utils.timezone import utc
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('releng', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='release',
+ name='last_modified',
+ field=models.DateTimeField(default=datetime.datetime(2001, 1, 1, tzinfo=utc), editable=False),
+ preserve_default=False,
+ ),
+ ]
diff --git a/releng/migrations/0003_release_populate_last_modified.py b/releng/migrations/0003_release_populate_last_modified.py
new file mode 100644
index 00000000..ec7b6fda
--- /dev/null
+++ b/releng/migrations/0003_release_populate_last_modified.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+def forwards(apps, schema_editor):
+ Release = apps.get_model('releng', 'Release')
+ Release.objects.update(last_modified=models.F('created'))
+
+def backwards(apps, schema_editor):
+ pass
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('releng', '0002_release_last_modified'),
+ ]
+
+ operations = [
+ migrations.RunPython(forwards, backwards)
+ ]
diff --git a/releng/migrations/__init__.py b/releng/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/releng/migrations/__init__.py
diff --git a/releng/models.py b/releng/models.py
new file mode 100644
index 00000000..a4af81ab
--- /dev/null
+++ b/releng/models.py
@@ -0,0 +1,193 @@
+from base64 import b64decode
+from bencode import bdecode, bencode
+from datetime import datetime
+import hashlib
+from pytz import utc
+
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.db import models
+from django.db.models.signals import pre_save
+from django.utils.safestring import mark_safe
+
+from main.fields import PositiveBigIntegerField
+from main.utils import set_created_field, parse_markdown
+
+
+class IsoOption(models.Model):
+ name = models.CharField(max_length=200)
+
+ def __unicode__(self):
+ return self.name
+
+ class Meta:
+ abstract = True
+
+
+class RollbackOption(IsoOption):
+ class Meta:
+ abstract = True
+
+
+class Iso(models.Model):
+ name = models.CharField(max_length=255)
+ created = models.DateTimeField(editable=False)
+ removed = models.DateTimeField(null=True, blank=True, default=None)
+ active = models.BooleanField(default=True)
+
+ def get_absolute_url(self):
+ return reverse('releng-results-iso', args=[self.pk])
+
+ def __unicode__(self):
+ return self.name
+
+ class Meta:
+ verbose_name = 'ISO'
+
+
+class Architecture(IsoOption):
+ pass
+
+
+class IsoType(IsoOption):
+ class Meta:
+ verbose_name = 'ISO type'
+
+
+class BootType(IsoOption):
+ pass
+
+
+class HardwareType(IsoOption):
+ pass
+
+
+class InstallType(IsoOption):
+ pass
+
+
+class Source(IsoOption):
+ pass
+
+
+class ClockChoice(IsoOption):
+ pass
+
+
+class Filesystem(RollbackOption):
+ pass
+
+
+class Module(RollbackOption):
+ pass
+
+
+class Bootloader(IsoOption):
+ pass
+
+
+class Test(models.Model):
+ user_name = models.CharField(max_length=500)
+ user_email = models.EmailField('email address')
+ ip_address = models.GenericIPAddressField('IP address', unpack_ipv4=True)
+ created = models.DateTimeField(editable=False)
+
+ iso = models.ForeignKey(Iso)
+ architecture = models.ForeignKey(Architecture)
+ iso_type = models.ForeignKey(IsoType)
+ boot_type = models.ForeignKey(BootType)
+ hardware_type = models.ForeignKey(HardwareType)
+ install_type = models.ForeignKey(InstallType)
+ source = models.ForeignKey(Source)
+ clock_choice = models.ForeignKey(ClockChoice)
+ filesystem = models.ForeignKey(Filesystem)
+ modules = models.ManyToManyField(Module, null=True, blank=True)
+ bootloader = models.ForeignKey(Bootloader)
+ rollback_filesystem = models.ForeignKey(Filesystem,
+ related_name="rollback_test_set", null=True, blank=True)
+ rollback_modules = models.ManyToManyField(Module,
+ related_name="rollback_test_set", null=True, blank=True)
+
+ success = models.BooleanField(default=True)
+ comments = models.TextField(null=True, blank=True)
+
+
+class Release(models.Model):
+ release_date = models.DateField(db_index=True)
+ version = models.CharField(max_length=50, unique=True)
+ kernel_version = models.CharField(max_length=50, blank=True)
+ md5_sum = models.CharField('MD5 digest', max_length=32, blank=True)
+ sha1_sum = models.CharField('SHA1 digest', max_length=40, blank=True)
+ created = models.DateTimeField(editable=False)
+ last_modified = models.DateTimeField(editable=False)
+ available = models.BooleanField(default=True)
+ info = models.TextField('Public information', blank=True)
+ torrent_data = models.TextField(blank=True,
+ help_text="base64-encoded torrent file")
+
+ class Meta:
+ get_latest_by = 'release_date'
+ ordering = ('-release_date', '-version')
+
+ def __unicode__(self):
+ return self.version
+
+ def get_absolute_url(self):
+ return reverse('releng-release-detail', args=[self.version])
+
+ def dir_path(self):
+ return "iso/%s/" % self.version
+
+ def iso_url(self):
+ return "iso/%s/%s-%s-dual.iso" % (self.version, settings.BRANDING_SLUG, self.version)
+
+ def magnet_uri(self):
+ query = [
+ ('dn', "%s-%s-dual.iso" % (settings.BRANDING_SLUG, self.version)),
+ ]
+ if settings.TORRENT_TRACKERS:
+ query.extend(('tr', uri) for uri in settings.TORRENT_TRACKERS)
+ metadata = self.torrent()
+ if metadata and 'info_hash' in metadata:
+ query.insert(0, ('xt', "urn:btih:%s" % metadata['info_hash']))
+ return "magnet:?%s" % '&'.join(['%s=%s' % (k, v) for k, v in query])
+
+ def info_html(self):
+ return mark_safe(parse_markdown(self.info))
+
+ def torrent(self):
+ try:
+ data = b64decode(self.torrent_data.encode('utf-8'))
+ except TypeError:
+ return None
+ if not data:
+ return None
+ data = bdecode(data)
+ # transform the data into a template-friendly dict
+ info = data.get('info', {})
+ metadata = {
+ 'comment': data.get('comment', None),
+ 'created_by': data.get('created by', None),
+ 'creation_date': None,
+ 'announce': data.get('announce', None),
+ 'file_name': info.get('name', None),
+ 'file_length': info.get('length', None),
+ 'piece_count': len(info.get('pieces', '')) / 20,
+ 'piece_length': info.get('piece length', None),
+ 'url_list': data.get('url-list', []),
+ 'info_hash': None,
+ }
+ if 'creation date' in data:
+ created= datetime.utcfromtimestamp(data['creation date'])
+ metadata['creation_date'] = created.replace(tzinfo=utc)
+ if info:
+ metadata['info_hash'] = hashlib.sha1(bencode(info)).hexdigest()
+
+ return metadata
+
+
+for model in (Iso, Test, Release):
+ pre_save.connect(set_created_field, sender=model,
+ dispatch_uid="releng.models")
+
+# vim: set ts=4 sw=4 et:
diff --git a/releng/urls.py b/releng/urls.py
new file mode 100644
index 00000000..ca76eb25
--- /dev/null
+++ b/releng/urls.py
@@ -0,0 +1,30 @@
+from django.conf.urls import include, patterns
+
+from .views import ReleaseListView, ReleaseDetailView
+
+feedback_patterns = patterns('releng.views',
+ (r'^$', 'test_results_overview', {}, 'releng-test-overview'),
+ (r'^submit/$', 'submit_test_result', {}, 'releng-test-submit'),
+ (r'^thanks/$', 'submit_test_thanks', {}, 'releng-test-thanks'),
+ (r'^iso/(?P<iso_id>\d+)/$', 'test_results_iso', {}, 'releng-results-iso'),
+ (r'^(?P<option>.+)/(?P<value>\d+)/$','test_results_for', {}, 'releng-results-for'),
+ (r'^iso/overview/$', 'iso_overview', {}, 'releng-iso-overview'),
+)
+
+releases_patterns = patterns('releng.views',
+ (r'^$',
+ ReleaseListView.as_view(), {}, 'releng-release-list'),
+ (r'^json/$',
+ 'releases_json', {}, 'releng-release-list-json'),
+ (r'^(?P<version>[-.\w]+)/$',
+ ReleaseDetailView.as_view(), {}, 'releng-release-detail'),
+ (r'^(?P<version>[-.\w]+)/torrent/$',
+ 'release_torrent', {}, 'releng-release-torrent'),
+)
+
+urlpatterns = patterns('',
+ (r'^feedback/', include(feedback_patterns)),
+ (r'^releases/', include(releases_patterns)),
+)
+
+# vim: set ts=4 sw=4 et:
diff --git a/releng/views.py b/releng/views.py
new file mode 100644
index 00000000..0fb55b29
--- /dev/null
+++ b/releng/views.py
@@ -0,0 +1,283 @@
+from base64 import b64decode
+import json
+
+from django import forms
+from django.conf import settings
+from django.core.serializers.json import DjangoJSONEncoder
+from django.core.urlresolvers import reverse
+from django.db.models import Count, Max
+from django.http import Http404, HttpResponse
+from django.shortcuts import get_object_or_404, redirect, render
+from django.views.generic import DetailView, ListView
+
+from .models import (Architecture, BootType, Bootloader, ClockChoice,
+ Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source,
+ Test, Release)
+
+
+def standard_field(model, empty_label=None, help_text=None, required=True):
+ return forms.ModelChoiceField(queryset=model.objects.all(),
+ widget=forms.RadioSelect(), empty_label=empty_label,
+ help_text=help_text, required=required)
+
+
+class TestForm(forms.ModelForm):
+ iso = forms.ModelChoiceField(queryset=Iso.objects.filter(
+ active=True).order_by('-id'))
+ architecture = standard_field(Architecture)
+ iso_type = standard_field(IsoType)
+ boot_type = standard_field(BootType)
+ hardware_type = standard_field(HardwareType)
+ install_type = standard_field(InstallType)
+ source = standard_field(Source)
+ clock_choice = standard_field(ClockChoice)
+ filesystem = standard_field(Filesystem,
+ help_text="Verify /etc/fstab, `df -hT` output and commands like "
+ "lvdisplay for special modules.")
+ modules = forms.ModelMultipleChoiceField(queryset=Module.objects.all(),
+ widget=forms.CheckboxSelectMultiple(), required=False)
+ bootloader = standard_field(Bootloader,
+ help_text="Verify that the entries in the bootloader config "
+ "looks OK.")
+ rollback_filesystem = standard_field(Filesystem,
+ help_text="If you did a rollback followed by a new attempt to "
+ "setup your blockdevices/filesystems, select which option you "
+ "took here.",
+ empty_label="N/A (did not rollback)", required=False)
+ rollback_modules = forms.ModelMultipleChoiceField(
+ queryset=Module.objects.all(),
+ help_text="If you did a rollback followed by a new attempt to "
+ "setup your blockdevices/filesystems, select which option you "
+ "took here.",
+ widget=forms.CheckboxSelectMultiple(), required=False)
+ success = forms.BooleanField(
+ help_text="Only check this if everything went fine. "
+ "If you ran into problems please create a ticket on <a "
+ "href=\""+settings.BUGTRACKER_RELENG_URL+"\">the "
+ "bugtracker</a> (or check that one already exists) and link to "
+ "it in the comments.",
+ required=False)
+ website = forms.CharField(label='',
+ widget=forms.TextInput(attrs={'style': 'display:none;'}),
+ required=False)
+
+ class Meta:
+ model = Test
+ fields = ("user_name", "user_email", "iso", "architecture",
+ "iso_type", "boot_type", "hardware_type",
+ "install_type", "source", "clock_choice", "filesystem",
+ "modules", "bootloader", "rollback_filesystem",
+ "rollback_modules", "success", "comments")
+ widgets = {
+ "modules": forms.CheckboxSelectMultiple(),
+ }
+
+
+def submit_test_result(request):
+ if request.POST:
+ form = TestForm(request.POST)
+ if form.is_valid() and request.POST['website'] == '':
+ test = form.save(commit=False)
+ test.ip_address = request.META.get("REMOTE_ADDR", None)
+ test.save()
+ form.save_m2m()
+ return redirect('releng-test-thanks')
+ else:
+ form = TestForm()
+
+ context = {'form': form}
+ return render(request, 'releng/add.html', context)
+
+
+def calculate_option_overview(field_name):
+ field = Test._meta.get_field(field_name)
+ model = field.rel.to
+ is_rollback = field_name.startswith('rollback_')
+ option = {
+ 'option': model,
+ 'field_name': field_name,
+ 'name': model._meta.verbose_name,
+ 'is_rollback': is_rollback,
+ 'values': []
+ }
+ if not is_rollback:
+ successes = dict(model.objects.values_list('pk').filter(
+ test__success=True).annotate(latest=Max('test__iso__id')))
+ failures = dict(model.objects.values_list('pk').filter(
+ test__success=False).annotate(latest=Max('test__iso__id')))
+ else:
+ successes = dict(model.objects.values_list('pk').filter(
+ rollback_test_set__success=True).annotate(
+ latest=Max('rollback_test_set__iso__id')))
+ failures = dict(model.objects.values_list('pk').filter(
+ rollback_test_set__success=False).annotate(
+ latest=Max('rollback_test_set__iso__id')))
+
+ for value in model.objects.all():
+ data = {
+ 'value': value,
+ 'success': successes.get(value.pk),
+ 'failure': failures.get(value.pk),
+ }
+ option['values'].append(data)
+
+ return option
+
+
+def options_fetch_iso(options):
+ '''Replaces the Iso PK with a full Iso model object in a list of options
+ used on the overview page. We do it this way to only have to query the Iso
+ table once rather than once per option.'''
+ # collect all necessary Iso PKs
+ all_pks = set()
+ for option in options:
+ all_pks.update(v['success'] for v in option['values'])
+ all_pks.update(v['failure'] for v in option['values'])
+
+ all_pks.discard(None)
+ all_isos = Iso.objects.in_bulk(all_pks)
+
+ for option in options:
+ for value in option['values']:
+ value['success'] = all_isos.get(value['success'])
+ value['failure'] = all_isos.get(value['failure'])
+
+ return options
+
+
+def test_results_overview(request):
+ # data structure produced:
+ # [ {
+ # option, name, is_rollback,
+ # values: [ { value, success, failure } ... ]
+ # }
+ # ...
+ # ]
+ all_options = []
+ fields = ['architecture', 'iso_type', 'boot_type', 'hardware_type',
+ 'install_type', 'source', 'clock_choice', 'filesystem', 'modules',
+ 'bootloader', 'rollback_filesystem', 'rollback_modules']
+ for field in fields:
+ all_options.append(calculate_option_overview(field))
+
+ all_options = options_fetch_iso(all_options)
+
+ context = {
+ 'options': all_options,
+ 'iso_url': settings.ISO_LIST_URL,
+ }
+ return render(request, 'releng/results.html', context)
+
+
+def test_results_iso(request, iso_id):
+ iso = get_object_or_404(Iso, pk=iso_id)
+ test_list = iso.test_set.select_related()
+ context = {
+ 'iso_name': iso.name,
+ 'test_list': test_list
+ }
+ return render(request, 'releng/result_list.html', context)
+
+
+def test_results_for(request, option, value):
+ if option not in Test._meta.get_all_field_names():
+ raise Http404
+ option_model = getattr(Test, option).field.rel.to
+ option_model.verbose_name = option_model._meta.verbose_name
+ real_value = get_object_or_404(option_model, pk=value)
+ test_list = real_value.test_set.select_related().order_by(
+ '-iso__name', '-pk')
+ context = {
+ 'option': option_model,
+ 'value': real_value,
+ 'value_id': value,
+ 'test_list': test_list
+ }
+ return render(request, 'releng/result_list.html', context)
+
+
+def submit_test_thanks(request):
+ return render(request, "releng/thanks.html", None)
+
+
+def iso_overview(request):
+ isos = Iso.objects.all().order_by('-pk')
+ successes = dict(Iso.objects.values_list('pk').filter(
+ test__success=True).annotate(ct=Count('test')))
+ failures = dict(Iso.objects.values_list('pk').filter(
+ test__success=False).annotate(ct=Count('test')))
+ for iso in isos:
+ iso.successes = successes.get(iso.pk, 0)
+ iso.failures = failures.get(iso.pk, 0)
+
+ # only show "useful" rows, currently active ISOs or those with results
+ isos = [iso for iso in isos if
+ iso.active is True or iso.successes > 0 or iso.failures > 0]
+
+ context = {
+ 'isos': isos
+ }
+ return render(request, 'releng/iso_overview.html', context)
+
+
+class ReleaseListView(ListView):
+ model = Release
+
+
+class ReleaseDetailView(DetailView):
+ model = Release
+ slug_field = 'version'
+ slug_url_kwarg = 'version'
+
+
+def release_torrent(request, version):
+ release = get_object_or_404(Release, version=version)
+ if not release.torrent_data:
+ raise Http404
+ data = b64decode(release.torrent_data.encode('utf-8'))
+ response = HttpResponse(data, content_type='application/x-bittorrent')
+ # TODO: this is duplicated from Release.iso_url()
+ filename = '%s-%s-dual.iso.torrent' % (settings.BRANDING_SLUG, release.version)
+ response['Content-Disposition'] = 'attachment; filename=%s' % filename
+ return response
+
+
+class ReleaseJSONEncoder(DjangoJSONEncoder):
+ release_attributes = ('release_date', 'version', 'kernel_version',
+ 'created', 'md5_sum', 'sha1_sum')
+
+ def default(self, obj):
+ if isinstance(obj, Release):
+ data = {attr: getattr(obj, attr) or None
+ for attr in self.release_attributes}
+ data['available'] = obj.available
+ data['iso_url'] = '/' + obj.iso_url()
+ data['magnet_uri'] = obj.magnet_uri()
+ data['torrent_url'] = reverse('releng-release-torrent', args=[obj.version])
+ data['info'] = obj.info_html()
+ torrent_data = obj.torrent()
+ if torrent_data:
+ torrent_data.pop('url_list', None)
+ data['torrent'] = torrent_data
+ return data
+ return super(ReleaseJSONEncoder, self).default(obj)
+
+
+def releases_json(request):
+ releases = Release.objects.all()
+ try:
+ latest_version = Release.objects.filter(available=True).values_list(
+ 'version', flat=True).latest()
+ except Release.DoesNotExist:
+ latest_version = None
+
+ data = {
+ 'version': 1,
+ 'releases': list(releases),
+ 'latest_version': latest_version,
+ }
+ to_json = json.dumps(data, ensure_ascii=False, cls=ReleaseJSONEncoder)
+ response = HttpResponse(to_json, content_type='application/json')
+ return response
+
+# vim: set ts=4 sw=4 et: