Write pxeconfig files per host
- Autogenerate pxe_key if missing - Adds AppConfig - Move signals to own file - Fixes local_setting simport on python 3
This commit is contained in:
1 parent
836a4d136e
commit
8d537dfba7
8 files changed
+237
-128
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'mdb.apps.MdbAppConfig'
|
||||||
+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
|
||||||
+4
-126
@@ -1,17 +1,14 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count
|
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.encoding import python_2_unicode_compatible
|
||||||
from django.utils.six import u
|
from django.utils.six import u
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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 mdb.validators import validate_hostname, validate_macaddr
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
@@ -548,6 +545,9 @@ class Host(models.Model):
|
|||||||
addresses = self.interface_set.filter(ip4address__isnull=False).values_list('ip4address__address', flat=True)
|
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
|
@python_2_unicode_compatible
|
||||||
class Interface(models.Model):
|
class Interface(models.Model):
|
||||||
@@ -585,125 +585,3 @@ class Ip6Address(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'IPv6 address'
|
verbose_name = 'IPv6 address'
|
||||||
verbose_name_plural = 'IPv6 addresses'
|
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 = 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(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)
|
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
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):
|
||||||
|
if not instance.pxe_key:
|
||||||
|
# generate key (prevent infinite recursion by using update)
|
||||||
|
pxe_key = uuid.uuid4().hex
|
||||||
|
Host.objects.filter(pk=instance.pk).update(pxe_key=pxe_key)
|
||||||
|
instance.pxe_key = pxe_key
|
||||||
|
|
||||||
|
for pxe_file_name, pxe_file in instance.as_pxe_files():
|
||||||
|
path = os.path.join(settings.MDB_PXE_TFTP_ROOT, pxe_file_name)
|
||||||
|
if instance.pxe_installable:
|
||||||
|
with open(path, 'w+') as f:
|
||||||
|
f.write(pxe_file)
|
||||||
|
logger.info("Created or updated {}".format(path))
|
||||||
|
else:
|
||||||
|
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
|
||||||
+11
-1
@@ -1,9 +1,11 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from mdb.models import Host
|
from django.test import TestCase
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from mdb.models import Host
|
||||||
|
|
||||||
|
|
||||||
class MyAPITestCases(APITestCase):
|
class MyAPITestCases(APITestCase):
|
||||||
fixtures = ['test_data']
|
fixtures = ['test_data']
|
||||||
@@ -26,3 +28,11 @@ class MyAPITestCases(APITestCase):
|
|||||||
res = self.client.post(reverse('api-validate-host-secret'), data, format='json')
|
res = self.client.post(reverse('api-validate-host-secret'), data, format='json')
|
||||||
self.assertEquals(res.status_code, 200, res.content)
|
self.assertEquals(res.status_code, 200, res.content)
|
||||||
self.assertFalse(Host.objects.get(pk=self.host.pk).pxe_installable)
|
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)
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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 """
|
||||||
|
from mdb.models import Host
|
||||||
|
assert isinstance(host, Host)
|
||||||
|
|
||||||
|
pxe_files = []
|
||||||
|
ifs = host.interface_set.filter(dhcp_client=True)
|
||||||
|
for _if in ifs:
|
||||||
|
filename = get_pxe_filename(_if)
|
||||||
|
content = render_pxelinux_cfg(_if, host)
|
||||||
|
pxe_files.append((filename, content))
|
||||||
|
|
||||||
|
return pxe_files
|
||||||
+4
-1
@@ -122,7 +122,10 @@ REST_FRAMEWORK = {
|
|||||||
'PAGINATE_BY': 10
|
'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:
|
try:
|
||||||
from local_settings import *
|
from .local_settings import *
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
Reference in new issue
Block a user