Skip to content

Commit

Permalink
Implement visibility control based on device tags (#19)
Browse files Browse the repository at this point in the history
* Implement visibility control based on device tags

Inifial visibility behavior for specific device tags is controlled by 'undisplayed_device_tags' plugin parameter. Devices with tags matching listed tag resular expressions are hidden on initial view load, you may then hide/display any layer with a control button on the topology view page.
By default, the plugin lists all discovered device tags in Select Layers menu. You can use 'select_layers_list_include_device_tags' and 'select_layers_list_exclude_device_tags' plugin parameters to filter the included tags.
All three tag visibility control parameters are optional lists of regular expressions. Tags matching 'undisplayed_device_tags' are always listed in Select Layers menu. Empty or unset 'select_layers_list_include_device_tags' allows all discovered tags to be listed in Select layers menu. If set, 'select_layers_list_include_device_tags' works as an allow list for matched tags. 'select_layers_list_exclude_device_tags' filters out matched tags from the list, expept for ones matching 'undisplayed_device_tags'.

* Update README.md

* v0.6.0

Co-authored-by: Debug All <iDebugAll@gmail.com>
  • Loading branch information
iDebugAll and iDebugAll authored Jun 22, 2020
1 parent caa39c7 commit 626bdc5
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 8 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ Optionally, update a PLUGINS_CONFIG parameter in **configuration.py** to rewrite
# Listed device role slugs are hidden on initial view load,
# you may then hide/display any layer with a control button.
# ),
# 'undisplayed_device_tags': (
# ADD YOUR SETTINGS HERE
# undisplayed_device_tags value is a list or a tuple of regex strings.
# Devices with tags matching any of listed regular expressions are hidden
# on initial view load, you may then hide/display any layer with a control button.
# ),
# 'select_layers_list_include_device_tags': (
# ADD YOUR SETTINGS HERE
# select_layers_list_include_device_tags value is a list or a tuple of regex strings.
# Use this parameter to control tags listed in Select Layers menu.
# If specified, it works as allow list.
# ),
# 'select_layers_list_exclude_device_tags': (
# ADD YOUR SETTINGS HERE
# select_layers_list_exclude_device_tags value is a list or a tuple of regex strings.
# Use this parameter to control tags listed in Select Layers menu.
# If specified, it filters out matched tags from the list, except ones mathcing 'undisplayed_device_tags'.
# ),
# 'DISPLAY_PASSIVE_DEVICES': True|False,
# 'DISPLAY_LOGICAL_MULTICABLE_LINKS': True|False,
# 'DISPLAY_UNCONNECTED': True|False,
Expand Down Expand Up @@ -166,10 +184,14 @@ Default mapping already contains some general categories:
<br/><br/>

The Plugin can control the visibility of the layers and/or specific nodes on the topology view.<br/>
The visibility control is currently implemented for specific device roles, unconnected devices, and passive devices:<br/>
The visibility control is currently implemented for specific device roles, device tags, unconnected devices, and passive devices:<br/>

- Inifial visibility behavior for specific device roles is controlled by 'undisplayed_device_role_slugs' plugin parameter. Listed device role slugs are hidden on initial view load, you may then hide/display any layer with a control button on the topology view page.<br/>

- Inifial visibility behavior for specific device tags is controlled by 'undisplayed_device_tags' plugin parameter. Devices with tags matching listed tag resular expressions are hidden on initial view load, you may then hide/display any layer with a control button on the topology view page.<br/>
By default, the plugin lists all discovered device tags in Select Layers menu. You can use 'select_layers_list_include_device_tags' and 'select_layers_list_exclude_device_tags' plugin parameters to filter the included tags.<br/>
All three tag visibility control parameters are optional lists of regular expressions. Tags matching 'undisplayed_device_tags' are always listed in Select Layers menu. Empty or unset 'select_layers_list_include_device_tags' allows all discovered tags to be listed in Select layers menu. If set, 'select_layers_list_include_device_tags' works as an allow list for matched tags. 'select_layers_list_exclude_device_tags' filters out matched tags from the list, expept for ones matching 'undisplayed_device_tags'.

- Initial visibility behavior for unconnected nodes is controlled by DISPLAY_UNCONNECTED boolean plugin parameter.<br/>
By default unconnected nodes are being displayed. Set DISPLAY_UNCONNECTED to False to hide them on initial topology view load.<br/>
A separate 'Hide/Display Unconnected' button may then be used to hide or display those nodes.
Expand Down
2 changes: 1 addition & 1 deletion nextbox_ui_plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class NextBoxUIConfig(PluginConfig):
name = 'nextbox_ui_plugin'
verbose_name = 'NextBox UI'
description = 'Test'
version = '0.5.0'
version = '0.6.0'
author = 'Igor Korotchenkov'
author_email = 'iDebugAll@gmail.com'
base_url = 'nextbox-ui'
Expand Down
4 changes: 4 additions & 0 deletions nextbox_ui_plugin/static/nextbox_ui_plugin/button_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ function layerSelectorOnChange(checkbox){
showHideDeviceRoles(checkbox.value, checkbox.checked);
};

function layerSelectorByTagOnChange(checkbox){
showHideDevicesByTag(checkbox.value, checkbox.checked)
};

function showHidePassiveDevicesButtonInitial() {
showHidePassiveDevicesButton = document.getElementById("showHidePassiveDevicesButton");
if (displayPassiveDevices == false) {
Expand Down
17 changes: 17 additions & 0 deletions nextbox_ui_plugin/static/nextbox_ui_plugin/next_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,20 @@
});
};

showHideDevicesByTag = function(deviceTag, isVisible) {
topologyData['nodes'].forEach(function(node){
if (node['tags'].length < 1) {
return;
};
node['tags'].forEach(function(tag){
if (tag == deviceTag) {
topo.graph().getVertex(node['id']).visible(isVisible);
return;
};
});
});
};

showHideLogicalMultiCableLinks = function() {
topologyData['links'].forEach(function(link){
if (link['isLogicalMultiCable']) {
Expand Down Expand Up @@ -328,6 +342,9 @@
undisplayedRoles.forEach(function(deviceRole){
showHideDeviceRoles(deviceRole, false);
});
undisplayedDeviceTags.forEach(function(deviceTag){
showHideDevicesByTag(deviceTag, false);
});
});

// Create an application instance
Expand Down
15 changes: 15 additions & 0 deletions nextbox_ui_plugin/templates/nextbox_ui_plugin/site_topology.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,27 @@
Select Layers
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<h6 class="dropdown-header">Device Roles</h6>
<div class="dropdown-divider"></div>
{% for device_role_slug, device_role_name, is_visible in device_roles %}
<div class="checkbox ml-1 my-1">
<label>
<input onchange="layerSelectorOnChange(this)" type="checkbox" value="{{ device_role_slug }}" {{ is_visible|yesno:"checked," }}>{{ device_role_name }}
</label>
</div>
{% endfor %}
<div class="dropdown-divider"></div>
{% if device_tags %}
<h6 class="dropdown-header">Device Tags</h6>
{% for tag, is_visible in device_tags %}
<div class="checkbox ml-1 my-1">
<label>
<input onchange="layerSelectorByTagOnChange(this)" type="checkbox" value="{{ tag }}" {{ is_visible|yesno:"checked," }}>{{ tag }}
</label>
</div>
{% endfor %}
{% endif %}
<div class="dropdown-divider"></div>
</div>
</div>
</div>
Expand All @@ -49,6 +63,7 @@
var displayPassiveDevices = {{ display_passive_devices|yesno:"true,false" }};
var displayLogicalMultiCableLinks = {{ display_logical_multicable_links|yesno:"true,false" }};
var undisplayedRoles = {{ undisplayed_roles|safe }};
var undisplayedDeviceTags = {{ undisplayed_device_tags|safe }};
var topologyData = {{ source_data|safe }};
var initialLayout = '{{ initial_layout|safe }}';
console.log(topologyData);
Expand Down
58 changes: 54 additions & 4 deletions nextbox_ui_plugin/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.conf import settings
import json
import re


# Default NeXt UI icons
Expand Down Expand Up @@ -131,6 +132,13 @@
# Hide these roles by default
UNDISPLAYED_DEVICE_ROLE_SLUGS = PLUGIN_SETTINGS.get("undisplayed_device_role_slugs", tuple())

# Hide devices tagged with these tags
UNDISPLAYED_DEVICE_TAGS = PLUGIN_SETTINGS.get("undisplayed_device_tags", tuple())

# Filter device tags listed in Select Layers menu
SELECT_LAYERS_LIST_INCLUDE_DEVICE_TAGS = PLUGIN_SETTINGS.get("select_layers_list_include_device_tags", tuple())
SELECT_LAYERS_LIST_EXCLUDE_DEVICE_TAGS = PLUGIN_SETTINGS.get("select_layers_list_exclude_device_tags", tuple())

# Defines the initial layer alignment direction on the view
INITIAL_LAYOUT = PLUGIN_SETTINGS.get("INITIAL_LAYOUT", 'vertical')
if INITIAL_LAYOUT not in ('vertical', 'horizontal', 'auto'):
Expand Down Expand Up @@ -184,22 +192,59 @@ def get_icon_type(device_id):
return 'unknown'


def tag_is_hidden(tag):
for tag_regex in UNDISPLAYED_DEVICE_TAGS:
if re.search(tag_regex, tag):
return True
return False


def filter_tags(tags):
if not tags:
return []
if SELECT_LAYERS_LIST_INCLUDE_DEVICE_TAGS:
filtered_tags = []
for tag in tags:
for tag_regex in SELECT_LAYERS_LIST_INCLUDE_DEVICE_TAGS:
if re.search(tag_regex, tag):
filtered_tags.append(tag)
break
if tag_is_hidden(tag):
filtered_tags.append(tag)
tags = filtered_tags
if SELECT_LAYERS_LIST_EXCLUDE_DEVICE_TAGS:
filtered_tags = []
for tag in tags:
for tag_regex in SELECT_LAYERS_LIST_EXCLUDE_DEVICE_TAGS:
if re.search(tag_regex, tag) and not tag_is_hidden(tag):
break
else:
filtered_tags.append(tag)
tags = filtered_tags
return tags


def get_site_topology(site_id):
topology_dict = {'nodes': [], 'links': []}
device_roles = set()
site_device_tags = set()
multi_cable_connections = []
if not site_id:
return topology_dict, device_roles, multi_cable_connections
return topology_dict, device_roles, multi_cable_connections, list(site_device_tags)
nb_devices = Device.objects.filter(site_id=site_id)
if not nb_devices:
return topology_dict, device_roles, multi_cable_connections
return topology_dict, device_roles, multi_cable_connections, list(site_device_tags)
links = []
device_ids = [d.id for d in nb_devices]
for nb_device in nb_devices:
device_is_passive = False
primary_ip = ''
if nb_device.primary_ip:
primary_ip = str(nb_device.primary_ip.address)
tags = [str(tag) for tag in nb_device.tags.names()] or []
tags = filter_tags(tags)
for tag in tags:
site_device_tags.add((tag, not tag_is_hidden(tag)))
links_from_device = Cable.objects.filter(_termination_a_device_id=nb_device.id)
links_to_device = Cable.objects.filter(_termination_b_device_id=nb_device.id)
if links_from_device:
Expand Down Expand Up @@ -230,6 +275,7 @@ def get_site_topology(site_id):
nb_device.id
),
'isPassive': device_is_passive,
'tags': tags,
})
is_visible = not (nb_device.device_role.slug in UNDISPLAYED_DEVICE_ROLE_SLUGS)
device_roles.add((nb_device.device_role.slug, nb_device.device_role.name, is_visible))
Expand All @@ -241,6 +287,8 @@ def get_site_topology(site_id):
links.append(link)
device_roles = list(device_roles)
device_roles.sort(key=lambda i: get_node_layer_sort_preference(i[0]))
site_device_tags = list(site_device_tags)
site_device_tags.sort()
if not links:
return topology_dict, device_roles, multi_cable_connections
link_ids = set()
Expand Down Expand Up @@ -272,21 +320,23 @@ def get_site_topology(site_id):
"tgtIfName": if_shortname(cable_path[-1][2].name),
"isLogicalMultiCable": True,
})
return topology_dict, device_roles, multi_cable_connections
return topology_dict, device_roles, multi_cable_connections, site_device_tags


class TopologyView(PermissionRequiredMixin, View):
"""Site Topology View"""
permission_required = ('dcim.view_site', 'dcim.view_device', 'dcim.view_cable')

def get(self, request, site_id):
topology_dict, device_roles, multi_cable_connections = get_site_topology(site_id)
topology_dict, device_roles, multi_cable_connections, device_tags = get_site_topology(site_id)

return render(request, 'nextbox_ui_plugin/site_topology.html', {
'source_data': json.dumps(topology_dict),
'display_unconnected': DISPLAY_UNCONNECTED,
'device_roles': device_roles,
'device_tags': device_tags,
'undisplayed_roles': list(UNDISPLAYED_DEVICE_ROLE_SLUGS),
'undisplayed_device_tags': list(UNDISPLAYED_DEVICE_TAGS),
'display_logical_multicable_links': DISPLAY_LOGICAL_MULTICABLE_LINKS,
'display_passive_devices': DISPLAY_PASSIVE_DEVICES,
'initial_layout': INITIAL_LAYOUT,
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

setup(
name='nextbox_ui_plugin',
version='0.5.0',
version='0.6.0',
url='https://github.com/iDebugAll/nextbox-ui-plugin',
download_url='https://github.com/iDebugAll/nextbox-ui-plugin/archive/v0.5.0.tar.gz',
download_url='https://github.com/iDebugAll/nextbox-ui-plugin/archive/v0.6.0.tar.gz',
description='A topology visualization plugin for Netbox powered by NextUI Toolkit.',
long_description=long_description,
long_description_content_type='text/markdown',
Expand Down

0 comments on commit 626bdc5

Please sign in to comment.