diff --git a/api/serializers.py b/api/serializers.py index 65a82eb6..94144b40 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -153,7 +153,8 @@ class VlanSerializer(NamespacedHMSerializer): """ class Meta: model = machines.Vlan - fields = ('vlan_id', 'name', 'comment', 'api_url') + fields = ('vlan_id', 'name', 'comment', 'arp_protect', 'dhcp_snooping', + 'dhcpv6_snooping', 'igmp', 'mld', 'api_url') class NasSerializer(NamespacedHMSerializer): @@ -310,6 +311,16 @@ class OuverturePortSerializer(NamespacedHMSerializer): fields = ('begin', 'end', 'port_list', 'protocole', 'io', 'api_url') +class RoleSerializer(NamespacedHMSerializer): + """Serialize `machines.models.OuverturePort` objects. + """ + servers = InterfaceSerializer(read_only=True, many=True) + + class Meta: + model = machines.Role + fields = ('role_type', 'servers', 'api_url') + + # PREFERENCES @@ -338,11 +349,15 @@ class OptionalMachineSerializer(NamespacedHMSerializer): class OptionalTopologieSerializer(NamespacedHMSerializer): """Serialize `preferences.models.OptionalTopologie` objects. """ + switchs_management_interface_ip = serializers.CharField() class Meta: model = preferences.OptionalTopologie fields = ('radius_general_policy', 'vlan_decision_ok', - 'vlan_decision_nok') + 'vlan_decision_nok', 'switchs_ip_type', 'switchs_web_management', + 'switchs_web_management_ssl', 'switchs_rest_management', + 'switchs_management_utils', 'switchs_management_interface_ip', + 'provision_switchs_enabled', 'switchs_provision', 'switchs_management_sftp_creds') class GeneralOptionSerializer(NamespacedHMSerializer): @@ -467,16 +482,30 @@ class BuildingSerializer(NamespacedHMSerializer): class SwitchPortSerializer(NamespacedHMSerializer): """Serialize `topologie.models.Port` objects """ + + get_port_profil = NamespacedHIField(view_name='portprofile-detail', read_only=True) + class Meta: model = topologie.Port fields = ('switch', 'port', 'room', 'machine_interface', 'related', - 'custom_profile', 'state', 'details', 'api_url') + 'custom_profile', 'state', 'get_port_profil', 'details', 'api_url') extra_kwargs = { 'related': {'view_name': 'switchport-detail'}, 'api_url': {'view_name': 'switchport-detail'}, } +class PortProfileSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.Room` objects + """ + class Meta: + model = topologie.PortProfile + fields = ('name', 'profil_default', 'vlan_untagged', 'vlan_tagged', + 'radius_type', 'radius_mode', 'speed', 'mac_limit', 'flow_control', + 'dhcp_snooping', 'dhcpv6_snooping', 'dhcpv6_snooping', 'arp_protect', + 'ra_guard', 'loop_protect', 'api_url') + + class RoomSerializer(NamespacedHMSerializer): """Serialize `topologie.models.Room` objects """ @@ -644,6 +673,89 @@ class ServiceRegenSerializer(NamespacedHMSerializer): 'api_url': {'view_name': 'serviceregen-detail'} } +# Switches et ports + +class InterfaceVlanSerializer(NamespacedHMSerializer): + domain = serializers.CharField(read_only=True) + ipv4 = serializers.CharField(read_only=True) + ipv6 = Ipv6ListSerializer(read_only=True, many=True) + vlan_id = serializers.IntegerField(source='type.ip_type.vlan.vlan_id', read_only=True) + + class Meta: + model = machines.Interface + fields = ('ipv4', 'ipv6', 'domain', 'vlan_id') + +class InterfaceRoleSerializer(NamespacedHMSerializer): + interface = InterfaceVlanSerializer(source='machine.interface_set', read_only=True, many=True) + + class Meta: + model = machines.Interface + fields = ('interface',) + + +class RoleSerializer(NamespacedHMSerializer): + """Serialize `machines.models.OuverturePort` objects. + """ + servers = InterfaceRoleSerializer(read_only=True, many=True) + + class Meta: + model = machines.Role + fields = ('role_type', 'servers', 'specific_role') + + +class VlanPortSerializer(NamespacedHMSerializer): + class Meta: + model = machines.Vlan + fields = ('vlan_id', 'name') + + +class ProfilSerializer(NamespacedHMSerializer): + vlan_untagged = VlanSerializer(read_only=True) + vlan_tagged = VlanPortSerializer(read_only=True, many=True) + + class Meta: + model = topologie.PortProfile + fields = ('name', 'profil_default', 'vlan_untagged', 'vlan_tagged', 'radius_type', 'radius_mode', 'speed', 'mac_limit', 'flow_control', 'dhcp_snooping', 'dhcpv6_snooping', 'arp_protect', 'ra_guard', 'loop_protect', 'vlan_untagged', 'vlan_tagged') + + +class ModelSwitchSerializer(NamespacedHMSerializer): + constructor = serializers.CharField(read_only=True) + + class Meta: + model = topologie.ModelSwitch + fields = ('reference', 'firmware', 'constructor') + + +class SwitchBaySerializer(NamespacedHMSerializer): + class Meta: + model = topologie.SwitchBay + fields = ('name',) + + +class PortsSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Ipv6List` objects. + """ + get_port_profil = ProfilSerializer(read_only=True) + + + class Meta: + model = topologie.Port + fields = ('state', 'port', 'pretty_name', 'get_port_profil') + + + +class SwitchPortSerializer(serializers.ModelSerializer): + """Serialize the data about the switches""" + ports = PortsSerializer(many=True, read_only=True) + model = ModelSwitchSerializer(read_only=True) + switchbay = SwitchBaySerializer(read_only=True) + + + class Meta: + model = topologie.Switch + fields = ('short_name', 'model', 'switchbay', 'ports', 'ipv4', 'ipv6', + 'interfaces_subnet', 'interfaces6_subnet', 'automatic_provision', 'rest_enabled', + 'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value') # LOCAL EMAILS diff --git a/api/urls.py b/api/urls.py index abc466e1..e8c63657 100644 --- a/api/urls.py +++ b/api/urls.py @@ -63,6 +63,7 @@ router.register_viewset(r'machines/service', views.ServiceViewSet) router.register_viewset(r'machines/servicelink', views.ServiceLinkViewSet, base_name='servicelink') router.register_viewset(r'machines/ouvertureportlist', views.OuverturePortListViewSet) router.register_viewset(r'machines/ouvertureport', views.OuverturePortViewSet) +router.register_viewset(r'machines/role', views.RoleViewSet) # PREFERENCES router.register_view(r'preferences/optionaluser', views.OptionalUserView), router.register_view(r'preferences/optionalmachine', views.OptionalMachineView), @@ -81,7 +82,8 @@ router.register_viewset(r'topologie/modelswitch', views.ModelSwitchViewSet) router.register_viewset(r'topologie/constructorswitch', views.ConstructorSwitchViewSet) router.register_viewset(r'topologie/switchbay', views.SwitchBayViewSet) router.register_viewset(r'topologie/building', views.BuildingViewSet) -router.register(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport') +router.register_viewset(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport') +router.register_viewset(r'topologie/portprofile', views.PortProfileViewSet, base_name='portprofile') router.register_viewset(r'topologie/room', views.RoomViewSet) router.register(r'topologie/portprofile', views.PortProfileViewSet) # USERS @@ -105,6 +107,9 @@ router.register_view(r'localemail/users', views.LocalEmailUsersView), # Firewall router.register_view(r'firewall/subnet-ports', views.SubnetPortsOpenView), router.register_view(r'firewall/interface-ports', views.InterfacePortsOpenView), +# Switches config +router.register_view(r'switchs/ports-config', views.SwitchPortView), +router.register_view(r'switchs/role', views.RoleView), # DNS router.register_view(r'dns/zones', views.DNSZonesView), router.register_view(r'dns/reverse-zones', views.DNSReverseZonesView), diff --git a/api/views.py b/api/views.py index 6e5265f6..22901d09 100644 --- a/api/views.py +++ b/api/views.py @@ -242,6 +242,13 @@ class OuverturePortViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = serializers.OuverturePortSerializer +class RoleViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Machine` objects. + """ + queryset = machines.Role.objects.all() + serializer_class = serializers.RoleSerializer + + # PREFERENCES # Those views differ a bit because there is only one object # to display, so we don't bother with the listing part @@ -397,6 +404,13 @@ class SwitchPortViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = serializers.SwitchPortSerializer +class PortProfileViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.PortProfile` objects. + """ + queryset = topologie.PortProfile.objects.all() + serializer_class = serializers.PortProfileSerializer + + class RoomViewSet(viewsets.ReadOnlyModelViewSet): """Exposes list and details of `topologie.models.Room` objects. """ @@ -515,6 +529,24 @@ class ServiceRegenViewSet(viewsets.ModelViewSet): queryset = queryset.filter(server__domain__name__iexact=hostname) return queryset +# Config des switches + +class SwitchPortView(generics.ListAPIView): + """Exposes the associations between hostname, mac address and IPv4 in + order to build the DHCP lease files. + """ + queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room") + + serializer_class = serializers.SwitchPortSerializer + + +class RoleView(generics.ListAPIView): + """Exposes the associations between hostname, mac address and IPv4 in + order to build the DHCP lease files. + """ + queryset = machines.Role.objects.all().prefetch_related('servers') + serializer_class = serializers.RoleSerializer + # LOCAL EMAILS diff --git a/machines/forms.py b/machines/forms.py index 4af060d3..e9c75d59 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -576,13 +576,24 @@ class VlanForm(FormRevMixin, ModelForm): """Ajout d'un vlan : id, nom""" class Meta: model = Vlan - fields = '__all__' + fields = ['vlan_id', 'name', 'comment'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(VlanForm, self).__init__(*args, prefix=prefix, **kwargs) +class EditOptionVlanForm(FormRevMixin, ModelForm): + """Ajout d'un vlan : id, nom""" + class Meta: + model = Vlan + fields = ['dhcp_snooping', 'dhcpv6_snooping', 'arp_protect', 'igmp', 'mld'] + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditOptionVlanForm, self).__init__(*args, prefix=prefix, **kwargs) + + class DelVlanForm(FormRevMixin, Form): """Suppression d'un ou plusieurs vlans""" vlan = forms.ModelMultipleChoiceField( diff --git a/machines/migrations/0095_auto_20180919_2225.py b/machines/migrations/0095_auto_20180919_2225.py new file mode 100644 index 00000000..66c082ff --- /dev/null +++ b/machines/migrations/0095_auto_20180919_2225.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-09-19 20:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0094_auto_20180815_1918'), + ] + + operations = [ + migrations.AddField( + model_name='vlan', + name='arp_protect', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='vlan', + name='dhcp_snooping', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='vlan', + name='dhcpv6_snooping', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='vlan', + name='igmp', + field=models.BooleanField(default=False, help_text='Gestion multicast v4'), + ), + migrations.AddField( + model_name='vlan', + name='mld', + field=models.BooleanField(default=False, help_text='Gestion multicast v6'), + ), + ] diff --git a/machines/models.py b/machines/models.py index 4de0b012..040043a1 100644 --- a/machines/models.py +++ b/machines/models.py @@ -201,6 +201,12 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): de cette machine""" return str(self.interface_set.first().domain.name) + @cached_property + def complete_name(self): + """Par defaut, renvoie le nom de la première interface + de cette machine""" + return str(self.interface_set.first()) + @cached_property def all_short_names(self): """Renvoie de manière unique, le nom des interfaces de cette @@ -515,6 +521,18 @@ class Vlan(RevMixin, AclMixin, models.Model): vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)]) name = models.CharField(max_length=256) comment = models.CharField(max_length=256, blank=True) + #Réglages supplémentaires + arp_protect = models.BooleanField(default=False) + dhcp_snooping = models.BooleanField(default=False) + dhcpv6_snooping = models.BooleanField(default=False) + igmp = models.BooleanField( + default=False, + help_text="Gestion multicast v4" + ) + mld = models.BooleanField( + default=False, + help_text="Gestion multicast v6" + ) class Meta: permissions = ( @@ -1634,6 +1652,19 @@ class Role(RevMixin, AclMixin, models.Model): machine__interface__role=cls.objects.filter(specific_role=roletype) ) + @classmethod + def get_instance(cls, machineid, *_args, **_kwargs): + """Get the Machine instance with machineid. + :param userid: The id + :return: The user + """ + return cls.objects.get(pk=machineid) + + @classmethod + def interface_for_roletype(cls, roletype): + """Return interfaces for a roletype""" + return Interface.objects.filter(role=cls.objects.filter(specific_role=roletype)) + def save(self, *args, **kwargs): super(Role, self).save(*args, **kwargs) diff --git a/preferences/admin.py b/preferences/admin.py index 5ca90095..b7b171d0 100644 --- a/preferences/admin.py +++ b/preferences/admin.py @@ -37,7 +37,10 @@ from .models import ( MailContact, AssoOption, MailMessageOption, - HomeOption + HomeOption, + RadiusKey, + SwitchManagementCred, + Reminder ) @@ -86,6 +89,18 @@ class HomeOptionAdmin(VersionAdmin): pass +class RadiusKeyAdmin(VersionAdmin): + """Class radiuskey""" + pass + +class SwitchManagementCredAdmin(VersionAdmin): + """Class managementcred for switch""" + pass + +class ReminderAdmin(VersionAdmin): + """Class reminder for switch""" + pass + admin.site.register(OptionalUser, OptionalUserAdmin) admin.site.register(OptionalMachine, OptionalMachineAdmin) admin.site.register(OptionalTopologie, OptionalTopologieAdmin) @@ -93,5 +108,8 @@ admin.site.register(GeneralOption, GeneralOptionAdmin) admin.site.register(HomeOption, HomeOptionAdmin) admin.site.register(Service, ServiceAdmin) admin.site.register(MailContact, MailContactAdmin) +admin.site.register(Reminder, ReminderAdmin) +admin.site.register(RadiusKey, RadiusKeyAdmin) +admin.site.register(SwitchManagementCred, SwitchManagementCredAdmin) admin.site.register(AssoOption, AssoOptionAdmin) admin.site.register(MailMessageOption, MailMessageOptionAdmin) diff --git a/preferences/forms.py b/preferences/forms.py index 73731750..5933107c 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -38,9 +38,12 @@ from .models import ( MailMessageOption, HomeOption, Service, - MailContact + MailContact, + Reminder, + RadiusKey, + SwitchManagementCred, ) - +from topologie.models import Switch class EditOptionalUserForm(ModelForm): """Formulaire d'édition des options de l'user. (solde, telephone..)""" @@ -92,7 +95,14 @@ class EditOptionalMachineForm(ModelForm): class EditOptionalTopologieForm(ModelForm): - """Options de topologie, formulaire d'edition (vlan par default etc)""" + """Options de topologie, formulaire d'edition (vlan par default etc) + On rajoute un champ automatic provision switchs pour gérer facilement + l'ajout de switchs au provisionning automatique""" + automatic_provision_switchs = forms.ModelMultipleChoiceField( + Switch.objects.all(), + required=False + ) + class Meta: model = OptionalTopologie fields = '__all__' @@ -110,6 +120,14 @@ class EditOptionalTopologieForm(ModelForm): self.fields['vlan_decision_nok'].label = _("VLAN for machines rejected" " by RADIUS") + self.initial['automatic_provision_switchs'] = Switch.objects.filter(automatic_provision=True) + + def save(self, commit=True): + instance = super().save(commit) + Switch.objects.all().update(automatic_provision=False) + self.cleaned_data['automatic_provision_switchs'].update(automatic_provision=True) + return instance + class EditGeneralOptionForm(ModelForm): """Options générales (affichages de résultats de recherche, etc)""" @@ -242,8 +260,68 @@ class DelServiceForm(Form): else: self.fields['services'].queryset = Service.objects.all() -class MailContactForm(FormRevMixin, ModelForm): - """Edit and add contact email adress""" +class ReminderForm(FormRevMixin, ModelForm): + """Edition, ajout de services sur la page d'accueil""" + class Meta: + model = Reminder + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(ReminderForm, self).__init__(*args, prefix=prefix, **kwargs) + + +class RadiusKeyForm(FormRevMixin, ModelForm): + """Edition, ajout de clef radius""" + members = forms.ModelMultipleChoiceField( + queryset=Switch.objects.all(), + required=False + ) + + class Meta: + model = RadiusKey + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(RadiusKeyForm, self).__init__(*args, prefix=prefix, **kwargs) + instance = kwargs.get('instance', None) + if instance: + self.initial['members'] = Switch.objects.filter(radius_key=instance) + + def save(self, commit=True): + instance = super().save(commit) + instance.switch_set = self.cleaned_data['members'] + return instance + + +class SwitchManagementCredForm(FormRevMixin, ModelForm): + """Edition, ajout de creds de management pour gestion + et interface rest des switchs""" + members = forms.ModelMultipleChoiceField( + Switch.objects.all(), + required=False + ) + + class Meta: + model = SwitchManagementCred + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(SwitchManagementCredForm, self).__init__(*args, prefix=prefix, **kwargs) + instance = kwargs.get('instance', None) + if instance: + self.initial['members'] = Switch.objects.filter(management_creds=instance) + + def save(self, commit=True): + instance = super().save(commit) + instance.switch_set = self.cleaned_data['members'] + return instance + + +class MailContactForm(ModelForm): + """Edition, ajout d'adresse de contact""" class Meta: model = MailContact fields = '__all__' diff --git a/preferences/migrations/0051_auto_20180919_2225.py b/preferences/migrations/0051_auto_20180919_2225.py new file mode 100644 index 00000000..f776a9a6 --- /dev/null +++ b/preferences/migrations/0051_auto_20180919_2225.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-09-19 20:25 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.aes_field +import re2o.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0095_auto_20180919_2225'), + ('preferences', '0050_auto_20180818_1329'), + ] + + operations = [ + migrations.CreateModel( + name='RadiusKey', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('radius_key', re2o.aes_field.AESEncryptedField(help_text='Clef radius', max_length=255)), + ('comment', models.CharField(blank=True, help_text='Commentaire de cette clef', max_length=255, null=True)), + ('default_switch', models.BooleanField(default=True, help_text='Clef par défaut des switchs', unique=True)), + ], + options={ + 'permissions': (('view_radiuskey', 'Peut voir un objet radiuskey'),), + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.CreateModel( + name='Reminder', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('days', models.IntegerField(default=7, help_text="Délais entre le mail et la fin d'adhésion", unique=True)), + ('message', models.CharField(blank=True, default='', help_text='Message affiché spécifiquement pour ce rappel', max_length=255, null=True)), + ], + options={ + 'permissions': (('view_reminder', 'Peut voir un objet reminder'),), + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.CreateModel( + name='SwitchManagementCred', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('management_id', models.CharField(help_text='Login du switch', max_length=63)), + ('management_pass', re2o.aes_field.AESEncryptedField(help_text='Mot de passe', max_length=63)), + ('default_switch', models.BooleanField(default=True, help_text='Creds par défaut des switchs', unique=True)), + ], + options={ + 'permissions': (('view_switchmanagementcred', 'Peut voir un objet switchmanagementcred'),), + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.AddField( + model_name='optionaltopologie', + name='sftp_login', + field=models.CharField(blank=True, help_text='Login sftp des switchs', max_length=32, null=True), + ), + migrations.AddField( + model_name='optionaltopologie', + name='sftp_pass', + field=re2o.aes_field.AESEncryptedField(blank=True, help_text='Mot de passe sftp', max_length=63, null=True), + ), + migrations.AddField( + model_name='optionaltopologie', + name='switchs_ip_type', + field=models.OneToOneField(blank=True, help_text="Plage d'ip de management des switchs", null=True, on_delete=django.db.models.deletion.PROTECT, to='machines.IpType'), + ), + migrations.AddField( + model_name='optionaltopologie', + name='switchs_provision', + field=models.CharField(choices=[('sftp', 'sftp'), ('tftp', 'tftp')], default='tftp', help_text='Mode de récupération des confs par les switchs', max_length=32), + ), + migrations.AddField( + model_name='optionaltopologie', + name='switchs_rest_management', + field=models.BooleanField(default=False, help_text='Rest management, activé si provision auto'), + ), + migrations.AddField( + model_name='optionaltopologie', + name='switchs_web_management', + field=models.BooleanField(default=False, help_text='Web management, activé si provision automatique'), + ), + migrations.AddField( + model_name='optionaltopologie', + name='switchs_web_management_ssl', + field=models.BooleanField(default=False, help_text='Web management ssl. Assurez-vous que un certif est installé sur le switch !'), + ), + migrations.AlterField( + model_name='mailmessageoption', + name='welcome_mail_en', + field=models.TextField(default='', help_text='Mail de bienvenue en anglais'), + ), + migrations.AlterField( + model_name='mailmessageoption', + name='welcome_mail_fr', + field=models.TextField(default='', help_text='Mail de bienvenue en français'), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 3199dd6c..31d382aa 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -34,7 +34,10 @@ from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _ import machines.models + from re2o.mixins import AclMixin +from re2o.aes_field import AESEncryptedField +from datetime import timedelta class PreferencesModel(models.Model): @@ -180,6 +183,10 @@ class OptionalTopologie(AclMixin, PreferencesModel): (MACHINE, _("On the IP range's VLAN of the machine")), (DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")), ) + CHOICE_PROVISION = ( + ('sftp', 'sftp'), + ('tftp', 'tftp'), + ) radius_general_policy = models.CharField( max_length=32, @@ -200,6 +207,97 @@ class OptionalTopologie(AclMixin, PreferencesModel): blank=True, null=True ) + switchs_web_management = models.BooleanField( + default=False, + help_text="Web management, activé si provision automatique" + ) + switchs_web_management_ssl = models.BooleanField( + default=False, + help_text="Web management ssl. Assurez-vous que un certif est installé sur le switch !" + ) + switchs_rest_management = models.BooleanField( + default=False, + help_text="Rest management, activé si provision auto" + ) + switchs_ip_type = models.OneToOneField( + 'machines.IpType', + on_delete=models.PROTECT, + blank=True, + null=True, + help_text="Plage d'ip de management des switchs" + ) + switchs_provision = models.CharField( + max_length=32, + choices=CHOICE_PROVISION, + default='tftp', + help_text="Mode de récupération des confs par les switchs" + ) + sftp_login = models.CharField( + max_length=32, + null=True, + blank=True, + help_text="Login sftp des switchs" + ) + sftp_pass = AESEncryptedField( + max_length=63, + null=True, + blank=True, + help_text="Mot de passe sftp" + ) + + @cached_property + def provisioned_switchs(self): + """Liste des switches provisionnés""" + from topologie.models import Switch + return Switch.objects.filter(automatic_provision=True) + + @cached_property + def switchs_management_interface(self): + """Return the ip of the interface that the switch have to contact to get it's config""" + if self.switchs_ip_type: + from machines.models import Role, Interface + return Interface.objects.filter(machine__interface__in=Role.interface_for_roletype("switch-conf-server")).filter(type__ip_type=self.switchs_ip_type).first() + else: + return None + + @cached_property + def switchs_management_interface_ip(self): + """Same, but return the ipv4""" + if not self.switchs_management_interface: + return None + return self.switchs_management_interface.ipv4 + + @cached_property + def switchs_management_sftp_creds(self): + """Credentials des switchs pour provion sftp""" + if self.sftp_login and self.sftp_pass: + return {'login' : self.sftp_login, 'pass' : self.sftp_pass} + else: + return None + + @cached_property + def switchs_management_utils(self): + """Used for switch_conf, return a list of ip on vlans""" + from machines.models import Role, Ipv6List, Interface + def return_ips_dict(interfaces): + return {'ipv4' : [str(interface.ipv4) for interface in interfaces], 'ipv6' : Ipv6List.objects.filter(interface__in=interfaces).values_list('ipv6', flat=True)} + + ntp_servers = Role.all_interfaces_for_roletype("ntp-server").filter(type__ip_type=self.switchs_ip_type) + log_servers = Role.all_interfaces_for_roletype("log-server").filter(type__ip_type=self.switchs_ip_type) + radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type) + dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server") + subnet = None + subnet6 = None + if self.switchs_ip_type: + subnet = self.switchs_ip_type.ip_set_full_info + subnet6 = self.switchs_ip_type.ip6_set_full_info + return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'subnet': subnet, 'subnet6': subnet6} + + @cached_property + def provision_switchs_enabled(self): + """Return true if all settings are ok : switchs on automatic provision, + ip_type""" + return bool(self.provisioned_switchs and self.switchs_ip_type and SwitchManagementCred.objects.filter(default_switch=True).exists() and self.switchs_management_interface_ip and bool(self.switchs_provision != 'sftp' or self.switchs_management_sftp_creds)) class Meta: permissions = ( @@ -215,6 +313,91 @@ def optionaltopologie_post_save(**kwargs): topologie_pref.set_in_cache() +class RadiusKey(AclMixin, models.Model): + """Class of a radius key""" + radius_key = AESEncryptedField( + max_length=255, + help_text="Clef radius" + ) + comment = models.CharField( + max_length=255, + null=True, + blank=True, + help_text="Commentaire de cette clef" + ) + default_switch = models.BooleanField( + default=True, + unique=True, + help_text= "Clef par défaut des switchs" + ) + + class Meta: + permissions = ( + ("view_radiuskey", "Peut voir un objet radiuskey"), + ) + + def __str__(self): + return "Clef radius " + str(self.id) + " " + str(self.comment) + + +class SwitchManagementCred(AclMixin, models.Model): + """Class of a management creds of a switch, for rest management""" + management_id = models.CharField( + max_length=63, + help_text="Login du switch" + ) + management_pass = AESEncryptedField( + max_length=63, + help_text="Mot de passe" + ) + default_switch = models.BooleanField( + default=True, + unique=True, + help_text= "Creds par défaut des switchs" + ) + + class Meta: + permissions = ( + ("view_switchmanagementcred", "Peut voir un objet switchmanagementcred"), + ) + + def __str__(self): + return "Identifiant " + str(self.management_id) + + +class Reminder(AclMixin, models.Model): + """Options pour les mails de notification de fin d'adhésion. + Days: liste des nombres de jours pour lesquells un mail est envoyé + optionalMessage: message additionel pour le mail + """ + PRETTY_NAME="Options pour le mail de fin d'adhésion" + + days = models.IntegerField( + default=7, + unique=True, + help_text="Délais entre le mail et la fin d'adhésion" + ) + message = models.CharField( + max_length=255, + default="", + null=True, + blank=True, + help_text="Message affiché spécifiquement pour ce rappel" + ) + + class Meta: + permissions = ( + ("view_reminder", "Peut voir un objet reminder"), + ) + + def users_to_remind(self): + from re2o.utils import all_has_access + date = timezone.now().replace(minute=0,hour=0) + futur_date = date + timedelta(days=self.days) + users = all_has_access(futur_date).exclude(pk__in = all_has_access(futur_date + timedelta(days=1))) + return users + + class GeneralOption(AclMixin, PreferencesModel): """Options générales : nombre de resultats par page, nom du site, temps où les liens sont valides""" @@ -383,8 +566,8 @@ def homeoption_post_save(**kwargs): class MailMessageOption(AclMixin, models.Model): """Reglages, mail de bienvenue et autre""" - welcome_mail_fr = models.TextField(default="") - welcome_mail_en = models.TextField(default="") + welcome_mail_fr = models.TextField(default="", help_text="Mail de bienvenue en français") + welcome_mail_en = models.TextField(default="", help_text="Mail de bienvenue en anglais") class Meta: permissions = ( diff --git a/preferences/templates/preferences/aff_radiuskey.html b/preferences/templates/preferences/aff_radiuskey.html new file mode 100644 index 00000000..0d58efe3 --- /dev/null +++ b/preferences/templates/preferences/aff_radiuskey.html @@ -0,0 +1,57 @@ +{% 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 © 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. +{% endcomment %} +{% load acl %} +{% load logs_extra %} + + + + + + + + + + + + {% for radiuskey in radiuskey_list %} + + + + + + + + {% endfor %} +
Id ClefCommentaireClef par default des switchsClef utilisée par les switchs
{{ radiuskey.id }}{{ radiuskey.comment }}{{ radiuskey.default_switch }}{{ radiuskey.switch_set.all|join:", " }} + {% can_edit radiuskey %} + {% include 'buttons/edit.html' with href='preferences:edit-radiuskey' id=radiuskey.id %} + {% acl_end %} + {% can_delete radiuskey %} + + + + {% acl_end %} + {% history_button radiuskey %} +
+ diff --git a/preferences/templates/preferences/aff_switchmanagementcred.html b/preferences/templates/preferences/aff_switchmanagementcred.html new file mode 100644 index 00000000..ef8b0143 --- /dev/null +++ b/preferences/templates/preferences/aff_switchmanagementcred.html @@ -0,0 +1,55 @@ +{% 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 © 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. +{% endcomment %} +{% load acl %} +{% load logs_extra %} + + + + + + + + + + + {% for switchmanagementcred in switchmanagementcred_list %} + + + + + + + {% endfor %} +
IdentifiantCreds par default des switchsUtilisé pour les switchs
{{ switchmanagementcred.management_id }}{{ switchmanagementcred.default_switch }}{{ switchmanagementcred.switch_set.all|join:", " }} + {% can_edit switchmanagementcred %} + {% include 'buttons/edit.html' with href='preferences:edit-switchmanagementcred' id=switchmanagementcred.id %} + {% acl_end %} + {% can_delete switchmanagementcred %} + + + + {% acl_end %} + {% history_button switchmanagementcred %} +
+ diff --git a/preferences/templates/preferences/delete.html b/preferences/templates/preferences/delete.html new file mode 100644 index 00000000..ce277647 --- /dev/null +++ b/preferences/templates/preferences/delete.html @@ -0,0 +1,40 @@ +{% 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 © 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. +{% endcomment %} + +{% load bootstrap3 %} + +{% block title %}Création et modification de machines{% endblock %} + +{% block content %} + +
+ {% csrf_token %} +

Attention, voulez-vous vraiment supprimer cet objet {{ objet_name }} ( {{ objet }} ) ?

+ {% bootstrap_button "Confirmer" button_type="submit" icon="trash" %} +
+
+
+
+{% endblock %} diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 6a499969..0cba4d78 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -118,6 +118,75 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "VLAN for machines rejected by RADIUS" %} {{ topologieoptions.vlan_decision_nok }} + + Placement sur ce vlan par default en cas de rejet + {{ topologieoptions.vlan_decision_nok }} + + + +
Clef radius
+ {% can_create RadiusKey%} + Ajouter une clef radius + {% acl_end %} + {% include "preferences/aff_radiuskey.html" with radiuskey_list=radiuskey_list %} + +

Configuration des switches

+ + + + + + + +
Web management, activé si provision automatique{{ topologieoptions.switchs_web_management }}Rest management, activé si provision auto{{ topologieoptions.switchs_rest_management }}
+ + + +
{% if topologieoptions.provision_switchs_enabled %}Provision de la config des switchs{% else %}Provision de la config des switchs{% endif%}
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Switchs configurés automatiquement{{ topologieoptions.provisioned_switchs|join:", " }} {% if topologieoptions.provisioned_switchs %} OK{% else %}Manquant{% endif %}
Plage d'ip de management des switchs{{ topologieoptions.switchs_ip_type }} {% if topologieoptions.switchs_ip_type %} OK{% else %}Manquant{% endif %}
Serveur des config des switchs{{ topologieoptions.switchs_management_interface }} {% if topologieoptions.switchs_management_interface %} - {{ topologieoptions.switchs_management_interface_ip }} OK{% else %}Manquant{% endif %}
Mode de provision des switchs{{ topologieoptions.switchs_provision }}
Mode TFTP OK
Mode SFTP{% if topologieoptions.switchs_management_sftp_creds %} OK{% else %}Creds manquants{% endif %}
+ +
Creds de management des switchs
+ {% can_create SwitchManagementCred%} + Ajouter un id/mdp de management switch + {% acl_end %} +

+

+ {% if switchmanagementcred_list %} OK{% else %}Manquant{% endif %} + {% include "preferences/aff_switchmanagementcred.html" with switchmanagementcred_list=switchmanagementcred_list %} + + + +
{% if topologieoptions.provisioned_switchs %}Provision de la config des switchs{% else %}Provision de la config des switchs{% endif%}
+ + + + +
Switchs configurés automatiquement{{ topologieoptions.provisioned_switchs|join:", " }}

{% trans "General preferences" %}

diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index 30ab2423..a1540f33 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -36,7 +36,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} - {% massive_bootstrap_form options 'utilisateur_asso' %} + {% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %} {% trans "Edit" as tr_edit %} {% bootstrap_button tr_edit button_type="submit" icon='ok' button_class='btn-success' %}
diff --git a/preferences/templates/preferences/preferences.html b/preferences/templates/preferences/preferences.html index 447df487..8ae9b6bc 100644 --- a/preferences/templates/preferences/preferences.html +++ b/preferences/templates/preferences/preferences.html @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load i18n %} +{% load massive_bootstrap_form %} {% block title %}{% trans "Preferences" %}{% endblock %} @@ -37,7 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} {% if preferenceform %} - {% bootstrap_form preferenceform %} + {% massive_bootstrap_form preferenceform 'members' %} {% endif %} {% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
diff --git a/preferences/urls.py b/preferences/urls.py index a89fcdf3..89d0254c 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -80,5 +80,26 @@ urlpatterns = [ name='edit-mailcontact' ), url(r'^del_mailcontact/$', views.del_mailcontact, name='del-mailcontact'), + url(r'^add_reminder/$', views.add_reminder, name='add-reminder'), + url( + r'^edit_reminder/(?P[0-9]+)$', + views.edit_reminder, + name='edit-reminder' + ), + url(r'^del_reminder/$', views.del_reminder, name='del-reminder'), + url(r'^add_radiuskey/$', views.add_radiuskey, name='add-radiuskey'), + url( + r'^edit_radiuskey/(?P[0-9]+)$', + views.edit_radiuskey, + name='edit-radiuskey' + ), + url(r'^del_radiuskey/(?P[0-9]+)$', views.del_radiuskey, name='del-radiuskey'), + url(r'^add_switchmanagementcred/$', views.add_switchmanagementcred, name='add-switchmanagementcred'), + url( + r'^edit_switchmanagementcred/(?P[0-9]+)$', + views.edit_switchmanagementcred, + name='edit-switchmanagementcred' + ), + url(r'^del_switchmanagementcred/(?P[0-9]+)$', views.del_switchmanagementcred, name='del-switchmanagementcred'), url(r'^$', views.display_options, name='display-options'), ] diff --git a/preferences/views.py b/preferences/views.py index 559cdfef..e9f10d53 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -41,10 +41,14 @@ from django.utils.translation import ugettext as _ from reversion import revisions as reversion from re2o.views import form -from re2o.acl import can_create, can_edit, can_delete_set, can_view_all +from re2o.acl import can_create, can_edit, can_delete_set, can_view_all, can_delete +from .forms import MailContactForm, DelMailContactForm from .forms import ( - ServiceForm, DelServiceForm, MailContactForm, DelMailContactForm + ServiceForm, + ReminderForm, + RadiusKeyForm, + SwitchManagementCredForm ) from .models import ( Service, @@ -55,7 +59,10 @@ from .models import ( MailMessageOption, GeneralOption, OptionalTopologie, - HomeOption + HomeOption, + Reminder, + RadiusKey, + SwitchManagementCred ) from . import models from . import forms @@ -76,6 +83,9 @@ def display_options(request): mailmessageoptions, _created = MailMessageOption.objects.get_or_create() service_list = Service.objects.all() mailcontact_list = MailContact.objects.all() + reminder_list = Reminder.objects.all() + radiuskey_list = RadiusKey.objects.all() + switchmanagementcred_list = SwitchManagementCred.objects.all() return form({ 'useroptions': useroptions, 'machineoptions': machineoptions, @@ -85,7 +95,10 @@ def display_options(request): 'homeoptions': homeoptions, 'mailmessageoptions': mailmessageoptions, 'service_list': service_list, - 'mailcontact_list': mailcontact_list + 'mailcontact_list': mailcontact_list, + 'reminder_list': reminder_list, + 'radiuskey_list' : radiuskey_list, + 'switchmanagementcred_list': switchmanagementcred_list, }, 'preferences/display_preferences.html', request) @@ -196,6 +209,164 @@ def del_service(request, instances): request ) +@login_required +@can_create(Reminder) +def add_reminder(request): + """Ajout d'un service de la page d'accueil""" + reminder = ReminderForm(request.POST or None, request.FILES or None) + if service.is_valid(): + with transaction.atomic(), reversion.create_revision(): + reminder.save() + reversion.set_user(request.user) + reversion.set_comment("Creation") + messages.success(request, _("The service was added.")) + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': service, 'action_name': _("Add a service")}, + 'preferences/preferences.html', + request + ) + +@login_required +@can_edit(Reminder) +def edit_reminder(request, service_instance, **_kwargs): + """Edition des services affichés sur la page d'accueil""" + reminder = ReminderForm( + request.POST or None, + request.FILES or None, + instance=reminder_instance + ) + if reminder.is_valid(): + with transaction.atomic(), reversion.create_revision(): + reminder.save() + reversion.set_user(request.user) + reversion.set_comment( + "Field(s) edited: %s" % ', '.join( + field for field in reminder.changed_data + ) + ) + messages.success(request, _("The service was edited.")) + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': service, 'action_name': _("Edit")}, + 'preferences/preferences.html', + request + ) + + + +@login_required +@can_delete(Reminder) +def del_reminder(request, reminder_instance, **_kwargs): + """Destruction d'un reminder""" + if request.method == "POST": + reminder_instance.delete() + messages.success(request, "Le reminder a été détruit") + return redirect(reverse('preferences:display-options')) + return form( + {'objet': reminder_instance, 'objet_name': 'reminder'}, + 'preferences/delete.html', + request + ) + + +@login_required +@can_create(RadiusKey) +def add_radiuskey(request): + """Ajout d'une clef radius""" + radiuskey = RadiusKeyForm(request.POST or None) + if radiuskey.is_valid(): + radiuskey.save() + messages.success(request, "Cette clef a été ajouté") + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': radiuskey, 'action_name': 'Ajouter'}, + 'preferences/preferences.html', + request + ) + +@can_edit(RadiusKey) +def edit_radiuskey(request, radiuskey_instance, **_kwargs): + """Edition des clefs radius""" + radiuskey = RadiusKeyForm(request.POST or None, instance=radiuskey_instance) + if radiuskey.is_valid(): + radiuskey.save() + messages.success(request, "Radiuskey modifié") + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': radiuskey, 'action_name': 'Editer'}, + 'preferences/preferences.html', + request + ) + + +@login_required +@can_delete(RadiusKey) +def del_radiuskey(request, radiuskey_instance, **_kwargs): + """Destruction d'un radiuskey""" + if request.method == "POST": + try: + radiuskey_instance.delete() + messages.success(request, "La radiuskey a été détruite") + except ProtectedError: + messages.error(request, "Erreur la\ + clef ne peut être supprimé, elle est affectée à des switchs") + return redirect(reverse('preferences:display-options')) + return form( + {'objet': radiuskey_instance, 'objet_name': 'radiuskey'}, + 'preferences/delete.html', + request + ) + + +@login_required +@can_create(SwitchManagementCred) +def add_switchmanagementcred(request): + """Ajout de creds de management""" + switchmanagementcred = SwitchManagementCredForm(request.POST or None) + if switchmanagementcred.is_valid(): + switchmanagementcred.save() + messages.success(request, "Ces creds ont été ajoutés") + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': switchmanagementcred, 'action_name': 'Ajouter'}, + 'preferences/preferences.html', + request + ) + +@can_edit(SwitchManagementCred) +def edit_switchmanagementcred(request, switchmanagementcred_instance, **_kwargs): + """Edition des creds de management""" + switchmanagementcred = SwitchManagementCredForm(request.POST or None, instance=switchmanagementcred_instance) + if switchmanagementcred.is_valid(): + switchmanagementcred.save() + messages.success(request, "Creds de managament modifié") + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': switchmanagementcred, 'action_name': 'Editer'}, + 'preferences/preferences.html', + request + ) + + +@login_required +@can_delete(SwitchManagementCred) +def del_switchmanagementcred(request, switchmanagementcred_instance, **_kwargs): + """Destruction d'un switchmanagementcred""" + if request.method == "POST": + try: + switchmanagementcred_instance.delete() + messages.success(request, "Ces creds ont été détruits") + except ProtectedError: + messages.error(request, "Erreur ces\ + creds ne peuvent être supprimés, ils sont affectés à des switchs") + return redirect(reverse('preferences:display-options')) + return form( + {'objet': switchmanagementcred_instance, 'objet_name': 'switchmanagementcred'}, + 'preferences/delete.html', + request + ) + @login_required @can_create(MailContact) diff --git a/topologie/migrations/0063_auto_20180919_2225.py b/topologie/migrations/0063_auto_20180919_2225.py new file mode 100644 index 00000000..45228340 --- /dev/null +++ b/topologie/migrations/0063_auto_20180919_2225.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-09-19 20:25 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0051_auto_20180919_2225'), + ('topologie', '0062_auto_20180815_1918'), + ] + + operations = [ + migrations.AddField( + model_name='modelswitch', + name='firmware', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='switch', + name='management_creds', + field=models.ForeignKey(blank=True, help_text='Identifiant de management de ce switch', null=True, on_delete=django.db.models.deletion.PROTECT, to='preferences.SwitchManagementCred'), + ), + migrations.AddField( + model_name='switch', + name='radius_key', + field=models.ForeignKey(blank=True, help_text='Clef radius du switch', null=True, on_delete=django.db.models.deletion.PROTECT, to='preferences.RadiusKey'), + ), + ] diff --git a/topologie/migrations/0064_switch_automatic_provision.py b/topologie/migrations/0064_switch_automatic_provision.py new file mode 100644 index 00000000..d8d280cf --- /dev/null +++ b/topologie/migrations/0064_switch_automatic_provision.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-09-20 16:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('topologie', '0063_auto_20180919_2225'), + ] + + operations = [ + migrations.AddField( + model_name='switch', + name='automatic_provision', + field=models.BooleanField(default=False, help_text='Provision automatique de ce switch'), + ), + ] diff --git a/topologie/models.py b/topologie/models.py index b63bece6..d02fce40 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -49,6 +49,11 @@ from django.db import transaction from django.utils.translation import ugettext_lazy as _ from reversion import revisions as reversion +from preferences.models import ( + OptionalTopologie, + RadiusKey, + SwitchManagementCred +) from machines.models import Machine, regen from re2o.mixins import AclMixin, RevMixin @@ -228,6 +233,24 @@ class Switch(AclMixin, Machine): null=True, on_delete=models.SET_NULL, ) + radius_key = models.ForeignKey( + 'preferences.RadiusKey', + blank=True, + null=True, + on_delete=models.PROTECT, + help_text="Clef radius du switch" + ) + management_creds = models.ForeignKey( + 'preferences.SwitchManagementCred', + blank=True, + null=True, + on_delete=models.PROTECT, + help_text="Identifiant de management de ce switch" + ) + automatic_provision = models.BooleanField( + default=False, + help_text='Provision automatique de ce switch', + ) class Meta: unique_together = ('stack', 'stack_member_id') @@ -285,13 +308,78 @@ class Switch(AclMixin, Machine): ValidationError(_("Creation of an existing port.")) def main_interface(self): - """ Returns the 'main' interface of the switch """ + """ 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') + if switch_iptype: + return self.interface_set.filter(type__ip_type=switch_iptype).first() return self.interface_set.first() @cached_property def get_name(self): return self.name or self.main_interface().domain.name + @cached_property + def get_radius_key(self): + """Retourne l'objet de la clef radius de ce switch""" + return self.radius_key or RadiusKey.objects.filter(default_switch=True).first() + + @cached_property + def get_radius_key_value(self): + """Retourne la valeur en str de la clef radius, none si il n'y en a pas""" + if self.get_radius_key: + return self.get_radius_key.radius_key + else: + return None + + @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 + + @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 + + @cached_property + def ipv4(self): + """Return the switch's management ipv4""" + return str(self.main_interface().ipv4) + + @cached_property + def ipv6(self): + """Returne the switch's management ipv6""" + return str(self.main_interface().ipv6().first()) + + @cached_property + 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()) + + @cached_property + 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()) + def __str__(self): return str(self.get_name) @@ -304,6 +392,11 @@ class ModelSwitch(AclMixin, RevMixin, models.Model): 'topologie.ConstructorSwitch', on_delete=models.PROTECT ) + firmware = models.CharField( + max_length=255, + null=True, + blank=True + ) class Meta: permissions = ( @@ -437,8 +530,20 @@ class Port(AclMixin, RevMixin, models.Model): verbose_name_plural = _("ports") @cached_property - def get_port_profile(self): - """Return the config profile for this port + 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 + def get_port_profil(self): + """Return the config profil for this port :returns: the profile of self (port)""" def profile_or_nothing(profile): port_profile = PortProfile.objects.filter( @@ -447,7 +552,7 @@ class Port(AclMixin, RevMixin, models.Model): return port_profile else: nothing_profile, _created = PortProfile.objects.get_or_create( - profile_default='nothing', + profil_default='nothing', name='nothing', radius_type='NO' ) diff --git a/topologie/templates/topologie/aff_model_switch.html b/topologie/templates/topologie/aff_model_switch.html index 6b3e7156..fc49331f 100644 --- a/topologie/templates/topologie/aff_model_switch.html +++ b/topologie/templates/topologie/aff_model_switch.html @@ -35,15 +35,25 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Reference" as tr_ref %} {% include "buttons/sort.html" with prefix='model-switch' col='reference' text=tr_ref %} + Firmware {% trans "Switch constructor" as tr_constructor %} {% include "buttons/sort.html" with prefix='model-switch' col='constructor' text=tr_constructor %} - + {% trans "Switches" %} + {% for model_switch in model_switch_list %} {{ model_switch.reference }} + {{model_switch.firmware}} {{ model_switch.constructor }} + + {% for switch in model_switch.switch_set.all %} +
+ {{ switch }} + + {% endfor %} + {% can_edit model_switch %} @@ -64,4 +74,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if model_switch_list.paginator %} {% include "pagination.html" with list=model_switch_list %} {% endif %} + + diff --git a/topologie/templates/topologie/aff_vlanoptions.html b/topologie/templates/topologie/aff_vlanoptions.html new file mode 100644 index 00000000..3810cb7f --- /dev/null +++ b/topologie/templates/topologie/aff_vlanoptions.html @@ -0,0 +1,60 @@ +{% 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 © 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. +{% endcomment %} + +{% load acl %} +{% load logs_extra %} + +
+ + + + + + + + + + + + + + {% for vlan in vlan_list %} + + + + + + + + + + + {% endfor %} +
IdNomArp ProtectDhcp SnoopingDhcpv6 SnoopingIgmpMld
{{ vlan.vlan_id }}{{ vlan.name }}{{ vlan.arp_protect }}{{ vlan.dhcp_snooping }}{{ vlan.dhcpv6_snooping }}{{ vlan.igmp }}{{ vlan.mld }} + {% can_edit vlan %} + {% include 'buttons/edit.html' with href='topologie:edit-vlanoptions' id=vlan.id %} + {% acl_end %} + {% history_button vlan %} +
+
diff --git a/topologie/templates/topologie/index_portprofile.html b/topologie/templates/topologie/index_portprofile.html index 730823f6..4a603210 100644 --- a/topologie/templates/topologie/index_portprofile.html +++ b/topologie/templates/topologie/index_portprofile.html @@ -30,12 +30,18 @@ with this program; if not, write to the Free Software Foundation, Inc., {% 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 %} + + +

{% trans "Sécurité par vlan" %}

+{% include "topologie/aff_vlanoptions.html" with vlan_list=vlan_list %} +


diff --git a/topologie/urls.py b/topologie/urls.py index c314c800..77d68d50 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -120,4 +120,7 @@ urlpatterns = [ url(r'^del_port_profile/(?P[0-9]+)$', views.del_port_profile, name='del-port-profile'), -] + url(r'^edit_vlanoptions/(?P[0-9]+)$', + views.edit_vlanoptions, + name='edit-vlanoptions'), + ] diff --git a/topologie/views.py b/topologie/views.py index 3d8a3044..74ea1148 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -60,10 +60,15 @@ from re2o.settings import MEDIA_ROOT from machines.forms import ( DomainForm, EditInterfaceForm, - AddInterfaceForm + AddInterfaceForm, + EditOptionVlanForm ) from machines.views import generate_ipv4_mbf_param -from machines.models import Interface, Service_link +from machines.models import ( + Interface, + Service_link, + Vlan +) from preferences.models import AssoOption, GeneralOption from .models import ( @@ -153,10 +158,11 @@ def index_port_profile(request): 'vlan_untagged') port_profile_list = re2o_paginator( request, port_profile_list, pagination_number) + vlan_list = Vlan.objects.all().order_by('vlan_id') return render( request, 'topologie/index_portprofile.html', - {'port_profile_list': port_profile_list} + {'port_profile_list': port_profile_list, 'vlan_list': vlan_list} ) @@ -307,6 +313,23 @@ def index_model_switch(request): ) +@login_required +@can_edit(Vlan) +def edit_vlanoptions(request, vlan_instance, **_kwargs): + """ View used to edit options for switch of VLAN object """ + vlan = EditOptionVlanForm(request.POST or None, instance=vlan_instance) + if vlan.is_valid(): + if vlan.changed_data: + vlan.save() + messages.success(request, "Vlan modifié") + return redirect(reverse('topologie:index-port-profile')) + return form( + {'vlanform': vlan, 'action_name': 'Editer'}, + 'machines/machine.html', + request + ) + + @login_required @can_create(Port) def new_port(request, switchid):