diff --git a/machines/admin.py b/machines/admin.py index e0256fe0..93e936e6 100644 --- a/machines/admin.py +++ b/machines/admin.py @@ -27,8 +27,8 @@ from django.contrib import admin from reversion.admin import VersionAdmin from .models import IpType, Machine, MachineType, Domain, IpList, Interface -from .models import Extension, Mx, Ns, Vlan, Text, Nas, Service, OuverturePort -from .models import OuverturePortList +from .models import Extension, SOA, Mx, Ns, Vlan, Text, Nas, Service +from .models import OuverturePort, OuverturePortList class MachineAdmin(VersionAdmin): @@ -51,6 +51,10 @@ class ExtensionAdmin(VersionAdmin): pass +class SOAAdmin(VersionAdmin): + pass + + class MxAdmin(VersionAdmin): pass @@ -95,6 +99,7 @@ admin.site.register(Machine, MachineAdmin) admin.site.register(MachineType, MachineTypeAdmin) admin.site.register(IpType, IpTypeAdmin) admin.site.register(Extension, ExtensionAdmin) +admin.site.register(SOA, SOAAdmin) admin.site.register(Mx, MxAdmin) admin.site.register(Ns, NsAdmin) admin.site.register(Text, TextAdmin) diff --git a/machines/forms.py b/machines/forms.py index fc30258c..92392aba 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -45,6 +45,7 @@ from .models import ( IpList, MachineType, Extension, + SOA, Mx, Text, Ns, @@ -279,6 +280,7 @@ class ExtensionForm(ModelForm): self.fields['name'].label = 'Extension à ajouter' self.fields['origin'].label = 'Enregistrement A origin' self.fields['origin_v6'].label = 'Enregistrement AAAA origin' + self.fields['soa'].label = 'En-tête SOA à utiliser' class DelExtensionForm(Form): @@ -290,6 +292,26 @@ class DelExtensionForm(Form): ) +class SOAForm(ModelForm): + """Ajout et edition d'un SOA""" + class Meta: + model = SOA + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(SOAForm, self).__init__(*args, prefix=prefix, **kwargs) + + +class DelSOAForm(Form): + """Suppression d'un ou plusieurs SOA""" + soa = forms.ModelMultipleChoiceField( + queryset=SOA.objects.all(), + label="SOA actuels", + widget=forms.CheckboxSelectMultiple + ) + + class MxForm(ModelForm): """Ajout et edition d'un MX""" class Meta: diff --git a/machines/migrations/0063_auto_20171020_0040.py b/machines/migrations/0063_auto_20171020_0040.py new file mode 100644 index 00000000..d2f9afd9 --- /dev/null +++ b/machines/migrations/0063_auto_20171020_0040.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-19 22:40 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import machines.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0062_extension_origin_v6'), + ] + + operations = [ + migrations.CreateModel( + name='SOA', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('mail', models.EmailField(help_text='Email du contact pour la zone', max_length=254)), + ('refresh', models.PositiveIntegerField(default=86400, help_text='Secondes avant que les DNS secondaires doivent demander le serial du DNS primaire pour détecter une modification')), + ('retry', models.PositiveIntegerField(default=7200, help_text='Secondes avant que les DNS secondaires fassent une nouvelle demande de serial en cas de timeout du DNS primaire')), + ('expire', models.PositiveIntegerField(default=3600000, help_text='Secondes après lesquelles les DNS secondaires arrêtent de de répondre aux requêtes en cas de timeout du DNS primaire')), + ('ttl', models.PositiveIntegerField(default=172800, help_text='Time To Live')), + ], + ), + migrations.AddField( + model_name='extension', + name='soa', + field=models.ForeignKey(default=machines.models.SOA.new_default_soa, on_delete=django.db.models.deletion.CASCADE, to='machines.SOA'), + ), + ] diff --git a/machines/models.py b/machines/models.py index e3c14ea4..a1349ba7 100644 --- a/machines/models.py +++ b/machines/models.py @@ -234,6 +234,78 @@ class Nas(models.Model): return self.name +class SOA(models.Model): + """ + Un enregistrement SOA associé à une extension + Les valeurs par défault viennent des recommandations RIPE : + https://www.ripe.net/publications/docs/ripe-203 + """ + PRETTY_NAME = "Enregistrement SOA" + + name = models.CharField(max_length=255) + mail = models.EmailField( + help_text='Email du contact pour la zone' + ) + refresh = models.PositiveIntegerField( + default=86400, # 24 hours + help_text='Secondes avant que les DNS secondaires doivent demander le\ + serial du DNS primaire pour détecter une modification' + ) + retry = models.PositiveIntegerField( + default=7200, # 2 hours + help_text='Secondes avant que les DNS secondaires fassent une nouvelle\ + demande de serial en cas de timeout du DNS primaire' + ) + expire = models.PositiveIntegerField( + default=3600000, # 1000 hours + help_text='Secondes après lesquelles les DNS secondaires arrêtent de\ + de répondre aux requêtes en cas de timeout du DNS primaire' + ) + ttl = models.PositiveIntegerField( + default=172800, # 2 days + help_text='Time To Live' + ) + + def __str__(self): + return str(self.name) + + @cached_property + def dns_soa_param(self): + """ + Renvoie la partie de l'enregistrement SOA correspondant aux champs : + ; refresh + ; retry + ; expire + ; TTL + """ + return ( + ' {refresh}; refresh\n' + ' {retry}; retry\n' + ' {expire}; expire\n' + ' {ttl}; TTL' + ).format( + refresh=str(self.refresh).ljust(12), + retry=str(self.retry).ljust(12), + expire=str(self.expire).ljust(12), + ttl=str(self.ttl).ljust(12) + ) + + @cached_property + def dns_soa_mail(self): + """ Renvoie le mail dans l'enregistrement SOA """ + mail_fields = str(self.mail).split('@') + return mail_fields[0].replace('.', '\\.') + '.' + mail_fields[1] + '.' + + @classmethod + def new_default_soa(cls): + """ Fonction pour créer un SOA par défaut, utile pour les nouvelles + extensions . + /!\ Ne jamais supprimer ou renommer cette fonction car elle est + utilisée dans les migrations de la BDD. """ + return cls.objects.get_or_create(name="SOA to edit", mail="postmaser@example.com")[0].pk + + + class Extension(models.Model): """ Extension dns type example.org. Précise si tout le monde peut l'utiliser, associé à un origin (ip d'origine)""" @@ -252,17 +324,22 @@ class Extension(models.Model): null=True, blank=True ) + soa = models.ForeignKey( + 'SOA', + on_delete=models.CASCADE, + default=SOA.new_default_soa + ) @cached_property def dns_entry(self): """ Une entrée DNS A et AAAA sur origin (zone self)""" entry = "" if self.origin: - entry += "@ IN A " + str(self.origin) + entry += "@ IN A " + str(self.origin) if self.origin_v6: if entry: entry += "\n" - entry += "@ IN AAAA " + str(self.origin_v6) + entry += "@ IN AAAA " + str(self.origin_v6) return entry def __str__(self): @@ -283,7 +360,7 @@ class Mx(models.Model): def dns_entry(self): """Renvoie l'entrée DNS complète pour un MX à mettre dans les fichiers de zones""" - return "@ IN MX " + str(self.priority) + " " + str(self.name) + return "@ IN MX " + str(self.priority).ljust(3) + " " + str(self.name) def __str__(self): return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name) @@ -299,7 +376,7 @@ class Ns(models.Model): @cached_property def dns_entry(self): """Renvoie un enregistrement NS complet pour les filezones""" - return "@ IN NS " + str(self.ns) + return "@ IN NS " + str(self.ns) def __str__(self): return str(self.zone) + ' ' + str(self.ns) @@ -307,7 +384,7 @@ class Ns(models.Model): class Text(models.Model): """ Un enregistrement TXT associé à une extension""" - PRETTY_NAME = "Enregistrement text" + PRETTY_NAME = "Enregistrement TXT" zone = models.ForeignKey('Extension', on_delete=models.PROTECT) field1 = models.CharField(max_length=255) @@ -320,7 +397,7 @@ class Text(models.Model): @cached_property def dns_entry(self): """Renvoie l'enregistrement TXT complet pour le fichier de zone""" - return str(self.field1) + " IN TXT " + str(self.field2) + return str(self.field1).ljust(15) + " IN TXT " + str(self.field2) class Interface(models.Model): @@ -506,7 +583,7 @@ class Domain(models.Model): def dns_entry(self): """ Une entrée DNS""" if self.cname: - return str(self.name) + " IN CNAME " + str(self.cname) + "." + return str(self.name).ljust(15) + " IN CNAME " + str(self.cname) + "." def save(self, *args, **kwargs): """ Empèche le save sans extension valide. @@ -790,6 +867,18 @@ def extension_post_selete(sender, **kwargs): regen('dns') +@receiver(post_save, sender=SOA) +def soa_post_save(sender, **kwargs): + """Regeneration dns après modification d'un SOA""" + regen('dns') + + +@receiver(post_delete, sender=SOA) +def soa_post_delete(sender, **kwargs): + """Regeneration dns après suppresson d'un SOA""" + regen('dns') + + @receiver(post_save, sender=Mx) def mx_post_save(sender, **kwargs): """Regeneration dns après modification d'un MX""" diff --git a/machines/serializers.py b/machines/serializers.py index 7d222ef0..7d78ebc3 100644 --- a/machines/serializers.py +++ b/machines/serializers.py @@ -158,10 +158,11 @@ class ExtensionSerializer(serializers.ModelSerializer): des foreign_key donc evalués en get_...""" origin = serializers.SerializerMethodField('get_origin_ip') zone_entry = serializers.SerializerMethodField('get_zone_name') + soa = serializers.SerializerMethodField('get_soa_data') class Meta: model = Extension - fields = ('name', 'origin', 'origin_v6', 'zone_entry') + fields = ('name', 'origin', 'origin_v6', 'zone_entry', 'soa') def get_origin_ip(self, obj): return obj.origin.ipv4 @@ -169,6 +170,9 @@ class ExtensionSerializer(serializers.ModelSerializer): def get_zone_name(self, obj): return str(obj.dns_entry) + def get_soa_data(self, obj): + return { 'mail': obj.soa.dns_soa_mail, 'param': obj.soa.dns_soa_param } + class MxSerializer(serializers.ModelSerializer): """Serialisation d'un MX, evaluation du nom, de la zone diff --git a/machines/templates/machines/aff_extension.html b/machines/templates/machines/aff_extension.html index 069f545a..15a4c637 100644 --- a/machines/templates/machines/aff_extension.html +++ b/machines/templates/machines/aff_extension.html @@ -26,19 +26,25 @@ with this program; if not, write to the Free Software Foundation, Inc., Extension - Autorisation infra pour utiliser l'extension + Droit infra pour utiliser ? + Enregistrement SOA Enregistrement A origin + {% if ipv6_enabled %} Enregistrement AAAA origin + {% endif %} {% for extension in extension_list %} {{ extension.name }} - {{ extension.need_infra }} + {{ extension.need_infra }} + {{ extension.soa}} {{ extension.origin }} - {{ extension.origin_v6 }} - + {% if ipv6_enabled %} + {{ extension.origin_v6 }} + {% endif %} + {% if is_infra %} {% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %} {% endif %} diff --git a/machines/templates/machines/aff_soa.html b/machines/templates/machines/aff_soa.html new file mode 100644 index 00000000..3dad11c7 --- /dev/null +++ b/machines/templates/machines/aff_soa.html @@ -0,0 +1,56 @@ +{% comment %} +Re2o est un logiciel d'administration développé initiallement au rezometz. Il +se veut agnostique au réseau considéré, de manière à être installable en +quelques clics. + +Copyright © 2017 Gabriel Détraz +Copyright © 2017 Goulven Kermarec +Copyright © 2017 Augustin Lemesle + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +{% endcomment %} + + + + + + + + + + + + + + + {% for soa in soa_list %} + + + + + + + + + + {% endfor %} +
NomMailRefreshRetryExpireTTL
{{ soa.name }}{{ soa.mail }}{{ soa.refresh }}{{ soa.retry }}{{ soa.expire }}{{ soa.ttl }} + {% if is_infra %} + {% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %} + {% endif %} + {% include 'buttons/history.html' with href='machines:history' name='soa' id=soa.id %} +
+ + diff --git a/machines/templates/machines/index_extension.html b/machines/templates/machines/index_extension.html index 88eb5411..ceae84c6 100644 --- a/machines/templates/machines/index_extension.html +++ b/machines/templates/machines/index_extension.html @@ -35,6 +35,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% include "machines/aff_extension.html" with extension_list=extension_list %} +

Liste des enregistrements SOA

+ {% if is_infra %} + Ajouter un enregistrement SOA + Supprimer un enregistrement SOA + {% endif %} + {% include "machines/aff_soa.html" with soa_list=soa_list %}

Liste des enregistrements MX

{% if is_infra %} Ajouter un enregistrement MX diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 86bf7b90..636c8e2f 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -100,6 +100,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Extension

{% massive_bootstrap_form extensionform 'origin' %} {% endif %} + {% if soaform %} +

Enregistrement SOA

+ {% bootstrap_form soaform %} + {% endif %} {% if mxform %}

Enregistrement MX

{% massive_bootstrap_form mxform 'name' %} diff --git a/machines/urls.py b/machines/urls.py index fa9b3497..3669b5b9 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -44,6 +44,9 @@ urlpatterns = [ url(r'^add_extension/$', views.add_extension, name='add-extension'), url(r'^edit_extension/(?P[0-9]+)$', views.edit_extension, name='edit-extension'), url(r'^del_extension/$', views.del_extension, name='del-extension'), + url(r'^add_soa/$', views.add_soa, name='add-soa'), + url(r'^edit_soa/(?P[0-9]+)$', views.edit_soa, name='edit-soa'), + url(r'^del_soa/$', views.del_soa, name='del-soa'), url(r'^add_mx/$', views.add_mx, name='add-mx'), url(r'^edit_mx/(?P[0-9]+)$', views.edit_mx, name='edit-mx'), url(r'^del_mx/$', views.del_mx, name='del-mx'), @@ -74,6 +77,7 @@ urlpatterns = [ url(r'^history/(?Pinterface)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pmachinetype)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pextension)/(?P[0-9]+)$', views.history, name='history'), + url(r'^history/(?Psoa)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pmx)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Pns)/(?P[0-9]+)$', views.history, name='history'), url(r'^history/(?Ptxt)/(?P[0-9]+)$', views.history, name='history'), diff --git a/machines/views.py b/machines/views.py index 1271e277..69f41581 100644 --- a/machines/views.py +++ b/machines/views.py @@ -77,6 +77,8 @@ from .forms import ( DomainForm, AliasForm, DelAliasForm, + SOAForm, + DelSOAForm, NsForm, DelNsForm, TxtForm, @@ -98,6 +100,7 @@ from .models import ( IpList, MachineType, Extension, + SOA, Mx, Ns, Domain, @@ -519,6 +522,54 @@ def del_extension(request): return redirect("/machines/index_extension") return form({'extensionform': extension}, 'machines/machine.html', request) +@login_required +@permission_required('infra') +def add_soa(request): + soa = SOAForm(request.POST or None) + if soa.is_valid(): + with transaction.atomic(), reversion.create_revision(): + soa.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + messages.success(request, "Cet enregistrement SOA a été ajouté") + return redirect("/machines/index_extension") + return form({'soaform': soa}, 'machines/machine.html', request) + +@login_required +@permission_required('infra') +def edit_soa(request, soaid): + try: + soa_instance = SOA.objects.get(pk=soaid) + except SOA.DoesNotExist: + messages.error(request, u"Entrée inexistante" ) + return redirect("/machines/index_extension/") + soa = SOAForm(request.POST or None, instance=soa_instance) + if soa.is_valid(): + with transaction.atomic(), reversion.create_revision(): + soa.save() + reversion.set_user(request.user) + reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in soa.changed_data)) + messages.success(request, "SOA modifié") + return redirect("/machines/index_extension/") + return form({'soaform': soa}, 'machines/machine.html', request) + +@login_required +@permission_required('infra') +def del_soa(request): + soa = DelSOAForm(request.POST or None) + if soa.is_valid(): + soa_dels = soa.cleaned_data['soa'] + for soa_del in soa_dels: + try: + with transaction.atomic(), reversion.create_revision(): + soa_del.delete() + reversion.set_user(request.user) + messages.success(request, "Le SOA a été supprimée") + except ProtectedError: + messages.error(request, "Erreur le SOA suivant %s ne peut être supprimé" % soa_del) + return redirect("/machines/index_extension") + return form({'soaform': soa}, 'machines/machine.html', request) + @login_required @permission_required('infra') def add_mx(request): @@ -925,11 +976,12 @@ def index_nas(request): @login_required @permission_required('cableur') def index_extension(request): - extension_list = Extension.objects.select_related('origin').order_by('name') + extension_list = Extension.objects.select_related('origin').select_related('soa').order_by('name') + soa_list = SOA.objects.order_by('name') mx_list = Mx.objects.order_by('zone').select_related('zone').select_related('name__extension') ns_list = Ns.objects.order_by('zone').select_related('zone').select_related('ns__extension') text_list = Text.objects.all().select_related('zone') - return render(request, 'machines/index_extension.html', {'extension_list':extension_list, 'mx_list': mx_list, 'ns_list': ns_list, 'text_list' : text_list}) + return render(request, 'machines/index_extension.html', {'extension_list':extension_list, 'soa_list': soa_list, 'mx_list': mx_list, 'ns_list': ns_list, 'text_list' : text_list}) @login_required def index_alias(request, interfaceid): @@ -998,6 +1050,12 @@ def history(request, object, id): except Extension.DoesNotExist: messages.error(request, "Extension inexistante") return redirect("/machines/") + elif object == 'soa' and request.user.has_perms(('cableur',)): + try: + object_instance = SOA.objects.get(pk=id) + except SOA.DoesNotExist: + messages.error(request, "SOA inexistant") + return redirect("/machines/") elif object == 'mx' and request.user.has_perms(('cableur',)): try: object_instance = Mx.objects.get(pk=id)