915 lines
35 KiB
Python
915 lines
35 KiB
Python
import json
|
|
from functools import reduce
|
|
from typing import DefaultDict, Dict, Optional, Union
|
|
import time
|
|
from itertools import chain
|
|
|
|
from utilities.htmx import is_htmx
|
|
from circuits.models import Circuit, CircuitTermination
|
|
from dcim.models import (
|
|
Cable,
|
|
CableTermination,
|
|
Device,
|
|
device_components,
|
|
DeviceRole,
|
|
FrontPort,
|
|
Interface,
|
|
PowerFeed,
|
|
PowerPanel,
|
|
RearPort,
|
|
)
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db.models import Q, QuerySet
|
|
from django.db.models.functions import Lower
|
|
from django.http import HttpRequest, HttpResponseRedirect, QueryDict
|
|
from django.shortcuts import render, get_object_or_404
|
|
from django.views.generic import View
|
|
from extras.models import Tag
|
|
from wireless.models import WirelessLink
|
|
from netbox.views.generic import ObjectView, ObjectListView, ObjectEditView, ObjectDeleteView, ObjectChangeLogView
|
|
|
|
|
|
from netbox_topology_views.filters import DeviceFilterSet, CoordinatesFilterSet
|
|
from netbox_topology_views.forms import DeviceFilterForm, IndividualOptionsForm, CoordinateGroupsForm, CoordinatesForm, CoordinatesFilterForm
|
|
from netbox_topology_views.models import RoleImage, CoordinateGroup, Coordinate, IndividualOptions
|
|
from netbox_topology_views.tables import CoordinateGroupListTable, CoordinateListTable
|
|
from netbox_topology_views.utils import (
|
|
CONF_IMAGE_DIR,
|
|
find_image_url,
|
|
get_model_role,
|
|
get_model_slug,
|
|
image_static_url,
|
|
LinePattern,
|
|
get_query_settings
|
|
)
|
|
|
|
|
|
|
|
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
|
|
is_device = isinstance(entity, Device)
|
|
query = (
|
|
{"object_id": entity.device_role_id}
|
|
if is_device
|
|
else {"content_type_id": ContentType.objects.get_for_model(entity).pk}
|
|
)
|
|
|
|
try:
|
|
return RoleImage.objects.get(**query).get_image_url()
|
|
except RoleImage.DoesNotExist:
|
|
return find_image_url(
|
|
entity.device_role.slug if is_device else get_model_slug(entity.__class__)
|
|
)
|
|
|
|
|
|
def create_node(
|
|
device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool, group_id="default"
|
|
):
|
|
node = {}
|
|
node_content = ""
|
|
if isinstance(device, Circuit):
|
|
dev_name = device.cid
|
|
node["id"] = f"c{device.pk}"
|
|
|
|
if device.provider is not None:
|
|
node_content += (
|
|
f"<tr><th>Provider: </th><td>{device.provider.name}</td></tr>"
|
|
)
|
|
if device.type is not None:
|
|
node_content += f"<tr><th>Type: </th><td>{device.type.name}</td></tr>"
|
|
elif isinstance(device, PowerPanel):
|
|
dev_name = device.name
|
|
node["id"] = f"p{device.pk}"
|
|
|
|
if device.site is not None:
|
|
node_content += f"<tr><th>Site: </th><td>{device.site.name}</td></tr>"
|
|
if device.location is not None:
|
|
node_content += (
|
|
f"<tr><th>Location: </th><td>{device.location.name}</td></tr>"
|
|
)
|
|
elif isinstance(device, PowerFeed):
|
|
dev_name = device.name
|
|
node["id"] = f"f{device.pk}"
|
|
|
|
if device.power_panel is not None:
|
|
node_content += (
|
|
f"<tr><th>Power Panel: </th><td>{device.power_panel.name}</td></tr>"
|
|
)
|
|
if device.type is not None:
|
|
node_content += f"<tr><th>Type: </th><td>{device.type}</td></tr>"
|
|
if device.supply is not None:
|
|
node_content += f"<tr><th>Supply: </th><td>{device.supply}</td></tr>"
|
|
if device.phase is not None:
|
|
node_content += f"<tr><th>Phase: </th><td>{device.phase}</td></tr>"
|
|
if device.amperage is not None:
|
|
node_content += f"<tr><th>Amperage: </th><td>{device.amperage}</td></tr>"
|
|
if device.voltage is not None:
|
|
node_content += f"<tr><th>Voltage: </th><td>{device.voltage}</td></tr>"
|
|
else:
|
|
dev_name = device.name
|
|
if dev_name is None:
|
|
dev_name = device.device_type.get_full_name
|
|
|
|
if device.device_type is not None:
|
|
node_content += (
|
|
f"<tr><th>Type: </th><td>{device.device_type.model}</td></tr>"
|
|
)
|
|
if device.device_role.name is not None:
|
|
node_content += (
|
|
f"<tr><th>Role: </th><td>{device.device_role.name}</td></tr>"
|
|
)
|
|
if device.serial != "":
|
|
node_content += f"<tr><th>Serial: </th><td>{device.serial}</td></tr>"
|
|
if device.primary_ip is not None:
|
|
node_content += (
|
|
f"<tr><th>IP Address: </th><td>{device.primary_ip.address}</td></tr>"
|
|
)
|
|
if device.site is not None:
|
|
node_content += f"<tr><th>Site: </th><td>{device.site.name}</td></tr>"
|
|
if device.location is not None:
|
|
node_content += (
|
|
f"<tr><th>Location: </th><td>{device.location.name}</td></tr>"
|
|
)
|
|
if device.rack is not None:
|
|
node_content += f"<tr><th>Rack: </th><td>{device.rack.name}</td></tr>"
|
|
if device.position is not None:
|
|
if device.face is not None:
|
|
node_content += f"<tr><th>Position: </th><td>{device.position} ({device.face})</td></tr>"
|
|
else:
|
|
node_content += (
|
|
f"<tr><th>Position: </th><td>{device.position}</td></tr>"
|
|
)
|
|
|
|
node["id"] = device.pk
|
|
|
|
if device.device_role.color != "":
|
|
node["color.border"] = "#" + device.device_role.color
|
|
|
|
dev_title = "<table><tbody> %s</tbody></table>" % (node_content)
|
|
|
|
node["title"] = dev_title
|
|
node["name"] = dev_name
|
|
node["label"] = dev_name
|
|
node["shape"] = "image"
|
|
node["href"] = device.get_absolute_url()
|
|
node["image"] = get_image_for_entity(device)
|
|
|
|
if group_id is None or group_id == "default":
|
|
# Default group named "default" must always exist in order to make sure
|
|
# that coordinate values can be stored even if no coordinate group has been
|
|
# selected. The default group will be added automatically if it does not exist.
|
|
try:
|
|
if CoordinateGroup.objects.filter(name="default"):
|
|
group = CoordinateGroup.objects.get(name="default")
|
|
group_id = group.pk
|
|
else:
|
|
group = CoordinateGroup(
|
|
name="default",
|
|
description="Automatically generated default group. If you delete "
|
|
"this group, all default coordinates are gone for good but "
|
|
"the group itself will be re-created."
|
|
)
|
|
group.save()
|
|
group_id = group.pk
|
|
except:
|
|
pass
|
|
|
|
group = get_object_or_404(CoordinateGroup, pk=group_id)
|
|
|
|
node["physics"] = True
|
|
if Coordinate.objects.filter(group=group, device=device.pk).values('x') and Coordinate.objects.filter(group=group, device=device.pk).values('y'):
|
|
node["x"] = Coordinate.objects.get(group=group, device=device.pk).x
|
|
node["y"] = Coordinate.objects.get(group=group, device=device.pk).y
|
|
node["physics"] = False
|
|
elif "coordinates" in device.custom_field_data:
|
|
# We prefer the new Coordinate model but leave the deprecated method
|
|
# for now as fallback for compatibility reasons
|
|
if device.custom_field_data["coordinates"] is not None:
|
|
if ";" in device.custom_field_data["coordinates"]:
|
|
cords = device.custom_field_data["coordinates"].split(";")
|
|
node["x"] = int(cords[0])
|
|
node["y"] = int(cords[1])
|
|
node["physics"] = False
|
|
elif save_coords:
|
|
node["physics"] = False
|
|
elif save_coords:
|
|
node["physics"] = False
|
|
|
|
return node
|
|
|
|
|
|
def create_edge(
|
|
edge_id: int,
|
|
termination_a: Dict,
|
|
termination_b: Dict,
|
|
circuit: Optional[Dict] = None,
|
|
cable: Optional[Cable] = None,
|
|
wireless: Optional[Dict] = None,
|
|
power: Optional[bool] = None,
|
|
interface: Optional[Interface] = None,
|
|
):
|
|
cable_a_name = (
|
|
"device A name unknown"
|
|
if termination_a["termination_name"] is None
|
|
else termination_a["termination_name"]
|
|
)
|
|
cable_a_dev_name = (
|
|
"device A name unknown"
|
|
if termination_a["termination_device_name"] is None
|
|
else termination_a["termination_device_name"]
|
|
)
|
|
cable_b_name = (
|
|
"device A name unknown"
|
|
if termination_b["termination_name"] is None
|
|
else termination_b["termination_name"]
|
|
)
|
|
cable_b_dev_name = (
|
|
"cable B name unknown"
|
|
if termination_b["termination_device_name"] is None
|
|
else termination_b["termination_device_name"]
|
|
)
|
|
|
|
edge = {}
|
|
edge["id"] = edge_id
|
|
edge["from"] = termination_a["device_id"]
|
|
edge["to"] = termination_b["device_id"]
|
|
edge["color"] = '#2b7ce9'
|
|
title = "Cable"
|
|
|
|
if circuit is not None:
|
|
edge["dashes"] = True
|
|
title = f"Circuit provider: {circuit['provider_name']}<br>Termination"
|
|
|
|
elif wireless is not None:
|
|
edge["dashes"] = LinePattern().wireless
|
|
title = "Wireless Connection"
|
|
|
|
elif power is not None:
|
|
edge["dashes"] = LinePattern().power
|
|
title = "Power Connection"
|
|
|
|
elif interface is not None:
|
|
title = "Interface Connection"
|
|
edge["width"] = 3
|
|
edge["dashes"] = LinePattern().logical
|
|
edge["color"] = '#f1c232'
|
|
edge["href"] = interface.get_absolute_url() + "trace"
|
|
|
|
edge[
|
|
"title"
|
|
] = f"{title} between<br>{cable_a_dev_name} [{cable_a_name}]<br>{cable_b_dev_name} [{cable_b_name}]"
|
|
|
|
if cable is not None:
|
|
edge["href"] = cable.get_absolute_url()
|
|
if hasattr(cable, 'color') and cable.color != "":
|
|
edge["color"] = "#" + cable.color
|
|
|
|
return edge
|
|
|
|
|
|
def create_circuit_termination(termination):
|
|
if isinstance(termination, CircuitTermination):
|
|
return {
|
|
"termination_name": termination.circuit.provider.name,
|
|
"termination_device_name": termination.circuit.cid,
|
|
"device_id": "c{}".format(termination.circuit.pk),
|
|
}
|
|
if (
|
|
isinstance(termination, Interface)
|
|
or isinstance(termination, FrontPort)
|
|
or isinstance(termination, RearPort)
|
|
):
|
|
return {
|
|
"termination_name": termination.name,
|
|
"termination_device_name": termination.device.name,
|
|
"device_id": termination.device.pk,
|
|
}
|
|
return None
|
|
|
|
|
|
def get_topology_data(
|
|
queryset: QuerySet,
|
|
individualOptions: IndividualOptions,
|
|
show_unconnected: bool,
|
|
save_coords: bool,
|
|
show_cables: bool,
|
|
show_circuit: bool,
|
|
show_logical_connections: bool,
|
|
show_single_cable_logical_conns: bool,
|
|
show_neighbors: bool,
|
|
show_power: bool,
|
|
show_wireless: bool,
|
|
group_id,
|
|
):
|
|
|
|
supported_termination_types = []
|
|
for t in IndividualOptions.CHOICES:
|
|
supported_termination_types.append(t[1])
|
|
|
|
if not queryset:
|
|
return None
|
|
|
|
nodes_devices = {}
|
|
edges = []
|
|
nodes = []
|
|
edge_ids = 0
|
|
nodes_circuits: Dict[int, Circuit] = {}
|
|
nodes_powerpanel: Dict[int, PowerPanel] = {}
|
|
nodes_powerfeed: Dict[int, PowerFeed] = {}
|
|
nodes_provider_networks = {}
|
|
cable_ids = DefaultDict(dict)
|
|
interface_ids = DefaultDict(dict)
|
|
|
|
ignore_cable_type = individualOptions.ignore_cable_type
|
|
|
|
device_ids = [d.pk for d in queryset]
|
|
site_ids = [d.site_id for d in queryset]
|
|
|
|
if show_neighbors:
|
|
interfaces = Interface.objects.filter(
|
|
Q(device_id__in=device_ids)
|
|
)
|
|
frontports = FrontPort.objects.filter(
|
|
Q(device_id__in=device_ids)
|
|
)
|
|
rearports = RearPort.objects.filter(
|
|
Q(device_id__in=device_ids)
|
|
)
|
|
|
|
ports = chain(interfaces, frontports, rearports)
|
|
for port in ports:
|
|
for link_peer in port.link_peers:
|
|
if link_peer.device.id not in device_ids:
|
|
device_ids.append(link_peer.device.id)
|
|
|
|
if show_logical_connections:
|
|
path_complete_interfaces = Interface.objects.filter(
|
|
Q(_path__is_complete=True) & Q(device_id__in=device_ids)
|
|
)
|
|
for path_complete_interface in path_complete_interfaces:
|
|
for connected_endpoint in path_complete_interface.connected_endpoints:
|
|
device_ids.append(connected_endpoint.device.id)
|
|
|
|
if show_circuit:
|
|
circuit_terminations = CircuitTermination.objects.filter(
|
|
Q(site_id__in=site_ids) | Q(provider_network__isnull=False)
|
|
).prefetch_related("provider_network", "circuit")
|
|
for circuit_termination in circuit_terminations:
|
|
circuit_termination: CircuitTermination
|
|
if (
|
|
show_unconnected
|
|
and circuit_termination.circuit_id not in nodes_circuits
|
|
):
|
|
nodes_circuits[
|
|
circuit_termination.circuit.pk
|
|
] = circuit_termination.circuit
|
|
|
|
termination_a = {}
|
|
termination_b = {}
|
|
circuit_model = {}
|
|
if circuit_termination.cable is not None:
|
|
termination_a = create_circuit_termination(
|
|
circuit_termination.cable.a_terminations[0]
|
|
)
|
|
termination_b = create_circuit_termination(
|
|
circuit_termination.cable.b_terminations[0]
|
|
)
|
|
elif circuit_termination.provider_network is not None:
|
|
if (
|
|
circuit_termination.provider_network_id
|
|
not in nodes_provider_networks
|
|
):
|
|
nodes_provider_networks[
|
|
circuit_termination.provider_network.pk
|
|
] = circuit_termination.provider_network
|
|
|
|
if bool(termination_a) and bool(termination_b):
|
|
circuit_model = {
|
|
"provider_name": circuit_termination.circuit.provider.name
|
|
}
|
|
edge_ids += 1
|
|
edges.append(
|
|
create_edge(
|
|
edge_id=edge_ids,
|
|
cable=circuit_termination.cable,
|
|
circuit=circuit_model,
|
|
termination_a=termination_a,
|
|
termination_b=termination_b,
|
|
)
|
|
)
|
|
|
|
circuit_has_connections = False
|
|
for termination in [
|
|
circuit_termination.cable.a_terminations[0],
|
|
circuit_termination.cable.b_terminations[0],
|
|
]:
|
|
if not isinstance(termination, CircuitTermination):
|
|
if (
|
|
termination.device_id not in nodes_devices
|
|
and termination.device_id in device_ids
|
|
):
|
|
nodes_devices[termination.device_id] = termination.device
|
|
circuit_has_connections = True
|
|
else:
|
|
if termination.device_id in device_ids:
|
|
circuit_has_connections = True
|
|
|
|
if circuit_has_connections and not show_unconnected:
|
|
if circuit_termination.circuit_id not in nodes_circuits:
|
|
nodes_circuits[
|
|
circuit_termination.circuit.pk
|
|
] = circuit_termination.circuit
|
|
|
|
for d in nodes_circuits.values():
|
|
nodes.append(create_node(d, save_coords, group_id))
|
|
|
|
if show_power:
|
|
power_panels_ids = PowerPanel.objects.filter(
|
|
Q(site_id__in=site_ids)
|
|
).values_list("pk", flat=True)
|
|
power_feeds: QuerySet[PowerFeed] = PowerFeed.objects.filter(
|
|
Q(power_panel_id__in=power_panels_ids)
|
|
)
|
|
|
|
for power_feed in power_feeds:
|
|
if show_unconnected or (
|
|
not show_unconnected and power_feed.cable_id is not None
|
|
):
|
|
if power_feed.power_panel_id not in nodes_powerpanel:
|
|
nodes_powerpanel[power_feed.power_panel.pk] = power_feed.power_panel
|
|
|
|
power_link_name = ""
|
|
if power_feed.pk not in nodes_powerfeed:
|
|
if not show_unconnected:
|
|
if power_feed.link_peers[0].device_id in device_ids:
|
|
nodes_powerfeed[power_feed.pk] = power_feed
|
|
power_link_name = power_feed.link_peers[0].name
|
|
else:
|
|
nodes_powerfeed[power_feed.pk] = power_feed
|
|
|
|
edge_ids += 1
|
|
termination_a = {
|
|
"termination_name": power_feed.power_panel.name,
|
|
"termination_device_name": "",
|
|
"device_id": f"p{power_feed.power_panel_id}",
|
|
}
|
|
termination_b = {
|
|
"termination_name": power_feed.name,
|
|
"termination_device_name": power_link_name,
|
|
"device_id": f"f{power_feed.pk}",
|
|
}
|
|
edges.append(
|
|
create_edge(
|
|
edge_id=edge_ids,
|
|
termination_a=termination_a,
|
|
termination_b=termination_b,
|
|
power=True,
|
|
)
|
|
)
|
|
|
|
if power_feed.cable_id is not None:
|
|
cable_ids[power_feed.cable_id][power_feed.cable_end] = termination_b
|
|
|
|
for d in nodes_powerfeed.values():
|
|
nodes.append(create_node(d, save_coords, group_id))
|
|
|
|
for d in nodes_powerpanel.values():
|
|
nodes.append(create_node(d, save_coords, group_id))
|
|
|
|
if show_logical_connections:
|
|
interfaces = Interface.objects.filter(
|
|
Q(_path__is_complete=True) & Q(device_id__in=device_ids)
|
|
)
|
|
|
|
for interface in interfaces:
|
|
# print('{} {} {} {}'.format(interface.device.name, interface.name, interface._path.destinations[0].device.name, interface._path.destinations[0].name))
|
|
for destination in interface._path.destinations:
|
|
if isinstance(destination, device_components.Interface):
|
|
if destination.device.id not in device_ids:
|
|
# print('Destination interface not in device queryset, ignoring')
|
|
continue
|
|
|
|
if destination.id in interface_ids:
|
|
# we've already captured the destination interface, ignore this connection
|
|
# print('Destination interface already exists, ignoring')
|
|
continue
|
|
|
|
if not show_single_cable_logical_conns and interface.cable_id==destination.cable_id and show_cables:
|
|
# interface connection is the same as the cable connection, ignore this connection
|
|
continue
|
|
|
|
interface_ids[interface.id]=interface
|
|
edge_ids += 1
|
|
termination_a = { "termination_name": interface.name, "termination_device_name": interface.device.name, "device_id": interface.device.id }
|
|
termination_b = { "termination_name": destination.name, "termination_device_name": destination.device.name, "device_id": destination.device.id }
|
|
edges.append(create_edge(edge_id=edge_ids, termination_a=termination_a, termination_b=termination_b, interface=interface))
|
|
nodes_devices[interface.device.id] = interface.device
|
|
nodes_devices[destination.device.id] = destination.device
|
|
|
|
if show_cables:
|
|
links: QuerySet[CableTermination] = CableTermination.objects.filter(
|
|
Q(_device_id__in=device_ids)
|
|
).select_related("termination_type")
|
|
|
|
for link in links:
|
|
if link.termination_type.name in ignore_cable_type:
|
|
continue
|
|
|
|
# Normal device cables
|
|
if link.termination_type.name in supported_termination_types:
|
|
complete_link = False
|
|
if link.cable_end == "A":
|
|
if link.cable_id not in cable_ids:
|
|
cable_ids[link.cable_id] = {}
|
|
else:
|
|
if "B" in cable_ids[link.cable_id]:
|
|
if cable_ids[link.cable_id]["B"] is not None:
|
|
complete_link = True
|
|
elif link.cable_end == "B":
|
|
if link.cable_id not in cable_ids:
|
|
cable_ids[link.cable_id] = {}
|
|
else:
|
|
if "A" in cable_ids[link.cable_id]:
|
|
if cable_ids[link.cable_id]["A"] is not None:
|
|
complete_link = True
|
|
else:
|
|
print("Unkown cable end")
|
|
cable_ids[link.cable_id][link.cable_end] = link
|
|
|
|
if complete_link:
|
|
edge_ids += 1
|
|
if isinstance(cable_ids[link.cable_id]["B"], CableTermination):
|
|
if cable_ids[link.cable_id]["B"]._device_id not in nodes_devices:
|
|
nodes_devices[
|
|
cable_ids[link.cable_id]["B"]._device_id
|
|
] = cable_ids[link.cable_id]["B"].termination.device
|
|
termination_b = {
|
|
"termination_name": cable_ids[link.cable_id][
|
|
"B"
|
|
].termination.name,
|
|
"termination_device_name": cable_ids[link.cable_id][
|
|
"B"
|
|
].termination.device.name,
|
|
"device_id": cable_ids[link.cable_id][
|
|
"B"
|
|
].termination.device_id,
|
|
}
|
|
else:
|
|
termination_b = cable_ids[link.cable_id]["B"]
|
|
|
|
if isinstance(cable_ids[link.cable_id]["A"], CableTermination):
|
|
if cable_ids[link.cable_id]["A"]._device_id not in nodes_devices:
|
|
nodes_devices[
|
|
cable_ids[link.cable_id]["A"]._device_id
|
|
] = cable_ids[link.cable_id]["A"].termination.device
|
|
termination_a = {
|
|
"termination_name": cable_ids[link.cable_id][
|
|
"A"
|
|
].termination.name,
|
|
"termination_device_name": cable_ids[link.cable_id][
|
|
"A"
|
|
].termination.device.name,
|
|
"device_id": cable_ids[link.cable_id][
|
|
"A"
|
|
].termination.device_id,
|
|
}
|
|
else:
|
|
termination_a = cable_ids[link.cable_id]["A"]
|
|
|
|
edges.append(
|
|
create_edge(
|
|
edge_id=edge_ids,
|
|
cable=link.cable,
|
|
termination_a=termination_a,
|
|
termination_b=termination_b,
|
|
)
|
|
)
|
|
|
|
if show_wireless:
|
|
wlan_links: QuerySet[WirelessLink] = WirelessLink.objects.filter(
|
|
Q(_interface_a_device_id__in=device_ids)
|
|
& Q(_interface_b_device_id__in=device_ids)
|
|
)
|
|
|
|
for wlan_link in wlan_links:
|
|
if wlan_link.interface_a.device_id not in nodes_devices:
|
|
nodes_devices[
|
|
wlan_link.interface_a.device.pk
|
|
] = wlan_link.interface_a.device
|
|
if wlan_link.interface_b.device_id not in nodes_devices:
|
|
nodes_devices[
|
|
wlan_link.interface_b.device.pk
|
|
] = wlan_link.interface_b.device
|
|
|
|
termination_a = {
|
|
"termination_name": wlan_link.interface_a.name,
|
|
"termination_device_name": wlan_link.interface_a.device.name,
|
|
"device_id": wlan_link.interface_a.device_id,
|
|
}
|
|
termination_b = {
|
|
"termination_name": wlan_link.interface_b.name,
|
|
"termination_device_name": wlan_link.interface_b.device.name,
|
|
"device_id": wlan_link.interface_b.device_id,
|
|
}
|
|
wireless = {"ssid": wlan_link.ssid}
|
|
|
|
edge_ids += 1
|
|
edges.append(
|
|
create_edge(
|
|
edge_id=edge_ids,
|
|
cable=wlan_link,
|
|
termination_a=termination_a,
|
|
termination_b=termination_b,
|
|
wireless=wireless,
|
|
)
|
|
)
|
|
|
|
for qs_device in queryset:
|
|
if qs_device.pk not in nodes_devices and show_unconnected:
|
|
nodes_devices[qs_device.pk] = qs_device
|
|
|
|
results = {}
|
|
|
|
for d in nodes_devices.values():
|
|
nodes.append(create_node(d, save_coords, group_id))
|
|
|
|
results["nodes"] = nodes
|
|
results["edges"] = edges
|
|
results["group"] = group_id
|
|
return results
|
|
|
|
|
|
class TopologyHomeView(PermissionRequiredMixin, View):
|
|
permission_required = ("dcim.view_site", "dcim.view_device")
|
|
|
|
"""
|
|
Show the home page
|
|
"""
|
|
|
|
def get(self, request):
|
|
self.filterset = DeviceFilterSet
|
|
self.queryset = Device.objects.all().select_related(
|
|
"device_type", "device_role"
|
|
)
|
|
self.queryset = self.filterset(request.GET, self.queryset).qs
|
|
self.model = self.queryset.model
|
|
topo_data = None
|
|
|
|
individualOptions, created = IndividualOptions.objects.get_or_create(
|
|
user_id=request.user.id,
|
|
)
|
|
|
|
if request.GET:
|
|
|
|
save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = get_query_settings(request)
|
|
|
|
if "group" not in request.GET:
|
|
group_id = "default"
|
|
else:
|
|
group_id = request.GET["group"]
|
|
|
|
if not "draw_init" in request.GET or "draw_init" in request.GET and request.GET["draw_init"].lower() == "true":
|
|
topo_data = get_topology_data(
|
|
queryset=self.queryset,
|
|
individualOptions=individualOptions,
|
|
save_coords=save_coords,
|
|
show_unconnected=show_unconnected,
|
|
show_cables=show_cables,
|
|
show_logical_connections=show_logical_connections,
|
|
show_single_cable_logical_conns=show_single_cable_logical_conns,
|
|
show_neighbors=show_neighbors,
|
|
show_circuit=show_circuit,
|
|
show_power=show_power,
|
|
show_wireless=show_wireless,
|
|
group_id=group_id,
|
|
)
|
|
|
|
else:
|
|
# No GET-Request in URL. We most likely came here from the navigation menu.
|
|
preselected_device_roles = IndividualOptions.objects.get(id=individualOptions.id).preselected_device_roles.all().values_list('id', flat=True)
|
|
preselected_tags = IndividualOptions.objects.get(id=individualOptions.id).preselected_tags.all().values_list(Lower('name'), flat=True)
|
|
|
|
q = QueryDict(mutable=True)
|
|
q.setlist("device_role_id", list(preselected_device_roles))
|
|
q.setlist("tag", list(preselected_tags))
|
|
|
|
if individualOptions.show_unconnected: q['show_unconnected'] = "on"
|
|
if individualOptions.show_cables: q['show_cables'] = "on"
|
|
if individualOptions.show_logical_connections: q['show_logical_connections'] = "on"
|
|
if individualOptions.show_single_cable_logical_conns: q['show_single_cable_logical_conns'] = "on"
|
|
if individualOptions.show_neighbors: q['show_neighbors'] = "on"
|
|
if individualOptions.show_circuit: q['show_circuit'] = "on"
|
|
if individualOptions.show_power: q['show_power'] = "on"
|
|
if individualOptions.show_wireless: q['show_wireless'] = "on"
|
|
if individualOptions.draw_default_layout:
|
|
q['draw_init'] = "true"
|
|
else:
|
|
q['draw_init'] = "false"
|
|
|
|
query_string = q.urlencode()
|
|
return HttpResponseRedirect(f"{request.path}?{query_string}")
|
|
|
|
|
|
if is_htmx(request):
|
|
return render(
|
|
request,
|
|
"netbox_topology_views/htmx_topology.html",
|
|
{
|
|
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
|
|
"topology_data": json.dumps(topo_data),
|
|
"broken_image": find_image_url("role-unknown"),
|
|
"epoch": int(time.time())
|
|
},
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
"netbox_topology_views/index.html",
|
|
{
|
|
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
|
|
"topology_data": json.dumps(topo_data),
|
|
"broken_image": find_image_url("role-unknown"),
|
|
"model": self.model
|
|
},
|
|
)
|
|
|
|
|
|
CONFIG = settings.PLUGINS_CONFIG["netbox_topology_views"]
|
|
ADDITIONAL_ROLES = (PowerPanel, PowerFeed, Circuit)
|
|
|
|
|
|
class TopologyImagesView(PermissionRequiredMixin, View):
|
|
permission_required = (
|
|
"dcim.view_site",
|
|
"dcim.view_device_role",
|
|
"dcim.add_device_role",
|
|
"dcim.change_device_role",
|
|
)
|
|
|
|
def get(self, request: HttpRequest):
|
|
images = [
|
|
{"url": image_static_url(image), "title": image.stem}
|
|
for image in CONF_IMAGE_DIR.iterdir()
|
|
]
|
|
|
|
roles = reduce(
|
|
lambda acc, cur: {
|
|
**acc,
|
|
cur.name: {
|
|
"id": cur.pk,
|
|
"name": cur.name,
|
|
"slug": cur.slug,
|
|
"image": find_image_url(cur.slug),
|
|
},
|
|
},
|
|
DeviceRole.objects.all(),
|
|
dict(),
|
|
)
|
|
|
|
for additional_role in ADDITIONAL_ROLES:
|
|
cur = get_model_role(additional_role)
|
|
ct = ContentType.objects.get_for_model(additional_role).pk
|
|
|
|
roles[cur.name] = {
|
|
"id": f"ct{ct}",
|
|
"name": cur.name,
|
|
"slug": cur.slug,
|
|
"image": find_image_url(cur.slug),
|
|
}
|
|
|
|
role_images = RoleImage.objects.all()
|
|
|
|
for role_image in role_images:
|
|
roles[role_image.role.name]["image"] = role_image.get_image_url()
|
|
|
|
return render(
|
|
request,
|
|
"netbox_topology_views/images.html",
|
|
{
|
|
"roles": sorted(list(roles.values()), key=lambda r: r["name"]),
|
|
"images": images,
|
|
},
|
|
)
|
|
|
|
class CoordinateView(PermissionRequiredMixin, ObjectView):
|
|
permission_required = 'netbox_topology_views.view_coordinate'
|
|
|
|
queryset = Coordinate.objects.all()
|
|
|
|
class CoordinateAddView(PermissionRequiredMixin, ObjectEditView):
|
|
permission_required = 'netbox_topology_views.add_coordinate'
|
|
|
|
queryset = Coordinate.objects.all()
|
|
form = CoordinatesForm
|
|
template_name = 'netbox_topology_views/coordinate_add.html'
|
|
|
|
class CoordinateListView(PermissionRequiredMixin, ObjectListView):
|
|
permission_required = 'netbox_topology_views.view_coordinate'
|
|
|
|
queryset = Coordinate.objects.all()
|
|
table = CoordinateListTable
|
|
template_name = 'netbox_topology_views/coordinate_list.html'
|
|
filterset = CoordinatesFilterSet
|
|
filterset_form = CoordinatesFilterForm
|
|
|
|
class CoordinateEditView(PermissionRequiredMixin, ObjectEditView):
|
|
permission_required = 'netbox_topology_views.change_coordinate'
|
|
|
|
queryset = Coordinate.objects.all()
|
|
form = CoordinatesForm
|
|
template_name = 'netbox_topology_views/coordinate_edit.html'
|
|
|
|
class CoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|
permission_required = 'netbox_topology_views.delete_coordinate'
|
|
|
|
queryset = Coordinate.objects.all()
|
|
|
|
class CoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
|
|
permission_required = 'netbox_topology_views.view_coordinate'
|
|
|
|
queryset = Coordinate.objects.all()
|
|
|
|
class CoordinateGroupView(PermissionRequiredMixin, ObjectView):
|
|
permission_required = 'netbox_topology_views.view_coordinategroup'
|
|
|
|
queryset = CoordinateGroup.objects.all()
|
|
|
|
class CoordinateGroupAddView(PermissionRequiredMixin, ObjectEditView):
|
|
permission_required = 'netbox_topology_views.add_coordinategroup'
|
|
|
|
queryset = CoordinateGroup.objects.all()
|
|
form = CoordinateGroupsForm
|
|
template_name = 'netbox_topology_views/coordinategroup_add.html'
|
|
|
|
class CoordinateGroupListView(PermissionRequiredMixin, ObjectListView):
|
|
permission_required = 'netbox_topology_views.view_coordinategroup'
|
|
|
|
queryset = CoordinateGroup.objects.all()
|
|
table = CoordinateGroupListTable
|
|
template_name = 'netbox_topology_views/coordinategroup_list.html'
|
|
|
|
class CoordinateGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
|
permission_required = 'netbox_topology_views.change_coordinategroup'
|
|
|
|
queryset = CoordinateGroup.objects.all()
|
|
form = CoordinateGroupsForm
|
|
template_name = 'netbox_topology_views/coordinategroup_edit.html'
|
|
|
|
class CoordinateGroupDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|
permission_required = 'netbox_topology_views.delete_coordinategroup'
|
|
|
|
queryset = CoordinateGroup.objects.all()
|
|
|
|
class CoordinateGroupChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
|
|
permission_required = 'netbox_topology_views.view_coordinategroup'
|
|
|
|
queryset = CoordinateGroup.objects.all()
|
|
|
|
class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
|
|
permission_required = 'netbox_topology_views.change_individualoption'
|
|
|
|
def post(self, request):
|
|
instance = IndividualOptions.objects.get(user_id=request.user.id)
|
|
form = IndividualOptionsForm(request.POST, instance=instance)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, "Options have been sucessfully saved")
|
|
else:
|
|
messages.error(request, form.errors)
|
|
|
|
return HttpResponseRedirect("./")
|
|
|
|
def get(self, request):
|
|
queryset, created = IndividualOptions.objects.get_or_create(
|
|
user_id=request.user.id,
|
|
)
|
|
|
|
form = IndividualOptionsForm(
|
|
initial={
|
|
'user_id': request.user.id,
|
|
'ignore_cable_type': tuple(queryset.ignore_cable_type.translate({ord(i): None for i in '[]\''}).split(', ')),
|
|
'preselected_device_roles': IndividualOptions.objects.get(id=queryset.id).preselected_device_roles.all(),
|
|
'preselected_tags': IndividualOptions.objects.get(id=queryset.id).preselected_tags.all(),
|
|
'show_unconnected': queryset.show_unconnected,
|
|
'show_cables': queryset.show_cables,
|
|
'show_logical_connections': queryset.show_logical_connections,
|
|
'show_single_cable_logical_conns': queryset.show_single_cable_logical_conns,
|
|
'show_neighbors': queryset.show_neighbors,
|
|
'show_circuit': queryset.show_circuit,
|
|
'show_power': queryset.show_power,
|
|
'show_wireless': queryset.show_wireless,
|
|
'draw_default_layout': queryset.draw_default_layout,
|
|
},
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
"netbox_topology_views/individual_options.html",
|
|
{
|
|
"form": form,
|
|
"object": queryset,
|
|
},
|
|
)
|