8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-16 00:13:12 +00:00

Merge branch 'graph_topo' into 'master'

branche de création de graph

See merge request federez/re2o!164
This commit is contained in:
chirac 2018-05-23 12:05:43 +02:00
commit d610ac6d93
6 changed files with 466 additions and 102 deletions

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-05-21 19:13
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('machines', '0080_auto_20180502_2334'),
]
operations = [
migrations.AlterField(
model_name='extension',
name='soa',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='machines.SOA'),
),
]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-05-21 19:13
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('machines', '0081_auto_20180521_1413'),
('topologie', '0059_auto_20180415_2249'),
]
operations = [
migrations.CreateModel(
name='Server',
fields=[
],
options={
'proxy': True,
},
bases=('machines.machine',),
),
]

View file

@ -40,7 +40,8 @@ from __future__ import unicode_literals
import itertools import itertools
from django.db import models from django.db import models
from django.db.models.signals import post_save, post_delete from django.db.models.signals import pre_save, post_save, post_delete
from django.utils.functional import cached_property
from django.dispatch import receiver from django.dispatch import receiver
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError from django.db import IntegrityError
@ -50,6 +51,11 @@ from reversion import revisions as reversion
from machines.models import Machine, regen from machines.models import Machine, regen
from re2o.mixins import AclMixin, RevMixin from re2o.mixins import AclMixin, RevMixin
from os.path import isfile
from os import remove
class Stack(AclMixin, RevMixin, models.Model): class Stack(AclMixin, RevMixin, models.Model):
"""Un objet stack. Regrouppe des switchs en foreign key """Un objet stack. Regrouppe des switchs en foreign key
@ -103,6 +109,70 @@ class AccessPoint(AclMixin, Machine):
("view_accesspoint", "Peut voir une borne"), ("view_accesspoint", "Peut voir une borne"),
) )
def port(self):
"""Return the queryset of ports for this device"""
return Port.objects.filter(
machine_interface__machine=self
)
def switch(self):
"""Return the switch where this is plugged"""
return Switch.objects.filter(
ports__machine_interface__machine=self
)
def building(self):
"""Return the building of the AP/Server (building of the switchs connected to...)"""
return Building.objects.filter(
switchbay__switch=self.switch()
)
@cached_property
def short_name(self):
return str(self.interface_set.first().domain.name)
@classmethod
def all_ap_in(cls, building_instance):
"""Get a building as argument, returns all ap of a building"""
return cls.objects.filter(interface__port__switch__switchbay__building=building_instance)
def __str__(self):
return str(self.interface_set.first())
class Server(Machine):
"""Dummy class, to retrieve servers of a building, or get switch of a server"""
class Meta:
proxy = True
def port(self):
"""Return the queryset of ports for this device"""
return Port.objects.filter(
machine_interface__machine=self
)
def switch(self):
"""Return the switch where this is plugged"""
return Switch.objects.filter(
ports__machine_interface__machine=self
)
def building(self):
"""Return the building of the AP/Server (building of the switchs connected to...)"""
return Building.objects.filter(
switchbay__switch=self.switch()
)
@cached_property
def short_name(self):
return str(self.interface_set.first().domain.name)
@classmethod
def all_server_in(cls, building_instance):
"""Get a building as argument, returns all server of a building"""
return cls.objects.filter(interface__port__switch__switchbay__building=building_instance).exclude(accesspoint__isnull=False)
def __str__(self): def __str__(self):
return str(self.interface_set.first()) return str(self.interface_set.first())
@ -422,15 +492,47 @@ class Room(AclMixin, RevMixin, models.Model):
def ap_post_save(**_kwargs): def ap_post_save(**_kwargs):
"""Regeneration des noms des bornes vers le controleur""" """Regeneration des noms des bornes vers le controleur"""
regen('unifi-ap-names') regen('unifi-ap-names')
regen("graph_topo")
@receiver(post_delete, sender=AccessPoint) @receiver(post_delete, sender=AccessPoint)
def ap_post_delete(**_kwargs): def ap_post_delete(**_kwargs):
"""Regeneration des noms des bornes vers le controleur""" """Regeneration des noms des bornes vers le controleur"""
regen('unifi-ap-names') regen('unifi-ap-names')
regen("graph_topo")
@receiver(post_delete, sender=Stack) @receiver(post_delete, sender=Stack)
def stack_post_delete(**_kwargs): def stack_post_delete(**_kwargs):
"""Vide les id des switches membres d'une stack supprimée""" """Vide les id des switches membres d'une stack supprimée"""
Switch.objects.filter(stack=None).update(stack_member_id=None) Switch.objects.filter(stack=None).update(stack_member_id=None)
@receiver(post_save, sender=Port)
def port_post_save(**_kwargs):
regen("graph_topo")
@receiver(post_delete, sender=Port)
def port_post_delete(**_kwargs):
regen("graph_topo")
@receiver(post_save, sender=ModelSwitch)
def modelswitch_post_save(**_kwargs):
regen("graph_topo")
@receiver(post_delete, sender=ModelSwitch)
def modelswitch_post_delete(**_kwargs):
regen("graph_topo")
@receiver(post_save, sender=Building)
def building_post_save(**_kwargs):
regen("graph_topo")
@receiver(post_delete, sender=Building)
def building_post_delete(**_kwargs):
regen("graph_topo")
@receiver(post_save, sender=Switch)
def switch_post_save(**_kwargs):
regen("graph_topo")
@receiver(post_delete, sender=Switch)
def switch_post_delete(**_kwargs):
regen("graph_topo")

View file

@ -0,0 +1,135 @@
{% block graph_dot %}
strict digraph {
graph [label="TOPOLOGIE DU RÉSEAU", labelloc=t, fontsize=40];
node [fontname=Helvetica fontsize=8 shape=plaintext];
edge[arrowhead=none];
{% block subgraphs %}
{% for sub in subs %}
subgraph cluster_{{ sub.bat_id }} {
fontsize=15;
label="Bâtiment {{ sub.bat_name }}";
{% if sub.bornes %}
{% block bornes %}
node [label=<
<TABLE BGCOLOR="{{ colors.back}}" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="{{ colors.head_bornes }}">
<FONT FACE="Helvetica Bold" COLOR="white">Borne</FONT></TD>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="{{ colors.head_bornes }}">
<FONT FACE="Helvetica Bold" COLOR="white">Switch</FONT></TD>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="{{ colors.head_bornes }}">
<FONT FACE="Helvetica Bold" COLOR="white">Port</FONT></TD>
</TR>
{% for borne in sub.bornes %}
<TR>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BORDER="0">
<FONT COLOR="{{ colors.texte }}" >{{ borne.name }}</FONT>
</TD>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BORDER="0">
<FONT COLOR="{{ colors.texte }}" >{{ borne.switch }}</FONT>
</TD>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BORDER="0">
<FONT COLOR="{{ colors.texte }}" >{{ borne.port }}</FONT>
</TD>
</TR>
{% endfor %}
</TABLE>
>] "{{sub.bat_name}}bornes";
{% endblock %}
{% endif %}
{% if sub.machines %}
{% block machines %}
node [label=<
<TABLE BGCOLOR="{{ colors.back}}" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="{{ colors.head_server }}">
<FONT FACE="Helvetica Bold" COLOR="white">Machine</FONT></TD>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="{{ colors.head_server }}">
<FONT FACE="Helvetica Bold" COLOR="white">Switch</FONT></TD>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="{{ colors.head_server }}">
<FONT FACE="Helvetica Bold" COLOR="white">Port</FONT></TD>
</TR>
{% for machine in sub.machines %}
<TR>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BORDER="0">
<FONT COLOR="{{ colors.texte }}" >{{ machine.name }}</FONT>
</TD>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BORDER="0">
<FONT COLOR="{{ colors.texte }}" >{{ machine.switch }}</FONT>
</TD>
<TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BORDER="0">
<FONT COLOR="{{ colors.texte }}" >{{ machine.port }}</FONT>
</TD>
</TR>
{% endfor %}
</TABLE>
>] "{{sub.bat_name}}machines";
{% endblock %}
{% endif %}
{% block switchs %}
{% for switch in sub.switchs %}
node [label=<
<TABLE BGCOLOR="{{ colors.back }}" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="{{ colors.head }}">
<FONT FACE="Helvetica Bold" COLOR="white">
{{ switch.name }}
</FONT></TD></TR>
<TR><TD ALIGN="LEFT" BORDER="0">
<FONT COLOR="{{ colors.texte }}" >Modèle</FONT>
</TD>
<TD ALIGN="LEFT">
<FONT COLOR="{{ colors.texte }}" >{{ switch.model }}</FONT>
</TD></TR>
<TR><TD ALIGN="LEFT" BORDER="0">
<FONT COLOR="{{ colors.texte }}" >Taille</FONT>
</TD>
<TD ALIGN="LEFT">
<FONT COLOR="{{ colors.texte }}" >{{ switch.nombre }}</FONT>
</TD></TR>
{% block liens %}
{% for port in switch.ports %}
<TR><TD ALIGN="LEFT" BORDER="0">
<FONT COLOR="{{ colors.texte }}" >{{ port.numero }}</FONT>
</TD>
<TD ALIGN="LEFT">
<FONT COLOR="{{ colors.texte }}" >{{ port.related }}</FONT>
</TD></TR>
{% endfor %}
{% endblock %}
</TABLE>
>] "{{ switch.id }}" ;
{% endfor %}
{% endblock %}
}
{% endfor %}
{% endblock %}
{% block isoles %}
{% for switchs in alone %}
"{{switchs.id}}" [label=<
<TABLE BGCOLOR="{{ colors.back }}" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="{{ colors.head }}">
<FONT FACE="Helvetica Bold" COLOR="white">
{{switchs.name}}
</FONT></TD></TR>
</TABLE>
>]
{% endfor %}
{% endblock %}
{% block links %}
{% for link in links %}
"{{ link.depart }}" -> "{{ link.arrive }}";
{% endfor %}
{% endblock %}
}
{% endblock %}

View file

@ -29,18 +29,39 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}Switchs{% endblock %} {% block title %}Switchs{% endblock %}
{% block content %} {% block content %}
<img id="zoom_01" src="/media/images/switchs.png" data-zoom-image="/media/images/switchs.png" width=100% />
<script type="text/javascript" src="/static/js/jquery.ez-plus.js"></script> <script type="text/javascript" src="/static/js/jquery.ez-plus.js"></script>
<script> <script>
$("#zoom_01").ezPlus({ function toggle_graph() {
scrollZoom: true, $("#collImg").collapse('toggle');
zoomType: 'inner',
cursor: 'crosshair' ezImg = $("#zoom_01").data("ezPlus");
}); if (ezImg) {
ezImg.destroy();
$("#zoom_01").removeData("ezPlus");
} else {
$("#zoom_01").ezPlus({
scrollZoom: true,
zoomType: 'inner',
cursor: 'crosshair'
});
}
}
</script> </script>
<button class="btn btn-primary" type="button" onclick="toggle_graph()">
Topologie des Switchs
</button>
<a target="_blank" href="/media/images/switchs.png" class="btn btn-primary">
<span class="fa fa-arrows-alt"></span>
</a>
<div id="collImg" class="collapse" aria-expanded="false">
<img id="zoom_01" src="/media/images/switchs.png" href="/media/images/switchs.png" target="_blank" data-zoom-image="/media/images/switchs.png" width=100% />
</div>
<h2>Switchs</h2> <h2>Switchs</h2>
{% can_create Switch %} {% can_create Switch %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'topologie:new-switch' %}"><i class="fa fa-plus"></i> Ajouter un switch</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'topologie:new-switch' %}"><i class="fa fa-plus"></i> Ajouter un switch</a>

View file

@ -43,6 +43,12 @@ from django.db import IntegrityError
from django.db.models import ProtectedError, Prefetch from django.db.models import ProtectedError, Prefetch
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django.template.loader import get_template
from django.template import Context, Template, loader
from django.db.models.signals import post_save
from django.dispatch import receiver
import tempfile
from users.views import form from users.views import form
from re2o.utils import re2o_paginator, SortTable from re2o.utils import re2o_paginator, SortTable
@ -53,13 +59,14 @@ from re2o.acl import (
can_view, can_view,
can_view_all, can_view_all,
) )
from re2o.settings import MEDIA_ROOT
from machines.forms import ( from machines.forms import (
DomainForm, DomainForm,
EditInterfaceForm, EditInterfaceForm,
AddInterfaceForm AddInterfaceForm
) )
from machines.views import generate_ipv4_mbf_param from machines.views import generate_ipv4_mbf_param
from machines.models import Interface from machines.models import Interface, Service_link
from preferences.models import AssoOption, GeneralOption from preferences.models import AssoOption, GeneralOption
from .models import ( from .models import (
@ -71,7 +78,8 @@ from .models import (
ConstructorSwitch, ConstructorSwitch,
AccessPoint, AccessPoint,
SwitchBay, SwitchBay,
Building Building,
Server,
) )
from .forms import ( from .forms import (
EditPortForm, EditPortForm,
@ -89,7 +97,13 @@ from .forms import (
EditBuildingForm EditBuildingForm
) )
from subprocess import Popen,PIPE from subprocess import (
Popen,
PIPE
)
from os.path import isfile
from os import remove
@login_required @login_required
@ -112,6 +126,14 @@ def index(request):
) )
pagination_number = GeneralOption.get_cached_value('pagination_number') pagination_number = GeneralOption.get_cached_value('pagination_number')
switch_list = re2o_paginator(request, switch_list, pagination_number) switch_list = re2o_paginator(request, switch_list, pagination_number)
if any(service_link.need_regen() for service_link in Service_link.objects.filter(service__service_type='graph_topo')):
make_machine_graph()
for service_link in Service_link.objects.filter(service__service_type='graph_topo'):
service_link.done_regen()
if not isfile("/var/www/re2o/media/images/switchs.png"):
make_machine_graph()
return render( return render(
request, request,
'topologie/index.html', 'topologie/index.html',
@ -935,95 +957,133 @@ def del_constructor_switch(request, constructor_switch, **_kwargs):
def make_machine_graph(): def make_machine_graph():
""" """
Crée le fichier dot et l'image du graph des Switchs Create the graph of switchs, machines and access points.
""" """
#Syntaxe DOT temporaire, A mettre dans un template: dico = {
lignes=['''digraph Switchs { 'subs': [],
node [ 'links' : [],
fontname=Helvetica 'alone': [],
fontsize=8 'colors': {
shape=plaintext] 'head': "#7f0505", # Color parameters for the graph
edge[arrowhead=odot,arrowtail=dot]'''] 'back': "#b5adad",
node_fixe='''node [label=< 'texte': "#563d01",
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0"> 'border_bornes': "#02078e",
<TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4"> 'head_bornes': "#25771c",
<FONT FACE="Helvetica Bold" COLOR="white"> 'head_server': "#1c3777"
{} }
</FONT></TD></TR> }
<TR><TD ALIGN="LEFT" BORDER="0"> missing = list(Switch.objects.all())
<FONT COLOR="#7B7B7B" >{}</FONT> detected = []
</TD> for building in Building.objects.all(): # Visit all buildings
<TD ALIGN="LEFT">
<FONT COLOR="#7B7B7B" >{}</FONT> dico['subs'].append(
</TD></TR> {
<TR><TD ALIGN="LEFT" BORDER="0"> 'bat_id': building.id,
<FONT COLOR="#7B7B7B" >{}</FONT> 'bat_name': building,
</TD> 'switchs': [],
<TD ALIGN="LEFT"> 'bornes': [],
<FONT>{}</FONT> 'machines': []
</TD></TR>''' }
node_ports='''<TR><TD ALIGN="LEFT" BORDER="0"> )
<FONT COLOR="#7B7B7B" >{}</FONT> # Visit all switchs in this building
</TD>
<TD ALIGN="LEFT">
<FONT>{}</FONT>
</TD></TR>'''
cluster='''subgraph cluster_{} {{
color=blue;
label="Batiment {}";'''
end_table='''</TABLE>
>] \"{}_{}\" ;'''
switch_alone='''{} [label=<
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4">
<FONT FACE="Helvetica Bold" COLOR="white">
{}
</FONT></TD></TR>
</TABLE>
>]'''
missing=[]
detected=[]
for sw in Switch.objects.all():
if(sw not in detected):
missing.append(sw)
for building in Building.objects.all():
lignes.append(cluster.format(len(lignes),building))
for switch in Switch.objects.filter(switchbay__building=building): for switch in Switch.objects.filter(switchbay__building=building):
lignes.append(node_fixe.format(switch.main_interface().domain.name,"Modèle",switch.model,"Nombre de ports",switch.number)) dico['subs'][-1]['switchs'].append({
for p in switch.ports.all().filter(related__isnull=False): 'name': switch.main_interface().domain.name,
lignes.append(node_ports.format(p.port,p.related.switch.main_interface().domain.name)) 'nombre': switch.number,
lignes.append(end_table.format(building.id,switch.id)) 'model': switch.model,
lignes.append("}") 'id': switch.id,
while(missing!=[]): 'batiment': building,
lignes,new_detected=recursive_switchs(missing[0].ports.all().filter(related=None).first(),None,lignes,[missing[0]]) 'ports': []
missing=[i for i in missing if i not in new_detected] })
detected+=new_detected # visit all ports of this switch and add the switchs linked to it
for switch in Switch.objects.all().filter(switchbay__isnull=True).exclude(ports__related__isnull=False): for port in switch.ports.filter(related__isnull=False):
lignes.append(switch_alone.format(switch.id,switch.main_interface().domain.name)) dico['subs'][-1]['switchs'][-1]['ports'].append({
lignes.append("}") 'numero': port.port,
fichier = open("media/images/switchs.dot","w") 'related': port.related.switch.main_interface().domain.name
for ligne in lignes: })
fichier.write(ligne+"\n")
fichier.close() for ap in AccessPoint.all_ap_in(building):
unflatten = Popen(["unflatten","-l", "3", "media/images/switchs.dot"], stdout=PIPE) dico['subs'][-1]['bornes'].append({
image = Popen(["dot", "-Tpng", "-o", "media/images/switchs.png"], stdin=unflatten.stdout, stdout=PIPE) 'name': ap.short_name,
'switch': ap.switch()[0].main_interface().domain.name,
'port': ap.switch()[0].ports.filter(
machine_interface__machine=ap
)[0].port
})
for server in Server.all_server_in(building):
dico['subs'][-1]['machines'].append({
'name': server.short_name,
'switch': server.switch()[0].main_interface().domain.name,
'port': Port.objects.filter(machine_interface__machine=server)[0].port
})
# While the list of forgotten ones is not empty
while missing:
if missing[0].ports.count(): # The switch is not empty
links, new_detected = recursive_switchs(missing[0], None, [missing[0]])
for link in links:
dico['links'].append(link)
# Update the lists of missings and already detected switchs
missing=[i for i in missing if i not in new_detected]
detected += new_detected
else: # If the switch have no ports, don't explore it and hop to the next one
del missing[0]
# Switchs that are not connected or not in a building
for switch in Switch.objects.filter(switchbay__isnull=True).exclude(ports__related__isnull=False):
dico['alone'].append({
'id': switch.id,
'name': switch.main_interface().domain.name
})
def recursive_switchs(port_start, switch_before, lignes,detected): dot_data=generate_dot(dico,'topologie/graph_switch.dot') # generate the dot file
"""
Parcour récursivement le switchs auquel appartient port_start pour trouver les ports suivants liés f = tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8', delete=False) # Create a temporary file to store the dot data
""" with f:
l_ports=port_start.switch.ports.filter(related__isnull=False) f.write(dot_data)
for port in l_ports: unflatten = Popen( # unflatten the graph to make it look better
if port.related.switch!=switch_before and port.related.switch!=port.switch: ["unflatten","-l", "3", f.name],
links=[] stdout=PIPE
for sw in [switch for switch in [port_start.switch,port.related.switch]]: )
if(sw not in detected): image = Popen( # pipe the result of the first command into the second
detected.append(sw) ["dot", "-Tpng", "-o", MEDIA_ROOT + "/images/switchs.png"],
if(sw.switchbay.building): stdin=unflatten.stdout,
links.append("\"{}_{}\"".format(sw.switchbay.building.id,sw.id)) stdout=PIPE
else: )
links.append("\"{}\"".format(sw.id))
lignes.append(links[0]+" -> "+links[1]) def generate_dot(data,template):
lignes, detected = recursive_switchs(port.related, port_start.switch, lignes, detected) """create the dot file
return (lignes, detected) :param data: dictionary passed to the template
:param template: path to the dot template
:return: all the lines of the dot file"""
t = loader.get_template(template)
if not isinstance(t, Template) and not (hasattr(t, 'template') and isinstance(t.template, Template)):
raise Exception("Le template par défaut de Django n'est pas utilisé."
"Cela peut mener à des erreurs de rendu."
"Vérifiez les paramètres")
c = Context(data).flatten()
dot = t.render(c)
return(dot)
def recursive_switchs(switch_start, switch_before, detected):
"""Visit the switch and travel to the switchs linked to it.
:param switch_start: the switch to begin the visit on
:param switch_before: the switch that you come from. None if switch_start is the first one
:param detected: list of all switchs already visited. None if switch_start is the first one
:return: A list of all the links found and a list of all the switchs visited"""
links_return=[] # list of dictionaries of the links to be detected
for port in switch_start.ports.filter(related__isnull=False): # Ports that are related to another switch
if port.related.switch != switch_before and port.related.switch != port.switch: # Not the switch that we come from, not the current switch
links = { # Dictionary of a link
'depart':switch_start.id,
'arrive':port.related.switch.id
}
if port.related.switch not in detected: # The switch at the end of this link has not been visited
links_down, detected = recursive_switchs(port.related.switch, switch_start, detected) # explore it and get the results
for link in links_down: # Add the non empty links to the current list
if link:
links_return.append(link)
links_return.append(links) # Add current and below levels links
detected.append(switch_start) # This switch is considered detected
return (links_return, detected)