Ce serveur Gitlab sera éteint le 30 juin 2020, pensez à migrer vos projets vers les serveurs gitlab-research.centralesupelec.fr et gitlab-student.centralesupelec.fr !

models.py 60.9 KB
Newer Older
1
# -*- mode: python; coding: utf-8 -*-
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017  Gabriel Détraz
# Copyright © 2017  Goulven Kermarec
# Copyright © 2017  Augustin Lemesle
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 24 25
"""machines.models
The models definitions for the Machines app
"""
26

27 28
from __future__ import unicode_literals

Gabriel Detraz's avatar
Gabriel Detraz committed
29 30
from datetime import timedelta
import re
31
from ipaddress import IPv6Address
32
from itertools import chain
33
from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress
Gabriel Detraz's avatar
Gabriel Detraz committed
34

35
from django.db import models
Gabriel Detraz's avatar
Gabriel Detraz committed
36
from django.db.models.signals import post_save, post_delete
37
from django.dispatch import receiver
38
from django.forms import ValidationError
39
from django.utils.functional import cached_property
40
from django.utils import timezone
Gabriel Detraz's avatar
Gabriel Detraz committed
41 42
from django.core.validators import MaxValueValidator

43
from macaddress.fields import MACAddressField
44

45
from re2o.field_permissions import FieldPermissionModelMixin
46
from re2o.mixins import AclMixin, RevMixin
47

Maël Kervella's avatar
Maël Kervella committed
48 49 50
import users.models
import preferences.models

51

52
class Machine(RevMixin, FieldPermissionModelMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
53 54
    """ Class définissant une machine, object parent user, objets fils
    interfaces"""
55
    PRETTY_NAME = "Machine"
Gabriel Detraz's avatar
Gabriel Detraz committed
56

57
    user = models.ForeignKey('users.User', on_delete=models.PROTECT)
Gabriel Detraz's avatar
Gabriel Detraz committed
58 59 60 61 62 63
    name = models.CharField(
        max_length=255,
        help_text="Optionnel",
        blank=True,
        null=True
    )
64
    active = models.BooleanField(default=True)
65

66 67 68
    class Meta:
        permissions = (
            ("view_machine", "Peut voir un objet machine quelquonque"),
69 70
            ("change_machine_user",
             "Peut changer le propriétaire d'une machine"),
71 72
        )

73 74
    @classmethod
    def get_instance(cls, machineid, *_args, **_kwargs):
75 76 77 78
        """Get the Machine instance with machineid.
        :param userid: The id
        :return: The user
        """
79
        return cls.objects.get(pk=machineid)
80

81 82 83
    def linked_objects(self):
        """Return linked objects : machine and domain.
        Usefull in history display"""
84 85 86 87 88 89
        return chain(
            self.interface_set.all(),
            Domain.objects.filter(
                interface_parent__in=self.interface_set.all()
            )
        )
90

91
    @staticmethod
92
    def can_change_user(user_request, *_args, **_kwargs):
93 94 95 96 97 98 99 100 101 102
        """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.
        """
103 104
        return (user_request.has_perm('machines.change_machine_user'),
                "Vous ne pouvez pas modifier l'utilisateur de la machine.")
105

106 107
    @staticmethod
    def can_view_all(user_request, *_args, **_kwargs):
108 109 110 111 112
        """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'):
113 114
            return False, (u"Vous ne pouvez pas afficher l'ensemble des "
                           "machines sans permission")
115 116
        return True, None

117 118
    @staticmethod
    def can_create(user_request, userid, *_args, **_kwargs):
119 120 121 122 123
        """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"""
Maël Kervella's avatar
Maël Kervella committed
124
        try:
125
            user = users.models.User.objects.get(pk=userid)
Maël Kervella's avatar
Maël Kervella committed
126 127
        except users.models.User.DoesNotExist:
            return False, u"Utilisateur inexistant"
128 129 130 131
        max_lambdauser_interfaces = (preferences.models.OptionalMachine
                                     .get_cached_value(
                                         'max_lambdauser_interfaces'
                                     ))
132
        if not user_request.has_perm('machines.add_machine'):
133 134
            if not (preferences.models.OptionalMachine
                    .get_cached_value('create_machine')):
135
                return False, u"Vous ne pouvez pas ajouter une machine"
Maël Kervella's avatar
Maël Kervella committed
136
            if user != user_request:
137 138
                return False, (u"Vous ne pouvez pas ajouter une machine à un "
                               "autre user que vous sans droit")
Maël Kervella's avatar
Maël Kervella committed
139
            if user.user_interfaces().count() >= max_lambdauser_interfaces:
140 141 142
                return False, (u"Vous avez atteint le maximum d'interfaces "
                               "autorisées que vous pouvez créer vous même "
                               "(%s) " % max_lambdauser_interfaces)
Maël Kervella's avatar
Maël Kervella committed
143 144
        return True, None

145
    def can_edit(self, user_request, *args, **kwargs):
146 147 148 149 150
        """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"""
151
        if self.user != user_request:
152 153 154 155 156 157 158 159 160
            if (not user_request.has_perm('machines.change_interface') or
                    not self.user.can_edit(
                        self.user,
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
161 162
        return True, None

163
    def can_delete(self, user_request, *args, **kwargs):
164 165 166 167 168
        """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"""
169
        if self.user != user_request:
170 171 172 173 174 175 176 177 178
            if (not user_request.has_perm('machines.change_interface') or
                    not self.user.can_edit(
                        self.user,
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
179 180
        return True, None

181
    def can_view(self, user_request, *_args, **_kwargs):
182 183 184 185 186
        """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"""
187 188 189 190
        if (not user_request.has_perm('machines.view_machine') and
                self.user != user_request):
            return False, (u"Vous n'avez pas droit de voir les machines autre "
                           "que les vôtres")
191
        return True, None
192

193 194 195
    def __init__(self, *args, **kwargs):
        super(Machine, self).__init__(*args, **kwargs)
        self.field_permissions = {
196
            'user': self.can_change_user,
197 198
        }

199
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
200 201
        return str(self.user) + ' - ' + str(self.id) + ' - ' + str(self.name)

202

203
class MachineType(RevMixin, AclMixin, models.Model):
204
    """ Type de machine, relié à un type d'ip, affecté aux interfaces"""
205 206
    PRETTY_NAME = "Type de machine"

207
    type = models.CharField(max_length=255)
Gabriel Detraz's avatar
Gabriel Detraz committed
208 209 210 211 212 213
    ip_type = models.ForeignKey(
        'IpType',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
214

215 216 217
    class Meta:
        permissions = (
            ("view_machinetype", "Peut voir un objet machinetype"),
218 219
            ("use_all_machinetype",
             "Peut utiliser n'importe quel type de machine"),
220 221
        )

222
    def all_interfaces(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
223 224
        """ Renvoie toutes les interfaces (cartes réseaux) de type
        machinetype"""
225 226
        return Interface.objects.filter(type=self)

227 228
    @staticmethod
    def can_use_all(user_request, *_args, **_kwargs):
229 230 231 232 233 234 235 236
        """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.
        """
237
        if not user_request.has_perm('machines.use_all_machinetype'):
238 239
            return False, (u"Vous n'avez pas le droit d'utiliser tout types "
                           "de machines")
240 241
        return True, None

242
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
243 244
        return self.type

245

246
class IpType(RevMixin, AclMixin, models.Model):
247
    """ Type d'ip, définissant un range d'ip, affecté aux machine types"""
248 249
    PRETTY_NAME = "Type d'ip"

250
    type = models.CharField(max_length=255)
251
    extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
252
    need_infra = models.BooleanField(default=False)
253 254
    domaine_ip_start = models.GenericIPAddressField(protocol='IPv4')
    domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4')
Gabriel Detraz's avatar
Gabriel Detraz committed
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
    prefix_v6 = models.GenericIPAddressField(
        protocol='IPv6',
        null=True,
        blank=True
    )
    vlan = models.ForeignKey(
        'Vlan',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
    ouverture_ports = models.ForeignKey(
        'OuverturePortList',
        blank=True,
        null=True
    )
271

272 273 274 275 276 277
    class Meta:
        permissions = (
            ("view_iptype", "Peut voir un objet iptype"),
            ("use_all_iptype", "Peut utiliser tous les iptype"),
        )

278
    @cached_property
279
    def ip_range(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
280 281
        """ Renvoie un objet IPRange à partir de l'objet IpType"""
        return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop)
282 283 284

    @cached_property
    def ip_set(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
285
        """ Renvoie une IPSet à partir de l'iptype"""
286
        return IPSet(self.ip_range)
287 288 289

    @cached_property
    def ip_set_as_str(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
290
        """ Renvoie une liste des ip en string"""
291 292 293
        return [str(x) for x in self.ip_set]

    def ip_objects(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
294
        """ Renvoie tous les objets ipv4 relié à ce type"""
295 296 297
        return IpList.objects.filter(ip_type=self)

    def free_ip(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
298
        """ Renvoie toutes les ip libres associées au type donné (self)"""
Gabriel Detraz's avatar
Gabriel Detraz committed
299 300 301
        return IpList.objects.filter(
            interface__isnull=True
        ).filter(ip_type=self)
302 303

    def gen_ip_range(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
304 305 306
        """ 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"""
307
        # Creation du range d'ip dans les objets iplist
308 309 310 311
        networks = []
        for net in self.ip_range.cidrs():
            networks += net.iter_hosts()
        ip_obj = [IpList(ip_type=self, ipv4=str(ip)) for ip in networks]
Gabriel Detraz's avatar
Gabriel Detraz committed
312 313 314
        listes_ip = IpList.objects.filter(
            ipv4__in=[str(ip) for ip in networks]
        )
315 316 317 318 319 320
        # 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)
321
        return
322 323

    def del_ip_range(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
324 325
        """ Methode dépréciée, IpList est en mode cascade et supprimé
        automatiquement"""
326
        if Interface.objects.filter(ipv4__in=self.ip_objects()):
Gabriel Detraz's avatar
Gabriel Detraz committed
327 328
            raise ValidationError("Une ou plusieurs ip du range sont\
            affectées, impossible de supprimer le range")
329 330 331
        for ip in self.ip_objects():
            ip.delete()

332 333 334 335 336
    def check_replace_prefixv6(self):
        """Remplace les prefixv6 des interfaces liées à ce type d'ip"""
        if not self.prefix_v6:
            return
        else:
337
            for ipv6 in Ipv6List.objects.filter(
338 339 340 341
                    interface__in=Interface.objects.filter(
                        type__in=MachineType.objects.filter(ip_type=self)
                    )
                ):
342 343
                ipv6.check_and_replace_prefix(prefix=self.prefix_v6)

344
    def clean(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
345 346 347 348 349
        """ 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"""
350 351 352 353
        if IPAddress(self.domaine_ip_start) > IPAddress(self.domaine_ip_stop):
            raise ValidationError("Domaine end doit être après start...")
        # On ne crée pas plus grand qu'un /16
        if self.ip_range.size > 65536:
Gabriel Detraz's avatar
Gabriel Detraz committed
354 355
            raise ValidationError("Le range est trop gros, vous ne devez\
            pas créer plus grand qu'un /16")
356
        # On check que les / ne se recoupent pas
357
        for element in IpType.objects.all().exclude(pk=self.pk):
358
            if not self.ip_set.isdisjoint(element.ip_set):
Gabriel Detraz's avatar
Gabriel Detraz committed
359 360
                raise ValidationError("Le range indiqué n'est pas disjoint\
                des ranges existants")
361 362 363
        # On formate le prefix v6
        if self.prefix_v6:
            self.prefix_v6 = str(IPNetwork(self.prefix_v6 + '/64').network)
364 365 366 367 368 369
        return

    def save(self, *args, **kwargs):
        self.clean()
        super(IpType, self).save(*args, **kwargs)

370 371
    @staticmethod
    def can_use_all(user_request, *_args, **_kwargs):
372 373
        """Superdroit qui permet d'utiliser toutes les extensions sans
        restrictions
374 375
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
376
        return user_request.has_perm('machines.use_all_iptype'), None
377

378 379
    def __str__(self):
        return self.type
chirac's avatar
chirac committed
380

Gabriel Detraz's avatar
Gabriel Detraz committed
381

382
class Vlan(RevMixin, AclMixin, models.Model):
383 384
    """ Un vlan : vlan_id et nom
    On limite le vlan id entre 0 et 4096, comme défini par la norme"""
385 386
    PRETTY_NAME = "Vlans"

387
    vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)])
388 389 390
    name = models.CharField(max_length=256)
    comment = models.CharField(max_length=256, blank=True)

391 392 393 394 395
    class Meta:
        permissions = (
            ("view_vlan", "Peut voir un objet vlan"),
        )

396 397 398
    def __str__(self):
        return self.name

Gabriel Detraz's avatar
Gabriel Detraz committed
399

400
class Nas(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
401
    """ Les nas. Associé à un machine_type.
Gabriel Detraz's avatar
Gabriel Detraz committed
402 403
    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"""
404 405
    PRETTY_NAME = "Correspondance entre les nas et les machines connectées"

406 407 408 409 410 411
    default_mode = '802.1X'
    AUTH = (
        ('802.1X', '802.1X'),
        ('Mac-address', 'Mac-address'),
    )

412
    name = models.CharField(max_length=255, unique=True)
Gabriel Detraz's avatar
Gabriel Detraz committed
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
    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
    )
428
    autocapture_mac = models.BooleanField(default=False)
429

430 431 432 433 434
    class Meta:
        permissions = (
            ("view_nas", "Peut voir un objet Nas"),
        )

435 436 437
    def __str__(self):
        return self.name

Gabriel Detraz's avatar
Gabriel Detraz committed
438

439
class SOA(RevMixin, AclMixin, models.Model):
440 441 442 443 444 445 446
    """
    Un enregistrement SOA associé à une extension
    Les valeurs par défault viennent des recommandations RIPE :
    https://www.ripe.net/publications/docs/ripe-203
    """
    PRETTY_NAME = "Enregistrement SOA"

447
    name = models.CharField(max_length=255)
448 449 450 451
    mail = models.EmailField(
        help_text='Email du contact pour la zone'
    )
    refresh = models.PositiveIntegerField(
452
        default=86400,  # 24 hours
453 454 455 456
        help_text='Secondes avant que les DNS secondaires doivent demander le\
                   serial du DNS primaire pour détecter une modification'
    )
    retry = models.PositiveIntegerField(
457
        default=7200,  # 2 hours
458 459 460 461
        help_text='Secondes avant que les DNS secondaires fassent une nouvelle\
                   demande de serial en cas de timeout du DNS primaire'
    )
    expire = models.PositiveIntegerField(
462
        default=3600000,  # 1000 hours
463 464 465 466 467 468 469 470
        help_text='Secondes après lesquelles les DNS secondaires arrêtent de\
                   de répondre aux requêtes en cas de timeout du DNS primaire'
    )
    ttl = models.PositiveIntegerField(
        default=172800,  # 2 days
        help_text='Time To Live'
    )

471 472 473 474 475
    class Meta:
        permissions = (
            ("view_soa", "Peut voir un objet soa"),
        )

476 477 478 479 480 481 482 483 484 485 486 487 488
    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 (
489 490 491 492
            '    {refresh}; refresh\n'
            '    {retry}; retry\n'
            '    {expire}; expire\n'
            '    {ttl}; TTL'
493
        ).format(
494 495 496 497
            refresh=str(self.refresh).ljust(12),
            retry=str(self.retry).ljust(12),
            expire=str(self.expire).ljust(12),
            ttl=str(self.ttl).ljust(12)
498 499 500 501 502 503
        )

    @cached_property
    def dns_soa_mail(self):
        """ Renvoie le mail dans l'enregistrement SOA """
        mail_fields = str(self.mail).split('@')
504
        return mail_fields[0].replace('.', '\\.') + '.' + mail_fields[1] + '.'
505 506 507 508 509 510 511

    @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. """
512 513 514 515
        return cls.objects.get_or_create(
            name="SOA to edit",
            mail="postmaser@example.com"
        )[0].pk
516 517


518
class Extension(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
519 520
    """ Extension dns type example.org. Précise si tout le monde peut
    l'utiliser, associé à un origin (ip d'origine)"""
521 522
    PRETTY_NAME = "Extensions dns"

Gabriel Detraz's avatar
Gabriel Detraz committed
523 524 525 526 527
    name = models.CharField(
        max_length=255,
        unique=True,
        help_text="Nom de la zone, doit commencer par un point (.example.org)"
    )
528
    need_infra = models.BooleanField(default=False)
529
    origin = models.ForeignKey(
Gabriel Detraz's avatar
Gabriel Detraz committed
530 531 532
        'IpList',
        on_delete=models.PROTECT,
        blank=True,
Gabriel Detraz's avatar
Gabriel Detraz committed
533 534
        null=True,
        help_text="Enregistrement A associé à la zone"
Gabriel Detraz's avatar
Gabriel Detraz committed
535
    )
536 537 538
    origin_v6 = models.GenericIPAddressField(
        protocol='IPv6',
        null=True,
Gabriel Detraz's avatar
Gabriel Detraz committed
539
        blank=True,
lhark's avatar
lhark committed
540
        help_text="Enregistrement AAAA associé à la zone"
541
    )
542 543
    soa = models.ForeignKey(
        'SOA',
544
        on_delete=models.CASCADE
545
    )
546

547 548 549 550 551 552
    class Meta:
        permissions = (
            ("view_extension", "Peut voir un objet extension"),
            ("use_all_extension", "Peut utiliser toutes les extension"),
        )

553 554
    @cached_property
    def dns_entry(self):
555 556 557
        """ Une entrée DNS A et AAAA sur origin (zone self)"""
        entry = ""
        if self.origin:
558
            entry += "@               IN  A       " + str(self.origin)
559 560 561
        if self.origin_v6:
            if entry:
                entry += "\n"
562
            entry += "@               IN  AAAA    " + str(self.origin_v6)
563
        return entry
564

565
    def get_associated_a_records(self):
566 567
        from re2o.utils import all_active_assigned_interfaces
        return (all_active_assigned_interfaces()
568
                .filter(type__ip_type__extension=self)
569
                .filter(ipv4__isnull=False))
570 571

    def get_associated_aaaa_records(self):
572 573 574
        from re2o.utils import all_active_interfaces
        return (all_active_interfaces(full=True)
                .filter(type__ip_type__extension=self))
575 576

    def get_associated_cname_records(self):
577
        from re2o.utils import all_active_assigned_interfaces
578 579 580
        return (Domain.objects
                .filter(extension=self)
                .filter(cname__isnull=False)
581
                .filter(interface_parent__in=all_active_assigned_interfaces())
582
                .prefetch_related('cname'))
583

584 585
    @staticmethod
    def can_use_all(user_request, *_args, **_kwargs):
586 587
        """Superdroit qui permet d'utiliser toutes les extensions sans
        restrictions
588 589
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
590
        return user_request.has_perm('machines.use_all_extension'), None
591

592 593
    def __str__(self):
        return self.name
chirac's avatar
chirac committed
594

595
    def clean(self, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
596 597 598 599
        if self.name and self.name[0] != '.':
            raise ValidationError("Une extension doit commencer par un point")
        super(Extension, self).clean(*args, **kwargs)

Gabriel Detraz's avatar
Gabriel Detraz committed
600

601
class Mx(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
602 603
    """ Entrées des MX. Enregistre la zone (extension) associée et la
    priorité
Gabriel Detraz's avatar
Gabriel Detraz committed
604
    Todo : pouvoir associer un MX à une interface """
605 606 607
    PRETTY_NAME = "Enregistrements MX"

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
608
    priority = models.PositiveIntegerField(unique=True)
chirac's avatar
chirac committed
609
    name = models.OneToOneField('Domain', on_delete=models.PROTECT)
610

611 612 613 614 615
    class Meta:
        permissions = (
            ("view_mx", "Peut voir un objet mx"),
        )

616 617
    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
618 619
        """Renvoie l'entrée DNS complète pour un MX à mettre dans les
        fichiers de zones"""
620 621
        return "@               IN  MX  {prior} {name}".format(
            prior=str(self.priority).ljust(3),
Maël Kervella's avatar
Maël Kervella committed
622
            name=str(self.name)
623
        )
624

625 626 627
    def __str__(self):
        return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name)

Gabriel Detraz's avatar
Gabriel Detraz committed
628

629
class Ns(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
630
    """Liste des enregistrements name servers par zone considéérée"""
631 632 633
    PRETTY_NAME = "Enregistrements NS"

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
634
    ns = models.ForeignKey('Domain', on_delete=models.PROTECT)
635

636 637
    class Meta:
        permissions = (
Gabriel Detraz's avatar
Gabriel Detraz committed
638
            ("view_ns", "Peut voir un objet ns"),
639 640
        )

641 642
    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
643
        """Renvoie un enregistrement NS complet pour les filezones"""
644
        return "@               IN  NS      " + str(self.ns)
645

646
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
647
        return str(self.zone) + ' ' + str(self.ns)
648

Gabriel Detraz's avatar
Gabriel Detraz committed
649

650
class Txt(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
651
    """ Un enregistrement TXT associé à une extension"""
652
    PRETTY_NAME = "Enregistrement TXT"
Gabriel Detraz's avatar
Gabriel Detraz committed
653 654 655

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
    field1 = models.CharField(max_length=255)
656
    field2 = models.TextField(max_length=2047)
Gabriel Detraz's avatar
Gabriel Detraz committed
657

658 659 660 661 662
    class Meta:
        permissions = (
            ("view_txt", "Peut voir un objet txt"),
        )

Gabriel Detraz's avatar
Gabriel Detraz committed
663
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
664 665
        return str(self.zone) + " : " + str(self.field1) + " " +\
            str(self.field2)
Gabriel Detraz's avatar
Gabriel Detraz committed
666 667 668

    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
669
        """Renvoie l'enregistrement TXT complet pour le fichier de zone"""
670
        return str(self.field1).ljust(15) + " IN  TXT     " + str(self.field2)
Gabriel Detraz's avatar
Gabriel Detraz committed
671

Gabriel Detraz's avatar
Gabriel Detraz committed
672

673
class Srv(RevMixin, AclMixin, models.Model):
674
    """ A SRV record """
Gabriel Detraz's avatar
Gabriel Detraz committed
675 676 677 678 679
    PRETTY_NAME = "Enregistrement Srv"

    TCP = 'TCP'
    UDP = 'UDP'

680
    service = models.CharField(max_length=31)
Gabriel Detraz's avatar
Gabriel Detraz committed
681 682 683 684 685 686 687 688 689 690 691 692 693 694
    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(
695
        default=0,
Gabriel Detraz's avatar
Gabriel Detraz committed
696
        validators=[MaxValueValidator(65535)],
697 698 699
        help_text=("La priorité du serveur cible (valeur entière non "
                   "négative, plus elle est faible, plus ce serveur sera "
                   "utilisé s'il est disponible)")
Gabriel Detraz's avatar
Gabriel Detraz committed
700 701
    )
    weight = models.PositiveIntegerField(
702
        default=0,
Gabriel Detraz's avatar
Gabriel Detraz committed
703 704 705 706 707 708 709 710 711 712 713 714 715 716
        validators=[MaxValueValidator(65535)],
        help_text="Poids relatif pour les enregistrements de même priorité\
            (valeur entière de 0 à 65535)"
    )
    port = models.PositiveIntegerField(
        validators=[MaxValueValidator(65535)],
        help_text="Port (tcp/udp)"
    )
    target = models.ForeignKey(
        'Domain',
        on_delete=models.PROTECT,
        help_text="Serveur cible"
    )

717 718
    class Meta:
        permissions = (
719
            ("view_srv", "Peut voir un objet srv"),
720 721
        )

Gabriel Detraz's avatar
Gabriel Detraz committed
722 723 724 725 726 727 728 729 730 731 732 733 734 735
    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) + '.'


736
class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
737 738 739
    """ Une interface. Objet clef de l'application machine :
    - une address mac unique. Possibilité de la rendre unique avec le
    typemachine
Gabriel Detraz's avatar
Gabriel Detraz committed
740 741 742 743
    - 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"""
744 745
    PRETTY_NAME = "Interface"

Gabriel Detraz's avatar
Gabriel Detraz committed
746 747 748 749 750 751
    ipv4 = models.OneToOneField(
        'IpList',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
752
    mac_address = MACAddressField(integer=False, unique=True)
753
    machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
754
    type = models.ForeignKey('MachineType', on_delete=models.PROTECT)
755
    details = models.CharField(max_length=255, blank=True)
756
    port_lists = models.ManyToManyField('OuverturePortList', blank=True)
chirac's avatar
chirac committed
757

758 759 760
    class Meta:
        permissions = (
            ("view_interface", "Peut voir un objet interface"),
761 762
            ("change_interface_machine",
             "Peut changer le propriétaire d'une interface"),
763 764
        )

765
    @cached_property
Dalahro's avatar
Dalahro committed
766 767 768 769
    def is_active(self):
        """ Renvoie si une interface doit avoir accès ou non """
        machine = self.machine
        user = self.machine.user
770
        return machine.active and user.has_access()
Dalahro's avatar
Dalahro committed
771

772
    @cached_property
773
    def ipv6_slaac(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
774 775
        """ Renvoie un objet type ipv6 à partir du prefix associé à
        l'iptype parent"""
776
        if self.type.ip_type.prefix_v6:
Gabriel Detraz's avatar
Gabriel Detraz committed
777 778 779
            return EUI(self.mac_address).ipv6(
                IPNetwork(self.type.ip_type.prefix_v6).network
            )
780 781 782
        else:
            return None

783 784 785 786 787 788
    @cached_property
    def gen_ipv6_dhcpv6(self):
        """Cree une ip, à assigner avec dhcpv6 sur une machine"""
        prefix_v6 = self.type.ip_type.prefix_v6
        if not prefix_v6:
            return None
789 790 791 792
        return IPv6Address(
            IPv6Address(prefix_v6).exploded[:20] +
            IPv6Address(self.id).exploded[20:]
        )
793 794 795 796 797 798 799 800 801 802 803 804 805

    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

806 807 808 809 810 811 812 813
    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
814 815 816
        ipv6_object = (Ipv6List.objects
                       .filter(interface=self, slaac_ip=True)
                       .first())
817 818 819 820 821 822
        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()

823 824
    def sync_ipv6(self):
        """Cree et met à jour l'ensemble des ipv6 en fonction du mode choisi"""
825 826
        if (preferences.models.OptionalMachine
                .get_cached_value('ipv6_mode') == 'SLAAC'):
827
            self.sync_ipv6_slaac()
828 829
        elif (preferences.models.OptionalMachine
              .get_cached_value('ipv6_mode') == 'DHCPV6'):
830 831 832 833
            self.sync_ipv6_dhcpv6()
        else:
            return

834
    def ipv6(self):
835
        """ Renvoie le queryset de la liste des ipv6
836 837 838 839
        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'):
840
            return self.ipv6list.all()
841 842
        elif (preferences.models.OptionalMachine
              .get_cached_value('ipv6_mode') == 'DHCPV6'):
843
            return self.ipv6list.filter(slaac_ip=False)
844 845
        else:
            return None
846

847
    def mac_bare(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
848
        """ Formatage de la mac type mac_bare"""
849 850
        return str(EUI(self.mac_address, dialect=mac_bare)).lower()

851
    def filter_macaddress(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
852 853
        """ Tente un formatage mac_bare, si échoue, lève une erreur de
        validation"""
854 855
        try:
            self.mac_address = str(EUI(self.mac_address))
Gabriel Detraz's avatar
Gabriel Detraz committed
856
        except:
857 858
            raise ValidationError("La mac donnée est invalide")

859
    def clean(self, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
860 861
        """ Formate l'addresse mac en mac_bare (fonction filter_mac)
        et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
862 863 864 865 866 867 868
        # 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.
869
        if not hasattr(self, 'type'):
870
            raise ValidationError("Le type d'ip choisi n'est pas valide")
871
        self.filter_macaddress()
872
        self.mac_address = str(EUI(self.mac_address)) or None
873
        if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
874
            self.assign_ipv4()
875
        super(Interface, self).clean(*args, **kwargs)
876 877 878 879 880 881 882

    def assign_ipv4(self):
        """ Assigne une ip à l'interface """
        free_ips = self.type.ip_type.free_ip()
        if free_ips:
            self.ipv4 = free_ips[0]
        else:
Gabriel Detraz's avatar
Gabriel Detraz committed
883 884
            raise ValidationError("Il n'y a plus d'ip disponibles\
            dans le slash")
885 886 887
        return

    def unassign_ipv4(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
888
        """ Sans commentaire, désassigne une ipv4"""
889
        self.ipv4 = None
890

891 892 893 894 895
    def update_type(self):
        """ Lorsque le machinetype est changé de type d'ip, on réassigne"""
        self.clean()
        self.save()

896
    def save(self, *args, **kwargs):
897
        self.filter_macaddress()
898
        # On verifie la cohérence en forçant l'extension par la méthode
899 900 901 902
        if self.ipv4:
            if self.type.ip_type != self.ipv4.ip_type:
                raise ValidationError("L'ipv4 et le type de la machine ne\
                correspondent pas")
903 904
        super(Interface, self).save(*args, **kwargs)

905 906
    @staticmethod
    def can_create(user_request, machineid, *_args, **_kwargs):
907 908 909 910 911
        """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"""
912
        try:
913
            machine = Machine.objects.get(pk=machineid)
914 915
        except Machine.DoesNotExist:
            return False, u"Machine inexistante"
916
        if not user_request.has_perm('machines.add_interface'):
917 918
            if not (preferences.models.OptionalMachine
                    .get_cached_value('create_machine')):
919
                return False, u"Vous ne pouvez pas ajouter une machine"
920 921 922 923
            max_lambdauser_interfaces = (preferences.models.OptionalMachine
                                         .get_cached_value(
                                             'max_lambdauser_interfaces'
                                         ))
924 925 926
            if machine.user != user_request:
                return False, u"Vous ne pouvez pas ajouter une interface à une\
                        machine d'un autre user que vous sans droit"
927 928
            if (machine.user.user_interfaces().count() >=
                    max_lambdauser_interfaces):
929 930 931 932 933
                return False, u"Vous avez atteint le maximum d'interfaces\
                        autorisées que vous pouvez créer vous même (%s) "\
                        % max_lambdauser_interfaces
        return True, None

934
    @staticmethod
935 936 937
    def can_change_machine(user_request, *_args, **_kwargs):
        """Check if a user can change the machine associated with an
        Interface object """
938 939
        return (user_request.has_perm('machines.change_interface_machine'),
                "Droit requis pour changer la machine")
940

941
    def can_edit(self, user_request, *args, **kwargs):
942 943 944 945 946
        """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"""
947
        if self.machine.user != user_request:
948 949 950 951 952 953 954 955
            if (not user_request.has_perm('machines.change_interface') or
                    not self.machine.user.can_edit(
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
956 957
        return True, None

958
    def can_delete(self, user_request, *args, **kwargs):
959
        """Verifie que l'user a les bons droits delete object pour del
960 961 962 963
        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"""
964
        if self.machine.user != user_request:
965 966 967 968 969 970 971 972
            if (not user_request.has_perm('machines.change_interface') or
                    not self.machine.user.can_edit(
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
973 974
        return True, None

975
    def can_view(self, user_request, *_args, **_kwargs):
976
        """Vérifie qu'on peut bien voir cette instance particulière avec
977
        droit view objet ou qu'elle appartient à l'user
978 979 980
        :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"""
981 982 983 984
        if (not user_request.has_perm('machines.view_interface') and
                self.machine.user != user_request):
            return False, (u"Vous n'avez pas le droit de voir des machines "
                           "autre que les vôtres")
985 986
        return True, None

987 988 989
    def __init__(self, *args, **kwargs):
        super(Interface, self).__init__(*args, **kwargs)
        self.field_permissions = {
990
            'machine': self.can_change_machine,
991 992
        }

993
    def __str__(self):
chirac's avatar
chirac committed
994 995 996 997 998
        try:
            domain = self.domain
        except:
            domain = None
        return str(domain)
999

1000
    def has_private_ip(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1001
        """ True si l'ip associée est privée"""
1002
        if self.ipv4:
1003 1004 1005
            return IPAddress(str(self.ipv4)).is_private()
        else:
            return False
1006

1007
    def may_have_port_open(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1008
        """ True si l'interface a une ip et une ip publique.
Gabriel Detraz's avatar
Gabriel Detraz committed
1009 1010
        Permet de ne pas exporter des ouvertures sur des ip privées
        (useless)"""
1011
        return self.ipv4 and not self.has_private_ip()
1012

Gabriel Detraz's avatar
Gabriel Detraz committed
1013

1014
class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
1015
    """ A list of IPv6 """
1016 1017 1018 1019 1020 1021
    PRETTY_NAME = 'Enregistrements Ipv6 des machines'

    ipv6 = models.GenericIPAddressField(
        protocol='IPv6',
        unique=True
    )
1022 1023 1024 1025 1026
    interface = models.ForeignKey(
        'Interface',
        on_delete=models.CASCADE,
        related_name='ipv6list'
    )
1027 1028 1029 1030 1031
    slaac_ip = models.BooleanField(default=False)

    class Meta:
        permissions = (
            ("view_ipv6list", "Peut voir un objet ipv6"),
1032 1033
            ("change_ipv6list_slaac_ip",
             "Peut changer la valeur slaac sur une ipv6"),
1034 1035
        )

1036 1037
    @staticmethod
    def can_create(user_request, interfaceid, *_args, **_kwargs):
1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053
        """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, u"Interface inexistante"
        if not user_request.has_perm('machines.add_ipv6list'):
            if interface.machine.user != user_request:
                return False, u"Vous ne pouvez pas ajouter un alias à une\
                        machine d'un autre user que vous sans droit"
        return True, None

    @staticmethod
1054 1055
    def can_change_slaac_ip(user_request, *_args, **_kwargs):
        """ Check if a user can change the slaac value """
1056 1057
        return (user_request.has_perm('machines.change_ipv6list_slaac_ip'),
                "Droit requis pour changer la valeur slaac ip")
1058 1059 1060 1061 1062 1063 1064 1065

    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:
1066 1067 1068 1069 1070 1071 1072 1073
            if (not user_request.has_perm('machines.change_ipv6list') or
                    not self.interface.machine.user.can_edit(
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
1074 1075 1076 1077 1078 1079 1080 1081 1082
        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:
1083 1084 1085 1086 1087 1088 1089 1090
            if (not user_request.has_perm('machines.change_ipv6list') or
                    not self.interface.machine.user.can_edit(
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
1091 1092
        return True, None

1093
    def can_view(self, user_request, *_args, **_kwargs):
1094 1095 1096 1097 1098
        """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"""
1099 1100 1101 1102
        if (not user_request.has_perm('machines.view_ipv6list') and
                self.interface.machine.user != user_request):
            return False, (u"Vous n'avez pas le droit de voir des machines "
                           "autre que les vôtres")
1103 1104 1105 1106 1107
        return True, None

    def __init__(self, *args, **kwargs):
        super(Ipv6List, self).__init__(*args, **kwargs)
        self.field_permissions = {
1108
            'slaac_ip': self.can_change_slaac_ip,
1109 1110
        }

1111 1112
    def check_and_replace_prefix(self, prefix=None):
        """Si le prefixe v6 est incorrect, on maj l'ipv6"""
1113 1114 1115
        prefix_v6 = prefix or self.interface.type.ip_type.prefix_v6
        if not prefix_v6:
            return
1116 1117 1118 1119 1120 1121
        if (IPv6Address(self.ipv6).exploded[:20] !=
                IPv6Address(prefix_v6).exploded[:20]):
            self.ipv6 = IPv6Address(
                IPv6Address(prefix_v6).exploded[:20] +
                IPv6Address(self.ipv6).exploded[20:]
            )
1122 1123
            self.save()

1124
    def clean(self, *args, **kwargs):
1125 1126 1127
        if self.slaac_ip and (Ipv6List.objects
                              .filter(interface=self.interface, slaac_ip=True)
                              .exclude(id=self.id)):
1128
            raise ValidationError("Une ip slaac est déjà enregistrée")
1129 1130
        prefix_v6 = self.interface.type.ip_type.prefix_v6
        if prefix_v6:
1131 1132 1133 1134 1135 1136
            if (IPv6Address(self.ipv6).exploded[:20] !=
                    IPv6Address(prefix_v6).exploded[:20]):
                raise ValidationError(
                    "Le prefixv6 est incorrect et ne correspond pas au type "
                    "associé à la machine"
                )
1137 1138 1139 1140 1141 1142 1143
        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)

1144 1145 1146 1147
    def __str__(self):
        return str(self.ipv6)


1148
class Domain(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
1149 1150 1151
    """ 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"""
chirac's avatar
chirac committed
1152
    PRETTY_NAME = "Domaine dns"
1153

Gabriel Detraz's avatar
Gabriel Detraz committed
1154 1155 1156 1157 1158 1159 1160 1161 1162 1163
    interface_parent = models.OneToOneField(
        'Interface',
        on_delete=models.CASCADE,
        blank=True,
        null=True
    )
    name = models.CharField(
        help_text="Obligatoire et unique, ne doit pas comporter de points",
        max_length=255
    )
chirac's avatar
chirac committed
1164
    extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
Gabriel Detraz's avatar
Gabriel Detraz committed
1165 1166 1167 1168 1169 1170
    cname = models.ForeignKey(
        'self',
        null=True,
        blank=True,
        related_name='related_domain'
    )
chirac's avatar
chirac committed
1171 1172

    class Meta:
1173
        unique_together = (("name", "extension"),)
1174 1175 1176
        permissions = (
            ("view_domain", "Peut voir un objet domain"),
        )
chirac's avatar
chirac committed
1177

1178
    def get_extension(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1179 1180
        """ Retourne l'extension de l'interface parente si c'est un A
         Retourne l'extension propre si c'est un cname, renvoie None sinon"""
1181 1182
        if self.interface_parent:
            return self.interface_parent.type.ip_type.extension
Gabriel Detraz's avatar
Gabriel Detraz committed
1183
        elif hasattr(self, 'extension'):
1184
            return self.extension
1185 1186
        else:
            return None
1187

chirac's avatar
chirac committed
1188
    def clean(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1189
        """ Validation :
Gabriel Detraz's avatar
Gabriel Detraz committed
1190 1191
        - l'objet est bien soit A soit CNAME
        - le cname est pas pointé sur lui-même
Gabriel Detraz's avatar
Gabriel Detraz committed
1192 1193
        - le nom contient bien les caractères autorisés par la norme
        dns et moins de 63 caractères au total
Gabriel Detraz's avatar
Gabriel Detraz committed
1194
        - le couple nom/extension est bien unique"""
1195
        if self.get_extension():
Gabriel Detraz's avatar
Gabriel Detraz committed
1196
            self.extension = self.get_extension()
chirac's avatar
chirac committed
1197 1198
        if self.interface_parent and self.cname:
            raise ValidationError("On ne peut créer à la fois A et CNAME")
Gabriel Detraz's avatar
Gabriel Detraz committed
1199
        if self.cname == self:
chirac's avatar
chirac committed
1200
            raise ValidationError("On ne peut créer un cname sur lui même")
Gabriel Detraz's avatar
Gabriel Detraz committed
1201
        HOSTNAME_LABEL_PATTERN = re.compile(
1202
            r"(?!-)[A-Z\d-]+(?<!-)$",
Gabriel Detraz's avatar
Gabriel Detraz committed
1203 1204
            re.IGNORECASE
        )
1205 1206
        dns = self.name.lower()
        if len(dns) > 63:
Gabriel Detraz's avatar
Gabriel Detraz committed
1207 1208
            raise ValidationError("Le nom de domaine %s est trop long\
            (maximum de 63 caractères)." % dns)
1209
        if not HOSTNAME_LABEL_PATTERN.match(dns):
Gabriel Detraz's avatar
Gabriel Detraz committed
1210 1211
            raise ValidationError("Ce nom de domaine %s contient des\
            carractères interdits." % dns)
1212 1213 1214
        self.validate_unique()
        super(Domain, self).clean()

1215 1216
    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1217
        """ Une entrée DNS"""
1218
        if self.cname:
1219 1220 1221 1222
            return "{name} IN CNAME   {cname}.".format(
                name=str(self.name).ljust(15),
                cname=str(self.cname)
            )
1223

1224
    def save(self, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
1225 1226
        """ Empèche le save sans extension valide.
        Force à avoir appellé clean avant"""
1227 1228 1229
        if not self.get_extension():
            raise ValidationError("Extension invalide")
        self.full_clean()
1230 1231
        super(Domain, self).save(*args, **kwargs)

1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243
    @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()

1244 1245
    @staticmethod
    def can_create(user_request, interfaceid, *_args, **_kwargs):
1246 1247 1248 1249 1250
        """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"""
1251
        try:
1252
            interface = Interface.objects.get(pk=interfaceid)
1253 1254
        except Interface.DoesNotExist:
            return False, u"Interface inexistante"
1255
        if not user_request.has_perm('machines.add_domain'):
1256 1257 1258 1259
            max_lambdauser_aliases = (preferences.models.OptionalMachine
                                      .get_cached_value(
                                          'max_lambdauser_aliases'
                                      ))
1260
            if interface.machine.user != user_request:
1261 1262
                return False, (u"Vous ne pouvez pas ajouter un alias à une "