mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-30 08:32:26 +00:00
2085 lines
75 KiB
Python
2085 lines
75 KiB
Python
# -*- 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 © 2016-2018 Gabriel Détraz
|
|
# Copyright © 2017 Goulven Kermarec
|
|
# Copyright © 2017 Augustin Lemesle
|
|
# Copyright © 2018 Charlie Jacomme
|
|
#
|
|
# 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.
|
|
"""machines.models
|
|
The models definitions for the Machines app
|
|
"""
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
import base64
|
|
import hashlib
|
|
import re
|
|
from datetime import timedelta
|
|
from ipaddress import IPv6Address
|
|
from itertools import chain
|
|
|
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
from django.db import models
|
|
from django.db.models import Q
|
|
from django.db.models.signals import post_save, post_delete
|
|
from django.dispatch import receiver
|
|
from django.forms import ValidationError
|
|
from django.utils import timezone
|
|
from django.db import transaction
|
|
from reversion import revisions as reversion
|
|
from django.utils.functional import cached_property
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from macaddress.fields import MACAddressField, default_dialect
|
|
from netaddr import mac_bare, EUI, NotRegisteredError, IPSet, IPRange, IPNetwork, IPAddress
|
|
|
|
import preferences.models
|
|
import users.models
|
|
from re2o.field_permissions import FieldPermissionModelMixin
|
|
from re2o.mixins import AclMixin, RevMixin
|
|
|
|
|
|
class Machine(RevMixin, FieldPermissionModelMixin, models.Model):
|
|
""" Class définissant une machine, object parent user, objets fils
|
|
interfaces"""
|
|
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
|
|
name = models.CharField(
|
|
max_length=255,
|
|
help_text=_("Optional"),
|
|
blank=True,
|
|
null=True
|
|
)
|
|
active = models.BooleanField(default=True)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_machine", _("Can view a machine object")),
|
|
("change_machine_user",
|
|
_("Can change the user of a machine")),
|
|
)
|
|
verbose_name = _("machine")
|
|
verbose_name_plural = _("machines")
|
|
|
|
@classmethod
|
|
def get_instance(cls, machineid, *_args, **_kwargs):
|
|
"""Get the Machine instance with machineid.
|
|
:param userid: The id
|
|
:return: The user
|
|
"""
|
|
return cls.objects.get(pk=machineid)
|
|
|
|
def linked_objects(self):
|
|
"""Return linked objects : machine and domain.
|
|
Usefull in history display"""
|
|
return chain(
|
|
self.interface_set.all(),
|
|
Domain.objects.filter(
|
|
interface_parent__in=self.interface_set.all()
|
|
)
|
|
)
|
|
|
|
@staticmethod
|
|
def can_change_user(user_request, *_args, **_kwargs):
|
|
"""Checks if an user is allowed to change the user who owns a
|
|
Machine.
|
|
|
|
Args:
|
|
user_request: The user requesting to change owner.
|
|
|
|
Returns:
|
|
A tuple with a boolean stating if edition is allowed and an
|
|
explanation message.
|
|
"""
|
|
return (user_request.has_perm('machines.change_machine_user'),
|
|
_("You don't have the right to change the machine's user."))
|
|
|
|
@staticmethod
|
|
def can_view_all(user_request, *_args, **_kwargs):
|
|
"""Vérifie qu'on peut bien afficher l'ensemble des machines,
|
|
droit particulier correspondant
|
|
:param user_request: instance user qui fait l'edition
|
|
:return: True ou False avec la raison de l'échec le cas échéant"""
|
|
if not user_request.has_perm('machines.view_machine'):
|
|
return False, _("You don't have the right to view all the"
|
|
" machines.")
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def can_create(user_request, userid, *_args, **_kwargs):
|
|
"""Vérifie qu'un user qui fait la requète peut bien créer la machine
|
|
et n'a pas atteint son quota, et crée bien une machine à lui
|
|
:param user_request: Utilisateur qui fait la requête
|
|
:param userid: id de l'user dont on va créer une machine
|
|
:return: soit True, soit False avec la raison de l'échec"""
|
|
try:
|
|
user = users.models.User.objects.get(pk=userid)
|
|
except users.models.User.DoesNotExist:
|
|
return False, _("Nonexistent user.")
|
|
max_lambdauser_interfaces = (preferences.models.OptionalMachine
|
|
.get_cached_value(
|
|
'max_lambdauser_interfaces'
|
|
))
|
|
if not user_request.has_perm('machines.add_machine'):
|
|
if not (preferences.models.OptionalMachine
|
|
.get_cached_value('create_machine')):
|
|
return False, (_("You don't have the right to add a machine."))
|
|
if user != user_request:
|
|
return False, (_("You don't have the right to add a machine"
|
|
" to another user."))
|
|
if user.user_interfaces().count() >= max_lambdauser_interfaces:
|
|
return False, (_("You reached the maximum number of interfaces"
|
|
" that you are allowed to create yourself"
|
|
" (%s)." % max_lambdauser_interfaces))
|
|
return True, None
|
|
|
|
def can_edit(self, user_request, *args, **kwargs):
|
|
"""Vérifie qu'on peut bien éditer cette instance particulière (soit
|
|
machine de soi, soit droit particulier
|
|
:param self: instance machine à éditer
|
|
:param user_request: instance user qui fait l'edition
|
|
:return: True ou False avec la raison le cas échéant"""
|
|
if self.user != user_request:
|
|
if (not user_request.has_perm('machines.change_interface') or
|
|
not self.user.can_edit(
|
|
self.user,
|
|
user_request,
|
|
*args,
|
|
**kwargs
|
|
)[0]):
|
|
return False, (_("You don't have the right to edit a machine"
|
|
" of another user."))
|
|
return True, None
|
|
|
|
def can_delete(self, user_request, *args, **kwargs):
|
|
"""Vérifie qu'on peut bien supprimer cette instance particulière (soit
|
|
machine de soi, soit droit particulier
|
|
:param self: instance machine à supprimer
|
|
:param user_request: instance user qui fait l'edition
|
|
:return: True ou False avec la raison de l'échec le cas échéant"""
|
|
if self.user != user_request:
|
|
if (not user_request.has_perm('machines.change_interface') or
|
|
not self.user.can_edit(
|
|
self.user,
|
|
user_request,
|
|
*args,
|
|
**kwargs
|
|
)[0]):
|
|
return False, _("You don't have the right to delete a machine"
|
|
" of another user.")
|
|
return True, None
|
|
|
|
def can_view(self, user_request, *_args, **_kwargs):
|
|
"""Vérifie qu'on peut bien voir cette instance particulière (soit
|
|
machine de soi, soit droit particulier
|
|
:param self: instance machine à éditer
|
|
:param user_request: instance user qui fait l'edition
|
|
:return: True ou False avec la raison de l'échec le cas échéant"""
|
|
if (not user_request.has_perm('machines.view_machine') and
|
|
self.user != user_request):
|
|
return False, _("You don't have the right to view other machines"
|
|
" than yours.")
|
|
return True, None
|
|
|
|
@cached_property
|
|
def short_name(self):
|
|
"""Par defaut, renvoie le nom de la première interface
|
|
de cette machine"""
|
|
interfaces_set = self.interface_set.first()
|
|
if interfaces_set:
|
|
return str(interfaces_set.domain.name)
|
|
else:
|
|
return _("No name")
|
|
|
|
@cached_property
|
|
def complete_name(self):
|
|
"""Par defaut, renvoie le nom de la première interface
|
|
de cette machine"""
|
|
return str(self.interface_set.first())
|
|
|
|
@cached_property
|
|
def all_short_names(self):
|
|
"""Renvoie de manière unique, le nom des interfaces de cette
|
|
machine"""
|
|
return Domain.objects.filter(
|
|
interface_parent__machine=self
|
|
).values_list('name', flat=True).distinct()
|
|
|
|
@cached_property
|
|
def get_name(self):
|
|
"""Return a name : user provided name or first interface name"""
|
|
return self.name or self.short_name
|
|
|
|
@classmethod
|
|
def mass_delete(cls, machine_queryset):
|
|
"""Mass delete for machine queryset"""
|
|
Domain.objects.filter(cname__interface_parent__machine__in=machine_queryset)._raw_delete(machine_queryset.db)
|
|
Domain.objects.filter(interface_parent__machine__in=machine_queryset)._raw_delete(machine_queryset.db)
|
|
Ipv6List.objects.filter(interface__machine__in=machine_queryset)._raw_delete(machine_queryset.db)
|
|
Interface.objects.filter(machine__in=machine_queryset).filter(port_lists__isnull=False).delete()
|
|
Interface.objects.filter(machine__in=machine_queryset)._raw_delete(machine_queryset.db)
|
|
machine_queryset._raw_delete(machine_queryset.db)
|
|
|
|
@cached_property
|
|
def all_complete_names(self):
|
|
"""Renvoie tous les tls complets de la machine"""
|
|
return [str(domain) for domain in Domain.objects.filter(
|
|
Q(cname__interface_parent__machine=self) | Q(interface_parent__machine=self)
|
|
)]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Machine, self).__init__(*args, **kwargs)
|
|
self.field_permissions = {
|
|
'user': self.can_change_user,
|
|
}
|
|
|
|
def __str__(self):
|
|
return str(self.user) + ' - ' + str(self.id) + ' - ' + str(self.name)
|
|
|
|
|
|
class MachineType(RevMixin, AclMixin, models.Model):
|
|
""" Type de machine, relié à un type d'ip, affecté aux interfaces"""
|
|
name = models.CharField(max_length=255)
|
|
ip_type = models.ForeignKey(
|
|
'IpType',
|
|
on_delete=models.PROTECT,
|
|
blank=True,
|
|
null=True
|
|
)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_machinetype", _("Can view a machine type object")),
|
|
("use_all_machinetype", _("Can use all machine types")),
|
|
)
|
|
verbose_name = _("machine type")
|
|
verbose_name_plural = _("machine types")
|
|
|
|
def all_interfaces(self):
|
|
""" Renvoie toutes les interfaces (cartes réseaux) de type
|
|
machinetype"""
|
|
return Interface.objects.filter(machine_type=self)
|
|
|
|
@staticmethod
|
|
def can_use_all(user_request, *_args, **_kwargs):
|
|
"""Check if an user can use every MachineType.
|
|
|
|
Args:
|
|
user_request: The user requesting edition.
|
|
Returns:
|
|
A tuple with a boolean stating if user can acces and an explanation
|
|
message is acces is not allowed.
|
|
"""
|
|
if not user_request.has_perm('machines.use_all_machinetype'):
|
|
return False, (_("You don't have the right to use all machine"
|
|
" types."))
|
|
return True, None
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class IpType(RevMixin, AclMixin, models.Model):
|
|
""" Type d'ip, définissant un range d'ip, affecté aux machine types"""
|
|
name = models.CharField(max_length=255)
|
|
extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
|
need_infra = models.BooleanField(default=False)
|
|
domaine_ip_start = models.GenericIPAddressField(protocol='IPv4')
|
|
domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4')
|
|
domaine_ip_network = models.GenericIPAddressField(
|
|
protocol='IPv4',
|
|
null=True,
|
|
blank=True,
|
|
help_text=_("Network containing the domain's IPv4 range (optional)")
|
|
)
|
|
domaine_ip_netmask = models.IntegerField(
|
|
default=24,
|
|
validators=[
|
|
MaxValueValidator(31),
|
|
MinValueValidator(8)
|
|
],
|
|
help_text=_("Netmask for the domain's IPv4 range")
|
|
)
|
|
reverse_v4 = models.BooleanField(
|
|
default=False,
|
|
help_text=_("Enable reverse DNS for IPv4"),
|
|
)
|
|
prefix_v6 = models.GenericIPAddressField(
|
|
protocol='IPv6',
|
|
null=True,
|
|
blank=True
|
|
)
|
|
prefix_v6_length = models.IntegerField(
|
|
default=64,
|
|
validators=[
|
|
MaxValueValidator(128),
|
|
MinValueValidator(0)
|
|
]
|
|
)
|
|
reverse_v6 = models.BooleanField(
|
|
default=False,
|
|
help_text=_("Enable reverse DNS for IPv6"),
|
|
)
|
|
vlan = models.ForeignKey(
|
|
'Vlan',
|
|
on_delete=models.PROTECT,
|
|
blank=True,
|
|
null=True
|
|
)
|
|
ouverture_ports = models.ForeignKey(
|
|
'OuverturePortList',
|
|
blank=True,
|
|
null=True
|
|
)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_iptype", _("Can view an IP type object")),
|
|
("use_all_iptype", _("Can use all IP types")),
|
|
)
|
|
verbose_name = _("IP type")
|
|
verbose_name_plural = _("IP types")
|
|
|
|
@cached_property
|
|
def ip_range(self):
|
|
""" Renvoie un objet IPRange à partir de l'objet IpType"""
|
|
return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop)
|
|
|
|
@cached_property
|
|
def ip_set(self):
|
|
""" Renvoie une IPSet à partir de l'iptype"""
|
|
return IPSet(self.ip_range)
|
|
|
|
@cached_property
|
|
def ip_set_as_str(self):
|
|
""" Renvoie une liste des ip en string"""
|
|
return [str(x) for x in self.ip_set]
|
|
|
|
@cached_property
|
|
def ip_set_cidrs_as_str(self):
|
|
"""Renvoie la liste des cidrs du range en str"""
|
|
return [str(ip_range) for ip_range in self.ip_set.iter_cidrs()]
|
|
|
|
@cached_property
|
|
def ip_set_full_info(self):
|
|
"""Iter sur les range cidr, et renvoie network, broacast , etc"""
|
|
return [
|
|
{
|
|
'network': str(ip_set.network),
|
|
'netmask': str(ip_set.netmask),
|
|
'netmask_cidr': str(ip_set.prefixlen),
|
|
'broadcast': str(ip_set.broadcast),
|
|
'vlan': str(self.vlan),
|
|
'vlan_id': self.vlan.vlan_id
|
|
} for ip_set in self.ip_set.iter_cidrs()
|
|
]
|
|
|
|
@cached_property
|
|
def ip6_set_full_info(self):
|
|
if self.prefix_v6:
|
|
return {
|
|
'network': str(self.prefix_v6),
|
|
'netmask': 'ffff:ffff:ffff:ffff::',
|
|
'netmask_cidr': str(self.prefix_v6_length),
|
|
'vlan': str(self.vlan),
|
|
'vlan_id': self.vlan.vlan_id
|
|
}
|
|
else:
|
|
return None
|
|
|
|
@cached_property
|
|
def ip_network(self):
|
|
"""Renvoie le network parent du range start-stop, si spécifié
|
|
Différent de ip_set_cidrs ou iP_set, car lui est supérieur ou égal"""
|
|
if self.domaine_ip_network:
|
|
return IPNetwork(str(self.domaine_ip_network) + '/' + str(self.domaine_ip_netmask))
|
|
return None
|
|
|
|
@cached_property
|
|
def ip_net_full_info(self):
|
|
"""Renvoie les infos du network contenant du range"""
|
|
return {
|
|
'network': str(self.ip_network.network),
|
|
'netmask': str(self.ip_network.netmask),
|
|
'broadcast': str(self.ip_network.broadcast),
|
|
'netmask_cidr': str(self.ip_network.prefixlen),
|
|
}
|
|
|
|
@cached_property
|
|
def complete_prefixv6(self):
|
|
"""Return the complete prefix v6 as cidr"""
|
|
return str(self.prefix_v6) + "/" + str(self.prefix_v6_length)
|
|
|
|
def ip_objects(self):
|
|
""" Renvoie tous les objets ipv4 relié à ce type"""
|
|
return IpList.objects.filter(ip_type=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)
|
|
|
|
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
|
|
ip_obj = [IpList(ip_type=self, ipv4=str(ip)) for ip in self.ip_range]
|
|
listes_ip = IpList.objects.filter(
|
|
ipv4__in=[str(ip) for ip in self.ip_range]
|
|
)
|
|
# Si il n'y a pas d'ip, on les crée
|
|
if not listes_ip:
|
|
IpList.objects.bulk_create(ip_obj)
|
|
# Sinon on update l'ip_type
|
|
else:
|
|
listes_ip.update(ip_type=self)
|
|
return
|
|
|
|
def del_ip_range(self):
|
|
""" Methode dépréciée, IpList est en mode cascade et supprimé
|
|
automatiquement"""
|
|
if Interface.objects.filter(ipv4__in=self.ip_objects()):
|
|
raise ValidationError(_("One or several IP addresses from the"
|
|
" range are affected, impossible to delete"
|
|
" the range."))
|
|
for ip in self.ip_objects():
|
|
ip.delete()
|
|
|
|
def check_replace_prefixv6(self):
|
|
"""Remplace les prefixv6 des interfaces liées à ce type d'ip"""
|
|
if not self.prefix_v6:
|
|
return
|
|
else:
|
|
for ipv6 in Ipv6List.objects.filter(
|
|
interface__in=Interface.objects.filter(
|
|
machine_type__in=MachineType.objects.filter(ip_type=self)
|
|
)
|
|
):
|
|
ipv6.check_and_replace_prefix(prefix=self.prefix_v6)
|
|
|
|
def get_associated_ptr_records(self):
|
|
from re2o.utils import all_active_assigned_interfaces
|
|
if self.reverse_v4:
|
|
return (all_active_assigned_interfaces()
|
|
.filter(machine_type__ip_type=self)
|
|
.filter(ipv4__isnull=False))
|
|
else:
|
|
return None
|
|
|
|
def get_associated_ptr_v6_records(self):
|
|
from re2o.utils import all_active_interfaces
|
|
if self.reverse_v6:
|
|
return (all_active_interfaces(full=True)
|
|
.filter(machine_type__ip_type=self))
|
|
else:
|
|
return None
|
|
|
|
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):
|
|
raise ValidationError(_("Range end must be after range start..."))
|
|
# On ne crée pas plus grand qu'un /16
|
|
if self.ip_range.size > 65536:
|
|
raise ValidationError(_("The range is too large, you can't create"
|
|
" a larger one than a /16."))
|
|
# On check que les / ne se recoupent pas
|
|
for element in IpType.objects.all().exclude(pk=self.pk):
|
|
if not self.ip_set.isdisjoint(element.ip_set):
|
|
raise ValidationError(_("The specified range is not disjoint"
|
|
" from existing ranges."))
|
|
# On formate le prefix v6
|
|
if self.prefix_v6:
|
|
self.prefix_v6 = str(IPNetwork(self.prefix_v6 + '/64').network)
|
|
# On vérifie qu'un domaine network/netmask contiens bien le domaine ip start-stop
|
|
if self.domaine_ip_network:
|
|
if not self.domaine_ip_start in self.ip_network or not self.domaine_ip_stop in self.ip_network:
|
|
raise ValidationError(_("If you specify a domain network or"
|
|
" netmask, it must contain the"
|
|
" domain's IP range."))
|
|
return
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.clean()
|
|
super(IpType, self).save(*args, **kwargs)
|
|
|
|
@staticmethod
|
|
def can_use_all(user_request, *_args, **_kwargs):
|
|
"""Superdroit qui permet d'utiliser toutes les extensions sans
|
|
restrictions
|
|
:param user_request: instance user qui fait l'edition
|
|
:return: True ou False avec la raison de l'échec le cas échéant"""
|
|
return user_request.has_perm('machines.use_all_iptype'), None
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Vlan(RevMixin, AclMixin, models.Model):
|
|
""" Un vlan : vlan_id et nom
|
|
On limite le vlan id entre 0 et 4096, comme défini par la norme"""
|
|
vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)])
|
|
name = models.CharField(max_length=256)
|
|
comment = models.CharField(max_length=256, blank=True)
|
|
#Réglages supplémentaires
|
|
arp_protect = models.BooleanField(default=False)
|
|
dhcp_snooping = models.BooleanField(default=False)
|
|
dhcpv6_snooping = models.BooleanField(default=False)
|
|
igmp = models.BooleanField(
|
|
default=False,
|
|
help_text=_("v4 multicast management")
|
|
)
|
|
mld = models.BooleanField(
|
|
default=False,
|
|
help_text=_("v6 multicast management")
|
|
)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_vlan", _("Can view a VLAN object")),
|
|
)
|
|
verbose_name = _("VLAN")
|
|
verbose_name_plural = _("VLANs")
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Nas(RevMixin, AclMixin, 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"""
|
|
default_mode = '802.1X'
|
|
AUTH = (
|
|
('802.1X', '802.1X'),
|
|
('Mac-address', _("MAC-address")),
|
|
)
|
|
|
|
name = models.CharField(max_length=255, unique=True)
|
|
nas_type = models.ForeignKey(
|
|
'MachineType',
|
|
on_delete=models.PROTECT,
|
|
related_name='nas_type'
|
|
)
|
|
machine_type = models.ForeignKey(
|
|
'MachineType',
|
|
on_delete=models.PROTECT,
|
|
related_name='machinetype_on_nas'
|
|
)
|
|
port_access_mode = models.CharField(
|
|
choices=AUTH,
|
|
default=default_mode,
|
|
max_length=32
|
|
)
|
|
autocapture_mac = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_nas", _("Can view a NAS device object")),
|
|
)
|
|
verbose_name = _("NAS device")
|
|
verbose_name_plural = _("NAS devices")
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class SOA(RevMixin, AclMixin, 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
|
|
"""
|
|
name = models.CharField(max_length=255)
|
|
mail = models.EmailField(
|
|
help_text=_("Contact email address for the zone")
|
|
)
|
|
refresh = models.PositiveIntegerField(
|
|
default=86400, # 24 hours
|
|
help_text=_("Seconds before the secondary DNS have to ask the primary"
|
|
" DNS serial to detect a modification")
|
|
)
|
|
retry = models.PositiveIntegerField(
|
|
default=7200, # 2 hours
|
|
help_text=_("Seconds before the secondary DNS ask the serial again in"
|
|
" case of a primary DNS timeout")
|
|
)
|
|
expire = models.PositiveIntegerField(
|
|
default=3600000, # 1000 hours
|
|
help_text=_("Seconds before the secondary DNS stop answering requests"
|
|
" in case of primary DNS timeout")
|
|
)
|
|
ttl = models.PositiveIntegerField(
|
|
default=172800, # 2 days
|
|
help_text=_("Time to Live")
|
|
)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_soa", _("Can view an SOA record object")),
|
|
)
|
|
verbose_name = _("SOA record")
|
|
verbose_name_plural = _("SOA records")
|
|
|
|
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="postmaster@example.com"
|
|
)[0].pk
|
|
|
|
|
|
class Extension(RevMixin, AclMixin, models.Model):
|
|
""" Extension dns type example.org. Précise si tout le monde peut
|
|
l'utiliser, associé à un origin (ip d'origine)"""
|
|
name = models.CharField(
|
|
max_length=255,
|
|
unique=True,
|
|
help_text=_("Zone name, must begin with a dot (.example.org)")
|
|
)
|
|
need_infra = models.BooleanField(default=False)
|
|
origin = models.ForeignKey(
|
|
'IpList',
|
|
on_delete=models.PROTECT,
|
|
blank=True,
|
|
null=True,
|
|
help_text=_("A record associated with the zone")
|
|
)
|
|
origin_v6 = models.GenericIPAddressField(
|
|
protocol='IPv6',
|
|
null=True,
|
|
blank=True,
|
|
help_text=_("AAAA record associated with the zone")
|
|
)
|
|
soa = models.ForeignKey(
|
|
'SOA',
|
|
on_delete=models.CASCADE
|
|
)
|
|
dnssec = models.BooleanField(
|
|
default=False,
|
|
help_text=_("Should the zone be signed with DNSSEC")
|
|
)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_extension", _("Can view an extension object")),
|
|
("use_all_extension", _("Can use all extensions")),
|
|
)
|
|
verbose_name = _("DNS extension")
|
|
verbose_name_plural = _("DNS extensions")
|
|
|
|
@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)
|
|
if self.origin_v6:
|
|
if entry:
|
|
entry += "\n"
|
|
entry += "@ IN AAAA " + str(self.origin_v6)
|
|
return entry
|
|
|
|
def get_associated_sshfp_records(self):
|
|
from re2o.utils import all_active_assigned_interfaces
|
|
return (all_active_assigned_interfaces()
|
|
.filter(machine_type__ip_type__extension=self)
|
|
.filter(machine__id__in=SshFp.objects.values('machine')))
|
|
|
|
def get_associated_a_records(self):
|
|
from re2o.utils import all_active_assigned_interfaces
|
|
return (all_active_assigned_interfaces()
|
|
.filter(machine_type__ip_type__extension=self)
|
|
.filter(ipv4__isnull=False))
|
|
|
|
def get_associated_aaaa_records(self):
|
|
from re2o.utils import all_active_interfaces
|
|
return (all_active_interfaces(full=True)
|
|
.filter(machine_type__ip_type__extension=self))
|
|
|
|
def get_associated_cname_records(self):
|
|
from re2o.utils import all_active_assigned_interfaces
|
|
return (Domain.objects
|
|
.filter(extension=self)
|
|
.filter(cname__interface_parent__in=all_active_assigned_interfaces())
|
|
.prefetch_related('cname'))
|
|
|
|
def get_associated_dname_records(self):
|
|
return (DName.objects.filter(alias=self))
|
|
|
|
@staticmethod
|
|
def can_use_all(user_request, *_args, **_kwargs):
|
|
"""Superdroit qui permet d'utiliser toutes les extensions sans
|
|
restrictions
|
|
:param user_request: instance user qui fait l'edition
|
|
:return: True ou False avec la raison de l'échec le cas échéant"""
|
|
return user_request.has_perm('machines.use_all_extension'), None
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def clean(self, *args, **kwargs):
|
|
if self.name and self.name[0] != '.':
|
|
raise ValidationError(_("An extension must begin with a dot."))
|
|
super(Extension, self).clean(*args, **kwargs)
|
|
|
|
|
|
class Mx(RevMixin, AclMixin, models.Model):
|
|
""" Entrées des MX. Enregistre la zone (extension) associée et la
|
|
priorité
|
|
Todo : pouvoir associer un MX à une interface """
|
|
zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
|
priority = models.PositiveIntegerField()
|
|
name = models.ForeignKey('Domain', on_delete=models.PROTECT)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_mx", _("Can view an MX record object")),
|
|
)
|
|
verbose_name = _("MX record")
|
|
verbose_name_plural = _("MX records")
|
|
|
|
@cached_property
|
|
def dns_entry(self):
|
|
"""Renvoie l'entrée DNS complète pour un MX à mettre dans les
|
|
fichiers de zones"""
|
|
return "@ IN MX {prior} {name}".format(
|
|
prior=str(self.priority).ljust(3),
|
|
name=str(self.name)
|
|
)
|
|
|
|
def __str__(self):
|
|
return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name)
|
|
|
|
|
|
class Ns(RevMixin, AclMixin, models.Model):
|
|
"""Liste des enregistrements name servers par zone considéérée"""
|
|
zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
|
ns = models.ForeignKey('Domain', on_delete=models.PROTECT)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_ns", _("Can view an NS record object")),
|
|
)
|
|
verbose_name = _("NS record")
|
|
verbose_name_plural = _("NS records")
|
|
|
|
@cached_property
|
|
def dns_entry(self):
|
|
"""Renvoie un enregistrement NS complet pour les filezones"""
|
|
return "@ IN NS " + str(self.ns)
|
|
|
|
def __str__(self):
|
|
return str(self.zone) + ' ' + str(self.ns)
|
|
|
|
|
|
class Txt(RevMixin, AclMixin, models.Model):
|
|
""" Un enregistrement TXT associé à une extension"""
|
|
zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
|
field1 = models.CharField(max_length=255)
|
|
field2 = models.TextField(max_length=2047)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_txt", _("Can view a TXT record object")),
|
|
)
|
|
verbose_name = _("TXT record")
|
|
verbose_name_plural = _("TXT records")
|
|
|
|
def __str__(self):
|
|
return str(self.zone) + " : " + str(self.field1) + " " + \
|
|
str(self.field2)
|
|
|
|
@cached_property
|
|
def dns_entry(self):
|
|
"""Renvoie l'enregistrement TXT complet pour le fichier de zone"""
|
|
return str(self.field1).ljust(15) + " IN TXT " + str(self.field2)
|
|
|
|
|
|
class DName(RevMixin, AclMixin, models.Model):
|
|
"""A DNAME entry for the DNS."""
|
|
zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
|
alias = models.CharField(max_length=255)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_dname", _("Can view a DNAME record object")),
|
|
)
|
|
verbose_name = _("DNAME record")
|
|
verbose_name_plural = _("DNAME records")
|
|
|
|
def __str__(self):
|
|
return str(self.zone) + " : " + str(self.alias)
|
|
|
|
@cached_property
|
|
def dns_entry(self):
|
|
"""Returns the DNAME record for the DNS zone file."""
|
|
return str(self.alias).ljust(15) + " IN DNAME " + str(self.zone)
|
|
|
|
|
|
class Srv(RevMixin, AclMixin, models.Model):
|
|
""" A SRV record """
|
|
TCP = 'TCP'
|
|
UDP = 'UDP'
|
|
|
|
service = models.CharField(max_length=31)
|
|
protocole = models.CharField(
|
|
max_length=3,
|
|
choices=(
|
|
(TCP, 'TCP'),
|
|
(UDP, 'UDP'),
|
|
),
|
|
default=TCP,
|
|
)
|
|
extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
|
ttl = models.PositiveIntegerField(
|
|
default=172800, # 2 days
|
|
help_text=_("Time to Live")
|
|
)
|
|
priority = models.PositiveIntegerField(
|
|
default=0,
|
|
validators=[MaxValueValidator(65535)],
|
|
help_text=_("Priority of the target server (positive integer value,"
|
|
" the lower it is, the more the server will be used if"
|
|
" available)")
|
|
)
|
|
weight = models.PositiveIntegerField(
|
|
default=0,
|
|
validators=[MaxValueValidator(65535)],
|
|
help_text=_("Relative weight for records with the same priority"
|
|
" (integer value between 0 and 65535)")
|
|
)
|
|
port = models.PositiveIntegerField(
|
|
validators=[MaxValueValidator(65535)],
|
|
help_text=_("TCP/UDP port")
|
|
)
|
|
target = models.ForeignKey(
|
|
'Domain',
|
|
on_delete=models.PROTECT,
|
|
help_text=_("Target server")
|
|
)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_srv", _("Can view an SRV record object")),
|
|
)
|
|
verbose_name = _("SRV record")
|
|
verbose_name_plural = _("SRV records")
|
|
|
|
def __str__(self):
|
|
return str(self.service) + ' ' + str(self.protocole) + ' ' + \
|
|
str(self.extension) + ' ' + str(self.priority) + \
|
|
' ' + str(self.weight) + str(self.port) + str(self.target)
|
|
|
|
@cached_property
|
|
def dns_entry(self):
|
|
"""Renvoie l'enregistrement SRV complet pour le fichier de zone"""
|
|
return str(self.service) + '._' + str(self.protocole).lower() + \
|
|
str(self.extension) + '. ' + str(self.ttl) + ' IN SRV ' + \
|
|
str(self.priority) + ' ' + str(self.weight) + ' ' + \
|
|
str(self.port) + ' ' + str(self.target) + '.'
|
|
|
|
|
|
class SshFp(RevMixin, AclMixin, models.Model):
|
|
"""A fingerprint of an SSH public key"""
|
|
|
|
ALGO = (
|
|
("ssh-rsa", "ssh-rsa"),
|
|
("ssh-ed25519", "ssh-ed25519"),
|
|
("ecdsa-sha2-nistp256", "ecdsa-sha2-nistp256"),
|
|
("ecdsa-sha2-nistp384", "ecdsa-sha2-nistp384"),
|
|
("ecdsa-sha2-nistp521", "ecdsa-sha2-nistp521"),
|
|
)
|
|
|
|
machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
|
|
pub_key_entry = models.TextField(
|
|
help_text=_("SSH public key"),
|
|
max_length=2048
|
|
)
|
|
algo = models.CharField(
|
|
choices=ALGO,
|
|
max_length=32
|
|
)
|
|
comment = models.CharField(
|
|
help_text=_("Comment"),
|
|
max_length=255,
|
|
null=True,
|
|
blank=True
|
|
)
|
|
|
|
@cached_property
|
|
def algo_id(self):
|
|
"""Return the id of the algorithm for this key"""
|
|
if "ecdsa" in self.algo:
|
|
return 3
|
|
elif "rsa" in self.algo:
|
|
return 1
|
|
else:
|
|
return 2
|
|
|
|
@cached_property
|
|
def hash(self):
|
|
"""Return the hashess for the pub key with correct id
|
|
cf RFC, 1 is sha1 , 2 sha256"""
|
|
return {
|
|
"1": hashlib.sha1(base64.b64decode(self.pub_key_entry)).hexdigest(),
|
|
"2": hashlib.sha256(base64.b64decode(self.pub_key_entry)).hexdigest(),
|
|
}
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_sshfp", _("Can view an SSHFP record object")),
|
|
)
|
|
verbose_name = _("SSHFP record")
|
|
verbose_name_plural = _("SSHFP records")
|
|
|
|
def can_view(self, user_request, *_args, **_kwargs):
|
|
return self.machine.can_view(user_request, *_args, **_kwargs)
|
|
|
|
def can_edit(self, user_request, *args, **kwargs):
|
|
return self.machine.can_edit(user_request, *args, **kwargs)
|
|
|
|
def can_delete(self, user_request, *args, **kwargs):
|
|
return self.machine.can_delete(user_request, *args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return str(self.algo) + ' ' + str(self.comment)
|
|
|
|
|
|
class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, 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"""
|
|
ipv4 = models.OneToOneField(
|
|
'IpList',
|
|
on_delete=models.PROTECT,
|
|
blank=True,
|
|
null=True
|
|
)
|
|
mac_address = MACAddressField(integer=False)
|
|
machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
|
|
machine_type = models.ForeignKey('MachineType', on_delete=models.PROTECT)
|
|
details = models.CharField(max_length=255, blank=True)
|
|
port_lists = models.ManyToManyField('OuverturePortList', blank=True)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_interface", _("Can view an interface object")),
|
|
("change_interface_machine",
|
|
_("Can change the owner of an interface")),
|
|
)
|
|
verbose_name = _("interface")
|
|
verbose_name_plural = _("interfaces")
|
|
|
|
@cached_property
|
|
def is_active(self):
|
|
""" Renvoie si une interface doit avoir accès ou non """
|
|
machine = self.machine
|
|
user = self.machine.user
|
|
return machine.active and user.has_access()
|
|
|
|
@cached_property
|
|
def ipv6_slaac(self):
|
|
""" Renvoie un objet type ipv6 à partir du prefix associé à
|
|
l'iptype parent"""
|
|
if self.machine_type.ip_type.prefix_v6:
|
|
return EUI(self.mac_address).ipv6(
|
|
IPNetwork(self.machine_type.ip_type.prefix_v6).network
|
|
)
|
|
else:
|
|
return None
|
|
|
|
@cached_property
|
|
def gen_ipv6_dhcpv6(self):
|
|
"""Cree une ip, à assigner avec dhcpv6 sur une machine"""
|
|
prefix_v6 = self.machine_type.ip_type.prefix_v6.encode().decode('utf-8')
|
|
if not prefix_v6:
|
|
return None
|
|
return IPv6Address(
|
|
IPv6Address(prefix_v6).exploded[:20] +
|
|
IPv6Address(self.id).exploded[20:]
|
|
)
|
|
@cached_property
|
|
def get_vendor(self):
|
|
"""Retourne le vendeur associé à la mac de l'interface"""
|
|
mac = EUI(self.mac_address)
|
|
try:
|
|
oui = mac.oui
|
|
vendor = oui.registration().org
|
|
except NotRegisteredError:
|
|
vendor = "Unknown vendor"
|
|
return(vendor)
|
|
|
|
def sync_ipv6_dhcpv6(self):
|
|
"""Affecte une ipv6 dhcpv6 calculée à partir de l'id de la machine"""
|
|
ipv6_dhcpv6 = self.gen_ipv6_dhcpv6
|
|
if not ipv6_dhcpv6:
|
|
return
|
|
ipv6 = Ipv6List.objects.filter(ipv6=str(ipv6_dhcpv6)).first()
|
|
if not ipv6:
|
|
ipv6 = Ipv6List(ipv6=str(ipv6_dhcpv6))
|
|
ipv6.interface = self
|
|
ipv6.save()
|
|
return
|
|
|
|
def sync_ipv6_slaac(self):
|
|
"""Cree, mets à jour et supprime si il y a lieu l'ipv6 slaac associée
|
|
à la machine
|
|
Sans prefixe ipv6, on return
|
|
Si l'ip slaac n'est pas celle qu'elle devrait être, on maj"""
|
|
ipv6_slaac = self.ipv6_slaac
|
|
if not ipv6_slaac:
|
|
return
|
|
ipv6_object = (Ipv6List.objects
|
|
.filter(interface=self, slaac_ip=True)
|
|
.first())
|
|
if not ipv6_object:
|
|
ipv6_object = Ipv6List(interface=self, slaac_ip=True)
|
|
if ipv6_object.ipv6 != str(ipv6_slaac):
|
|
ipv6_object.ipv6 = str(ipv6_slaac)
|
|
ipv6_object.save()
|
|
|
|
def sync_ipv6(self):
|
|
"""Cree et met à jour l'ensemble des ipv6 en fonction du mode choisi"""
|
|
if (preferences.models.OptionalMachine
|
|
.get_cached_value('ipv6_mode') == 'SLAAC'):
|
|
self.sync_ipv6_slaac()
|
|
elif (preferences.models.OptionalMachine
|
|
.get_cached_value('ipv6_mode') == 'DHCPV6'):
|
|
self.sync_ipv6_dhcpv6()
|
|
else:
|
|
return
|
|
|
|
def ipv6(self):
|
|
""" Renvoie le queryset de la liste des ipv6
|
|
On renvoie l'ipv6 slaac que si le mode slaac est activé
|
|
(et non dhcpv6)"""
|
|
if (preferences.models.OptionalMachine
|
|
.get_cached_value('ipv6_mode') == 'SLAAC'):
|
|
return self.ipv6list.all()
|
|
elif (preferences.models.OptionalMachine
|
|
.get_cached_value('ipv6_mode') == 'DHCPV6'):
|
|
return self.ipv6list.filter(slaac_ip=False)
|
|
else:
|
|
return []
|
|
|
|
def mac_bare(self):
|
|
""" Formatage de la mac type mac_bare"""
|
|
return str(EUI(self.mac_address, dialect=mac_bare)).lower()
|
|
|
|
def filter_macaddress(self):
|
|
""" Tente un formatage mac_bare, si échoue, lève une erreur de
|
|
validation"""
|
|
try:
|
|
self.mac_address = str(EUI(self.mac_address, dialect=default_dialect()))
|
|
except:
|
|
raise ValidationError(_("The given MAC address is invalid."))
|
|
|
|
def assign_ipv4(self):
|
|
""" Assigne une ip à l'interface """
|
|
free_ips = self.machine_type.ip_type.free_ip()
|
|
if free_ips:
|
|
self.ipv4 = free_ips[0]
|
|
else:
|
|
raise ValidationError(_("There is no IP address available in the"
|
|
" slash."))
|
|
return
|
|
|
|
def unassign_ipv4(self):
|
|
""" Sans commentaire, désassigne une ipv4"""
|
|
self.ipv4 = None
|
|
|
|
@classmethod
|
|
def mass_unassign_ipv4(cls, interface_list):
|
|
"""Unassign ipv4 to multiple interfaces"""
|
|
with transaction.atomic(), reversion.create_revision():
|
|
interface_list.update(ipv4=None)
|
|
reversion.set_comment(_("IPv4 unassigning"))
|
|
|
|
@classmethod
|
|
def mass_assign_ipv4(cls, interface_list):
|
|
for interface in interface_list:
|
|
with transaction.atomic(), reversion.create_revision():
|
|
interface.assign_ipv4()
|
|
interface.save()
|
|
reversion.set_comment(_("IPv4 assigning"))
|
|
|
|
def update_type(self):
|
|
""" Lorsque le machinetype est changé de type d'ip, on réassigne"""
|
|
self.clean()
|
|
self.save()
|
|
|
|
def has_private_ip(self):
|
|
""" True si l'ip associée est privée"""
|
|
if self.ipv4:
|
|
return IPAddress(str(self.ipv4)).is_private()
|
|
else:
|
|
return False
|
|
|
|
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()
|
|
|
|
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"""
|
|
# If type was an invalid value, django won't create an attribute type
|
|
# but try clean() as we may be able to create it from another value
|
|
# so even if the error as yet been detected at this point, django
|
|
# continues because the error might not prevent us from creating the
|
|
# instance.
|
|
# But in our case, it's impossible to create a type value so we raise
|
|
# the error.
|
|
if not hasattr(self, 'machine_type'):
|
|
raise ValidationError(_("The selected IP type is invalid."))
|
|
self.filter_macaddress()
|
|
if not self.ipv4 or self.machine_type.ip_type != self.ipv4.ip_type:
|
|
self.assign_ipv4()
|
|
super(Interface, self).clean(*args, **kwargs)
|
|
|
|
def validate_unique(self, *args, **kwargs):
|
|
super(Interface, self).validate_unique(*args, **kwargs)
|
|
interfaces_similar = Interface.objects.filter(mac_address=self.mac_address, machine_type__ip_type=self.machine_type.ip_type)
|
|
if interfaces_similar and interfaces_similar.first() != self:
|
|
raise ValidationError(_("Mac address already registered in this Machine Type/Subnet"))
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.filter_macaddress()
|
|
# On verifie la cohérence en forçant l'extension par la méthode
|
|
if self.ipv4:
|
|
if self.machine_type.ip_type != self.ipv4.ip_type:
|
|
raise ValidationError(_("The IPv4 address and the machine type"
|
|
" don't match."))
|
|
self.validate_unique()
|
|
super(Interface, self).save(*args, **kwargs)
|
|
|
|
@staticmethod
|
|
def can_create(user_request, machineid, *_args, **_kwargs):
|
|
"""Verifie que l'user a les bons droits infra pour créer
|
|
une interface, ou bien que la machine appartient bien à l'user
|
|
:param macineid: Id de la machine parente de l'interface
|
|
:param user_request: instance utilisateur qui fait la requête
|
|
:return: soit True, soit False avec la raison de l'échec"""
|
|
try:
|
|
machine = Machine.objects.get(pk=machineid)
|
|
except Machine.DoesNotExist:
|
|
return False, _("Nonexistent machine.")
|
|
if not user_request.has_perm('machines.add_interface'):
|
|
if not (preferences.models.OptionalMachine
|
|
.get_cached_value('create_machine')):
|
|
return False, _("You can't add a machine.")
|
|
max_lambdauser_interfaces = (preferences.models.OptionalMachine
|
|
.get_cached_value(
|
|
'max_lambdauser_interfaces'
|
|
))
|
|
if machine.user != user_request:
|
|
return False, _("You don't have the right to add an interface"
|
|
" to a machine of another user.")
|
|
if (machine.user.user_interfaces().count() >=
|
|
max_lambdauser_interfaces):
|
|
return False, (_("You reached the maximum number of interfaces"
|
|
" that you are allowed to create yourself"
|
|
" (%s)." % max_lambdauser_interfaces))
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def can_change_machine(user_request, *_args, **_kwargs):
|
|
"""Check if a user can change the machine associated with an
|
|
Interface object """
|
|
return (user_request.has_perm('machines.change_interface_machine'),
|
|
_("Permission required to edit the machine."))
|
|
|
|
def can_edit(self, user_request, *args, **kwargs):
|
|
"""Verifie que l'user a les bons droits infra pour editer
|
|
cette instance interface, ou qu'elle lui appartient
|
|
:param self: Instance interface à editer
|
|
:param user_request: Utilisateur qui fait la requête
|
|
:return: soit True, soit False avec la raison de l'échec"""
|
|
if self.machine.user != user_request:
|
|
if (not user_request.has_perm('machines.change_interface') or
|
|
not self.machine.user.can_edit(
|
|
user_request,
|
|
*args,
|
|
**kwargs
|
|
)[0]):
|
|
return False, _("You don't have the right to edit a machine of"
|
|
" another user.")
|
|
return True, None
|
|
|
|
def can_delete(self, user_request, *args, **kwargs):
|
|
"""Verifie que l'user a les bons droits delete object pour del
|
|
cette instance interface, ou qu'elle lui appartient
|
|
:param self: Instance interface à del
|
|
:param user_request: Utilisateur qui fait la requête
|
|
:return: soit True, soit False avec la raison de l'échec"""
|
|
if self.machine.user != user_request:
|
|
if (not user_request.has_perm('machines.change_interface') or
|
|
not self.machine.user.can_edit(
|
|
user_request,
|
|
*args,
|
|
**kwargs
|
|
)[0]):
|
|
return False, _("You don't have the right to edit a machine of"
|
|
" another user.")
|
|
return True, None
|
|
|
|
def can_view(self, user_request, *_args, **_kwargs):
|
|
"""Vérifie qu'on peut bien voir cette instance particulière avec
|
|
droit view objet ou qu'elle appartient à l'user
|
|
:param self: instance interface à voir
|
|
:param user_request: instance user qui fait l'edition
|
|
:return: True ou False avec la raison de l'échec le cas échéant"""
|
|
if (not user_request.has_perm('machines.view_interface') and
|
|
self.machine.user != user_request):
|
|
return False, _("You don't have the right to view machines other"
|
|
" than yours.")
|
|
return True, None
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Interface, self).__init__(*args, **kwargs)
|
|
self.field_permissions = {
|
|
'machine': self.can_change_machine,
|
|
}
|
|
|
|
def __str__(self):
|
|
try:
|
|
domain = self.domain
|
|
except:
|
|
domain = None
|
|
return str(domain)
|
|
|
|
|
|
class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
|
|
""" A list of IPv6 """
|
|
|
|
ipv6 = models.GenericIPAddressField(
|
|
protocol='IPv6',
|
|
)
|
|
interface = models.ForeignKey(
|
|
'Interface',
|
|
on_delete=models.CASCADE,
|
|
related_name='ipv6list'
|
|
)
|
|
slaac_ip = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_ipv6list", _("Can view an IPv6 addresses list object")),
|
|
("change_ipv6list_slaac_ip", _("Can change the SLAAC value of an"
|
|
" IPv6 addresses list")),
|
|
)
|
|
verbose_name = _("IPv6 addresses list")
|
|
verbose_name_plural = _("IPv6 addresses lists")
|
|
|
|
@staticmethod
|
|
def can_create(user_request, interfaceid, *_args, **_kwargs):
|
|
"""Verifie que l'user a les bons droits infra pour créer
|
|
une ipv6, ou possède l'interface associée
|
|
:param interfaceid: Id de l'interface associée à cet objet domain
|
|
:param user_request: instance utilisateur qui fait la requête
|
|
:return: soit True, soit False avec la raison de l'échec"""
|
|
try:
|
|
interface = Interface.objects.get(pk=interfaceid)
|
|
except Interface.DoesNotExist:
|
|
return False, _("Nonexistent interface.")
|
|
if not user_request.has_perm('machines.add_ipv6list'):
|
|
if interface.machine.user != user_request:
|
|
return False, _("You don't have the right to add an alias to a"
|
|
" machine of another user.")
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def can_change_slaac_ip(user_request, *_args, **_kwargs):
|
|
""" Check if a user can change the slaac value """
|
|
return (user_request.has_perm('machines.change_ipv6list_slaac_ip'),
|
|
_("Permission required to change the SLAAC value of an IPv6"
|
|
" address"))
|
|
|
|
def can_edit(self, user_request, *args, **kwargs):
|
|
"""Verifie que l'user a les bons droits infra pour editer
|
|
cette instance interface, ou qu'elle lui appartient
|
|
:param self: Instance interface à editer
|
|
:param user_request: Utilisateur qui fait la requête
|
|
:return: soit True, soit False avec la raison de l'échec"""
|
|
if self.interface.machine.user != user_request:
|
|
if (not user_request.has_perm('machines.change_ipv6list') or
|
|
not self.interface.machine.user.can_edit(
|
|
user_request,
|
|
*args,
|
|
**kwargs
|
|
)[0]):
|
|
return False, _("You don't have the right to edit a machine of"
|
|
" another user.")
|
|
return True, None
|
|
|
|
def can_delete(self, user_request, *args, **kwargs):
|
|
"""Verifie que l'user a les bons droits delete object pour del
|
|
cette instance interface, ou qu'elle lui appartient
|
|
:param self: Instance interface à del
|
|
:param user_request: Utilisateur qui fait la requête
|
|
:return: soit True, soit False avec la raison de l'échec"""
|
|
if self.interface.machine.user != user_request:
|
|
if (not user_request.has_perm('machines.change_ipv6list') or
|
|
not self.interface.machine.user.can_edit(
|
|
user_request,
|
|
*args,
|
|
**kwargs
|
|
)[0]):
|
|
return False, _("You don't have the right to edit a machine of"
|
|
" another user.")
|
|
return True, None
|
|
|
|
def can_view(self, user_request, *_args, **_kwargs):
|
|
"""Vérifie qu'on peut bien voir cette instance particulière avec
|
|
droit view objet ou qu'elle appartient à l'user
|
|
:param self: instance interface à voir
|
|
:param user_request: instance user qui fait l'edition
|
|
:return: True ou False avec la raison de l'échec le cas échéant"""
|
|
if (not user_request.has_perm('machines.view_ipv6list') and
|
|
self.interface.machine.user != user_request):
|
|
return False, _("You don't have the right to view machines other"
|
|
" than yours.")
|
|
return True, None
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Ipv6List, self).__init__(*args, **kwargs)
|
|
self.field_permissions = {
|
|
'slaac_ip': self.can_change_slaac_ip,
|
|
}
|
|
|
|
def check_and_replace_prefix(self, prefix=None):
|
|
"""Si le prefixe v6 est incorrect, on maj l'ipv6"""
|
|
prefix_v6 = prefix or self.interface.machine_type.ip_type.prefix_v6.encode().decode('utf-8')
|
|
if not prefix_v6:
|
|
return
|
|
if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] !=
|
|
IPv6Address(prefix_v6).exploded[:20]):
|
|
self.ipv6 = IPv6Address(
|
|
IPv6Address(prefix_v6).exploded[:20] +
|
|
IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[20:]
|
|
)
|
|
self.save()
|
|
|
|
def clean(self, *args, **kwargs):
|
|
if self.slaac_ip and (Ipv6List.objects
|
|
.filter(interface=self.interface, slaac_ip=True)
|
|
.exclude(id=self.id)):
|
|
raise ValidationError(_("A SLAAC IP address is already registered."))
|
|
try:
|
|
prefix_v6 = self.interface.machine_type.ip_type.prefix_v6.encode().decode('utf-8')
|
|
except AttributeError: # Prevents from crashing when there is no defined prefix_v6
|
|
prefix_v6 = None
|
|
if prefix_v6:
|
|
if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] !=
|
|
IPv6Address(prefix_v6).exploded[:20]):
|
|
raise ValidationError(_("The v6 prefix is incorrect and"
|
|
" doesn't match the type associated"
|
|
" with the machine."))
|
|
super(Ipv6List, self).clean(*args, **kwargs)
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""Force à avoir appellé clean avant"""
|
|
self.full_clean()
|
|
super(Ipv6List, self).save(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return str(self.ipv6)
|
|
|
|
|
|
class Domain(RevMixin, AclMixin, 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"""
|
|
|
|
interface_parent = models.OneToOneField(
|
|
'Interface',
|
|
on_delete=models.CASCADE,
|
|
blank=True,
|
|
null=True
|
|
)
|
|
name = models.CharField(
|
|
help_text=_("Mandatory and unique, must not contain dots."),
|
|
max_length=255
|
|
)
|
|
extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
|
cname = models.ForeignKey(
|
|
'self',
|
|
null=True,
|
|
blank=True,
|
|
related_name='related_domain'
|
|
)
|
|
|
|
class Meta:
|
|
unique_together = (("name", "extension"),)
|
|
permissions = (
|
|
("view_domain", _("Can view a domain object")),
|
|
)
|
|
verbose_name = _("domain")
|
|
verbose_name_plural = _("domains")
|
|
|
|
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:
|
|
return self.interface_parent.machine_type.ip_type.extension
|
|
elif hasattr(self, 'extension'):
|
|
return self.extension
|
|
else:
|
|
return None
|
|
|
|
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():
|
|
self.extension = self.get_extension()
|
|
if self.interface_parent and self.cname:
|
|
raise ValidationError(_("You can't create a both A and CNAME"
|
|
" record."))
|
|
if self.cname == self:
|
|
raise ValidationError(_("You can't create a CNAME record pointing"
|
|
" to itself."))
|
|
HOSTNAME_LABEL_PATTERN = re.compile(
|
|
r"(?!-)[A-Z\d-]+(?<!-)$",
|
|
re.IGNORECASE
|
|
)
|
|
dns = self.name.lower()
|
|
if len(dns) > 63:
|
|
raise ValidationError(_("The domain name %s is too long (over 63"
|
|
" characters).") % dns)
|
|
if not HOSTNAME_LABEL_PATTERN.match(dns):
|
|
raise ValidationError(_("The domain name %s contains forbidden"
|
|
" characters.") % dns)
|
|
self.validate_unique()
|
|
super(Domain, self).clean()
|
|
|
|
@cached_property
|
|
def dns_entry(self):
|
|
""" Une entrée DNS"""
|
|
if self.cname:
|
|
return "{name} IN CNAME {cname}.".format(
|
|
name=str(self.name).ljust(15),
|
|
cname=str(self.cname)
|
|
)
|
|
|
|
def save(self, *args, **kwargs):
|
|
""" Empèche le save sans extension valide.
|
|
Force à avoir appellé clean avant"""
|
|
if not self.get_extension():
|
|
raise ValidationError(_("Invalid extension."))
|
|
self.full_clean()
|
|
super(Domain, self).save(*args, **kwargs)
|
|
|
|
@cached_property
|
|
def get_source_interface(self):
|
|
"""Renvoie l'interface source :
|
|
- l'interface reliée si c'est un A
|
|
- si c'est un cname, suit le cname jusqu'à atteindre le A
|
|
et renvoie l'interface parente
|
|
Fonction récursive"""
|
|
if self.interface_parent:
|
|
return self.interface_parent
|
|
else:
|
|
return self.cname.get_parent_interface()
|
|
|
|
@staticmethod
|
|
def can_create(user_request, interfaceid, *_args, **_kwargs):
|
|
"""Verifie que l'user a les bons droits infra pour créer
|
|
un domain, ou possède l'interface associée
|
|
:param interfaceid: Id de l'interface associée à cet objet domain
|
|
:param user_request: instance utilisateur qui fait la requête
|
|
:return: soit True, soit False avec la raison de l'échec"""
|
|
try:
|
|
interface = Interface.objects.get(pk=interfaceid)
|
|
except Interface.DoesNotExist:
|
|
return False, _("Nonexistent interface.")
|
|
if not user_request.has_perm('machines.add_domain'):
|
|
max_lambdauser_aliases = (preferences.models.OptionalMachine
|
|
.get_cached_value(
|
|
'max_lambdauser_aliases'
|
|
))
|
|
if interface.machine.user != user_request:
|
|
return False, _("You don't have the right to add an alias to a"
|
|
" machine of another user.")
|
|
if Domain.objects.filter(
|
|
cname__in=Domain.objects.filter(
|
|
interface_parent__in=(interface.machine.user
|
|
.user_interfaces())
|
|
)
|
|
).count() >= max_lambdauser_aliases:
|
|
return False, _("You reached the maximum number of alias that"
|
|
" you are allowed to create yourself (%s). "
|
|
% max_lambdauser_aliases)
|
|
return True, None
|
|
|
|
def can_edit(self, user_request, *_args, **_kwargs):
|
|
"""Verifie que l'user a les bons droits pour editer
|
|
cette instance domain
|
|
:param self: Instance domain à editer
|
|
:param user_request: Utilisateur qui fait la requête
|
|
:return: soit True, soit False avec la raison de l'échec"""
|
|
if (not user_request.has_perm('machines.change_domain') and
|
|
self.get_source_interface.machine.user != user_request):
|
|
return False, _("You don't have the right to edit an alias of a"
|
|
" machine of another user.")
|
|
return True, None
|
|
|
|
def can_delete(self, user_request, *_args, **_kwargs):
|
|
"""Verifie que l'user a les bons droits delete object pour del
|
|
cette instance domain, ou qu'elle lui appartient
|
|
:param self: Instance domain à del
|
|
:param user_request: Utilisateur qui fait la requête
|
|
:return: soit True, soit False avec la raison de l'échec"""
|
|
if (not user_request.has_perm('machines.delete_domain') and
|
|
self.get_source_interface.machine.user != user_request):
|
|
return False, _("You don't have the right to delete an alias of a"
|
|
" machine of another user.")
|
|
return True, None
|
|
|
|
def can_view(self, user_request, *_args, **_kwargs):
|
|
"""Vérifie qu'on peut bien voir cette instance particulière avec
|
|
droit view objet ou qu'elle appartient à l'user
|
|
:param self: instance domain à voir
|
|
:param user_request: instance user qui fait l'edition
|
|
:return: True ou False avec la raison de l'échec le cas échéant"""
|
|
if (not user_request.has_perm('machines.view_domain') and
|
|
self.get_source_interface.machine.user != user_request):
|
|
return False, _("You don't have the right to view machines other"
|
|
" than yours.")
|
|
return True, None
|
|
|
|
def __str__(self):
|
|
return str(self.name) + str(self.extension)
|
|
|
|
|
|
class IpList(RevMixin, AclMixin, models.Model):
|
|
""" A list of IPv4 """
|
|
|
|
ipv4 = models.GenericIPAddressField(protocol='IPv4', unique=True)
|
|
ip_type = models.ForeignKey('IpType', on_delete=models.CASCADE)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_iplist", _("Can view an IPv4 addresses list object")),
|
|
)
|
|
verbose_name = _("IPv4 addresses list")
|
|
verbose_name_plural = _("IPv4 addresses lists")
|
|
|
|
@cached_property
|
|
def need_infra(self):
|
|
""" Permet de savoir si un user basique peut assigner cette ip ou
|
|
non"""
|
|
return self.ip_type.need_infra
|
|
|
|
def clean(self):
|
|
""" Erreur si l'ip_type est incorrect"""
|
|
if not str(self.ipv4) in self.ip_type.ip_set_as_str:
|
|
raise ValidationError(_("The IPv4 address and the range of the IP"
|
|
" type don't match."))
|
|
return
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.clean()
|
|
super(IpList, self).save(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return self.ipv4
|
|
|
|
|
|
class Role(RevMixin, AclMixin, models.Model):
|
|
"""Define the role of a machine.
|
|
Allow automated generation of the server configuration.
|
|
"""
|
|
|
|
ROLE = (
|
|
('dhcp-server', _("DHCP server")),
|
|
('switch-conf-server', _("Switches configuration server")),
|
|
('dns-recursive-server', _("Recursive DNS server")),
|
|
('ntp-server', _("NTP server")),
|
|
('radius-server', _("RADIUS server")),
|
|
('log-server', _("Log server")),
|
|
('ldap-master-server', _("LDAP master server")),
|
|
('ldap-backup-server', _("LDAP backup server")),
|
|
('smtp-server', _("SMTP server")),
|
|
('postgresql-server', _("postgreSQL server")),
|
|
('mysql-server', _("mySQL server")),
|
|
('sql-client', _("SQL client")),
|
|
('gateway', _("Gateway")),
|
|
)
|
|
|
|
role_type = models.CharField(max_length=255, unique=True)
|
|
servers = models.ManyToManyField('Interface')
|
|
specific_role = models.CharField(
|
|
choices=ROLE,
|
|
null=True,
|
|
blank=True,
|
|
max_length=32,
|
|
)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_role", _("Can view a role object")),
|
|
)
|
|
verbose_name = _("server role")
|
|
verbose_name_plural = _("server roles")
|
|
|
|
@classmethod
|
|
def interface_for_roletype(cls, roletype):
|
|
"""Return interfaces for a roletype"""
|
|
return Interface.objects.filter(
|
|
role=cls.objects.filter(specific_role=roletype)
|
|
)
|
|
|
|
@classmethod
|
|
def all_interfaces_for_roletype(cls, roletype):
|
|
"""Return all interfaces for a roletype"""
|
|
return Interface.objects.filter(
|
|
machine__interface__role=cls.objects.filter(specific_role=roletype)
|
|
)
|
|
|
|
@classmethod
|
|
def interface_for_roletype(cls, roletype):
|
|
"""Return interfaces for a roletype"""
|
|
return Interface.objects.filter(role=cls.objects.filter(specific_role=roletype))
|
|
|
|
def save(self, *args, **kwargs):
|
|
super(Role, self).save(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return str(self.role_type)
|
|
|
|
|
|
class Service(RevMixin, AclMixin, models.Model):
|
|
""" Definition d'un service (dhcp, dns, etc)"""
|
|
|
|
service_type = models.CharField(max_length=255, blank=True, unique=True)
|
|
min_time_regen = models.DurationField(
|
|
default=timedelta(minutes=1),
|
|
help_text=_("Minimal time before regeneration of the service.")
|
|
)
|
|
regular_time_regen = models.DurationField(
|
|
default=timedelta(hours=1),
|
|
help_text=_("Maximal time before regeneration of the service.")
|
|
)
|
|
servers = models.ManyToManyField('Interface', through='Service_link')
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_service", _("Can view a service object")),
|
|
)
|
|
verbose_name = _("service to generate (DHCP, DNS, ...)")
|
|
verbose_name_plural = _("services to generate (DHCP, DNS, ...)")
|
|
|
|
def ask_regen(self):
|
|
""" Marque à True la demande de régénération pour un service x """
|
|
Service_link.objects.filter(service=self).exclude(asked_regen=True) \
|
|
.update(asked_regen=True)
|
|
return
|
|
|
|
def process_link(self, servers):
|
|
""" Django ne peut créer lui meme les relations manytomany avec table
|
|
intermediaire explicite"""
|
|
for serv in servers.exclude(
|
|
pk__in=Interface.objects.filter(service=self)
|
|
):
|
|
link = Service_link(service=self, server=serv)
|
|
link.save()
|
|
Service_link.objects.filter(service=self).exclude(server__in=servers) \
|
|
.delete()
|
|
return
|
|
|
|
def save(self, *args, **kwargs):
|
|
super(Service, self).save(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return str(self.service_type)
|
|
|
|
|
|
def regen(service):
|
|
""" Fonction externe pour régérération d'un service, prend un objet service
|
|
en arg"""
|
|
obj = Service.objects.filter(service_type=service)
|
|
if obj:
|
|
obj[0].ask_regen()
|
|
return
|
|
|
|
|
|
class Service_link(RevMixin, AclMixin, models.Model):
|
|
""" Definition du lien entre serveurs et services"""
|
|
|
|
service = models.ForeignKey('Service', on_delete=models.CASCADE)
|
|
server = models.ForeignKey('Interface', on_delete=models.CASCADE)
|
|
last_regen = models.DateTimeField(auto_now_add=True)
|
|
asked_regen = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_service_link", _("Can view a service server link object")),
|
|
)
|
|
verbose_name = _("link between service and server")
|
|
verbose_name_plural = _("links between service and server")
|
|
|
|
def done_regen(self):
|
|
""" Appellé lorsqu'un serveur a regénéré son service"""
|
|
self.last_regen = timezone.now()
|
|
self.asked_regen = False
|
|
self.save()
|
|
|
|
@property
|
|
def need_regen(self):
|
|
""" Décide si le temps minimal écoulé est suffisant pour provoquer une
|
|
régénération de service"""
|
|
return bool(
|
|
(self.asked_regen and (
|
|
self.last_regen + self.service.min_time_regen
|
|
) < timezone.now()
|
|
) or (
|
|
self.last_regen + self.service.regular_time_regen
|
|
) < timezone.now()
|
|
)
|
|
|
|
@need_regen.setter
|
|
def need_regen(self, value):
|
|
"""
|
|
Force to set the need_regen value. True means a regen is asked and False
|
|
means a regen has been done.
|
|
|
|
:param value: (bool) The value to set to
|
|
"""
|
|
if not value:
|
|
self.last_regen = timezone.now()
|
|
self.asked_regen = value
|
|
self.save()
|
|
|
|
def __str__(self):
|
|
return str(self.server) + " " + str(self.service)
|
|
|
|
|
|
class OuverturePortList(RevMixin, AclMixin, models.Model):
|
|
"""Liste des ports ouverts sur une interface."""
|
|
|
|
name = models.CharField(
|
|
help_text=_("Name of the ports configuration"),
|
|
max_length=255
|
|
)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("view_ouvertureportlist", _("Can view a ports opening list"
|
|
" object")),
|
|
)
|
|
verbose_name = _("ports opening list")
|
|
verbose_name_plural = _("ports opening lists")
|
|
|
|
def can_delete(self, user_request, *_args, **_kwargs):
|
|
"""Verifie que l'user a les bons droits bureau pour delete
|
|
cette instance ouvertureportlist
|
|
:param self: Instance ouvertureportlist à delete
|
|
:param user_request: Utilisateur qui fait la requête
|
|
:return: soit True, soit False avec la raison de l'échec"""
|
|
if not user_request.has_perm('machines.delete_ouvertureportlist'):
|
|
return False, _("You don't have the right to delete a ports"
|
|
" opening list.")
|
|
if self.interface_set.all():
|
|
return False, _("This ports opening list is used.")
|
|
return True, None
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def tcp_ports_in(self):
|
|
"""Renvoie la liste des ports ouverts en TCP IN pour ce profil"""
|
|
return self.ouvertureport_set.filter(
|
|
protocole=OuverturePort.TCP,
|
|
io=OuverturePort.IN
|
|
)
|
|
|
|
def udp_ports_in(self):
|
|
"""Renvoie la liste des ports ouverts en UDP IN pour ce profil"""
|
|
return self.ouvertureport_set.filter(
|
|
protocole=OuverturePort.UDP,
|
|
io=OuverturePort.IN
|
|
)
|
|
|
|
def tcp_ports_out(self):
|
|
"""Renvoie la liste des ports ouverts en TCP OUT pour ce profil"""
|
|
return self.ouvertureport_set.filter(
|
|
protocole=OuverturePort.TCP,
|
|
io=OuverturePort.OUT
|
|
)
|
|
|
|
def udp_ports_out(self):
|
|
"""Renvoie la liste des ports ouverts en UDP OUT pour ce profil"""
|
|
return self.ouvertureport_set.filter(
|
|
protocole=OuverturePort.UDP,
|
|
io=OuverturePort.OUT
|
|
)
|
|
|
|
|
|
class OuverturePort(RevMixin, AclMixin, models.Model):
|
|
"""
|
|
Représente un simple port ou une plage de ports.
|
|
|
|
Les ports de la plage sont compris entre begin et en inclus.
|
|
Si begin == end alors on ne représente qu'un seul port.
|
|
|
|
On limite les ports entre 0 et 65535, tels que défini par la RFC
|
|
"""
|
|
|
|
TCP = 'T'
|
|
UDP = 'U'
|
|
IN = 'I'
|
|
OUT = 'O'
|
|
begin = models.PositiveIntegerField(validators=[MaxValueValidator(65535)])
|
|
end = models.PositiveIntegerField(validators=[MaxValueValidator(65535)])
|
|
port_list = models.ForeignKey(
|
|
'OuverturePortList',
|
|
on_delete=models.CASCADE
|
|
)
|
|
protocole = models.CharField(
|
|
max_length=1,
|
|
choices=(
|
|
(TCP, 'TCP'),
|
|
(UDP, 'UDP'),
|
|
),
|
|
default=TCP,
|
|
)
|
|
io = models.CharField(
|
|
max_length=1,
|
|
choices=(
|
|
(IN, 'IN'),
|
|
(OUT, 'OUT'),
|
|
),
|
|
default=OUT,
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("ports opening")
|
|
verbose_name_plural = _("ports openings")
|
|
|
|
def __str__(self):
|
|
if self.begin == self.end:
|
|
return str(self.begin)
|
|
return ':'.join([str(self.begin), str(self.end)])
|
|
|
|
def show_port(self):
|
|
"""Formatage plus joli, alias pour str"""
|
|
return str(self)
|
|
|
|
|
|
@receiver(post_save, sender=Machine)
|
|
def machine_post_save(**kwargs):
|
|
"""Synchronisation ldap et régen parefeu/dhcp lors de la modification
|
|
d'une machine"""
|
|
user = kwargs['instance'].user
|
|
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
|
|
regen('dhcp')
|
|
regen('mac_ip_list')
|
|
|
|
|
|
@receiver(post_delete, sender=Machine)
|
|
def machine_post_delete(**kwargs):
|
|
"""Synchronisation ldap et régen parefeu/dhcp lors de la suppression
|
|
d'une machine"""
|
|
machine = kwargs['instance']
|
|
user = machine.user
|
|
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
|
|
regen('dhcp')
|
|
regen('mac_ip_list')
|
|
|
|
|
|
@receiver(post_save, sender=Interface)
|
|
def interface_post_save(**kwargs):
|
|
"""Synchronisation ldap et régen parefeu/dhcp lors de la modification
|
|
d'une interface"""
|
|
interface = kwargs['instance']
|
|
interface.sync_ipv6()
|
|
user = interface.machine.user
|
|
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
|
|
# Regen services
|
|
regen('dhcp')
|
|
regen('mac_ip_list')
|
|
|
|
|
|
@receiver(post_delete, sender=Interface)
|
|
def interface_post_delete(**kwargs):
|
|
"""Synchronisation ldap et régen parefeu/dhcp lors de la suppression
|
|
d'une interface"""
|
|
interface = kwargs['instance']
|
|
user = interface.machine.user
|
|
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
|
|
|
|
|
|
@receiver(post_save, sender=IpType)
|
|
def iptype_post_save(**kwargs):
|
|
"""Generation des objets ip après modification d'un range ip"""
|
|
iptype = kwargs['instance']
|
|
iptype.gen_ip_range()
|
|
iptype.check_replace_prefixv6()
|
|
|
|
|
|
@receiver(post_save, sender=MachineType)
|
|
def machinetype_post_save(**kwargs):
|
|
"""Mise à jour des interfaces lorsque changement d'attribution
|
|
d'une machinetype (changement iptype parent)"""
|
|
machinetype = kwargs['instance']
|
|
for interface in machinetype.all_interfaces():
|
|
interface.update_type()
|
|
|
|
|
|
@receiver(post_save, sender=Domain)
|
|
def domain_post_save(**_kwargs):
|
|
"""Regeneration dns après modification d'un domain object"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_delete, sender=Domain)
|
|
def domain_post_delete(**_kwargs):
|
|
"""Regeneration dns après suppression d'un domain object"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_save, sender=Extension)
|
|
def extension_post_save(**_kwargs):
|
|
"""Regeneration dns après modification d'une extension"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_delete, sender=Extension)
|
|
def extension_post_selete(**_kwargs):
|
|
"""Regeneration dns après suppression d'une extension"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_save, sender=SOA)
|
|
def soa_post_save(**_kwargs):
|
|
"""Regeneration dns après modification d'un SOA"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_delete, sender=SOA)
|
|
def soa_post_delete(**_kwargs):
|
|
"""Regeneration dns après suppresson d'un SOA"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_save, sender=Mx)
|
|
def mx_post_save(**_kwargs):
|
|
"""Regeneration dns après modification d'un MX"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_delete, sender=Mx)
|
|
def mx_post_delete(**_kwargs):
|
|
"""Regeneration dns après suppresson d'un MX"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_save, sender=Ns)
|
|
def ns_post_save(**_kwargs):
|
|
"""Regeneration dns après modification d'un NS"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_delete, sender=Ns)
|
|
def ns_post_delete(**_kwargs):
|
|
"""Regeneration dns après modification d'un NS"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_save, sender=Txt)
|
|
def text_post_save(**_kwargs):
|
|
"""Regeneration dns après modification d'un TXT"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_delete, sender=Txt)
|
|
def text_post_delete(**_kwargs):
|
|
"""Regeneration dns après modification d'un TX"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_save, sender=DName)
|
|
def dname_post_save(**_kwargs):
|
|
"""Updates the DNS regen after modification of a DName object."""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_delete, sender=DName)
|
|
def dname_post_delete(**_kwargs):
|
|
"""Updates the DNS regen after deletion of a DName object."""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_save, sender=Srv)
|
|
def srv_post_save(**_kwargs):
|
|
"""Regeneration dns après modification d'un SRV"""
|
|
regen('dns')
|
|
|
|
|
|
@receiver(post_delete, sender=Srv)
|
|
def srv_post_delete(**_kwargs):
|
|
"""Regeneration dns après modification d'un SRV"""
|
|
regen('dns')
|