diff --git a/.gitignore b/.gitignore index c91623e..8f3eedc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.swp temp *.log -settings.py -dns_mdb.db +local_settings.py +db.sqlite3 /.idea /venv diff --git a/README b/README deleted file mode 100644 index 510496c..0000000 --- a/README +++ /dev/null @@ -1,17 +0,0 @@ -A Django app for managing computer equipment. -Generates DHCP configs and zone files. - -Dependencies: - sudo apt-get install libmysqlclient-dev python-dev libldap2-dev libsasl2-dev bind9utils - -Setup environment: - mdb$ virtualenv venv - mdb$ . venv/bin/activate - (venv)mdb$ pip install -r requirements.txt - -Create a config file: - mdb$ cp mdb/settings-sample.py mdb/settings.py - # Edit settings.py with your favorite editor - -Run the server: - (venv)mdb$ python manage.py runserver diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6956ad --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +A Django app for managing computer equipment. + +## Installation + sudo apt install libmysqlclient-dev python-dev libldap2-dev libsasl2-dev bind9utils + virtualenv venv + . venv/bin/activate + pip install -r requirements.txt + python manage.py migrate + python manage.py runserver + +## Development tasks +Dump test data: + + python manage.py dumpdata mdb --natural-foreign --indent=4 > mdb/fixtures/test_data.json + +## Features + +* Creates DHCPD configuration files +* Creates DNS zone files for BIND +* Creates PXE config files (with random secret for use with Puppet autosign) +* Partial IPv6 support diff --git a/manage.py b/manage.py index e59d7d2..f6ab478 100755 --- a/manage.py +++ b/manage.py @@ -1,9 +1,8 @@ #!/usr/bin/env python -import os, sys +import os +import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mdb.settings") - + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mdbsite.settings") from django.core.management import execute_from_command_line - execute_from_command_line(sys.argv) 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/admin.py b/mdb/admin.py index 6ababb0..e025f20 100644 --- a/mdb/admin.py +++ b/mdb/admin.py @@ -1,5 +1,8 @@ -from mdb.models import * -from django.contrib import admin +from django.contrib import admin, messages + +from mdb.models import Ip6Address, Interface, Ip4Address, DhcpOption, DhcpCustomField, DomainSrvRecord, DomainTxtRecord, \ + DomainCnameRecord, DomainARecord, Domain, Host, Ip4Subnet, Ip6Subnet, Nameserver, MailExchange, OperatingSystem, \ + HostType, DhcpConfig class Ip6AddressInline(admin.TabularInline): @@ -20,17 +23,17 @@ class HostAdmin(admin.ModelAdmin): ordering = ('hostname',) inlines = [InterfaceInline] list_display = ['hostname', 'owner', 'host_type', 'location', 'mac_addresses', 'ip_addresses', 'in_domain', - 'ipv6_enabled'] - list_filter = ['host_type', 'owner', 'location'] + '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 = ( ('Owner Information', { - 'fields': ('owner', 'location', 'description' ) + 'fields': ('owner', 'location', 'description') }), ('Hardware and Software Information', { - 'fields': (('brand', 'model', 'serial_number'), ('hostname', 'host_type'), ('operating_system', 'virtual')) + 'fields': (('hostname', 'host_type'), ('pxe_key', 'pxe_installable'), ('brand', 'model'), 'serial_number', ('operating_system', 'virtual')) }), # FIXME: Not in use # ('Domain and Kerberos Information', { @@ -40,6 +43,26 @@ class HostAdmin(admin.ModelAdmin): # ('kerberos_principal_name', 'kerberos_principal_created_date')) # }), ) + actions = ['set_installable'] + + def _get_host_warning_message(self, host, 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) + + def set_installable(self, request, queryset): + invalid_ifs_per_host = [] + + for obj in queryset: + invalid_ifs = obj.interface_set.filter(ip4address__isnull=True) + if invalid_ifs.exists(): + invalid_ifs_per_host.append(self._get_host_warning_message(obj, invalid_ifs)) + obj.pxe_installable = True + obj.save() # triggers post save signal + + if invalid_ifs_per_host: + self.message_user(request, message='\n'.join(invalid_ifs_per_host), level=messages.WARNING) + + set_installable.short_description = "Mark selected hosts as PXE installable" def get_queryset(self, request): return super(HostAdmin, self).get_queryset(request).select_related() diff --git a/__init__.py b/mdb/api/__init__.py similarity index 100% rename from __init__.py rename to mdb/api/__init__.py diff --git a/mdb/api/serializers.py b/mdb/api/serializers.py new file mode 100644 index 0000000..eb5e7d7 --- /dev/null +++ b/mdb/api/serializers.py @@ -0,0 +1,46 @@ +from mdb.models import Host +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + + +class HostPXEValidateSerializer(serializers.Serializer): + certname = serializers.CharField(required=True) + pxe_key = serializers.CharField(required=True) + host = None + + def _get_host_by_fqdn(self, attrs): + fqdn = attrs['certname'] + host_name = fqdn.split('.')[0] + domain = '.'.join(fqdn.split('.')[1:]) + + try: + return Host.objects.get(hostname=host_name, interface__domain__domain_name=domain) + except (Host.DoesNotExist, Host.MultipleObjectsReturned): + return None + + def validate(self, attrs): + # custom validation + host = self._get_host_by_fqdn(attrs) + + if host is None: + raise ValidationError('Host \'{}\' not found.'.format(attrs['certname'])) + + if not host.pxe_installable: + raise ValidationError('Host \'{}\' is marked as not installable via PXE.'.format(attrs['certname'])) + + if not host.pxe_key: + raise ValidationError('Host \'{}\' has no pxe_key.'.format(attrs['certname'])) + + if host.pxe_key != attrs['pxe_key']: + raise ValidationError('Supplied pxe_key does not match host \'{}\'.'.format(attrs['certname'])) + + self.host = host + + return attrs + + def create(self, validated_data): + # Make requests kind of idempotent by only allowing 1 request / installation + self.host.pxe_installable = False + self.host.save() + + return self.host diff --git a/mdb/api/views.py b/mdb/api/views.py new file mode 100644 index 0000000..dd9ec89 --- /dev/null +++ b/mdb/api/views.py @@ -0,0 +1,20 @@ +from rest_framework.authentication import TokenAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from mdb.api.serializers import HostPXEValidateSerializer + + +class HostPXEValidate(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request): + serializer = HostPXEValidateSerializer(data=request.data) + + serializer.is_valid(raise_exception=True) + + host = serializer.save() # sets Host.pxe_installable=False + + return Response({'valid_pxe_key': True}) 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/fixtures/test_data.json b/mdb/fixtures/test_data.json new file mode 100644 index 0000000..1da19d9 --- /dev/null +++ b/mdb/fixtures/test_data.json @@ -0,0 +1,2756 @@ +[ +{ + "model": "mdb.nameserver", + "fields": { + "hostname": "sega.neuf.no" + }, + "pk": 1 +}, +{ + "model": "mdb.nameserver", + "fields": { + "hostname": "atari.neuf.no" + }, + "pk": 2 +}, +{ + "model": "mdb.mailexchange", + "fields": { + "priority": 10, + "hostname": "mx.neuf.no" + }, + "pk": 1 +}, +{ + "model": "mdb.domain", + "fields": { + "domain_active_serial": 0, + "domain_soa": "sega.neuf.no", + "domain_retry": 7200, + "domain_expire": 604800, + "domain_ipaddr": "127.0.0.1", + "domain_mailexchanges": [ + 1 + ], + "domain_minimum_ttl": 86400, + "domain_ttl": 3600, + "domain_refresh": 28800, + "domain_admin": "sysadmin@neuf.no", + "domain_serial": 2015100313, + "domain_nameservers": [ + 1, + 2 + ], + "domain_filename": "/tmp/bind-db.edb.technology", + "created_date": "2015-10-03T13:02:53.581", + "domain_name": "edb.technology" + }, + "pk": 1 +}, +{ + "model": "mdb.dhcpconfig", + "fields": { + "log_facility": "local7", + "max_lease_time": 7200, + "ddns_update_style": "none", + "authoritative": false, + "serial": 2015100401, + "active_serial": 1, + "name": "default", + "default_lease_time": 600 + }, + "pk": 1 +}, +{ + "model": "mdb.ip6subnet", + "fields": { + "created_date": "2015-10-04T12:50:12.248", + "domain_soa": "sega.neuf.no", + "domain_retry": 7200, + "domain_expire": 604800, + "domain_ttl": 3600, + "domain_minimum_ttl": 86400, + "network": "2001:16d8:eeb6:3", + "domain_serial": 1, + "domain_refresh": 28800, + "domain_admin": "sysadmin@neuf.no", + "domain_active_serial": 0, + "netmask": 64, + "domain_nameservers": [ + 1, + 2 + ], + "domain_filename": "/tmp/bind-reverse-v6-2001-16d8-eeb6-3-64.ip6.arpa", + "name": "client-ipv6", + "domain_name": "3.0.0.0.6.b.e.e.8.d.6.1.1.0.0.2.ip6.arpa" + }, + "pk": 1 +}, +{ + "model": "mdb.ip4subnet", + "fields": { + "created_date": "2015-10-03T13:08:17.765", + "domain_soa": "sega.neuf.no", + "dhcp_config": 1, + "domain_retry": 7200, + "dhcp_dynamic_start": "10.0.3.2", + "domain_expire": 604800, + "domain_ttl": 3600, + "domain_nameservers": [ + 1, + 2 + ], + "dhcp_dynamic_end": "10.0.3.254", + "domain_minimum_ttl": 86400, + "network": "10.0.3.0", + "domain_serial": 2015100313, + "domain_refresh": 28800, + "domain_admin": "sysadmin@neuf.no", + "domain_active_serial": 0, + "netmask": "255.255.255.0", + "dhcp_dynamic": true, + "domain_filename": "/tmp/bind-db.3.0.10.in-addr.arpa", + "name": "clients", + "domain_name": "3.0.10.in-addr.arpa" + }, + "pk": 1 +}, +{ + "model": "mdb.dhcpoption", + "fields": { + "key": "domain-name", + "ip4subnet": 1, + "value": "\"edb.technology\"" + }, + "pk": 1 +}, +{ + "model": "mdb.dhcpoption", + "fields": { + "key": "domain-name-servers", + "ip4subnet": 1, + "value": "158.36.190.194, 158.36.190.199, 8.8.8.8" + }, + "pk": 2 +}, +{ + "model": "mdb.dhcpoption", + "fields": { + "key": "routers", + "ip4subnet": 1, + "value": "158.36.186.1" + }, + "pk": 3 +}, +{ + "model": "mdb.dhcpcustomfield", + "fields": { + "ip4subnet": 1, + "value": "deny unknown-clients" + }, + "pk": 1 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.1", + "last_contact": null + }, + "pk": 1 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.2", + "last_contact": null + }, + "pk": 2 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.3", + "last_contact": null + }, + "pk": 3 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.4", + "last_contact": null + }, + "pk": 4 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.5", + "last_contact": null + }, + "pk": 5 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.6", + "last_contact": null + }, + "pk": 6 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.7", + "last_contact": null + }, + "pk": 7 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.8", + "last_contact": null + }, + "pk": 8 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.9", + "last_contact": null + }, + "pk": 9 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.10", + "last_contact": null + }, + "pk": 10 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.11", + "last_contact": null + }, + "pk": 11 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.12", + "last_contact": null + }, + "pk": 12 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.13", + "last_contact": null + }, + "pk": 13 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.14", + "last_contact": null + }, + "pk": 14 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.15", + "last_contact": null + }, + "pk": 15 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.16", + "last_contact": null + }, + "pk": 16 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.17", + "last_contact": null + }, + "pk": 17 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.18", + "last_contact": null + }, + "pk": 18 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.19", + "last_contact": null + }, + "pk": 19 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.20", + "last_contact": null + }, + "pk": 20 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.21", + "last_contact": null + }, + "pk": 21 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.22", + "last_contact": null + }, + "pk": 22 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.23", + "last_contact": null + }, + "pk": 23 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.24", + "last_contact": null + }, + "pk": 24 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.25", + "last_contact": null + }, + "pk": 25 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.26", + "last_contact": null + }, + "pk": 26 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.27", + "last_contact": null + }, + "pk": 27 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.28", + "last_contact": null + }, + "pk": 28 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.29", + "last_contact": null + }, + "pk": 29 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.30", + "last_contact": null + }, + "pk": 30 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.31", + "last_contact": null + }, + "pk": 31 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.32", + "last_contact": null + }, + "pk": 32 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.33", + "last_contact": null + }, + "pk": 33 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.34", + "last_contact": null + }, + "pk": 34 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.35", + "last_contact": null + }, + "pk": 35 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.36", + "last_contact": null + }, + "pk": 36 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.37", + "last_contact": null + }, + "pk": 37 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.38", + "last_contact": null + }, + "pk": 38 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.39", + "last_contact": null + }, + "pk": 39 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.40", + "last_contact": null + }, + "pk": 40 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.41", + "last_contact": null + }, + "pk": 41 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.42", + "last_contact": null + }, + "pk": 42 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.43", + "last_contact": null + }, + "pk": 43 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.44", + "last_contact": null + }, + "pk": 44 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.45", + "last_contact": null + }, + "pk": 45 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.46", + "last_contact": null + }, + "pk": 46 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.47", + "last_contact": null + }, + "pk": 47 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.48", + "last_contact": null + }, + "pk": 48 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.49", + "last_contact": null + }, + "pk": 49 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.50", + "last_contact": null + }, + "pk": 50 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.51", + "last_contact": null + }, + "pk": 51 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.52", + "last_contact": null + }, + "pk": 52 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.53", + "last_contact": null + }, + "pk": 53 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.54", + "last_contact": null + }, + "pk": 54 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.55", + "last_contact": null + }, + "pk": 55 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.56", + "last_contact": null + }, + "pk": 56 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.57", + "last_contact": null + }, + "pk": 57 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.58", + "last_contact": null + }, + "pk": 58 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.59", + "last_contact": null + }, + "pk": 59 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.60", + "last_contact": null + }, + "pk": 60 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.61", + "last_contact": null + }, + "pk": 61 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.62", + "last_contact": null + }, + "pk": 62 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.63", + "last_contact": null + }, + "pk": 63 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.64", + "last_contact": null + }, + "pk": 64 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.65", + "last_contact": null + }, + "pk": 65 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.66", + "last_contact": null + }, + "pk": 66 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.67", + "last_contact": null + }, + "pk": 67 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.68", + "last_contact": null + }, + "pk": 68 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.69", + "last_contact": null + }, + "pk": 69 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.70", + "last_contact": null + }, + "pk": 70 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.71", + "last_contact": null + }, + "pk": 71 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.72", + "last_contact": null + }, + "pk": 72 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.73", + "last_contact": null + }, + "pk": 73 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.74", + "last_contact": null + }, + "pk": 74 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.75", + "last_contact": null + }, + "pk": 75 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.76", + "last_contact": null + }, + "pk": 76 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.77", + "last_contact": null + }, + "pk": 77 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.78", + "last_contact": null + }, + "pk": 78 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.79", + "last_contact": null + }, + "pk": 79 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.80", + "last_contact": null + }, + "pk": 80 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.81", + "last_contact": null + }, + "pk": 81 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.82", + "last_contact": null + }, + "pk": 82 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.83", + "last_contact": null + }, + "pk": 83 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.84", + "last_contact": null + }, + "pk": 84 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.85", + "last_contact": null + }, + "pk": 85 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.86", + "last_contact": null + }, + "pk": 86 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.87", + "last_contact": null + }, + "pk": 87 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.88", + "last_contact": null + }, + "pk": 88 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.89", + "last_contact": null + }, + "pk": 89 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.90", + "last_contact": null + }, + "pk": 90 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.91", + "last_contact": null + }, + "pk": 91 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.92", + "last_contact": null + }, + "pk": 92 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.93", + "last_contact": null + }, + "pk": 93 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.94", + "last_contact": null + }, + "pk": 94 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.95", + "last_contact": null + }, + "pk": 95 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.96", + "last_contact": null + }, + "pk": 96 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.97", + "last_contact": null + }, + "pk": 97 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.98", + "last_contact": null + }, + "pk": 98 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.99", + "last_contact": null + }, + "pk": 99 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.100", + "last_contact": null + }, + "pk": 100 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.101", + "last_contact": null + }, + "pk": 101 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.102", + "last_contact": null + }, + "pk": 102 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.103", + "last_contact": null + }, + "pk": 103 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.104", + "last_contact": null + }, + "pk": 104 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.105", + "last_contact": null + }, + "pk": 105 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.106", + "last_contact": null + }, + "pk": 106 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.107", + "last_contact": null + }, + "pk": 107 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.108", + "last_contact": null + }, + "pk": 108 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.109", + "last_contact": null + }, + "pk": 109 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.110", + "last_contact": null + }, + "pk": 110 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.111", + "last_contact": null + }, + "pk": 111 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.112", + "last_contact": null + }, + "pk": 112 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.113", + "last_contact": null + }, + "pk": 113 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.114", + "last_contact": null + }, + "pk": 114 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.115", + "last_contact": null + }, + "pk": 115 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.116", + "last_contact": null + }, + "pk": 116 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.117", + "last_contact": null + }, + "pk": 117 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.118", + "last_contact": null + }, + "pk": 118 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.119", + "last_contact": null + }, + "pk": 119 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.120", + "last_contact": null + }, + "pk": 120 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.121", + "last_contact": null + }, + "pk": 121 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.122", + "last_contact": null + }, + "pk": 122 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.123", + "last_contact": null + }, + "pk": 123 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.124", + "last_contact": null + }, + "pk": 124 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.125", + "last_contact": null + }, + "pk": 125 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.126", + "last_contact": null + }, + "pk": 126 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.127", + "last_contact": null + }, + "pk": 127 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.128", + "last_contact": null + }, + "pk": 128 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.129", + "last_contact": null + }, + "pk": 129 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.130", + "last_contact": null + }, + "pk": 130 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.131", + "last_contact": null + }, + "pk": 131 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.132", + "last_contact": null + }, + "pk": 132 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.133", + "last_contact": null + }, + "pk": 133 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.134", + "last_contact": null + }, + "pk": 134 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.135", + "last_contact": null + }, + "pk": 135 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.136", + "last_contact": null + }, + "pk": 136 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.137", + "last_contact": null + }, + "pk": 137 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.138", + "last_contact": null + }, + "pk": 138 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.139", + "last_contact": null + }, + "pk": 139 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.140", + "last_contact": null + }, + "pk": 140 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.141", + "last_contact": null + }, + "pk": 141 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.142", + "last_contact": null + }, + "pk": 142 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.143", + "last_contact": null + }, + "pk": 143 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.144", + "last_contact": null + }, + "pk": 144 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.145", + "last_contact": null + }, + "pk": 145 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.146", + "last_contact": null + }, + "pk": 146 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.147", + "last_contact": null + }, + "pk": 147 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.148", + "last_contact": null + }, + "pk": 148 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.149", + "last_contact": null + }, + "pk": 149 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.150", + "last_contact": null + }, + "pk": 150 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.151", + "last_contact": null + }, + "pk": 151 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.152", + "last_contact": null + }, + "pk": 152 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.153", + "last_contact": null + }, + "pk": 153 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.154", + "last_contact": null + }, + "pk": 154 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.155", + "last_contact": null + }, + "pk": 155 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.156", + "last_contact": null + }, + "pk": 156 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.157", + "last_contact": null + }, + "pk": 157 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.158", + "last_contact": null + }, + "pk": 158 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.159", + "last_contact": null + }, + "pk": 159 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.160", + "last_contact": null + }, + "pk": 160 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.161", + "last_contact": null + }, + "pk": 161 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.162", + "last_contact": null + }, + "pk": 162 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.163", + "last_contact": null + }, + "pk": 163 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.164", + "last_contact": null + }, + "pk": 164 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.165", + "last_contact": null + }, + "pk": 165 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.166", + "last_contact": null + }, + "pk": 166 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.167", + "last_contact": null + }, + "pk": 167 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.168", + "last_contact": null + }, + "pk": 168 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.169", + "last_contact": null + }, + "pk": 169 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.170", + "last_contact": null + }, + "pk": 170 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.171", + "last_contact": null + }, + "pk": 171 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.172", + "last_contact": null + }, + "pk": 172 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.173", + "last_contact": null + }, + "pk": 173 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.174", + "last_contact": null + }, + "pk": 174 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.175", + "last_contact": null + }, + "pk": 175 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.176", + "last_contact": null + }, + "pk": 176 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.177", + "last_contact": null + }, + "pk": 177 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.178", + "last_contact": null + }, + "pk": 178 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.179", + "last_contact": null + }, + "pk": 179 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.180", + "last_contact": null + }, + "pk": 180 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.181", + "last_contact": null + }, + "pk": 181 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.182", + "last_contact": null + }, + "pk": 182 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.183", + "last_contact": null + }, + "pk": 183 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.184", + "last_contact": null + }, + "pk": 184 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.185", + "last_contact": null + }, + "pk": 185 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.186", + "last_contact": null + }, + "pk": 186 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.187", + "last_contact": null + }, + "pk": 187 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.188", + "last_contact": null + }, + "pk": 188 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.189", + "last_contact": null + }, + "pk": 189 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.190", + "last_contact": null + }, + "pk": 190 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.191", + "last_contact": null + }, + "pk": 191 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.192", + "last_contact": null + }, + "pk": 192 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.193", + "last_contact": null + }, + "pk": 193 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.194", + "last_contact": null + }, + "pk": 194 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.195", + "last_contact": null + }, + "pk": 195 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.196", + "last_contact": null + }, + "pk": 196 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.197", + "last_contact": null + }, + "pk": 197 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.198", + "last_contact": null + }, + "pk": 198 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.199", + "last_contact": null + }, + "pk": 199 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.200", + "last_contact": null + }, + "pk": 200 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.201", + "last_contact": null + }, + "pk": 201 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.202", + "last_contact": null + }, + "pk": 202 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.203", + "last_contact": null + }, + "pk": 203 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.204", + "last_contact": null + }, + "pk": 204 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.205", + "last_contact": null + }, + "pk": 205 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.206", + "last_contact": null + }, + "pk": 206 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.207", + "last_contact": null + }, + "pk": 207 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.208", + "last_contact": null + }, + "pk": 208 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.209", + "last_contact": null + }, + "pk": 209 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.210", + "last_contact": null + }, + "pk": 210 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.211", + "last_contact": null + }, + "pk": 211 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.212", + "last_contact": null + }, + "pk": 212 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.213", + "last_contact": null + }, + "pk": 213 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.214", + "last_contact": null + }, + "pk": 214 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.215", + "last_contact": null + }, + "pk": 215 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.216", + "last_contact": null + }, + "pk": 216 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.217", + "last_contact": null + }, + "pk": 217 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.218", + "last_contact": null + }, + "pk": 218 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.219", + "last_contact": null + }, + "pk": 219 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.220", + "last_contact": null + }, + "pk": 220 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.221", + "last_contact": null + }, + "pk": 221 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.222", + "last_contact": null + }, + "pk": 222 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.223", + "last_contact": null + }, + "pk": 223 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.224", + "last_contact": null + }, + "pk": 224 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.225", + "last_contact": null + }, + "pk": 225 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.226", + "last_contact": null + }, + "pk": 226 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.227", + "last_contact": null + }, + "pk": 227 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.228", + "last_contact": null + }, + "pk": 228 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.229", + "last_contact": null + }, + "pk": 229 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.230", + "last_contact": null + }, + "pk": 230 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.231", + "last_contact": null + }, + "pk": 231 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.232", + "last_contact": null + }, + "pk": 232 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.233", + "last_contact": null + }, + "pk": 233 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.234", + "last_contact": null + }, + "pk": 234 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.235", + "last_contact": null + }, + "pk": 235 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.236", + "last_contact": null + }, + "pk": 236 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.237", + "last_contact": null + }, + "pk": 237 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.238", + "last_contact": null + }, + "pk": 238 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.239", + "last_contact": null + }, + "pk": 239 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.240", + "last_contact": null + }, + "pk": 240 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.241", + "last_contact": null + }, + "pk": 241 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.242", + "last_contact": null + }, + "pk": 242 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.243", + "last_contact": null + }, + "pk": 243 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.244", + "last_contact": null + }, + "pk": 244 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.245", + "last_contact": null + }, + "pk": 245 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.246", + "last_contact": null + }, + "pk": 246 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.247", + "last_contact": null + }, + "pk": 247 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.248", + "last_contact": null + }, + "pk": 248 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.249", + "last_contact": null + }, + "pk": 249 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.250", + "last_contact": null + }, + "pk": 250 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.251", + "last_contact": null + }, + "pk": 251 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.252", + "last_contact": null + }, + "pk": 252 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.253", + "last_contact": null + }, + "pk": 253 +}, +{ + "model": "mdb.ip4address", + "fields": { + "subnet": 1, + "ping_avg_rtt": null, + "address": "10.0.3.254", + "last_contact": null + }, + "pk": 254 +}, +{ + "model": "mdb.hosttype", + "fields": { + "description": "Client machine (puppet controlled)", + "host_type": "client" + }, + "pk": 1 +}, +{ + "model": "mdb.operatingsystem", + "fields": { + "arch": "x86_64", + "version": "14.04", + "name": "ubuntu" + }, + "pk": 1 +}, +{ + "model": "mdb.host", + "fields": { + "kerberos_principal_created": false, + "model": "OptiPlex 760", + "location": "kak", + "kerberos_principal_created_date": null, + "owner": "edb", + "created_date": "2015-10-03T13:23:37.061", + "hostname": "tut", + "host_type": 1, + "serial_number": "12345678", + "virtual": false, + "brand": "Dell", + "pxe_installable": true, + "kerberos_principal_name": "", + "description": "testclient", + "pxe_key": "e17b532276e247fe80b92540bef71007", + "operating_system": 1, + "request_kerberos_principal": false + }, + "pk": 1 +}, +{ + "model": "mdb.interface", + "fields": { + "created_date": "2015-10-03T13:23:37.063", + "domain": 1, + "macaddr": "f0:de:f1:58:be:1a", + "pxe_filename": "pxelinux.0", + "dhcp_client": true, + "ip4address": 2, + "name": "eth0", + "host": 1 + }, + "pk": 1 +}, +{ + "model": "mdb.ip6address", + "fields": { + "subnet": 1, + "interface": 1, + "address": "::2" + }, + "pk": 1 +} +] diff --git a/mdb/management/commands/dhcp-sync.py b/mdb/management/commands/dhcp-sync.py index d12124e..0f62474 100644 --- a/mdb/management/commands/dhcp-sync.py +++ b/mdb/management/commands/dhcp-sync.py @@ -6,7 +6,11 @@ from difflib import context_diff from django.core.management.base import BaseCommand from django.core.mail import mail_admins from optparse import make_option -from commands import getstatusoutput +try: + from subprocess import getstatusoutput +except ImportError: + from commands import getstatusoutput + from mdb.models import DhcpConfig @@ -42,13 +46,13 @@ class Command(BaseCommand): # do we need to do anything? if config.serial == config.active_serial and not force: - print "Nothing to do, serial is the same: %s" % config.serial + print("Nothing to do, serial is the same: %s" % config.serial) sys.exit(0) # write to stdout if output == sys.stdout: - print config.dhcpd_configuration() - print 'Not writing to file, serial not updated' + print(config.dhcpd_configuration()) + print('Not writing to file, serial not updated') return # read current file contents @@ -71,7 +75,7 @@ class Command(BaseCommand): tofile=config.serial, lineterm='')) - print diff + print(diff) # update serial old_serial = config.active_serial diff --git a/mdb/management/commands/zone-sync.py b/mdb/management/commands/zone-sync.py index 6b1bc4e..ea57037 100644 --- a/mdb/management/commands/zone-sync.py +++ b/mdb/management/commands/zone-sync.py @@ -7,8 +7,12 @@ from difflib import context_diff from django.core.management.base import BaseCommand from django.core.mail import mail_admins from optparse import make_option -from commands import getstatusoutput -from mdb.models import * +try: + from subprocess import getstatusoutput +except ImportError: + from commands import getstatusoutput + +from mdb.models import Domain, Ip4Subnet, Ip6Subnet class Command(BaseCommand): @@ -43,7 +47,7 @@ class Command(BaseCommand): # do the binaries exist? for f in (self.checkzone_bin, self.bind_bin): if not os.path.isfile(f): - print "ERROR: no such file %s, exiting..." % f + print("ERROR: no such file %s, exiting..." % f) if not debug: sys.exit(1) @@ -106,11 +110,11 @@ class Command(BaseCommand): 'diff': None, } - print "updating %s %s [%d -> %d]" % ( + print("updating %s %s [%d -> %d]" % ( zonetype, zone.domain_name, zone.domain_active_serial, - zone.domain_serial) + zone.domain_serial)) # generate zone file contents new = zone.zone_file_contents() diff --git a/mdb/migrations/0006_auto_20151001_2235.py b/mdb/migrations/0006_auto_20151001_2235.py new file mode 100644 index 0000000..bb38709 --- /dev/null +++ b/mdb/migrations/0006_auto_20151001_2235.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.core.validators +import re + + +class Migration(migrations.Migration): + + dependencies = [ + ('mdb', '0005_auto_20150822_2204'), + ] + + operations = [ + migrations.AlterModelOptions( + name='dhcpcustomfield', + options={'verbose_name': 'DHCP custom field'}, + ), + migrations.AlterField( + model_name='domain', + name='domain_admin', + field=models.EmailField(max_length=254), + ), + migrations.AlterField( + model_name='domain', + name='domain_ipaddr', + field=models.GenericIPAddressField(protocol='IPv4'), + ), + migrations.AlterField( + model_name='host', + name='hostname', + field=models.CharField(validators=[django.core.validators.RegexValidator(code='invalid', message='Enter a valid hostname', regex=re.compile('^(?!-)[-a-z0-9]+(? 0: - content += "\tfilename \"%s\";\n" % interface.pxe_filename + (_if.host.hostname, _if.domain.domain_name) + if len(_if.pxe_filename) > 0: + content += "\tfilename \"%s\";\n" % _if.pxe_filename content += "}\n" return content - def __unicode__(self): + def __str__(self): return self.name class Meta: verbose_name = 'DHCP config' +@python_2_unicode_compatible class Ip6Subnet(models.Model): name = models.CharField(max_length=255) network = models.CharField(max_length=255) @@ -254,7 +279,7 @@ class Ip6Subnet(models.Model): domain_admin = models.EmailField() domain_filename = models.CharField(max_length=256) - def __unicode__(self): + def __str__(self): return self.network + " (" + self.name + ")" def zone_file_contents(self, generate_unassigned=False): @@ -292,7 +317,7 @@ class Ip6Subnet(models.Model): addr.interface.host.hostname, addr.interface.domain.domain_name) - ip = ipaddr.IPv6Address(self.network + addr.address) + ip = ipaddress.IPv6Address(self.network + addr.address) ip = ".".join(ip.exploded.replace(":", "")[16:])[::-1] content += "%s\tPTR\t%s.\n" % (ip, hostname) @@ -305,10 +330,11 @@ class Ip6Subnet(models.Model): verbose_name = 'IPv6 subnet' +@python_2_unicode_compatible class Ip4Subnet(models.Model): name = models.CharField(max_length=256) - netmask = models.IPAddressField() - network = models.IPAddressField() + netmask = models.GenericIPAddressField(protocol='IPv4') + network = models.GenericIPAddressField(protocol='IPv4') created_date = models.DateTimeField(auto_now_add=True) domain_name = models.CharField(max_length=255, editable=False) domain_nameservers = models.ManyToManyField(Nameserver) @@ -324,31 +350,28 @@ class Ip4Subnet(models.Model): domain_filename = models.CharField(max_length=256) dhcp_dynamic = models.BooleanField(default=False) - dhcp_dynamic_start = models.IPAddressField(null=True, blank=True) - dhcp_dynamic_end = models.IPAddressField(null=True, blank=True) + dhcp_dynamic_start = models.GenericIPAddressField(protocol='IPv4', null=True, blank=True) + dhcp_dynamic_end = models.GenericIPAddressField(protocol='IPv4', null=True, blank=True) dhcp_config = models.ForeignKey(DhcpConfig) - def __unicode__(self): + def __str__(self): return self.network + " (" + self.name + ")" def num_addresses(self): - subnet = ipaddr.IPv4Network(self.network + "/" + self.netmask) - return subnet.numhosts + subnet = ipaddress.IPv4Network(self.network + "/" + self.netmask) + return subnet.num_addresses def broadcast_address(self): - subnet = ipaddr.IPv4Network(self.network + "/" + self.netmask) - return subnet.broadcast + subnet = ipaddress.IPv4Network(self.network + "/" + self.netmask) + return subnet.broadcast_address def first_address(self): - subnet = ipaddr.IPv4Network(self.network + "/" + self.netmask) - return subnet.iterhosts().next() + subnet = ipaddress.IPv4Network(self.network + "/" + self.netmask) + return next(subnet.hosts()) def last_address(self): - subnet = ipaddr.IPv4Network(self.network + "/" + self.netmask) - for curr in subnet.iterhosts(): - pass # horribly ineficcient - - return curr + subnet = ipaddress.IPv4Network(self.network + "/" + self.netmask) + return list(subnet.hosts())[-1] broadcast_address.short_description = 'broadcast' num_addresses.short_description = '#addresses' @@ -378,26 +401,18 @@ class Ip4Subnet(models.Model): content += ";\n" for addr in self.ip4address_set.all(): - if addr.interface_set.count() == 0 and generate_unassigned: + """ Get PTR records for each v4 address in subnet """ + if hasattr(addr, 'interface') and addr.interface.domain: + hostname = "%s.%s" % ( + addr.interface.host.hostname, + addr.interface.domain.domain_name) + content += "%-20s\tIN\tPTR\t%s.\n" % \ + (addr.address.split(".")[3], hostname) + elif not hasattr(addr, 'interface') and generate_unassigned: content += "%s\tIN\tPTR\t%s.%s\n" % ( addr.address, addr.address.split(".")[3], "dhcp.neuf.no.") - continue - - for interface in addr.interface_set.all(): - if interface.domain is None and generate_unassigned: - content += "%s\tIN\tPTR\t%s.%s.\n" % ( - addr.address, - addr.address.split(".")[3], - "dhcp.neuf.no") - - else: - hostname = "%s.%s" % ( - interface.host.hostname, - interface.domain.domain_name) - content += "%-20s\tIN\tPTR\t%s.\n" % \ - (addr.address.split(".")[3], hostname) return content @@ -405,12 +420,13 @@ class Ip4Subnet(models.Model): verbose_name = 'IPv4 subnet' +@python_2_unicode_compatible class DhcpOption(models.Model): key = models.CharField(max_length=255) value = models.CharField(max_length=255) ip4subnet = models.ForeignKey(Ip4Subnet) - def __unicode__(self): + def __str__(self): return self.key + " " + self.value class Meta: @@ -425,25 +441,22 @@ class DhcpCustomField(models.Model): verbose_name = 'DHCP custom field' +@python_2_unicode_compatible class Ip4Address(models.Model): subnet = models.ForeignKey(Ip4Subnet) - address = models.IPAddressField() + address = models.GenericIPAddressField(protocol='IPv4') last_contact = models.DateTimeField(null=True, blank=True) ping_avg_rtt = models.FloatField(null=True, blank=True) - def __unicode__(self): - # FIXME: generates 2 SQL queries - _if = self.interface_set.all() - if not _if: + def __str__(self): + # FIXME: generates 2 SQL queries? + if not hasattr(self, 'interface'): return self.address - return "{} ({})".format( - self.address, - _if.get().host.hostname - ) + return "{} ({})".format(self.address, self.interface.host.hostname) def assigned_to_host(self): - return self.interface_set.get().host + return self.interface.host assigned_to_host.short_description = "Assigned to Host" @@ -452,11 +465,12 @@ class Ip4Address(models.Model): verbose_name_plural = 'IPv4 addresses' +@python_2_unicode_compatible class HostType(models.Model): host_type = models.CharField(max_length=64) description = models.CharField(max_length=1024) - def __unicode__(self): + def __str__(self): return self.host_type def num_members(self): @@ -466,6 +480,7 @@ class HostType(models.Model): ordering = ("host_type",) +@python_2_unicode_compatible class OperatingSystem(models.Model): OS_ARCHITECTURES = ( ('mips', _('MIPS')), @@ -482,13 +497,14 @@ class OperatingSystem(models.Model): version = models.CharField(max_length=64) arch = models.CharField(max_length=255, choices=OS_ARCHITECTURES, blank=True, null=True) - def __unicode__(self): + def __str__(self): return self.name + " " + self.version + " (" + self.arch + ")" class Meta: ordering = ("name", "version") +@python_2_unicode_compatible class Host(models.Model): location = models.CharField(max_length=1024) brand = models.CharField(max_length=1024) @@ -507,12 +523,15 @@ class Host(models.Model): kerberos_principal_name = models.CharField(max_length=256, editable=False) kerberos_principal_created_date = models.DateTimeField(null=True, blank=True, editable=False) - def __unicode__(self): + pxe_key = models.CharField(max_length=254, blank=True) + pxe_installable = models.BooleanField(default=False) + + def __str__(self): return self.hostname def in_domain(self): domains = self.interface_set.values_list('domain__domain_name', flat=True) - return u",".join(domains) + return ",".join(domains) in_domain.short_description = "in domains" @@ -524,39 +543,44 @@ class Host(models.Model): def mac_addresses(self): addresses = self.interface_set.filter(macaddr__isnull=False).values_list('macaddr', flat=True) - return ",".join(addresses) + return ", ".join(addresses) def ip_addresses(self): addresses = self.interface_set.filter(ip4address__isnull=False).values_list('ip4address__address', flat=True) - return ",".join(addresses) + return ", ".join(addresses) + + def as_pxe_files(self): + return host_as_pxe_files(self) +@python_2_unicode_compatible class Interface(models.Model): name = models.CharField(max_length=128) macaddr = models.CharField(max_length=17, validators=[validate_macaddr]) pxe_filename = models.CharField(max_length=64, blank=True) dhcp_client = models.BooleanField(default=False) host = models.ForeignKey(Host) - ip4address = models.ForeignKey(Ip4Address, blank=True, null=True, unique=True) + ip4address = models.OneToOneField(Ip4Address, blank=True, null=True) created_date = models.DateTimeField(auto_now_add=True) domain = models.ForeignKey(Domain) - def __unicode__(self): + def __str__(self): return "%s (%s on %s)" % (self.macaddr, self.name, self.host.hostname) def ipv6_enabled(self): - return self.ip6address_set.count() > 0 + return self.ip6address_set.exists() +@python_2_unicode_compatible class Ip6Address(models.Model): subnet = models.ForeignKey(Ip6Subnet) - address = models.CharField(max_length=64) + address = models.GenericIPAddressField(protocol='IPv6') interface = models.ForeignKey(Interface) def full_address(self): return self.subnet.network + self.address - def __unicode__(self): + def __str__(self): return "%s (%s on %s)" % ( self.full_address(), self.interface.name, @@ -565,125 +589,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 = ipaddr.IPv4Network(instance.network + "/" + instance.netmask) - - for addr in subnet.iterhosts(): - 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 = ipaddr.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/settings-sample.py b/mdb/settings-sample.py deleted file mode 100644 index 32b2658..0000000 --- a/mdb/settings-sample.py +++ /dev/null @@ -1,191 +0,0 @@ -# Django settings for dns_mdb project. - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - ('System Administrator', 'sysadmin@example.com'), -) - -MANAGERS = ADMINS - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': 'mdb.db', # Or path to database file if using sqlite3. - 'USER': 'mdb', # Not used with sqlite3. - 'PASSWORD': 'mdb', # Not used with sqlite3. - 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '3306', # Set to empty string for default. Not used with sqlite3. - } -} - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'Europe/Oslo' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale -USE_L10N = True - -# Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/home/media/media.lawrence.com/media/" -MEDIA_ROOT = '' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash. -# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" -MEDIA_URL = '' - -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/home/media/media.lawrence.com/static/" -STATIC_ROOT = '' - -# URL prefix for static files. -# Example: "http://media.lawrence.com/static/" -STATIC_URL = '/static/' - -# URL prefix for admin static files -- CSS, JavaScript and images. -# Make sure to use a trailing slash. -# Examples: "http://foo.com/static/admin/", "/static/admin/". -ADMIN_MEDIA_PREFIX = '/media/' - -# Additional locations of static files -STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# Make this unique, and don't share it with anybody. -SECRET_KEY = '@4ps=ebofcpv8sb#y7gm896=l&l*u0u-pgo7bcdj%+(k+2lqnl' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', -) - -ROOT_URLCONF = 'urls' - -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'mdb', - 'suit', - 'django.contrib.admin', -) - -# Defines a test runner -TEST_RUNNER = 'django.test.runner.DiscoverRunner' - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} - -EMAIL_SUBJECT_PREFIX = '[MDB] ' -EMAIL_HOST = 'smtp.example.com' -EMAIL_PORT = 25 - - -# for django-suit -from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS as TCP - -TEMPLATE_CONTEXT_PROCESSORS = TCP + ( - 'django.core.context_processors.request', -) - -# django-suit configuration -SUIT_CONFIG = { - # header - 'ADMIN_NAME': 'MDB', - 'HEADER_DATE_FORMAT': 'l, Y-m-d', - 'HEADER_TIME_FORMAT': 'H:i', - - # forms - # 'SHOW_REQUIRED_ASTERISK': True, # Default True - # 'CONFIRM_UNSAVED_CHANGES': True, # Default True - - # menu - 'SEARCH_URL': '/admin/mdb/host/', - # 'MENU_ICONS': { - # 'sites': 'icon-leaf', - # 'auth': 'icon-lock', - # }, - # 'MENU_OPEN_FIRST_CHILD': True, # Default True - # 'MENU_EXCLUDE': ('auth.group',), - 'MENU': ( - {'app': 'mdb', 'label': 'MDB', 'url': 'mdb.host'}, - { - 'label': 'Settings', - 'icon': 'icon-cog', - 'models': ('auth.user', 'auth.group') - }, - ), - - # misc - 'LIST_PER_PAGE': 30 -} diff --git a/mdb/signals.py b/mdb/signals.py new file mode 100644 index 0000000..bd91bbc --- /dev/null +++ b/mdb/signals.py @@ -0,0 +1,140 @@ +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): + host = instance + + if not host.pxe_key: + # generate key (prevent infinite recursion by using update) + pxe_key = uuid.uuid4().hex + Host.objects.filter(pk=host.pk).update(pxe_key=pxe_key) + host.pxe_key = pxe_key + + for pxe_file_name, pxe_file in host.as_pxe_files(): + path = os.path.join(settings.MDB_PXE_TFTP_ROOT, pxe_file_name) + if host.pxe_installable: + with open(path, 'w+') as f: + f.write(pxe_file) + logger.info("Created or updated {}".format(path)) + + # Also set necessary fields for pxe installation + interfaces = host.interface_set.exclude(ip4address__isnull=True) + + for _if in interfaces: + changed = False + if not _if.pxe_filename: + _if.pxe_filename = 'pxelinux.0' # don't overwrite + changed = True + if not _if.dhcp_client: + _if.dhcp_client = True + changed = True + + if changed: + _if.save() + + else: + if os.path.exists(path): + 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 501deb7..98f16ae 100644 --- a/mdb/tests.py +++ b/mdb/tests.py @@ -1,16 +1,56 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" - +import io +from django.contrib.auth.models import User +from django.core.management import call_command +from django.core.urlresolvers import reverse from django.test import TestCase +from rest_framework.authtoken.models import Token +from rest_framework.test import APITestCase + +from mdb.models import Host -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) +class MyAPITestCases(APITestCase): + fixtures = ['test_data'] + + def create_user_with_token(self): + self.user = User.objects.create(username='admin') + self.token = Token.objects.create(user=self.user) + self.host = Host.objects.first() + + def setUp(self): + self.create_user_with_token() + return super(MyAPITestCases, self).setUp() + + def test_validate_puppet_host_by_secret(self): + data = { + 'pxe_key': self.host.pxe_key, + 'certname': '{}.{}'.format(self.host.hostname, self.host.interface_set.first().domain.domain_name) + } + self.client.credentials(HTTP_AUTHORIZATION='Token {}'.format(self.token.key)) + 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) + + +class RunManagementCommands(TestCase): + fixtures = ['test_data'] + + def test_zone_sync(self): + expected_zone_file = '' + with io.BytesIO() as f: + call_command('zone-sync', debug=True, stdout=f) + # TODO check expected_zone_file + + def test_dhcp_sync(self): + expected_dhcp_file = '' + with io.BytesIO() as f: + call_command('dhcp-sync', ('--debug',), stdout=f) + # TODO check expected_dhcp_file diff --git a/mdb/utils.py b/mdb/utils.py new file mode 100644 index 0000000..766d5c1 --- /dev/null +++ b/mdb/utils.py @@ -0,0 +1,81 @@ +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 interface with an IPv4 address """ + from mdb.models import Host + assert isinstance(host, Host) + + pxe_files = [] + for _if in host.interface_set.exclude(ip4address__isnull=True): + filename = get_pxe_filename(_if) + content = render_pxelinux_cfg(_if, host) + pxe_files.append((filename, content)) + + return pxe_files diff --git a/mdb/validators.py b/mdb/validators.py index 58bfb1b..5959f1b 100644 --- a/mdb/validators.py +++ b/mdb/validators.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from django.core.validators import RegexValidator import re @@ -5,11 +6,11 @@ import re hostname_re = re.compile(r'^(?!-)[-a-z0-9]+(?=1.7,<1.8 -MySQL-python==1.2.5 -ipaddr==2.1.11 +Django>=1.8,<1.9 django-auth-ldap -django-suit +django-extensions==1.5.7 +djangorestframework==3.2.4 +django-suit==0.2.15 +ipaddress==1.0.14 +mysqlclient==1.3.6 diff --git a/urls.py b/urls.py deleted file mode 100644 index 4d30b2a..0000000 --- a/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.conf.urls import patterns, include, url -from django.contrib import admin -from django.contrib.staticfiles.urls import staticfiles_urlpatterns -from django.views.generic.base import RedirectView - -admin.autodiscover() - -urlpatterns = patterns( - '', - url(r'^$', RedirectView.as_view(url='admin/')), - url(r'^admin/', include(admin.site.urls)), -) - -urlpatterns += staticfiles_urlpatterns()