8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-22 19:33:11 +00:00

Merge branch 'pref_SOA' into 'master'

Pref soa

See merge request rezo/re2o!23
This commit is contained in:
Mael Kervella 2017-10-22 04:44:12 +02:00
commit a9bde65040
11 changed files with 304 additions and 16 deletions

View file

@ -27,8 +27,8 @@ from django.contrib import admin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from .models import IpType, Machine, MachineType, Domain, IpList, Interface from .models import IpType, Machine, MachineType, Domain, IpList, Interface
from .models import Extension, Mx, Ns, Vlan, Text, Nas, Service, OuverturePort from .models import Extension, SOA, Mx, Ns, Vlan, Text, Nas, Service
from .models import OuverturePortList from .models import OuverturePort, OuverturePortList
class MachineAdmin(VersionAdmin): class MachineAdmin(VersionAdmin):
@ -51,6 +51,10 @@ class ExtensionAdmin(VersionAdmin):
pass pass
class SOAAdmin(VersionAdmin):
pass
class MxAdmin(VersionAdmin): class MxAdmin(VersionAdmin):
pass pass
@ -95,6 +99,7 @@ admin.site.register(Machine, MachineAdmin)
admin.site.register(MachineType, MachineTypeAdmin) admin.site.register(MachineType, MachineTypeAdmin)
admin.site.register(IpType, IpTypeAdmin) admin.site.register(IpType, IpTypeAdmin)
admin.site.register(Extension, ExtensionAdmin) admin.site.register(Extension, ExtensionAdmin)
admin.site.register(SOA, SOAAdmin)
admin.site.register(Mx, MxAdmin) admin.site.register(Mx, MxAdmin)
admin.site.register(Ns, NsAdmin) admin.site.register(Ns, NsAdmin)
admin.site.register(Text, TextAdmin) admin.site.register(Text, TextAdmin)

View file

@ -45,6 +45,7 @@ from .models import (
IpList, IpList,
MachineType, MachineType,
Extension, Extension,
SOA,
Mx, Mx,
Text, Text,
Ns, Ns,
@ -279,6 +280,7 @@ class ExtensionForm(ModelForm):
self.fields['name'].label = 'Extension à ajouter' self.fields['name'].label = 'Extension à ajouter'
self.fields['origin'].label = 'Enregistrement A origin' self.fields['origin'].label = 'Enregistrement A origin'
self.fields['origin_v6'].label = 'Enregistrement AAAA origin' self.fields['origin_v6'].label = 'Enregistrement AAAA origin'
self.fields['soa'].label = 'En-tête SOA à utiliser'
class DelExtensionForm(Form): 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): class MxForm(ModelForm):
"""Ajout et edition d'un MX""" """Ajout et edition d'un MX"""
class Meta: class Meta:

View file

@ -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'),
),
]

View file

@ -234,6 +234,78 @@ class Nas(models.Model):
return self.name 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> ; refresh
<retry> ; retry
<expire> ; expire
<ttl> ; 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): class Extension(models.Model):
""" Extension dns type example.org. Précise si tout le monde peut """ Extension dns type example.org. Précise si tout le monde peut
l'utiliser, associé à un origin (ip d'origine)""" l'utiliser, associé à un origin (ip d'origine)"""
@ -252,6 +324,11 @@ class Extension(models.Model):
null=True, null=True,
blank=True blank=True
) )
soa = models.ForeignKey(
'SOA',
on_delete=models.CASCADE,
default=SOA.new_default_soa
)
@cached_property @cached_property
def dns_entry(self): def dns_entry(self):
@ -283,7 +360,7 @@ class Mx(models.Model):
def dns_entry(self): def dns_entry(self):
"""Renvoie l'entrée DNS complète pour un MX à mettre dans les """Renvoie l'entrée DNS complète pour un MX à mettre dans les
fichiers de zones""" 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): def __str__(self):
return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name) return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name)
@ -307,7 +384,7 @@ class Ns(models.Model):
class Text(models.Model): class Text(models.Model):
""" Un enregistrement TXT associé à une extension""" """ Un enregistrement TXT associé à une extension"""
PRETTY_NAME = "Enregistrement text" PRETTY_NAME = "Enregistrement TXT"
zone = models.ForeignKey('Extension', on_delete=models.PROTECT) zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
field1 = models.CharField(max_length=255) field1 = models.CharField(max_length=255)
@ -320,7 +397,7 @@ class Text(models.Model):
@cached_property @cached_property
def dns_entry(self): def dns_entry(self):
"""Renvoie l'enregistrement TXT complet pour le fichier de zone""" """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): class Interface(models.Model):
@ -506,7 +583,7 @@ class Domain(models.Model):
def dns_entry(self): def dns_entry(self):
""" Une entrée DNS""" """ Une entrée DNS"""
if self.cname: 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): def save(self, *args, **kwargs):
""" Empèche le save sans extension valide. """ Empèche le save sans extension valide.
@ -790,6 +867,18 @@ def extension_post_selete(sender, **kwargs):
regen('dns') 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) @receiver(post_save, sender=Mx)
def mx_post_save(sender, **kwargs): def mx_post_save(sender, **kwargs):
"""Regeneration dns après modification d'un MX""" """Regeneration dns après modification d'un MX"""

View file

@ -158,10 +158,11 @@ class ExtensionSerializer(serializers.ModelSerializer):
des foreign_key donc evalués en get_...""" des foreign_key donc evalués en get_..."""
origin = serializers.SerializerMethodField('get_origin_ip') origin = serializers.SerializerMethodField('get_origin_ip')
zone_entry = serializers.SerializerMethodField('get_zone_name') zone_entry = serializers.SerializerMethodField('get_zone_name')
soa = serializers.SerializerMethodField('get_soa_data')
class Meta: class Meta:
model = Extension model = Extension
fields = ('name', 'origin', 'origin_v6', 'zone_entry') fields = ('name', 'origin', 'origin_v6', 'zone_entry', 'soa')
def get_origin_ip(self, obj): def get_origin_ip(self, obj):
return obj.origin.ipv4 return obj.origin.ipv4
@ -169,6 +170,9 @@ class ExtensionSerializer(serializers.ModelSerializer):
def get_zone_name(self, obj): def get_zone_name(self, obj):
return str(obj.dns_entry) 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): class MxSerializer(serializers.ModelSerializer):
"""Serialisation d'un MX, evaluation du nom, de la zone """Serialisation d'un MX, evaluation du nom, de la zone

View file

@ -26,9 +26,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<thead> <thead>
<tr> <tr>
<th>Extension</th> <th>Extension</th>
<th>Autorisation infra pour utiliser l'extension</th> <th>Droit infra pour utiliser ?</th>
<th>Enregistrement SOA</th>
<th>Enregistrement A origin</th> <th>Enregistrement A origin</th>
{% if ipv6_enabled %}
<th>Enregistrement AAAA origin</th> <th>Enregistrement AAAA origin</th>
{% endif %}
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -36,8 +39,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td>{{ extension.name }}</td> <td>{{ extension.name }}</td>
<td>{{ extension.need_infra }}</td> <td>{{ extension.need_infra }}</td>
<td>{{ extension.soa}}</td>
<td>{{ extension.origin }}</td> <td>{{ extension.origin }}</td>
{% if ipv6_enabled %}
<td>{{ extension.origin_v6 }}</td> <td>{{ extension.origin_v6 }}</td>
{% endif %}
<td class="text-right"> <td class="text-right">
{% if is_infra %} {% if is_infra %}
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %} {% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}

View file

@ -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 %}
<table class="table table-striped">
<thead>
<tr>
<th>Nom</th>
<th>Mail</th>
<th>Refresh</th>
<th>Retry</th>
<th>Expire</th>
<th>TTL</th>
<th></th>
<th></th>
</tr>
</thead>
{% for soa in soa_list %}
<tr>
<td>{{ soa.name }}</td>
<td>{{ soa.mail }}</td>
<td>{{ soa.refresh }}</td>
<td>{{ soa.retry }}</td>
<td>{{ soa.expire }}</td>
<td>{{ soa.ttl }}</td>
<td class="text-right">
{% 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 %}
</td>
</tr>
{% endfor %}
</table>

View file

@ -35,6 +35,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %} {% endif %}
{% include "machines/aff_extension.html" with extension_list=extension_list %} {% include "machines/aff_extension.html" with extension_list=extension_list %}
<h2>Liste des enregistrements SOA</h2>
{% if is_infra %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-soa' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un enregistrement SOA</a>
<a class="btn btn-danger btn-sm" role="button" href="{% url 'machines:del-soa' %}"><i class="glyphicon glyphicon-trash"></i> Supprimer un enregistrement SOA</a>
{% endif %}
{% include "machines/aff_soa.html" with soa_list=soa_list %}
<h2>Liste des enregistrements MX</h2> <h2>Liste des enregistrements MX</h2>
{% if is_infra %} {% if is_infra %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-mx' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un enregistrement MX</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-mx' %}"><i class="glyphicon glyphicon-plus"></i> Ajouter un enregistrement MX</a>

View file

@ -100,6 +100,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<h3>Extension</h3> <h3>Extension</h3>
{% massive_bootstrap_form extensionform 'origin' %} {% massive_bootstrap_form extensionform 'origin' %}
{% endif %} {% endif %}
{% if soaform %}
<h3>Enregistrement SOA</h3>
{% bootstrap_form soaform %}
{% endif %}
{% if mxform %} {% if mxform %}
<h3>Enregistrement MX</h3> <h3>Enregistrement MX</h3>
{% massive_bootstrap_form mxform 'name' %} {% massive_bootstrap_form mxform 'name' %}

View file

@ -44,6 +44,9 @@ urlpatterns = [
url(r'^add_extension/$', views.add_extension, name='add-extension'), url(r'^add_extension/$', views.add_extension, name='add-extension'),
url(r'^edit_extension/(?P<extensionid>[0-9]+)$', views.edit_extension, name='edit-extension'), url(r'^edit_extension/(?P<extensionid>[0-9]+)$', views.edit_extension, name='edit-extension'),
url(r'^del_extension/$', views.del_extension, name='del-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<soaid>[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'^add_mx/$', views.add_mx, name='add-mx'),
url(r'^edit_mx/(?P<mxid>[0-9]+)$', views.edit_mx, name='edit-mx'), url(r'^edit_mx/(?P<mxid>[0-9]+)$', views.edit_mx, name='edit-mx'),
url(r'^del_mx/$', views.del_mx, name='del-mx'), url(r'^del_mx/$', views.del_mx, name='del-mx'),
@ -74,6 +77,7 @@ urlpatterns = [
url(r'^history/(?P<object>interface)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>interface)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>machinetype)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>machinetype)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>extension)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>extension)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>soa)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>mx)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>mx)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>ns)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>ns)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>txt)/(?P<id>[0-9]+)$', views.history, name='history'), url(r'^history/(?P<object>txt)/(?P<id>[0-9]+)$', views.history, name='history'),

View file

@ -77,6 +77,8 @@ from .forms import (
DomainForm, DomainForm,
AliasForm, AliasForm,
DelAliasForm, DelAliasForm,
SOAForm,
DelSOAForm,
NsForm, NsForm,
DelNsForm, DelNsForm,
TxtForm, TxtForm,
@ -98,6 +100,7 @@ from .models import (
IpList, IpList,
MachineType, MachineType,
Extension, Extension,
SOA,
Mx, Mx,
Ns, Ns,
Domain, Domain,
@ -519,6 +522,54 @@ def del_extension(request):
return redirect("/machines/index_extension") return redirect("/machines/index_extension")
return form({'extensionform': extension}, 'machines/machine.html', request) 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 @login_required
@permission_required('infra') @permission_required('infra')
def add_mx(request): def add_mx(request):
@ -925,11 +976,12 @@ def index_nas(request):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_extension(request): 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') 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') ns_list = Ns.objects.order_by('zone').select_related('zone').select_related('ns__extension')
text_list = Text.objects.all().select_related('zone') 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 @login_required
def index_alias(request, interfaceid): def index_alias(request, interfaceid):
@ -998,6 +1050,12 @@ def history(request, object, id):
except Extension.DoesNotExist: except Extension.DoesNotExist:
messages.error(request, "Extension inexistante") messages.error(request, "Extension inexistante")
return redirect("/machines/") 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',)): elif object == 'mx' and request.user.has_perms(('cableur',)):
try: try:
object_instance = Mx.objects.get(pk=id) object_instance = Mx.objects.get(pk=id)