models.py 28.7 KB
Newer Older
1
# -*- mode: python; coding: utf-8 -*-
lhark's avatar
lhark committed
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
"""
lhark's avatar
lhark committed
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
guimoz's avatar
guimoz committed
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

Maël Kervella's avatar
Maël Kervella committed
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
Gabriel Detraz's avatar
Gabriel Detraz committed
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):
Gabriel Detraz's avatar
Gabriel Detraz committed
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):
Gabriel Detraz's avatar
Gabriel Detraz committed
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):
Gabriel Detraz's avatar
Gabriel Detraz committed
98
    """Define a wireless AP. Inherit from machines.interfaces
Maël Kervella's avatar
Maël Kervella committed
99

Gabriel Detraz's avatar
Gabriel Detraz committed
100
101
102
    Definition pour une borne wifi , hérite de machines.interfaces
    """

Maël Kervella's avatar
Maël Kervella committed
103
    location = models.CharField(
Gabriel Detraz's avatar
Gabriel Detraz committed
104
        max_length=255,
105
        help_text=_("Details about the AP's location"),
Gabriel Detraz's avatar
Gabriel Detraz committed
106
107
108
109
110
111
        blank=True,
        null=True
    )

    class Meta:
        permissions = (
112
            ("view_accesspoint", _("Can view an access point object")),
Gabriel Detraz's avatar
Gabriel Detraz committed
113
        )
114
115
        verbose_name = _("access point")
        verbose_name_plural = _("access points")
Gabriel Detraz's avatar
Gabriel Detraz committed
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())

Gabriel Detraz's avatar
Gabriel Detraz committed
196

197
class Switch(AclMixin, Machine):
198
    """ Definition d'un switch. Contient un nombre de ports (number),
Gabriel Detraz's avatar
Gabriel Detraz committed
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 ?
Gabriel Detraz's avatar
Gabriel Detraz committed
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.")}
                )
Gabriel Detraz's avatar
Gabriel Detraz committed
281

282
    def create_ports(self, begin, end):
Maël Kervella's avatar
Maël Kervella committed
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

Gabriel Detraz's avatar
Gabriel Detraz committed
371
372
373
374
    @cached_property
    def list_modules(self):
        """Return modules of that switch, list of dict (rank, reference)"""
        modules = []
375
        if getattr(self.model, 'is_modular', None):
Gabriel Detraz's avatar
Gabriel Detraz committed
376
377
378
379
380
381
            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

Gabriel Detraz's avatar
Gabriel Detraz committed
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),
Gabriel Detraz's avatar
Gabriel Detraz committed
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 :
Gabriel Detraz's avatar
Gabriel Detraz committed
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
Gabriel Detraz's avatar
Gabriel Detraz committed
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
Gabriel Detraz's avatar
Gabriel Detraz committed
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
    )
chirac's avatar
chirac committed
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,
chirac's avatar
chirac committed
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
chirac's avatar
chirac committed
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

chirac's avatar
chirac committed
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

Gabriel Detraz's avatar
Gabriel Detraz committed
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

Gabriel Detraz's avatar
Gabriel Detraz committed
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
Gabriel Detraz's avatar
Gabriel Detraz committed
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:
Gabriel Detraz's avatar
Gabriel Detraz committed
692
                self.make_port_related()
693
        elif hasattr(self, 'related_port'):
Gabriel Detraz's avatar
Gabriel Detraz committed
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'
    ]
chirac's avatar
chirac committed
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)
        ]
chirac's avatar
chirac committed
849

chirac's avatar
chirac committed
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

guimoz's avatar
guimoz committed
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