From 8d537dfba78f7bdb6ca3ce41de136cbe08556443 Mon Sep 17 00:00:00 2001 From: Nikolai Kristiansen Date: Sat, 3 Oct 2015 17:05:34 +0200 Subject: [PATCH] Write pxeconfig files per host - Autogenerate pxe_key if missing - Adds AppConfig - Move signals to own file - Fixes local_setting simport on python 3 --- mdb/__init__.py | 1 + mdb/apps.py | 10 +++ mdb/models.py | 130 ++----------------------------------- mdb/signals.py | 121 ++++++++++++++++++++++++++++++++++ mdb/templates/pxelinux.cfg | 4 ++ mdb/tests.py | 12 +++- mdb/utils.py | 82 +++++++++++++++++++++++ mdbsite/settings.py | 5 +- 8 files changed, 237 insertions(+), 128 deletions(-) create mode 100644 mdb/apps.py create mode 100644 mdb/signals.py create mode 100644 mdb/templates/pxelinux.cfg create mode 100644 mdb/utils.py diff --git a/mdb/__init__.py b/mdb/__init__.py index e69de29..d600276 100644 --- a/mdb/__init__.py +++ b/mdb/__init__.py @@ -0,0 +1 @@ +default_app_config = 'mdb.apps.MdbAppConfig' diff --git a/mdb/apps.py b/mdb/apps.py new file mode 100644 index 0000000..159d9aa --- /dev/null +++ b/mdb/apps.py @@ -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 \ No newline at end of file diff --git a/mdb/models.py b/mdb/models.py index d55a720..26b1bc3 100644 --- a/mdb/models.py +++ b/mdb/models.py @@ -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) diff --git a/mdb/signals.py b/mdb/signals.py new file mode 100644 index 0000000..d18e467 --- /dev/null +++ b/mdb/signals.py @@ -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) diff --git a/mdb/templates/pxelinux.cfg b/mdb/templates/pxelinux.cfg new file mode 100644 index 0000000..b333dd3 --- /dev/null +++ b/mdb/templates/pxelinux.cfg @@ -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 \ No newline at end of file diff --git a/mdb/tests.py b/mdb/tests.py index bfd4846..6c975cf 100644 --- a/mdb/tests.py +++ b/mdb/tests.py @@ -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) diff --git a/mdb/utils.py b/mdb/utils.py new file mode 100644 index 0000000..19e734b --- /dev/null +++ b/mdb/utils.py @@ -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 diff --git a/mdbsite/settings.py b/mdbsite/settings.py index 3a0bf85..1e0549c 100644 --- a/mdbsite/settings.py +++ b/mdbsite/settings.py @@ -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