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.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.six import u
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mdb.utils import host_as_pxe_files
|
||||
|
||||
from mdb.validators import validate_hostname, validate_macaddr
|
||||
|
||||
import ipaddress
|
||||
import datetime
|
||||
import re
|
||||
|
||||
|
||||
@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)
|
||||
return ",".join(addresses)
|
||||
|
||||
def as_pxe_files(self):
|
||||
return host_as_pxe_files(self)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Interface(models.Model):
|
||||
@@ -585,125 +585,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 = 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.core.urlresolvers import reverse
|
||||
from mdb.models import Host
|
||||
from django.test import TestCase
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from mdb.models import Host
|
||||
|
||||
|
||||
class MyAPITestCases(APITestCase):
|
||||
fixtures = ['test_data']
|
||||
@@ -26,3 +28,11 @@ class MyAPITestCases(APITestCase):
|
||||
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)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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 *
|
||||
from .local_settings import *
|
||||
except ImportError:
|
||||
pass
|
||||
Reference in new issue
Block a user