models.py 28.6 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 26 27 28 29 30 31 32 33 34 35 36
"""
Definition des modèles de l'application topologie.

On défini les models suivants :

- stack (id, id_min, id_max et nom) regrouppant les switches
- switch : nom, nombre de port, et interface
machine correspondante (mac, ip, etc) (voir machines.models.interface)
- Port: relié à un switch parent par foreign_key, numero du port,
relié de façon exclusive à un autre port, une machine
(serveur ou borne) ou une prise murale
- room : liste des prises murales, nom et commentaire de l'état de
la prise
"""
37

38 39
from __future__ import unicode_literals

40 41
import itertools

chirac's avatar
chirac committed
42
from django.db import models
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
43
from django.db.models.signals import post_save, post_delete
grisel-davy's avatar
grisel-davy committed
44
from django.utils.functional import cached_property
45
from django.dispatch import receiver
46
from django.core.exceptions import ValidationError
47 48
from django.db import IntegrityError
from django.db import transaction
Laouen Fernet's avatar
Laouen Fernet committed
49
from django.utils.translation import ugettext_lazy as _
50
from reversion import revisions as reversion
chirac's avatar
chirac committed
51

52 53 54 55 56
from preferences.models import (
    OptionalTopologie,
    RadiusKey,
    SwitchManagementCred
)
57
from machines.models import Machine, regen
58 59
from re2o.mixins import AclMixin, RevMixin

60

61
class Stack(AclMixin, RevMixin, models.Model):
62 63
    """Un objet stack. Regrouppe des switchs en foreign key
    ,contient une id de stack, un switch id min et max dans
64
    le stack"""
65 66 67 68

    name = models.CharField(max_length=32, blank=True, null=True)
    stack_id = models.CharField(max_length=32, unique=True)
    details = models.CharField(max_length=255, blank=True, null=True)
69 70
    member_id_min = models.PositiveIntegerField()
    member_id_max = models.PositiveIntegerField()
71

72 73
    class Meta:
        permissions = (
74
            ("view_stack", _("Can view a stack object")),
75
        )
76 77
        verbose_name = _("switches stack")
        verbose_name_plural = _("switches stacks")
78

79 80 81 82
    def __str__(self):
        return " ".join([self.name, self.stack_id])

    def save(self, *args, **kwargs):
83
        self.clean()
84 85 86 87 88
        if not self.name:
            self.name = self.stack_id
        super(Stack, self).save(*args, **kwargs)

    def clean(self):
89
        """ Verification que l'id_max < id_min"""
90
        if self.member_id_max < self.member_id_min:
91 92 93 94
            raise ValidationError(
                    {'member_id_max': _("The maximum ID is less than the"
                                        " minimum ID.")}
            )
95

chirac's avatar
chirac committed
96

97
class AccessPoint(AclMixin, Machine):
98
    """Define a wireless AP. Inherit from machines.interfaces
99

100 101 102
    Definition pour une borne wifi , hérite de machines.interfaces
    """

103
    location = models.CharField(
104
        max_length=255,
105
        help_text=_("Details about the AP's location"),
106 107 108 109 110 111
        blank=True,
        null=True
    )

    class Meta:
        permissions = (
112
            ("view_accesspoint", _("Can view an access point object")),
113
        )
114 115
        verbose_name = _("access point")
        verbose_name_plural = _("access points")
116

grisel-davy's avatar
grisel-davy committed
117 118 119 120 121 122 123 124 125 126 127 128 129
    def port(self):
        """Return the queryset of ports for this device"""
        return Port.objects.filter(
            machine_interface__machine=self
        )

    def switch(self):
        """Return the switch where this is plugged"""
        return Switch.objects.filter(
            ports__machine_interface__machine=self
        )

    def building(self):
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
130 131 132 133
        """
        Return the building of the AP/Server (building of the switchs
        connected to...)
        """
grisel-davy's avatar
grisel-davy committed
134 135 136 137 138 139 140 141 142 143 144
        return Building.objects.filter(
            switchbay__switch=self.switch()
        )

    @cached_property
    def short_name(self):
        return str(self.interface_set.first().domain.name)

    @classmethod
    def all_ap_in(cls, building_instance):
        """Get a building as argument, returns all ap of a building"""
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
145 146 147
        return cls.objects.filter(
            interface__port__switch__switchbay__building=building_instance
        )
grisel-davy's avatar
grisel-davy committed
148 149 150 151 152 153

    def __str__(self):
        return str(self.interface_set.first())


class Server(Machine):
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
154 155 156
    """
    Dummy class, to retrieve servers of a building, or get switch of a server
    """
grisel-davy's avatar
grisel-davy committed
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173

    class Meta:
        proxy = True

    def port(self):
        """Return the queryset of ports for this device"""
        return Port.objects.filter(
            machine_interface__machine=self
        )

    def switch(self):
        """Return the switch where this is plugged"""
        return Switch.objects.filter(
            ports__machine_interface__machine=self
        )

    def building(self):
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
174 175 176 177
        """
        Return the building of the AP/Server
        (building of the switchs connected to...)
        """
grisel-davy's avatar
grisel-davy committed
178 179 180 181 182 183 184 185 186 187 188
        return Building.objects.filter(
            switchbay__switch=self.switch()
        )

    @cached_property
    def short_name(self):
        return str(self.interface_set.first().domain.name)

    @classmethod
    def all_server_in(cls, building_instance):
        """Get a building as argument, returns all server of a building"""
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
189 190 191
        return cls.objects.filter(
            interface__port__switch__switchbay__building=building_instance
        ).exclude(accesspoint__isnull=False)
grisel-davy's avatar
grisel-davy committed
192

193 194 195
    def __str__(self):
        return str(self.interface_set.first())

196

197
class Switch(AclMixin, Machine):
198
    """ Definition d'un switch. Contient un nombre de ports (number),
199 200 201
    un emplacement (location), un stack parent (optionnel, stack)
    et un id de membre dans le stack (stack_member_id)
    relié en onetoone à une interface
202
    Pourquoi ne pas avoir fait hériter switch de interface ?
203 204
    Principalement par méconnaissance de la puissance de cette façon de faire.
    Ceci étant entendu, django crée en interne un onetoone, ce qui a un
205 206 207 208
    effet identique avec ce que l'on fait ici

    Validation au save que l'id du stack est bien dans le range id_min
    id_max de la stack parente"""
209

Gabriel Detraz's avatar
Gabriel Detraz committed
210
    number = models.PositiveIntegerField(
211
        help_text=_("Number of ports")
Gabriel Detraz's avatar
Gabriel Detraz committed
212
    )
213
    stack = models.ForeignKey(
214
        'topologie.Stack',
215 216 217
        blank=True,
        null=True,
        on_delete=models.SET_NULL
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
218
    )
Gabriel Detraz's avatar
Gabriel Detraz committed
219 220
    stack_member_id = models.PositiveIntegerField(
        blank=True,
221
        null=True
Gabriel Detraz's avatar
Gabriel Detraz committed
222
    )
223 224 225 226
    model = models.ForeignKey(
        'topologie.ModelSwitch',
        blank=True,
        null=True,
Gabriel Detraz's avatar
Gabriel Detraz committed
227
        on_delete=models.SET_NULL,
228
        help_text=_("Switch model")
229
    )
230 231 232 233
    switchbay = models.ForeignKey(
        'topologie.SwitchBay',
        blank=True,
        null=True,
Gabriel Detraz's avatar
Gabriel Detraz committed
234
        on_delete=models.SET_NULL,
235
    )
236 237 238 239 240 241 242
    radius_key = models.ForeignKey(
        'preferences.RadiusKey',
        blank=True,
        null=True,
        on_delete=models.PROTECT,
        help_text="Clef radius du switch"
    )
243 244 245 246 247 248 249
    management_creds = models.ForeignKey(
        'preferences.SwitchManagementCred',
        blank=True,
        null=True,
        on_delete=models.PROTECT,
        help_text="Identifiant de management de ce switch"
    )
250 251 252 253
    automatic_provision = models.BooleanField(
        default=False,
        help_text='Provision automatique de ce switch',
    )
254
  
255 256

    class Meta:
257
        unique_together = ('stack', 'stack_member_id')
258
        permissions = (
259
            ("view_switch", _("Can view a switch object")),
260
        )
261 262
        verbose_name = _("switch")
        verbose_name_plural = _("switches")
chirac's avatar
chirac committed
263

264
    def clean(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
265 266 267
        """ Verifie que l'id stack est dans le bon range
        Appelle également le clean de la classe parente"""
        super(Switch, self).clean()
268 269
        if self.stack is not None:
            if self.stack_member_id is not None:
270
                if (self.stack_member_id > self.stack.member_id_max) or\
chirac's avatar
chirac committed
271 272
                        (self.stack_member_id < self.stack.member_id_min):
                    raise ValidationError(
273 274 275
                        {'stack_member_id': _("The switch ID exceeds the"
                                              " limits allowed by the stack.")}
                        )
276
            else:
277 278 279 280
                raise ValidationError(
                        {'stack_member_id': _("The stack member ID can't be"
                                              " void.")}
                )
281

282
    def create_ports(self, begin, end):
283 284
        """ Crée les ports de begin à end si les valeurs données
        sont cohérentes. """
285
        if end < begin:
286 287
            raise ValidationError(_("The end port is less than the start"
                                    " port."))
288
        ports_to_create = range(begin, end + 1)
Gabriel Detraz's avatar
Gabriel Detraz committed
289 290 291 292
        existing_ports = Port.objects.filter(switch=self.switch).values_list('port', flat=True)
        non_existing_ports = list(set(ports_to_create) - set(existing_ports))

        if len(non_existing_ports) + existing_ports.count() > self.number:
293
            raise ValidationError(_("This switch can't have that many ports."))
Gabriel Detraz's avatar
Gabriel Detraz committed
294 295 296
        with transaction.atomic(), reversion.create_revision():
            reversion.set_comment(_("Creation"))
            Port.objects.bulk_create([Port(switch=self.switch, port=port_id) for port_id in non_existing_ports])
297

298
    def main_interface(self):
299 300 301
        """ Returns the 'main' interface of the switch
        It must the the management interface for that device"""
        switch_iptype = OptionalTopologie.get_cached_value('switchs_ip_type')
302
        if switch_iptype:
303
            return self.interface_set.filter(type__ip_type=switch_iptype).first()
304 305
        return self.interface_set.first()

Gabriel Detraz's avatar
Gabriel Detraz committed
306 307
    @cached_property
    def get_name(self):
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
308
        return self.name or getattr(self.main_interface(), 'domain', 'Unknown')
309 310 311

    @cached_property
    def get_radius_key(self):
312
        """Retourne l'objet de la clef radius de ce switch"""
313 314 315 316
        return self.radius_key or RadiusKey.objects.filter(default_switch=True).first()

    @cached_property
    def get_radius_key_value(self):
317
        """Retourne la valeur en str de la clef radius, none si il n'y en a pas"""
318 319 320 321 322
        if self.get_radius_key:
            return self.get_radius_key.radius_key
        else:
            return None

323 324 325 326 327 328 329 330 331 332 333 334 335
    @cached_property
    def get_management_cred(self):
        """Retourne l'objet des creds de managament de ce switch"""
        return self.management_creds or SwitchManagementCred.objects.filter(default_switch=True).first()

    @cached_property
    def get_management_cred_value(self):
        """Retourne un dict des creds de management du switch"""
        if self.get_management_cred:
            return {'id': self.get_management_cred.management_id, 'pass': self.get_management_cred.management_pass}
        else:
            return None

336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
    @cached_property
    def rest_enabled(self):
        return OptionalTopologie.get_cached_value('switchs_rest_management') or self.automatic_provision

    @cached_property
    def web_management_enabled(self):
        sw_management = OptionalTopologie.get_cached_value('switchs_web_management')
        sw_management_ssl = OptionalTopologie.get_cached_value('switchs_web_management_ssl')
        if sw_management_ssl:
            return "ssl"
        elif sw_management:
            return "plain"
        else:
            return self.automatic_provision

351 352
    @cached_property
    def ipv4(self):
353
        """Return the switch's management ipv4"""
354 355 356 357
        return str(self.main_interface().ipv4)

    @cached_property
    def ipv6(self):
358
        """Returne the switch's management ipv6"""
359
        return str(self.main_interface().ipv6().first())
Gabriel Detraz's avatar
Gabriel Detraz committed
360

361
    @cached_property
362 363 364
    def interfaces_subnet(self):
        """Return dict ip:subnet for all ip of the switch"""
        return dict((str(interface.ipv4), interface.type.ip_type.ip_set_full_info) for interface in self.interface_set.all())
365 366

    @cached_property
367 368 369
    def interfaces6_subnet(self):
        """Return dict ip6:subnet for all ipv6 of the switch"""
        return dict((str(interface.ipv6().first()), interface.type.ip_type.ip6_set_full_info) for interface in self.interface_set.all())
370

371 372 373 374 375 376 377 378 379 380 381
    @cached_property
    def list_modules(self):
        """Return modules of that switch, list of dict (rank, reference)"""
        modules = []
        if self.model.is_modular:
            if self.model.is_itself_module:
                modules.append((1, self.model.reference))
            for module_of_self in self.moduleonswitch_set.all():
                modules.append((module_of_self.slot, module_of_self.module.reference))
        return modules

382
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
383
        return str(self.get_name)
384

chirac's avatar
chirac committed
385

386
class ModelSwitch(AclMixin, RevMixin, models.Model):
387
    """Un modèle (au sens constructeur) de switch"""
388

389
    reference = models.CharField(max_length=255)
390 391 392 393 394
    commercial_name = models.CharField(
        max_length=255,
        null=True,
        blank=True
    )
395 396 397 398
    constructor = models.ForeignKey(
        'topologie.ConstructorSwitch',
        on_delete=models.PROTECT
    )
399 400 401 402 403
    firmware = models.CharField(
        max_length=255,
        null=True,
        blank=True
    )
404 405 406 407 408 409 410 411
    is_modular = models.BooleanField(
        default=False,
        help_text=_("Is this switch model modular"),
    ) 
    is_itself_module = models.BooleanField(
        default=False,
        help_text=_("Does the switch, itself, considered as a module"),
    ) 
412

413 414
    class Meta:
        permissions = (
415
            ("view_modelswitch", _("Can view a switch model object")),
416
        )
417 418
        verbose_name = _("switch model")
        verbose_name_plural = _("switch models")
419

420
    def __str__(self):
421 422 423 424
        if self.commercial_name:
            return str(self.constructor) + ' ' + str(self.commercial_name)
        else:
            return str(self.constructor) + ' ' + self.reference
425 426


427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467
class ModuleSwitch(AclMixin, RevMixin, models.Model):
    """A module of a switch"""
    reference = models.CharField(
        max_length=255,
        help_text=_("Reference of a module"),
        verbose_name=_("Module reference")   
    )
    comment = models.CharField(
        max_length=255,
        null=True,
        blank=True,
        help_text=_("Comment"),
        verbose_name=_("Comment")   
    )

    class Meta:
        permissions = (
            ("view_moduleswitch", _("Can view a module object")),
        )
        verbose_name = _("Module of a switch")


    def __str__(self):
        return str(self.reference)


class ModuleOnSwitch(AclMixin, RevMixin, models.Model):
    """Link beetween module and switch"""
    module = models.ForeignKey('ModuleSwitch', on_delete=models.CASCADE)
    switch = models.ForeignKey('Switch', on_delete=models.CASCADE)
    slot = models.CharField(
        max_length=15,
        help_text=_("Slot on switch"),
        verbose_name=_("Slot")   
    )

    class Meta:
        permissions = (
            ("view_moduleonswitch", _("Can view a moduleonswitch object")),
        )
        verbose_name = _("link between switchs and modules")
468 469 470 471
        unique_together = ['slot', 'switch']

    def __str__(self):
        return 'On slot ' + str(self.slot) + ' of ' + str(self.switch)
472 473


474
class ConstructorSwitch(AclMixin, RevMixin, models.Model):
475
    """Un constructeur de switch"""
476

477 478
    name = models.CharField(max_length=255)

479 480
    class Meta:
        permissions = (
481 482
            ("view_constructorswitch", _("Can view a switch constructor"
                                         " object")),
483
        )
484 485
        verbose_name = _("switch constructor")
        verbose_name_plural = ("switch constructors")
486

487
    def __str__(self):
488
        return self.name
489 490


491 492
class SwitchBay(AclMixin, RevMixin, models.Model):
    """Une baie de brassage"""
493

494 495 496 497 498 499 500 501
    name = models.CharField(max_length=255)
    building = models.ForeignKey(
        'Building',
        on_delete=models.PROTECT
    )
    info = models.CharField(
        max_length=255,
        blank=True,
502
        null=True
503 504 505 506
    )

    class Meta:
        permissions = (
507
            ("view_switchbay", _("Can view a switch bay object")),
508
        )
509 510
        verbose_name = _("switch bay")
        verbose_name_plural = _("switch bays")
511 512 513 514 515 516 517

    def __str__(self):
        return self.name


class Building(AclMixin, RevMixin, models.Model):
    """Un batiment"""
518

519 520 521 522
    name = models.CharField(max_length=255)

    class Meta:
        permissions = (
523
            ("view_building", _("Can view a building object")),
524
        )
525 526
        verbose_name = _("building")
        verbose_name_plural = _("buildings")
527

528 529 530 531
    def all_ap_in(self):
        """Returns all ap of the building"""
        return AccessPoint.all_ap_in(self)

532 533 534 535
    def __str__(self):
        return self.name


536
class Port(AclMixin, RevMixin, models.Model):
537
    """ Definition d'un port. Relié à un switch(foreign_key),
538 539 540 541
    un port peut etre relié de manière exclusive à :
    - une chambre (room)
    - une machine (serveur etc) (machine_interface)
    - un autre port (uplink) (related)
542
    Champs supplémentaires :
543
    - RADIUS (mode STRICT : connexion sur port uniquement si machine
544 545
    d'un adhérent à jour de cotisation et que la chambre est également à
    jour de cotisation
546 547 548 549
    mode COMMON : vérification uniquement du statut de la machine
    mode NO : accepte toute demande venant du port et place sur le vlan normal
    mode BLOQ : rejet de toute authentification
    - vlan_force : override la politique générale de placement vlan, permet
550
    de forcer un port sur un vlan particulier. S'additionne à la politique
551
    RADIUS"""
552

553 554 555 556 557
    switch = models.ForeignKey(
        'Switch',
        related_name="ports",
        on_delete=models.CASCADE
    )
558
    port = models.PositiveIntegerField()
559 560 561 562 563
    room = models.ForeignKey(
        'Room',
        on_delete=models.PROTECT,
        blank=True,
        null=True
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
564
    )
565 566 567 568 569
    machine_interface = models.ForeignKey(
        'machines.Interface',
        on_delete=models.SET_NULL,
        blank=True,
        null=True
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
570
    )
571 572 573 574 575
    related = models.OneToOneField(
        'self',
        null=True,
        blank=True,
        related_name='related_port'
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
576
    )
577
    custom_profile = models.ForeignKey(
578 579
        'PortProfile',
        on_delete=models.PROTECT,
580 581
        blank=True,
        null=True
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
582
    )
583 584
    state = models.BooleanField(
        default=True,
585
        help_text='Port state Active',
586
        verbose_name=_("Port state Active")
587
    )
Dalahro's avatar
Dalahro committed
588
    details = models.CharField(max_length=255, blank=True)
chirac's avatar
chirac committed
589 590

    class Meta:
591
        unique_together = ('switch', 'port')
592
        permissions = (
593
            ("view_port", _("Can view a port object")),
594
        )
595 596
        verbose_name = _("port")
        verbose_name_plural = _("ports")
597

598
    @cached_property
599 600 601 602 603 604 605 606 607 608 609 610
    def pretty_name(self):
        """More elaborated name for label on switch conf"""
        if self.related:
            return "Uplink : " + self.related.switch.short_name
        elif self.machine_interface:
            return "Machine : " + str(self.machine_interface.domain)
        elif self.room:
            return "Chambre : " + str(self.room)
        else:
            return "Inconnue"

    @cached_property
611
    def get_port_profile(self):
612
        """Return the config profil for this port
613
        :returns: the profile of self (port)"""
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
614 615
        def profile_or_nothing(profile):
            port_profile = PortProfile.objects.filter(
616
                profil_default=profile).first()
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
617 618
            if port_profile:
                return port_profile
619
            else:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
620
                nothing_profile, _created = PortProfile.objects.get_or_create(
detraz's avatar
detraz committed
621
                    profil_default='nothing',
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
622 623 624 625
                    name='nothing',
                    radius_type='NO'
                )
                return nothing_profile
626

627 628
        if self.custom_profile:
            return self.custom_profile
629
        elif self.related:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
630
            return profile_or_nothing('uplink')
631
        elif self.machine_interface:
632
            if hasattr(self.machine_interface.machine, 'accesspoint'):
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
633
                return profile_or_nothing('access_point')
634
            else:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
635
                return profile_or_nothing('asso_machine')
636
        elif self.room:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
637
            return profile_or_nothing('room')
638
        else:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
639
            return profile_or_nothing('nothing')
640

641 642 643 644 645 646 647 648 649
    @classmethod
    def get_instance(cls, portid, *_args, **kwargs):
        return (cls.objects
                .select_related('machine_interface__domain__extension')
                .select_related('machine_interface__machine__switch')
                .select_related('room')
                .select_related('related')
                .prefetch_related('switch__interface_set__domain__extension')
                .get(pk=portid))
650

651 652 653 654 655
    def make_port_related(self):
        """ Synchronise le port distant sur self"""
        related_port = self.related
        related_port.related = self
        related_port.save()
656

657 658 659 660 661 662
    def clean_port_related(self):
        """ Supprime la relation related sur self"""
        related_port = self.related_port
        related_port.related = None
        related_port.save()

663
    def clean(self):
664 665 666 667
        """ Verifie que un seul de chambre, interface_parent et related_port
        est rempli. Verifie que le related n'est pas le port lui-même....
        Verifie que le related n'est pas déjà occupé par une machine ou une
        chambre. Si ce n'est pas le cas, applique la relation related
668
        Si un port related point vers self, on nettoie la relation
669 670 671
        A priori pas d'autre solution que de faire ça à la main. A priori
        tout cela est dans un bloc transaction, donc pas de problème de
        cohérence"""
lhark's avatar
lhark committed
672 673
        if hasattr(self, 'switch'):
            if self.port > self.switch.number:
674
                raise ValidationError(
675
                    _("The port can't exist, its number is too great.")
676 677 678 679 680
                )
        if (self.room and self.machine_interface or
                self.room and self.related or
                self.machine_interface and self.related):
            raise ValidationError(
681
                _("Room, interface and related port are mutually exclusive.")
682
            )
683
        if self.related == self:
684
            raise ValidationError(_("A port can't be related to itself."))
685 686
        if self.related and not self.related.related:
            if self.related.machine_interface or self.related.room:
687
                raise ValidationError(
688 689
                    _("The related port is already used, please clear it"
                      " before creating the relation.")
690
                )
691
            else:
692
                self.make_port_related()
693
        elif hasattr(self, 'related_port'):
694
            self.clean_port_related()
chirac's avatar
chirac committed
695 696

    def __str__(self):
chirac's avatar
chirac committed
697
        return str(self.switch) + " - " + str(self.port)
chirac's avatar
chirac committed
698

chirac's avatar
chirac committed
699

700
class Room(AclMixin, RevMixin, models.Model):
701
    """Une chambre/local contenant une prise murale"""
702

lhark's avatar
lhark committed
703
    name = models.CharField(max_length=255, unique=True)
chirac's avatar
chirac committed
704
    details = models.CharField(max_length=255, blank=True)
chirac's avatar
chirac committed
705

706 707
    class Meta:
        ordering = ['name']
708
        permissions = (
709
            ("view_room", _("Can view a room object")),
710
        )
711 712
        verbose_name = _("room")
        verbose_name_plural = _("rooms")
713

chirac's avatar
chirac committed
714
    def __str__(self):
715
        return self.name
chirac's avatar
chirac committed
716

chirac's avatar
chirac committed
717

Gabriel Detraz's avatar
Gabriel Detraz committed
718
class PortProfile(AclMixin, RevMixin, models.Model):
Laouen Fernet's avatar
Laouen Fernet committed
719 720 721 722 723
    """Contains the information of the ports' configuration for a switch"""
    TYPES = (
        ('NO', 'NO'),
        ('802.1X', '802.1X'),
        ('MAC-radius', 'MAC-radius'),
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
724
    )
Laouen Fernet's avatar
Laouen Fernet committed
725 726 727
    MODES = (
        ('STRICT', 'STRICT'),
        ('COMMON', 'COMMON'),
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
728
    )
Gabriel Detraz's avatar
Gabriel Detraz committed
729 730 731 732 733 734 735 736 737
    SPEED = (
        ('10-half', '10-half'),
        ('100-half', '100-half'),
        ('10-full', '10-full'),
        ('100-full', '100-full'),
        ('1000-full', '1000-full'),
        ('auto', 'auto'),
        ('auto-10', 'auto-10'),
        ('auto-100', 'auto-100'),
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
738 739
    )
    PROFIL_DEFAULT = (
Gabriel Detraz's avatar
Gabriel Detraz committed
740
        ('room', 'room'),
741
        ('access_point', 'access_point'),
Gabriel Detraz's avatar
Gabriel Detraz committed
742 743
        ('uplink', 'uplink'),
        ('asso_machine', 'asso_machine'),
744
        ('nothing', 'nothing'),
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
745
    )
Laouen Fernet's avatar
Laouen Fernet committed
746
    name = models.CharField(max_length=255, verbose_name=_("Name"))
Gabriel Detraz's avatar
Gabriel Detraz committed
747 748 749 750 751 752
    profil_default = models.CharField(
        max_length=32,
        choices=PROFIL_DEFAULT,
        blank=True,
        null=True,
        unique=True,
753
        verbose_name=_("Default profile")
Gabriel Detraz's avatar
Gabriel Detraz committed
754
    )
Laouen Fernet's avatar
Laouen Fernet committed
755
    vlan_untagged = models.ForeignKey(
Gabriel Detraz's avatar
Gabriel Detraz committed
756 757 758 759 760 761
        'machines.Vlan',
        related_name='vlan_untagged',
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        verbose_name=_("VLAN untagged")
Laouen Fernet's avatar
Laouen Fernet committed
762 763
    )
    vlan_tagged = models.ManyToManyField(
Gabriel Detraz's avatar
Gabriel Detraz committed
764 765 766 767 768
        'machines.Vlan',
        related_name='vlan_tagged',
        blank=True,
        verbose_name=_("VLAN(s) tagged")
    )
Laouen Fernet's avatar
Laouen Fernet committed
769
    radius_type = models.CharField(
Gabriel Detraz's avatar
Gabriel Detraz committed
770 771
        max_length=32,
        choices=TYPES,
772 773
        help_text=_("Type of RADIUS authentication : inactive, MAC-address or"
                    " 802.1X"),
Gabriel Detraz's avatar
Gabriel Detraz committed
774
        verbose_name=_("RADIUS type")
Laouen Fernet's avatar
Laouen Fernet committed
775 776
    )
    radius_mode = models.CharField(
Gabriel Detraz's avatar
Gabriel Detraz committed
777 778 779
        max_length=32,
        choices=MODES,
        default='COMMON',
780 781
        help_text=_("In case of MAC-authentication : mode COMMON or STRICT on"
                    " this port"),
Gabriel Detraz's avatar
Gabriel Detraz committed
782 783 784 785 786 787
        verbose_name=_("RADIUS mode")
    )
    speed = models.CharField(
        max_length=32,
        choices=SPEED,
        default='auto',
788
        help_text=_("Port speed limit"),
Gabriel Detraz's avatar
Gabriel Detraz committed
789 790 791 792
    )
    mac_limit = models.IntegerField(
        null=True,
        blank=True,
793 794
        help_text=_("Limit of MAC-address on this port"),
        verbose_name=_("MAC limit")
Gabriel Detraz's avatar
Gabriel Detraz committed
795 796 797
    )
    flow_control = models.BooleanField(
        default=False,
798
        help_text=_("Flow control"),
Gabriel Detraz's avatar
Gabriel Detraz committed
799 800 801
    )
    dhcp_snooping = models.BooleanField(
        default=False,
802 803
        help_text=_("Protect against rogue DHCP"),
        verbose_name=_("DHCP snooping")
Gabriel Detraz's avatar
Gabriel Detraz committed
804 805 806
    )
    dhcpv6_snooping = models.BooleanField(
        default=False,
807 808
        help_text=_("Protect against rogue DHCPv6"),
        verbose_name=_("DHCPv6 snooping")
Gabriel Detraz's avatar
Gabriel Detraz committed
809 810 811
    )
    arp_protect = models.BooleanField(
        default=False,
812 813
        help_text=_("Check if IP adress is DHCP assigned"),
        verbose_name=_("ARP protection")
Laouen Fernet's avatar
Laouen Fernet committed
814
    )
Gabriel Detraz's avatar
Gabriel Detraz committed
815 816
    ra_guard = models.BooleanField(
        default=False,
817 818
        help_text=_("Protect against rogue RA"),
        verbose_name=_("RA guard")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
819
    )
Gabriel Detraz's avatar
Gabriel Detraz committed
820 821
    loop_protect = models.BooleanField(
        default=False,
822 823
        help_text=_("Protect against loop"),
        verbose_name=_("Loop protection")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
824
    )
Laouen Fernet's avatar
Laouen Fernet committed
825 826 827

    class Meta:
        permissions = (
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
828
            ("view_port_profile", _("Can view a port profile object")),
Laouen Fernet's avatar
Laouen Fernet committed
829
        )
830 831
        verbose_name = _("port profile")
        verbose_name_plural = _("port profiles")
Laouen Fernet's avatar
Laouen Fernet committed
832

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
833 834 835 836 837 838 839 840
    security_parameters_fields = [
        'loop_protect',
        'ra_guard',
        'arp_protect',
        'dhcpv6_snooping',
        'dhcp_snooping',
        'flow_control'
    ]
841 842 843

    @cached_property
    def security_parameters_enabled(self):
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
844 845 846 847 848
        return [
            parameter
            for parameter in self.security_parameters_fields
            if getattr(self, parameter)
        ]
849

850 851 852 853
    @cached_property
    def security_parameters_as_str(self):
        return ','.join(self.security_parameters_enabled)

Laouen Fernet's avatar
Laouen Fernet committed
854 855 856 857
    def __str__(self):
        return self.name


858
@receiver(post_save, sender=AccessPoint)
859
def ap_post_save(**_kwargs):
860 861
    """Regeneration des noms des bornes vers le controleur"""
    regen('unifi-ap-names')
grisel-davy's avatar
grisel-davy committed
862
    regen("graph_topo")
863

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
864

865
@receiver(post_delete, sender=AccessPoint)
866
def ap_post_delete(**_kwargs):
867 868
    """Regeneration des noms des bornes vers le controleur"""
    regen('unifi-ap-names')
grisel-davy's avatar
grisel-davy committed
869
    regen("graph_topo")
870

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
871

872
@receiver(post_delete, sender=Stack)
873
def stack_post_delete(**_kwargs):
874 875
    """Vide les id des switches membres d'une stack supprimée"""
    Switch.objects.filter(stack=None).update(stack_member_id=None)
grisel-davy's avatar
grisel-davy committed
876

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
877

grisel-davy's avatar
grisel-davy committed
878 879 880 881
@receiver(post_save, sender=Port)
def port_post_save(**_kwargs):
    regen("graph_topo")

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
882

grisel-davy's avatar
grisel-davy committed
883 884 885 886
@receiver(post_delete, sender=Port)
def port_post_delete(**_kwargs):
    regen("graph_topo")

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
887

grisel-davy's avatar
grisel-davy committed
888 889 890 891
@receiver(post_save, sender=ModelSwitch)
def modelswitch_post_save(**_kwargs):
    regen("graph_topo")

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
892

grisel-davy's avatar
grisel-davy committed
893 894 895 896
@receiver(post_delete, sender=ModelSwitch)
def modelswitch_post_delete(**_kwargs):
    regen("graph_topo")

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
897

grisel-davy's avatar
grisel-davy committed
898 899 900 901
@receiver(post_save, sender=Building)
def building_post_save(**_kwargs):
    regen("graph_topo")

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
902

grisel-davy's avatar
grisel-davy committed
903 904 905 906
@receiver(post_delete, sender=Building)
def building_post_delete(**_kwargs):
    regen("graph_topo")

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
907

grisel-davy's avatar
grisel-davy committed
908 909 910 911
@receiver(post_save, sender=Switch)
def switch_post_save(**_kwargs):
    regen("graph_topo")

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
912

grisel-davy's avatar
grisel-davy committed
913 914 915
@receiver(post_delete, sender=Switch)
def switch_post_delete(**_kwargs):
    regen("graph_topo")
916