From 54e90ecbec35458ab9a983d631c8decc0f12fa26 Mon Sep 17 00:00:00 2001
From: Keith Knowles <128771411+NetTech2001@users.noreply.github.com>
Date: Sun, 12 May 2024 11:58:17 -0600
Subject: [PATCH 01/10] Update __init__.py
Adds Netbox 4 plugin resource locations
---
netbox_interface_sync/__init__.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/netbox_interface_sync/__init__.py b/netbox_interface_sync/__init__.py
index 6046865..f7ceee8 100644
--- a/netbox_interface_sync/__init__.py
+++ b/netbox_interface_sync/__init__.py
@@ -1,13 +1,13 @@
-from extras.plugins import PluginConfig
+from netbox.plugins import PluginConfig
class Config(PluginConfig):
name = 'netbox_interface_sync'
- verbose_name = 'NetBox interface synchronization'
- description = 'Compare and synchronize components (interfaces, ports, outlets, etc.) between NetBox device types ' \
+ verbose_name = 'NetBox 4 Interface Synchronization'
+ description = 'Compare and synchronize components (interfaces, ports, outlets, etc.) between NetBox 4 device types ' \
'and devices'
- version = '0.2.0'
- author = 'Victor Golovanenko'
+ version = '0.4.0'
+ author = 'based on work by Victor Golovanenko'
author_email = 'drygdryg2014@yandex.ru'
default_settings = {
# Ignore case and spaces in names when matching components between device type and device
From 6fe7da696ca709d0ffabe8431cb8697bb8ccb2b3 Mon Sep 17 00:00:00 2001
From: Keith Knowles <128771411+NetTech2001@users.noreply.github.com>
Date: Sun, 12 May 2024 12:54:13 -0600
Subject: [PATCH 02/10] Update template_content.py
Netbox 4 support
---
netbox_interface_sync/template_content.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/netbox_interface_sync/template_content.py b/netbox_interface_sync/template_content.py
index 4e95d3c..71a4cc9 100644
--- a/netbox_interface_sync/template_content.py
+++ b/netbox_interface_sync/template_content.py
@@ -1,4 +1,4 @@
-from extras.plugins import PluginTemplateExtension
+from netbox.plugins import PluginTemplateExtension
from dcim.models import Interface, InterfaceTemplate
From 6d7eb97001253c80840aa643783171709a26a83f Mon Sep 17 00:00:00 2001
From: Keith Knowles <128771411+NetTech2001@users.noreply.github.com>
Date: Sun, 12 May 2024 13:00:07 -0600
Subject: [PATCH 03/10] Update setup.py
Update versioning and author
---
setup.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/setup.py b/setup.py
index 6b8cfc3..adefb7d 100644
--- a/setup.py
+++ b/setup.py
@@ -5,11 +5,11 @@ with open('README.md', encoding='utf-8') as f:
setup(
name='netbox-interface-sync',
- version='0.2.0',
- description='Syncing interfaces with the interfaces from device type for NetBox devices',
+ version='0.4.0',
+ description='Syncing interfaces with the interfaces from device type for NetBox 4 devices',
long_description=long_description,
long_description_content_type='text/markdown',
- author='Victor Golovanenko',
+ author='Based on work by Victor Golovanenko',
author_email='drygdryg2014@yandex.com',
license='GPL-3.0',
install_requires=['attrs>=21.1.0'],
From 1f0869ad1349f50d43bc0ebf823b1e01a95ed7fd Mon Sep 17 00:00:00 2001
From: Keith Knowles <128771411+NetTech2001@users.noreply.github.com>
Date: Sun, 12 May 2024 13:25:42 -0600
Subject: [PATCH 04/10] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 7940f97..9c8e113 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[Русская версия](./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
+Tested with NetBox versions 2.10, 2.11, 3.X, 4.0
## Installation
If your NetBox installation uses virtualenv, activate it like this:
```
From c9c1130b6b44737459aed7f28fe859d3846e04a9 Mon Sep 17 00:00:00 2001
From: Keith Knowles <128771411+NetTech2001@users.noreply.github.com>
Date: Sun, 12 May 2024 13:56:44 -0600
Subject: [PATCH 05/10] Update compare_components_button.html
Removed btn-sm veriable
---
.../netbox_interface_sync/compare_components_button.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html b/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html
index c451520..3682f71 100644
--- a/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html
+++ b/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html
@@ -1,6 +1,6 @@
{% if perms.dcim.change_device %}
-
+
Device type sync
-{% endif %}
\ No newline at end of file
+{% endif %}
From 8cab8c9b700c3b36be0bdec11950a57badfe8ec6 Mon Sep 17 00:00:00 2001
From: Keith Knowles <128771411+NetTech2001@users.noreply.github.com>
Date: Sun, 12 May 2024 14:09:15 -0600
Subject: [PATCH 06/10] Update README.md
Removed old version which would not be supported from README
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 9c8e113..ec7eeb3 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[Русская версия](./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, 3.X, 4.0
+Tested with NetBox version 4.0
## Installation
If your NetBox installation uses virtualenv, activate it like this:
```
From 51b75bc6d587593a1a5cd3c21d157e41d080dbff Mon Sep 17 00:00:00 2001
From: Keith Knowles <128771411+NetTech2001@users.noreply.github.com>
Date: Sun, 12 May 2024 14:10:22 -0600
Subject: [PATCH 07/10] Update README_ru.md
Removed old unsupported version from Russian README.
---
README_ru.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README_ru.md b/README_ru.md
index 0d04b4f..0b48464 100644
--- a/README_ru.md
+++ b/README_ru.md
@@ -1,7 +1,7 @@
# netbox-interface-sync
[English version](./README.md)
## Обзор
-Плагин для NetBox, позволяющий сравнивать и синхронизировать интерфейсы между устройствами (devices) и типами устройств (device types). Полезен для поиска и исправления несоответствий между интерфейсами. Работа проверена с NetBox версий 2.10, 2.11
+Плагин для NetBox, позволяющий сравнивать и синхронизировать интерфейсы между устройствами (devices) и типами устройств (device types). Полезен для поиска и исправления несоответствий между интерфейсами. Работа проверена с NetBox версий 4.0
## Установка
Если NetBox использует virtualenv, то активируйте его, например, так:
```
From b48cfaca4bd24b99a009af80b3739c81feaaeb6f Mon Sep 17 00:00:00 2001
From: NetTech2001 <128771411+NetTech2001@users.noreply.github.com>
Date: Fri, 17 May 2024 16:00:56 -0600
Subject: [PATCH 08/10] Ready for netboxcommunity
This plugin has been re-developed for Netbox 4. The sync button has been redesigned as well as the colors of the sync table.
---
netbox_interface_sync/__init__.py | 22 +-
netbox_interface_sync/comparison.py | 156 ------
netbox_interface_sync/forms.py | 6 +
netbox_interface_sync/template_content.py | 4 +-
.../compare_components_button.html | 65 ---
.../compare_interfaces_button.html | 3 +
.../compare_interfaces_button.html-old | 3 +
.../components_comparison.html | 158 ------
.../interface_comparison.html | 161 ++++++
.../interface_comparison.html-keith | 161 ++++++
.../interface_comparison.html-old | 161 ++++++
.../number_of_interfaces_panel.html | 20 +-
netbox_interface_sync/urls.py | 41 +-
netbox_interface_sync/utils.py | 40 +-
netbox_interface_sync/views.py | 519 +++---------------
15 files changed, 616 insertions(+), 904 deletions(-)
delete mode 100644 netbox_interface_sync/comparison.py
create mode 100644 netbox_interface_sync/forms.py
delete mode 100644 netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html
create mode 100644 netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html
create mode 100644 netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html-old
delete mode 100644 netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html
create mode 100644 netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html
create mode 100644 netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-keith
create mode 100644 netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-old
diff --git a/netbox_interface_sync/__init__.py b/netbox_interface_sync/__init__.py
index f7ceee8..20abc48 100644
--- a/netbox_interface_sync/__init__.py
+++ b/netbox_interface_sync/__init__.py
@@ -3,25 +3,13 @@ from netbox.plugins import PluginConfig
class Config(PluginConfig):
name = 'netbox_interface_sync'
- verbose_name = 'NetBox 4 Interface Synchronization'
- description = 'Compare and synchronize components (interfaces, ports, outlets, etc.) between NetBox 4 device types ' \
- 'and devices'
+ verbose_name = 'NetBox interface synchronization'
+ description = 'Syncing interfaces with the interfaces from device type for NetBox 4'
version = '0.4.0'
- author = 'based on work by Victor Golovanenko'
- author_email = 'drygdryg2014@yandex.ru'
+ author = 'Keith Knowles'
+ author_email = 'mkknowles@outlook.com'
default_settings = {
- # Ignore case and spaces in names when matching components between device type and device
- 'name_comparison': {
- 'case-insensitive': True,
- 'space-insensitive': True
- },
- # Exclude virtual interfaces (bridge, link aggregation group (LAG), "virtual") from comparison
- 'exclude_virtual_interfaces': True,
- # Add a panel with information about the number of interfaces to the device page
- 'include_interfaces_panel': False,
- # Consider component descriptions when comparing. If this option is set to True, then take into account
- # component descriptions when comparing components and synchronizing their attributes, otherwise - ignore
- 'sync_descriptions': True
+ 'exclude_virtual_interfaces': True
}
diff --git a/netbox_interface_sync/comparison.py b/netbox_interface_sync/comparison.py
deleted file mode 100644
index d643f9d..0000000
--- a/netbox_interface_sync/comparison.py
+++ /dev/null
@@ -1,156 +0,0 @@
-from typing import Optional
-
-import attr
-from attrs import fields
-from django.conf import settings
-
-from netbox.models import PrimaryModel
-
-config = settings.PLUGINS_CONFIG["netbox_interface_sync"]
-SYNC_DESCRIPTIONS: bool = config["sync_descriptions"]
-
-
-@attr.s(frozen=True, auto_attribs=True)
-class BaseComparison:
- """Common fields of a device component"""
- # Do not compare IDs
- id: int = attr.ib(eq=False, metadata={'printable': False, 'netbox_exportable': False})
- # Compare names case-insensitively and spaces-insensitively
- name: str = attr.ib(metadata={'printable': False})
- label: str = attr.ib()
- # Compare descriptions if it is set by the configuration
- description: str = attr.ib(eq=SYNC_DESCRIPTIONS, metadata={'synced': SYNC_DESCRIPTIONS})
- # Do not compare `is_template` properties
- is_template: bool = attr.ib(
- default=False, kw_only=True, eq=False,
- metadata={'printable': False, 'netbox_exportable': False}
- )
-
- @property
- def fields_display(self) -> str:
- """Generate human-readable list of printable fields to display in the comparison table"""
- fields_to_display = []
- for field in fields(self.__class__):
- if not field.metadata.get('printable', True):
- continue
- field_value = getattr(self, field.name)
- if not field_value:
- continue
- field_caption = field.metadata.get('displayed_caption') or field.name.replace('_', ' ').capitalize()
- if isinstance(field_value, BaseComparison):
- field_value = f'{field_value.name} (ID: {field_value.id})'
- fields_to_display.append(f'{field_caption}: {field_value}')
- return '\n'.join(fields_to_display)
-
- def get_fields_for_netbox_component(self, sync=False):
- """
- Returns a dict of fields and values for creating or updating a NetBox component object
- :param sync: if True, returns fields for syncing an existing component, otherwise - for creating a new one.
- """
-
- def field_filter(field: attr.Attribute, _):
- result = field.metadata.get('netbox_exportable', True)
- if sync:
- result &= field.metadata.get('synced', True)
- return result
-
- return attr.asdict(self, recurse=True, filter=field_filter)
-
-
-@attr.s(frozen=True, auto_attribs=True)
-class BaseTypedComparison(BaseComparison):
- """Common fields of a device typed component"""
- type: str = attr.ib(metadata={'printable': False})
- type_display: str = attr.ib(eq=False, metadata={'displayed_caption': 'Type', 'netbox_exportable': False})
-
-
-@attr.s(frozen=True, auto_attribs=True)
-class ConsolePortComparison(BaseTypedComparison):
- """A unified way to represent the consoleport and consoleport template"""
- pass
-
-
-@attr.s(frozen=True, auto_attribs=True)
-class ConsoleServerPortComparison(BaseTypedComparison):
- """A unified way to represent the consoleserverport and consoleserverport template"""
- pass
-
-
-@attr.s(frozen=True, auto_attribs=True)
-class PowerPortComparison(BaseTypedComparison):
- """A unified way to represent the power port and power port template"""
- maximum_draw: str = attr.ib()
- allocated_draw: str = attr.ib()
-
-
-@attr.s(frozen=True, auto_attribs=True)
-class PowerOutletComparison(BaseTypedComparison):
- """A unified way to represent the power outlet and power outlet template"""
- power_port: PowerPortComparison = attr.ib()
- feed_leg: str = attr.ib()
-
-
-@attr.s(frozen=True, auto_attribs=True)
-class InterfaceComparison(BaseTypedComparison):
- """A unified way to represent the interface and interface template"""
- mgmt_only: bool = attr.ib()
-
-
-@attr.s(frozen=True, auto_attribs=True)
-class FrontPortComparison(BaseTypedComparison):
- """A unified way to represent the front port and front port template"""
- color: str = attr.ib()
- # rear_port_id: int
- rear_port_position: int = attr.ib(metadata={'displayed_caption': 'Position'})
-
-
-@attr.s(frozen=True, auto_attribs=True)
-class RearPortComparison(BaseTypedComparison):
- """A unified way to represent the rear port and rear port template"""
- color: str = attr.ib()
- positions: int = attr.ib()
-
-
-@attr.s(frozen=True, auto_attribs=True)
-class DeviceBayComparison(BaseComparison):
- """A unified way to represent the device bay and device bay template"""
- pass
-
-
-def from_netbox_object(netbox_object: PrimaryModel) -> Optional[BaseComparison]:
- """Makes a comparison object from the NetBox object"""
- type_map = {
- "DeviceBay": DeviceBayComparison,
- "Interface": InterfaceComparison,
- "FrontPort": FrontPortComparison,
- "RearPort": RearPortComparison,
- "ConsolePort": ConsolePortComparison,
- "ConsoleServerPort": ConsoleServerPortComparison,
- "PowerPort": PowerPortComparison,
- "PowerOutlet": PowerOutletComparison
- }
-
- obj_name = netbox_object._meta.object_name
- if obj_name.endswith("Template"):
- is_template = True
- obj_name = obj_name[:-8] # TODO: use `removesuffix` introduced in Python 3.9
- else:
- is_template = False
-
- comparison = type_map.get(obj_name)
- if not comparison:
- return
-
- values = {}
- for field in fields(comparison):
- if field.name == "is_template":
- continue
- if field.name == "type_display":
- values[field.name] = netbox_object.get_type_display()
- else:
- field_value = getattr(netbox_object, field.name)
- if isinstance(field_value, PrimaryModel):
- field_value = from_netbox_object(field_value)
- values[field.name] = field_value
-
- return comparison(**values, is_template=is_template)
diff --git a/netbox_interface_sync/forms.py b/netbox_interface_sync/forms.py
new file mode 100644
index 0000000..0799ebe
--- /dev/null
+++ b/netbox_interface_sync/forms.py
@@ -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)
diff --git a/netbox_interface_sync/template_content.py b/netbox_interface_sync/template_content.py
index 71a4cc9..750ee09 100644
--- a/netbox_interface_sync/template_content.py
+++ b/netbox_interface_sync/template_content.py
@@ -6,9 +6,9 @@ class DeviceViewExtension(PluginTemplateExtension):
model = "dcim.device"
def buttons(self):
- """Implements a compare button at the top of the page"""
+ """Implements a compare interfaces button at the top of the page"""
obj = self.context['object']
- return self.render("netbox_interface_sync/compare_components_button.html", extra_context={
+ return self.render("netbox_interface_sync/compare_interfaces_button.html", extra_context={
"device": obj
})
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html b/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html
deleted file mode 100644
index 3682f71..0000000
--- a/netbox_interface_sync/templates/netbox_interface_sync/compare_components_button.html
+++ /dev/null
@@ -1,65 +0,0 @@
-{% if perms.dcim.change_device %}
-
-
- Device type sync
-
-
-
-{% endif %}
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html b/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html
new file mode 100644
index 0000000..5594df4
--- /dev/null
+++ b/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html
@@ -0,0 +1,3 @@
+
+ Interface Sync
+
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html-old b/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html-old
new file mode 100644
index 0000000..a64fec2
--- /dev/null
+++ b/netbox_interface_sync/templates/netbox_interface_sync/compare_interfaces_button.html-old
@@ -0,0 +1,3 @@
+
+ Interface Sync
+
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html b/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html
deleted file mode 100644
index 7ca570c..0000000
--- a/netbox_interface_sync/templates/netbox_interface_sync/components_comparison.html
+++ /dev/null
@@ -1,158 +0,0 @@
-{% extends 'base/layout.html' %}
-
-{% block title %}{{ device }} - {{ component_type_name|capfirst }} comparison{% endblock %}
-{% block header %}
-
-
- Devices
- {{ device.site }}
- {{ device }}
-
-
- {{ block.super }}
-{% endblock %}
-
-{% block content %}
-
-
-
-
-
-{% endblock %}
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html
new file mode 100644
index 0000000..eda27e1
--- /dev/null
+++ b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html
@@ -0,0 +1,161 @@
+{% extends 'base/layout.html' %}
+
+{% block title %}{{ device }} - Interface comparison{% endblock %}
+{% block header %}
+
+
+ Devices
+ {{ device.site }}
+ {{ device }}
+
+
+ {{ block.super }}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+{% if templates_count == interfaces_count %}
+ The device and device type have the same number of interfaces.
+{% else %}
+ The device and device type have a different number of interfaces.
+ Device: {{ interfaces_count }}
+ Device type: {{ templates_count }}
+{% endif %}
+
+
+
+
+{% endblock %}
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-keith b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-keith
new file mode 100644
index 0000000..54d102d
--- /dev/null
+++ b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-keith
@@ -0,0 +1,161 @@
+{% extends 'base/layout.html' %}
+
+{% block title %}{{ device }} - Interface comparison{% endblock %}
+{% block header %}
+
+
+ Devices
+ {{ device.site }}
+ {{ device }}
+
+
+ {{ block.super }}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+{% if templates_count == interfaces_count %}
+ The device and device type have the same number of interfaces.
+{% else %}
+ The device and device type have a different number of interfaces.
+ Device: {{ interfaces_count }}
+ Device type: {{ templates_count }}
+{% endif %}
+
+
+
+
+{% endblock %}
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-old b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-old
new file mode 100644
index 0000000..063634d
--- /dev/null
+++ b/netbox_interface_sync/templates/netbox_interface_sync/interface_comparison.html-old
@@ -0,0 +1,161 @@
+{% extends 'base/layout.html' %}
+
+{% block title %}{{ device }} - Interface comparison{% endblock %}
+{% block header %}
+
+
+ Devices
+ {{ device.site }}
+ {{ device }}
+
+
+ {{ block.super }}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+{% if templates_count == interfaces_count %}
+ The device and device type have the same number of interfaces.
+{% else %}
+ The device and device type have a different number of interfaces.
+ Device: {{ interfaces_count }}
+ Device type: {{ templates_count }}
+{% endif %}
+
+
+
+
+{% endblock %}
diff --git a/netbox_interface_sync/templates/netbox_interface_sync/number_of_interfaces_panel.html b/netbox_interface_sync/templates/netbox_interface_sync/number_of_interfaces_panel.html
index 8d9b2ad..98bc1ef 100644
--- a/netbox_interface_sync/templates/netbox_interface_sync/number_of_interfaces_panel.html
+++ b/netbox_interface_sync/templates/netbox_interface_sync/number_of_interfaces_panel.html
@@ -1,12 +1,10 @@
-{% if config.include_interfaces_panel %}
-
-
-
- Total interfaces: {{ interfaces|length }}
- {% if config.exclude_virtual_interfaces %}
- Non-virtual interfaces: {{ real_interfaces|length }}
- {% endif %}
- Interfaces in the assigned device type: {{ interface_templates|length }}
-
+
+
+
+ Total interfaces: {{ interfaces|length }}
+ {% if config.exclude_virtual_interfaces %}
+ Non-virtual interfaces: {{ real_interfaces|length }}
+ {% endif %}
+ Interfaces in the assigned device type: {{ interface_templates|length }}
-{% endif %}
\ No newline at end of file
+
\ No newline at end of file
diff --git a/netbox_interface_sync/urls.py b/netbox_interface_sync/urls.py
index 847e18c..cef81f2 100644
--- a/netbox_interface_sync/urls.py
+++ b/netbox_interface_sync/urls.py
@@ -6,44 +6,5 @@ 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(
- "consoleport-comparison/
/",
- views.ConsolePortComparisonView.as_view(),
- name="consoleport_comparison",
- ),
- path(
- "consoleserverport-comparison//",
- views.ConsoleServerPortComparisonView.as_view(),
- name="consoleserverport_comparison",
- ),
- path(
- "interface-comparison//",
- views.InterfaceComparisonView.as_view(),
- name="interface_comparison",
- ),
- path(
- "powerport-comparison//",
- views.PowerPortComparisonView.as_view(),
- name="powerport_comparison",
- ),
- path(
- "poweroutlet-comparison//",
- views.PowerOutletComparisonView.as_view(),
- name="poweroutlet_comparison",
- ),
- # path(
- # "frontport-comparison//",
- # views.FrontPortComparisonView.as_view(),
- # name="frontport_comparison",
- # ),
- path(
- "rearport-comparison//",
- views.RearPortComparisonView.as_view(),
- name="rearport_comparison",
- ),
- path(
- "devicebay-comparison//",
- views.DeviceBayComparisonView.as_view(),
- name="devicebay_comparison",
- ),
+ path("interface-comparison//", views.InterfaceComparisonView.as_view(), name="interface_comparison"),
)
diff --git a/netbox_interface_sync/utils.py b/netbox_interface_sync/utils.py
index e6b0bd6..19d7fd1 100644
--- a/netbox_interface_sync/utils.py
+++ b/netbox_interface_sync/utils.py
@@ -1,13 +1,11 @@
import re
-from typing import Iterable, List
-from django.conf import settings
-
-config = settings.PLUGINS_CONFIG['netbox_interface_sync']
+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")
+ for x, y in re.findall(r'(\d*)(\D*)', s):
+ yield '', int(x or '0')
yield y, 0
@@ -19,21 +17,19 @@ def human_sorted(iterable: Iterable):
return sorted(iterable, key=natural_keys)
-def make_integer_list(lst: List[str]):
- return [int(i) for i in lst if i.isdigit()]
+@dataclass(frozen=True)
+class UnifiedInterface:
+ """A unified way to represent the interface and interface template"""
+ id: int
+ name: str
+ type: str
+ type_display: 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 get_permissions_for_model(model, actions: Iterable[str]) -> List[str]:
- """
- Resolve a list of permissions for a given model (or instance).
-
- :param model: A model or instance
- :param actions: List of actions: view, add, change, or delete
- """
- permissions = []
- for action in actions:
- if action not in ("view", "add", "change", "delete"):
- raise ValueError(f"Unsupported action: {action}")
- permissions.append(f'{model._meta.app_label}.{action}_{model._meta.model_name}')
-
- return permissions
+ def __hash__(self):
+ # Ignore some fields when hashing; ignore interface name case and whitespaces
+ return hash((self.name.lower().replace(' ', ''), self.type))
diff --git a/netbox_interface_sync/views.py b/netbox_interface_sync/views.py
index fd46a84..55adfcd 100644
--- a/netbox_interface_sync/views.py
+++ b/netbox_interface_sync/views.py
@@ -1,461 +1,114 @@
-from collections import namedtuple
-from typing import Type, Tuple
-
-from django.db.models import QuerySet
-from django.shortcuts import get_object_or_404, redirect, render
+from django.shortcuts import get_object_or_404, render, redirect
from django.views.generic import View
-from dcim.models import (Device, Interface, InterfaceTemplate, PowerPort, PowerPortTemplate, ConsolePort,
- ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, DeviceBay,
- DeviceBayTemplate, FrontPort, FrontPortTemplate, PowerOutlet, PowerOutletTemplate, RearPort,
- RearPortTemplate)
-from django.contrib.auth.mixins import PermissionRequiredMixin
+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 netbox.models import PrimaryModel
-from dcim.constants import VIRTUAL_IFACE_TYPES
-
-from . import comparison
-from .utils import get_permissions_for_model, make_integer_list, human_sorted
+from .utils import UnifiedInterface, natural_keys
+from .forms import InterfaceComparisonForm
config = settings.PLUGINS_CONFIG['netbox_interface_sync']
-ComparisonTableRow = namedtuple('ComparisonTableRow', ('component_template', 'component'))
-class GenericComparisonView(PermissionRequiredMixin, View):
- """
- Generic object comparison view
-
- obj_model: Model of the object involved in the comparison (for example, Interface)
- obj_template_model: Model of the object template involved in the comparison (for example, InterfaceTemplate)
- """
- obj_model: Type[PrimaryModel] = None
- obj_template_model: Type[PrimaryModel] = None
-
- def get_permission_required(self):
- # User must have permission to view the device whose components are being compared
- permissions = ["dcim.view_device"]
-
- # Resolve permissions related to the object and the object template
- permissions.extend(get_permissions_for_model(self.obj_model, ("view", "add", "change", "delete")))
- permissions.extend(get_permissions_for_model(self.obj_template_model, ("view",)))
-
- return permissions
-
- @staticmethod
- def filter_comparison_components(component_templates: QuerySet, components: QuerySet) -> Tuple[QuerySet, QuerySet]:
- """Override this in the inherited View to implement special comparison objects filtering logic"""
- return component_templates, components
-
- def _fetch_comparison_objects(self, device_id: int):
- self.device = get_object_or_404(Device, id=device_id)
- component_templates = self.obj_template_model.objects.filter(device_type_id=self.device.device_type.id)
- components = self.obj_model.objects.filter(device_id=device_id)
- self.component_templates, self.components = self.filter_comparison_components(component_templates, components)
- self.comparison_component_templates = [comparison.from_netbox_object(obj) for obj in self.component_templates]
- self.comparison_components = [comparison.from_netbox_object(obj) for obj in self.components]
-
- name_comparison_config = config['name_comparison']
-
- def name_key(obj_name: str) -> str:
- name = obj_name
- if name_comparison_config.get('case-insensitive'):
- name = name.lower()
- if name_comparison_config.get('space-insensitive'):
- name = name.replace(' ', '')
- return name
-
- component_templates_dict = {name_key(obj.name): obj for obj in self.comparison_component_templates}
- components_dict = {name_key(obj.name): obj for obj in self.comparison_components}
-
- self.comparison_table = tuple(
- ComparisonTableRow(
- component_template=component_templates_dict.get(component_name),
- component=components_dict.get(component_name)
- )
- for component_name in human_sorted(set().union(component_templates_dict.keys(), components_dict.keys()))
- )
+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):
- self._fetch_comparison_objects(device_id)
-
- return render(request, "netbox_interface_sync/components_comparison.html", {
- "component_type_name": self.obj_model._meta.verbose_name_plural,
- "comparison_items": self.comparison_table,
- "templates_count": len(self.comparison_component_templates),
- "components_count": len(self.comparison_components),
- "device": self.device,
- })
-
- def post(self, request, device_id):
- components_to_add = make_integer_list(request.POST.getlist("add"))
- components_to_delete = make_integer_list(request.POST.getlist("remove"))
- components_to_sync = make_integer_list(request.POST.getlist("sync"))
- if not any((components_to_add, components_to_delete, components_to_sync)):
- messages.warning(request, "No actions selected")
- return redirect(request.path)
-
- self._fetch_comparison_objects(device_id)
-
- component_ids_to_delete = []
- components_to_bulk_create = []
- synced_count = 0
- for template, component in self.comparison_table:
- if template and (template.id in components_to_add):
- # Add component to the device from the template
- components_to_bulk_create.append(
- self.obj_model(device=self.device, **template.get_fields_for_netbox_component())
- )
- elif component and (component.id in components_to_delete):
- # Delete component from the device
- component_ids_to_delete.append(component.id)
- elif (template and component) and (component.id in components_to_sync):
- # Update component attributes from the template
- synced_count += self.components.filter(id=component.id).update(
- **template.get_fields_for_netbox_component(sync=True)
- )
-
- deleted_count = self.obj_model.objects.filter(id__in=component_ids_to_delete).delete()[0]
- created_count = len(self.obj_model.objects.bulk_create(components_to_bulk_create))
-
- # Generating result message
- component_type_name = self.obj_model._meta.verbose_name_plural
- message = []
- if synced_count > 0:
- message.append(f"synced {synced_count} {component_type_name}")
- if created_count > 0:
- message.append(f"created {created_count} {component_type_name}")
- if deleted_count > 0:
- message.append(f"deleted {deleted_count} {component_type_name}")
- messages.success(request, "; ".join(message).capitalize())
-
- return redirect(request.path)
-
-
-class ConsolePortComparisonView(GenericComparisonView):
- """Comparison of console ports between a device and a device type and beautiful visualization"""
- obj_model = ConsolePort
- obj_template_model = ConsolePortTemplate
-
-
-class ConsoleServerPortComparisonView(GenericComparisonView):
- """Comparison of console server ports between a device and a device type and beautiful visualization"""
- obj_model = ConsoleServerPort
- obj_template_model = ConsoleServerPortTemplate
-
-
-class InterfaceComparisonView(GenericComparisonView):
- """Comparison of interfaces between a device and a device type and beautiful visualization"""
- obj_model = Interface
- obj_template_model = InterfaceTemplate
-
- @staticmethod
- def filter_comparison_components(component_templates: QuerySet, components: QuerySet) -> Tuple[QuerySet, QuerySet]:
- if config["exclude_virtual_interfaces"]:
- components = components.exclude(type__in=VIRTUAL_IFACE_TYPES)
- component_templates = component_templates.exclude(type__in=VIRTUAL_IFACE_TYPES)
- return component_templates, components
-
-
-class PowerPortComparisonView(GenericComparisonView):
- """Comparison of power ports between a device and a device type and beautiful visualization"""
- obj_model = PowerPort
- obj_template_model = PowerPortTemplate
-
-
-class PowerOutletComparisonView(GenericComparisonView):
- """Comparison of power outlets between a device and a device type and beautiful visualization"""
- obj_model = PowerOutlet
- obj_template_model = PowerOutletTemplate
-
- def post(self, request, device_id):
device = get_object_or_404(Device.objects.filter(id=device_id))
+ interfaces = device.vc_interfaces()
+ 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)
- poweroutlets = device.poweroutlets.all()
- poweroutlets_templates = PowerOutletTemplate.objects.filter(device_type=device.device_type)
+ unified_interfaces = [UnifiedInterface(i.id, i.name, i.type, i.get_type_display()) for i in interfaces]
+ unified_interface_templates = [
+ UnifiedInterface(i.id, i.name, i.type, i.get_type_display(), is_template=True) for i in interface_templates]
- # Generating result message
- message = []
- created = 0
- updated = 0
- fixed = 0
+ # 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))
- remove_from_device = filter(
- lambda i: i in poweroutlets.values_list("id", flat=True),
- map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove")))
+ 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
+ }
)
- # Remove selected power outlets from the device and count them
- deleted = PowerOutlet.objects.filter(id__in=remove_from_device).delete()[0]
+ 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 = device.vc_interfaces()
+ if config["exclude_virtual_interfaces"]:
+ interfaces = interfaces.exclude(type__in=["virtual", "lag"])
+ interface_templates = InterfaceTemplate.objects.filter(device_type=device.device_type)
- # Get device power ports to check dependency between power outlets
- device_pp = PowerPort.objects.filter(device_id=device.id)
-
- matching = {}
- mismatch = False
- for i in poweroutlets_templates:
- found = False
- if i.power_port_id is not None:
- ppt = PowerPortTemplate.objects.get(id=i.power_port_id)
- for pp in device_pp:
- if pp.name == ppt.name:
- # Save matching to add the correct power port later
- matching[i.id] = pp.id
- found = True
-
- # If at least one power port is not found in device there is a dependency
- # Better not to sync at all
- if not found:
- mismatch = True
- break
-
- if not mismatch:
+ # Manually validating interfaces and interface templates lists
add_to_device = filter(
- lambda i: i in poweroutlets_templates.values_list("id", flat=True),
- map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add")))
+ 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 component to the device and count them
- add_to_device_component = PowerOutletTemplate.objects.filter(id__in=add_to_device)
+ # Remove selected interfaces from the device and count them
+ interfaces_deleted = Interface.objects.filter(id__in=remove_from_device).delete()[0]
- bulk_create = []
- updated = 0
- keys_to_avoid = ["id"]
+ # 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
+ ]))
- if not config["compare_description"]:
- keys_to_avoid.append("description")
-
- for i in add_to_device_component.values():
- to_create = False
+ # 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, i.get_type_display()) 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, interface.get_type_display())
try:
- # If power outlets already exists, update and do not recreate
- po = device.poweroutlets.get(name=i["name"])
- except PowerOutlet.DoesNotExist:
- po = PowerOutlet()
- po.device = device
- to_create = True
-
- # Copy all fields from template
- for k in i.keys():
- if k not in keys_to_avoid:
- setattr(po, k, i[k])
- po.power_port_id = matching.get(i["id"], None)
-
- if to_create:
- bulk_create.append(po)
- else:
- po.save()
- updated += 1
-
- created = len(PowerOutlet.objects.bulk_create(bulk_create))
-
- # Getting and validating a list of components to rename
- fix_name_components = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), poweroutlets)
-
- # Casting component templates into Unified objects for proper comparison with component for renaming
- unified_component_templates = [
- PowerOutletComparison(i.id, i.name, i.label, i.description, i.type, i.get_type_display(),
- power_port_name=PowerPortTemplate.objects.get(id=i.power_port_id).name
- if i.power_port_id is not None else "",
- feed_leg=i.feed_leg, is_template=True)
- for i in poweroutlets_templates]
-
- # Rename selected power outlets
- fixed = 0
- for component in fix_name_components:
- unified_poweroutlet = PowerOutletComparison(
- component.id, component.name, component.label, component.description, component.type,
- component.get_type_display(),
- power_port_name=PowerPort.objects.get(id=component.power_port_id).name
- if component.power_port_id is not None else "",
- feed_leg=component.feed_leg
- )
- try:
- # Try to extract a component template with the corresponding name
- corresponding_template = unified_component_templates[
- unified_component_templates.index(unified_poweroutlet)
- ]
- component.name = corresponding_template.name
- component.save()
- fixed += 1
+ # 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
- else:
- messages.error(request, "Dependency detected, sync power ports first!")
- if created > 0:
- message.append(f"created {created} power outlets")
- if updated > 0:
- message.append(f"updated {updated} power outlets")
- if deleted > 0:
- message.append(f"deleted {deleted} power outlets")
- if fixed > 0:
- message.append(f"fixed {fixed} power outlets")
+ # 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())
- messages.info(request, "; ".join(message).capitalize())
-
- return redirect(request.path)
-
-
-class RearPortComparisonView(GenericComparisonView):
- """Comparison of rear ports between a device and a device type and beautiful visualization"""
- obj_model = RearPort
- obj_template_model = RearPortTemplate
-
-
-class DeviceBayComparisonView(GenericComparisonView):
- """Comparison of device bays between a device and a device type and beautiful visualization"""
- obj_model = DeviceBay
- obj_template_model = DeviceBayTemplate
-#
-#
-# class FrontPortComparisonView(LoginRequiredMixin, PermissionRequiredMixin, View):
-# """Comparison of front ports between a device and a device type and beautiful visualization"""
-# permission_required = get_permissions_for_object("dcim", "frontport")
-#
-# def get(self, request, device_id):
-#
-# device = get_object_or_404(Device.objects.filter(id=device_id))
-#
-# frontports = device.frontports.all()
-# frontports_templates = FrontPortTemplate.objects.filter(device_type=device.device_type)
-#
-# unified_frontports = [
-# FrontPortComparison(
-# i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.color, i.rear_port_position)
-# for i in frontports]
-# unified_frontports_templates = [
-# FrontPortComparison(
-# i.id, i.name, i.label, i.description, i.type, i.get_type_display(), i.color,
-# i.rear_port_position, is_template=True)
-# for i in frontports_templates]
-#
-# return get_components(request, device, frontports, unified_frontports, unified_frontports_templates)
-#
-# def post(self, request, device_id):
-# form = ComponentComparisonForm(request.POST)
-# if form.is_valid():
-# device = get_object_or_404(Device.objects.filter(id=device_id))
-#
-# frontports = device.frontports.all()
-# frontports_templates = FrontPortTemplate.objects.filter(device_type=device.device_type)
-#
-# # Generating result message
-# message = []
-# created = 0
-# updated = 0
-# fixed = 0
-#
-# remove_from_device = filter(
-# lambda i: i in frontports.values_list("id", flat=True),
-# map(int, filter(lambda x: x.isdigit(), request.POST.getlist("remove_from_device")))
-# )
-#
-# # Remove selected front ports from the device and count them
-# deleted = FrontPort.objects.filter(id__in=remove_from_device).delete()[0]
-#
-# # Get device rear ports to check dependency between front ports
-# device_rp = RearPort.objects.filter(device_id=device.id)
-#
-# matching = {}
-# mismatch = False
-# for i in frontports_templates:
-# found = False
-# if i.rear_port_id is not None:
-# rpt = RearPortTemplate.objects.get(id=i.rear_port_id)
-# for rp in device_rp:
-# if rp.name == rpt.name:
-# # Save matching to add the correct rear port later
-# matching[i.id] = rp.id
-# found = True
-#
-# # If at least one rear port is not found in device there is a dependency
-# # Better not to sync at all
-# if not found:
-# mismatch = True
-# break
-#
-# if not mismatch:
-# add_to_device = filter(
-# lambda i: i in frontports_templates.values_list("id", flat=True),
-# map(int, filter(lambda x: x.isdigit(), request.POST.getlist("add_to_device")))
-# )
-#
-# # Add selected component to the device and count them
-# add_to_device_component = FrontPortTemplate.objects.filter(id__in=add_to_device)
-#
-# bulk_create = []
-# updated = 0
-# keys_to_avoid = ["id"]
-#
-# if not config["compare_description"]:
-# keys_to_avoid.append("description")
-#
-# for i in add_to_device_component.values():
-# to_create = False
-#
-# try:
-# # If front port already exists, update and do not recreate
-# fp = device.frontports.get(name=i["name"])
-# except FrontPort.DoesNotExist:
-# fp = FrontPort()
-# fp.device = device
-# to_create = True
-#
-# # Copy all fields from template
-# for k in i.keys():
-# if k not in keys_to_avoid:
-# setattr(fp, k, i[k])
-# fp.rear_port_id = matching.get(i["id"], None)
-#
-# if to_create:
-# bulk_create.append(fp)
-# else:
-# fp.save()
-# updated += 1
-#
-# created = len(FrontPort.objects.bulk_create(bulk_create))
-#
-# # Getting and validating a list of components to rename
-# fix_name_components = filter(lambda i: str(i.id) in request.POST.getlist("fix_name"), frontports)
-#
-# # Casting component templates into Unified objects for proper comparison with component for renaming
-# unified_frontports_templates = [
-# FrontPortComparison(
-# i.id, i.name, i.label, i.description, i.type, i.get_type_display(),
-# i.color, i.rear_port_position, is_template=True)
-# for i in frontports_templates]
-# # Rename selected front ports
-# fixed = 0
-# for component in fix_name_components:
-# unified_frontport = FrontPortComparison(
-# component.id, component.name, component.label, component.description, component.type,
-# component.get_type_display(), component.color, component.rear_port_position
-# )
-#
-# try:
-# # Try to extract a component template with the corresponding name
-# corresponding_template = unified_frontports_templates[
-# unified_frontports_templates.index(unified_frontport)
-# ]
-# component.name = corresponding_template.name
-# component.save()
-# fixed += 1
-# except ValueError:
-# pass
-# else:
-# messages.error(request, "Dependency detected, sync rear ports first!")
-#
-# if created > 0:
-# message.append(f"created {created} front ports")
-# if updated > 0:
-# message.append(f"updated {updated} front ports")
-# if deleted > 0:
-# message.append(f"deleted {deleted} front ports")
-# if fixed > 0:
-# message.append(f"fixed {fixed} front ports")
-#
-# messages.info(request, "; ".join(message).capitalize())
-#
-# return redirect(request.path)
+ return redirect(request.path)
From 037111f8c2c13538001a35650977493f0e8048f8 Mon Sep 17 00:00:00 2001
From: NetTech2001 <128771411+NetTech2001@users.noreply.github.com>
Date: Fri, 17 May 2024 16:21:03 -0600
Subject: [PATCH 09/10] Updated Screenshots
---
README_ru.md | 44 -------------------------
docs/images/1_device_page.png | Bin 66004 -> 66619 bytes
docs/images/2_interface_comparison.png | Bin 117393 -> 108834 bytes
netbox_interface_sync/__init__.py | 2 +-
setup.py | 6 ++--
5 files changed, 4 insertions(+), 48 deletions(-)
delete mode 100644 README_ru.md
diff --git a/README_ru.md b/README_ru.md
deleted file mode 100644
index 0b48464..0000000
--- a/README_ru.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# netbox-interface-sync
-[English version](./README.md)
-## Обзор
-Плагин для NetBox, позволяющий сравнивать и синхронизировать интерфейсы между устройствами (devices) и типами устройств (device types). Полезен для поиска и исправления несоответствий между интерфейсами. Работа проверена с NetBox версий 4.0
-## Установка
-Если NetBox использует virtualenv, то активируйте его, например, так:
-```
-source /opt/netbox/venv/bin/activate
-```
-Установите плагин из репозитория PyPI:
-```
-pip install netbox-interface-sync
-```
-или клонируйте этот репозиторий, затем перейдите в папку с ним и установите плагин:
-```
-pip install .
-```
-Включите плагин в файле `configuration.py` (обычно он находится в `/opt/netbox/netbox/netbox/`), добавьте его имя в список `PLUGINS`:
-```
-PLUGINS = [
- 'netbox_interface_sync'
-]
-```
-Перезапустите NetBox:
-```
-sudo systemctl restart netbox
-```
-## Использование
-Для того чтобы сравнить интерфейсы, откройте страницу нужного устройства и найдите кнопку "Interface sync" справа сверху:
-
-Отметьте требуемые действия напротив интерфейсов флажками и нажмите "Apply".
-
-### Настройки плагина
-Если вы хотите переопределить значения по умолчанию, настройте переменную `PLUGINS_CONFIG` в вашем файле `configuration.py`:
-```
-PLUGINS_CONFIG = {
- 'netbox_interface_sync': {
- 'exclude_virtual_interfaces': True
- }
-}
-```
-| Настройка | Значение по умолчанию | Описание |
-| --- | --- | --- |
-| exclude_virtual_interfaces | `True` | Не учитывать виртуальные интерфейсы (VLAN, LAG) при сравнении
diff --git a/docs/images/1_device_page.png b/docs/images/1_device_page.png
index cca75bac42d7e3d874b700f68b297b17f580e82c..dd1d67fe4ff65c121741ee7174f590404ce35ad9 100644
GIT binary patch
literal 66619
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