diff --git a/.gitignore b/.gitignore index c91623e..8f3eedc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.swp temp *.log -settings.py -dns_mdb.db +local_settings.py +db.sqlite3 /.idea /venv diff --git a/README b/README deleted file mode 100644 index 510496c..0000000 --- a/README +++ /dev/null @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4f3fa3 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +A Django app for managing computer equipment. + +## Installation + sudo apt-get 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 + +## Features + +* Creates DHCPD configuration files +* Creates DNS zone files for BIND +* TODO: Creates PXE config files (with random secret for use with Puppet autosign) \ No newline at end of file diff --git a/manage.py b/manage.py index e59d7d2..f6ab478 100755 --- a/manage.py +++ b/manage.py @@ -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) diff --git a/__init__.py b/mdb/api/__init__.py similarity index 100% rename from __init__.py rename to mdb/api/__init__.py diff --git a/wsgi/__init__.py b/mdb/api/serializers.py similarity index 100% rename from wsgi/__init__.py rename to mdb/api/serializers.py diff --git a/mdb/api/views.py b/mdb/api/views.py new file mode 100644 index 0000000..6053112 --- /dev/null +++ b/mdb/api/views.py @@ -0,0 +1,6 @@ +from rest_framework.views import APIView + + +class ValidatePuppetHostSecret(APIView): + # TODO + pass diff --git a/mdb/management/commands/dhcp-sync.py b/mdb/management/commands/dhcp-sync.py index d12124e..0f62474 100644 --- a/mdb/management/commands/dhcp-sync.py +++ b/mdb/management/commands/dhcp-sync.py @@ -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 diff --git a/mdb/management/commands/zone-sync.py b/mdb/management/commands/zone-sync.py index 6b1bc4e..ea57037 100644 --- a/mdb/management/commands/zone-sync.py +++ b/mdb/management/commands/zone-sync.py @@ -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() diff --git a/mdb/migrations/0006_auto_20151001_2235.py b/mdb/migrations/0006_auto_20151001_2235.py new file mode 100644 index 0000000..bb38709 --- /dev/null +++ b/mdb/migrations/0006_auto_20151001_2235.py @@ -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]+(? 0 +@python_2_unicode_compatible class Ip6Address(models.Model): subnet = models.ForeignKey(Ip6Subnet) address = models.CharField(max_length=64) @@ -556,7 +574,7 @@ class Ip6Address(models.Model): 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, @@ -604,7 +622,7 @@ def create_ips_for_subnet(sender, instance, created, **kwargs): if not created: return - subnet = ipaddr.IPv4Network(instance.network + "/" + instance.netmask) + subnet = ipaddress.IPv4Network(instance.network + "/" + instance.netmask) for addr in subnet.iterhosts(): address = Ip4Address(address=str(addr), subnet=instance) @@ -642,7 +660,7 @@ def set_domain_name_for_ipv6_subnet(sender, instance, **kwargs): if len(instance.domain_name) > 0: return - network = ipaddr.IPv6Address("%s::" % instance.network) + network = ipaddress.IPv6Address("%s::" % instance.network) instance.domain_name = ".".join(network.exploded.replace(":", "")[:16])[::-1] + ".ip6.arpa" diff --git a/mdb/settings-sample.py b/mdb/settings-sample.py deleted file mode 100644 index 32b2658..0000000 --- a/mdb/settings-sample.py +++ /dev/null @@ -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 -} diff --git a/mdb/tests.py b/mdb/tests.py index 501deb7..f699e78 100644 --- a/mdb/tests.py +++ b/mdb/tests.py @@ -1,16 +1,12 @@ -""" -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. -""" - -from django.test import TestCase +from django.core.urlresolvers import reverse +from rest_framework.test import APITestCase -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) +class APITestCases(APITestCase): + def test_validate_puppet_host_by_secret(self): + data = { + 'secret': 'something', + 'certname': 'arnold.neuf.no' + } + res = self.client.post(reverse('api-validate-host-secret'), data) + self.fail() diff --git a/mdbsite/settings.py b/mdbsite/settings.py new file mode 100644 index 0000000..6da84b1 --- /dev/null +++ b/mdbsite/settings.py @@ -0,0 +1,128 @@ +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', + 'mdb', + 'suit', + 'django.contrib.admin', +) + +# Defines a test runner +TEST_RUNNER = 'django.test.runner.DiscoverRunner' + +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 +} + +try: + from local_settings import * +except ImportError: + pass diff --git a/urls.py b/mdbsite/urls.py similarity index 59% rename from urls.py rename to mdbsite/urls.py index 4d30b2a..8fab6a9 100644 --- a/urls.py +++ b/mdbsite/urls.py @@ -1,14 +1,15 @@ -from django.conf.urls import patterns, include, url +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 ValidatePuppetHostSecret admin.autodiscover() -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^$', RedirectView.as_view(url='admin/')), url(r'^admin/', include(admin.site.urls)), -) + url(r'^api/validate-host-secret', ValidatePuppetHostSecret.as_view(), name='api-validate-host-secret') +] urlpatterns += staticfiles_urlpatterns() diff --git a/wsgi/wsgi.py b/mdbsite/wsgi.py similarity index 100% rename from wsgi/wsgi.py rename to mdbsite/wsgi.py diff --git a/requirements.txt b/requirements.txt index 9bf7dda..af04a5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ -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-suit==0.2.15 +djangorestframework==3.2.4 +ipaddress==1.0.14 +mysqlclient==1.3.6