Initial commit

This commit is contained in:
Victor Golovanenko 2021-04-20 07:22:26 +00:00
commit d0b7fb5c44
No known key found for this signature in database
GPG Key ID: F3B58DF29DBC2099
15 changed files with 496 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

41
README.md Normal file
View File

@ -0,0 +1,41 @@
# netbox-interface-sync
[Русская версия](./README_ru.md)
## Overview
This plugin allows you to compare and synchronize interfaces between devices and device types in NetBox. It can be useful for finding and correcting inconsistencies between interfaces.
Tested with NetBox versions 2.10, 2.11
## Installation
If your NetBox installation uses virtualenv, activate it like this:
```
source /opt/netbox/venv/bin/activate
```
Clone this repository, then go to the folder with it and install the plugin:
```
pip install .
```
To enable to plugin, add the plugin's name to the `PLUGINS` list in `configuration.py` (it's usually located in `/opt/netbox/netbox/netbox/`) like so:
```
PLUGINS = [
'netbox_interface_sync'
]
```
Don't forget to restart NetBox:
```
sudo systemctl restart netbox
```
## Usage
To compare the interfaces, open the page of the desired device and find the "Compare device interfaces with device type interfaces" button":
![Device page](docs/images/1_device_page.png)
Mark the required actions with the checkboxes and click "Apply".
![Interface comparison](docs/images/2_interface_comparison.png)
### Plugin settings
If you want to override the default values, configure the `PLUGINS_CONFIG` in your `configuration.py`:
```
PLUGINS_CONFIG = {
'netbox_interface_sync': {
'exclude_virtual_interfaces': True
}
}
```
| Setting | Default value | Description |
| --- | --- | --- |
| exclude_virtual_interfaces | `True` | Exclude virtual interfaces (VLANs, LAGs) from comparison

40
README_ru.md Normal file
View File

@ -0,0 +1,40 @@
# netbox-interface-sync
[English version](./README.md)
## Обзор
Плагин для NetBox, позволяющий сравнивать и синхронизировать интерфейсы между устройствами (devices) и типами устройств (device types). Полезен для поиска и исправления несоответствий между интерфейсами. Работа проверена с NetBox версий 2.10, 2.11
## Установка
Если NetBox использует virtualenv, то активируйте его, например, так:
```
source /opt/netbox/venv/bin/activate
```
Склонируйте этот репозиторий, затем перейдите в папку с ним и установите плагин:
```
pip install .
```
Включите плагин в файле `configuration.py` (обычно он находится в `/opt/netbox/netbox/netbox/`), добавьте его имя в список `PLUGINS`:
```
PLUGINS = [
'netbox_interface_sync'
]
```
Перезапустите NetBox:
```
sudo systemctl restart netbox
```
## Использование
Для того чтобы сравнить интерфейсы, откройте страницу нужного устройства и найдите кнопку "Compare device interfaces with device type interfaces" справа сверху:
![Device page](docs/images/1_device_page.png)
Отметьте требуемые действия напротив интерфейсов флажками и нажмите "Apply".
![Interface comparison](docs/images/2_interface_comparison.png)
### Настройки плагина
Если вы хотите переопределить значения по умолчанию, настройте переменную `PLUGINS_CONFIG` в вашем файле `configuration.py`:
```
PLUGINS_CONFIG = {
'netbox_interface_sync': {
'exclude_virtual_interfaces': True
}
}
```
| Настройка | Значение по умолчанию | Описание |
| --- | --- | --- |
| exclude_virtual_interfaces | `True` | Не учитывать виртуальные интерфейсы (VLAN, LAG) при сравнении

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

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)

13
setup.py Normal file
View File

@ -0,0 +1,13 @@
from setuptools import setup
setup(
name='netbox-interface-sync',
version='0.1',
description='Syncing interfaces with the interfaces from device type for NetBox devices',
author='Victor Golovanenko',
license='GPL-3.0',
install_requires=[],
packages=["netbox_interface_sync"],
package_data={"netbox_interface_sync": ["templates/netbox_interface_sync/*.html"]},
zip_safe=False
)