Merge branch 'bump-bonanza-and-python3'
This commit is contained in:
commit
8946234b10
29 files changed
+3585
-479
No files matched your search
+2
-2
@@ -2,7 +2,7 @@
|
||||
*.swp
|
||||
temp
|
||||
*.log
|
||||
settings.py
|
||||
dns_mdb.db
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
/.idea
|
||||
/venv
|
||||
@@ -1,17 +0,0 @@
|
||||
A Django app for managing computer equipment.
|
||||
Generates DHCP configs and zone files.
|
||||
|
||||
Dependencies:
|
||||
sudo apt-get install libmysqlclient-dev python-dev libldap2-dev libsasl2-dev bind9utils
|
||||
|
||||
Setup environment:
|
||||
mdb$ virtualenv venv
|
||||
mdb$ . venv/bin/activate
|
||||
(venv)mdb$ pip install -r requirements.txt
|
||||
|
||||
Create a config file:
|
||||
mdb$ cp mdb/settings-sample.py mdb/settings.py
|
||||
# Edit settings.py with your favorite editor
|
||||
|
||||
Run the server:
|
||||
(venv)mdb$ python manage.py runserver
|
||||
@@ -0,0 +1,21 @@
|
||||
A Django app for managing computer equipment.
|
||||
|
||||
## Installation
|
||||
sudo apt install libmysqlclient-dev python-dev libldap2-dev libsasl2-dev bind9utils
|
||||
virtualenv venv
|
||||
. venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python manage.py migrate
|
||||
python manage.py runserver
|
||||
|
||||
## Development tasks
|
||||
Dump test data:
|
||||
|
||||
python manage.py dumpdata mdb --natural-foreign --indent=4 > mdb/fixtures/test_data.json
|
||||
|
||||
## Features
|
||||
|
||||
* Creates DHCPD configuration files
|
||||
* Creates DNS zone files for BIND
|
||||
* Creates PXE config files (with random secret for use with Puppet autosign)
|
||||
* Partial IPv6 support
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
import os, sys
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mdb.settings")
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mdbsite.settings")
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
@@ -0,0 +1 @@
|
||||
default_app_config = 'mdb.apps.MdbAppConfig'
|
||||
+29
-6
@@ -1,5 +1,8 @@
|
||||
from mdb.models import *
|
||||
from django.contrib import admin
|
||||
from django.contrib import admin, messages
|
||||
|
||||
from mdb.models import Ip6Address, Interface, Ip4Address, DhcpOption, DhcpCustomField, DomainSrvRecord, DomainTxtRecord, \
|
||||
DomainCnameRecord, DomainARecord, Domain, Host, Ip4Subnet, Ip6Subnet, Nameserver, MailExchange, OperatingSystem, \
|
||||
HostType, DhcpConfig
|
||||
|
||||
|
||||
class Ip6AddressInline(admin.TabularInline):
|
||||
@@ -20,17 +23,17 @@ class HostAdmin(admin.ModelAdmin):
|
||||
ordering = ('hostname',)
|
||||
inlines = [InterfaceInline]
|
||||
list_display = ['hostname', 'owner', 'host_type', 'location', 'mac_addresses', 'ip_addresses', 'in_domain',
|
||||
'ipv6_enabled']
|
||||
list_filter = ['host_type', 'owner', 'location']
|
||||
'pxe_installable', 'ipv6_enabled']
|
||||
list_filter = ['host_type', 'owner', 'location', 'pxe_installable']
|
||||
readonly_fields = ['kerberos_principal_name', 'kerberos_principal_created_date',
|
||||
'kerberos_principal_created']
|
||||
search_fields = ['hostname', 'location', 'interface__macaddr', 'interface__ip4address__address']
|
||||
fieldsets = (
|
||||
('Owner Information', {
|
||||
'fields': ('owner', 'location', 'description' )
|
||||
'fields': ('owner', 'location', 'description')
|
||||
}),
|
||||
('Hardware and Software Information', {
|
||||
'fields': (('brand', 'model', 'serial_number'), ('hostname', 'host_type'), ('operating_system', 'virtual'))
|
||||
'fields': (('hostname', 'host_type'), ('pxe_key', 'pxe_installable'), ('brand', 'model'), 'serial_number', ('operating_system', 'virtual'))
|
||||
}),
|
||||
# FIXME: Not in use
|
||||
# ('Domain and Kerberos Information', {
|
||||
@@ -40,6 +43,26 @@ class HostAdmin(admin.ModelAdmin):
|
||||
# ('kerberos_principal_name', 'kerberos_principal_created_date'))
|
||||
# }),
|
||||
)
|
||||
actions = ['set_installable']
|
||||
|
||||
def _get_host_warning_message(self, host, ifs):
|
||||
ifs_str = ', '.join([_if.name for _if in ifs])
|
||||
return 'Host {} might not be installable since interface(s) {} has no IP set.'.format(host, ifs_str)
|
||||
|
||||
def set_installable(self, request, queryset):
|
||||
invalid_ifs_per_host = []
|
||||
|
||||
for obj in queryset:
|
||||
invalid_ifs = obj.interface_set.filter(ip4address__isnull=True)
|
||||
if invalid_ifs.exists():
|
||||
invalid_ifs_per_host.append(self._get_host_warning_message(obj, invalid_ifs))
|
||||
obj.pxe_installable = True
|
||||
obj.save() # triggers post save signal
|
||||
|
||||
if invalid_ifs_per_host:
|
||||
self.message_user(request, message='\n'.join(invalid_ifs_per_host), level=messages.WARNING)
|
||||
|
||||
set_installable.short_description = "Mark selected hosts as PXE installable"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super(HostAdmin, self).get_queryset(request).select_related()
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,46 @@
|
||||
from mdb.models import Host
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
class HostPXEValidateSerializer(serializers.Serializer):
|
||||
certname = serializers.CharField(required=True)
|
||||
pxe_key = serializers.CharField(required=True)
|
||||
host = None
|
||||
|
||||
def _get_host_by_fqdn(self, attrs):
|
||||
fqdn = attrs['certname']
|
||||
host_name = fqdn.split('.')[0]
|
||||
domain = '.'.join(fqdn.split('.')[1:])
|
||||
|
||||
try:
|
||||
return Host.objects.get(hostname=host_name, interface__domain__domain_name=domain)
|
||||
except (Host.DoesNotExist, Host.MultipleObjectsReturned):
|
||||
return None
|
||||
|
||||
def validate(self, attrs):
|
||||
# custom validation
|
||||
host = self._get_host_by_fqdn(attrs)
|
||||
|
||||
if host is None:
|
||||
raise ValidationError('Host \'{}\' not found.'.format(attrs['certname']))
|
||||
|
||||
if not host.pxe_installable:
|
||||
raise ValidationError('Host \'{}\' is marked as not installable via PXE.'.format(attrs['certname']))
|
||||
|
||||
if not host.pxe_key:
|
||||
raise ValidationError('Host \'{}\' has no pxe_key.'.format(attrs['certname']))
|
||||
|
||||
if host.pxe_key != attrs['pxe_key']:
|
||||
raise ValidationError('Supplied pxe_key does not match host \'{}\'.'.format(attrs['certname']))
|
||||
|
||||
self.host = host
|
||||
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
# Make requests kind of idempotent by only allowing 1 request / installation
|
||||
self.host.pxe_installable = False
|
||||
self.host.save()
|
||||
|
||||
return self.host
|
||||
@@ -0,0 +1,20 @@
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from mdb.api.serializers import HostPXEValidateSerializer
|
||||
|
||||
|
||||
class HostPXEValidate(APIView):
|
||||
authentication_classes = [TokenAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
serializer = HostPXEValidateSerializer(data=request.data)
|
||||
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
host = serializer.save() # sets Host.pxe_installable=False
|
||||
|
||||
return Response({'valid_pxe_key': True})
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MdbAppConfig(AppConfig):
|
||||
name = 'mdb'
|
||||
verbose_name = 'Machine Database'
|
||||
|
||||
def ready(self):
|
||||
# all models are loaded, now attach signals
|
||||
import mdb.signals
|
||||
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,11 @@ from difflib import context_diff
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.mail import mail_admins
|
||||
from optparse import make_option
|
||||
from commands import getstatusoutput
|
||||
try:
|
||||
from subprocess import getstatusoutput
|
||||
except ImportError:
|
||||
from commands import getstatusoutput
|
||||
|
||||
from mdb.models import DhcpConfig
|
||||
|
||||
|
||||
@@ -42,13 +46,13 @@ class Command(BaseCommand):
|
||||
|
||||
# do we need to do anything?
|
||||
if config.serial == config.active_serial and not force:
|
||||
print "Nothing to do, serial is the same: %s" % config.serial
|
||||
print("Nothing to do, serial is the same: %s" % config.serial)
|
||||
sys.exit(0)
|
||||
|
||||
# write to stdout
|
||||
if output == sys.stdout:
|
||||
print config.dhcpd_configuration()
|
||||
print 'Not writing to file, serial not updated'
|
||||
print(config.dhcpd_configuration())
|
||||
print('Not writing to file, serial not updated')
|
||||
return
|
||||
|
||||
# read current file contents
|
||||
@@ -71,7 +75,7 @@ class Command(BaseCommand):
|
||||
tofile=config.serial,
|
||||
lineterm=''))
|
||||
|
||||
print diff
|
||||
print(diff)
|
||||
|
||||
# update serial
|
||||
old_serial = config.active_serial
|
||||
|
||||
@@ -7,8 +7,12 @@ from difflib import context_diff
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.mail import mail_admins
|
||||
from optparse import make_option
|
||||
from commands import getstatusoutput
|
||||
from mdb.models import *
|
||||
try:
|
||||
from subprocess import getstatusoutput
|
||||
except ImportError:
|
||||
from commands import getstatusoutput
|
||||
|
||||
from mdb.models import Domain, Ip4Subnet, Ip6Subnet
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -43,7 +47,7 @@ class Command(BaseCommand):
|
||||
# do the binaries exist?
|
||||
for f in (self.checkzone_bin, self.bind_bin):
|
||||
if not os.path.isfile(f):
|
||||
print "ERROR: no such file %s, exiting..." % f
|
||||
print("ERROR: no such file %s, exiting..." % f)
|
||||
if not debug:
|
||||
sys.exit(1)
|
||||
|
||||
@@ -106,11 +110,11 @@ class Command(BaseCommand):
|
||||
'diff': None,
|
||||
}
|
||||
|
||||
print "updating %s %s [%d -> %d]" % (
|
||||
print("updating %s %s [%d -> %d]" % (
|
||||
zonetype,
|
||||
zone.domain_name,
|
||||
zone.domain_active_serial,
|
||||
zone.domain_serial)
|
||||
zone.domain_serial))
|
||||
|
||||
# generate zone file contents
|
||||
new = zone.zone_file_contents()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.core.validators
|
||||
import re
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mdb', '0005_auto_20150822_2204'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='dhcpcustomfield',
|
||||
options={'verbose_name': 'DHCP custom field'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='domain',
|
||||
name='domain_admin',
|
||||
field=models.EmailField(max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='domain',
|
||||
name='domain_ipaddr',
|
||||
field=models.GenericIPAddressField(protocol='IPv4'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='host',
|
||||
name='hostname',
|
||||
field=models.CharField(validators=[django.core.validators.RegexValidator(code='invalid', message='Enter a valid hostname', regex=re.compile('^(?!-)[-a-z0-9]+(?<!-)$', 34))], max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='ip4address',
|
||||
field=models.OneToOneField(to='mdb.Ip4Address', null=True, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='macaddr',
|
||||
field=models.CharField(validators=[django.core.validators.RegexValidator(code='invalid', message='Enter a valid MAC address', regex=re.compile('^([0-9A-F]{2}[:]){5}([0-9A-F]{2})$', 34))], max_length=17),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ip4address',
|
||||
name='address',
|
||||
field=models.GenericIPAddressField(protocol='IPv4'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ip4subnet',
|
||||
name='dhcp_dynamic_end',
|
||||
field=models.GenericIPAddressField(protocol='IPv4', null=True, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ip4subnet',
|
||||
name='dhcp_dynamic_start',
|
||||
field=models.GenericIPAddressField(protocol='IPv4', null=True, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ip4subnet',
|
||||
name='domain_admin',
|
||||
field=models.EmailField(max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ip4subnet',
|
||||
name='netmask',
|
||||
field=models.GenericIPAddressField(protocol='IPv4'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ip4subnet',
|
||||
name='network',
|
||||
field=models.GenericIPAddressField(protocol='IPv4'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ip6subnet',
|
||||
name='domain_admin',
|
||||
field=models.EmailField(max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='operatingsystem',
|
||||
name='arch',
|
||||
field=models.CharField(max_length=255, choices=[('mips', 'MIPS'), ('arm', 'ARM'), ('broadcom', 'Broadcom'), ('rc32300', 'RC32300'), ('powerpc', 'PowerPC'), ('powerpc403ga', 'PowerPC403GA'), ('unknown', 'Unknown'), ('x86_64', 'x86-64'), ('i386', 'x86')], null=True, blank=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mdb', '0006_auto_20151001_2235'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='host',
|
||||
name='pxe_installable',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='host',
|
||||
name='pxe_key',
|
||||
field=models.CharField(max_length=254, blank=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mdb', '0007_auto_20151003_1313'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ip6address',
|
||||
name='address',
|
||||
field=models.GenericIPAddressField(protocol='IPv6'),
|
||||
),
|
||||
]
|
||||
+118
-216
@@ -1,32 +1,33 @@
|
||||
from django.db import models
|
||||
from django.db.models import Count
|
||||
from django.db.models.signals import post_save, pre_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from mdb.utils import host_as_pxe_files
|
||||
|
||||
from mdb.validators import validate_hostname, validate_macaddr
|
||||
|
||||
from validators import validate_hostname, validate_macaddr
|
||||
|
||||
import ipaddr
|
||||
import ipaddress
|
||||
import datetime
|
||||
import re
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Nameserver(models.Model):
|
||||
hostname = models.CharField(max_length=256)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.hostname
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class MailExchange(models.Model):
|
||||
priority = models.IntegerField()
|
||||
hostname = models.CharField(max_length=256)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return "(" + str(self.priority) + ") " + self.hostname
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Domain(models.Model):
|
||||
domain_name = models.CharField(max_length=256)
|
||||
domain_soa = models.CharField(max_length=256)
|
||||
@@ -38,31 +39,32 @@ class Domain(models.Model):
|
||||
domain_expire = models.IntegerField(default=604800)
|
||||
domain_minimum_ttl = models.IntegerField(default=86400)
|
||||
domain_admin = models.EmailField()
|
||||
domain_ipaddr = models.IPAddressField()
|
||||
domain_ipaddr = models.GenericIPAddressField(protocol='IPv4')
|
||||
domain_filename = models.CharField(max_length=256)
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
domain_nameservers = models.ManyToManyField(Nameserver)
|
||||
domain_mailexchanges = models.ManyToManyField(MailExchange)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.domain_name
|
||||
|
||||
def num_records(self):
|
||||
host_a_records = Host.objects.filter(interface__domain=self).count()
|
||||
size = {
|
||||
"cname": self.domaincnamerecord_set.count(),
|
||||
"srv": self.domainsrvrecord_set.count(),
|
||||
"txt": self.domaintxtrecord_set.count(),
|
||||
"a": self.domainarecord_set.count() + host_a_records
|
||||
"CNAME": self.domaincnamerecord_set.count(),
|
||||
"SRV": self.domainsrvrecord_set.count(),
|
||||
"TXT": self.domaintxtrecord_set.count(),
|
||||
"A": self.domainarecord_set.count() + host_a_records
|
||||
}
|
||||
return size
|
||||
return ', '.join(['{}: {}'.format(k, v) for k, v in size.items()])
|
||||
|
||||
num_records.short_description = "Num Records"
|
||||
|
||||
def __eq__(self, other):
|
||||
if not other or not hasattr(other, 'domain_name'):
|
||||
return False
|
||||
|
||||
return self.domain_name == other.domain_name
|
||||
|
||||
def zone_file_contents(self):
|
||||
@@ -93,12 +95,12 @@ class Domain(models.Model):
|
||||
content += "; SRV records\n"
|
||||
|
||||
for srv in self.domainsrvrecord_set.all():
|
||||
content += unicode(srv) + "\n"
|
||||
content += srv.as_record() + "\n"
|
||||
|
||||
content += "; A records\n"
|
||||
|
||||
for a in self.domainarecord_set.all():
|
||||
content += unicode(a) + "\n"
|
||||
content += a.as_record() + "\n"
|
||||
|
||||
content += "; CNAME records \n"
|
||||
|
||||
@@ -108,7 +110,7 @@ class Domain(models.Model):
|
||||
content += "; TXT records \n"
|
||||
|
||||
for txt in self.domaintxtrecord_set.all():
|
||||
content += unicode(txt) + "\n"
|
||||
content += txt.as_record() + "\n"
|
||||
|
||||
content += "; HOST records\n"
|
||||
|
||||
@@ -126,6 +128,7 @@ class Domain(models.Model):
|
||||
return content
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DomainSrvRecord(models.Model):
|
||||
srvce = models.CharField(max_length=64)
|
||||
prot = models.CharField(max_length=64)
|
||||
@@ -137,42 +140,65 @@ class DomainSrvRecord(models.Model):
|
||||
domain = models.ForeignKey(Domain)
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.srvce + "." + self.prot + "." + self.name + ". IN SRV " \
|
||||
+ str(self.priority) + " " + str(self.weight) + " " + \
|
||||
str(self.port) + " " + self.target + "."
|
||||
def as_record(self):
|
||||
return '{}.{}.{}. IN SRV {} {} {} {}.'.format(
|
||||
self.srvce,
|
||||
self.prot,
|
||||
self.name,
|
||||
|
||||
self.priority,
|
||||
self.weight,
|
||||
self.port,
|
||||
self.target
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.as_record()
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DomainTxtRecord(models.Model):
|
||||
name = models.CharField(max_length=256)
|
||||
target = models.CharField(max_length=256)
|
||||
domain = models.ForeignKey(Domain)
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name + " TXT " + self.target
|
||||
def as_record(self):
|
||||
return '{} TXT {}'.format(self.name, self.target)
|
||||
|
||||
def __str__(self):
|
||||
return self.as_record()
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DomainCnameRecord(models.Model):
|
||||
name = models.CharField(max_length=256)
|
||||
target = models.CharField(max_length=256)
|
||||
domain = models.ForeignKey(Domain)
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name + " IN CNAME " + self.target
|
||||
def as_record(self):
|
||||
return '{} IN CNAME {}'.format(self.name, self.target)
|
||||
|
||||
def __str__(self):
|
||||
return self.as_record()
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DomainARecord(models.Model):
|
||||
name = models.CharField(max_length=256)
|
||||
target = models.CharField(max_length=256)
|
||||
domain = models.ForeignKey(Domain)
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name + " IN A " + self.target
|
||||
def as_record(self):
|
||||
return '{} IN A {}'.format(self.name, self.target)
|
||||
|
||||
def __str__(self):
|
||||
return self.as_record()
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DhcpConfig(models.Model):
|
||||
serial = models.IntegerField()
|
||||
active_serial = models.IntegerField()
|
||||
@@ -214,28 +240,27 @@ class DhcpConfig(models.Model):
|
||||
# time to write host definitions
|
||||
for subnet in self.ip4subnet_set.all():
|
||||
for ip4address in subnet.ip4address_set.all():
|
||||
if ip4address.interface_set.count() == 0:
|
||||
if not hasattr(ip4address, 'interface') or not ip4address.interface.dhcp_client:
|
||||
continue
|
||||
interface = ip4address.interface_set.get()
|
||||
if not interface.dhcp_client:
|
||||
continue
|
||||
content += "\nhost %s {\n" % interface.host.hostname
|
||||
content += "\thardware ethernet %s;\n" % interface.macaddr
|
||||
_if = ip4address.interface
|
||||
content += "\nhost %s {\n" % _if.host.hostname
|
||||
content += "\thardware ethernet %s;\n" % _if.macaddr
|
||||
content += "\tfixed-address %s.%s;\n" % \
|
||||
(interface.host.hostname, interface.domain.domain_name)
|
||||
if len(interface.pxe_filename) > 0:
|
||||
content += "\tfilename \"%s\";\n" % interface.pxe_filename
|
||||
(_if.host.hostname, _if.domain.domain_name)
|
||||
if len(_if.pxe_filename) > 0:
|
||||
content += "\tfilename \"%s\";\n" % _if.pxe_filename
|
||||
content += "}\n"
|
||||
|
||||
return content
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'DHCP config'
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Ip6Subnet(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
network = models.CharField(max_length=255)
|
||||
@@ -254,7 +279,7 @@ class Ip6Subnet(models.Model):
|
||||
domain_admin = models.EmailField()
|
||||
domain_filename = models.CharField(max_length=256)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.network + " (" + self.name + ")"
|
||||
|
||||
def zone_file_contents(self, generate_unassigned=False):
|
||||
@@ -292,7 +317,7 @@ class Ip6Subnet(models.Model):
|
||||
addr.interface.host.hostname,
|
||||
addr.interface.domain.domain_name)
|
||||
|
||||
ip = ipaddr.IPv6Address(self.network + addr.address)
|
||||
ip = ipaddress.IPv6Address(self.network + addr.address)
|
||||
ip = ".".join(ip.exploded.replace(":", "")[16:])[::-1]
|
||||
content += "%s\tPTR\t%s.\n" % (ip, hostname)
|
||||
|
||||
@@ -305,10 +330,11 @@ class Ip6Subnet(models.Model):
|
||||
verbose_name = 'IPv6 subnet'
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Ip4Subnet(models.Model):
|
||||
name = models.CharField(max_length=256)
|
||||
netmask = models.IPAddressField()
|
||||
network = models.IPAddressField()
|
||||
netmask = models.GenericIPAddressField(protocol='IPv4')
|
||||
network = models.GenericIPAddressField(protocol='IPv4')
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
domain_name = models.CharField(max_length=255, editable=False)
|
||||
domain_nameservers = models.ManyToManyField(Nameserver)
|
||||
@@ -324,31 +350,28 @@ class Ip4Subnet(models.Model):
|
||||
domain_filename = models.CharField(max_length=256)
|
||||
|
||||
dhcp_dynamic = models.BooleanField(default=False)
|
||||
dhcp_dynamic_start = models.IPAddressField(null=True, blank=True)
|
||||
dhcp_dynamic_end = models.IPAddressField(null=True, blank=True)
|
||||
dhcp_dynamic_start = models.GenericIPAddressField(protocol='IPv4', null=True, blank=True)
|
||||
dhcp_dynamic_end = models.GenericIPAddressField(protocol='IPv4', null=True, blank=True)
|
||||
dhcp_config = models.ForeignKey(DhcpConfig)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.network + " (" + self.name + ")"
|
||||
|
||||
def num_addresses(self):
|
||||
subnet = ipaddr.IPv4Network(self.network + "/" + self.netmask)
|
||||
return subnet.numhosts
|
||||
subnet = ipaddress.IPv4Network(self.network + "/" + self.netmask)
|
||||
return subnet.num_addresses
|
||||
|
||||
def broadcast_address(self):
|
||||
subnet = ipaddr.IPv4Network(self.network + "/" + self.netmask)
|
||||
return subnet.broadcast
|
||||
subnet = ipaddress.IPv4Network(self.network + "/" + self.netmask)
|
||||
return subnet.broadcast_address
|
||||
|
||||
def first_address(self):
|
||||
subnet = ipaddr.IPv4Network(self.network + "/" + self.netmask)
|
||||
return subnet.iterhosts().next()
|
||||
subnet = ipaddress.IPv4Network(self.network + "/" + self.netmask)
|
||||
return next(subnet.hosts())
|
||||
|
||||
def last_address(self):
|
||||
subnet = ipaddr.IPv4Network(self.network + "/" + self.netmask)
|
||||
for curr in subnet.iterhosts():
|
||||
pass # horribly ineficcient
|
||||
|
||||
return curr
|
||||
subnet = ipaddress.IPv4Network(self.network + "/" + self.netmask)
|
||||
return list(subnet.hosts())[-1]
|
||||
|
||||
broadcast_address.short_description = 'broadcast'
|
||||
num_addresses.short_description = '#addresses'
|
||||
@@ -378,26 +401,18 @@ class Ip4Subnet(models.Model):
|
||||
content += ";\n"
|
||||
|
||||
for addr in self.ip4address_set.all():
|
||||
if addr.interface_set.count() == 0 and generate_unassigned:
|
||||
""" Get PTR records for each v4 address in subnet """
|
||||
if hasattr(addr, 'interface') and addr.interface.domain:
|
||||
hostname = "%s.%s" % (
|
||||
addr.interface.host.hostname,
|
||||
addr.interface.domain.domain_name)
|
||||
content += "%-20s\tIN\tPTR\t%s.\n" % \
|
||||
(addr.address.split(".")[3], hostname)
|
||||
elif not hasattr(addr, 'interface') and generate_unassigned:
|
||||
content += "%s\tIN\tPTR\t%s.%s\n" % (
|
||||
addr.address,
|
||||
addr.address.split(".")[3],
|
||||
"dhcp.neuf.no.")
|
||||
continue
|
||||
|
||||
for interface in addr.interface_set.all():
|
||||
if interface.domain is None and generate_unassigned:
|
||||
content += "%s\tIN\tPTR\t%s.%s.\n" % (
|
||||
addr.address,
|
||||
addr.address.split(".")[3],
|
||||
"dhcp.neuf.no")
|
||||
|
||||
else:
|
||||
hostname = "%s.%s" % (
|
||||
interface.host.hostname,
|
||||
interface.domain.domain_name)
|
||||
content += "%-20s\tIN\tPTR\t%s.\n" % \
|
||||
(addr.address.split(".")[3], hostname)
|
||||
|
||||
return content
|
||||
|
||||
@@ -405,12 +420,13 @@ class Ip4Subnet(models.Model):
|
||||
verbose_name = 'IPv4 subnet'
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DhcpOption(models.Model):
|
||||
key = models.CharField(max_length=255)
|
||||
value = models.CharField(max_length=255)
|
||||
ip4subnet = models.ForeignKey(Ip4Subnet)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.key + " " + self.value
|
||||
|
||||
class Meta:
|
||||
@@ -425,25 +441,22 @@ class DhcpCustomField(models.Model):
|
||||
verbose_name = 'DHCP custom field'
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Ip4Address(models.Model):
|
||||
subnet = models.ForeignKey(Ip4Subnet)
|
||||
address = models.IPAddressField()
|
||||
address = models.GenericIPAddressField(protocol='IPv4')
|
||||
last_contact = models.DateTimeField(null=True, blank=True)
|
||||
ping_avg_rtt = models.FloatField(null=True, blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
# FIXME: generates 2 SQL queries
|
||||
_if = self.interface_set.all()
|
||||
if not _if:
|
||||
def __str__(self):
|
||||
# FIXME: generates 2 SQL queries?
|
||||
if not hasattr(self, 'interface'):
|
||||
return self.address
|
||||
|
||||
return "{} ({})".format(
|
||||
self.address,
|
||||
_if.get().host.hostname
|
||||
)
|
||||
return "{} ({})".format(self.address, self.interface.host.hostname)
|
||||
|
||||
def assigned_to_host(self):
|
||||
return self.interface_set.get().host
|
||||
return self.interface.host
|
||||
|
||||
assigned_to_host.short_description = "Assigned to Host"
|
||||
|
||||
@@ -452,11 +465,12 @@ class Ip4Address(models.Model):
|
||||
verbose_name_plural = 'IPv4 addresses'
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class HostType(models.Model):
|
||||
host_type = models.CharField(max_length=64)
|
||||
description = models.CharField(max_length=1024)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.host_type
|
||||
|
||||
def num_members(self):
|
||||
@@ -466,6 +480,7 @@ class HostType(models.Model):
|
||||
ordering = ("host_type",)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class OperatingSystem(models.Model):
|
||||
OS_ARCHITECTURES = (
|
||||
('mips', _('MIPS')),
|
||||
@@ -482,13 +497,14 @@ class OperatingSystem(models.Model):
|
||||
version = models.CharField(max_length=64)
|
||||
arch = models.CharField(max_length=255, choices=OS_ARCHITECTURES, blank=True, null=True)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return self.name + " " + self.version + " (" + self.arch + ")"
|
||||
|
||||
class Meta:
|
||||
ordering = ("name", "version")
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Host(models.Model):
|
||||
location = models.CharField(max_length=1024)
|
||||
brand = models.CharField(max_length=1024)
|
||||
@@ -507,12 +523,15 @@ class Host(models.Model):
|
||||
kerberos_principal_name = models.CharField(max_length=256, editable=False)
|
||||
kerberos_principal_created_date = models.DateTimeField(null=True, blank=True, editable=False)
|
||||
|
||||
def __unicode__(self):
|
||||
pxe_key = models.CharField(max_length=254, blank=True)
|
||||
pxe_installable = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.hostname
|
||||
|
||||
def in_domain(self):
|
||||
domains = self.interface_set.values_list('domain__domain_name', flat=True)
|
||||
return u",".join(domains)
|
||||
return ",".join(domains)
|
||||
|
||||
in_domain.short_description = "in domains"
|
||||
|
||||
@@ -524,39 +543,44 @@ class Host(models.Model):
|
||||
|
||||
def mac_addresses(self):
|
||||
addresses = self.interface_set.filter(macaddr__isnull=False).values_list('macaddr', flat=True)
|
||||
return ",".join(addresses)
|
||||
return ", ".join(addresses)
|
||||
|
||||
def ip_addresses(self):
|
||||
addresses = self.interface_set.filter(ip4address__isnull=False).values_list('ip4address__address', flat=True)
|
||||
return ",".join(addresses)
|
||||
return ", ".join(addresses)
|
||||
|
||||
def as_pxe_files(self):
|
||||
return host_as_pxe_files(self)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Interface(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
macaddr = models.CharField(max_length=17, validators=[validate_macaddr])
|
||||
pxe_filename = models.CharField(max_length=64, blank=True)
|
||||
dhcp_client = models.BooleanField(default=False)
|
||||
host = models.ForeignKey(Host)
|
||||
ip4address = models.ForeignKey(Ip4Address, blank=True, null=True, unique=True)
|
||||
ip4address = models.OneToOneField(Ip4Address, blank=True, null=True)
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
domain = models.ForeignKey(Domain)
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return "%s (%s on %s)" % (self.macaddr, self.name, self.host.hostname)
|
||||
|
||||
def ipv6_enabled(self):
|
||||
return self.ip6address_set.count() > 0
|
||||
return self.ip6address_set.exists()
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Ip6Address(models.Model):
|
||||
subnet = models.ForeignKey(Ip6Subnet)
|
||||
address = models.CharField(max_length=64)
|
||||
address = models.GenericIPAddressField(protocol='IPv6')
|
||||
interface = models.ForeignKey(Interface)
|
||||
|
||||
def full_address(self):
|
||||
return self.subnet.network + self.address
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
return "%s (%s on %s)" % (
|
||||
self.full_address(),
|
||||
self.interface.name,
|
||||
@@ -565,125 +589,3 @@ class Ip6Address(models.Model):
|
||||
class Meta:
|
||||
verbose_name = 'IPv6 address'
|
||||
verbose_name_plural = 'IPv6 addresses'
|
||||
|
||||
|
||||
def format_domain_serial_and_add_one(serial):
|
||||
today = datetime.datetime.now()
|
||||
res = re.findall("^%4d%02d%02d(\d\d)$" % (
|
||||
today.year, today.month, today.day), str(serial), re.DOTALL)
|
||||
|
||||
if len(res) == 0:
|
||||
""" This probably means that the serial is malformed
|
||||
or the date is wrong. We assume that if the date is wrong,
|
||||
it is in the past. Just create a new serial starting from 1."""
|
||||
return "%4d%02d%02d%02d" % \
|
||||
(today.year, today.month, today.day, 1)
|
||||
elif len(res) == 1:
|
||||
""" The serial contains todays date, just update it. """
|
||||
try:
|
||||
number = int(res[0])
|
||||
except:
|
||||
number = 1
|
||||
if number >= 99:
|
||||
""" This is bad... Just keep the number on 99.
|
||||
We also send a mail to sysadmins telling them that
|
||||
something is wrong..."""
|
||||
|
||||
else:
|
||||
number += 1
|
||||
return "%4d%02d%02d%02d" % (
|
||||
today.year, today.month, today.day, number)
|
||||
else:
|
||||
""" Just return the first serial for today. """
|
||||
return "%4d%02d%02d%02d" % \
|
||||
(today.year, today.month, today.day, 1)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Ip4Subnet)
|
||||
def create_ips_for_subnet(sender, instance, created, **kwargs):
|
||||
if not created:
|
||||
return
|
||||
|
||||
subnet = ipaddr.IPv4Network(instance.network + "/" + instance.netmask)
|
||||
|
||||
for addr in subnet.iterhosts():
|
||||
address = Ip4Address(address=str(addr), subnet=instance)
|
||||
address.save()
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Ip4Subnet)
|
||||
def delete_ips_for_subnet(sender, instance, **kwargs):
|
||||
for addr in instance.ip4address_set.all():
|
||||
addr.delete()
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Ip4Subnet)
|
||||
def set_domain_name_for_subnet(sender, instance, **kwargs):
|
||||
# we assume that the reverse domain_name does not change
|
||||
if len(instance.domain_name) == 0:
|
||||
ipspl = instance.network.split(".")
|
||||
rev = "%s.%s.%s" % (ipspl[2], ipspl[1], ipspl[0])
|
||||
instance.domain_name = "%s.in-addr.arpa" % rev
|
||||
|
||||
# update it's own serial
|
||||
# if instance.domain_serial is not None:
|
||||
# instance.domain_serial = format_domain_serial_and_add_one(instance.domain_serial)
|
||||
|
||||
# lets update the serial of the dhcp config
|
||||
# when the subnet is changed
|
||||
if instance.dhcp_config:
|
||||
# instance.dhcp_config.serial = instance.dhcp_config.serial + 1
|
||||
instance.dhcp_config.serial = format_domain_serial_and_add_one(instance.dhcp_config.serial)
|
||||
instance.dhcp_config.save()
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Ip6Subnet)
|
||||
def set_domain_name_for_ipv6_subnet(sender, instance, **kwargs):
|
||||
if len(instance.domain_name) > 0:
|
||||
return
|
||||
|
||||
network = ipaddr.IPv6Address("%s::" % instance.network)
|
||||
instance.domain_name = ".".join(network.exploded.replace(":", "")[:16])[::-1] + ".ip6.arpa"
|
||||
|
||||
|
||||
@receiver(post_save, sender=Interface)
|
||||
def update_domain_serial_when_change_to_interface(sender, instance, created, **kwargs):
|
||||
if instance.domain is not None:
|
||||
domain = instance.domain
|
||||
domain.domain_serial = format_domain_serial_and_add_one(domain.domain_serial)
|
||||
domain.save()
|
||||
|
||||
if instance.ip4address is not None:
|
||||
subnet = instance.ip4address.subnet
|
||||
subnet.domain_serial = format_domain_serial_and_add_one(subnet.domain_serial)
|
||||
subnet.save()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Host)
|
||||
def update_domain_serial_when_change_to_host(sender, instance, created, **kwargs):
|
||||
for interface in instance.interface_set.all():
|
||||
if interface.domain is not None:
|
||||
domain = interface.domain
|
||||
domain.domain_serial = format_domain_serial_and_add_one(domain.domain_serial)
|
||||
domain.save()
|
||||
if interface.ip4address is not None:
|
||||
subnet = interface.ip4address.subnet
|
||||
subnet.domain_serial = format_domain_serial_and_add_one(subnet.domain_serial)
|
||||
subnet.save()
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Interface)
|
||||
def update_domain_serial_when_interface_deleted(sender, instance, **kwargs):
|
||||
if instance.domain is not None:
|
||||
domain = instance.domain
|
||||
domain.domain_serial += 1
|
||||
domain.save()
|
||||
|
||||
if instance.ip4address is not None:
|
||||
subnet = instance.ip4address.subnet
|
||||
subnet.domain_serial += 1
|
||||
subnet.save()
|
||||
|
||||
# @receiver(pre_save, sender=Domain)
|
||||
# def update_domain_serial_when_domain_is_saved(sender, instance, **kwargs):
|
||||
# instance.domain_serial = format_domain_serial_and_add_one(instance.domain_serial)
|
||||
@@ -1,191 +0,0 @@
|
||||
# Django settings for dns_mdb project.
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
ADMINS = (
|
||||
('System Administrator', 'sysadmin@example.com'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
'NAME': 'mdb.db', # Or path to database file if using sqlite3.
|
||||
'USER': 'mdb', # Not used with sqlite3.
|
||||
'PASSWORD': 'mdb', # Not used with sqlite3.
|
||||
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||
'PORT': '3306', # Set to empty string for default. Not used with sqlite3.
|
||||
}
|
||||
}
|
||||
|
||||
# Local time zone for this installation. Choices can be found here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
# On Unix systems, a value of None will cause Django to use the same
|
||||
# timezone as the operating system.
|
||||
# If running in a Windows environment this must be set to the same as your
|
||||
# system time zone.
|
||||
TIME_ZONE = 'Europe/Oslo'
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# If you set this to False, Django will make some optimizations so as not
|
||||
# to load the internationalization machinery.
|
||||
USE_I18N = True
|
||||
|
||||
# If you set this to False, Django will not format dates, numbers and
|
||||
# calendars according to the current locale
|
||||
USE_L10N = True
|
||||
|
||||
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||
# Example: "/home/media/media.lawrence.com/media/"
|
||||
MEDIA_ROOT = ''
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
|
||||
MEDIA_URL = ''
|
||||
|
||||
# Absolute path to the directory static files should be collected to.
|
||||
# Don't put anything in this directory yourself; store your static files
|
||||
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||
# Example: "/home/media/media.lawrence.com/static/"
|
||||
STATIC_ROOT = ''
|
||||
|
||||
# URL prefix for static files.
|
||||
# Example: "http://media.lawrence.com/static/"
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# URL prefix for admin static files -- CSS, JavaScript and images.
|
||||
# Make sure to use a trailing slash.
|
||||
# Examples: "http://foo.com/static/admin/", "/static/admin/".
|
||||
ADMIN_MEDIA_PREFIX = '/media/'
|
||||
|
||||
# Additional locations of static files
|
||||
STATICFILES_DIRS = (
|
||||
# Put strings here, like "/home/html/static" or "C:/www/django/static".
|
||||
# Always use forward slashes, even on Windows.
|
||||
# Don't forget to use absolute paths, not relative paths.
|
||||
)
|
||||
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
|
||||
)
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = '@4ps=ebofcpv8sb#y7gm896=l&l*u0u-pgo7bcdj%+(k+2lqnl'
|
||||
|
||||
# List of callables that know how to import templates from various sources.
|
||||
TEMPLATE_LOADERS = (
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
# 'django.template.loaders.eggs.Loader',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'urls'
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
|
||||
# Always use forward slashes, even on Windows.
|
||||
# Don't forget to use absolute paths, not relative paths.
|
||||
)
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'mdb',
|
||||
'suit',
|
||||
'django.contrib.admin',
|
||||
)
|
||||
|
||||
# Defines a test runner
|
||||
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
|
||||
|
||||
# A sample logging configuration. The only tangible logging
|
||||
# performed by this configuration is to send an email to
|
||||
# the site admins on every HTTP 500 error.
|
||||
# See http://docs.djangoproject.com/en/dev/topics/logging for
|
||||
# more details on how to customize your logging configuration.
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
'class': 'django.utils.log.AdminEmailHandler'
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'django.request': {
|
||||
'handlers': ['mail_admins'],
|
||||
'level': 'ERROR',
|
||||
'propagate': True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
EMAIL_SUBJECT_PREFIX = '[MDB] '
|
||||
EMAIL_HOST = 'smtp.example.com'
|
||||
EMAIL_PORT = 25
|
||||
|
||||
|
||||
# for django-suit
|
||||
from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS as TCP
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS = TCP + (
|
||||
'django.core.context_processors.request',
|
||||
)
|
||||
|
||||
# django-suit configuration
|
||||
SUIT_CONFIG = {
|
||||
# header
|
||||
'ADMIN_NAME': 'MDB',
|
||||
'HEADER_DATE_FORMAT': 'l, Y-m-d',
|
||||
'HEADER_TIME_FORMAT': 'H:i',
|
||||
|
||||
# forms
|
||||
# 'SHOW_REQUIRED_ASTERISK': True, # Default True
|
||||
# 'CONFIRM_UNSAVED_CHANGES': True, # Default True
|
||||
|
||||
# menu
|
||||
'SEARCH_URL': '/admin/mdb/host/',
|
||||
# 'MENU_ICONS': {
|
||||
# 'sites': 'icon-leaf',
|
||||
# 'auth': 'icon-lock',
|
||||
# },
|
||||
# 'MENU_OPEN_FIRST_CHILD': True, # Default True
|
||||
# 'MENU_EXCLUDE': ('auth.group',),
|
||||
'MENU': (
|
||||
{'app': 'mdb', 'label': 'MDB', 'url': 'mdb.host'},
|
||||
{
|
||||
'label': 'Settings',
|
||||
'icon': 'icon-cog',
|
||||
'models': ('auth.user', 'auth.group')
|
||||
},
|
||||
),
|
||||
|
||||
# misc
|
||||
'LIST_PER_PAGE': 30
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import pre_delete, post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
import ipaddress
|
||||
|
||||
from mdb.models import Interface, Host, Ip6Subnet, Ip4Subnet, Ip4Address
|
||||
from mdb.utils import format_domain_serial_and_add_one
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Ip4Subnet)
|
||||
def create_ips_for_subnet(sender, instance, created, **kwargs):
|
||||
if not created:
|
||||
return
|
||||
|
||||
subnet = ipaddress.IPv4Network(instance.network + "/" + instance.netmask)
|
||||
|
||||
for addr in subnet.hosts():
|
||||
address = Ip4Address(address=str(addr), subnet=instance)
|
||||
address.save()
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Ip4Subnet)
|
||||
def delete_ips_for_subnet(sender, instance, **kwargs):
|
||||
for addr in instance.ip4address_set.all():
|
||||
addr.delete()
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Ip4Subnet)
|
||||
def set_domain_name_for_subnet(sender, instance, **kwargs):
|
||||
# we assume that the reverse domain_name does not change
|
||||
if len(instance.domain_name) == 0:
|
||||
ipspl = instance.network.split(".")
|
||||
rev = "%s.%s.%s" % (ipspl[2], ipspl[1], ipspl[0])
|
||||
instance.domain_name = "%s.in-addr.arpa" % rev
|
||||
|
||||
# update it's own serial
|
||||
# if instance.domain_serial is not None:
|
||||
# instance.domain_serial = format_domain_serial_and_add_one(instance.domain_serial)
|
||||
|
||||
# lets update the serial of the dhcp config
|
||||
# when the subnet is changed
|
||||
if instance.dhcp_config:
|
||||
# instance.dhcp_config.serial = instance.dhcp_config.serial + 1
|
||||
instance.dhcp_config.serial = format_domain_serial_and_add_one(instance.dhcp_config.serial)
|
||||
instance.dhcp_config.save()
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Ip6Subnet)
|
||||
def set_domain_name_for_ipv6_subnet(sender, instance, **kwargs):
|
||||
if len(instance.domain_name) > 0:
|
||||
return
|
||||
|
||||
network = ipaddress.IPv6Address("%s::" % instance.network)
|
||||
instance.domain_name = ".".join(network.exploded.replace(":", "")[:16])[::-1] + ".ip6.arpa"
|
||||
|
||||
|
||||
@receiver(post_save, sender=Interface)
|
||||
def update_domain_serial_when_change_to_interface(sender, instance, created, **kwargs):
|
||||
if instance.domain is not None:
|
||||
domain = instance.domain
|
||||
domain.domain_serial = format_domain_serial_and_add_one(domain.domain_serial)
|
||||
domain.save()
|
||||
|
||||
if instance.ip4address is not None:
|
||||
subnet = instance.ip4address.subnet
|
||||
subnet.domain_serial = format_domain_serial_and_add_one(subnet.domain_serial)
|
||||
subnet.save()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Host)
|
||||
def update_domain_serial_when_change_to_host(sender, instance, created, **kwargs):
|
||||
for interface in instance.interface_set.all():
|
||||
if interface.domain is not None:
|
||||
domain = interface.domain
|
||||
domain.domain_serial = format_domain_serial_and_add_one(domain.domain_serial)
|
||||
domain.save()
|
||||
if interface.ip4address is not None:
|
||||
subnet = interface.ip4address.subnet
|
||||
subnet.domain_serial = format_domain_serial_and_add_one(subnet.domain_serial)
|
||||
subnet.save()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Host)
|
||||
def create_pxe_key_and_write_pxe_files_when_host_changes(sender, instance, created, **kwargs):
|
||||
host = instance
|
||||
|
||||
if not host.pxe_key:
|
||||
# generate key (prevent infinite recursion by using update)
|
||||
pxe_key = uuid.uuid4().hex
|
||||
Host.objects.filter(pk=host.pk).update(pxe_key=pxe_key)
|
||||
host.pxe_key = pxe_key
|
||||
|
||||
for pxe_file_name, pxe_file in host.as_pxe_files():
|
||||
path = os.path.join(settings.MDB_PXE_TFTP_ROOT, pxe_file_name)
|
||||
if host.pxe_installable:
|
||||
with open(path, 'w+') as f:
|
||||
f.write(pxe_file)
|
||||
logger.info("Created or updated {}".format(path))
|
||||
|
||||
# Also set necessary fields for pxe installation
|
||||
interfaces = host.interface_set.exclude(ip4address__isnull=True)
|
||||
|
||||
for _if in interfaces:
|
||||
changed = False
|
||||
if not _if.pxe_filename:
|
||||
_if.pxe_filename = 'pxelinux.0' # don't overwrite
|
||||
changed = True
|
||||
if not _if.dhcp_client:
|
||||
_if.dhcp_client = True
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
_if.save()
|
||||
|
||||
else:
|
||||
if os.path.exists(path):
|
||||
os.unlink(path)
|
||||
logger.info("deleted {}".format(path))
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Interface)
|
||||
def update_domain_serial_when_interface_deleted(sender, instance, **kwargs):
|
||||
if instance.domain is not None:
|
||||
domain = instance.domain
|
||||
domain.domain_serial += 1
|
||||
domain.save()
|
||||
|
||||
if instance.ip4address is not None:
|
||||
subnet = instance.ip4address.subnet
|
||||
subnet.domain_serial += 1
|
||||
subnet.save()
|
||||
|
||||
# @receiver(pre_save, sender=Domain)
|
||||
# def update_domain_serial_when_domain_is_saved(sender, instance, **kwargs):
|
||||
# instance.domain_serial = format_domain_serial_and_add_one(instance.domain_serial)
|
||||
@@ -0,0 +1,4 @@
|
||||
kernel {{kernel}}
|
||||
append ramdisk_size=14984 locale=en_US console-setup/ask_detect=false keyboard-configuration/layoutcode=no netcfg/wireless_wep= netcfg_choose_interface=eth0 netcfg/get_hostname= preseed/url={{preseed_config_url}} vga=normal initrd={{initrd}} -- snop={{host.pxe_key}}
|
||||
prompt 0
|
||||
timeout 0
|
||||
+53
-13
@@ -1,16 +1,56 @@
|
||||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
import io
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management import call_command
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from mdb.models import Host
|
||||
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.assertEqual(1 + 1, 2)
|
||||
class MyAPITestCases(APITestCase):
|
||||
fixtures = ['test_data']
|
||||
|
||||
def create_user_with_token(self):
|
||||
self.user = User.objects.create(username='admin')
|
||||
self.token = Token.objects.create(user=self.user)
|
||||
self.host = Host.objects.first()
|
||||
|
||||
def setUp(self):
|
||||
self.create_user_with_token()
|
||||
return super(MyAPITestCases, self).setUp()
|
||||
|
||||
def test_validate_puppet_host_by_secret(self):
|
||||
data = {
|
||||
'pxe_key': self.host.pxe_key,
|
||||
'certname': '{}.{}'.format(self.host.hostname, self.host.interface_set.first().domain.domain_name)
|
||||
}
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Token {}'.format(self.token.key))
|
||||
res = self.client.post(reverse('api-validate-host-secret'), data, format='json')
|
||||
self.assertEquals(res.status_code, 200, res.content)
|
||||
self.assertFalse(Host.objects.get(pk=self.host.pk).pxe_installable)
|
||||
|
||||
|
||||
class UnitTests(TestCase):
|
||||
fixtures = ['test_data']
|
||||
|
||||
def test_host_as_pxe_files(self):
|
||||
host = Host.objects.first()
|
||||
self.assertEqual(len(host.as_pxe_files()), 1)
|
||||
|
||||
|
||||
class RunManagementCommands(TestCase):
|
||||
fixtures = ['test_data']
|
||||
|
||||
def test_zone_sync(self):
|
||||
expected_zone_file = ''
|
||||
with io.BytesIO() as f:
|
||||
call_command('zone-sync', debug=True, stdout=f)
|
||||
# TODO check expected_zone_file
|
||||
|
||||
def test_dhcp_sync(self):
|
||||
expected_dhcp_file = ''
|
||||
with io.BytesIO() as f:
|
||||
call_command('dhcp-sync', ('--debug',), stdout=f)
|
||||
# TODO check expected_dhcp_file
|
||||
@@ -0,0 +1,81 @@
|
||||
import datetime
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
import ipaddress
|
||||
import re
|
||||
|
||||
|
||||
def format_domain_serial_and_add_one(serial):
|
||||
today = datetime.datetime.now()
|
||||
res = re.findall("^%4d%02d%02d(\d\d)$" % (
|
||||
today.year, today.month, today.day), str(serial), re.DOTALL)
|
||||
|
||||
if len(res) == 0:
|
||||
""" This probably means that the serial is malformed
|
||||
or the date is wrong. We assume that if the date is wrong,
|
||||
it is in the past. Just create a new serial starting from 1."""
|
||||
return "%4d%02d%02d%02d" % \
|
||||
(today.year, today.month, today.day, 1)
|
||||
elif len(res) == 1:
|
||||
""" The serial contains todays date, just update it. """
|
||||
try:
|
||||
number = int(res[0])
|
||||
except:
|
||||
number = 1
|
||||
if number >= 99:
|
||||
""" This is bad... Just keep the number on 99.
|
||||
We also send a mail to sysadmins telling them that
|
||||
something is wrong..."""
|
||||
|
||||
else:
|
||||
number += 1
|
||||
return "%4d%02d%02d%02d" % (
|
||||
today.year, today.month, today.day, number)
|
||||
else:
|
||||
""" Just return the first serial for today. """
|
||||
return "%4d%02d%02d%02d" % \
|
||||
(today.year, today.month, today.day, 1)
|
||||
|
||||
|
||||
def render_pxelinux_cfg(_if, host):
|
||||
# TODO add fields to os model and make dynamic per host
|
||||
distro = 'ubuntu/14_04/amd64/alternate/'
|
||||
kernel = distro + 'linux'
|
||||
initrd = distro + 'initrd.gz'
|
||||
preseed_config_url = settings.MDB_PXE_PRESEED_URL
|
||||
context = {
|
||||
'kernel': kernel,
|
||||
'initrd': initrd,
|
||||
'preseed_config_url': preseed_config_url,
|
||||
'host': host,
|
||||
'interface': _if
|
||||
}
|
||||
return render_to_string('pxelinux.cfg', context=context)
|
||||
|
||||
|
||||
def get_pxe_filename(interface, filename_format='mac_addr'):
|
||||
"""
|
||||
Mac or IP address based PXE filename
|
||||
Ref: http://www.syslinux.org/wiki/index.php/PXELINUX#Examples
|
||||
"""
|
||||
ip_addr_in_hex = '{:02X}'.format(int(ipaddress.IPv4Address(interface.ip4address.address)))
|
||||
formats = {
|
||||
'ip_addr': ip_addr_in_hex,
|
||||
'mac_addr': '01-{}'.format(interface.macaddr.lower().replace(':', '-'))
|
||||
}
|
||||
|
||||
return formats.get(filename_format, formats['mac_addr'])
|
||||
|
||||
|
||||
def host_as_pxe_files(host):
|
||||
""" Returns a list of filename,content for each host interface with an IPv4 address """
|
||||
from mdb.models import Host
|
||||
assert isinstance(host, Host)
|
||||
|
||||
pxe_files = []
|
||||
for _if in host.interface_set.exclude(ip4address__isnull=True):
|
||||
filename = get_pxe_filename(_if)
|
||||
content = render_pxelinux_cfg(_if, host)
|
||||
pxe_files.append((filename, content))
|
||||
|
||||
return pxe_files
|
||||
+3
-2
@@ -1,3 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from django.core.validators import RegexValidator
|
||||
import re
|
||||
|
||||
@@ -5,11 +6,11 @@ import re
|
||||
hostname_re = re.compile(r'^(?!-)[-a-z0-9]+(?<!-)$', re.IGNORECASE)
|
||||
validate_hostname = RegexValidator(
|
||||
regex=hostname_re,
|
||||
message=u'Enter a valid hostname',
|
||||
message='Enter a valid hostname',
|
||||
code='invalid')
|
||||
|
||||
macaddr_re = re.compile(r'^([0-9A-F]{2}[:]){5}([0-9A-F]{2})$', re.IGNORECASE)
|
||||
validate_macaddr = RegexValidator(
|
||||
regex=macaddr_re,
|
||||
message=u'Enter a valid MAC address',
|
||||
message='Enter a valid MAC address',
|
||||
code='invalid')
|
||||
File renamed without changes.
@@ -0,0 +1,131 @@
|
||||
import os
|
||||
from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS as TCP
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
ADMINS = (
|
||||
('System Administrator', 'sysadmin@example.com'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
TIME_ZONE = 'Europe/Oslo'
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
MEDIA_URL = '/media/'
|
||||
ADMIN_MEDIA_PREFIX = '/media/'
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
|
||||
# Additional locations of static files
|
||||
STATICFILES_DIRS = (
|
||||
)
|
||||
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
|
||||
)
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = '@4ps=ebofcpv8sb#y7gm896=l&l*u0u-pgo7bcdj%+(k+2lqnl'
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'mdbsite.urls'
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
|
||||
# Always use forward slashes, even on Windows.
|
||||
# Don't forget to use absolute paths, not relative paths.
|
||||
)
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'suit',
|
||||
'django.contrib.admin',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
|
||||
'mdb',
|
||||
)
|
||||
|
||||
EMAIL_SUBJECT_PREFIX = '[MDB] '
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS = TCP + (
|
||||
'django.core.context_processors.request', # for django-suit
|
||||
)
|
||||
|
||||
# django-suit configuration
|
||||
SUIT_CONFIG = {
|
||||
# header
|
||||
'ADMIN_NAME': 'MDB',
|
||||
'HEADER_DATE_FORMAT': 'l, Y-m-d',
|
||||
'HEADER_TIME_FORMAT': 'H:i',
|
||||
|
||||
# forms
|
||||
# 'SHOW_REQUIRED_ASTERISK': True, # Default True
|
||||
# 'CONFIRM_UNSAVED_CHANGES': True, # Default True
|
||||
|
||||
# menu
|
||||
'SEARCH_URL': '/admin/mdb/host/',
|
||||
# 'MENU_ICONS': {
|
||||
# 'sites': 'icon-leaf',
|
||||
# 'auth': 'icon-lock',
|
||||
# },
|
||||
# 'MENU_OPEN_FIRST_CHILD': True, # Default True
|
||||
# 'MENU_EXCLUDE': ('auth.group',),
|
||||
'MENU': (
|
||||
{'app': 'mdb', 'label': 'MDB', 'url': 'mdb.host'},
|
||||
{
|
||||
'label': 'Settings',
|
||||
'icon': 'icon-cog',
|
||||
'models': ('auth.user', 'auth.group')
|
||||
},
|
||||
),
|
||||
|
||||
# misc
|
||||
'LIST_PER_PAGE': 30
|
||||
}
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
|
||||
'PAGINATE_BY': 10
|
||||
}
|
||||
|
||||
MDB_PXE_TFTP_ROOT = '/var/lib/tftpboot/pxelinux/pxelinux.cfg/'
|
||||
MDB_PXE_PRESEED_URL = 'http://158.36.190.194/ubuntu/preseed_1404.cfg'
|
||||
|
||||
try:
|
||||
from .local_settings import *
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.views.generic.base import RedirectView
|
||||
from mdb.api.views import HostPXEValidate
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', RedirectView.as_view(url='admin/', permanent=True)),
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'^api/validate-host-secret/', HostPXEValidate.as_view(), name='api-validate-host-secret')
|
||||
]
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
File renamed without changes.
+6
-4
@@ -1,5 +1,7 @@
|
||||
Django>=1.7,<1.8
|
||||
MySQL-python==1.2.5
|
||||
ipaddr==2.1.11
|
||||
Django>=1.8,<1.9
|
||||
django-auth-ldap
|
||||
django-suit
|
||||
django-extensions==1.5.7
|
||||
djangorestframework==3.2.4
|
||||
django-suit==0.2.15
|
||||
ipaddress==1.0.14
|
||||
mysqlclient==1.3.6
|
||||
@@ -1,14 +0,0 @@
|
||||
from django.conf.urls import patterns, include, url
|
||||
from django.contrib import admin
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^$', RedirectView.as_view(url='admin/')),
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
)
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
Reference in new issue
Block a user