Commit 8d537dfb authored by Nikolai Kristiansen's avatar Nikolai Kristiansen
Browse files

Write pxeconfig files per host

- Autogenerate pxe_key if missing
- Adds AppConfig
- Move signals to own file
- Fixes local_setting simport on python 3
parent 836a4d13
default_app_config = 'mdb.apps.MdbAppConfig'
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
\ No newline at end of file
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)
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)
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
\ No newline at end of file
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)
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
......@@ -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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment