Initial commit

This commit is contained in:
Victor Golovanenko
2021-04-20 07:22:26 +00:00
commit d0b7fb5c44
15 changed files with 496 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
from extras.plugins import PluginConfig
class Config(PluginConfig):
name = 'netbox_interface_sync'
verbose_name = 'NetBox interface synchronization'
description = 'Syncing interfaces with the interfaces from device type for NetBox devices'
version = '0.1'
author = 'Victor Golovanenko'
author_email = 'drygdryg2014@yandex.ru'
default_settings = {
'exclude_virtual_interfaces': True
}
config = Config

View File

@@ -0,0 +1,6 @@
from django import forms
class InterfaceComparisonForm(forms.Form):
add_to_device = forms.BooleanField(required=False)
remove_from_device = forms.BooleanField(required=False)

View File

@@ -0,0 +1,29 @@
from extras.plugins import PluginTemplateExtension
from dcim.models import Interface, InterfaceTemplate
class DeviceViewExtension(PluginTemplateExtension):
model = "dcim.device"
def buttons(self):
"""Implements a compare interfaces button at the top of the page"""
obj = self.context['object']
return self.render("netbox_interface_sync/compare_interfaces_button.html", extra_context={
"device": obj
})
def right_page(self):
"""Implements a panel with the number of interfaces on the right side of the page"""
obj = self.context['object']
interfaces = Interface.objects.filter(device=obj)
real_interfaces = interfaces.exclude(type__in=["virtual", "lag"])
interface_templates = InterfaceTemplate.objects.filter(device_type=obj.device_type)
return self.render("netbox_interface_sync/number_of_interfaces_panel.html", extra_context={
"interfaces": interfaces,
"real_interfaces": real_interfaces,
"interface_templates": interface_templates
})
template_extensions = [DeviceViewExtension]

View File

@@ -0,0 +1,3 @@
<a href="{% url 'plugins:netbox_interface_sync:interface_comparison' device_id=device.id %}" class="btn btn-primary">
Compare device interfaces with device type interfaces
</a>

View File

@@ -0,0 +1,153 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}{{ device }} - Interface comparison{% endblock %}</h1>
{% endblock %}
{% block content %}
<style>
.checkbox-group {
position: absolute;
}
</style>
<script>
function toggle(event) {
event = event || window.event;
var src = event.target || event.srcElement || event;
checkboxes = document.getElementsByName(src.id);
for(var checkbox of checkboxes) checkbox.checked = src.checked;
}
function uncheck(event) {
event = event || window.event;
var src = event.target || event.srcElement || event;
if (src.checked == false) {
document.getElementById(src.name).checked = false;
}
}
</script>
<p>
{% if templates_count == interfaces_count %}
The device and device type have the same number of interfaces.
{% else %}
The device and device type have different number of interfaces.<br>
Device: {{ interfaces_count }}<br>
Device type: {{ templates_count }}
{% endif %}
</p>
<form method="post">
<!-- Interface templates -->
{% csrf_token %}
<table class="table" style="background:#F5F5F5; width: 50%; float: left;">
<tr>
<th colspan="2">Device type</th>
<th>Actions</th>
</tr>
<tr>
<th>Name</th>
<th>Type</th>
<th>
<label class="checkbox-group">
<input type="checkbox" id="add_to_device" onclick="toggle(this)">
Add to the device
</label>
</th>
</tr>
{% for template, interface in comparison_items %}
{% if template %}
<tr {% if not interface %}class="danger"{% endif %}>
<td>
{% if interface and template.name != interface.name %}
<span style="background-color: #eab2b2">{{ template.name }}</span>
{% else %}
{{ template.name }}
{% endif %}
</td>
<td>{{ template.type }}</td>
<td>
{% if not interface %}
<label class="checkbox-group">
<input type="checkbox" name="add_to_device" value="{{ template.id }}" onclick="uncheck(this)">
Add to device
</label>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
{% endif %}
{% endfor %}
</table>
<table class="table" style="width: 50%; float: right;">
<!-- Interfaces -->
<tr>
<th colspan="2">Device</th>
<th colspan="2">Actions</th>
</tr>
<tr>
<th>Name</th>
<th>Type</th>
<th>
<label class="checkbox-group">
<input type="checkbox" id="remove_from_device" onclick="toggle(this)">
Remove
</label>
</th>
<th>
<label class="checkbox-group">
<input type="checkbox" id="fix_name" onclick="toggle(this)">
Fix the name
</label>
</th>
</tr>
{% for template, interface in comparison_items %}
{% if interface %}
<tr {% if not template %}class="success"{% endif %}>
<td>
{% if template and template.name != interface.name %}
<span style="background-color: #cde8c2">{{ interface.name }}</span>
{% else %}
{{ interface.name }}
{% endif %}
</td>
<td>{{ interface.type }}</td>
<td>
{% if not template %}
<label class="checkbox-group">
<input type="checkbox" name="remove_from_device" value="{{ interface.id }}" onclick="uncheck(this)">
Remove
</label>
{% endif %}
</td>
<td>
{% if template and template.name != interface.name %}
<label class="checkbox-group">
<input type="checkbox" name="fix_name" value="{{ interface.id }}" onclick="uncheck(this)">
Fix name
</label>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
{% endif %}
{% endfor %}
</table>
<div class="text-right">
<input type="submit" value="Apply" class="btn btn-primary">
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,12 @@
<div class="panel panel-default">
<div class="panel-heading">
<strong>Number of interfaces</strong>
</div>
<div class="panel-body">
Total interfaces: {{ interfaces|length }}<br>
{% if config.exclude_virtual_interfaces %}
Non-virtual interfaces: {{ real_interfaces|length }}<br>
{% endif %}
Interfaces in the corresponding device type: {{ interface_templates|length }}
</div>
</div>

View File

@@ -0,0 +1,10 @@
from django.urls import path
from . import views
# Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to
# a specific view so that it can be accessed by users.
urlpatterns = (
path("interface-comparison/<int:device_id>/", views.InterfaceComparisonView.as_view(), name="interface_comparison"),
)

View File

@@ -0,0 +1,34 @@
import re
from typing import Iterable
from dataclasses import dataclass
def split(s):
for x, y in re.findall(r'(\d*)(\D*)', s):
yield '', int(x or '0')
yield y, 0
def natural_keys(c):
return tuple(split(c))
def human_sorted(iterable: Iterable):
return sorted(iterable, key=natural_keys)
@dataclass(frozen=True)
class UnifiedInterface:
"""A unified way to represent the interface and interface template"""
id: int
name: str
type: str
is_template: bool = False
def __eq__(self, other):
# Ignore some fields when comparing; ignore interface name case and whitespaces
return (self.name.lower().replace(' ', '') == other.name.lower().replace(' ', '')) and (self.type == other.type)
def __hash__(self):
# Ignore some fields when hashing; ignore interface name case and whitespaces
return hash((self.name.lower().replace(' ', ''), self.type))

View File

@@ -0,0 +1,112 @@
from django.shortcuts import get_object_or_404, render, redirect
from django.views.generic import View
from dcim.models import Device, Interface, InterfaceTemplate
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.conf import settings
from django.contrib import messages
from .utils import UnifiedInterface, natural_keys
from .forms import InterfaceComparisonForm
config = settings.PLUGINS_CONFIG['netbox_interface_sync']
class InterfaceComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View):
"""Comparison of interfaces between a device and a device type and beautiful visualization"""
permission_required = ("dcim.view_interface", "dcim.add_interface", "dcim.change_interface", "dcim.delete_interface")
def get(self, request, device_id):
device = get_object_or_404(Device.objects.filter(id=device_id))
interfaces = Interface.objects.filter(device=device)
if config["exclude_virtual_interfaces"]:
interfaces = list(filter(lambda i: not i.is_virtual, interfaces))
interface_templates = InterfaceTemplate.objects.filter(device_type=device.device_type)
unified_interfaces = [UnifiedInterface(i.id, i.name, i.type) for i in interfaces]
unified_interface_templates = [UnifiedInterface(i.id, i.name, i.type, is_template=True) for i in interface_templates]
# List of interfaces and interface templates presented in the unified format
overall_interfaces = list(set(unified_interface_templates + unified_interfaces))
overall_interfaces.sort(key=lambda o: natural_keys(o.name))
comparison_templates = []
comparison_interfaces = []
for i in overall_interfaces:
try:
comparison_templates.append(unified_interface_templates[unified_interface_templates.index(i)])
except ValueError:
comparison_templates.append(None)
try:
comparison_interfaces.append(unified_interfaces[unified_interfaces.index(i)])
except ValueError:
comparison_interfaces.append(None)
comparison_items = list(zip(comparison_templates, comparison_interfaces))
return render(
request, "netbox_interface_sync/interface_comparison.html",
{
"comparison_items": comparison_items,
"templates_count": len(interface_templates),
"interfaces_count": len(interfaces),
"device": device
}
)
def post(self, request, device_id):
form = InterfaceComparisonForm(request.POST)
if form.is_valid():
device = get_object_or_404(Device.objects.filter(id=device_id))
interfaces = Interface.objects.filter(device=device)
if config["exclude_virtual_interfaces"]:
interfaces = interfaces.exclude(type__in=["virtual", "lag"])
interface_templates = InterfaceTemplate.objects.filter(device_type=device.device_type)
# Manually validating interfaces and interface templates lists
add_to_device = filter(
lambda i: i in interface_templates.values_list("id", flat=True),
map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add_to_device")))
)
remove_from_device = filter(
lambda i: i in interfaces.values_list("id", flat=True),
map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device")))
)
# Add selected interfaces to the device and count them
add_to_device_interfaces = InterfaceTemplate.objects.filter(id__in=add_to_device)
interfaces_created = len(Interface.objects.bulk_create([
Interface(device=device, name=i.name, type=i.type) for i in add_to_device_interfaces
]))
# Remove selected interfaces from the device and count them
interfaces_deleted = Interface.objects.filter(id__in=remove_from_device).delete()[0]
# Getting and validating a list of interfaces to rename
fix_name_interfaces = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), interfaces)
# Casting interface templates into UnifiedInterface objects for proper comparison with interfaces for renaming
unified_interface_templates = [UnifiedInterface(i.id, i.name, i.type) for i in interface_templates]
# Rename selected interfaces
interfaces_fixed = 0
for interface in fix_name_interfaces:
unified_interface = UnifiedInterface(interface.id, interface.name, interface.type)
try:
# Try to extract an interface template with the corresponding name
corresponding_template = unified_interface_templates[unified_interface_templates.index(unified_interface)]
interface.name = corresponding_template.name
interface.save()
interfaces_fixed += 1
except ValueError:
pass
# Generating result message
message = []
if interfaces_created > 0:
message.append(f"created {interfaces_created} interfaces")
if interfaces_deleted > 0:
message.append(f"deleted {interfaces_deleted} interfaces")
if interfaces_fixed > 0:
message.append(f"fixed {interfaces_fixed} interfaces")
messages.success(request, "; ".join(message).capitalize())
return redirect(request.path)