8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-12-26 08:53:46 +00:00

Merge branch 'master' into ouverture_des_ports

This commit is contained in:
root 2017-10-09 21:11:09 +02:00
commit a08ae1027e
18 changed files with 8105 additions and 79 deletions

View file

@ -106,7 +106,7 @@ def index(request):
'user_id': v.revision.user_id, 'user_id': v.revision.user_id,
'version': v } 'version': v }
else : else :
to_remove.append(i) to_remove.insert(0,i)
# Remove all tagged invalid items # Remove all tagged invalid items
for i in to_remove : for i in to_remove :
versions.object_list.pop(i) versions.object_list.pop(i)

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
@ -5,6 +6,7 @@
# Copyright © 2017 Gabriel Détraz # Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec # Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle # Copyright © 2017 Augustin Lemesle
# Copyright © 2017 Maël Kervella
# #
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -52,8 +54,7 @@ class BaseEditMachineForm(EditMachineForm):
class EditInterfaceForm(ModelForm): class EditInterfaceForm(ModelForm):
class Meta: class Meta:
model = Interface model = Interface
# fields = '__all__' fields = ['machine', 'type', 'ipv4', 'mac_address', 'details']
exclude = ['port_lists']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditInterfaceForm, self).__init__(*args, **kwargs) super(EditInterfaceForm, self).__init__(*args, **kwargs)
@ -63,12 +64,14 @@ class EditInterfaceForm(ModelForm):
if "ipv4" in self.fields: if "ipv4" in self.fields:
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4" self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True) self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True)
# Add it's own address
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance)
if "machine" in self.fields: if "machine" in self.fields:
self.fields['machine'].queryset = Machine.objects.all().select_related('user') self.fields['machine'].queryset = Machine.objects.all().select_related('user')
class AddInterfaceForm(EditInterfaceForm): class AddInterfaceForm(EditInterfaceForm):
class Meta(EditInterfaceForm.Meta): class Meta(EditInterfaceForm.Meta):
fields = ['ipv4','mac_address','type','details'] fields = ['type','ipv4','mac_address','details']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
infra = kwargs.pop('infra') infra = kwargs.pop('infra')
@ -82,11 +85,11 @@ class AddInterfaceForm(EditInterfaceForm):
class NewInterfaceForm(EditInterfaceForm): class NewInterfaceForm(EditInterfaceForm):
class Meta(EditInterfaceForm.Meta): class Meta(EditInterfaceForm.Meta):
fields = ['mac_address','type','details'] fields = ['type','mac_address','details']
class BaseEditInterfaceForm(EditInterfaceForm): class BaseEditInterfaceForm(EditInterfaceForm):
class Meta(EditInterfaceForm.Meta): class Meta(EditInterfaceForm.Meta):
fields = ['ipv4','mac_address','type','details'] fields = ['type','ipv4','mac_address','details']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
infra = kwargs.pop('infra') infra = kwargs.pop('infra')
@ -95,8 +98,11 @@ class BaseEditInterfaceForm(EditInterfaceForm):
if not infra: if not infra:
self.fields['type'].queryset = MachineType.objects.filter(ip_type__in=IpType.objects.filter(need_infra=False)) self.fields['type'].queryset = MachineType.objects.filter(ip_type__in=IpType.objects.filter(need_infra=False))
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).filter(ip_type__in=IpType.objects.filter(need_infra=False)) self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).filter(ip_type__in=IpType.objects.filter(need_infra=False))
# Add it's own address
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance)
else: else:
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True) self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True)
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance)
class AliasForm(ModelForm): class AliasForm(ModelForm):
class Meta: class Meta:

View file

@ -56,6 +56,7 @@ class MachineType(models.Model):
ip_type = models.ForeignKey('IpType', on_delete=models.PROTECT, blank=True, null=True) ip_type = models.ForeignKey('IpType', on_delete=models.PROTECT, blank=True, null=True)
def all_interfaces(self): def all_interfaces(self):
""" Renvoie toutes les interfaces (cartes réseaux) de type machinetype"""
return Interface.objects.filter(type=self) return Interface.objects.filter(type=self)
def __str__(self): def __str__(self):
@ -76,23 +77,31 @@ class IpType(models.Model):
@cached_property @cached_property
def ip_range(self): def ip_range(self):
""" Renvoie un objet IPRange à partir de l'objet IpType"""
return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop) return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop)
@cached_property @cached_property
def ip_set(self): def ip_set(self):
""" Renvoie une IPSet à partir de l'iptype"""
return IPSet(self.ip_range) return IPSet(self.ip_range)
@cached_property @cached_property
def ip_set_as_str(self): def ip_set_as_str(self):
""" Renvoie une liste des ip en string"""
return [str(x) for x in self.ip_set] return [str(x) for x in self.ip_set]
def ip_objects(self): def ip_objects(self):
""" Renvoie tous les objets ipv4 relié à ce type"""
return IpList.objects.filter(ip_type=self) return IpList.objects.filter(ip_type=self)
def free_ip(self): def free_ip(self):
""" Renvoie toutes les ip libres associées au type donné (self)"""
return IpList.objects.filter(interface__isnull=True).filter(ip_type=self) return IpList.objects.filter(interface__isnull=True).filter(ip_type=self)
def gen_ip_range(self): def gen_ip_range(self):
""" Cree les IpList associées au type self. Parcours pédestrement et crée
les ip une par une. Si elles existent déjà, met à jour le type associé
à l'ip"""
# Creation du range d'ip dans les objets iplist # Creation du range d'ip dans les objets iplist
networks = [] networks = []
for net in self.ip_range.cidrs(): for net in self.ip_range.cidrs():
@ -115,6 +124,11 @@ class IpType(models.Model):
ip.delete() ip.delete()
def clean(self): def clean(self):
""" Nettoyage. Vérifie :
- Que ip_stop est après ip_start
- Qu'on ne crée pas plus gros qu'un /16
- Que le range crée ne recoupe pas un range existant
- Formate l'ipv6 donnée en /64"""
if IPAddress(self.domaine_ip_start) > IPAddress(self.domaine_ip_stop): if IPAddress(self.domaine_ip_start) > IPAddress(self.domaine_ip_stop):
raise ValidationError("Domaine end doit être après start...") raise ValidationError("Domaine end doit être après start...")
# On ne crée pas plus grand qu'un /16 # On ne crée pas plus grand qu'un /16
@ -137,6 +151,7 @@ class IpType(models.Model):
return self.type return self.type
class Vlan(models.Model): class Vlan(models.Model):
""" Un vlan : vlan_id et nom"""
PRETTY_NAME = "Vlans" PRETTY_NAME = "Vlans"
vlan_id = models.IntegerField() vlan_id = models.IntegerField()
@ -147,6 +162,9 @@ class Vlan(models.Model):
return self.name return self.name
class Nas(models.Model): class Nas(models.Model):
""" Les nas. Associé à un machine_type.
Permet aussi de régler le port_access_mode (802.1X ou mac-address) pour
le radius. Champ autocapture de la mac à true ou false"""
PRETTY_NAME = "Correspondance entre les nas et les machines connectées" PRETTY_NAME = "Correspondance entre les nas et les machines connectées"
default_mode = '802.1X' default_mode = '802.1X'
@ -165,6 +183,8 @@ class Nas(models.Model):
return self.name return self.name
class Extension(models.Model): class Extension(models.Model):
""" Extension dns type example.org. Précise si tout le monde peut l'utiliser,
associé à un origin (ip d'origine)"""
PRETTY_NAME = "Extensions dns" PRETTY_NAME = "Extensions dns"
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
@ -173,12 +193,15 @@ class Extension(models.Model):
@cached_property @cached_property
def dns_entry(self): def dns_entry(self):
""" Une entrée DNS A"""
return "@ IN A " + str(self.origin) return "@ IN A " + str(self.origin)
def __str__(self): def __str__(self):
return self.name return self.name
class Mx(models.Model): class Mx(models.Model):
""" Entrées des MX. Enregistre la zone (extension) associée et la priorité
Todo : pouvoir associer un MX à une interface """
PRETTY_NAME = "Enregistrements MX" PRETTY_NAME = "Enregistrements MX"
zone = models.ForeignKey('Extension', on_delete=models.PROTECT) zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
@ -206,6 +229,7 @@ class Ns(models.Model):
return str(self.zone) + ' ' + str(self.ns) return str(self.zone) + ' ' + str(self.ns)
class Text(models.Model): class Text(models.Model):
""" Un enregistrement TXT associé à une extension"""
PRETTY_NAME = "Enregistrement text" PRETTY_NAME = "Enregistrement text"
zone = models.ForeignKey('Extension', on_delete=models.PROTECT) zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
@ -220,6 +244,12 @@ class Text(models.Model):
return str(self.field1) + " IN TXT " + str(self.field2) return str(self.field1) + " IN TXT " + str(self.field2)
class Interface(models.Model): class Interface(models.Model):
""" Une interface. Objet clef de l'application machine :
- une address mac unique. Possibilité de la rendre unique avec le typemachine
- une onetoone vers IpList pour attribution ipv4
- le type parent associé au range ip et à l'extension
- un objet domain associé contenant son nom
- la liste des ports oiuvert"""
PRETTY_NAME = "Interface" PRETTY_NAME = "Interface"
ipv4 = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True) ipv4 = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True)
@ -239,6 +269,7 @@ class Interface(models.Model):
@cached_property @cached_property
def ipv6_object(self): def ipv6_object(self):
""" Renvoie un objet type ipv6 à partir du prefix associé à l'iptype parent"""
if self.type.ip_type.prefix_v6: if self.type.ip_type.prefix_v6:
return EUI(self.mac_address).ipv6(IPNetwork(self.type.ip_type.prefix_v6).network) return EUI(self.mac_address).ipv6(IPNetwork(self.type.ip_type.prefix_v6).network)
else: else:
@ -246,18 +277,23 @@ class Interface(models.Model):
@cached_property @cached_property
def ipv6(self): def ipv6(self):
""" Renvoie l'ipv6 en str. Mise en cache et propriété de l'objet"""
return str(self.ipv6_object) return str(self.ipv6_object)
def mac_bare(self): def mac_bare(self):
""" Formatage de la mac type mac_bare"""
return str(EUI(self.mac_address, dialect=mac_bare)).lower() return str(EUI(self.mac_address, dialect=mac_bare)).lower()
def filter_macaddress(self): def filter_macaddress(self):
""" Tente un formatage mac_bare, si échoue, lève une erreur de validation"""
try: try:
self.mac_address = str(EUI(self.mac_address)) self.mac_address = str(EUI(self.mac_address))
except : except :
raise ValidationError("La mac donnée est invalide") raise ValidationError("La mac donnée est invalide")
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
""" Formate l'addresse mac en mac_bare (fonction filter_mac)
et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
self.filter_macaddress() self.filter_macaddress()
self.mac_address = str(EUI(self.mac_address)) or None self.mac_address = str(EUI(self.mac_address)) or None
if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type: if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
@ -274,6 +310,7 @@ class Interface(models.Model):
return return
def unassign_ipv4(self): def unassign_ipv4(self):
""" Sans commentaire, désassigne une ipv4"""
self.ipv4 = None self.ipv4 = None
def update_type(self): def update_type(self):
@ -296,15 +333,20 @@ class Interface(models.Model):
return str(domain) return str(domain)
def has_private_ip(self): def has_private_ip(self):
""" True si l'ip associée est privée"""
if self.ipv4: if self.ipv4:
return IPAddress(str(self.ipv4)).is_private() return IPAddress(str(self.ipv4)).is_private()
else: else:
return False return False
def may_have_port_open(self): def may_have_port_open(self):
""" True si l'interface a une ip et une ip publique.
Permet de ne pas exporter des ouvertures sur des ip privées (useless)"""
return self.ipv4 and not self.has_private_ip() return self.ipv4 and not self.has_private_ip()
class Domain(models.Model): class Domain(models.Model):
""" Objet domain. Enregistrement A et CNAME en même temps : permet de stocker les
alias et les nom de machines, suivant si interface_parent ou cname sont remplis"""
PRETTY_NAME = "Domaine dns" PRETTY_NAME = "Domaine dns"
interface_parent = models.OneToOneField('Interface', on_delete=models.CASCADE, blank=True, null=True) interface_parent = models.OneToOneField('Interface', on_delete=models.CASCADE, blank=True, null=True)
@ -316,6 +358,8 @@ class Domain(models.Model):
unique_together = (("name", "extension"),) unique_together = (("name", "extension"),)
def get_extension(self): def get_extension(self):
""" Retourne l'extension de l'interface parente si c'est un A
Retourne l'extension propre si c'est un cname, renvoie None sinon"""
if self.interface_parent: if self.interface_parent:
return self.interface_parent.type.ip_type.extension return self.interface_parent.type.ip_type.extension
elif hasattr(self,'extension'): elif hasattr(self,'extension'):
@ -324,6 +368,11 @@ class Domain(models.Model):
return None return None
def clean(self): def clean(self):
""" Validation :
- l'objet est bien soit A soit CNAME
- le cname est pas pointé sur lui-même
- le nom contient bien les caractères autorisés par la norme dns et moins de 63 caractères au total
- le couple nom/extension est bien unique"""
if self.get_extension(): if self.get_extension():
self.extension=self.get_extension() self.extension=self.get_extension()
""" Validation du nom de domaine, extensions dans type de machine, prefixe pas plus long que 63 caractères """ """ Validation du nom de domaine, extensions dans type de machine, prefixe pas plus long que 63 caractères """
@ -342,10 +391,12 @@ class Domain(models.Model):
@cached_property @cached_property
def dns_entry(self): def dns_entry(self):
""" Une entrée DNS"""
if self.cname: if self.cname:
return str(self.name) + " IN CNAME " + str(self.cname) + "." return str(self.name) + " IN CNAME " + str(self.cname) + "."
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" Empèche le save sans extension valide. Force à avoir appellé clean avant"""
if not self.get_extension(): if not self.get_extension():
raise ValidationError("Extension invalide") raise ValidationError("Extension invalide")
self.full_clean() self.full_clean()
@ -362,9 +413,11 @@ class IpList(models.Model):
@cached_property @cached_property
def need_infra(self): def need_infra(self):
""" Permet de savoir si un user basique peut assigner cette ip ou non"""
return self.ip_type.need_infra return self.ip_type.need_infra
def clean(self): def clean(self):
""" Erreur si l'ip_type est incorrect"""
if not str(self.ipv4) in self.ip_type.ip_set_as_str: if not str(self.ipv4) in self.ip_type.ip_set_as_str:
raise ValidationError("L'ipv4 et le range de l'iptype ne correspondent pas!") raise ValidationError("L'ipv4 et le range de l'iptype ne correspondent pas!")
return return

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.

View file

@ -27,30 +27,36 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %} {% endif %}
<table class="table"> <table class="table">
<colgroup>
<col>
<col>
<col>
<col width="{% if ipv6_enabled %}300{% else %}150{% endif %}px">
<col width="144px">
</colgroup>
<thead> <thead>
<tr> <th>Nom DNS</th>
<th>Actions</th>
<th>Proprietaire</th>
<th>Nom dns</th>
<th>Type</th> <th>Type</th>
<th>Mac</th> <th>MAC</th>
<th>IP</th> <th>IP</th>
<th></th> <th>Actions</th>
</tr> <tbody>
</thead>
{% for machine in machines_list %} {% for machine in machines_list %}
{% for interface in machine.interface_set.all %} <tr class="info">
<tr class="active"> <td colspan="4">
{% if forloop.first %} <b>{{ machine.name }}</b> <i class="glyphicon glyphicon-chevron-right"></i>
<td rowspan="{{ machine.interface_set.all|length }}"> <a href="{% url 'users:profil' userid=machine.user.id %}" title="Voir le profil">
<i class="glyphicon glyphicon-user"></i> {{ machine.user }}
</a>
</td>
<td class="text-right">
{% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc='Ajouter une interface' %} {% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc='Ajouter une interface' %}
{% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %}
{% include 'buttons/history.html' with href='machines:history' name='machine' id=machine.id %} {% include 'buttons/history.html' with href='machines:history' name='machine' id=machine.id %}
{% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %}
</td> </td>
<td rowspan="{{ machine.interface_set.all|length }}"> </tr>
<a href="{% url 'users:profil' userid=machine.user.id %}"><b>{{ machine.user }}</b></a> {% for interface in machine.interface_set.all %}
</td> <tr>
{% endif %}
<td> <td>
{% if interface.domain.related_domain.all %} {% if interface.domain.related_domain.all %}
<div class="dropdown"> <div class="dropdown">
@ -72,19 +78,26 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{{ interface.domain }} {{ interface.domain }}
{% endif %} {% endif %}
</td> </td>
<td>{{ interface.type }}</td> <td>
<td>{{ interface.mac_address }}</td> {{ interface.type }}
<td><b>IPv4</b> {{ interface.ipv4 }} </td>
{% if ipv6_enabled %} <td>
{{ interface.mac_address }}
</td>
<td>
<b>IPv4</b> {{ interface.ipv4 }}
<br> <br>
{% if ipv6_enabled and interface.ipv6 != 'None'%}
<b>IPv6</b> {{ interface.ipv6 }} <b>IPv6</b> {{ interface.ipv6 }}
{% endif %} {% endif %}
</td> </td>
<td> <td class="text-right">
<div class="dropdown"> <div class="dropdown" style="width: 128px;">
<button class="btn btn-default dropdown-toggle" type="button" id="editioninterface" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> <button class="btn btn-primary btn-sm dropdown-toggle" type="button" id="editioninterface" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
Modifier <span class="caret"></span> <i class="glyphicon glyphicon-edit"></i> <span class="caret"></span>
</button> </button>
{% include 'buttons/history.html' with href='machines:history' name='interface' id=interface.id %}
{% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %}
<ul class="dropdown-menu" aria-labelledby="editioninterface"> <ul class="dropdown-menu" aria-labelledby="editioninterface">
<li> <li>
<a href="{% url 'machines:edit-interface' interface.id %}"> <a href="{% url 'machines:edit-interface' interface.id %}">
@ -101,16 +114,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="glyphicon glyphicon-edit"></i> Gerer la configuration des ports <i class="glyphicon glyphicon-edit"></i> Gerer la configuration des ports
</a> </a>
</li> </li>
<li>
<a href="{% url 'machines:history' 'interface' interface.id %}">
<i class="glyphicon glyphicon-time"></i> Historique
</a>
</li>
<li>
<a href="{% url 'machines:del-interface' interface.id %}">
<i class="glyphicon glyphicon-trash"></i> Supprimer
</a>
</li>
</ul> </ul>
</div> </div>
</td> </td>
@ -120,5 +123,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td colspan="8"></td> <td colspan="8"></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table> </table>

View file

@ -7,6 +7,7 @@ quelques clics.
Copyright © 2017 Gabriel Détraz Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle Copyright © 2017 Augustin Lemesle
Copyright © 2017 Maël Kervella
This program is free software; you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@ -24,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %} {% endcomment %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load bootstrap_form_typeahead %}
{% block title %}Création et modification de machines{% endblock %} {% block title %}Création et modification de machines{% endblock %}
@ -38,16 +40,30 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_form_errors domainform %} {% bootstrap_form_errors domainform %}
{% endif %} {% endif %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}
{% if machineform %} {% if machineform %}
<h3>Machine</h3>
{% bootstrap_form machineform %} {% bootstrap_form machineform %}
{% endif %} {% endif %}
{% if interfaceform %} {% if interfaceform %}
{% bootstrap_form interfaceform %} <h3>Interface</h3>
{% if i_bft_param %}
{% if 'machine' in interfaceform.fields %}
{% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %}
{% else %}
{% bootstrap_form_typeahead interfaceform 'ipv4' bft_param=i_bft_param %}
{% endif %}
{% else %}
{% if 'machine' in interfaceform.fields %}
{% bootstrap_form_typeahead interfaceform 'ipv4,machine' %}
{% else %}
{% bootstrap_form_typeahead interfaceform 'ipv4' %}
{% endif %}
{% endif %}
{% endif %} {% endif %}
{% if domainform %} {% if domainform %}
<h3>Domaine</h3>
{% bootstrap_form domainform %} {% bootstrap_form domainform %}
{% endif %} {% endif %}
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}

View file

@ -0,0 +1,21 @@
# -*- mode: python; coding: utf-8 -*-
# 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 Maël Kervella
#
# 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.

View file

@ -0,0 +1,386 @@
# -*- mode: python; coding: utf-8 -*-
# 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 Maël Kervella
#
# 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.
from django import template
from django.utils.safestring import mark_safe
from django.forms import TextInput
from bootstrap3.templatetags.bootstrap3 import bootstrap_form
from bootstrap3.utils import render_tag
from bootstrap3.forms import render_field
register = template.Library()
@register.simple_tag
def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs):
"""
Render a form where some specific fields are rendered using Typeahead.
Using Typeahead really improves the performance, the speed and UX when
dealing with very large datasets (select with 50k+ elts for instance).
For convenience, it accepts the same parameters as a standard bootstrap
can accept.
**Tag name**::
bootstrap_form_typeahead
**Parameters**:
form
The form that is to be rendered
typeahead_fields
A list of field names (comma separated) that should be rendered
with typeahead instead of the default bootstrap renderer.
bft_param
A dict of parameters for the bootstrap_form_typeahead tag. The
possible parameters are the following.
choices
A dict of strings representing the choices in JS. The keys of
the dict are the names of the concerned fields. The choices
must be an array of objects. Each of those objects must at
least have the fields 'key' (value to send) and 'value' (value
to display). Other fields can be added as desired.
For a more complex structure you should also consider
reimplementing the engine and the match_func.
If not specified, the key is the id of the object and the value
is its string representation as in a normal bootstrap form.
Example :
'choices' : {
'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]',
'field_B':...,
...
}
engine
A dict of strings representating the engine used for matching
queries and possible values with typeahead. The keys of the
dict are the names of the concerned fields. The string is valid
JS code.
If not specified, BloodHound with relevant basic properties is
used.
Example :
'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...}
match_func
A dict of strings representing a valid JS function used in the
dataset to overload the matching engine. The keys of the dict
are the names of the concerned fields. This function is used
the source of the dataset. This function receives 2 parameters,
the query and the synchronize function as specified in
typeahead.js documentation. If needed, the local variables
'choices_<fieldname>' and 'engine_<fieldname>' contains
respectively the array of all possible values and the engine
to match queries with possible values.
If not specified, the function used display up to the 10 first
elements if the query is empty and else the matching results.
Example :
'match_func' : {
'field_A': 'function(q, sync) { engine.search(q, sync); }',
'field_B': ...,
...
}
update_on
A dict of list of ids that the values depends on. The engine
and the typeahead properties are recalculated and reapplied.
Example :
'addition' : {
'field_A' : [ 'id0', 'id1', ... ] ,
'field_B' : ... ,
...
}
See boostrap_form_ for other arguments
**Usage**::
{% bootstrap_form_typeahead
form
[ '<field1>[,<field2>[,...]]' ]
[ {
[ 'choices': {
[ '<field1>': '<choices1>'
[, '<field2>': '<choices2>'
[, ... ] ] ]
} ]
[, 'engine': {
[ '<field1>': '<engine1>'
[, '<field2>': '<engine2>'
[, ... ] ] ]
} ]
[, 'match_func': {
[ '<field1>': '<match_func1>'
[, '<field2>': '<match_func2>'
[, ... ] ] ]
} ]
[, 'update_on': {
[ '<field1>': '<update_on1>'
[, '<field2>': '<update_on2>'
[, ... ] ] ]
} ]
} ]
[ <standard boostrap_form parameters> ]
%}
**Example**:
{% bootstrap_form_typeahead form 'ipv4' choices='[...]' %}
"""
t_fields = typeahead_fields.split(',')
params = kwargs.get('bft_param', {})
exclude = params.get('exclude', None)
exclude = exclude.split(',') if exclude else []
t_choices = params.get('choices', {})
t_engine = params.get('engine', {})
t_match_func = params.get('match_func', {})
t_update_on = params.get('update_on', {})
hidden = [h.name for h in django_form.hidden_fields()]
form = ''
for f_name, f_value in django_form.fields.items() :
if not f_name in exclude :
if f_name in t_fields and not f_name in hidden :
f_bound = f_value.get_bound_field( django_form, f_name )
f_value.widget = TextInput(
attrs={
'name': 'typeahead_'+f_name,
'placeholder': f_value.empty_label
}
)
form += render_field(
f_value.get_bound_field( django_form, f_name ),
*args,
**kwargs
)
form += render_tag(
'div',
content = hidden_tag( f_bound, f_name ) +
typeahead_js(
f_name,
f_value,
f_bound,
t_choices,
t_engine,
t_match_func,
t_update_on
)
)
else:
form += render_field(
f_value.get_bound_field(django_form, f_name),
*args,
**kwargs
)
return mark_safe( form )
def input_id( f_name ) :
""" The id of the HTML input element """
return 'id_'+f_name
def hidden_id( f_name ):
""" The id of the HTML hidden input element """
return 'typeahead_hidden_'+f_name
def hidden_tag( f_bound, f_name ):
""" The HTML hidden input element """
return render_tag(
'input',
attrs={
'id': hidden_id(f_name),
'name': f_name,
'type': 'hidden',
'value': f_bound.value() or ""
}
)
def typeahead_js( f_name, f_value, f_bound,
t_choices, t_engine, t_match_func, t_update_on ) :
""" The whole script to use """
choices = mark_safe( t_choices[f_name] ) if f_name in t_choices.keys() \
else default_choices( f_value )
engine = mark_safe( t_engine[f_name] ) if f_name in t_engine.keys() \
else default_engine ( f_name )
match_func = mark_safe(t_match_func[f_name]) \
if f_name in t_match_func.keys() else default_match_func( f_name )
update_on = t_update_on[f_name] if f_name in t_update_on.keys() else []
js_content = (
'var choices_{f_name} = {choices};'
'var engine_{f_name};'
'var setup_{f_name} = function() {{'
'engine_{f_name} = {engine};'
'$( "#{input_id}" ).typeahead( "destroy" );'
'$( "#{input_id}" ).typeahead( {datasets} );'
'}};'
'$( "#{input_id}" ).bind( "typeahead:select", {updater} );'
'$( "#{input_id}" ).bind( "typeahead:change", {change} );'
'{updates}'
'$( "#{input_id}" ).ready( function() {{'
'setup_{f_name}();'
'{init_input}'
'}} );'
).format(
f_name = f_name,
choices = choices,
engine = engine,
input_id = input_id( f_name ),
datasets = default_datasets( f_name, match_func ),
updater = typeahead_updater( f_name ),
change = typeahead_change( f_name ),
updates = ''.join( [ (
'$( "#{u_id}" ).change( function() {{'
'setup_{f_name}();'
'{reset_input}'
'}} );'
).format(
u_id = u_id,
reset_input = reset_input( f_name ),
f_name = f_name
) for u_id in update_on ]
),
init_input = init_input( f_name, f_bound ),
)
return render_tag( 'script', content=mark_safe( js_content ) )
def init_input( f_name, f_bound ) :
""" The JS script to init the fields values """
init_key = f_bound.value() or '""'
return (
'$( "#{input_id}" ).typeahead("val", {init_val});'
'$( "#{hidden_id}" ).val( {init_key} );'
).format(
input_id = input_id( f_name ),
init_val = '""' if init_key == '""' else
'engine_{f_name}.get( {init_key} )[0].value'.format(
f_name = f_name,
init_key = init_key
),
init_key = init_key,
hidden_id = hidden_id( f_name )
)
def reset_input( f_name ) :
""" The JS script to reset the fields values """
return (
'$( "#{input_id}" ).typeahead("val", "");'
'$( "#{hidden_id}" ).val( "" );'
).format(
input_id = input_id( f_name ),
hidden_id = hidden_id( f_name )
)
def default_choices( f_value ) :
""" The JS script creating the variable choices_<fieldname> """
return '[{objects}]'.format(
objects = ','.join(
[ '{{key:{k},value:"{v}"}}'.format(
k = choice[0] if choice[0] != '' else '""',
v = choice[1]
) for choice in f_value.choices ]
)
)
def default_engine ( f_name ) :
""" The JS script creating the variable engine_<field_name> """
return (
'new Bloodhound({{'
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),'
'queryTokenizer: Bloodhound.tokenizers.whitespace,'
'local: choices_{f_name},'
'identify: function(obj) {{ return obj.key; }}'
'}})'
).format(
f_name = f_name
)
def default_datasets( f_name, match_func ) :
""" The JS script creating the datasets to use with typeahead """
return (
'{{'
'hint: true,'
'highlight: true,'
'minLength: 0'
'}},'
'{{'
'display: "value",'
'name: "{f_name}",'
'source: {match_func}'
'}}'
).format(
f_name = f_name,
match_func = match_func
)
def default_match_func ( f_name ) :
""" The JS script creating the matching function to use with typeahed """
return (
'function ( q, sync ) {{'
'if ( q === "" ) {{'
'var first = choices_{f_name}.slice( 0, 5 ).map('
'function ( obj ) {{ return obj.key; }}'
');'
'sync( engine_{f_name}.get( first ) );'
'}} else {{'
'engine_{f_name}.search( q, sync );'
'}}'
'}}'
).format(
f_name = f_name
)
def typeahead_updater( f_name ):
""" The JS script creating the function triggered when an item is
selected through typeahead """
return (
'function(evt, item) {{'
'$( "#{hidden_id}" ).val( item.key );'
'$( "#{hidden_id}" ).change();'
'return item;'
'}}'
).format(
hidden_id = hidden_id( f_name )
)
def typeahead_change( f_name ):
""" The JS script creating the function triggered when an item is changed
(i.e. looses focus and value has changed since the moment it gained focus
"""
return (
'function(evt) {{'
'if ( $( "#{input_id}" ).typeahead( "val" ) === "" ) {{'
'$( "#{hidden_id}" ).val( "" );'
'$( "#{hidden_id}" ).change();'
'}}'
'}}'
).format(
input_id = input_id( f_name ),
hidden_id = hidden_id( f_name )
)

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.

View file

@ -1,3 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il # 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 # se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics. # quelques clics.
@ -5,6 +6,7 @@
# Copyright © 2017 Gabriel Détraz # Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec # Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle # Copyright © 2017 Augustin Lemesle
# Copyright © 2017 Maël Kervella
# #
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -34,7 +36,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.template import Context, RequestContext, loader from django.template import Context, RequestContext, loader
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.db.models import ProtectedError from django.db.models import ProtectedError, F
from django.forms import ValidationError, modelformset_factory from django.forms import ValidationError, modelformset_factory
from django.db import transaction from django.db import transaction
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
@ -53,6 +55,7 @@ from .models import IpType, Machine, Interface, IpList, MachineType, Extension,
from users.models import User from users.models import User
from users.models import all_has_access from users.models import all_has_access
from preferences.models import GeneralOption, OptionalMachine from preferences.models import GeneralOption, OptionalMachine
from .templatetags.bootstrap_form_typeahead import hidden_id, input_id
def all_active_interfaces(): def all_active_interfaces():
"""Renvoie l'ensemble des machines autorisées à sortir sur internet """ """Renvoie l'ensemble des machines autorisées à sortir sur internet """
@ -75,8 +78,90 @@ def form(ctx, template, request):
c.update(csrf(request)) c.update(csrf(request))
return render(request, template, c) return render(request, template, c)
def f_type_id( is_type_tt ):
""" The id that will be used in HTML to store the value of the field
type. Depends on the fact that type is generate using typeahead or not
"""
return hidden_id('type') if is_type_tt else input_id('type')
def generate_ipv4_choices( form ) :
""" Generate the parameter choices for the bootstrap_form_typeahead tag
"""
f_ipv4 = form.fields['ipv4']
used_mtype_id = []
choices = '{"":[{key:"",value:"Choisissez d\'abord un type de machine"},'
mtype_id = -1
for ip in f_ipv4.queryset.annotate(mtype_id=F('ip_type__machinetype__id')).order_by('mtype_id', 'id') :
if mtype_id != ip.mtype_id :
mtype_id = ip.mtype_id
used_mtype_id.append(mtype_id)
choices += '],"{t}":[{{key:"",value:"{v}"}},'.format(
t = mtype_id,
v = f_ipv4.empty_label or '""'
)
choices += '{{key:{k},value:"{v}"}},'.format(
k = ip.id,
v = ip.ipv4
)
for t in form.fields['type'].queryset.exclude(id__in=used_mtype_id) :
choices += '], "'+str(t.id)+'": ['
choices += '{key: "", value: "' + str(f_ipv4.empty_label) + '"},'
choices += ']}'
return choices
def generate_ipv4_engine( is_type_tt ) :
""" Generate the parameter engine for the bootstrap_form_typeahead tag
"""
return (
'new Bloodhound( {{'
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace( "value" ),'
'queryTokenizer: Bloodhound.tokenizers.whitespace,'
'local: choices_ipv4[ $( "#{type_id}" ).val() ],'
'identify: function( obj ) {{ return obj.key; }}'
'}} )'
).format(
type_id = f_type_id( is_type_tt )
)
def generate_ipv4_match_func( is_type_tt ) :
""" Generate the parameter match_func for the bootstrap_form_typeahead tag
"""
return (
'function(q, sync) {{'
'if (q === "") {{'
'var first = choices_ipv4[$("#{type_id}").val()].slice(0, 5);'
'first = first.map( function (obj) {{ return obj.key; }} );'
'sync(engine_ipv4.get(first));'
'}} else {{'
'engine_ipv4.search(q, sync);'
'}}'
'}}'
).format(
type_id = f_type_id( is_type_tt )
)
def generate_ipv4_bft_param( form, is_type_tt ):
""" Generate all the parameters to use with the bootstrap_form_typeahead
tag """
i_choices = { 'ipv4': generate_ipv4_choices( form ) }
i_engine = { 'ipv4': generate_ipv4_engine( is_type_tt ) }
i_match_func = { 'ipv4': generate_ipv4_match_func( is_type_tt ) }
i_update_on = { 'ipv4': [f_type_id( is_type_tt )] }
i_bft_param = {
'choices': i_choices,
'engine': i_engine,
'match_func': i_match_func,
'update_on': i_update_on
}
return i_bft_param
@login_required @login_required
def new_machine(request, userid): def new_machine(request, userid):
""" Fonction de creation d'une machine. Cree l'objet machine, le sous objet interface et l'objet domain
à partir de model forms.
Trop complexe, devrait être simplifié"""
try: try:
user = User.objects.get(pk=userid) user = User.objects.get(pk=userid)
except User.DoesNotExist: except User.DoesNotExist:
@ -118,10 +203,13 @@ def new_machine(request, userid):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "La machine a été créée") messages.success(request, "La machine a été créée")
return redirect("/users/profil/" + str(user.id)) return redirect("/users/profil/" + str(user.id))
return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain}, 'machines/machine.html', request) i_bft_param = generate_ipv4_bft_param( interface, False )
return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'machines/machine.html', request)
@login_required @login_required
def edit_interface(request, interfaceid): def edit_interface(request, interfaceid):
""" Edition d'une interface. Distingue suivant les droits les valeurs de interfaces et machines que l'user peut modifier
infra permet de modifier le propriétaire"""
try: try:
interface = Interface.objects.get(pk=interfaceid) interface = Interface.objects.get(pk=interfaceid)
except Interface.DoesNotExist: except Interface.DoesNotExist:
@ -155,10 +243,12 @@ def edit_interface(request, interfaceid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data)) reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data))
messages.success(request, "La machine a été modifiée") messages.success(request, "La machine a été modifiée")
return redirect("/users/profil/" + str(interface.machine.user.id)) return redirect("/users/profil/" + str(interface.machine.user.id))
return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form}, 'machines/machine.html', request) i_bft_param = generate_ipv4_bft_param( interface_form, False )
return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request)
@login_required @login_required
def del_machine(request, machineid): def del_machine(request, machineid):
""" Supprime une machine, interfaces en mode cascade"""
try: try:
machine = Machine.objects.get(pk=machineid) machine = Machine.objects.get(pk=machineid)
except Machine.DoesNotExist: except Machine.DoesNotExist:
@ -178,6 +268,7 @@ def del_machine(request, machineid):
@login_required @login_required
def new_interface(request, machineid): def new_interface(request, machineid):
""" Ajoute une interface et son domain associé à une machine existante"""
try: try:
machine = Machine.objects.get(pk=machineid) machine = Machine.objects.get(pk=machineid)
except Machine.DoesNotExist: except Machine.DoesNotExist:
@ -211,10 +302,12 @@ def new_interface(request, machineid):
reversion.set_comment("Création") reversion.set_comment("Création")
messages.success(request, "L'interface a été ajoutée") messages.success(request, "L'interface a été ajoutée")
return redirect("/users/profil/" + str(machine.user.id)) return redirect("/users/profil/" + str(machine.user.id))
return form({'interfaceform': interface_form, 'domainform': domain_form}, 'machines/machine.html', request) i_bft_param = generate_ipv4_bft_param( interface_form, False )
return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request)
@login_required @login_required
def del_interface(request, interfaceid): def del_interface(request, interfaceid):
""" Supprime une interface. Domain objet en mode cascade"""
try: try:
interface = Interface.objects.get(pk=interfaceid) interface = Interface.objects.get(pk=interfaceid)
except Interface.DoesNotExist: except Interface.DoesNotExist:
@ -238,6 +331,7 @@ def del_interface(request, interfaceid):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def add_iptype(request): def add_iptype(request):
""" Ajoute un range d'ip. Intelligence dans le models, fonction views minimaliste"""
iptype = IpTypeForm(request.POST or None) iptype = IpTypeForm(request.POST or None)
if iptype.is_valid(): if iptype.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -251,6 +345,7 @@ def add_iptype(request):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def edit_iptype(request, iptypeid): def edit_iptype(request, iptypeid):
""" Edition d'un range. Ne permet pas de le redimensionner pour éviter l'incohérence"""
try: try:
iptype_instance = IpType.objects.get(pk=iptypeid) iptype_instance = IpType.objects.get(pk=iptypeid)
except IpType.DoesNotExist: except IpType.DoesNotExist:
@ -269,6 +364,7 @@ def edit_iptype(request, iptypeid):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def del_iptype(request): def del_iptype(request):
""" Suppression d'un range ip. Supprime les objets ip associés"""
iptype = DelIpTypeForm(request.POST or None) iptype = DelIpTypeForm(request.POST or None)
if iptype.is_valid(): if iptype.is_valid():
iptype_dels = iptype.cleaned_data['iptypes'] iptype_dels = iptype.cleaned_data['iptypes']
@ -761,13 +857,13 @@ def index(request):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_iptype(request): def index_iptype(request):
iptype_list = IpType.objects.select_related('extension').order_by('type') iptype_list = IpType.objects.select_related('extension').select_related('vlan').order_by('type')
return render(request, 'machines/index_iptype.html', {'iptype_list':iptype_list}) return render(request, 'machines/index_iptype.html', {'iptype_list':iptype_list})
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_vlan(request): def index_vlan(request):
vlan_list = Vlan.objects.order_by('vlan_id') vlan_list = Vlan.objects.prefetch_related('iptype_set').order_by('vlan_id')
return render(request, 'machines/index_vlan.html', {'vlan_list':vlan_list}) return render(request, 'machines/index_vlan.html', {'vlan_list':vlan_list})
@login_required @login_required
@ -779,7 +875,7 @@ def index_machinetype(request):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_nas(request): def index_nas(request):
nas_list = Nas.objects.select_related('machine_type').order_by('name') nas_list = Nas.objects.select_related('machine_type').select_related('nas_type').order_by('name')
return render(request, 'machines/index_nas.html', {'nas_list':nas_list}) return render(request, 'machines/index_nas.html', {'nas_list':nas_list})
@login_required @login_required
@ -807,8 +903,8 @@ def index_alias(request, interfaceid):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_service(request): def index_service(request):
service_list = Service.objects.all() service_list = Service.objects.prefetch_related('service_link_set__server__domain__extension').all()
servers_list = Service_link.objects.all() servers_list = Service_link.objects.select_related('server__domain__extension').select_related('service').all()
return render(request, 'machines/index_service.html', {'service_list':service_list, 'servers_list':servers_list}) return render(request, 'machines/index_service.html', {'service_list':service_list, 'servers_list':servers_list})
@login_required @login_required
@ -916,7 +1012,7 @@ def history(request, object, id):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_portlist(request): def index_portlist(request):
port_list = OuverturePortList.objects.all().order_by('name') port_list = OuverturePortList.objects.prefetch_related('ouvertureport_set').prefetch_related('interface_set').order_by('name')
return render(request, "machines/index_portlist.html", {'port_list':port_list}) return render(request, "machines/index_portlist.html", {'port_list':port_list})
@login_required @login_required

View file

@ -0,0 +1,93 @@
span.twitter-typeahead .tt-menu,
span.twitter-typeahead .tt-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
list-style: none;
font-size: 14px;
text-align: left;
background-color: #ffffff;
border: 1px solid #cccccc;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-clip: padding-box;
}
span.twitter-typeahead .tt-suggestion {
display: block;
padding: 3px 20px;
clear: both;
font-weight: normal;
line-height: 1.42857143;
color: #333333;
white-space: nowrap;
}
span.twitter-typeahead .tt-suggestion.tt-cursor,
span.twitter-typeahead .tt-suggestion:hover,
span.twitter-typeahead .tt-suggestion:focus {
color: #ffffff;
text-decoration: none;
outline: 0;
background-color: #337ab7;
}
.input-group.input-group-lg span.twitter-typeahead .form-control {
height: 46px;
padding: 10px 16px;
font-size: 18px;
line-height: 1.3333333;
border-radius: 6px;
}
.input-group.input-group-sm span.twitter-typeahead .form-control {
height: 30px;
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
}
span.twitter-typeahead {
width: 100%;
}
.input-group span.twitter-typeahead {
display: block !important;
height: 34px;
}
.input-group span.twitter-typeahead .tt-menu,
.input-group span.twitter-typeahead .tt-dropdown-menu {
top: 32px !important;
}
.input-group span.twitter-typeahead:not(:first-child):not(:last-child) .form-control {
border-radius: 0;
}
.input-group span.twitter-typeahead:first-child .form-control {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group span.twitter-typeahead:last-child .form-control {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.input-group.input-group-sm span.twitter-typeahead {
height: 30px;
}
.input-group.input-group-sm span.twitter-typeahead .tt-menu,
.input-group.input-group-sm span.twitter-typeahead .tt-dropdown-menu {
top: 30px !important;
}
.input-group.input-group-lg span.twitter-typeahead {
height: 46px;
}
.input-group.input-group-lg span.twitter-typeahead .tt-menu,
.input-group.input-group-lg span.twitter-typeahead .tt-dropdown-menu {
top: 46px !important;
}

4840
static/js/handlebars.js Normal file

File diff suppressed because one or more lines are too long

2451
static/js/typeahead.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -32,8 +32,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<head> <head>
{# Load CSS and JavaScript #} {# Load CSS and JavaScript #}
{% bootstrap_css %} {% bootstrap_css %}
<link href="/static/css/typeaheadjs.css" rel="stylesheet">
{% bootstrap_javascript %} {% bootstrap_javascript %}
<script src="/static/js/typeahead.js"></script>
<script src="/static/js/handlebars.js"></script>
<link rel="stylesheet" href="{% static "/css/base.css" %}"> <link rel="stylesheet" href="{% static "/css/base.css" %}">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ site_name }} : {% block title %}Accueil{% endblock %}</title> <title>{{ site_name }} : {% block title %}Accueil{% endblock %}</title>

View file

@ -34,17 +34,11 @@ import reversion
from machines.models import Vlan from machines.models import Vlan
def make_port_related(port):
related_port = port.related
related_port.related = port
related_port.save()
def clean_port_related(port):
related_port = port.related_port
related_port.related = None
related_port.save()
class Stack(models.Model): class Stack(models.Model):
""" Un objet stack. Regrouppe des switchs en foreign key
, contient une id de stack, un switch id min et max dans
le stack"""
PRETTY_NAME = "Stack de switchs" PRETTY_NAME = "Stack de switchs"
name = models.CharField(max_length=32, blank=True, null=True) name = models.CharField(max_length=32, blank=True, null=True)
@ -57,15 +51,25 @@ class Stack(models.Model):
return " ".join([self.name, self.stack_id]) return " ".join([self.name, self.stack_id])
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.clean()
if not self.name: if not self.name:
self.name = self.stack_id self.name = self.stack_id
super(Stack, self).save(*args, **kwargs) super(Stack, self).save(*args, **kwargs)
def clean(self): def clean(self):
""" Verification que l'id_max < id_min"""
if self.member_id_max < self.member_id_min: if self.member_id_max < self.member_id_min:
raise ValidationError({'member_id_max':"L'id maximale est inférieure à l'id minimale"}) raise ValidationError({'member_id_max':"L'id maximale est inférieure à l'id minimale"})
class Switch(models.Model): class Switch(models.Model):
""" Definition d'un switch. Contient un nombre de ports (number),
un emplacement (location), un stack parent (optionnel, stack)
et un id de membre dans le stack (stack_member_id)
relié en onetoone à une interface
Pourquoi ne pas avoir fait hériter switch de interface ?
Principalement par méconnaissance de la puissance de cette façon de faire.
Ceci étant entendu, django crée en interne un onetoone, ce qui a un
effet identique avec ce que l'on fait ici"""
PRETTY_NAME = "Switch / Commutateur" PRETTY_NAME = "Switch / Commutateur"
switch_interface = models.OneToOneField('machines.Interface', on_delete=models.CASCADE) switch_interface = models.OneToOneField('machines.Interface', on_delete=models.CASCADE)
@ -82,6 +86,7 @@ class Switch(models.Model):
return str(self.location) + ' ' + str(self.switch_interface) return str(self.location) + ' ' + str(self.switch_interface)
def clean(self): def clean(self):
""" Verifie que l'id stack est dans le bon range"""
if self.stack is not None: if self.stack is not None:
if self.stack_member_id is not None: if self.stack_member_id is not None:
if (self.stack_member_id > self.stack.member_id_max) or (self.stack_member_id < self.stack.member_id_min): if (self.stack_member_id > self.stack.member_id_max) or (self.stack_member_id < self.stack.member_id_min):
@ -90,6 +95,20 @@ class Switch(models.Model):
raise ValidationError({'stack_member_id': "L'id dans la stack ne peut être nul"}) raise ValidationError({'stack_member_id': "L'id dans la stack ne peut être nul"})
class Port(models.Model): class Port(models.Model):
""" Definition d'un port. Relié à un switch(foreign_key),
un port peut etre relié de manière exclusive à :
- une chambre (room)
- une machine (serveur etc) (machine_interface)
- un autre port (uplink) (related)
Champs supplémentaires :
- RADIUS (mode STRICT : connexion sur port uniquement si machine
d'un adhérent à jour de cotisation et que la chambre est également à jour de cotisation
mode COMMON : vérification uniquement du statut de la machine
mode NO : accepte toute demande venant du port et place sur le vlan normal
mode BLOQ : rejet de toute authentification
- vlan_force : override la politique générale de placement vlan, permet
de forcer un port sur un vlan particulier. S'additionne à la politique
RADIUS"""
PRETTY_NAME = "Port de switch" PRETTY_NAME = "Port de switch"
STATES = ( STATES = (
('NO', 'NO'), ('NO', 'NO'),
@ -110,7 +129,26 @@ class Port(models.Model):
class Meta: class Meta:
unique_together = ('switch', 'port') unique_together = ('switch', 'port')
def make_port_related(self):
""" Synchronise le port distant sur self"""
related_port = self.related
related_port.related = self
related_port.save()
def clean_port_related(self):
""" Supprime la relation related sur self"""
related_port = self.related_port
related_port.related = None
related_port.save()
def clean(self): def clean(self):
""" Verifie que un seul de chambre, interface_parent et related_port est rempli.
Verifie que le related n'est pas le port lui-même....
Verifie que le related n'est pas déjà occupé par une machine ou une chambre. Si
ce n'est pas le cas, applique la relation related
Si un port related point vers self, on nettoie la relation
A priori pas d'autre solution que de faire ça à la main. A priori tout cela est dans
un bloc transaction, donc pas de problème de cohérence"""
if hasattr(self, 'switch'): if hasattr(self, 'switch'):
if self.port > self.switch.number: if self.port > self.switch.number:
raise ValidationError("Ce port ne peut exister, numero trop élevé") raise ValidationError("Ce port ne peut exister, numero trop élevé")
@ -122,14 +160,15 @@ class Port(models.Model):
if self.related.machine_interface or self.related.room: if self.related.machine_interface or self.related.room:
raise ValidationError("Le port relié est déjà occupé, veuillez le libérer avant de créer une relation") raise ValidationError("Le port relié est déjà occupé, veuillez le libérer avant de créer une relation")
else: else:
make_port_related(self) self.make_port_related()
elif hasattr(self, 'related_port'): elif hasattr(self, 'related_port'):
clean_port_related(self) self.clean_port_related()
def __str__(self): def __str__(self):
return str(self.switch) + " - " + str(self.port) return str(self.switch) + " - " + str(self.port)
class Room(models.Model): class Room(models.Model):
""" Une chambre/local contenant une prise murale"""
PRETTY_NAME = "Chambre/ Prise murale" PRETTY_NAME = "Chambre/ Prise murale"
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)

View file

@ -44,12 +44,14 @@ from preferences.models import AssoOption, GeneralOption
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index(request): def index(request):
switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain') """ Vue d'affichage de tous les swicthes"""
switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain').select_related('stack')
return render(request, 'topologie/index.html', {'switch_list': switch_list}) return render(request, 'topologie/index.html', {'switch_list': switch_list})
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def history(request, object, id): def history(request, object, id):
""" Vue générique pour afficher l'historique complet d'un objet"""
if object == 'switch': if object == 'switch':
try: try:
object_instance = Switch.objects.get(pk=id) object_instance = Switch.objects.get(pk=id)
@ -95,6 +97,7 @@ def history(request, object, id):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_port(request, switch_id): def index_port(request, switch_id):
""" Affichage de l'ensemble des ports reliés à un switch particulier"""
try: try:
switch = Switch.objects.get(pk=switch_id) switch = Switch.objects.get(pk=switch_id)
except Switch.DoesNotExist: except Switch.DoesNotExist:
@ -106,6 +109,7 @@ def index_port(request, switch_id):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_room(request): def index_room(request):
""" Affichage de l'ensemble des chambres"""
room_list = Room.objects.order_by('name') room_list = Room.objects.order_by('name')
options, created = GeneralOption.objects.get_or_create() options, created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number pagination_number = options.pagination_number
@ -124,13 +128,14 @@ def index_room(request):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def index_stack(request): def index_stack(request):
stack_list = Stack.objects.order_by('name') stack_list = Stack.objects.order_by('name').prefetch_related('switch_set__switch_interface__domain__extension')
return render(request, 'topologie/index_stack.html', {'stack_list': stack_list}) return render(request, 'topologie/index_stack.html', {'stack_list': stack_list})
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def new_port(request, switch_id): def new_port(request, switch_id):
""" Nouveau port"""
try: try:
switch = Switch.objects.get(pk=switch_id) switch = Switch.objects.get(pk=switch_id)
except Switch.DoesNotExist: except Switch.DoesNotExist:
@ -154,6 +159,7 @@ def new_port(request, switch_id):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def edit_port(request, port_id): def edit_port(request, port_id):
""" Edition d'un port. Permet de changer le switch parent et l'affectation du port"""
try: try:
port_object = Port.objects.select_related('switch__switch_interface__domain__extension').select_related('machine_interface__domain__extension').select_related('machine_interface__switch').select_related('room').select_related('related').get(pk=port_id) port_object = Port.objects.select_related('switch__switch_interface__domain__extension').select_related('machine_interface__domain__extension').select_related('machine_interface__switch').select_related('room').select_related('related').get(pk=port_id)
except Port.DoesNotExist: except Port.DoesNotExist:
@ -172,6 +178,7 @@ def edit_port(request, port_id):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def del_port(request,port_id): def del_port(request,port_id):
""" Supprime le port"""
try: try:
port = Port.objects.get(pk=port_id) port = Port.objects.get(pk=port_id)
except Port.DoesNotExist: except Port.DoesNotExist:
@ -263,6 +270,9 @@ def edit_switchs_stack(request,stack_id):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def new_switch(request): def new_switch(request):
""" Creation d'un switch. Cree en meme temps l'interface et la machine associée.
Vue complexe. Appelle successivement les 4 models forms adaptés : machine,
interface, domain et switch"""
switch = NewSwitchForm(request.POST or None) switch = NewSwitchForm(request.POST or None)
machine = NewMachineForm(request.POST or None) machine = NewMachineForm(request.POST or None)
interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',))) interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',)))
@ -304,6 +314,8 @@ def new_switch(request):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def edit_switch(request, switch_id): def edit_switch(request, switch_id):
""" Edition d'un switch. Permet de chambre nombre de ports, place dans le stack,
interface et machine associée"""
try: try:
switch = Switch.objects.get(pk=switch_id) switch = Switch.objects.get(pk=switch_id)
except Switch.DoesNotExist: except Switch.DoesNotExist:
@ -341,6 +353,7 @@ def edit_switch(request, switch_id):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def new_room(request): def new_room(request):
"""Nouvelle chambre """
room = EditRoomForm(request.POST or None) room = EditRoomForm(request.POST or None)
if room.is_valid(): if room.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -354,6 +367,7 @@ def new_room(request):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def edit_room(request, room_id): def edit_room(request, room_id):
""" Edition numero et details de la chambre"""
try: try:
room = Room.objects.get(pk=room_id) room = Room.objects.get(pk=room_id)
except Room.DoesNotExist: except Room.DoesNotExist:
@ -372,6 +386,7 @@ def edit_room(request, room_id):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def del_room(request, room_id): def del_room(request, room_id):
""" Suppression d'un chambre"""
try: try:
room = Room.objects.get(pk=room_id) room = Room.objects.get(pk=room_id)
except Room.DoesNotExist: except Room.DoesNotExist:

View file

@ -378,7 +378,7 @@ class User(AbstractBaseUser):
def user_interfaces(self, active=True): def user_interfaces(self, active=True):
""" Renvoie toutes les interfaces dont les machines appartiennent à self """ Renvoie toutes les interfaces dont les machines appartiennent à self
Par defaut ne prend que les interfaces actives""" Par defaut ne prend que les interfaces actives"""
return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=active)) return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=active)).select_related('domain__extension')
def assign_ips(self): def assign_ips(self):
""" Assign une ipv4 aux machines d'un user """ """ Assign une ipv4 aux machines d'un user """