diff --git a/topologie/admin.py b/topologie/admin.py index bc4ca81e..bd5a61c4 100644 --- a/topologie/admin.py +++ b/topologie/admin.py @@ -20,8 +20,8 @@ # 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. -""" -Fichier définissant les administration des models dans l'interface admin +"""topologie.admin +The objects, fields and datastructures visible in the Django admin view. """ from __future__ import unicode_literals @@ -45,67 +45,67 @@ from .models import ( class StackAdmin(VersionAdmin): - """Administration d'une stack de switches (inclus des switches)""" + """Admin class of stacks (includes switches).""" pass class SwitchAdmin(VersionAdmin): - """Administration d'un switch""" + """Admin class of switches.""" pass class PortAdmin(VersionAdmin): - """Administration d'un port de switches""" + """Admin class of switch ports.""" pass class AccessPointAdmin(VersionAdmin): - """Administration d'une borne""" + """Admin class of APs.""" pass class RoomAdmin(VersionAdmin): - """Administration d'un chambre""" + """Admin class of rooms.""" pass class ModelSwitchAdmin(VersionAdmin): - """Administration d'un modèle de switch""" + """Admin class of switch models.""" pass class ConstructorSwitchAdmin(VersionAdmin): - """Administration d'un constructeur d'un switch""" + """Admin class of switch constructors.""" pass class SwitchBayAdmin(VersionAdmin): - """Administration d'une baie de brassage""" + """Admin class of switch bays.""" pass class BuildingAdmin(VersionAdmin): - """Administration d'un batiment""" + """Admin class of buildings.""" pass class DormitoryAdmin(VersionAdmin): - """Administration d'une residence""" + """Admin class of dormitories.""" pass class PortProfileAdmin(VersionAdmin): - """Administration of a port profile""" + """Admin class of port profiles.""" pass diff --git a/topologie/forms.py b/topologie/forms.py index 91cc8367..205be5fb 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -20,14 +20,12 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Un forms le plus simple possible pour les objets topologie de re2o. +Forms for the topologie app of re2o. -Permet de créer et supprimer : un Port de switch, relié à un switch. - -Permet de créer des stacks et d'y ajouter des switchs (StackForm) - -Permet de créer, supprimer et editer un switch (EditSwitchForm, -NewSwitchForm) +The forms are used to: + * create and delete switch ports, related to a switch. + * create stacks and add switches to them (StackForm). + * create, edit and delete a switch (NewSwitchForm, EditSwitchForm). """ from __future__ import unicode_literals @@ -59,8 +57,7 @@ from .models import ( class PortForm(FormRevMixin, ModelForm): - """Formulaire pour la création d'un port d'un switch - Relié directement au modèle port""" + """Form used to manage a switch's port.""" class Meta: model = Port @@ -72,14 +69,11 @@ class PortForm(FormRevMixin, ModelForm): class EditPortForm(FormRevMixin, ModelForm): - """Form pour l'édition d'un port de switche : changement des reglages - radius ou vlan, ou attribution d'une chambre, autre port ou machine + """Form used to edit a switch's port: change in RADIUS or VLANs settings, + assignement to a room, port or machine. - Un port est relié à une chambre, un autre port (uplink) ou une machine - (serveur ou borne), mutuellement exclusif - Optimisation sur les queryset pour machines et port_related pour - optimiser le temps de chargement avec select_related (vraiment - lent sans)""" + A port is related to either a room, another port (uplink) or a machine (server or AP). + """ class Meta(PortForm.Meta): fields = [ @@ -106,8 +100,7 @@ class EditPortForm(FormRevMixin, ModelForm): class AddPortForm(FormRevMixin, ModelForm): - """Permet d'ajouter un port de switch. Voir EditPortForm pour plus - d'informations""" + """Form used to add a switch's port. See EditPortForm.""" class Meta(PortForm.Meta): fields = [ @@ -139,8 +132,7 @@ class AddPortForm(FormRevMixin, ModelForm): class StackForm(FormRevMixin, ModelForm): - """Permet d'edition d'une stack : stack_id, et switches membres - de la stack""" + """Form used to create and edit stacks.""" class Meta: model = Stack @@ -152,8 +144,7 @@ class StackForm(FormRevMixin, ModelForm): class AddAccessPointForm(NewMachineForm): - """Formulaire pour la création d'une borne - Relié directement au modèle borne""" + """Form used to create access points.""" class Meta: model = AccessPoint @@ -161,7 +152,7 @@ class AddAccessPointForm(NewMachineForm): class EditAccessPointForm(EditMachineForm): - """Edition d'une borne. Edition complète""" + """Form used to edit access points.""" class Meta: model = AccessPoint @@ -169,7 +160,7 @@ class EditAccessPointForm(EditMachineForm): class EditSwitchForm(EditMachineForm): - """Permet d'éditer un switch : nom et nombre de ports""" + """Form used to edit switches.""" class Meta: model = Switch @@ -177,15 +168,14 @@ class EditSwitchForm(EditMachineForm): class NewSwitchForm(NewMachineForm): - """Permet de créer un switch : emplacement, paramètres machine, - membre d'un stack (option), nombre de ports (number)""" + """Form used to create a switch.""" class Meta(EditSwitchForm.Meta): fields = ["name", "switchbay", "number", "stack", "stack_member_id"] class EditRoomForm(FormRevMixin, ModelForm): - """Permet d'éediter le nom et commentaire d'une prise murale""" + """Form used to edit a room.""" class Meta: model = Room @@ -197,14 +187,14 @@ class EditRoomForm(FormRevMixin, ModelForm): class CreatePortsForm(forms.Form): - """Permet de créer une liste de ports pour un switch.""" + """Form used to create switch ports lists.""" begin = forms.IntegerField(label=_("Start:"), min_value=0) end = forms.IntegerField(label=_("End:"), min_value=0) class EditModelSwitchForm(FormRevMixin, ModelForm): - """Permet d'éediter un modèle de switch : nom et constructeur""" + """Form used to edit switch models.""" members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False) @@ -226,7 +216,7 @@ class EditModelSwitchForm(FormRevMixin, ModelForm): class EditConstructorSwitchForm(FormRevMixin, ModelForm): - """Permet d'éediter le nom d'un constructeur""" + """Form used to edit switch constructors.""" class Meta: model = ConstructorSwitch @@ -238,7 +228,7 @@ class EditConstructorSwitchForm(FormRevMixin, ModelForm): class EditSwitchBayForm(FormRevMixin, ModelForm): - """Permet d'éditer une baie de brassage""" + """Form used to edit switch bays.""" members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False) @@ -260,7 +250,7 @@ class EditSwitchBayForm(FormRevMixin, ModelForm): class EditBuildingForm(FormRevMixin, ModelForm): - """Permet d'éditer le batiment""" + """Form used to edit buildings.""" class Meta: model = Building @@ -272,7 +262,7 @@ class EditBuildingForm(FormRevMixin, ModelForm): class EditDormitoryForm(FormRevMixin, ModelForm): - """Enable dormitory edition""" + """Form used to edit dormitories.""" class Meta: model = Dormitory @@ -284,7 +274,7 @@ class EditDormitoryForm(FormRevMixin, ModelForm): class EditPortProfileForm(FormRevMixin, ModelForm): - """Form to edit a port profile""" + """Form used to edit port profiles.""" class Meta: model = PortProfile @@ -296,7 +286,7 @@ class EditPortProfileForm(FormRevMixin, ModelForm): class EditModuleForm(FormRevMixin, ModelForm): - """Add and edit module instance""" + """Form used to add and edit switch modules.""" class Meta: model = ModuleSwitch @@ -308,7 +298,7 @@ class EditModuleForm(FormRevMixin, ModelForm): class EditSwitchModuleForm(FormRevMixin, ModelForm): - """Add/edit a switch to a module""" + """Form used to add and edit modules related to a switch.""" class Meta: model = ModuleOnSwitch diff --git a/topologie/models.py b/topologie/models.py index a184bde4..0dbb0e6a 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -21,18 +21,15 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Definition des modèles de l'application topologie. +Definition of models for the 'topologie' app. -On défini les models suivants : - -- stack (id, id_min, id_max et nom) regrouppant les switches -- switch : nom, nombre de port, et interface -machine correspondante (mac, ip, etc) (voir machines.models.interface) -- Port: relié à un switch parent par foreign_key, numero du port, -relié de façon exclusive à un autre port, une machine -(serveur ou borne) ou une prise murale -- room : liste des prises murales, nom et commentaire de l'état de -la prise +The following models are defined: + * stack (grouping switches): id, id_min, id_max and name + * switch: name, number of ports, related interface and machine (MAC + address, IP address etc.) (see machines.models.interface) + * port: related to a switch by foreign_key, number of the port, related + exclusively to another port, machine (server or AP) or room outlets + * room: list of outlets, name and comments about the plug's state """ from __future__ import unicode_literals @@ -56,9 +53,15 @@ from re2o.mixins import AclMixin, RevMixin class Stack(AclMixin, RevMixin, models.Model): - """Un objet stack. Regrouppe des switchs en foreign key - ,contient une id de stack, un switch id min et max dans - le stack""" + """Switch stack. + + Attributes: + name: the name of the stack. + stack_id: the ID of the stack, as a text chosen by the user. + details: the description to provide details about the stack. + member_id_min: the minimum switch ID in the stack. + member_id_max: the maximum switch ID in the stack. + """ name = models.CharField(max_length=32, blank=True, null=True) stack_id = models.CharField(max_length=32, unique=True) @@ -89,9 +92,10 @@ class Stack(AclMixin, RevMixin, models.Model): class AccessPoint(Machine): - """Define a wireless AP. Inherit from machines.interfaces + """Wireless Access Point. Inherits from machines.interfaces. - Definition pour une borne wifi , hérite de machines.interfaces + Attributes: + location: the text to provide details about the AP's location. """ location = models.CharField( @@ -107,17 +111,16 @@ class AccessPoint(Machine): verbose_name_plural = _("access points") def port(self): - """Return the queryset of ports for this device""" + """Return the queryset of ports for this device.""" return Port.objects.filter(machine_interface__machine=self) def switch(self): - """Return the switch where this is plugged""" + """Return the switch where this is plugged.""" return Switch.objects.filter(ports__machine_interface__machine=self) 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 switches + connected to...). """ return Building.objects.filter(switchbay__switch=self.switch()) @@ -127,7 +130,14 @@ class AccessPoint(Machine): @classmethod def all_ap_in(cls, building_instance): - """Get a building as argument, returns all ap of a building""" + """Get all the APs of the given building. + + Args: + building_instance: the building used to find APs. + + Returns: + The queryset of all APs in the given building. + """ return cls.objects.filter( interface__port__switch__switchbay__building=building_instance ) @@ -158,25 +168,24 @@ class AccessPoint(Machine): 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 def port(self): - """Return the queryset of ports for this device""" + """Return the queryset of ports for this device.""" return Port.objects.filter(machine_interface__machine=self) def switch(self): - """Return the switch where this is plugged""" + """Return the switch where this is plugged.""" return Switch.objects.filter(ports__machine_interface__machine=self) 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 switches + connected to...). """ return Building.objects.filter(switchbay__switch=self.switch()) @@ -186,7 +195,14 @@ class Server(Machine): @classmethod def all_server_in(cls, building_instance): - """Get a building as argument, returns all server of a building""" + """Get all the servers of the given building. + + Args: + building_instance: the building used to find servers. + + Returns: + The queryset of all servers in the given building. + """ return cls.objects.filter( interface__port__switch__switchbay__building=building_instance ).exclude(accesspoint__isnull=False) @@ -217,17 +233,19 @@ class Server(Machine): class Switch(Machine): - """ Definition d'un switch. Contient un nombre de ports (number), - un emplacement (location), un stack parent (optionnel, stack) - et un id de membre dans le stack (stack_member_id) - relié en onetoone à une interface - Pourquoi ne pas avoir fait hériter switch de interface ? - Principalement par méconnaissance de la puissance de cette façon de faire. - Ceci étant entendu, django crée en interne un onetoone, ce qui a un - effet identique avec ce que l'on fait ici + """Switch. - Validation au save que l'id du stack est bien dans le range id_min - id_max de la stack parente""" + Attributes: + number: the number of ports of the switch. + stack: the stack the switch is a part of. + stack_member_id: the ID of the switch in the related stack. + model: the model of the switch. + switchbay: the bay in which the switch is located. + radius_key: the RADIUS key of the switch. + management_creds: the management credentials of the switch. + automatic_provision: whether automatic provision is enabled for the + switch. + """ number = models.PositiveIntegerField(help_text=_("Number of ports.")) stack = models.ForeignKey( @@ -269,8 +287,9 @@ class Switch(Machine): verbose_name_plural = _("switches") def clean(self): - """ Verifie que l'id stack est dans le bon range - Appelle également le clean de la classe parente""" + """Check if the stack member ID is in the range of the stack's IDs and + calls the clean of the parent class. + """ super(Switch, self).clean() if self.stack is not None: if self.stack_member_id is not None: @@ -293,6 +312,12 @@ class Switch(Machine): def create_ports(self, begin, end): """ Crée les ports de begin à end si les valeurs données sont cohérentes. """ + """Create ports for the switch if the values are consistent. + + Args: + begin: the number of the start port. + end: the number of the end port. + """ if end < begin: raise ValidationError(_("The end port is less than the start port.")) ports_to_create = range(begin, end + 1) @@ -313,8 +338,7 @@ class Switch(Machine): ) def main_interface(self): - """ Returns the 'main' interface of the switch - It must the the management interface for that device""" + """Get the main interface of the switch (the management interface).""" switch_iptype = OptionalTopologie.get_cached_value("switchs_ip_type") if switch_iptype: return ( @@ -329,12 +353,14 @@ class Switch(Machine): @cached_property def get_radius_key(self): - """Retourne l'objet de la clef radius de ce switch""" + """Get the RADIUS key object related to the 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""" + """Get the RADIUS key as a string, or None if there are no RADIUS key + related to the switch. + """ if self.get_radius_key: return self.get_radius_key.radius_key else: @@ -362,7 +388,7 @@ class Switch(Machine): @cached_property def get_management_cred(self): - """Retourne l'objet des creds de managament de ce switch""" + """Get the management credentials objects of the switch.""" return ( self.management_creds or SwitchManagementCred.objects.filter(default_switch=True).first() @@ -370,7 +396,9 @@ class Switch(Machine): @cached_property def get_management_cred_value(self): - """Retourne un dict des creds de management du switch""" + """Get the management credentials as a dictionary, or None if there are + no management credentials related to the switch. + """ if self.get_management_cred: return { "id": self.get_management_cred.management_id, @@ -401,17 +429,19 @@ class Switch(Machine): @cached_property def ipv4(self): - """Return the switch's management ipv4""" + """Get the IPv4 address of the switch's management interface.""" return str(self.main_interface().ipv4) @cached_property def ipv6(self): - """Returne the switch's management ipv6""" + """Get the IPv6 address of the switch's management interface.""" return str(self.main_interface().ipv6().first()) @cached_property def interfaces_subnet(self): - """Return dict ip:subnet for all ip of the switch""" + """Get a dictionary of IPv4 addresses:subnets of all the switch's + interfaces. + """ return dict( ( str(interface.ipv4), @@ -423,7 +453,9 @@ class Switch(Machine): @cached_property def interfaces6_subnet(self): - """Return dict ip6:subnet for all ipv6 of the switch""" + """Get a dictionary of IPv6 addresses:subnets of all the switch's + interfaces. + """ return dict( ( str(interface.ipv6().first()), @@ -434,7 +466,9 @@ class Switch(Machine): @cached_property def list_modules(self): - """Return modules of that switch, list of dict (rank, reference)""" + """Get the list of dictionaries (rank, reference) of modules related to + the switch. + """ modules = [] if getattr(self.model, "is_modular", None): if self.model.is_itself_module: @@ -445,7 +479,7 @@ class Switch(Machine): @cached_property def get_dormitory(self): - """Returns the dormitory of that switch""" + """Get the dormitory in which the switch is located.""" if self.switchbay: return self.switchbay.building.dormitory else: @@ -453,19 +487,19 @@ class Switch(Machine): @classmethod def nothing_profile(cls): - """Return default nothing port profile""" + """Return default nothing port profile.""" nothing_profile, _created = PortProfile.objects.get_or_create( profil_default="nothing", name="nothing", radius_type="NO" ) return nothing_profile def profile_type_or_nothing(self, profile_type): - """Return the profile for a profile_type of this switch + """Return the profile for a profile_type of this switch. - If exists, returns the defined default profile for a profile type on the dormitory which - the switch belongs - - Otherwise, returns the nothing profile""" + If it exists, return the defined default profile for a profile type on + the dormitory which the switch belongs. + Otherwise, return the nothing profile. + """ profile_queryset = PortProfile.objects.filter(profil_default=profile_type) if self.get_dormitory: port_profile = ( @@ -478,22 +512,22 @@ class Switch(Machine): @cached_property def default_uplink_profile(self): - """Default uplink profile for that switch -- in cache""" + """Default uplink profile for that switch -- in cache.""" return self.profile_type_or_nothing("uplink") @cached_property def default_access_point_profile(self): - """Default ap profile for that switch -- in cache""" + """Default AP profile for that switch -- in cache.""" return self.profile_type_or_nothing("access_point") @cached_property def default_room_profile(self): - """Default room profile for that switch -- in cache""" + """Default room profile for that switch -- in cache.""" return self.profile_type_or_nothing("room") @cached_property def default_asso_machine_profile(self): - """Default asso machine profile for that switch -- in cache""" + """Default asso machine profile for that switch -- in cache.""" return self.profile_type_or_nothing("asso_machine") def __str__(self): @@ -522,7 +556,16 @@ class Switch(Machine): class ModelSwitch(AclMixin, RevMixin, models.Model): - """Un modèle (au sens constructeur) de switch""" + """Switch model. + + Attributes: + reference: the reference of the switch model. + commercial_name: the commercial name of the switch model. + constructor: the constructor of the switch model. + firmware: the firmware of the switch model. + is_modular: whether the switch model is modular. + is_itself_module: whether the switch is considered as a module. + """ reference = models.CharField(max_length=255) commercial_name = models.CharField(max_length=255, null=True, blank=True) @@ -550,7 +593,12 @@ class ModelSwitch(AclMixin, RevMixin, models.Model): class ModuleSwitch(AclMixin, RevMixin, models.Model): - """A module of a switch""" + """Switch module. + + Attributes: + reference: the reference of the switch module. + comment: the comment to describe the switch module. + """ reference = models.CharField( max_length=255, @@ -575,7 +623,13 @@ class ModuleSwitch(AclMixin, RevMixin, models.Model): class ModuleOnSwitch(AclMixin, RevMixin, models.Model): - """Link beetween module and switch""" + """Link beetween module and switch. + + Attributes: + module: the switch module related to the link. + switch: the switch related to the link. + slot: the slot on the switch related to the link. + """ module = models.ForeignKey("ModuleSwitch", on_delete=models.CASCADE) switch = models.ForeignKey("Switch", on_delete=models.CASCADE) @@ -601,7 +655,11 @@ class ModuleOnSwitch(AclMixin, RevMixin, models.Model): class ConstructorSwitch(AclMixin, RevMixin, models.Model): - """Un constructeur de switch""" + """Switch constructor. + + Attributes: + name: the name of the switch constructor. + """ name = models.CharField(max_length=255) @@ -617,7 +675,13 @@ class ConstructorSwitch(AclMixin, RevMixin, models.Model): class SwitchBay(AclMixin, RevMixin, models.Model): - """Une baie de brassage""" + """Switch bay. + + Attributes: + name: the name of the switch bay. + building: the building in which the switch bay is located. + info: the information to describe to switch bay. + """ name = models.CharField(max_length=255) building = models.ForeignKey("Building", on_delete=models.PROTECT) @@ -633,8 +697,11 @@ class SwitchBay(AclMixin, RevMixin, models.Model): class Dormitory(AclMixin, RevMixin, models.Model): - """A student accomodation/dormitory - Une résidence universitaire""" + """Dormitory. + + Attributes: + name: the name of the dormitory. + """ name = models.CharField(max_length=255) @@ -644,7 +711,7 @@ class Dormitory(AclMixin, RevMixin, models.Model): verbose_name_plural = _("dormitories") def all_ap_in(self): - """Returns all ap of the dorms""" + """Get all the APs in the dormitory.""" return AccessPoint.all_ap_in(self.building_set.all()) @classmethod @@ -660,8 +727,13 @@ class Dormitory(AclMixin, RevMixin, models.Model): class Building(AclMixin, RevMixin, models.Model): - """A building of a dormitory - Un batiment""" + """Building. + + Attributes: + name: the name of the building. + dormitory: the dormitory of the building (a Dormitory can contain + multiple dormitories). + """ name = models.CharField(max_length=255) dormitory = models.ForeignKey("Dormitory", on_delete=models.PROTECT) @@ -672,7 +744,7 @@ class Building(AclMixin, RevMixin, models.Model): verbose_name_plural = _("buildings") def all_ap_in(self): - """Returns all ap of the building""" + """Get all the APs in the building.""" return AccessPoint.all_ap_in(self) def get_name(self): @@ -690,21 +762,32 @@ class Building(AclMixin, RevMixin, models.Model): class Port(AclMixin, RevMixin, models.Model): - """ Definition d'un port. Relié à un switch(foreign_key), - un port peut etre relié de manière exclusive à : - - une chambre (room) - - une machine (serveur etc) (machine_interface) - - un autre port (uplink) (related) - Champs supplémentaires : - - RADIUS (mode STRICT : connexion sur port uniquement si machine - d'un adhérent à jour de cotisation et que la chambre est également à - jour de cotisation - mode COMMON : vérification uniquement du statut de la machine - mode NO : accepte toute demande venant du port et place sur le vlan normal - mode BLOQ : rejet de toute authentification - - vlan_force : override la politique générale de placement vlan, permet - de forcer un port sur un vlan particulier. S'additionne à la politique - RADIUS""" + """Port of a switch. + + A port is related exclusively to either: + * a room + * a machine, e.g. server + * another port + Behaviour according to the RADIUS mode: + * STRICT: connection only if the machine and room have access + * COMMON: check only the machine's state + * NO: accept only request coming from the port and set on the standard + VLAN. + * BLOQ: reject all requests. + The VLAN can be forced to override the general policy for VLAN setting. + This enables to force a port to a particular VLAN. It adds to the RADIUS + policy. + + Attributes: + switch: the switch to which the port belongs. + port: the port number on the switch for the Port object. + room: the room to which the port is related. + machine_interface: the machine to which the port is related + related: the other port to which is port is related. + custom_profile: the port profile of the port. + state: whether the port is active. + details: the details to describre the port. + """ switch = models.ForeignKey("Switch", related_name="ports", on_delete=models.CASCADE) port = models.PositiveIntegerField() @@ -733,7 +816,7 @@ class Port(AclMixin, RevMixin, models.Model): @cached_property def pretty_name(self): - """More elaborated name for label on switch conf""" + """More elaborated name for label on switch configuration.""" if self.related: return _("Uplink: ") + self.related.switch.short_name elif self.machine_interface: @@ -745,14 +828,13 @@ class Port(AclMixin, RevMixin, models.Model): @cached_property def get_port_profile(self): - """Return the config profil for this port - :returns: the profile of self (port) - - If is defined a custom profile, returns it - elIf a default profile is defined for its dormitory, returns it - Else, returns the global default profil - If not exists, create a nothing profile""" + """Get the configuration profile for this port. + Returns: + The custom profile if it exists, else the default profile of the + dormitory if it exists, else the global default profile, else the + nothing profile. + """ if self.custom_profile: return self.custom_profile elif self.related: @@ -779,26 +861,25 @@ class Port(AclMixin, RevMixin, models.Model): ) def make_port_related(self): - """ Synchronise le port distant sur self""" + """Synchronise the related port with self.""" related_port = self.related related_port.related = self related_port.save() def clean_port_related(self): - """ Supprime la relation related sur self""" + """Delete the related relation on self.""" related_port = self.related_port related_port.related = None related_port.save() def clean(self): - """ Verifie que un seul de chambre, interface_parent et related_port - est rempli. Verifie que le related n'est pas le port lui-même.... - Verifie que le related n'est pas déjà occupé par une machine ou une - chambre. Si ce n'est pas le cas, applique la relation related - Si un port related point vers self, on nettoie la relation - A priori pas d'autre solution que de faire ça à la main. A priori - tout cela est dans un bloc transaction, donc pas de problème de - cohérence""" + """ + Check if the port is only related exclusively to either a room, a + machine or another port. + Check if the related port is not self and applies the relation to the + related port if the relation is correct. + Delete the relation if it points to self. + """ if hasattr(self, "switch"): if self.port > self.switch.number: raise ValidationError( @@ -835,7 +916,13 @@ class Port(AclMixin, RevMixin, models.Model): class Room(AclMixin, RevMixin, models.Model): - """Une chambre/local contenant une prise murale""" + """Room. + + Attributes: + name: the name of the room. + details: the details describing the room. + building: the building in which the room is located. + """ name = models.CharField(max_length=255) details = models.CharField(max_length=255, blank=True) @@ -853,7 +940,28 @@ class Room(AclMixin, RevMixin, models.Model): class PortProfile(AclMixin, RevMixin, models.Model): - """Contains the information of the ports' configuration for a switch""" + """Port profile. + + Contains the information of the ports' configuration for a switch. + + Attributes: + name: the name of the port profile. + profil_default: the type of default profile (room, AP, uplink etc.). + on_dormitory: the dormitory with this default port profile. + vlan_untagged: the VLAN untagged of the port profile. + vlan_tagged: the VLAN(s) tagged of the port profile. + radius_type: the type of RADIUS authentication (inactive, MAC-address + or 802.1X) of the port profile. + radius_mode: the RADIUS mode of the port profile. + speed: the port speed limit of the port profile. + mac_limit: the MAC limit of the port profile. + flow_control: whether flow control is enabled. + dhcp_snooping: whether DHCP snooping is enabled. + dhcpv6_snooping: whether DHCPv6 snooping is enabled. + arp_protect: whether ARP protection is enabled. + ra_guard: whether RA guard is enabled. + loop_protect: whether loop protection is enabled. + """ TYPES = (("NO", "NO"), ("802.1X", "802.1X"), ("MAC-radius", _("MAC-RADIUS"))) MODES = (("STRICT", "STRICT"), ("COMMON", "COMMON")) @@ -983,7 +1091,7 @@ class PortProfile(AclMixin, RevMixin, models.Model): return ",".join(self.security_parameters_enabled) def clean(self): - """ Check that there is only one generic profil default""" + """Check that there is only one generic profile default.""" super(PortProfile, self).clean() if ( self.profil_default @@ -1007,21 +1115,21 @@ class PortProfile(AclMixin, RevMixin, models.Model): @receiver(post_save, sender=AccessPoint) def ap_post_save(**_kwargs): - """Regeneration des noms des bornes vers le controleur""" + """Regenerate the AP names towards the controller.""" 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""" + """Regenerate the AP names towards the controller.""" 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""" + """Empty the stack member ID of switches when a stack is deleted.""" Switch.objects.filter(stack=None).update(stack_member_id=None) diff --git a/topologie/views.py b/topologie/views.py index 3d3f742a..3b1aad30 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -20,18 +20,17 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Page des vues de l'application topologie +Views for the 'topologie' app of re2o. -Permet de créer, modifier et supprimer : -- un port (add_port, edit_port, del_port) -- un switch : les vues d'ajout et d'édition font appel aux forms de creation -de switch, mais aussi aux forms de machines.forms (domain, interface et -machine). Le views les envoie et les save en même temps. TODO : rationaliser -et faire que la creation de machines (interfaces, domain etc) soit gérée -coté models et forms de topologie -- une chambre (new_room, edit_room, del_room) -- une stack -- l'historique de tous les objets cités +They are used to create, edit and delete: + * a port (add_port, edit_port, del_port) + * a switch: the views call forms for switches but also machines (domain, + interface and machine), send and save them at the same time. + TODO rationalise, enforce the creation of machines (interfaces, domains + etc.) in models and forms from 'topologie' + * a room (new_room, edit_room, del_room) + * a stack + * histories of all objects mentioned. """ from __future__ import unicode_literals @@ -105,7 +104,7 @@ from os.path import isfile @login_required @can_view_all(Switch) def index(request): - """ Vue d'affichage de tous les swicthes""" + """View used to display all switches.""" switch_list = ( Switch.objects.prefetch_related( Prefetch( @@ -171,7 +170,7 @@ def index_port_profile(request): @can_view_all(Port) @can_view(Switch) def index_port(request, switch, switchid): - """ Affichage de l'ensemble des ports reliés à un switch particulier""" + """View used to display all ports related to the given switch.""" port_list = ( Port.objects.filter(switch=switch) .select_related("room__building__dormitory") @@ -204,7 +203,7 @@ def index_port(request, switch, switchid): @login_required @can_view_all(Room) def index_room(request): - """ Affichage de l'ensemble des chambres""" + """View used to display all rooms.""" room_list = Room.objects.select_related("building__dormitory") room_list = SortTable.sort( room_list, @@ -220,7 +219,7 @@ def index_room(request): @login_required @can_view_all(AccessPoint) def index_ap(request): - """ Affichage de l'ensemble des bornes""" + """View used to display all APs.""" ap_list = AccessPoint.objects.prefetch_related( Prefetch( "interface_set", @@ -245,7 +244,7 @@ def index_ap(request): @login_required @can_view_all(Stack, Building, Dormitory, SwitchBay) def index_physical_grouping(request): - """Affichage de la liste des stacks (affiche l'ensemble des switches)""" + """View used to display the list of stacks (display all switches).""" stack_list = Stack.objects.prefetch_related( "switch_set__interface_set__domain__extension" ) @@ -293,7 +292,7 @@ def index_physical_grouping(request): @login_required @can_view_all(ModelSwitch, ConstructorSwitch) def index_model_switch(request): - """ Affichage de l'ensemble des modèles de switches""" + """View used to display all switch models.""" model_switch_list = ModelSwitch.objects.select_related( "constructor" ).prefetch_related("switch_set__interface_set__domain") @@ -323,7 +322,7 @@ def index_model_switch(request): @login_required @can_view_all(ModuleSwitch) def index_module(request): - """Display all modules of switchs""" + """View used to display all switch modules.""" module_list = ModuleSwitch.objects.all() modular_switchs = ( Switch.objects.filter(model__is_modular=True) @@ -342,7 +341,7 @@ def index_module(request): @login_required @can_edit(Vlan) def edit_vlanoptions(request, vlan_instance, **_kwargs): - """ View used to edit options for switch of VLAN object """ + """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: @@ -357,7 +356,7 @@ def edit_vlanoptions(request, vlan_instance, **_kwargs): @login_required @can_create(Port) def new_port(request, switchid): - """ Nouveau port""" + """View used to create ports.""" try: switch = Switch.objects.get(pk=switchid) except Switch.DoesNotExist: @@ -383,9 +382,10 @@ def new_port(request, switchid): @login_required @can_edit(Port) def edit_port(request, port_object, **_kwargs): - """ Edition d'un port. Permet de changer le switch parent et - l'affectation du port""" + """View used to edit ports. + It enables to change the related switch and the port assignment. + """ port = EditPortForm(request.POST or None, instance=port_object) if port.is_valid(): if port.changed_data: @@ -410,7 +410,7 @@ def edit_port(request, port_object, **_kwargs): @login_required @can_delete(Port) def del_port(request, port, **_kwargs): - """ Supprime le port""" + """View used to delete ports.""" if request.method == "POST": try: port.delete() @@ -435,7 +435,7 @@ def del_port(request, port, **_kwargs): @login_required @can_create(Stack) def new_stack(request): - """Ajoute un nouveau stack : stackid_min, max, et nombre de switches""" + """View used to create stacks.""" stack = StackForm(request.POST or None) if stack.is_valid(): stack.save() @@ -449,7 +449,7 @@ def new_stack(request): @login_required @can_edit(Stack) def edit_stack(request, stack, **_kwargs): - """Edition d'un stack (nombre de switches, nom...)""" + """View used to edit stacks.""" stack = StackForm(request.POST or None, instance=stack) if stack.is_valid(): if stack.changed_data: @@ -464,7 +464,7 @@ def edit_stack(request, stack, **_kwargs): @login_required @can_delete(Stack) def del_stack(request, stack, **_kwargs): - """Supprime un stack""" + """View used to delete stacks.""" if request.method == "POST": try: stack.delete() @@ -487,8 +487,7 @@ def del_stack(request, stack, **_kwargs): @login_required @can_edit(Stack) def edit_switchs_stack(request, stack, **_kwargs): - """Permet d'éditer la liste des switches dans une stack et l'ajouter""" - + """View used to edit the list of switches of the given stack.""" if request.method == "POST": pass else: @@ -500,9 +499,12 @@ def edit_switchs_stack(request, stack, **_kwargs): @login_required @can_create(Switch) def new_switch(request): - """ Creation d'un switch. Cree en meme temps l'interface et la machine - associée. Vue complexe. Appelle successivement les 4 models forms - adaptés : machine, interface, domain et switch""" + """View used to create switches. + + At the same time, it creates the related interface and machine. The view + successively calls the 4 appropriate forms: machine, interface, domain and + switch. + """ switch = NewSwitchForm(request.POST or None, user=request.user) interface = AddInterfaceForm(request.POST or None, user=request.user) domain = DomainForm(request.POST or None, user=request.user) @@ -549,7 +551,7 @@ def new_switch(request): @login_required @can_create(Port) def create_ports(request, switchid): - """ Création d'une liste de ports pour un switch.""" + """View used to create port lists for the given switch.""" try: switch = Switch.objects.get(pk=switchid) except Switch.DoesNotExist: @@ -578,9 +580,11 @@ def create_ports(request, switchid): @login_required @can_edit(Switch) def edit_switch(request, switch, switchid): - """ Edition d'un switch. Permet de chambre nombre de ports, - place dans le stack, interface et machine associée""" + """View used to edit switches. + It enables to change the number of ports, location in the stack, or the + related interface and machine. + """ switch_form = EditSwitchForm( request.POST or None, instance=switch, user=request.user ) @@ -622,9 +626,11 @@ def edit_switch(request, switch, switchid): @login_required @can_create(AccessPoint) def new_ap(request): - """ Creation d'une ap. Cree en meme temps l'interface et la machine - associée. Vue complexe. Appelle successivement les 3 models forms - adaptés : machine, interface, domain et switch""" + """View used to create APs. + + At the same time, it creates the related interface and machine. The view + successively calls the 3 appropriate forms: machine, interface, domain. + """ ap = AddAccessPointForm(request.POST or None, user=request.user) interface = AddInterfaceForm(request.POST or None, user=request.user) domain = DomainForm(request.POST or None, user=request.user) @@ -671,8 +677,7 @@ def new_ap(request): @login_required @can_edit(AccessPoint) def edit_ap(request, ap, **_kwargs): - """ Edition d'un switch. Permet de chambre nombre de ports, - place dans le stack, interface et machine associée""" + """View used to edit APs.""" interface_form = EditInterfaceForm( request.POST or None, user=request.user, instance=ap.interface_set.first() ) @@ -723,7 +728,7 @@ def edit_ap(request, ap, **_kwargs): @login_required @can_create(Room) def new_room(request): - """Nouvelle chambre """ + """View used to create rooms.""" room = EditRoomForm(request.POST or None) if room.is_valid(): room.save() @@ -737,7 +742,7 @@ def new_room(request): @login_required @can_edit(Room) def edit_room(request, room, **_kwargs): - """ Edition numero et details de la chambre""" + """View used to edit rooms.""" room = EditRoomForm(request.POST or None, instance=room) if room.is_valid(): if room.changed_data: @@ -752,7 +757,7 @@ def edit_room(request, room, **_kwargs): @login_required @can_delete(Room) def del_room(request, room, **_kwargs): - """ Suppression d'un chambre""" + """View used to delete rooms.""" if request.method == "POST": try: room.delete() @@ -777,7 +782,7 @@ def del_room(request, room, **_kwargs): @login_required @can_create(ModelSwitch) def new_model_switch(request): - """Nouveau modèle de switch""" + """View used to create switch models.""" model_switch = EditModelSwitchForm(request.POST or None) if model_switch.is_valid(): model_switch.save() @@ -793,8 +798,7 @@ def new_model_switch(request): @login_required @can_edit(ModelSwitch) def edit_model_switch(request, model_switch, **_kwargs): - """ Edition d'un modèle de switch""" - + """View used to edit switch models.""" model_switch = EditModelSwitchForm(request.POST or None, instance=model_switch) if model_switch.is_valid(): if model_switch.changed_data: @@ -811,7 +815,7 @@ def edit_model_switch(request, model_switch, **_kwargs): @login_required @can_delete(ModelSwitch) def del_model_switch(request, model_switch, **_kwargs): - """ Suppression d'un modèle de switch""" + """View used to delete switch models.""" if request.method == "POST": try: model_switch.delete() @@ -838,7 +842,7 @@ def del_model_switch(request, model_switch, **_kwargs): @login_required @can_create(SwitchBay) def new_switch_bay(request): - """Nouvelle baie de switch""" + """View used to create switch bays.""" switch_bay = EditSwitchBayForm(request.POST or None) if switch_bay.is_valid(): switch_bay.save() @@ -854,7 +858,7 @@ def new_switch_bay(request): @login_required @can_edit(SwitchBay) def edit_switch_bay(request, switch_bay, **_kwargs): - """ Edition d'une baie de switch""" + """View used to edit switch bays.""" switch_bay = EditSwitchBayForm(request.POST or None, instance=switch_bay) if switch_bay.is_valid(): if switch_bay.changed_data: @@ -871,7 +875,7 @@ def edit_switch_bay(request, switch_bay, **_kwargs): @login_required @can_delete(SwitchBay) def del_switch_bay(request, switch_bay, **_kwargs): - """ Suppression d'une baie de switch""" + """View used to delete switch bays.""" if request.method == "POST": try: switch_bay.delete() @@ -898,8 +902,7 @@ def del_switch_bay(request, switch_bay, **_kwargs): @login_required @can_create(Building) def new_building(request): - """New Building of a dorm - Nouveau batiment""" + """View used to create buildings.""" building = EditBuildingForm(request.POST or None) if building.is_valid(): building.save() @@ -915,8 +918,7 @@ def new_building(request): @login_required @can_edit(Building) def edit_building(request, building, **_kwargs): - """Edit a building - Edition d'un batiment""" + """View used to edit buildings.""" building = EditBuildingForm(request.POST or None, instance=building) if building.is_valid(): if building.changed_data: @@ -931,8 +933,7 @@ def edit_building(request, building, **_kwargs): @login_required @can_delete(Building) def del_building(request, building, **_kwargs): - """Delete a building - Suppression d'un batiment""" + """View used to delete buildings.""" if request.method == "POST": try: building.delete() @@ -959,8 +960,7 @@ def del_building(request, building, **_kwargs): @login_required @can_create(Dormitory) def new_dormitory(request): - """A new dormitory - Nouvelle residence""" + """View used to create dormitories.""" dormitory = EditDormitoryForm(request.POST or None) if dormitory.is_valid(): dormitory.save() @@ -976,8 +976,7 @@ def new_dormitory(request): @login_required @can_edit(Dormitory) def edit_dormitory(request, dormitory, **_kwargs): - """Edit a dormitory - Edition d'une residence""" + """View used to edit dormitories.""" dormitory = EditDormitoryForm(request.POST or None, instance=dormitory) if dormitory.is_valid(): if dormitory.changed_data: @@ -994,8 +993,7 @@ def edit_dormitory(request, dormitory, **_kwargs): @login_required @can_delete(Dormitory) def del_dormitory(request, dormitory, **_kwargs): - """Delete a dormitory - Suppression d'une residence""" + """View used to delete dormitories.""" if request.method == "POST": try: dormitory.delete() @@ -1022,7 +1020,7 @@ def del_dormitory(request, dormitory, **_kwargs): @login_required @can_create(ConstructorSwitch) def new_constructor_switch(request): - """Nouveau constructeur de switch""" + """View used to create switch constructors.""" constructor_switch = EditConstructorSwitchForm(request.POST or None) if constructor_switch.is_valid(): constructor_switch.save() @@ -1038,8 +1036,7 @@ def new_constructor_switch(request): @login_required @can_edit(ConstructorSwitch) def edit_constructor_switch(request, constructor_switch, **_kwargs): - """ Edition d'un constructeur de switch""" - + """View used to edit switch constructors.""" constructor_switch = EditConstructorSwitchForm( request.POST or None, instance=constructor_switch ) @@ -1058,7 +1055,7 @@ def edit_constructor_switch(request, constructor_switch, **_kwargs): @login_required @can_delete(ConstructorSwitch) def del_constructor_switch(request, constructor_switch, **_kwargs): - """ Suppression d'un constructeur de switch""" + """View used to delete switch constructors.""" if request.method == "POST": try: constructor_switch.delete() @@ -1085,7 +1082,7 @@ def del_constructor_switch(request, constructor_switch, **_kwargs): @login_required @can_create(PortProfile) def new_port_profile(request): - """Create a new port profile""" + """View used to create port profiles.""" port_profile = EditPortProfileForm(request.POST or None) if port_profile.is_valid(): port_profile.save() @@ -1101,7 +1098,7 @@ def new_port_profile(request): @login_required @can_edit(PortProfile) def edit_port_profile(request, port_profile, **_kwargs): - """Edit a port profile""" + """View used to edit port profiles.""" port_profile = EditPortProfileForm(request.POST or None, instance=port_profile) if port_profile.is_valid(): if port_profile.changed_data: @@ -1118,7 +1115,7 @@ def edit_port_profile(request, port_profile, **_kwargs): @login_required @can_delete(PortProfile) def del_port_profile(request, port_profile, **_kwargs): - """Delete a port profile""" + """View used to delete port profiles.""" if request.method == "POST": try: port_profile.delete() @@ -1136,7 +1133,7 @@ def del_port_profile(request, port_profile, **_kwargs): @login_required @can_create(ModuleSwitch) def add_module(request): - """ View used to add a Module object """ + """View used to create switch modules.""" module = EditModuleForm(request.POST or None) if module.is_valid(): module.save() @@ -1150,7 +1147,7 @@ def add_module(request): @login_required @can_edit(ModuleSwitch) def edit_module(request, module_instance, **_kwargs): - """ View used to edit a Module object """ + """View used to edit switch modules.""" module = EditModuleForm(request.POST or None, instance=module_instance) if module.is_valid(): if module.changed_data: @@ -1165,7 +1162,7 @@ def edit_module(request, module_instance, **_kwargs): @login_required @can_delete(ModuleSwitch) def del_module(request, module, **_kwargs): - """Compleete delete a module""" + """View used to delete switch modules.""" if request.method == "POST": try: module.delete() @@ -1190,7 +1187,7 @@ def del_module(request, module, **_kwargs): @login_required @can_create(ModuleOnSwitch) def add_module_on(request): - """Add a module to a switch""" + """View used to add a module to a switch.""" module_switch = EditSwitchModuleForm(request.POST or None) if module_switch.is_valid(): module_switch.save() @@ -1206,7 +1203,7 @@ def add_module_on(request): @login_required @can_edit(ModuleOnSwitch) def edit_module_on(request, module_instance, **_kwargs): - """ View used to edit a Module object """ + """View used to edit a module on a switch.""" module = EditSwitchModuleForm(request.POST or None, instance=module_instance) if module.is_valid(): if module.changed_data: @@ -1221,7 +1218,7 @@ def edit_module_on(request, module_instance, **_kwargs): @login_required @can_delete(ModuleOnSwitch) def del_module_on(request, module, **_kwargs): - """Compleete delete a module""" + """View used to delete a module on a switch.""" if request.method == "POST": try: module.delete() @@ -1244,9 +1241,7 @@ def del_module_on(request, module, **_kwargs): def make_machine_graph(): - """ - Create the graph of switchs, machines and access points. - """ + """Create the graph of switches, machines and access points.""" dico = { "subs": [], "links": [], @@ -1284,7 +1279,7 @@ def make_machine_graph(): "machines": [], } ) - # Visit all switchs in this building + # Visit all switches in this building for switch in ( Switch.objects.filter(switchbay__building=building) .prefetch_related( @@ -1311,7 +1306,7 @@ def make_machine_graph(): "ports": [], } ) - # visit all ports of this switch and add the switchs linked to it + # visit all ports of this switch and add the switches linked to it for port in switch.ports.filter(related__isnull=False).select_related( "related__switch" ): @@ -1365,29 +1360,29 @@ def make_machine_graph(): 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 + # Update the lists of missings and already detected switches missing = [i for i in missing if i not in new_detected] detected += new_detected - # If the switch have no ports, don't explore it and hop to the next one + # If the switch has 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 + # Switches that are not connected or not in a building for switch in Switch.objects.filter(switchbay__isnull=True).exclude( ports__related__isnull=False ): dico["alone"].append({"id": switch.id, "name": switch.get_name}) - # generate the dot file + # Generate the dot file dot_data = generate_dot(dico, "topologie/graph_switch.dot") # 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) - unflatten = Popen( # unflatten the graph to make it look better + unflatten = Popen( # Unflatten the graph to make it look better ["unflatten", "-l", "3", f.name], stdout=PIPE ) - 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, @@ -1395,10 +1390,15 @@ def make_machine_graph(): 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""" + """Generate a dot file from the data and template given. + + Args: + data: dictionary passed to the template. + template: path to the dot template. + + Returns: + 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) @@ -1415,18 +1415,19 @@ def generate_dot(data, template): 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 + """Visit the switch and travel to the switches linked to it. + + Args: + switch_start: the switch to begin the visit on. + switch_before: the switch that you come from. None if switch_start is the first one. + detected: list of all switches already visited. None if switch_start is the first one. + + Returns: + A list of all the links found and a list of all the switches visited. """ detected.append(switch_start) - links_return = [] # list of dictionaries of the links to be detected - # create links to every switchs below + links_return = [] # List of dictionaries of the links to be detected + # Create links to every switches below for port in switch_start.ports.filter(related__isnull=False): # Not the switch that we come from, not the current switch if ( @@ -1440,11 +1441,11 @@ def recursive_switchs(switch_start, switch_before, detected): } links_return.append(links) # Add current and below levels links - # go down on every related switchs + # Go down on every related switches 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 + # Explore it and get the results links_down, detected = recursive_switchs( port.related.switch, switch_start, detected )