Merge branch 'mattieserver:master' into update-git-ignore

This commit is contained in:
Mario
2022-12-28 11:40:45 +01:00
committed by GitHub
15 changed files with 283 additions and 224 deletions
+21
View File
@@ -0,0 +1,21 @@
<!--
IMPORTANT:
==========
Thank you for contributing to NetBox Topology Views! Please
note that our contribution guidelines require a feature
request or bug report to be labeled "status: accepted". This
helps avoid waste time and effort on a proposed change that
we might not be able to accept.
Please prefix the title of your pull request according to
it's type. You should use "Closes #FEATURE_ID: ..." for feature
requests and "Fixes #ISSUE_ID: ..." for issues. This helps
us closing issues automatically after merging.
To sum it up:
1. Create Bug Report or Feature Request
2. Wait until the issue has been labeled "status: accepted"
3. Start working on the issue in a seperate branch
3. Create a pull request and name the title with a prefix
according to the issue ID.
-->
-93
View File
@@ -1,93 +0,0 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
schedule:
- cron: '35 10 * * *'
push:
branches: [ "master" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "master" ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@7e0881f8fe90b25e305bbf0309761e9314607e25
with:
cosign-release: 'v1.9.0'
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
COSIGN_EXPERIMENTAL: "true"
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: cosign sign ${{ steps.meta.outputs.tags }}@${{ steps.build-and-push.outputs.digest }}
-4
View File
@@ -1,4 +0,0 @@
FROM netboxcommunity/netbox:latest
COPY ./docker/plugin_requirements.txt /
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
+31
View File
@@ -12,6 +12,8 @@ Support to filter on name, site, tag and device role.
## Install
**_NOTE:_** For docker please see: [Docker install](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins)
The plugin is available as a Python package and can be installed with pip.
Run `pip install netbox-topology-views` in your virtual env.
@@ -44,6 +46,7 @@ python3 manage.py collectstatic --no-input
| netbox version | netbox-topology-views version |
| -------------- | ----------------------------- |
| >= 3.4.0 | >= v3.X.X |
| >= 3.3.0 | >= v3.0.0 |
| >= 3.2.0 | >= v1.1.0 |
| >= 3.1.8 | >= v1.0.0 |
@@ -85,6 +88,9 @@ PLUGINS_CONFIG = {
| ignore_cable_type | [] | The cable types that you want to ignore in the views |
| preselected_tags | [] | The name of tags you want to preload |
| draw_default_layout | False | (bool) Set to True if you want to load draw the topology on the initial load (when you go to the topology plugin page) |
| hide_single_cable_logical_conns | False | (bool) Set to True if you want to hide duplicate cables & logical connections |
### Custom Images
@@ -94,6 +100,31 @@ To change image with associated device use the `Images` page - it allows to map
Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plugins/netbox_topology_views/` to view your topologies
Select your options for the topology view:
![preview image](doc/img/selection_options.png?raw=true "preview")
<dl>
<dt>Hide Unconnected</dt>
<dd>Hide devices which have no connections.</dd>
<dt>Save Coordinates</dt>
<dd>Save the coordinates of devices in the topology view.</dd>
<dd>Please read the "Configure" chapter to set the allow_coordinates_saving option to True.</dd>
<dt>Show Cables</dt>
<dd>Show cable connections between devices, including cables connected to interfaces, front / rear ports, etc., in the topology view</dd>
<dt>Show Logical Connections</dt>
<dd>Show logical connections between interfaces (referred to as Interface Connections in NetBox) in the topology view. Where the path between
interfaces includes multiple cables (e.g., via patch panels), only the end interface connections are shown, not the
intermediate front / rear port connections, etc. This is similar to what was referred to as 'end-to-end' connections in previous versions. </dd>
<dd><i>Selecting both 'Show Cables' and 'Show Logical Interfaces' will sometimes result in a topology view with 2 cables per connection as these devices are diretly connected. Select only one of the options or set `hide_single_cable_logical_conns` to True.</i></dd>
<dt>Show Circuit Terminations</dt>
<dd>Show connections which end at a circuit termination in the topology view.</dd>
<dt>Show Wireless Links</dt>
<dd>Show wireless connections in the topology view.</dd>
<dt>Show Power Feeds</dt>
<dd>Show power connections from power feeds in the topology view.</dd>
</dl>
### Update
Run `pip install netbox-topology-views --upgrade` in your venv.
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

-1
View File
@@ -1 +0,0 @@
netbox-topology-views
+1
View File
@@ -29,6 +29,7 @@ class TopologyViewsConfig(PluginConfig):
"always_save_coordinates": False,
"preselected_tags": [],
"draw_default_layout": False,
"hide_single_cable_logical_conns": False,
}
+13
View File
@@ -31,10 +31,14 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
None,
(
"q",
"filter_id",
"hide_unconnected",
"save_coords",
"show_cables",
"show_circuit",
"show_logical_connections",
"show_power",
"show_wireless",
),
),
(
@@ -103,6 +107,15 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
hide_unconnected = forms.BooleanField(
label=_("Hide Unconnected"), required=False, initial=False
)
show_logical_connections = forms.BooleanField(
label =_("Show Logical Connections"), required=False, initial=False
)
show_cables = forms.BooleanField(
label =_("Show Cables"), required=False, initial=False
)
show_wireless = forms.BooleanField(
label =_("Show Wireless Links"), required=False, initial=False
)
show_circuit = forms.BooleanField(
label=_("Show Circuit Terminations"), required=False, initial=False
)
+6 -5
View File
@@ -1,8 +1,9 @@
from extras.plugins import PluginMenuItem
from extras.plugins import PluginMenu, PluginMenuItem
menu_items = (
PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),
PluginMenuItem(
link="plugins:netbox_topology_views:images", link_text="Topology Images"
menu = PluginMenu(
label='Topology Views',
groups=(
('Topology', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),),
('CUSTOMIZATION', (PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Topology Images"),),),
),
)
@@ -1 +1 @@
#visgraph{height:70vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
#visgraph{height:64vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
#visgraph {
height: 70vh;
height: 64vh;
}
html[data-netbox-color-mode=dark] #visgraph {
+10 -3
View File
@@ -100,9 +100,16 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
})
graph.on('doubleClick', (params) => {
params.nodes.forEach((node) => {
window.open(nodes.get(node).href, '_blank')
})
if (params.nodes.length > 0) {
params.nodes.forEach((node) => {
window.open(nodes.get(node).href, '_blank')
})
}
else {
params.edges.forEach((edge) => {
window.open(edges.get(edge).href, '_blank')
})
}
})
})()
@@ -48,6 +48,12 @@
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
<div class="tab-content">
{# Applied filters #}
{% if filter_form %}
{% applied_filters model filter_form request.GET %}
<hr style="margin-top: 0px;">
{% endif %}
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
<div class="panel-body">
<div id="visgraph"></div>
+192 -115
View File
@@ -9,6 +9,7 @@ from dcim.models import (
Cable,
CableTermination,
Device,
device_components,
DeviceRole,
FrontPort,
Interface,
@@ -177,6 +178,7 @@ def create_edge(
cable: Optional[Cable] = None,
wireless: Optional[Dict] = None,
power: Optional[bool] = None,
interface: Optional[bool] = None,
):
cable_a_name = (
"device A name unknown"
@@ -217,12 +219,21 @@ def create_edge(
edge["dashes"] = [5, 5, 3, 3]
title = "Power Connection"
elif interface is not None:
title = "Interface Connection"
edge["width"] = 3
edge["dashes"] = [1, 10, 1, 10]
edge["arrows"] = {"to": {"enabled": True, "scaleFactor": 0.5}, "from": {"enabled": True, "scaleFactor": 0.5}}
edge["color"] = '#f1c232'
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 and cable.color != "":
edge["color"] = "#" + cable.color
if cable is not None:
edge["href"] = cable.get_absolute_url()
if hasattr(cable, 'color') and cable.color != "":
edge["color"] = "#" + cable.color
return edge
@@ -251,8 +262,11 @@ def get_topology_data(
queryset: QuerySet,
hide_unconnected: bool,
save_coords: bool,
show_cables: bool,
show_circuit: bool,
show_logical_connections: bool,
show_power: bool,
show_wireless: bool,
):
if not queryset:
return None
@@ -266,9 +280,15 @@ def get_topology_data(
nodes_powerfeed: Dict[int, PowerFeed] = {}
nodes_provider_networks = {}
cable_ids = DefaultDict(dict)
interface_ids = DefaultDict(dict)
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"][
"ignore_cable_type"
]
hide_single_cable_logical_conns = bool(
settings.PLUGINS_CONFIG["netbox_topology_views"][
"hide_single_cable_logical_conns"
]
)
device_ids = [d.pk for d in queryset]
site_ids = [d.site_id for d in queryset]
@@ -314,6 +334,7 @@ def get_topology_data(
edges.append(
create_edge(
edge_id=edge_ids,
cable=circuit_termination.cable,
circuit=circuit_model,
termination_a=termination_a,
termination_b=termination_b,
@@ -345,15 +366,6 @@ def get_topology_data(
for d in nodes_circuits.values():
nodes.append(create_node(d, save_coords))
links: QuerySet[CableTermination] = CableTermination.objects.filter(
Q(_device_id__in=device_ids)
).select_related("termination_type")
wlan_links: QuerySet[WirelessLink] = WirelessLink.objects.filter(
Q(_interface_a_device_id__in=device_ids)
& Q(_interface_b_device_id__in=device_ids)
)
if show_power:
power_panels_ids = PowerPanel.objects.filter(
Q(site_id__in=site_ids)
@@ -407,112 +419,154 @@ def get_topology_data(
for d in nodes_powerpanel.values():
nodes.append(create_node(d, save_coords))
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,
)
)
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,
termination_a=termination_a,
termination_b=termination_b,
wireless=wireless,
)
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 hide_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=True))
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 not hide_unconnected:
nodes_devices[qs_device.pk] = qs_device
@@ -540,6 +594,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
"device_type", "device_role"
)
self.queryset = self.filterset(request.GET, self.queryset).qs
self.model = self.queryset.model
topo_data = None
if request.GET:
@@ -563,22 +618,43 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if request.GET["show_circuit"] == "on":
show_circuit = True
show_logical_connections = False
if "show_logical_connections" in request.GET:
if request.GET["show_logical_connections"] == "on" :
show_logical_connections = True
show_cables = False
if "show_cables" in request.GET:
if request.GET["show_cables"] == "on" :
show_cables = True
show_wireless = False
if "show_wireless" in request.GET:
if request.GET["show_wireless"] == "on" :
show_wireless = True
if "draw_init" in request.GET:
if request.GET["draw_init"].lower() == "true":
topo_data = get_topology_data(
self.queryset,
hide_unconnected,
save_coords,
show_cables,
show_circuit,
show_logical_connections,
show_power,
show_wireless,
)
else:
topo_data = get_topology_data(
self.queryset,
hide_unconnected,
save_coords,
show_cables,
show_circuit,
show_logical_connections,
show_power,
show_wireless,
)
else:
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"][
@@ -630,6 +706,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
"topology_data": json.dumps(topo_data),
"broken_image": find_image_url("role-unknown"),
"model": self.model,
},
)