Reformat all files with ruff

This commit is contained in:
ponas committed 2026-03-19 02:25:30 +01:00
1 parent 94c2b03138
commit f4950c6388
22 files changed
+420 -271

No files matched your search

Vendored
+16 -10
View File
@@ -7,17 +7,23 @@ from invoke import task
@task() @task()
def deploy(c): def deploy(c):
"""Make sure proxy_user is set to your neuf username.""" """Make sure proxy_user is set to your neuf username."""
project_path = '/opt/mdb' project_path = "/opt/mdb"
proxy_user = os.getenv('DEPLOY_USER', os.getenv('USER')) proxy_user = os.getenv("DEPLOY_USER", os.getenv("USER"))
c = Connection(host='gitdeploy@sega.neuf.no', gateway=Connection('login.neuf.no', user=proxy_user)) c = Connection(
host="gitdeploy@sega.neuf.no",
gateway=Connection("login.neuf.no", user=proxy_user),
)
with c.cd(project_path), c.prefix('source {}/.venv/bin/activate'.format(project_path)): with (
c.run('git pull') # Get source c.cd(project_path),
c.run('pip install -U uv') c.prefix("source {}/.venv/bin/activate".format(project_path)),
c.run('uv sync') # install deps in virtualenv ):
c.run('uv run manage.py collectstatic --noinput') # Collect static c.run("git pull") # Get source
c.run('uv run manage.py migrate') # Run DB migrations c.run("pip install -U uv")
c.run("uv sync") # install deps in virtualenv
c.run("uv run manage.py collectstatic --noinput") # Collect static
c.run("uv run manage.py migrate") # Run DB migrations
# Reload gunicorn # Reload gunicorn
c.sudo('/usr/bin/supervisorctl pid mdb.neuf.no | xargs kill -HUP', shell=False) c.sudo("/usr/bin/supervisorctl pid mdb.neuf.no | xargs kill -HUP", shell=False)
+1
View File
@@ -5,4 +5,5 @@ import sys
if __name__ == "__main__": if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mdbsite.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mdbsite.settings")
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
+1 -1
View File
@@ -1 +1 @@
default_app_config = 'mdb.apps.MdbAppConfig' default_app_config = "mdb.apps.MdbAppConfig"
+100 -42
View File
@@ -48,45 +48,70 @@ class InterfaceInline(admin.TabularInline):
class HostAdmin(admin.ModelAdmin): class HostAdmin(admin.ModelAdmin):
ordering = ('hostname',) ordering = ("hostname",)
inlines = [InterfaceInline] inlines = [InterfaceInline]
list_display = ['hostname', 'owner', 'host_type', 'location', 'mac_addresses', 'ip_addresses', 'in_domain', list_display = [
'pxe_installable', 'ipv6_enabled'] "hostname",
list_filter = ['host_type', 'owner', 'location', 'pxe_installable'] "owner",
readonly_fields = ['kerberos_principal_name', 'kerberos_principal_created_date', "host_type",
'kerberos_principal_created'] "location",
search_fields = ['hostname', 'location', 'interface__macaddr', 'interface__ip4address__address'] "mac_addresses",
"ip_addresses",
"in_domain",
"pxe_installable",
"ipv6_enabled",
]
list_filter = ["host_type", "owner", "location", "pxe_installable"]
readonly_fields = [
"kerberos_principal_name",
"kerberos_principal_created_date",
"kerberos_principal_created",
]
search_fields = [
"hostname",
"location",
"interface__macaddr",
"interface__ip4address__address",
]
fieldsets = ( fieldsets = (
('Owner Information', { ("Owner Information", {"fields": ("owner", "location", "description")}),
'fields': ('owner', 'location', 'description') (
}), "Hardware and Software Information",
('Hardware and Software Information', { {
'fields': ( "fields": (
('hostname', 'host_type'), ("hostname", "host_type"),
('pxe_key', 'pxe_installable'), ("pxe_key", "pxe_installable"),
('brand', 'model'), ("brand", "model"),
'serial_number', "serial_number",
('operating_system', 'virtual')) ("operating_system", "virtual"),
}), )
},
),
) )
actions = ['set_installable'] actions = ["set_installable"]
def mac_addresses(self, host): def mac_addresses(self, host):
addresses = host.interface_set.filter(macaddr__isnull=False).values_list('macaddr', flat=True) addresses = host.interface_set.filter(macaddr__isnull=False).values_list(
"macaddr", flat=True
)
return ", ".join(addresses) return ", ".join(addresses)
def ip_addresses(self, host): def ip_addresses(self, host):
addresses = host.interface_set.filter(ip4address__isnull=False).values_list('ip4address__address', flat=True) addresses = host.interface_set.filter(ip4address__isnull=False).values_list(
"ip4address__address", flat=True
)
return ", ".join(addresses) return ", ".join(addresses)
def in_domain(self, host): def in_domain(self, host):
domains = host.interface_set.values_list('domain__domain_name', flat=True) domains = host.interface_set.values_list("domain__domain_name", flat=True)
return ",".join(domains) return ",".join(domains)
in_domain.short_description = "in domains" in_domain.short_description = "in domains"
def ipv6_enabled(self, host): def ipv6_enabled(self, host):
ipv6_ifs = host.interface_set.annotate(num_ipv6=Count('ip6address')).filter(num_ipv6__gt=0) ipv6_ifs = host.interface_set.annotate(num_ipv6=Count("ip6address")).filter(
num_ipv6__gt=0
)
return ipv6_ifs.exists() return ipv6_ifs.exists()
ipv6_enabled.boolean = True ipv6_enabled.boolean = True
@@ -94,8 +119,10 @@ class HostAdmin(admin.ModelAdmin):
@staticmethod @staticmethod
def _get_host_warning_message(host, ifs): def _get_host_warning_message(host, ifs):
ifs_str = ', '.join([_if.name for _if in ifs]) ifs_str = ", ".join([_if.name for _if in ifs])
return 'Host {} might not be installable since interface(s) {} has no IP set.'.format(host, ifs_str) return "Host {} might not be installable since interface(s) {} has no IP set.".format(
host, ifs_str
)
def set_installable(self, request, queryset): def set_installable(self, request, queryset):
invalid_ifs_per_host = [] invalid_ifs_per_host = []
@@ -103,12 +130,16 @@ class HostAdmin(admin.ModelAdmin):
for obj in queryset: for obj in queryset:
invalid_ifs = obj.interface_set.filter(ip4address__isnull=True) invalid_ifs = obj.interface_set.filter(ip4address__isnull=True)
if invalid_ifs.exists(): if invalid_ifs.exists():
invalid_ifs_per_host.append(self._get_host_warning_message(obj, invalid_ifs)) invalid_ifs_per_host.append(
self._get_host_warning_message(obj, invalid_ifs)
)
obj.pxe_installable = True obj.pxe_installable = True
obj.save() # triggers post save signal obj.save() # triggers post save signal
if invalid_ifs_per_host: if invalid_ifs_per_host:
self.message_user(request, message='\n'.join(invalid_ifs_per_host), level=messages.WARNING) self.message_user(
request, message="\n".join(invalid_ifs_per_host), level=messages.WARNING
)
set_installable.short_description = "Mark selected hosts as PXE installable" set_installable.short_description = "Mark selected hosts as PXE installable"
@@ -119,7 +150,7 @@ class HostAdmin(admin.ModelAdmin):
class Ip4AddressInline(admin.TabularInline): class Ip4AddressInline(admin.TabularInline):
model = Ip4Address model = Ip4Address
extra = 0 extra = 0
readonly_fields = ['address', 'assigned_to_host'] readonly_fields = ["address", "assigned_to_host"]
def assigned_to_host(self, addr): def assigned_to_host(self, addr):
return addr.interface.host return addr.interface.host
@@ -138,7 +169,15 @@ class DhcpCustomFieldInline(admin.TabularInline):
class SubnetAdmin(admin.ModelAdmin): class SubnetAdmin(admin.ModelAdmin):
list_display = ['name', 'network', 'netmask', 'num_addresses', 'broadcast_address', 'first_address', 'last_address'] list_display = [
"name",
"network",
"netmask",
"num_addresses",
"broadcast_address",
"first_address",
"last_address",
]
inlines = [DhcpOptionInline, DhcpCustomFieldInline, Ip4AddressInline] inlines = [DhcpOptionInline, DhcpCustomFieldInline, Ip4AddressInline]
def num_addresses(self, ip4subnet): def num_addresses(self, ip4subnet):
@@ -157,10 +196,10 @@ class SubnetAdmin(admin.ModelAdmin):
subnet = ipaddress.IPv4Network(ip4subnet.network + "/" + ip4subnet.netmask) subnet = ipaddress.IPv4Network(ip4subnet.network + "/" + ip4subnet.netmask)
return list(subnet.hosts())[-1] return list(subnet.hosts())[-1]
broadcast_address.short_description = 'broadcast' broadcast_address.short_description = "broadcast"
num_addresses.short_description = '#addresses' num_addresses.short_description = "#addresses"
first_address.short_description = 'first address' first_address.short_description = "first address"
last_address.short_description = 'last address' last_address.short_description = "last address"
def get_queryset(self, request): def get_queryset(self, request):
# TODO: optimize # TODO: optimize
@@ -193,12 +232,24 @@ class DomainAAAARecordInline(admin.TabularInline):
class DomainAdmin(admin.ModelAdmin): class DomainAdmin(admin.ModelAdmin):
inlines = [DomainSrvRecordInline, DomainTxtRecordInline, DomainARecordInline, DomainAAAARecordInline, inlines = [
DomainCnameRecordInline] DomainSrvRecordInline,
list_display = ['domain_name', 'domain_soa', 'domain_admin', 'num_records', 'domain_ipaddr', 'domain_ip6addr'] DomainTxtRecordInline,
search_fields = ['domain_name'] DomainARecordInline,
DomainAAAARecordInline,
DomainCnameRecordInline,
]
list_display = [
"domain_name",
"domain_soa",
"domain_admin",
"num_records",
"domain_ipaddr",
"domain_ip6addr",
]
search_fields = ["domain_name"]
formfield_overrides = { formfield_overrides = {
models.IntegerField: {'widget': NumberInput(attrs={'style': 'width: 10em'})}, models.IntegerField: {"widget": NumberInput(attrs={"style": "width: 10em"})},
} }
def num_records(self, domain): def num_records(self, domain):
@@ -207,19 +258,26 @@ class DomainAdmin(admin.ModelAdmin):
"CNAME": domain.domaincnamerecord_set.count(), "CNAME": domain.domaincnamerecord_set.count(),
"SRV": domain.domainsrvrecord_set.count(), "SRV": domain.domainsrvrecord_set.count(),
"TXT": domain.domaintxtrecord_set.count(), "TXT": domain.domaintxtrecord_set.count(),
"A": domain.domainarecord_set.count() + host_a_records "A": domain.domainarecord_set.count() + host_a_records,
} }
return ', '.join(['{}: {}'.format(k, v) for k, v in size.items()]) return ", ".join(["{}: {}".format(k, v) for k, v in size.items()])
num_records.short_description = "Num Records" num_records.short_description = "Num Records"
class HostTypeAdmin(admin.ModelAdmin): class HostTypeAdmin(admin.ModelAdmin):
list_display = ['host_type', 'description', 'num_members'] list_display = ["host_type", "description", "num_members"]
class OperatingSystemAdmin(admin.ModelAdmin): class OperatingSystemAdmin(admin.ModelAdmin):
list_display = ['name', 'version', 'arch', 'pxe_kernel', 'pxe_initrd', 'pxe_preseed_config_url'] list_display = [
"name",
"version",
"arch",
"pxe_kernel",
"pxe_initrd",
"pxe_preseed_config_url",
]
admin.site.register(Domain, DomainAdmin) admin.site.register(Domain, DomainAdmin)
+21 -13
View File
@@ -10,13 +10,15 @@ class HostPXEValidateSerializer(serializers.Serializer):
host = None host = None
def _get_host_by_fqdn(self, attrs): def _get_host_by_fqdn(self, attrs):
fqdn = attrs['certname'] fqdn = attrs["certname"]
host_name = fqdn.split('.')[0] host_name = fqdn.split(".")[0]
domain = '.'.join(fqdn.split('.')[1:]) domain = ".".join(fqdn.split(".")[1:])
try: try:
return Host.objects.get(hostname=host_name, interface__domain__domain_name=domain) return Host.objects.get(
except (Host.DoesNotExist, Host.MultipleObjectsReturned): hostname=host_name, interface__domain__domain_name=domain
)
except Host.DoesNotExist, Host.MultipleObjectsReturned:
return None return None
def validate(self, attrs): def validate(self, attrs):
@@ -24,18 +26,24 @@ class HostPXEValidateSerializer(serializers.Serializer):
host = self._get_host_by_fqdn(attrs) host = self._get_host_by_fqdn(attrs)
if host is None: if host is None:
raise ValidationError('Host \'{}\' not found.'.format(attrs['certname'])) raise ValidationError("Host '{}' not found.".format(attrs["certname"]))
if not host.pxe_installable: if not host.pxe_installable:
raise ValidationError('Host \'{}\' is marked as not installable via PXE.'.format(attrs['certname'])) raise ValidationError(
"Host '{}' is marked as not installable via PXE.".format(
attrs["certname"]
)
)
if not host.pxe_key: if not host.pxe_key:
raise ValidationError('Host \'{}\' has no pxe_key.'.format(attrs['certname'])) raise ValidationError("Host '{}' has no pxe_key.".format(attrs["certname"]))
if host.pxe_key != attrs['pxe_key']: if host.pxe_key != attrs["pxe_key"]:
raise ValidationError('Supplied pxe_key \'{}\' does not match host \'{}\'.'.format( raise ValidationError(
attrs['pxe_key'], "Supplied pxe_key '{}' does not match host '{}'.".format(
attrs['certname'])) attrs["pxe_key"], attrs["certname"]
)
)
self.host = host self.host = host
@@ -44,7 +52,7 @@ class HostPXEValidateSerializer(serializers.Serializer):
def create(self, validated_data): def create(self, validated_data):
# Make requests kind of idempotent by only allowing 1 request / installation # Make requests kind of idempotent by only allowing 1 request / installation
self.host.pxe_installable = False self.host.pxe_installable = False
self.host.pxe_key = '' self.host.pxe_key = ""
self.host.save() self.host.save()
return self.host return self.host
+1 -1
View File
@@ -17,4 +17,4 @@ class HostPXEValidate(APIView):
serializer.save() # sets Host.pxe_installable=False serializer.save() # sets Host.pxe_installable=False
return Response({'valid_pxe_key': True}) return Response({"valid_pxe_key": True})
+2 -2
View File
@@ -2,8 +2,8 @@ from django.apps import AppConfig
class MdbAppConfig(AppConfig): class MdbAppConfig(AppConfig):
name = 'mdb' name = "mdb"
verbose_name = 'Machine Database' verbose_name = "Machine Database"
def ready(self): def ready(self):
# all models are loaded, now attach signals # all models are loaded, now attach signals
+6 -6
View File
@@ -7,7 +7,7 @@ from mdb.models import Interface, Ip4Address
class Ip4ModelChoiceField(ModelChoiceField): class Ip4ModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj): def label_from_instance(self, obj):
if not hasattr(obj, 'interface'): if not hasattr(obj, "interface"):
return obj.address return obj.address
return "{} ({})".format(obj.address, obj.interface.host.hostname) return "{} ({})".format(obj.address, obj.interface.host.hostname)
@@ -17,18 +17,18 @@ class InterfaceForm(forms.ModelForm):
model = Interface model = Interface
def clean(self): def clean(self):
macaddr = self.cleaned_data['macaddr'] macaddr = self.cleaned_data["macaddr"]
ip4address = self.cleaned_data['ip4address'] ip4address = self.cleaned_data["ip4address"]
for interface in Interface.objects.filter(macaddr=macaddr): for interface in Interface.objects.filter(macaddr=macaddr):
subnet = interface.ip4address.subnet subnet = interface.ip4address.subnet
if subnet == ip4address.subnet and interface != self.instance: if subnet == ip4address.subnet and interface != self.instance:
raise ValidationError('Macaddr already in this subnet') raise ValidationError("Macaddr already in this subnet")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(InterfaceForm, self).__init__(*args, **kwargs) super(InterfaceForm, self).__init__(*args, **kwargs)
# Join Ip4Address with tables interface and host, # Join Ip4Address with tables interface and host,
# before printing the select labels that walk those two relations. # before printing the select labels that walk those two relations.
queryset = Ip4Address.objects.select_related('interface', 'interface__host') queryset = Ip4Address.objects.select_related("interface", "interface__host")
self.fields['ip4address'] = Ip4ModelChoiceField(queryset=queryset) self.fields["ip4address"] = Ip4ModelChoiceField(queryset=queryset)
+2 -1
View File
@@ -64,7 +64,8 @@ class Command(BaseCommand):
self.stdout.write("fail\n") self.stdout.write("fail\n")
message = ( message = (
"Failed to restart isc-dhcp-server, please inspect...\n\n" "Failed to restart isc-dhcp-server, please inspect...\n\n"
"%s@%s$ %s\n%s" % (os.getlogin(), os.uname()[1], service.restart_command, output) "%s@%s$ %s\n%s"
% (os.getlogin(), os.uname()[1], service.restart_command, output)
) )
discord.notify( discord.notify(
subject="⚠️ FAIL: dhcpd %s -> %s" % (old_serial, config.active_serial), subject="⚠️ FAIL: dhcpd %s -> %s" % (old_serial, config.active_serial),
+91 -62
View File
@@ -34,8 +34,10 @@ class Domain(models.Model):
domain_expire = models.IntegerField(default=604800) domain_expire = models.IntegerField(default=604800)
domain_minimum_ttl = models.IntegerField(default=86400) domain_minimum_ttl = models.IntegerField(default=86400)
domain_admin = models.EmailField() domain_admin = models.EmailField()
domain_ipaddr = models.GenericIPAddressField(protocol='IPv4') domain_ipaddr = models.GenericIPAddressField(protocol="IPv4")
domain_ip6addr = models.GenericIPAddressField(protocol='IPv6', blank=True, null=True) domain_ip6addr = models.GenericIPAddressField(
protocol="IPv6", blank=True, null=True
)
domain_filename = models.CharField(max_length=256) domain_filename = models.CharField(max_length=256)
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
@@ -46,7 +48,7 @@ class Domain(models.Model):
return self.domain_name return self.domain_name
def __eq__(self, other): def __eq__(self, other):
if not other or not hasattr(other, 'domain_name'): if not other or not hasattr(other, "domain_name"):
return False return False
return self.domain_name == other.domain_name return self.domain_name == other.domain_name
@@ -59,7 +61,8 @@ class Domain(models.Model):
content += "$TTL %s\n" % self.domain_ttl content += "$TTL %s\n" % self.domain_ttl
content += "@ IN SOA %s. %s. (\n" % ( content += "@ IN SOA %s. %s. (\n" % (
self.domain_soa, self.domain_soa,
self.domain_admin.replace("@", ".")) self.domain_admin.replace("@", "."),
)
content += "\t%d\t; serial\n" % self.domain_serial content += "\t%d\t; serial\n" % self.domain_serial
content += "\t%d\t; refresh\n" % self.domain_refresh content += "\t%d\t; refresh\n" % self.domain_refresh
content += "\t%d\t; retry\n" % self.domain_retry content += "\t%d\t; retry\n" % self.domain_retry
@@ -111,11 +114,13 @@ class Domain(models.Model):
if interface.ip4address: if interface.ip4address:
content += "%-20s\tIN\tA\t%s\n" % ( content += "%-20s\tIN\tA\t%s\n" % (
host.hostname, host.hostname,
interface.ip4address.address) interface.ip4address.address,
)
for ipv6addr in interface.ip6address_set.all(): for ipv6addr in interface.ip6address_set.all():
content += "%-20s\tIN\tAAAA\t%s\n" % ( content += "%-20s\tIN\tAAAA\t%s\n" % (
host.hostname, host.hostname,
ipv6addr.full_address()) ipv6addr.full_address(),
)
return content return content
@@ -132,15 +137,14 @@ class DomainSrvRecord(models.Model):
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
def as_record(self): def as_record(self):
return '{}.{}.{}. IN SRV {} {} {} {}.'.format( return "{}.{}.{}. IN SRV {} {} {} {}.".format(
self.srvce, self.srvce,
self.prot, self.prot,
self.name, self.name,
self.priority, self.priority,
self.weight, self.weight,
self.port, self.port,
self.target self.target,
) )
def __str__(self): def __str__(self):
@@ -157,7 +161,7 @@ class DomainTxtRecord(models.Model):
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
def as_record(self): def as_record(self):
return '{} TXT {}'.format(self.name, self.target) return "{} TXT {}".format(self.name, self.target)
def __str__(self): def __str__(self):
return self.as_record() return self.as_record()
@@ -170,7 +174,7 @@ class DomainCnameRecord(models.Model):
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
def as_record(self): def as_record(self):
return '{} IN CNAME {}'.format(self.name, self.target) return "{} IN CNAME {}".format(self.name, self.target)
def __str__(self): def __str__(self):
return self.as_record() return self.as_record()
@@ -178,12 +182,12 @@ class DomainCnameRecord(models.Model):
class DomainARecord(models.Model): class DomainARecord(models.Model):
name = models.CharField(max_length=256) name = models.CharField(max_length=256)
target = models.GenericIPAddressField(protocol='IPv4') target = models.GenericIPAddressField(protocol="IPv4")
domain = models.ForeignKey(Domain, models.CASCADE) domain = models.ForeignKey(Domain, models.CASCADE)
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
def as_record(self): def as_record(self):
return '{} IN A {}'.format(self.name, self.target) return "{} IN A {}".format(self.name, self.target)
def __str__(self): def __str__(self):
return self.as_record() return self.as_record()
@@ -191,12 +195,12 @@ class DomainARecord(models.Model):
class DomainAAAARecord(models.Model): class DomainAAAARecord(models.Model):
name = models.CharField(max_length=256) name = models.CharField(max_length=256)
target = models.GenericIPAddressField(protocol='IPv6') target = models.GenericIPAddressField(protocol="IPv6")
domain = models.ForeignKey(Domain, models.CASCADE) domain = models.ForeignKey(Domain, models.CASCADE)
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
def as_record(self): def as_record(self):
return '{} IN AAAA {}'.format(self.name, self.target) return "{} IN AAAA {}".format(self.name, self.target)
def __str__(self): def __str__(self):
return self.as_record() return self.as_record()
@@ -224,9 +228,7 @@ class DhcpConfig(models.Model):
# time to write the subnet definitions # time to write the subnet definitions
for subnet in self.ip4subnet_set.all(): for subnet in self.ip4subnet_set.all():
content += "\n# %s\n" % subnet.name content += "\n# %s\n" % subnet.name
content += "subnet %s netmask %s {\n" % ( content += "subnet %s netmask %s {\n" % (subnet.network, subnet.netmask)
subnet.network,
subnet.netmask)
for option in subnet.dhcpoption_set.all(): for option in subnet.dhcpoption_set.all():
content += "\toption %s %s;\n" % (option.key, option.value) content += "\toption %s %s;\n" % (option.key, option.value)
@@ -235,23 +237,30 @@ class DhcpConfig(models.Model):
content += "\t%s;\n" % option.value content += "\t%s;\n" % option.value
if subnet.dhcp_dynamic: if subnet.dhcp_dynamic:
content += "\trange %s %s;\n" \ content += "\trange %s %s;\n" % (
% (subnet.dhcp_dynamic_start, subnet.dhcp_dynamic_end) subnet.dhcp_dynamic_start,
subnet.dhcp_dynamic_end,
)
content += "}\n" content += "}\n"
# time to write host definitions # time to write host definitions
for subnet in self.ip4subnet_set.all(): for subnet in self.ip4subnet_set.all():
for ip4address in subnet.ip4address_set.all(): for ip4address in subnet.ip4address_set.all():
if not hasattr(ip4address, 'interface') or not ip4address.interface.dhcp_client: if (
not hasattr(ip4address, "interface")
or not ip4address.interface.dhcp_client
):
continue continue
_if = ip4address.interface _if = ip4address.interface
content += "\nhost %s {\n" % _if.host.hostname content += "\nhost %s {\n" % _if.host.hostname
content += "\thardware ethernet %s;\n" % _if.macaddr content += "\thardware ethernet %s;\n" % _if.macaddr
content += "\tfixed-address %s.%s;\n" % \ content += "\tfixed-address %s.%s;\n" % (
(_if.host.hostname, _if.domain.domain_name) _if.host.hostname,
_if.domain.domain_name,
)
if len(_if.pxe_filename) > 0: if len(_if.pxe_filename) > 0:
content += "\tfilename \"%s\";\n" % _if.pxe_filename content += '\tfilename "%s";\n' % _if.pxe_filename
content += "}\n" content += "}\n"
return content return content
@@ -260,7 +269,7 @@ class DhcpConfig(models.Model):
return self.name return self.name
class Meta: class Meta:
verbose_name = 'DHCP config' verbose_name = "DHCP config"
class Ip6Subnet(models.Model): class Ip6Subnet(models.Model):
@@ -292,7 +301,8 @@ class Ip6Subnet(models.Model):
content += "$TTL %s\n" % self.domain_ttl content += "$TTL %s\n" % self.domain_ttl
content += "@ IN SOA %s. %s. (\n" % ( content += "@ IN SOA %s. %s. (\n" % (
self.domain_soa, self.domain_soa,
self.domain_admin.replace("@", ".")) self.domain_admin.replace("@", "."),
)
content += "\t%d\t; serial\n" % self.domain_serial content += "\t%d\t; serial\n" % self.domain_serial
content += "\t%d\t; refresh\n" % self.domain_refresh content += "\t%d\t; refresh\n" % self.domain_refresh
content += "\t%d\t; retry\n" % self.domain_retry content += "\t%d\t; retry\n" % self.domain_retry
@@ -317,7 +327,8 @@ class Ip6Subnet(models.Model):
continue continue
hostname = "%s.%s" % ( hostname = "%s.%s" % (
addr.interface.host.hostname, addr.interface.host.hostname,
addr.interface.domain.domain_name) addr.interface.domain.domain_name,
)
ip = ipaddress.IPv6Address(self.network + addr.address) ip = ipaddress.IPv6Address(self.network + addr.address)
ip = ".".join(ip.exploded.replace(":", "")[16:])[::-1] ip = ".".join(ip.exploded.replace(":", "")[16:])[::-1]
@@ -329,13 +340,13 @@ class Ip6Subnet(models.Model):
return content return content
class Meta: class Meta:
verbose_name = 'IPv6 subnet' verbose_name = "IPv6 subnet"
class Ip4Subnet(models.Model): class Ip4Subnet(models.Model):
name = models.CharField(max_length=256) name = models.CharField(max_length=256)
netmask = models.GenericIPAddressField(protocol='IPv4') netmask = models.GenericIPAddressField(protocol="IPv4")
network = models.GenericIPAddressField(protocol='IPv4') network = models.GenericIPAddressField(protocol="IPv4")
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
domain_name = models.CharField(max_length=255, editable=False) domain_name = models.CharField(max_length=255, editable=False)
domain_nameservers = models.ManyToManyField(Nameserver) domain_nameservers = models.ManyToManyField(Nameserver)
@@ -351,8 +362,12 @@ class Ip4Subnet(models.Model):
domain_filename = models.CharField(max_length=256) domain_filename = models.CharField(max_length=256)
dhcp_dynamic = models.BooleanField(default=False) dhcp_dynamic = models.BooleanField(default=False)
dhcp_dynamic_start = models.GenericIPAddressField(protocol='IPv4', null=True, blank=True) dhcp_dynamic_start = models.GenericIPAddressField(
dhcp_dynamic_end = models.GenericIPAddressField(protocol='IPv4', null=True, blank=True) protocol="IPv4", null=True, blank=True
)
dhcp_dynamic_end = models.GenericIPAddressField(
protocol="IPv4", null=True, blank=True
)
dhcp_config = models.ForeignKey(DhcpConfig, models.CASCADE) dhcp_config = models.ForeignKey(DhcpConfig, models.CASCADE)
def __str__(self): def __str__(self):
@@ -366,7 +381,8 @@ class Ip4Subnet(models.Model):
content += "$TTL %s\n" % self.domain_ttl content += "$TTL %s\n" % self.domain_ttl
content += "@ IN SOA %s. %s. (\n" % ( content += "@ IN SOA %s. %s. (\n" % (
self.domain_soa, self.domain_soa,
self.domain_admin.replace("@", ".")) self.domain_admin.replace("@", "."),
)
content += "\t%d\t; serial\n" % self.domain_serial content += "\t%d\t; serial\n" % self.domain_serial
content += "\t%d\t; refresh\n" % self.domain_refresh content += "\t%d\t; refresh\n" % self.domain_refresh
content += "\t%d\t; retry\n" % self.domain_retry content += "\t%d\t; retry\n" % self.domain_retry
@@ -382,22 +398,26 @@ class Ip4Subnet(models.Model):
for addr in self.ip4address_set.all(): for addr in self.ip4address_set.all():
""" Get PTR records for each v4 address in subnet """ """ Get PTR records for each v4 address in subnet """
if hasattr(addr, 'interface') and addr.interface.domain: if hasattr(addr, "interface") and addr.interface.domain:
hostname = "%s.%s" % ( hostname = "%s.%s" % (
addr.interface.host.hostname, addr.interface.host.hostname,
addr.interface.domain.domain_name) addr.interface.domain.domain_name,
content += "%-20s\tIN\tPTR\t%s.\n" % \ )
(addr.address.split(".")[3], hostname) content += "%-20s\tIN\tPTR\t%s.\n" % (
elif not hasattr(addr, 'interface') and generate_unassigned: addr.address.split(".")[3],
hostname,
)
elif not hasattr(addr, "interface") and generate_unassigned:
content += "%s\tIN\tPTR\t%s.%s\n" % ( content += "%s\tIN\tPTR\t%s.%s\n" % (
addr.address, addr.address,
addr.address.split(".")[3], addr.address.split(".")[3],
"dhcp.neuf.no.") "dhcp.neuf.no.",
)
return content return content
class Meta: class Meta:
verbose_name = 'IPv4 subnet' verbose_name = "IPv4 subnet"
class DhcpOption(models.Model): class DhcpOption(models.Model):
@@ -409,7 +429,7 @@ class DhcpOption(models.Model):
return self.key + " " + self.value return self.key + " " + self.value
class Meta: class Meta:
verbose_name = 'DHCP option' verbose_name = "DHCP option"
class DhcpCustomField(models.Model): class DhcpCustomField(models.Model):
@@ -417,12 +437,12 @@ class DhcpCustomField(models.Model):
ip4subnet = models.ForeignKey(Ip4Subnet, models.CASCADE) ip4subnet = models.ForeignKey(Ip4Subnet, models.CASCADE)
class Meta: class Meta:
verbose_name = 'DHCP custom field' verbose_name = "DHCP custom field"
class Ip4Address(models.Model): class Ip4Address(models.Model):
subnet = models.ForeignKey(Ip4Subnet, models.CASCADE) subnet = models.ForeignKey(Ip4Subnet, models.CASCADE)
address = models.GenericIPAddressField(protocol='IPv4') address = models.GenericIPAddressField(protocol="IPv4")
last_contact = models.DateTimeField(null=True, blank=True) last_contact = models.DateTimeField(null=True, blank=True)
ping_avg_rtt = models.FloatField(null=True, blank=True) ping_avg_rtt = models.FloatField(null=True, blank=True)
@@ -430,8 +450,8 @@ class Ip4Address(models.Model):
return self.address return self.address
class Meta: class Meta:
verbose_name = 'IPv4 address' verbose_name = "IPv4 address"
verbose_name_plural = 'IPv4 addresses' verbose_name_plural = "IPv4 addresses"
class HostType(models.Model): class HostType(models.Model):
@@ -450,19 +470,21 @@ class HostType(models.Model):
class OperatingSystem(models.Model): class OperatingSystem(models.Model):
OS_ARCHITECTURES = ( OS_ARCHITECTURES = (
('mips', _('MIPS')), ("mips", _("MIPS")),
('arm', _('ARM')), ("arm", _("ARM")),
('broadcom', _('Broadcom')), ("broadcom", _("Broadcom")),
('rc32300', _('RC32300')), ("rc32300", _("RC32300")),
('powerpc', _('PowerPC')), ("powerpc", _("PowerPC")),
('powerpc403ga', _('PowerPC403GA')), ("powerpc403ga", _("PowerPC403GA")),
('unknown', _('Unknown')), ("unknown", _("Unknown")),
('x86_64', _('x86-64')), ("x86_64", _("x86-64")),
('i386', _('x86')), ("i386", _("x86")),
) )
name = models.CharField(max_length=256) name = models.CharField(max_length=256)
version = models.CharField(max_length=64) version = models.CharField(max_length=64)
arch = models.CharField(max_length=255, choices=OS_ARCHITECTURES, blank=True, null=True) arch = models.CharField(
max_length=255, choices=OS_ARCHITECTURES, blank=True, null=True
)
pxe_kernel = models.CharField(max_length=255, blank=True, null=True) pxe_kernel = models.CharField(max_length=255, blank=True, null=True)
pxe_initrd = models.CharField(max_length=255, blank=True, null=True) pxe_initrd = models.CharField(max_length=255, blank=True, null=True)
@@ -486,12 +508,16 @@ class Host(models.Model):
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
host_type = models.ForeignKey(HostType, models.CASCADE) # FIXME: SET_NULL? host_type = models.ForeignKey(HostType, models.CASCADE) # FIXME: SET_NULL?
virtual = models.BooleanField(default=False) virtual = models.BooleanField(default=False)
operating_system = models.ForeignKey(OperatingSystem, models.CASCADE) # FIXME: SET_NULL? operating_system = models.ForeignKey(
OperatingSystem, models.CASCADE
) # FIXME: SET_NULL?
request_kerberos_principal = models.BooleanField(default=False) request_kerberos_principal = models.BooleanField(default=False)
kerberos_principal_created = models.BooleanField(default=False, editable=False) kerberos_principal_created = models.BooleanField(default=False, editable=False)
kerberos_principal_name = models.CharField(max_length=256, editable=False) kerberos_principal_name = models.CharField(max_length=256, editable=False)
kerberos_principal_created_date = models.DateTimeField(null=True, blank=True, editable=False) kerberos_principal_created_date = models.DateTimeField(
null=True, blank=True, editable=False
)
pxe_key = models.CharField(max_length=254, blank=True) pxe_key = models.CharField(max_length=254, blank=True)
pxe_installable = models.BooleanField(default=False) pxe_installable = models.BooleanField(default=False)
@@ -509,7 +535,9 @@ class Interface(models.Model):
pxe_filename = models.CharField(max_length=64, blank=True) pxe_filename = models.CharField(max_length=64, blank=True)
dhcp_client = models.BooleanField(default=False) dhcp_client = models.BooleanField(default=False)
host = models.ForeignKey(Host, models.CASCADE) host = models.ForeignKey(Host, models.CASCADE)
ip4address = models.OneToOneField(Ip4Address, blank=True, null=True, on_delete=models.SET_NULL) ip4address = models.OneToOneField(
Ip4Address, blank=True, null=True, on_delete=models.SET_NULL
)
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
domain = models.ForeignKey(Domain, models.CASCADE) domain = models.ForeignKey(Domain, models.CASCADE)
@@ -522,7 +550,7 @@ class Interface(models.Model):
class Ip6Address(models.Model): class Ip6Address(models.Model):
subnet = models.ForeignKey(Ip6Subnet, models.CASCADE) subnet = models.ForeignKey(Ip6Subnet, models.CASCADE)
address = models.GenericIPAddressField(protocol='IPv6') address = models.GenericIPAddressField(protocol="IPv6")
interface = models.ForeignKey(Interface, models.CASCADE) interface = models.ForeignKey(Interface, models.CASCADE)
def full_address(self): def full_address(self):
@@ -532,8 +560,9 @@ class Ip6Address(models.Model):
return "%s (%s on %s)" % ( return "%s (%s on %s)" % (
self.full_address(), self.full_address(),
self.interface.name, self.interface.name,
self.interface.host.hostname) self.interface.host.hostname,
)
class Meta: class Meta:
verbose_name = 'IPv6 address' verbose_name = "IPv6 address"
verbose_name_plural = 'IPv6 addresses' verbose_name_plural = "IPv6 addresses"
+15 -12
View File
@@ -14,39 +14,42 @@ def render_pxelinux_cfg(_if, host):
# Per host config (via OS model) # Per host config (via OS model)
_os = host.operating_system _os = host.operating_system
pxe_attrs = [_os.pxe_initrd, _os.pxe_kernel, _os.pxe_preseed_config_url] pxe_attrs = [_os.pxe_initrd, _os.pxe_kernel, _os.pxe_preseed_config_url]
if None not in pxe_attrs and '' not in pxe_attrs: if None not in pxe_attrs and "" not in pxe_attrs:
kernel = _os.pxe_kernel kernel = _os.pxe_kernel
initrd = _os.pxe_initrd initrd = _os.pxe_initrd
preseed_config_url = _os.pxe_preseed_config_url preseed_config_url = _os.pxe_preseed_config_url
context = { context = {
'kernel': kernel, "kernel": kernel,
'initrd': initrd, "initrd": initrd,
'preseed_config_url': preseed_config_url, "preseed_config_url": preseed_config_url,
'host': host, "host": host,
'interface': _if "interface": _if,
} }
return render_to_string('pxelinux.cfg', context=context) return render_to_string("pxelinux.cfg", context=context)
def get_pxe_filename(interface, filename_format='mac_addr'): def get_pxe_filename(interface, filename_format="mac_addr"):
""" """
Mac or IP address based PXE filename Mac or IP address based PXE filename
Ref: http://www.syslinux.org/wiki/index.php/PXELINUX#Examples Ref: http://www.syslinux.org/wiki/index.php/PXELINUX#Examples
""" """
ip_addr_in_hex = '{:02X}'.format(int(ipaddress.IPv4Address(interface.ip4address.address))) ip_addr_in_hex = "{:02X}".format(
int(ipaddress.IPv4Address(interface.ip4address.address))
)
formats = { formats = {
'ip_addr': ip_addr_in_hex, "ip_addr": ip_addr_in_hex,
'mac_addr': '01-{}'.format(interface.macaddr.lower().replace(':', '-')) "mac_addr": "01-{}".format(interface.macaddr.lower().replace(":", "-")),
} }
return formats.get(filename_format, formats['mac_addr']) return formats.get(filename_format, formats["mac_addr"])
def host_as_pxe_files(host): def host_as_pxe_files(host):
"""Returns a list of filename,content for each host interface with an IPv4 address.""" """Returns a list of filename,content for each host interface with an IPv4 address."""
from mdb.models import Host from mdb.models import Host
assert isinstance(host, Host) assert isinstance(host, Host)
pxe_files = [] pxe_files = []
+18 -7
View File
@@ -46,7 +46,9 @@ def set_domain_name_for_subnet(sender, instance, **kwargs):
# when the subnet is changed # when the subnet is changed
if instance.dhcp_config: if instance.dhcp_config:
# instance.dhcp_config.serial = instance.dhcp_config.serial + 1 # 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.serial = format_domain_serial_and_add_one(
instance.dhcp_config.serial
)
instance.dhcp_config.save() instance.dhcp_config.save()
@@ -56,7 +58,9 @@ def set_domain_name_for_ipv6_subnet(sender, instance, **kwargs):
return return
network = ipaddress.IPv6Address("%s::" % instance.network) network = ipaddress.IPv6Address("%s::" % instance.network)
instance.domain_name = ".".join(network.exploded.replace(":", "")[:16])[::-1] + ".ip6.arpa" instance.domain_name = (
".".join(network.exploded.replace(":", "")[:16])[::-1] + ".ip6.arpa"
)
@receiver(post_save, sender=Interface) @receiver(post_save, sender=Interface)
@@ -77,16 +81,22 @@ def update_domain_serial_when_change_to_host(sender, instance, created, **kwargs
for interface in instance.interface_set.all(): for interface in instance.interface_set.all():
if interface.domain is not None: if interface.domain is not None:
domain = interface.domain domain = interface.domain
domain.domain_serial = format_domain_serial_and_add_one(domain.domain_serial) domain.domain_serial = format_domain_serial_and_add_one(
domain.domain_serial
)
domain.save() domain.save()
if interface.ip4address is not None: if interface.ip4address is not None:
subnet = interface.ip4address.subnet subnet = interface.ip4address.subnet
subnet.domain_serial = format_domain_serial_and_add_one(subnet.domain_serial) subnet.domain_serial = format_domain_serial_and_add_one(
subnet.domain_serial
)
subnet.save() subnet.save()
@receiver(post_save, sender=Host) @receiver(post_save, sender=Host)
def create_pxe_key_and_write_pxe_files_when_host_changes(sender, instance, created, **kwargs): def create_pxe_key_and_write_pxe_files_when_host_changes(
sender, instance, created, **kwargs
):
host = instance host = instance
if not host.pxe_key: if not host.pxe_key:
@@ -99,7 +109,7 @@ def create_pxe_key_and_write_pxe_files_when_host_changes(sender, instance, creat
path = os.path.join(settings.MDB_PXE_TFTP_ROOT, pxe_file_name) path = os.path.join(settings.MDB_PXE_TFTP_ROOT, pxe_file_name)
if host.pxe_installable: if host.pxe_installable:
# Write pxeconfig file to disk # Write pxeconfig file to disk
with open(path, 'w+') as f: with open(path, "w+") as f:
f.write(pxe_file) f.write(pxe_file)
logger.info("Created or updated {}".format(path)) logger.info("Created or updated {}".format(path))
os.chmod(path, 0o644) # Not writeable for all os.chmod(path, 0o644) # Not writeable for all
@@ -110,7 +120,7 @@ def create_pxe_key_and_write_pxe_files_when_host_changes(sender, instance, creat
for _if in interfaces: for _if in interfaces:
changed = False changed = False
if not _if.pxe_filename: if not _if.pxe_filename:
_if.pxe_filename = 'pxelinux.0' # don't overwrite _if.pxe_filename = "pxelinux.0" # don't overwrite
changed = True changed = True
if not _if.dhcp_client: if not _if.dhcp_client:
_if.dhcp_client = True _if.dhcp_client = True
@@ -138,6 +148,7 @@ def update_domain_serial_when_interface_deleted(sender, instance, **kwargs):
subnet.domain_serial += 1 subnet.domain_serial += 1
subnet.save() subnet.save()
# @receiver(pre_save, sender=Domain) # @receiver(pre_save, sender=Domain)
# def update_domain_serial_when_domain_is_saved(sender, instance, **kwargs): # def update_domain_serial_when_domain_is_saved(sender, instance, **kwargs):
# instance.domain_serial = format_domain_serial_and_add_one(instance.domain_serial) # instance.domain_serial = format_domain_serial_and_add_one(instance.domain_serial)
+8 -8
View File
@@ -4,15 +4,17 @@ import re
def format_domain_serial_and_add_one(serial): def format_domain_serial_and_add_one(serial):
today = datetime.datetime.now() today = datetime.datetime.now()
res = re.findall(r"^%4d%02d%02d(\d\d)$" % ( res = re.findall(
today.year, today.month, today.day), str(serial), re.DOTALL) r"^%4d%02d%02d(\d\d)$" % (today.year, today.month, today.day),
str(serial),
re.DOTALL,
)
if len(res) == 0: if len(res) == 0:
""" This probably means that the serial is malformed """ This probably means that the serial is malformed
or the date is wrong. We assume that if the date is wrong, 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.""" it is in the past. Just create a new serial starting from 1."""
return "%4d%02d%02d%02d" % \ return "%4d%02d%02d%02d" % (today.year, today.month, today.day, 1)
(today.year, today.month, today.day, 1)
elif len(res) == 1: elif len(res) == 1:
""" The serial contains todays date, just update it. """ """ The serial contains todays date, just update it. """
try: try:
@@ -26,9 +28,7 @@ def format_domain_serial_and_add_one(serial):
else: else:
number += 1 number += 1
return "%4d%02d%02d%02d" % ( return "%4d%02d%02d%02d" % (today.year, today.month, today.day, number)
today.year, today.month, today.day, number)
else: else:
""" Just return the first serial for today. """ """ Just return the first serial for today. """
return "%4d%02d%02d%02d" % \ return "%4d%02d%02d%02d" % (today.year, today.month, today.day, 1)
(today.year, today.month, today.day, 1)
+6 -8
View File
@@ -2,14 +2,12 @@ from django.core.validators import RegexValidator
import re import re
# http://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names # http://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
hostname_re = re.compile(r'^(?!-)[-a-z0-9]+(?<!-)$', re.IGNORECASE) hostname_re = re.compile(r"^(?!-)[-a-z0-9]+(?<!-)$", re.IGNORECASE)
validate_hostname = RegexValidator( validate_hostname = RegexValidator(
regex=hostname_re, regex=hostname_re, message="Enter a valid hostname", code="invalid"
message='Enter a valid hostname', )
code='invalid')
macaddr_re = re.compile(r'^([0-9A-F]{2}[:]){5}([0-9A-F]{2})$', re.IGNORECASE) macaddr_re = re.compile(r"^([0-9A-F]{2}[:]){5}([0-9A-F]{2})$", re.IGNORECASE)
validate_macaddr = RegexValidator( validate_macaddr = RegexValidator(
regex=macaddr_re, regex=macaddr_re, message="Enter a valid MAC address", code="invalid"
message='Enter a valid MAC address', )
code='invalid')
+75 -80
View File
@@ -2,140 +2,135 @@ import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__))
DEBUG = os.getenv('DJANGO_DEBUG', True) DEBUG = os.getenv("DJANGO_DEBUG", True)
ADMINS = ( ADMINS = (("System Administrator", "sysadmin@example.com"),)
('System Administrator', 'sysadmin@example.com'),
)
MANAGERS = ADMINS MANAGERS = ADMINS
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.sqlite3', "ENGINE": "django.db.backends.sqlite3",
'NAME': 'db.sqlite3', "NAME": "db.sqlite3",
} }
} }
TIME_ZONE = 'Europe/Oslo' TIME_ZONE = "Europe/Oslo"
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = "en-us"
SITE_ID = 1 SITE_ID = 1
USE_I18N = True USE_I18N = True
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = '/media/' MEDIA_URL = "/media/"
ADMIN_MEDIA_PREFIX = '/media/' ADMIN_MEDIA_PREFIX = "/media/"
# Static # Static
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
STATIC_URL = '/static/' STATIC_URL = "/static/"
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', "django.contrib.staticfiles.finders.FileSystemFinder",
'django.contrib.staticfiles.finders.AppDirectoriesFinder', "django.contrib.staticfiles.finders.AppDirectoriesFinder",
) )
# Make this unique, and don't share it with anybody. # Make this unique, and don't share it with anybody.
SECRET_KEY = '@4ps=ebofcpv8sb#y7gm896=l&l*u0u-pgo7bcdj%+(k+2lqnl' SECRET_KEY = "@4ps=ebofcpv8sb#y7gm896=l&l*u0u-pgo7bcdj%+(k+2lqnl"
MIDDLEWARE = ( MIDDLEWARE = (
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
) )
ROOT_URLCONF = 'mdbsite.urls' ROOT_URLCONF = "mdbsite.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [], "DIRS": [],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.template.context_processors.i18n', "django.template.context_processors.i18n",
'django.template.context_processors.media', "django.template.context_processors.media",
'django.template.context_processors.static', "django.template.context_processors.static",
'django.template.context_processors.tz', "django.template.context_processors.tz",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
], ],
}, },
}, },
] ]
INSTALLED_APPS = ( INSTALLED_APPS = (
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.sites', "django.contrib.sites",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.staticfiles', "django.contrib.staticfiles",
"django.contrib.admin",
'django.contrib.admin', "rest_framework",
"rest_framework.authtoken",
'rest_framework', "django_extensions",
'rest_framework.authtoken', "mdb",
'django_extensions',
'mdb',
) )
EMAIL_SUBJECT_PREFIX = '[MDB] ' EMAIL_SUBJECT_PREFIX = "[MDB] "
# Logging # Logging
LOGGING = { LOGGING = {
'version': 1, "version": 1,
'disable_existing_loggers': True, "disable_existing_loggers": True,
'formatters': { "formatters": {
'verbose': { "verbose": {
'format': '%(levelname)s %(asctime)s %(module)s ' "format": "%(levelname)s %(asctime)s %(module)s "
'%(process)d %(thread)d %(message)s' "%(process)d %(thread)d %(message)s"
}, },
}, },
'handlers': { "handlers": {
'console': { "console": {
'level': 'DEBUG', "level": "DEBUG",
'class': 'logging.StreamHandler', "class": "logging.StreamHandler",
'formatter': 'verbose' "formatter": "verbose",
} }
}, },
'loggers': { "loggers": {
'root': { "root": {
'level': 'WARNING', "level": "WARNING",
'handlers': ['console'], "handlers": ["console"],
}, },
'django.db.backends': { "django.db.backends": {
'level': 'ERROR', "level": "ERROR",
'handlers': ['console'], "handlers": ["console"],
'propagate': False, "propagate": False,
}, },
}, },
} }
# REST Framework # REST Framework
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',), "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
'PAGINATE_BY': 10 "PAGINATE_BY": 10,
} }
# PXE # PXE
MDB_PXE_TFTP_ROOT = '/var/lib/tftpboot/pxelinux/pxelinux.cfg/' MDB_PXE_TFTP_ROOT = "/var/lib/tftpboot/pxelinux/pxelinux.cfg/"
MDB_PXE_KERNEL = 'ubuntu/14_04/amd64/alternate/linux' MDB_PXE_KERNEL = "ubuntu/14_04/amd64/alternate/linux"
MDB_PXE_INITRD = 'ubuntu/14_04/amd64/alternate/initrd.gz' MDB_PXE_INITRD = "ubuntu/14_04/amd64/alternate/initrd.gz"
MDB_PXE_PRESEED_URL = 'http://158.36.190.194/ubuntu/preseed_1404.cfg' MDB_PXE_PRESEED_URL = "http://158.36.190.194/ubuntu/preseed_1404.cfg"
# Sentry # Sentry
SENTRY_DSN = os.getenv('SENTRY_DSN') SENTRY_DSN = os.getenv("SENTRY_DSN")
SENTRY_ENVIRONMENT = os.getenv('SENTRY_ENVIRONENT', 'dev') SENTRY_ENVIRONMENT = os.getenv("SENTRY_ENVIRONENT", "dev")
ADMIN_SITE_NAME = 'MDB' ADMIN_SITE_NAME = "MDB"
try: try:
from .local_settings import * from .local_settings import *
+7 -3
View File
@@ -7,9 +7,13 @@ from mdb.api.views import HostPXEValidate
admin.autodiscover() admin.autodiscover()
urlpatterns = [ urlpatterns = [
re_path(r'^$', RedirectView.as_view(url='admin/', permanent=True)), re_path(r"^$", RedirectView.as_view(url="admin/", permanent=True)),
path('admin/', admin.site.urls), path("admin/", admin.site.urls),
path('api/validate-host-secret/', HostPXEValidate.as_view(), name='api-validate-host-secret') path(
"api/validate-host-secret/",
HostPXEValidate.as_view(),
name="api-validate-host-secret",
),
] ]
urlpatterns += staticfiles_urlpatterns() urlpatterns += staticfiles_urlpatterns()
+15
View File
@@ -28,6 +28,21 @@ dev = [
"ruff>=0.15.6,<1.0.0", "ruff>=0.15.6,<1.0.0",
] ]
[tool.ruff]
exclude = ["mdb/migrations/**"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"UP", # pyupgrade
]
ignore = [
"E501", # line-too-long
]
[tool.pytest.ini_options] [tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "mdbsite.settings" DJANGO_SETTINGS_MODULE = "mdbsite.settings"
python_files = ["tests/test_*.py"] python_files = ["tests/test_*.py"]
+4 -5
View File
@@ -48,16 +48,15 @@ def db_domain(db, tmp_path):
DomainARecord.objects.create(domain=domain, name="mail", target="9.10.11.12") DomainARecord.objects.create(domain=domain, name="mail", target="9.10.11.12")
DomainARecord.objects.create(domain=domain, name="www", target="1.2.3.4") DomainARecord.objects.create(domain=domain, name="www", target="1.2.3.4")
DomainAAAARecord.objects.create(domain=domain, name="www", target="2001:db8::1") DomainAAAARecord.objects.create(domain=domain, name="www", target="2001:db8::1")
DomainCnameRecord.objects.create(domain=domain, name="ftp", target="www.example.org.") DomainCnameRecord.objects.create(
domain=domain, name="ftp", target="www.example.org."
)
DomainTxtRecord.objects.create(domain=domain, name="@", target='"v=spf1 mx ~all"') DomainTxtRecord.objects.create(domain=domain, name="@", target='"v=spf1 mx ~all"')
return domain return domain
def load_command_class(): def load_command_class():
path = ( path = pathlib.Path(__file__).parent.parent / "mdb/management/commands/zone-sync.py"
pathlib.Path(__file__).parent.parent
/ "mdb/management/commands/zone-sync.py"
)
spec = importlib.util.spec_from_file_location("zone_sync_command", path) spec = importlib.util.spec_from_file_location("zone_sync_command", path)
mod = importlib.util.module_from_spec(spec) mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) spec.loader.exec_module(mod)
+7 -5
View File
@@ -20,7 +20,7 @@ def api_client():
@pytest.fixture @pytest.fixture
def user_with_token(db_with_fixtures): def user_with_token(db_with_fixtures):
user = User.objects.create(username='admin') user = User.objects.create(username="admin")
token = Token.objects.create(user=user) token = Token.objects.create(user=user)
return user, token return user, token
@@ -34,10 +34,12 @@ def host(db_with_fixtures):
def test_validate_puppet_host_by_secret(api_client, user_with_token, host): def test_validate_puppet_host_by_secret(api_client, user_with_token, host):
_, token = user_with_token _, token = user_with_token
data = { data = {
'pxe_key': host.pxe_key, "pxe_key": host.pxe_key,
'certname': '{}.{}'.format(host.hostname, host.interface_set.first().domain.domain_name), "certname": "{}.{}".format(
host.hostname, host.interface_set.first().domain.domain_name
),
} }
api_client.credentials(HTTP_AUTHORIZATION='Token {}'.format(token.key)) api_client.credentials(HTTP_AUTHORIZATION="Token {}".format(token.key))
res = api_client.post(reverse('api-validate-host-secret'), data, format='json') res = api_client.post(reverse("api-validate-host-secret"), data, format="json")
assert res.status_code == 200, res.content assert res.status_code == 200, res.content
assert not Host.objects.get(pk=host.pk).pxe_installable assert not Host.objects.get(pk=host.pk).pxe_installable
+10 -2
View File
@@ -9,7 +9,9 @@ def make_config(serial=2, active_serial=1):
config = MagicMock() config = MagicMock()
config.serial = serial config.serial = serial
config.active_serial = active_serial config.active_serial = active_serial
config.dhcpd_configuration.return_value = "# dhcpd config\nsubnet 10.0.0.0 netmask 255.255.255.0 {}\n" config.dhcpd_configuration.return_value = (
"# dhcpd config\nsubnet 10.0.0.0 netmask 255.255.255.0 {}\n"
)
return config return config
@@ -20,6 +22,7 @@ def service():
# --- needs_update --- # --- needs_update ---
def test_needs_update_true_when_serials_differ(service): def test_needs_update_true_when_serials_differ(service):
config = make_config(serial=2, active_serial=1) config = make_config(serial=2, active_serial=1)
assert service.needs_update(config) is True assert service.needs_update(config) is True
@@ -38,6 +41,7 @@ def test_needs_update_true_when_force_even_if_serials_match():
# --- calculate_diff --- # --- calculate_diff ---
def test_calculate_diff_identical_returns_empty(service): def test_calculate_diff_identical_returns_empty(service):
config = make_config() config = make_config()
assert service.calculate_diff(config, "line\n", "line\n") == "" assert service.calculate_diff(config, "line\n", "line\n") == ""
@@ -54,6 +58,7 @@ def test_calculate_diff_format(service):
# --- write_config --- # --- write_config ---
def test_write_config_creates_file_with_config_contents(service, tmp_path): def test_write_config_creates_file_with_config_contents(service, tmp_path):
config = make_config() config = make_config()
output_path = str(tmp_path / "dhcpd.conf") output_path = str(tmp_path / "dhcpd.conf")
@@ -99,6 +104,7 @@ def test_write_config_diff_reflects_changes(service, tmp_path):
# --- restart_service --- # --- restart_service ---
def test_restart_service_returns_true_on_success(service): def test_restart_service_returns_true_on_success(service):
with patch("mdb.services.dhcp_sync.getstatusoutput", return_value=(0, "")): with patch("mdb.services.dhcp_sync.getstatusoutput", return_value=(0, "")):
success, output = service.restart_service() success, output = service.restart_service()
@@ -107,7 +113,9 @@ def test_restart_service_returns_true_on_success(service):
def test_restart_service_returns_false_on_failure(service): def test_restart_service_returns_false_on_failure(service):
with patch("mdb.services.dhcp_sync.getstatusoutput", return_value=(1, "Job failed")): with patch(
"mdb.services.dhcp_sync.getstatusoutput", return_value=(1, "Job failed")
):
success, output = service.restart_service() success, output = service.restart_service()
assert success is False assert success is False
assert output == "Job failed" assert output == "Job failed"
+4
View File
@@ -31,6 +31,7 @@ def make_service(serial=2, active_serial=1, restart_success=True, restart_output
# --- handle: nothing to do --- # --- handle: nothing to do ---
def test_handle_prints_nothing_to_do_when_no_update_needed(cmd): def test_handle_prints_nothing_to_do_when_no_update_needed(cmd):
svc, config = make_service(serial=1, active_serial=1) svc, config = make_service(serial=1, active_serial=1)
svc.needs_update.return_value = False svc.needs_update.return_value = False
@@ -41,6 +42,7 @@ def test_handle_prints_nothing_to_do_when_no_update_needed(cmd):
# --- handle: stdout mode --- # --- handle: stdout mode ---
def test_handle_prints_config_to_stdout_when_no_output_path(cmd): def test_handle_prints_config_to_stdout_when_no_output_path(cmd):
svc, config = make_service() svc, config = make_service()
with patch("mdb.management.commands.dhcp_sync.DhcpSyncService", return_value=svc): with patch("mdb.management.commands.dhcp_sync.DhcpSyncService", return_value=svc):
@@ -58,6 +60,7 @@ def test_handle_does_not_write_file_when_no_output_path(cmd):
# --- handle: file mode --- # --- handle: file mode ---
def test_handle_calls_write_config_when_output_given(cmd, tmp_path): def test_handle_calls_write_config_when_output_given(cmd, tmp_path):
svc, config = make_service() svc, config = make_service()
output_path = str(tmp_path / "dhcpd.conf") output_path = str(tmp_path / "dhcpd.conf")
@@ -86,6 +89,7 @@ def test_handle_calls_restart_and_notify_when_restart_true(cmd, tmp_path):
# --- restart_and_notify --- # --- restart_and_notify ---
def test_restart_and_notify_success_writes_ok(cmd): def test_restart_and_notify_success_writes_ok(cmd):
svc, config = make_service(restart_success=True) svc, config = make_service(restart_success=True)
with patch("mdb.management.commands.dhcp_sync.discord"): with patch("mdb.management.commands.dhcp_sync.discord"):
+10 -3
View File
@@ -59,16 +59,23 @@ def test_send_json_posts_with_mdb_username():
def test_send_as_file_posts_multipart_with_attachment(): def test_send_as_file_posts_multipart_with_attachment():
mock_resp = MagicMock() mock_resp = MagicMock()
with patch("requests.post", return_value=mock_resp) as mock_post: with patch("requests.post", return_value=mock_resp) as mock_post:
discord._send_as_file("https://discord.example/webhook", "Subject", "full content") discord._send_as_file(
assert mock_post.call_args.kwargs.get("files") or "files" in str(mock_post.call_args) "https://discord.example/webhook", "Subject", "full content"
)
assert mock_post.call_args.kwargs.get("files") or "files" in str(
mock_post.call_args
)
mock_resp.raise_for_status.assert_called_once() mock_resp.raise_for_status.assert_called_once()
def test_send_as_file_content_mentions_attachment(): def test_send_as_file_content_mentions_attachment():
import json import json
mock_resp = MagicMock() mock_resp = MagicMock()
with patch("requests.post", return_value=mock_resp) as mock_post: with patch("requests.post", return_value=mock_resp) as mock_post:
discord._send_as_file("https://discord.example/webhook", "MySubject", "full content") discord._send_as_file(
"https://discord.example/webhook", "MySubject", "full content"
)
data = mock_post.call_args.kwargs["data"] data = mock_post.call_args.kwargs["data"]
payload = json.loads(data["payload_json"]) payload = json.loads(data["payload_json"])
assert "MySubject" in payload["content"] assert "MySubject" in payload["content"]