From f0386df8094af3fb243e3f546c1b20529a9cbdf9 Mon Sep 17 00:00:00 2001 From: chirac Date: Wed, 1 Aug 2018 17:50:07 +0000 Subject: [PATCH] Merge branch 'switch_conf_json' into 'dev' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch conf json See merge request federez/re2o!180 (cherry picked from commit a02e03ac71fa056a012d2e0e5ad58f6d4bd4eae8) 7ab0d656 add model PortProfile adc5757f Merge branch 'switch_conf_json' of https://gitlab.federez.net/federez/re2o into switch_conf_json 983b5620 Refactor port_profil e7b49bd5 Pas de null sur manytomany 92f30fbe Ajout reglages sécurité + frontend d123ce92 Menu séparé pour les profils de ports 447919a2 Ajout et transfert des anciennes données vers le nouveau système de profil de ports e6b8c5c8 Finition, gestion du renvoie du profil par défaut du port b2d45d00 Adapte freeradius pour le nouveau système de profil de ports 51793bde Boolean direct pour désactiver un port + logo 2e092b3f Fix langue et 802.X radius + divers a07e0d92 Petit bug affichage vlans eefa0b4a PEP8 mon amour + typos feddc3f6 Conflicts fix switch_conf_json --- freeradius_utils/auth.py | 50 +++- search/views.py | 4 +- topologie/admin.py | 7 +- topologie/forms.py | 23 +- topologie/migrations/0061_portprofile.py | 44 ++++ .../migrations/0062_auto_20180627_0123.py | 25 ++ .../migrations/0063_port_custom_profil.py | 21 ++ topologie/migrations/0064_createprofil.py | 53 ++++ .../migrations/0065_auto_20180630_1703.py | 23 ++ .../migrations/0066_auto_20180630_1855.py | 25 ++ .../migrations/0067_auto_20180701_0016.py | 75 ++++++ topologie/models.py | 241 ++++++++++++++++-- topologie/templates/topologie/aff_port.html | 80 +++--- .../templates/topologie/aff_port_profile.html | 85 ++++++ topologie/templates/topologie/index.html | 1 - .../topologie/index_portprofile.html | 43 ++++ topologie/templates/topologie/sidebar.html | 6 +- topologie/urls.py | 12 + topologie/views.py | 202 +++++++++++---- 19 files changed, 888 insertions(+), 132 deletions(-) create mode 100644 topologie/migrations/0061_portprofile.py create mode 100644 topologie/migrations/0062_auto_20180627_0123.py create mode 100644 topologie/migrations/0063_port_custom_profil.py create mode 100644 topologie/migrations/0064_createprofil.py create mode 100644 topologie/migrations/0065_auto_20180630_1703.py create mode 100644 topologie/migrations/0066_auto_20180630_1855.py create mode 100644 topologie/migrations/0067_auto_20180701_0016.py create mode 100644 topologie/templates/topologie/aff_port_profile.html create mode 100644 topologie/templates/topologie/index_portprofile.html diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index fcf55cfa..d743495f 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -355,27 +355,47 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, port=port_number ) .first()) + # Si le port est inconnu, on place sur le vlan defaut + # Aucune information particulière ne permet de déterminer quelle + # politique à appliquer sur ce port if not port: return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK) - # Si un vlan a été précisé, on l'utilise pour VLAN_OK - if port.vlan_force: - DECISION_VLAN = int(port.vlan_force.vlan_id) + # On récupère le profil du port + port_profile = port.get_port_profile + + # Si un vlan a été précisé dans la config du port, + # on l'utilise pour VLAN_OK + if port_profile.vlan_untagged: + DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id) extra_log = u"Force sur vlan " + str(DECISION_VLAN) else: DECISION_VLAN = VLAN_OK - if port.radius == 'NO': + # Si le port est désactivé, on rejette sur le vlan de déconnexion + if not port.state: + return (sw_name, port.room, u'Port desactivé', VLAN_NOK) + + # Si radius est désactivé, on laisse passer + if port_profile.radius_type == 'NO': return (sw_name, "", u"Pas d'authentification sur ce port" + extra_log, DECISION_VLAN) - if port.radius == 'BLOQ': - return (sw_name, port.room, u'Port desactive', VLAN_NOK) + # Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment + # Par conséquent, on laisse passer sur le bon vlan + if nas_type.port_access_mode == '802.1X' and port_profile.radius_type == '802.1X': + room = port.room or "Chambre/local inconnu" + return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN) - if port.radius == 'STRICT': + # Sinon, cela veut dire qu'on fait de l'auth radius par mac + # Si le port est en mode strict, on vérifie que tous les users + # rattachés à ce port sont bien à jour de cotisation. Sinon on rejette (anti squattage) + # Il n'est pas possible de se connecter sur une prise strict sans adhérent à jour de cotis + # dedans + if port_profile.radius_mode == 'STRICT': room = port.room if not room: return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK) @@ -390,7 +410,8 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, return (sw_name, room, u'Chambre resident desactive', VLAN_NOK) # else: user OK, on passe à la verif MAC - if port.radius == 'COMMON' or port.radius == 'STRICT': + # Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd + if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT': # Authentification par mac interface = (Interface.objects .filter(mac_address=mac_address) @@ -399,15 +420,19 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, .first()) if not interface: room = port.room - # On essaye de register la mac + # On essaye de register la mac, si l'autocapture a été activée + # Sinon on rejette sur vlan_nok if not nas_type.autocapture_mac: return (sw_name, "", u'Machine inconnue', VLAN_NOK) + # On ne peut autocapturer que si on connait la chambre et donc l'user correspondant elif not room: return (sw_name, "Inconnue", u'Chambre et machine inconnues', VLAN_NOK) else: + # Si la chambre est vide (local club, prises en libre services) + # Impossible d'autocapturer if not room_user: room_user = User.objects.filter( Q(club__room=port.room) | Q(adherent__room=port.room) @@ -418,6 +443,8 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, u'Machine et propriétaire de la chambre ' 'inconnus', VLAN_NOK) + # Si il y a plus d'un user dans la chambre, impossible de savoir à qui + # Ajouter la machine elif room_user.count() > 1: return (sw_name, room, @@ -425,11 +452,13 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, 'dans la chambre/local -> ajout de mac ' 'automatique impossible', VLAN_NOK) + # Si l'adhérent de la chambre n'est pas à jour de cotis, pas d'autocapture elif not room_user.first().has_access(): return (sw_name, room, u'Machine inconnue et adhérent non cotisant', VLAN_NOK) + # Sinon on capture et on laisse passer sur le bon vlan else: result, reason = (room_user .first() @@ -449,6 +478,9 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, reason + str(mac_address) ), VLAN_NOK) + # L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject + # Si elle n'a pas d'ipv4, on lui en met une + # Enfin on laisse passer sur le vlan pertinent else: room = port.room if not interface.is_active: diff --git a/search/views.py b/search/views.py index 871515fa..73333834 100644 --- a/search/views.py +++ b/search/views.py @@ -262,9 +262,9 @@ def search_single_word(word, filters, user, ) | Q( related__switch__interface__domain__name__icontains=word ) | Q( - radius__icontains=word + custom_profile__name__icontains=word ) | Q( - vlan_force__name__icontains=word + custom_profile__profil_default__icontains=word ) | Q( details__icontains=word ) diff --git a/topologie/admin.py b/topologie/admin.py index 62ffd6c4..d2a25461 100644 --- a/topologie/admin.py +++ b/topologie/admin.py @@ -38,7 +38,8 @@ from .models import ( ConstructorSwitch, AccessPoint, SwitchBay, - Building + Building, + PortProfile, ) @@ -86,6 +87,9 @@ class BuildingAdmin(VersionAdmin): """Administration d'un batiment""" pass +class PortProfileAdmin(VersionAdmin): + """Administration of a port profile""" + pass admin.site.register(Port, PortAdmin) admin.site.register(AccessPoint, AccessPointAdmin) @@ -96,3 +100,4 @@ admin.site.register(ModelSwitch, ModelSwitchAdmin) admin.site.register(ConstructorSwitch, ConstructorSwitchAdmin) admin.site.register(Building, BuildingAdmin) admin.site.register(SwitchBay, SwitchBayAdmin) +admin.site.register(PortProfile, PortProfileAdmin) diff --git a/topologie/forms.py b/topologie/forms.py index 0cc7761b..86b5c541 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -35,6 +35,7 @@ from __future__ import unicode_literals from django import forms from django.forms import ModelForm from django.db.models import Prefetch +from django.utils.translation import ugettext_lazy as _ from machines.models import Interface from machines.forms import ( @@ -53,6 +54,7 @@ from .models import ( AccessPoint, SwitchBay, Building, + PortProfile, ) @@ -78,8 +80,8 @@ class EditPortForm(FormRevMixin, ModelForm): optimiser le temps de chargement avec select_related (vraiment lent sans)""" class Meta(PortForm.Meta): - fields = ['room', 'related', 'machine_interface', 'radius', - 'vlan_force', 'details'] + fields = ['room', 'related', 'machine_interface', 'custom_profile', + 'state', 'details'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) @@ -99,8 +101,8 @@ class AddPortForm(FormRevMixin, ModelForm): 'room', 'machine_interface', 'related', - 'radius', - 'vlan_force', + 'custom_profile', + 'state', 'details' ] @@ -254,3 +256,16 @@ class EditBuildingForm(FormRevMixin, ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditBuildingForm, self).__init__(*args, prefix=prefix, **kwargs) + +class EditPortProfileForm(FormRevMixin, ModelForm): + """Form to edit a port profile""" + class Meta: + model = PortProfile + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditPortProfileForm, self).__init__(*args, + prefix=prefix, + **kwargs) + diff --git a/topologie/migrations/0061_portprofile.py b/topologie/migrations/0061_portprofile.py new file mode 100644 index 00000000..7e130163 --- /dev/null +++ b/topologie/migrations/0061_portprofile.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-26 16:37 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0082_auto_20180525_2209'), + ('topologie', '0060_server'), + ] + + operations = [ + migrations.CreateModel( + name='PortProfile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('profil_default', models.CharField(blank=True, choices=[('room', 'room'), ('nothing', 'nothing'), ('accespoint', 'accesspoint'), ('uplink', 'uplink'), ('asso_machine', 'asso_machine')], max_length=32, null=True, unique=True, verbose_name='profil default')), + ('radius_type', models.CharField(choices=[('NO', 'NO'), ('802.1X', '802.1X'), ('MAC-radius', 'MAC-radius')], max_length=32, verbose_name='RADIUS type')), + ('radius_mode', models.CharField(choices=[('STRICT', 'STRICT'), ('COMMON', 'COMMON')], default='COMMON', max_length=32, verbose_name='RADIUS mode')), + ('speed', models.CharField(choices=[('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')], default='auto', help_text='Mode de transmission et vitesse du port', max_length=32, verbose_name='Speed')), + ('mac_limit', models.IntegerField(blank=True, help_text='Limit du nombre de mac sur le port', null=True, verbose_name='Mac limit')), + ('flow_control', models.BooleanField(default=False, help_text='Gestion des débits', verbose_name='Flow control')), + ('dhcp_snooping', models.BooleanField(default=False, help_text='Protection dhcp pirate', verbose_name='Dhcp snooping')), + ('dhcpv6_snooping', models.BooleanField(default=False, help_text='Protection dhcpv6 pirate', verbose_name='Dhcpv6 snooping')), + ('arp_protect', models.BooleanField(default=False, help_text="Verification assignation de l'IP par dhcp", verbose_name='Arp protect')), + ('ra_guard', models.BooleanField(default=False, help_text='Protection contre ra pirate', verbose_name='Ra guard')), + ('loop_protect', models.BooleanField(default=False, help_text='Protection contre les boucles', verbose_name='Loop Protect')), + ('vlan_tagged', models.ManyToManyField(blank=True, related_name='vlan_tagged', to='machines.Vlan', verbose_name='VLAN(s) tagged')), + ('vlan_untagged', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vlan_untagged', to='machines.Vlan', verbose_name='VLAN untagged')), + ], + options={ + 'verbose_name': 'Port profile', + 'permissions': (('view_port_profile', 'Can view a port profile object'),), + 'verbose_name_plural': 'Port profiles', + }, + bases=(re2o.mixins.AclMixin, re2o.mixins.RevMixin, models.Model), + ), + ] diff --git a/topologie/migrations/0062_auto_20180627_0123.py b/topologie/migrations/0062_auto_20180627_0123.py new file mode 100644 index 00000000..b8135de8 --- /dev/null +++ b/topologie/migrations/0062_auto_20180627_0123.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-26 23:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0061_portprofile'), + ] + + operations = [ + migrations.AlterField( + model_name='portprofile', + name='radius_mode', + field=models.CharField(choices=[('STRICT', 'STRICT'), ('COMMON', 'COMMON')], default='COMMON', help_text="En cas d'auth par mac, auth common ou strcit sur le port", max_length=32, verbose_name='RADIUS mode'), + ), + migrations.AlterField( + model_name='portprofile', + name='radius_type', + field=models.CharField(choices=[('NO', 'NO'), ('802.1X', '802.1X'), ('MAC-radius', 'MAC-radius')], help_text="Choix du type d'authentification radius : non actif, mac ou 802.1X", max_length=32, verbose_name='RADIUS type'), + ), + ] diff --git a/topologie/migrations/0063_port_custom_profil.py b/topologie/migrations/0063_port_custom_profil.py new file mode 100644 index 00000000..15feebce --- /dev/null +++ b/topologie/migrations/0063_port_custom_profil.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-28 07:49 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0062_auto_20180627_0123'), + ] + + operations = [ + migrations.AddField( + model_name='port', + name='custom_profil', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='topologie.PortProfile'), + ), + ] diff --git a/topologie/migrations/0064_createprofil.py b/topologie/migrations/0064_createprofil.py new file mode 100644 index 00000000..2f165386 --- /dev/null +++ b/topologie/migrations/0064_createprofil.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-31 19:53 +from __future__ import unicode_literals + +from django.db import migrations + + +def transfer_profil(apps, schema_editor): + db_alias = schema_editor.connection.alias + port = apps.get_model("topologie", "Port") + profil = apps.get_model("topologie", "PortProfile") + vlan = apps.get_model("machines", "Vlan") + port_list = port.objects.using(db_alias).all() + profil_nothing = profil.objects.using(db_alias).create(name='nothing', profil_default='nothing', radius_type='NO') + profil_uplink = profil.objects.using(db_alias).create(name='uplink', profil_default='uplink', radius_type='NO') + profil_machine = profil.objects.using(db_alias).create(name='asso_machine', profil_default='asso_machine', radius_type='NO') + profil_room = profil.objects.using(db_alias).create(name='room', profil_default='room', radius_type='NO') + profil_borne = profil.objects.using(db_alias).create(name='accesspoint', profil_default='accesspoint', radius_type='NO') + for vlan_instance in vlan.objects.using(db_alias).all(): + if port.objects.using(db_alias).filter(vlan_force=vlan_instance): + custom_profil = profil.objects.using(db_alias).create(name='vlan-force-' + str(vlan_instance.vlan_id), radius_type='NO', vlan_untagged=vlan_instance) + port.objects.using(db_alias).filter(vlan_force=vlan_instance).update(custom_profil=custom_profil) + if port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').count() and port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').count(): + profil_room.radius_type = 'MAC-radius' + profil_room.radius_mode = 'STRICT' + common_profil = profil.objects.using(db_alias).create(name='mac-radius-common', radius_type='MAC-radius', radius_mode='COMMON') + no_rad_profil = profil.objects.using(db_alias).create(name='no-radius', radius_type='NO') + port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').update(custom_profil=common_profil) + port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').update(custom_profil=no_rad_profil) + elif port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').count() and port.objects.using(db_alias).filter(room__isnull=False).filter(radius='COMMON').count() > port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').count(): + profil_room.radius_type = 'MAC-radius' + profil_room.radius_mode = 'COMMON' + strict_profil = profil.objects.using(db_alias).create(name='mac-radius-strict', radius_type='MAC-radius', radius_mode='STRICT') + no_rad_profil = profil.objects.using(db_alias).create(name='no-radius', radius_type='NO') + port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').update(custom_profil=strict_profil) + port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').update(custom_profil=no_rad_profil) + else: + strict_profil = profil.objects.using(db_alias).create(name='mac-radius-strict', radius_type='MAC-radius', radius_mode='STRICT') + common_profil = profil.objects.using(db_alias).create(name='mac-radius-common', radius_type='MAC-radius', radius_mode='COMMON') + port.objects.using(db_alias).filter(room__isnull=False).filter(radius='STRICT').update(custom_profil=strict_profil) + port.objects.using(db_alias).filter(room__isnull=False).filter(radius='NO').update(custom_profil=common_profil) + profil_room.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0063_port_custom_profil'), + ] + + operations = [ + migrations.RunPython(transfer_profil), + ] diff --git a/topologie/migrations/0065_auto_20180630_1703.py b/topologie/migrations/0065_auto_20180630_1703.py new file mode 100644 index 00000000..9fed2d83 --- /dev/null +++ b/topologie/migrations/0065_auto_20180630_1703.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-30 15:03 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0064_createprofil'), + ] + + operations = [ + migrations.RemoveField( + model_name='port', + name='radius', + ), + migrations.RemoveField( + model_name='port', + name='vlan_force', + ), + ] diff --git a/topologie/migrations/0066_auto_20180630_1855.py b/topologie/migrations/0066_auto_20180630_1855.py new file mode 100644 index 00000000..b197f568 --- /dev/null +++ b/topologie/migrations/0066_auto_20180630_1855.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-30 16:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0065_auto_20180630_1703'), + ] + + operations = [ + migrations.AddField( + model_name='port', + name='state', + field=models.BooleanField(default=True, help_text='Etat du port Actif', verbose_name='Etat du port Actif'), + ), + migrations.AlterField( + model_name='portprofile', + name='profil_default', + field=models.CharField(blank=True, choices=[('room', 'room'), ('accespoint', 'accesspoint'), ('uplink', 'uplink'), ('asso_machine', 'asso_machine'), ('nothing', 'nothing')], max_length=32, null=True, unique=True, verbose_name='profil default'), + ), + ] diff --git a/topologie/migrations/0067_auto_20180701_0016.py b/topologie/migrations/0067_auto_20180701_0016.py new file mode 100644 index 00000000..578ee7d6 --- /dev/null +++ b/topologie/migrations/0067_auto_20180701_0016.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-30 22:16 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0066_auto_20180630_1855'), + ] + + operations = [ + migrations.RenameField( + model_name='port', + old_name='custom_profil', + new_name='custom_profile', + ), + migrations.AlterField( + model_name='port', + name='state', + field=models.BooleanField(default=True, help_text='Port state Active', verbose_name='Port State Active'), + ), + migrations.AlterField( + model_name='portprofile', + name='arp_protect', + field=models.BooleanField(default=False, help_text='Check if ip is dhcp assigned', verbose_name='Arp protect'), + ), + migrations.AlterField( + model_name='portprofile', + name='dhcp_snooping', + field=models.BooleanField(default=False, help_text='Protect against rogue dhcp', verbose_name='Dhcp snooping'), + ), + migrations.AlterField( + model_name='portprofile', + name='dhcpv6_snooping', + field=models.BooleanField(default=False, help_text='Protect against rogue dhcpv6', verbose_name='Dhcpv6 snooping'), + ), + migrations.AlterField( + model_name='portprofile', + name='flow_control', + field=models.BooleanField(default=False, help_text='Flow control', verbose_name='Flow control'), + ), + migrations.AlterField( + model_name='portprofile', + name='loop_protect', + field=models.BooleanField(default=False, help_text='Protect again loop', verbose_name='Loop Protect'), + ), + migrations.AlterField( + model_name='portprofile', + name='mac_limit', + field=models.IntegerField(blank=True, help_text='Limit of mac-address on this port', null=True, verbose_name='Mac limit'), + ), + migrations.AlterField( + model_name='portprofile', + name='ra_guard', + field=models.BooleanField(default=False, help_text='Protect against rogue ra', verbose_name='Ra guard'), + ), + migrations.AlterField( + model_name='portprofile', + name='radius_mode', + field=models.CharField(choices=[('STRICT', 'STRICT'), ('COMMON', 'COMMON')], default='COMMON', help_text='In case of mac-auth : mode common or strict on this port', max_length=32, verbose_name='RADIUS mode'), + ), + migrations.AlterField( + model_name='portprofile', + name='radius_type', + field=models.CharField(choices=[('NO', 'NO'), ('802.1X', '802.1X'), ('MAC-radius', 'MAC-radius')], help_text='Type of radius auth : inactive, mac-address or 802.1X', max_length=32, verbose_name='RADIUS type'), + ), + migrations.AlterField( + model_name='portprofile', + name='speed', + field=models.CharField(choices=[('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')], default='auto', help_text='Port speed limit', max_length=32, verbose_name='Speed'), + ), + ] diff --git a/topologie/models.py b/topologie/models.py index 522b9673..a0333d46 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -40,22 +40,18 @@ from __future__ import unicode_literals import itertools from django.db import models -from django.db.models.signals import pre_save, post_save, post_delete +from django.db.models.signals import post_save, post_delete from django.utils.functional import cached_property from django.dispatch import receiver from django.core.exceptions import ValidationError from django.db import IntegrityError from django.db import transaction +from django.utils.translation import ugettext_lazy as _ from reversion import revisions as reversion from machines.models import Machine, regen from re2o.mixins import AclMixin, RevMixin -from os.path import isfile -from os import remove - - - class Stack(AclMixin, RevMixin, models.Model): """Un objet stack. Regrouppe des switchs en foreign key @@ -122,7 +118,10 @@ class AccessPoint(AclMixin, Machine): ) def building(self): - """Return the building of the AP/Server (building of the switchs connected to...)""" + """ + Return the building of the AP/Server (building of the switchs + connected to...) + """ return Building.objects.filter( switchbay__switch=self.switch() ) @@ -134,14 +133,18 @@ class AccessPoint(AclMixin, Machine): @classmethod def all_ap_in(cls, building_instance): """Get a building as argument, returns all ap of a building""" - return cls.objects.filter(interface__port__switch__switchbay__building=building_instance) + return cls.objects.filter( + interface__port__switch__switchbay__building=building_instance + ) def __str__(self): return str(self.interface_set.first()) class Server(Machine): - """Dummy class, to retrieve servers of a building, or get switch of a server""" + """ + Dummy class, to retrieve servers of a building, or get switch of a server + """ class Meta: proxy = True @@ -159,7 +162,10 @@ class Server(Machine): ) def building(self): - """Return the building of the AP/Server (building of the switchs connected to...)""" + """ + Return the building of the AP/Server + (building of the switchs connected to...) + """ return Building.objects.filter( switchbay__switch=self.switch() ) @@ -171,7 +177,9 @@ class Server(Machine): @classmethod def all_server_in(cls, building_instance): """Get a building as argument, returns all server of a building""" - return cls.objects.filter(interface__port__switch__switchbay__building=building_instance).exclude(accesspoint__isnull=False) + return cls.objects.filter( + interface__port__switch__switchbay__building=building_instance + ).exclude(accesspoint__isnull=False) def __str__(self): return str(self.interface_set.first()) @@ -199,7 +207,7 @@ class Switch(AclMixin, Machine): blank=True, null=True, on_delete=models.SET_NULL - ) + ) stack_member_id = models.PositiveIntegerField( blank=True, null=True, @@ -237,7 +245,7 @@ class Switch(AclMixin, Machine): raise ValidationError( {'stack_member_id': "L'id de ce switch est en\ dehors des bornes permises pas la stack"} - ) + ) else: raise ValidationError({'stack_member_id': "L'id dans la stack\ ne peut être nul"}) @@ -369,12 +377,6 @@ class Port(AclMixin, RevMixin, models.Model): de forcer un port sur un vlan particulier. S'additionne à la politique RADIUS""" PRETTY_NAME = "Port de switch" - STATES = ( - ('NO', 'NO'), - ('STRICT', 'STRICT'), - ('BLOQ', 'BLOQ'), - ('COMMON', 'COMMON'), - ) switch = models.ForeignKey( 'Switch', @@ -387,26 +389,30 @@ class Port(AclMixin, RevMixin, models.Model): on_delete=models.PROTECT, blank=True, null=True - ) + ) machine_interface = models.ForeignKey( 'machines.Interface', on_delete=models.SET_NULL, blank=True, null=True - ) + ) related = models.OneToOneField( 'self', null=True, blank=True, related_name='related_port' - ) - radius = models.CharField(max_length=32, choices=STATES, default='NO') - vlan_force = models.ForeignKey( - 'machines.Vlan', - on_delete=models.SET_NULL, + ) + custom_profile = models.ForeignKey( + 'PortProfile', + on_delete=models.PROTECT, blank=True, null=True - ) + ) + state = models.BooleanField( + default=True, + help_text='Port state Active', + verbose_name=_("Port State Active") + ) details = models.CharField(max_length=255, blank=True) class Meta: @@ -415,6 +421,37 @@ class Port(AclMixin, RevMixin, models.Model): ("view_port", "Peut voir un objet port"), ) + @cached_property + def get_port_profile(self): + """Return the config profile for this port + :returns: the profile of self (port)""" + def profile_or_nothing(profile): + port_profile = PortProfile.objects.filter( + profile_default=profile).first() + if port_profile: + return port_profile + else: + nothing_profile, _created = PortProfile.objects.get_or_create( + profile_default='nothing', + name='nothing', + radius_type='NO' + ) + return nothing_profile + + if self.custom_profile: + return self.custom_profile + elif self.related: + return profile_or_nothing('uplink') + elif self.machine_interface: + if hasattr(self.machine_interface.machine, 'accesspoint'): + return profile_or_nothing('access_point') + else: + return profile_or_nothing('asso_machine') + elif self.room: + return profile_or_nothing('room') + else: + return profile_or_nothing('nothing') + @classmethod def get_instance(cls, portid, *_args, **kwargs): return (cls.objects @@ -492,51 +529,201 @@ class Room(AclMixin, RevMixin, models.Model): return self.name +class PortProfile(AclMixin, RevMixin, models.Model): + """Contains the information of the ports' configuration for a switch""" + TYPES = ( + ('NO', 'NO'), + ('802.1X', '802.1X'), + ('MAC-radius', 'MAC-radius'), + ) + MODES = ( + ('STRICT', 'STRICT'), + ('COMMON', 'COMMON'), + ) + 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'), + ) + PROFIL_DEFAULT = ( + ('room', 'room'), + ('accespoint', 'accesspoint'), + ('uplink', 'uplink'), + ('asso_machine', 'asso_machine'), + ('nothing', 'nothing'), + ) + name = models.CharField(max_length=255, verbose_name=_("Name")) + profil_default = models.CharField( + max_length=32, + choices=PROFIL_DEFAULT, + blank=True, + null=True, + unique=True, + verbose_name=_("profil default") + ) + vlan_untagged = models.ForeignKey( + 'machines.Vlan', + related_name='vlan_untagged', + on_delete=models.SET_NULL, + blank=True, + null=True, + verbose_name=_("VLAN untagged") + ) + vlan_tagged = models.ManyToManyField( + 'machines.Vlan', + related_name='vlan_tagged', + blank=True, + verbose_name=_("VLAN(s) tagged") + ) + radius_type = models.CharField( + max_length=32, + choices=TYPES, + help_text="Type of radius auth : inactive, mac-address or 802.1X", + verbose_name=_("RADIUS type") + ) + radius_mode = models.CharField( + max_length=32, + choices=MODES, + default='COMMON', + help_text="In case of mac-auth : mode common or strict on this port", + verbose_name=_("RADIUS mode") + ) + speed = models.CharField( + max_length=32, + choices=SPEED, + default='auto', + help_text='Port speed limit', + verbose_name=_("Speed") + ) + mac_limit = models.IntegerField( + null=True, + blank=True, + help_text='Limit of mac-address on this port', + verbose_name=_("Mac limit") + ) + flow_control = models.BooleanField( + default=False, + help_text='Flow control', + verbose_name=_("Flow control") + ) + dhcp_snooping = models.BooleanField( + default=False, + help_text='Protect against rogue dhcp', + verbose_name=_("Dhcp snooping") + ) + dhcpv6_snooping = models.BooleanField( + default=False, + help_text='Protect against rogue dhcpv6', + verbose_name=_("Dhcpv6 snooping") + ) + arp_protect = models.BooleanField( + default=False, + help_text='Check if ip is dhcp assigned', + verbose_name=_("Arp protect") + ) + ra_guard = models.BooleanField( + default=False, + help_text='Protect against rogue ra', + verbose_name=_("Ra guard") + ) + loop_protect = models.BooleanField( + default=False, + help_text='Protect again loop', + verbose_name=_("Loop Protect") + ) + + class Meta: + permissions = ( + ("view_port_profile", _("Can view a port profile object")), + ) + verbose_name = _("Port profile") + verbose_name_plural = _("Port profiles") + + security_parameters_fields = [ + 'loop_protect', + 'ra_guard', + 'arp_protect', + 'dhcpv6_snooping', + 'dhcp_snooping', + 'flow_control' + ] + + @cached_property + def security_parameters_enabled(self): + return [ + parameter + for parameter in self.security_parameters_fields + if getattr(self, parameter) + ] + + @cached_property + def security_parameters_as_str(self): + return ','.join(self.security_parameters_enabled) + + def __str__(self): + return self.name + + @receiver(post_save, sender=AccessPoint) def ap_post_save(**_kwargs): """Regeneration des noms des bornes vers le controleur""" regen('unifi-ap-names') regen("graph_topo") + @receiver(post_delete, sender=AccessPoint) def ap_post_delete(**_kwargs): """Regeneration des noms des bornes vers le controleur""" regen('unifi-ap-names') regen("graph_topo") + @receiver(post_delete, sender=Stack) def stack_post_delete(**_kwargs): """Vide les id des switches membres d'une stack supprimée""" Switch.objects.filter(stack=None).update(stack_member_id=None) + @receiver(post_save, sender=Port) def port_post_save(**_kwargs): regen("graph_topo") + @receiver(post_delete, sender=Port) def port_post_delete(**_kwargs): regen("graph_topo") + @receiver(post_save, sender=ModelSwitch) def modelswitch_post_save(**_kwargs): regen("graph_topo") + @receiver(post_delete, sender=ModelSwitch) def modelswitch_post_delete(**_kwargs): regen("graph_topo") + @receiver(post_save, sender=Building) def building_post_save(**_kwargs): regen("graph_topo") + @receiver(post_delete, sender=Building) def building_post_delete(**_kwargs): regen("graph_topo") + @receiver(post_save, sender=Switch) def switch_post_save(**_kwargs): regen("graph_topo") + @receiver(post_delete, sender=Switch) def switch_post_delete(**_kwargs): regen("graph_topo") diff --git a/topologie/templates/topologie/aff_port.html b/topologie/templates/topologie/aff_port.html index f01269a8..44e6ec82 100644 --- a/topologie/templates/topologie/aff_port.html +++ b/topologie/templates/topologie/aff_port.html @@ -41,17 +41,26 @@ with this program; if not, write to the Free Software Foundation, Inc., {% for port in port_list %} - {{ port.port }} - - {% if port.room %}{{ port.room }}{% endif %} - - - {% if port.machine_interface %} - {% can_view port.machine_interface.machine.user %} - - {{ port.machine_interface }} - - {% acl_else %} + {% include "buttons/sort.html" with prefix='port' col='port' text='Port' %} + {% include "buttons/sort.html" with prefix='port' col='room' text='Room' %} + {% include "buttons/sort.html" with prefix='port' col='interface' text='Interface machine' %} + {% include "buttons/sort.html" with prefix='port' col='related' text='Related' %} + Etat du port + Profil du port + Détails + + + + {% for port in port_list %} + + {{ port.port }} + + {% if port.room %}{{ port.room }}{% endif %} + + + {% if port.machine_interface %} + {% can_view port.machine_interface.machine.user %} + {{ port.machine_interface }} {% acl_end %} {% endif %} @@ -64,26 +73,31 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_else %} {{ port.related }} - {% acl_end %} - {% endif %} - - {{ port.radius }} - {% if not port.vlan_force %}Aucun{% else %}{{ port.vlan_force }}{% endif %} - {{ port.details }} - - {% history_button port %} - {% can_edit port %} - - - - {% acl_end %} - {% can_delete port %} - - - - {% acl_end %} - - - {% endfor %} - + + {% acl_else %} + {{ port.related }} + {% acl_end %} + {% endif %} + + {% if port.state %} Actif{% else %}Désactivé{% endif %} + {% if not port.custom_profile %}Par défaut : {% endif %}{{port.get_port_profil}} + {{ port.details }} + + + + + {% can_edit port %} + + + + {% acl_end %} + {% can_delete port %} + + + + {% acl_end %} + + + {% endfor %} + diff --git a/topologie/templates/topologie/aff_port_profile.html b/topologie/templates/topologie/aff_port_profile.html new file mode 100644 index 00000000..65446236 --- /dev/null +++ b/topologie/templates/topologie/aff_port_profile.html @@ -0,0 +1,85 @@ +{% comment %} +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 © 2018 Gabriel Détraz + +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. +{% endcomment %} + +{% load acl %} +{% load i18n %} + +
+ +{% if port_profile_list.paginator %} +{% include "pagination.html" with list=port_profile_list %} +{% endif %} + + + + + + + + + + + + + + + + {% for port_profile in port_profile_list %} + + + + + + {% endif %} + + + + + + {% endfor %} +
{% trans "Name" %}{% trans "Default for" %}{% trans "VLANs" %}{% trans "RADIUS settings" %}{% trans "Speed" %}{% trans "Mac address limit" %}{% trans "Security" %}
{{port_profile.name}}{{port_profile.profil_default}} + {% if port_profile.vlan_untagged %} + Untagged : {{port_profile.vlan_untagged}} +
+ {% endif %} + {% if port_profile.vlan_tagged.all %} + Tagged : {{port_profile.vlan_tagged.all|join:", "}} + {% endif %} +
+ Type : {{port_profile.radius_type}} + {% if port_profile.radius_type == "MAC-radius" %} +
+ Mode : {{port_profile.radius_mode}}
{{port_profile.speed}}{{port_profile.mac_limit}}{{port_profile.security_parameters_enabled|join:"
"}}
+ {% include 'buttons/history.html' with href='topologie:history' name='portprofile' id=port_profile.pk %} + {% can_edit port_profile %} + {% include 'buttons/edit.html' with href='topologie:edit-port-profile' id=port_profile.pk %} + {% acl_end %} + {% can_delete port_profile %} + {% include 'buttons/suppr.html' with href='topologie:del-port-profile' id=port_profile.pk %} + {% acl_end %} +
+ +{% if port_profile_list.paginator %} +{% include "pagination.html" with list=port_profile_list %} +{% endif %} + +
diff --git a/topologie/templates/topologie/index.html b/topologie/templates/topologie/index.html index a7c4bb51..f596e6a5 100644 --- a/topologie/templates/topologie/index.html +++ b/topologie/templates/topologie/index.html @@ -72,5 +72,4 @@ Topologie des Switchs

- {% endblock %} diff --git a/topologie/templates/topologie/index_portprofile.html b/topologie/templates/topologie/index_portprofile.html new file mode 100644 index 00000000..f95415c8 --- /dev/null +++ b/topologie/templates/topologie/index_portprofile.html @@ -0,0 +1,43 @@ +{% extends "topologie/sidebar.html" %} +{% comment %} +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 © 2018 Gabriel Détraz + + +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. +{% endcomment %} + +{% load bootstrap3 %} +{% load acl %} +{% load i18n %} + +{% block title %}Switchs{% endblock %} + +{% block content %} + +

{% trans "Port profiles" %}

+{% can_create PortProfile %} +{% trans " Add a port profile" %} +
+{% acl_end %} +{% include "topologie/aff_port_profile.html" with port_profile_list=port_profile_list %} +
+
+
+ +{% endblock %} diff --git a/topologie/templates/topologie/sidebar.html b/topologie/templates/topologie/sidebar.html index ce7b4114..04ee5202 100644 --- a/topologie/templates/topologie/sidebar.html +++ b/topologie/templates/topologie/sidebar.html @@ -33,7 +33,11 @@ with this program; if not, write to the Free Software Foundation, Inc., Switchs - + + + Config des ports switchs + + Bornes WiFi diff --git a/topologie/urls.py b/topologie/urls.py index e4b3225c..c314c800 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -108,4 +108,16 @@ urlpatterns = [ url(r'^del_building/(?P[0-9]+)$', views.del_building, name='del-building'), + url(r'^index_port_profile/$', + views.index_port_profile, + name='index-port-profile'), + url(r'^new_port_profile/$', + views.new_port_profile, + name='new-port-profile'), + url(r'^edit_port_profile/(?P[0-9]+)$', + views.edit_port_profile, + name='edit-port-profile'), + url(r'^del_port_profile/(?P[0-9]+)$', + views.del_port_profile, + name='del-port-profile'), ] diff --git a/topologie/views.py b/topologie/views.py index 3945abc5..a6c6ab69 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -42,11 +42,8 @@ from django.contrib.auth.decorators import login_required from django.db import IntegrityError from django.db.models import ProtectedError, Prefetch from django.core.exceptions import ValidationError -from django.contrib.staticfiles.storage import staticfiles_storage -from django.template.loader import get_template from django.template import Context, Template, loader -from django.db.models.signals import post_save -from django.dispatch import receiver +from django.utils.translation import ugettext as _ import tempfile @@ -80,6 +77,7 @@ from .models import ( SwitchBay, Building, Server, + PortProfile, ) from .forms import ( EditPortForm, @@ -94,7 +92,8 @@ from .forms import ( AddAccessPointForm, EditAccessPointForm, EditSwitchBayForm, - EditBuildingForm + EditBuildingForm, + EditPortProfileForm, ) from subprocess import ( @@ -102,8 +101,7 @@ from subprocess import ( PIPE ) -from os.path import isfile -from os import remove +from os.path import isfile @login_required @@ -124,12 +122,18 @@ def index(request): request.GET.get('order'), SortTable.TOPOLOGIE_INDEX ) + pagination_number = GeneralOption.get_cached_value('pagination_number') switch_list = re2o_paginator(request, switch_list, pagination_number) - if any(service_link.need_regen() for service_link in Service_link.objects.filter(service__service_type='graph_topo')): + if any( + service_link.need_regen + for service_link in Service_link.objects.filter( + service__service_type='graph_topo') + ): make_machine_graph() - for service_link in Service_link.objects.filter(service__service_type='graph_topo'): + for service_link in Service_link.objects.filter( + service__service_type='graph_topo'): service_link.done_regen() if not isfile("/var/www/re2o/media/images/switchs.png"): @@ -141,6 +145,21 @@ def index(request): ) +@login_required +@can_view_all(PortProfile) +def index_port_profile(request): + pagination_number = GeneralOption.get_cached_value('pagination_number') + port_profile_list = PortProfile.objects.all().select_related( + 'vlan_untagged') + port_profile_list = re2o_paginator( + request, port_profile_list, pagination_number) + return render( + request, + 'topologie/index_portprofile.html', + {'port_profile_list': port_profile_list} + ) + + @login_required @can_view_all(Port) @can_view(Switch) @@ -442,7 +461,7 @@ def new_switch(request): ) domain = DomainForm( request.POST or None, - ) + ) if switch.is_valid() and interface.is_valid(): user = AssoOption.get_cached_value('utilisateur_asso') if not user: @@ -512,7 +531,7 @@ def create_ports(request, switchid): return redirect(reverse( 'topologie:index-port', kwargs={'switchid': switchid} - )) + )) return form( {'id_switch': switchid, 'topoform': port_form}, 'topologie/switch.html', @@ -530,16 +549,16 @@ def edit_switch(request, switch, switchid): request.POST or None, instance=switch, user=request.user - ) + ) interface_form = EditInterfaceForm( request.POST or None, instance=switch.interface_set.first(), user=request.user - ) + ) domain_form = DomainForm( request.POST or None, instance=switch.interface_set.first().domain - ) + ) if switch_form.is_valid() and interface_form.is_valid(): new_switch_obj = switch_form.save(commit=False) new_interface_obj = interface_form.save(commit=False) @@ -583,7 +602,7 @@ def new_ap(request): ) domain = DomainForm( request.POST or None, - ) + ) if ap.is_valid() and interface.is_valid(): user = AssoOption.get_cached_value('utilisateur_asso') if not user: @@ -638,7 +657,7 @@ def edit_ap(request, ap, **_kwargs): domain_form = DomainForm( request.POST or None, instance=ap.interface_set.first().domain - ) + ) if ap_form.is_valid() and interface_form.is_valid(): user = AssoOption.get_cached_value('utilisateur_asso') if not user: @@ -952,7 +971,61 @@ def del_constructor_switch(request, constructor_switch, **_kwargs): return form({ 'objet': constructor_switch, 'objet_name': 'Constructeur de switch' - }, 'topologie/delete.html', request) + }, 'topologie/delete.html', request) + + +@login_required +@can_create(PortProfile) +def new_port_profile(request): + """Create a new port profile""" + port_profile = EditPortProfileForm(request.POST or None) + if port_profile.is_valid(): + port_profile.save() + messages.success(request, _("Port profile created")) + return redirect(reverse('topologie:index')) + return form( + {'topoform': port_profile, 'action_name': _("Create")}, + 'topologie/topo.html', + request + ) + + +@login_required +@can_edit(PortProfile) +def edit_port_profile(request, port_profile, **_kwargs): + """Edit a port profile""" + port_profile = EditPortProfileForm( + request.POST or None, instance=port_profile) + if port_profile.is_valid(): + if port_profile.changed_data: + port_profile.save() + messages.success(request, _("Port profile modified")) + return redirect(reverse('topologie:index')) + return form( + {'topoform': port_profile, 'action_name': _("Edit")}, + 'topologie/topo.html', + request + ) + + +@login_required +@can_delete(PortProfile) +def del_port_profile(request, port_profile, **_kwargs): + """Delete a port profile""" + if request.method == 'POST': + try: + port_profile.delete() + messages.success(request, + _("The port profile was successfully deleted")) + except ProtectedError: + messages.success(request, + _("Impossible to delete the port profile")) + return redirect(reverse('topologie:index')) + return form( + {'objet': port_profile, 'objet_name': _("Port profile")}, + 'topologie/delete.html', + request + ) def make_machine_graph(): @@ -961,7 +1034,7 @@ def make_machine_graph(): """ dico = { 'subs': [], - 'links' : [], + 'links': [], 'alone': [], 'colors': { 'head': "#7f0505", # Color parameters for the graph @@ -970,23 +1043,23 @@ def make_machine_graph(): 'border_bornes': "#02078e", 'head_bornes': "#25771c", 'head_server': "#1c3777" - } } + } missing = list(Switch.objects.all()) detected = [] for building in Building.objects.all(): # Visit all buildings dico['subs'].append( { - 'bat_id': building.id, - 'bat_name': building, - 'switchs': [], - 'bornes': [], - 'machines': [] + 'bat_id': building.id, + 'bat_name': building, + 'switchs': [], + 'bornes': [], + 'machines': [] } ) # Visit all switchs in this building - for switch in Switch.objects.filter(switchbay__building=building): + for switch in Switch.objects.filter(switchbay__building=building): dico['subs'][-1]['switchs'].append({ 'name': switch.main_interface().domain.name, 'nombre': switch.number, @@ -996,7 +1069,7 @@ def make_machine_graph(): 'ports': [] }) # visit all ports of this switch and add the switchs linked to it - for port in switch.ports.filter(related__isnull=False): + for port in switch.ports.filter(related__isnull=False): dico['subs'][-1]['switchs'][-1]['ports'].append({ 'numero': port.port, 'related': port.related.switch.main_interface().domain.name @@ -1014,50 +1087,58 @@ def make_machine_graph(): dico['subs'][-1]['machines'].append({ 'name': server.short_name, 'switch': server.switch()[0].main_interface().domain.name, - 'port': Port.objects.filter(machine_interface__machine=server)[0].port + 'port': Port.objects.filter( + machine_interface__machine=server + )[0].port }) # While the list of forgotten ones is not empty while missing: if missing[0].ports.count(): # The switch is not empty - links, new_detected = recursive_switchs(missing[0], None, [missing[0]]) + links, new_detected = recursive_switchs( + missing[0], None, [missing[0]]) for link in links: dico['links'].append(link) # Update the lists of missings and already detected switchs - missing=[i for i in missing if i not in new_detected] + missing = [i for i in missing if i not in new_detected] detected += new_detected - else: # If the switch have no ports, don't explore it and hop to the next one + # If the switch have no ports, don't explore it and hop to the next one + else: del missing[0] # Switchs that are not connected or not in a building - for switch in Switch.objects.filter(switchbay__isnull=True).exclude(ports__related__isnull=False): + for switch in Switch.objects.filter( + switchbay__isnull=True).exclude(ports__related__isnull=False): dico['alone'].append({ 'id': switch.id, 'name': switch.main_interface().domain.name - }) + }) + # generate the dot file + dot_data = generate_dot(dico, 'topologie/graph_switch.dot') - dot_data=generate_dot(dico,'topologie/graph_switch.dot') # generate the dot file - - f = tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8', delete=False) # Create a temporary file to store the dot data + # Create a temporary file to store the dot data + f = tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8', delete=False) with f: - f.write(dot_data) + f.write(dot_data) unflatten = Popen( # unflatten the graph to make it look better - ["unflatten","-l", "3", f.name], + ["unflatten", "-l", "3", f.name], stdout=PIPE ) - image = Popen( # pipe the result of the first command into the second + Popen( # pipe the result of the first command into the second ["dot", "-Tpng", "-o", MEDIA_ROOT + "/images/switchs.png"], stdin=unflatten.stdout, stdout=PIPE ) -def generate_dot(data,template): + +def generate_dot(data, template): """create the dot file :param data: dictionary passed to the template :param template: path to the dot template :return: all the lines of the dot file""" t = loader.get_template(template) - if not isinstance(t, Template) and not (hasattr(t, 'template') and isinstance(t.template, Template)): + if not isinstance(t, Template) and \ + not (hasattr(t, 'template') and isinstance(t.template, Template)): raise Exception("Le template par défaut de Django n'est pas utilisé." "Cela peut mener à des erreurs de rendu." "Vérifiez les paramètres") @@ -1065,27 +1146,40 @@ def generate_dot(data,template): dot = t.render(c) return(dot) + def recursive_switchs(switch_start, switch_before, detected): """Visit the switch and travel to the switchs linked to it. :param switch_start: the switch to begin the visit on - :param switch_before: the switch that you come from. None if switch_start is the first one - :param detected: list of all switchs already visited. None if switch_start is the first one - :return: A list of all the links found and a list of all the switchs visited""" + :param switch_before: the switch that you come from. + None if switch_start is the first one + :param detected: list of all switchs already visited. + None if switch_start is the first one + :return: A list of all the links found and a list of + all the switchs visited + """ detected.append(switch_start) - links_return=[] # list of dictionaries of the links to be detected - for port in switch_start.ports.filter(related__isnull=False): # create links to every switchs below - if port.related.switch != switch_before and port.related.switch != port.switch and port.related.switch not in detected: # Not the switch that we come from, not the current switch + links_return = [] # list of dictionaries of the links to be detected + # create links to every switchs below + for port in switch_start.ports.filter(related__isnull=False): + # Not the switch that we come from, not the current switch + if port.related.switch != switch_before \ + and port.related.switch != port.switch \ + and port.related.switch not in detected: links = { # Dictionary of a link - 'depart':switch_start.id, - 'arrive':port.related.switch.id + 'depart': switch_start.id, + 'arrive': port.related.switch.id } links_return.append(links) # Add current and below levels links - for port in switch_start.ports.filter(related__isnull=False): # go down on every related switchs - if port.related.switch not in detected: # The switch at the end of this link has not been visited - links_down, detected = recursive_switchs(port.related.switch, switch_start, detected) # explore it and get the results - for link in links_down: # Add the non empty links to the current list + # go down on every related switchs + for port in switch_start.ports.filter(related__isnull=False): + # The switch at the end of this link has not been visited + if port.related.switch not in detected: + # explore it and get the results + links_down, detected = recursive_switchs( + port.related.switch, switch_start, detected) + # Add the non empty links to the current list + for link in links_down: if link: - links_return.append(link) + links_return.append(link) return (links_return, detected) -