diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml
index 6a12548..02a5a42 100644
--- a/.github/workflows/commit.yaml
+++ b/.github/workflows/commit.yaml
@@ -24,14 +24,14 @@ jobs:
- name: Run ruff format
run: ruff format .
- name: Run ruff
- run: ruff .
+ run: ruff check .
test:
runs-on: ubuntu-latest
strategy:
max-parallel: 10
matrix:
- netbox_version: ["v3.5.9", "v3.6.9", "v3.7.8"]
+ netbox_version: ["v3.6.9", "v3.7.8", "v4.0.7"]
steps:
- name: Checkout
uses: actions/checkout@v3
diff --git a/README.md b/README.md
index dc1ebe2..d025e33 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![NetBox version](https://img.shields.io/badge/NetBox-3.5|3.6-blue.svg)](https://github.com/netbox-community/netbox)
+[![NetBox version](https://img.shields.io/badge/NetBox-3.6|3.7|4.0-blue.svg)](https://github.com/netbox-community/netbox)
[![Supported Versions](https://img.shields.io/pypi/pyversions/netbox-config-diff.svg)](https://pypi.org/project/netbox-config-diff/)
[![PyPI version](https://badge.fury.io/py/netbox-config-diff.svg)](https://badge.fury.io/py/netbox-config-diff)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -42,9 +42,10 @@ This is possible thanks to the scrapli_cfg. Read [Scrapli](https://github.com/sc
## Compatibility
-| NetBox Version | Plugin Version |
-|----------------|----------------|
-| 3.5, 3.6, 3.7 | =>0.1.0 |
+| NetBox Version | Plugin Version |
+|----------------|------------------|
+| 3.5 | =>0.1.0, <=2.5.0 |
+| 3.6, 3.7, 4.0 | =>0.1.0 |
## Installing
@@ -107,7 +108,7 @@ Read this [doc](https://miaow2.github.io/netbox-config-diff/colliecting-diffs/)
## Video
-My presention about plugin at October NetBox community call (19.10.2023).
+My presention about plugin at October NetBox community call (19.10.2023, plugin version 2.0.0).
[![October NetBox community call](https://img.youtube.com/vi/B4uhtYh278o/0.jpg)](https://youtu.be/B4uhtYh278o?t=425)
diff --git a/development/Dockerfile b/development/Dockerfile
index 528d2e6..f5fc69d 100644
--- a/development/Dockerfile
+++ b/development/Dockerfile
@@ -6,7 +6,7 @@ ENV PYTHONDONTWRITEBYTECODE 1
ARG NETBOX_VERSION
RUN apt-get update \
- && apt-get install -y --no-install-recommends git \
+ && apt-get install -y --no-install-recommends git postgresql-client libpq-dev gcc build-essential \
&& pip install --upgrade pip
# Install NetBox
diff --git a/docs/changelog.md b/docs/changelog.md
index f9ff8fd..14f44e4 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,6 +1,12 @@
# Changelog
+## 2.6.0 (2024-07-14)
+
+* [#62](https://github.com/miaow2/netbox-config-diff/issues/62) Add support for NetBox 4.0
+
+This release drops support for NetBox 3.5.
+
## 2.5.0 (2024-06-30)
* [#67](https://github.com/miaow2/netbox-config-diff/issues/67) Add option `default_desired_privilege_level` to plugins variables (thanks to [@cknost](https://github.com/cknost))
diff --git a/netbox_config_diff/__init__.py b/netbox_config_diff/__init__.py
index cd81cd8..698e976 100644
--- a/netbox_config_diff/__init__.py
+++ b/netbox_config_diff/__init__.py
@@ -1,8 +1,14 @@
-from extras.plugins import PluginConfig
+from netbox.settings import VERSION
+
+if VERSION.startswith("3."):
+ from extras.plugins import PluginConfig
+else:
+ from netbox.plugins import PluginConfig
+
__author__ = "Artem Kotik"
__email__ = "miaow2@yandex.ru"
-__version__ = "2.5.0"
+__version__ = "2.6.0"
class ConfigDiffConfig(PluginConfig):
@@ -14,7 +20,7 @@ class ConfigDiffConfig(PluginConfig):
version = __version__
base_url = "config-diff"
required_settings = ["USERNAME", "PASSWORD"]
- min_version = "3.5.0"
+ min_version = "3.6.0"
default_settings = {
"USER_SECRET_ROLE": "Username",
"PASSWORD_SECRET_ROLE": "Password",
diff --git a/netbox_config_diff/api/serializers.py b/netbox_config_diff/api/serializers.py
index 9b3a12d..43cd638 100644
--- a/netbox_config_diff/api/serializers.py
+++ b/netbox_config_diff/api/serializers.py
@@ -2,16 +2,22 @@
from dcim.models import Device
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
+from netbox.settings import VERSION
from rest_framework import serializers
from rest_framework.serializers import ValidationError
from users.api.nested_serializers import NestedUserSerializer
-from utilities.utils import local_now
from netbox_config_diff.choices import ConfigComplianceStatusChoices, ConfigurationRequestStatusChoices
from netbox_config_diff.constants import ACCEPTABLE_DRIVERS
from netbox_config_diff.models import ConfigCompliance, ConfigurationRequest, PlatformSetting, Substitute
+if VERSION.startswith("3."):
+ from utilities.utils import local_now
+else:
+ from utilities.datetime import local_now
+
+# TODO: after droping support for NetBox 3.x, delete nested serializers and add brief_fields
class ConfigComplianceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name="plugins-api:netbox_config_diff-api:configcompliance-detail")
device = NestedDeviceSerializer()
diff --git a/netbox_config_diff/compliance/base.py b/netbox_config_diff/compliance/base.py
index f4fee83..0dd6ba4 100644
--- a/netbox_config_diff/compliance/base.py
+++ b/netbox_config_diff/compliance/base.py
@@ -11,15 +11,20 @@
from django.db.models import Q
from extras.scripts import MultiObjectVar, ObjectVar, TextVar
from jinja2.exceptions import TemplateError
+from netbox.settings import VERSION
from netutils.config.compliance import diff_network_config
from utilities.exceptions import AbortScript
-from utilities.utils import render_jinja2
from netbox_config_diff.models import ConplianceDeviceDataClass
from .secrets import SecretsMixin
from .utils import PLATFORM_MAPPING, CustomChoiceVar, exclude_lines, get_remediation_commands, get_unified_diff
+if VERSION.startswith("3."):
+ from utilities.utils import render_jinja2
+else:
+ from utilities.jinja2 import render_jinja2
+
class ConfigDiffBase(SecretsMixin):
site = ObjectVar(
diff --git a/netbox_config_diff/compliance/secrets.py b/netbox_config_diff/compliance/secrets.py
index a7df185..eaf7bdb 100644
--- a/netbox_config_diff/compliance/secrets.py
+++ b/netbox_config_diff/compliance/secrets.py
@@ -2,11 +2,16 @@
from typing import TYPE_CHECKING
from dcim.models import Device
-from extras.plugins import get_installed_plugins, get_plugin_config
+from netbox.settings import VERSION
if TYPE_CHECKING:
from netbox_secrets.models import Secret
+if VERSION.startswith("3."):
+ from extras.plugins import get_installed_plugins, get_plugin_config
+else:
+ from netbox.plugins import get_installed_plugins, get_plugin_config
+
class SecretsMixin:
username: str
diff --git a/netbox_config_diff/configurator/base.py b/netbox_config_diff/configurator/base.py
index 537517e..1fa70f7 100644
--- a/netbox_config_diff/configurator/base.py
+++ b/netbox_config_diff/configurator/base.py
@@ -7,11 +7,11 @@
from asgiref.sync import sync_to_async
from dcim.models import Device
from jinja2.exceptions import TemplateError
+from netbox.settings import VERSION
from netutils.config.compliance import diff_network_config
from scrapli import AsyncScrapli
from scrapli_cfg.platform.base.async_platform import AsyncScrapliCfgPlatform
from scrapli_cfg.response import ScrapliCfgResponse
-from utilities.utils import NetBoxFakeRequest
from netbox_config_diff.compliance.secrets import SecretsMixin
from netbox_config_diff.compliance.utils import PLATFORM_MAPPING, get_remediation_commands, get_unified_diff
@@ -22,6 +22,11 @@
from .factory import AsyncScrapliCfg
+if VERSION.startswith("3."):
+ from utilities.utils import NetBoxFakeRequest
+else:
+ from utilities.request import NetBoxFakeRequest
+
class Configurator(SecretsMixin):
def __init__(self, devices: Iterable[Device], request: NetBoxFakeRequest) -> None:
diff --git a/netbox_config_diff/forms/__init__.py b/netbox_config_diff/forms/__init__.py
new file mode 100644
index 0000000..60d8800
--- /dev/null
+++ b/netbox_config_diff/forms/__init__.py
@@ -0,0 +1,23 @@
+from .general import (
+ ConfigComplianceFilterForm,
+ ConfigurationRequestFilterForm,
+ ConfigurationRequestForm,
+ ConfigurationRequestScheduleForm,
+ PlatformSettingBulkEditForm,
+ PlatformSettingFilterForm,
+ PlatformSettingForm,
+ SubstituteFilterForm,
+ SubstituteForm,
+)
+
+__all__ = [
+ "ConfigComplianceFilterForm",
+ "ConfigurationRequestFilterForm",
+ "ConfigurationRequestForm",
+ "ConfigurationRequestScheduleForm",
+ "PlatformSettingBulkEditForm",
+ "PlatformSettingFilterForm",
+ "PlatformSettingForm",
+ "SubstituteFilterForm",
+ "SubstituteForm",
+]
diff --git a/netbox_config_diff/forms/base.py b/netbox_config_diff/forms/base.py
new file mode 100644
index 0000000..cca0e96
--- /dev/null
+++ b/netbox_config_diff/forms/base.py
@@ -0,0 +1,12 @@
+from django.forms import ModelForm
+from netbox.settings import VERSION
+
+if VERSION.startswith("3."):
+ from utilities.forms.mixins import BootstrapMixin
+
+ class CustomForm(BootstrapMixin, ModelForm):
+ pass
+else:
+
+ class CustomForm(ModelForm):
+ pass
diff --git a/netbox_config_diff/forms.py b/netbox_config_diff/forms/general.py
similarity index 96%
rename from netbox_config_diff/forms.py
rename to netbox_config_diff/forms/general.py
index 90a3e7a..4aae6d0 100644
--- a/netbox_config_diff/forms.py
+++ b/netbox_config_diff/forms/general.py
@@ -3,19 +3,25 @@
from django import forms
from django.contrib.auth import get_user_model
from netbox.forms import NetBoxModelBulkEditForm, NetBoxModelFilterSetForm, NetBoxModelForm
+from netbox.settings import VERSION
from utilities.forms.fields import (
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
TagFilterField,
)
-from utilities.forms.mixins import BootstrapMixin
from utilities.forms.widgets import DateTimePicker
-from utilities.utils import local_now
from netbox_config_diff.choices import ConfigComplianceStatusChoices, ConfigurationRequestStatusChoices
from netbox_config_diff.constants import ACCEPTABLE_DRIVERS
from netbox_config_diff.models import ConfigCompliance, ConfigurationRequest, PlatformSetting, Substitute
+from .base import CustomForm
+
+if VERSION.startswith("3."):
+ from utilities.utils import local_now
+else:
+ from utilities.datetime import local_now
+
class ConfigComplianceFilterForm(NetBoxModelFilterSetForm):
model = ConfigCompliance
@@ -157,7 +163,7 @@ class ConfigurationRequestFilterForm(NetBoxModelFilterSetForm):
tag = TagFilterField(model)
-class ConfigurationRequestScheduleForm(BootstrapMixin, forms.ModelForm):
+class ConfigurationRequestScheduleForm(CustomForm):
scheduled = forms.DateTimeField(
widget=DateTimePicker(),
label="Schedule at",
diff --git a/netbox_config_diff/graphql/__init__.py b/netbox_config_diff/graphql/__init__.py
new file mode 100644
index 0000000..b8a6607
--- /dev/null
+++ b/netbox_config_diff/graphql/__init__.py
@@ -0,0 +1,6 @@
+from netbox.settings import VERSION
+
+if VERSION.startswith("3."):
+ from .old.schema import schema # noqa
+else:
+ from .new.schema import schema # noqa
diff --git a/netbox_config_diff/graphql/new/__init__.py b/netbox_config_diff/graphql/new/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/netbox_config_diff/graphql/new/filters.py b/netbox_config_diff/graphql/new/filters.py
new file mode 100644
index 0000000..47976b4
--- /dev/null
+++ b/netbox_config_diff/graphql/new/filters.py
@@ -0,0 +1,34 @@
+import strawberry_django
+from netbox.graphql.filter_mixins import BaseFilterMixin, autotype_decorator
+
+from netbox_config_diff.filtersets import (
+ ConfigComplianceFilterSet,
+ ConfigurationRequestFilterSet,
+ PlatformSettingFilterSet,
+ SubstituteFilterSet,
+)
+from netbox_config_diff.models import ConfigCompliance, ConfigurationRequest, PlatformSetting, Substitute
+
+
+@strawberry_django.filter(ConfigCompliance, lookups=True)
+@autotype_decorator(ConfigComplianceFilterSet)
+class ConfigComplianceFilter(BaseFilterMixin):
+ pass
+
+
+@strawberry_django.filter(ConfigurationRequest, lookups=True)
+@autotype_decorator(ConfigurationRequestFilterSet)
+class ConfigurationRequestFilter(BaseFilterMixin):
+ pass
+
+
+@strawberry_django.filter(PlatformSetting, lookups=True)
+@autotype_decorator(PlatformSettingFilterSet)
+class PlatformSettingFilter(BaseFilterMixin):
+ pass
+
+
+@strawberry_django.filter(Substitute, lookups=True)
+@autotype_decorator(SubstituteFilterSet)
+class SubstituteFilter(BaseFilterMixin):
+ pass
diff --git a/netbox_config_diff/graphql/new/schema.py b/netbox_config_diff/graphql/new/schema.py
new file mode 100644
index 0000000..b0ff6b2
--- /dev/null
+++ b/netbox_config_diff/graphql/new/schema.py
@@ -0,0 +1,36 @@
+import strawberry
+import strawberry_django
+
+from netbox_config_diff.models import ConfigCompliance, ConfigurationRequest, PlatformSetting, Substitute
+
+from .types import ConfigComplianceType, ConfigurationRequestType, PlatformSettingType, SubstituteType
+
+
+@strawberry.type
+class NetBoxConfigDiffQuery:
+ @strawberry.field
+ def config_compliance(self, id: int) -> ConfigComplianceType:
+ return ConfigCompliance.objects.get(pk=id)
+
+ config_compliance_list: list[ConfigComplianceType] = strawberry_django.field()
+
+ @strawberry.field
+ def configuration_request(self, id: int) -> ConfigurationRequestType:
+ return ConfigurationRequest.objects.get(pk=id)
+
+ configuration_request_list: list[ConfigurationRequestType] = strawberry_django.field()
+
+ @strawberry.field
+ def platform_setting(self, id: int) -> PlatformSettingType:
+ return PlatformSetting.objects.get(pk=id)
+
+ platform_setting_list: list[PlatformSettingType] = strawberry_django.field()
+
+ @strawberry.field
+ def substitute(self, id: int) -> SubstituteType:
+ return Substitute.objects.get(pk=id)
+
+ substitute_list: list[SubstituteType] = strawberry_django.field()
+
+
+schema = [NetBoxConfigDiffQuery]
diff --git a/netbox_config_diff/graphql/new/types.py b/netbox_config_diff/graphql/new/types.py
new file mode 100644
index 0000000..3493fc0
--- /dev/null
+++ b/netbox_config_diff/graphql/new/types.py
@@ -0,0 +1,55 @@
+from typing import Annotated
+
+import strawberry
+import strawberry_django
+from dcim.graphql.types import DeviceType, PlatformType
+from netbox.graphql.types import NetBoxObjectType, ObjectType
+from users.graphql.types import UserType
+
+from netbox_config_diff.models import ConfigCompliance, ConfigurationRequest, PlatformSetting, Substitute
+
+from .filters import ConfigComplianceFilter, ConfigurationRequestFilter, PlatformSettingFilter, SubstituteFilter
+
+
+@strawberry_django.type(ConfigCompliance, fields="__all__", filters=ConfigComplianceFilter)
+class ConfigComplianceType(ObjectType):
+ device: Annotated["DeviceType", strawberry.lazy("dcim.graphql.types")]
+ status: str
+ diff: str
+ error: str
+ actual_config: str
+ rendered_config: str
+ missing: str
+ extra: str
+ patch: str
+
+
+@strawberry_django.type(ConfigurationRequest, fields="__all__", filters=ConfigurationRequestFilter)
+class ConfigurationRequestType(NetBoxObjectType):
+ created_by: Annotated["UserType", strawberry.lazy("users.graphql.types")] | None
+ approved_by: Annotated["UserType", strawberry.lazy("users.graphql.types")] | None
+ scheduled_by: Annotated["UserType", strawberry.lazy("users.graphql.types")] | None
+ status: str
+ devices: list[Annotated["DeviceType", strawberry.lazy("dcim.graphql.types")]]
+ description: str
+ comments: str
+ scheduled: str
+ started: str
+ completed: str
+
+
+@strawberry_django.type(PlatformSetting, fields="__all__", filters=PlatformSettingFilter)
+class PlatformSettingType(NetBoxObjectType):
+ platform: Annotated["PlatformType", strawberry.lazy("dcim.graphql.types")]
+ description: str
+ driver: str
+ command: str
+ exclude_regex: str
+
+
+@strawberry_django.type(Substitute, fields="__all__", filters=SubstituteFilter)
+class SubstituteType(NetBoxObjectType):
+ platform_setting: Annotated["PlatformSettingType", strawberry.lazy("netbox_config_diff.graphql.new.types")]
+ name: str
+ description: str
+ regexp: str
diff --git a/netbox_config_diff/graphql/old/__init__.py b/netbox_config_diff/graphql/old/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/netbox_config_diff/graphql.py b/netbox_config_diff/graphql/old/schema.py
similarity index 100%
rename from netbox_config_diff/graphql.py
rename to netbox_config_diff/graphql/old/schema.py
diff --git a/netbox_config_diff/jobs.py b/netbox_config_diff/jobs.py
index 873414d..9dd481a 100644
--- a/netbox_config_diff/jobs.py
+++ b/netbox_config_diff/jobs.py
@@ -3,12 +3,18 @@
from core.choices import JobStatusChoices
from core.models import Job
-from utilities.utils import NetBoxFakeRequest
+from netbox.settings import VERSION
from netbox_config_diff.choices import ConfigurationRequestStatusChoices
from netbox_config_diff.configurator.base import Configurator
from netbox_config_diff.models import ConfigurationRequest
+if VERSION.startswith("3."):
+ from utilities.utils import NetBoxFakeRequest
+else:
+ from utilities.request import NetBoxFakeRequest
+
+
logger = logging.getLogger(__name__)
diff --git a/netbox_config_diff/models/models.py b/netbox_config_diff/models/models.py
index 8fd7493..8f886e5 100644
--- a/netbox_config_diff/models/models.py
+++ b/netbox_config_diff/models/models.py
@@ -11,14 +11,19 @@
from netbox.constants import RQ_QUEUE_DEFAULT
from netbox.models import NetBoxModel, PrimaryModel
from netbox.models.features import ChangeLoggingMixin, JobsMixin
+from netbox.settings import VERSION
from rq.exceptions import InvalidJobOperation
from utilities.querysets import RestrictedQuerySet
-from utilities.utils import copy_safe_request
from netbox_config_diff.choices import ConfigComplianceStatusChoices, ConfigurationRequestStatusChoices
from .base import AbsoluteURLMixin
+if VERSION.startswith("3."):
+ from utilities.utils import copy_safe_request
+else:
+ from utilities.request import copy_safe_request
+
class ConfigCompliance(AbsoluteURLMixin, ChangeLoggingMixin, models.Model):
device = models.OneToOneField(
diff --git a/netbox_config_diff/navigation.py b/netbox_config_diff/navigation.py
index c52f406..fece289 100644
--- a/netbox_config_diff/navigation.py
+++ b/netbox_config_diff/navigation.py
@@ -1,5 +1,12 @@
-from extras.plugins import PluginMenu, PluginMenuButton, PluginMenuItem
-from utilities.choices import ButtonColorChoices
+from netbox.settings import VERSION
+
+if VERSION.startswith("3."):
+ from extras.plugins import PluginMenu, PluginMenuButton, PluginMenuItem
+ from utilities.choices import ButtonColorChoices
+else:
+ # TODO: after droping support for NetBox 3.x, delete ButtonColorChoices
+ from netbox.choices import ButtonColorChoices
+ from netbox.plugins import PluginMenu, PluginMenuButton, PluginMenuItem
def get_add_button(model: str) -> PluginMenuButton:
diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html
index 874799a..92981c0 100644
--- a/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html
+++ b/netbox_config_diff/templates/netbox_config_diff/configcompliance/config.html
@@ -5,22 +5,7 @@
{% block content %}
-
-
- {% if config %}
-
{{ config }}
- {% else %}
-
No configuration
- {% endif %}
-
+ {% include 'netbox_config_diff/inc/commands_card.html' with data=config header=header pre_id=config_field %}
{% endblock %}
diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html
index e845774..3b576df 100644
--- a/netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html
+++ b/netbox_config_diff/templates/netbox_config_diff/configcompliance/data.html
@@ -6,7 +6,7 @@
-
+ {% if version|first == "3" %}
{% endif %}
Device |
@@ -17,16 +17,16 @@
{% badge instance.get_status_display bg_color=instance.get_status_color %} |
-
+ {% if version|first == "3" %}
{% endif %}
{% if instance.error %}
-
+ {% if version|first == "3" %}
{% endif %}
{{ instance.error }}
-
+ {% if version|first == "3" %}
{% endif %}
{% endif %}
diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html
index 5de7f9e..fb603b0 100644
--- a/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html
+++ b/netbox_config_diff/templates/netbox_config_diff/configcompliance/missing_extra.html
@@ -1,6 +1,6 @@
{% extends "netbox_config_diff/configcompliance.html" %}
-{% block title %}{{ object }} - Missing/Extra{% endblock %}
+{% block title %}{{ object }} - {{ header }}{% endblock %}
{% block content %}
diff --git a/netbox_config_diff/templates/netbox_config_diff/configcompliance/patch.html b/netbox_config_diff/templates/netbox_config_diff/configcompliance/patch.html
deleted file mode 100644
index ac77fe7..0000000
--- a/netbox_config_diff/templates/netbox_config_diff/configcompliance/patch.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "netbox_config_diff/configcompliance.html" %}
-
-{% block title %}{{ object }} - Patch commands{% endblock %}
-
-{% block content %}
-
-
- {% include 'netbox_config_diff/inc/commands_card.html' with data=object.patch header='Patch commands' pre_id='patch' %}
-
-
-{% endblock %}
diff --git a/netbox_config_diff/templates/netbox_config_diff/configurationrequest.html b/netbox_config_diff/templates/netbox_config_diff/configurationrequest.html
index 8798f84..afb276a 100644
--- a/netbox_config_diff/templates/netbox_config_diff/configurationrequest.html
+++ b/netbox_config_diff/templates/netbox_config_diff/configurationrequest.html
@@ -6,7 +6,7 @@
-
+ {% if version|first == "3" %}
{% endif %}
Status |
@@ -60,18 +60,18 @@
Scheduled |
- {{ object.scheduled|annotated_date|placeholder }} |
+ {{ object.scheduled|placeholder }} |
Started |
- {{ object.started|annotated_date|placeholder }} |
+ {{ object.started|placeholder }} |
Completed |
- {{ object.completed|annotated_date|placeholder }} |
+ {{ object.completed|placeholder }} |
-
+ {% if version|first == "3" %}
{% endif %}
{% include 'inc/panels/comments.html' %}
{% include "inc/panels/tags.html" %}
@@ -82,7 +82,7 @@
-
+ {% if version|first == "3" %}
+ {% if version|first == "3" %}
{% endif %}
diff --git a/netbox_config_diff/templates/netbox_config_diff/configurationrequest/base.html b/netbox_config_diff/templates/netbox_config_diff/configurationrequest/base.html
index 11e2303..55c9110 100644
--- a/netbox_config_diff/templates/netbox_config_diff/configurationrequest/base.html
+++ b/netbox_config_diff/templates/netbox_config_diff/configurationrequest/base.html
@@ -3,15 +3,19 @@
{% load perms %}
{% block controls %}
+ {% if version|first == "3" %}
+ {% else %}
+
+ {% endif %}
{% if perms.extras.add_bookmark and object.bookmarks %}
{% bookmark_button object %}
{% endif %}
{% if not object.finished %}
@@ -19,7 +23,7 @@
{% if not object.approved_by %}
@@ -27,19 +31,19 @@
{% if object.approved_by %}
{% if object.scheduled_by %}
{% else %}
-
+
Schedule
{% endif %}
@@ -52,6 +56,11 @@
{% if request.user|can_delete:object %}
{% delete_button object %}
{% endif %}
+
+ {% if version|first == "3" %}
+
+ {% else %}
+ {% endif %}
{% endblock controls %}
diff --git a/netbox_config_diff/templates/netbox_config_diff/configurationrequest/diffs.html b/netbox_config_diff/templates/netbox_config_diff/configurationrequest/diffs.html
index 0c89ab1..db63d30 100644
--- a/netbox_config_diff/templates/netbox_config_diff/configurationrequest/diffs.html
+++ b/netbox_config_diff/templates/netbox_config_diff/configurationrequest/diffs.html
@@ -8,7 +8,7 @@
-
+ {% if version|first == "3" %}
{% endif %}
Name |
@@ -25,28 +25,28 @@
{{ job.user|placeholder }} |
-
+ {% if version|first == "3" %}
{% endif %}
-
+ {% if version|first == "3" %}
{% endif %}
Created |
- {{ job.created|annotated_date }} |
+ {{ job.created }} |
Started |
- {{ job.started|annotated_date|placeholder }} |
+ {{ job.started|placeholder }} |
Completed |
- {{ job.completed|annotated_date|placeholder }} |
+ {{ job.completed|placeholder }} |
-
+ {% if version|first == "3" %}
{% endif %}
diff --git a/netbox_config_diff/templates/netbox_config_diff/inc/commands_card.html b/netbox_config_diff/templates/netbox_config_diff/inc/commands_card.html
index dae39c8..a961798 100644
--- a/netbox_config_diff/templates/netbox_config_diff/inc/commands_card.html
+++ b/netbox_config_diff/templates/netbox_config_diff/inc/commands_card.html
@@ -1,16 +1,30 @@
-
diff --git a/netbox_config_diff/templates/netbox_config_diff/inc/job_log.html b/netbox_config_diff/templates/netbox_config_diff/inc/job_log.html
index 4e0c508..3018341 100644
--- a/netbox_config_diff/templates/netbox_config_diff/inc/job_log.html
+++ b/netbox_config_diff/templates/netbox_config_diff/inc/job_log.html
@@ -2,7 +2,7 @@
-
+ {% if version|first == "3" %}
{% endif %}
Time |
@@ -23,5 +23,5 @@
{% endfor %}
-
+ {% if version|first == "3" %}
{% endif %}
diff --git a/netbox_config_diff/templates/netbox_config_diff/platformsetting.html b/netbox_config_diff/templates/netbox_config_diff/platformsetting.html
index 4bdf7f5..a277516 100644
--- a/netbox_config_diff/templates/netbox_config_diff/platformsetting.html
+++ b/netbox_config_diff/templates/netbox_config_diff/platformsetting.html
@@ -7,7 +7,7 @@
-
+ {% if version|first == "3" %}
{% endif %}
Platform |
@@ -36,7 +36,7 @@
-
+ {% if version|first == "3" %}
{% endif %}
{% include "inc/panels/custom_fields.html" %}
diff --git a/netbox_config_diff/templates/netbox_config_diff/substitute.html b/netbox_config_diff/templates/netbox_config_diff/substitute.html
index 51b940c..9de41f6 100644
--- a/netbox_config_diff/templates/netbox_config_diff/substitute.html
+++ b/netbox_config_diff/templates/netbox_config_diff/substitute.html
@@ -7,7 +7,7 @@
-
+ {% if version|first == "3" %}
{% endif %}
Platform Setting |
@@ -26,7 +26,7 @@
{{ object.regexp }} |
-
+ {% if version|first == "3" %}
{% endif %}
{% include "inc/panels/custom_fields.html" %}
diff --git a/netbox_config_diff/views/base.py b/netbox_config_diff/views/base.py
index b01a7b8..0d9cc04 100644
--- a/netbox_config_diff/views/base.py
+++ b/netbox_config_diff/views/base.py
@@ -1,6 +1,7 @@
from django.http import HttpResponse
from django.shortcuts import render
from django.urls import reverse
+from netbox.settings import VERSION
from netbox.views.generic import ObjectDeleteView, ObjectEditView, ObjectView
@@ -15,24 +16,34 @@ def default_return_url(self) -> str:
return f"plugins:netbox_config_diff:{self.queryset.model._meta.model_name}_list"
-class BaseExportView(ObjectView):
- def export_parts(self, name, lines, suffix):
+class BaseConfigComplianceConfigView(ObjectView):
+ config_field = None
+ template_header = None
+
+ def export_parts(self, name: str, lines: str, suffix: str | None = None) -> HttpResponse:
response = HttpResponse(lines, content_type="text")
- filename = f"{name}_{suffix}.txt"
+ filename = f"{name}_{suffix if suffix else self.config_field}.txt"
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
-
-class BaseConfigComplianceConfigView(BaseExportView):
- config_field = None
- template_header = None
-
def get(self, request, **kwargs):
instance = self.get_object(**kwargs)
context = self.get_extra_context(request, instance)
- if request.GET.get("export"):
- return self.export_parts(instance.device.name, context["config"], self.config_field)
+ if request.GET.get("export_rendered_config"):
+ return self.export_parts(instance.device.name, context["config"])
+
+ if request.GET.get("export_actual_config"):
+ return self.export_parts(instance.device.name, context["config"])
+
+ if request.GET.get("export_missing"):
+ return self.export_parts(instance.device.name, instance.missing, "missing")
+
+ if request.GET.get("export_extra"):
+ return self.export_parts(instance.device.name, instance.extra, "extra")
+
+ if request.GET.get("export_patch"):
+ return self.export_parts(instance.device.name, context["config"])
return render(
request,
@@ -49,4 +60,5 @@ def get_extra_context(self, request, instance):
"header": self.template_header,
"config": getattr(instance, self.config_field),
"config_field": self.config_field,
+ "version": VERSION,
}
diff --git a/netbox_config_diff/views/compliance.py b/netbox_config_diff/views/compliance.py
index a5b0d5b..aa88b0b 100644
--- a/netbox_config_diff/views/compliance.py
+++ b/netbox_config_diff/views/compliance.py
@@ -1,6 +1,7 @@
from dcim.models import Device
from django.shortcuts import redirect, render
from django.utils.translation import gettext as _
+from netbox.settings import VERSION
from netbox.views import generic
from utilities.views import ViewTab, register_model_view
@@ -14,7 +15,7 @@
from netbox_config_diff.models import ConfigCompliance, PlatformSetting
from netbox_config_diff.tables import ConfigComplianceTable, PlatformSettingTable
-from .base import BaseConfigComplianceConfigView, BaseExportView, BaseObjectDeleteView, BaseObjectEditView
+from .base import BaseConfigComplianceConfigView, BaseObjectDeleteView, BaseObjectEditView
@register_model_view(ConfigCompliance)
@@ -27,6 +28,7 @@ def get_extra_context(self, request, instance):
return {
"instance": instance,
"base_template": self.base_template,
+ "version": VERSION,
}
@@ -54,62 +56,29 @@ class ConfigComplianceActualConfigView(BaseConfigComplianceConfigView):
)
-@register_model_view(ConfigCompliance, "missing-extra")
-class ConfigComplianceMissingExtraConfigView(BaseExportView):
+@register_model_view(ConfigCompliance, "patch")
+class ConfigCompliancePatchView(BaseConfigComplianceConfigView):
queryset = ConfigCompliance.objects.all()
- template_name = "netbox_config_diff/configcompliance/missing_extra.html"
+ template_name = "netbox_config_diff/configcompliance/config.html"
+ config_field = "patch"
+ template_header = "Patch"
tab = ViewTab(
- label=_("Missing/Extra"),
- weight=520,
+ label=_(template_header),
+ weight=515,
)
- def get(self, request, **kwargs):
- instance = self.get_object(**kwargs)
- context = self.get_extra_context(request, instance)
-
- if request.GET.get("export_missing"):
- return self.export_parts(instance.device.name, instance.missing, "missing")
-
- if request.GET.get("export_extra"):
- return self.export_parts(instance.device.name, instance.extra, "extra")
- return render(
- request,
- self.get_template_name(),
- {
- "object": instance,
- "tab": self.tab,
- **context,
- },
- )
-
-
-@register_model_view(ConfigCompliance, "patch")
-class ConfigCompliancePatchView(BaseExportView):
+@register_model_view(ConfigCompliance, "missing-extra")
+class ConfigComplianceMissingExtraConfigView(BaseConfigComplianceConfigView):
queryset = ConfigCompliance.objects.all()
- template_name = "netbox_config_diff/configcompliance/patch.html"
+ template_name = "netbox_config_diff/configcompliance/missing_extra.html"
+ config_field = "missing"
+ template_header = "Missing/Extra"
tab = ViewTab(
- label=_("Patch"),
- weight=515,
+ label=_(template_header),
+ weight=520,
)
- def get(self, request, **kwargs):
- instance = self.get_object(**kwargs)
- context = self.get_extra_context(request, instance)
-
- if request.GET.get("export_patch"):
- return self.export_parts(instance.device.name, instance.patch, "patch")
-
- return render(
- request,
- self.get_template_name(),
- {
- "object": instance,
- "tab": self.tab,
- **context,
- },
- )
-
@register_model_view(Device, "config_compliance", "config-compliance")
class ConfigComplianceDeviceView(generic.ObjectView):
@@ -164,6 +133,11 @@ class ConfigComplianceBulkDeleteView(generic.BulkDeleteView):
class PlatformSettingView(generic.ObjectView):
queryset = PlatformSetting.objects.all()
+ def get_extra_context(self, request, instance):
+ return {
+ "version": VERSION,
+ }
+
class PlatformSettingListView(generic.ObjectListView):
queryset = PlatformSetting.objects.prefetch_related("platform", "tags")
diff --git a/netbox_config_diff/views/configuration.py b/netbox_config_diff/views/configuration.py
index f4beefb..c599a1f 100644
--- a/netbox_config_diff/views/configuration.py
+++ b/netbox_config_diff/views/configuration.py
@@ -5,17 +5,16 @@
from core.models import Job
from core.tables import JobTable
from django.contrib import messages
-from django.contrib.auth.models import User
from django.db.models import Q
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
from netbox.constants import RQ_QUEUE_DEFAULT
+from netbox.settings import VERSION
from netbox.views import generic
from netbox.views.generic.base import BaseObjectView
from rq.exceptions import InvalidJobOperation
from utilities.forms import restrict_form_fields
from utilities.rqworker import get_workers_for_queue
-from utilities.utils import normalize_querydict
from utilities.views import ViewTab, register_model_view
from netbox_config_diff.choices import ConfigurationRequestStatusChoices
@@ -32,6 +31,13 @@
from .base import BaseObjectDeleteView, BaseObjectEditView
+if VERSION.startswith("3."):
+ from django.contrib.auth.models import User
+ from utilities.utils import normalize_querydict
+else:
+ from users.models import User
+ from utilities.querydict import normalize_querydict
+
@register_model_view(ConfigurationRequest)
class ConfigurationRequestView(generic.ObjectView):
@@ -44,6 +50,7 @@ def get_extra_context(self, request, instance):
return {
"job": job,
+ "version": VERSION,
}
@@ -306,13 +313,17 @@ class JobListView(generic.ObjectListView):
filterset = JobFilterSet
filterset_form = JobFilterForm
table = JobTable
- actions = ("export", "delete", "bulk_delete")
@register_model_view(Substitute)
class SubstituteView(generic.ObjectView):
queryset = Substitute.objects.all()
+ def get_extra_context(self, request, instance):
+ return {
+ "version": VERSION,
+ }
+
class SubstituteListView(generic.ObjectListView):
queryset = Substitute.objects.prefetch_related("platform_setting", "tags")
diff --git a/pyproject.toml b/pyproject.toml
index 42a1404..93dc0b3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,6 +24,7 @@ classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
]
license = { file = "LICENSE" }
@@ -53,11 +54,13 @@ optional-dependencies.test = { file = ["requirements/test.txt"] }
[tool.ruff]
exclude = ["migrations", "__pycache__"]
-select = ["C", "E", "F", "I"]
-ignore = ["C901"]
line-length = 120
target-version = "py310"
+[tool.ruff.lint]
+select = ["C", "E", "F", "I"]
+ignore = ["C901"]
+
[tool.pytest.ini_options]
addopts = "-p no:warnings -vv --no-migrations"
DJANGO_SETTINGS_MODULE = "netbox.settings"
diff --git a/requirements/base.txt b/requirements/base.txt
index 1a27887..64e564a 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -1,5 +1,5 @@
hier-config==2.2.3
-netutils==1.5.0
+netutils==1.9.0
scrapli[asyncssh]==2024.01.30
scrapli-cfg==2024.01.30
scrapli-community==2024.01.30
diff --git a/requirements/dev.txt b/requirements/dev.txt
index e06651c..264e6f2 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -1 +1 @@
-ruff==0.1.2
+ruff==0.5.1
diff --git a/requirements/docs.txt b/requirements/docs.txt
index f968d4c..61bcb40 100644
--- a/requirements/docs.txt
+++ b/requirements/docs.txt
@@ -1,7 +1,7 @@
-mkdocs==1.4.3
-mkdocs-autorefs==0.4.1
-mkdocs-include-markdown-plugin==4.0.4
-mkdocs-material==9.1.18
-mkdocs-material-extensions==1.1.1
-mkdocstrings==0.22.0
+mkdocs==1.6.0
+mkdocs-autorefs==1.0.1
+mkdocs-include-markdown-plugin==6.2.1
+mkdocs-material==9.5.28
+mkdocs-material-extensions==1.3.1
+mkdocstrings==0.25.1
mkdocstrings-python-legacy==0.2.3
diff --git a/requirements/publish.txt b/requirements/publish.txt
index 9d8b967..ab46f7b 100644
--- a/requirements/publish.txt
+++ b/requirements/publish.txt
@@ -1,2 +1,2 @@
-build==0.10.0
-twine==4.0.2
\ No newline at end of file
+build==1.2.1
+twine==5.1.1
diff --git a/requirements/test.txt b/requirements/test.txt
index b8209bb..8f1c5ad 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -1,3 +1,3 @@
factory_boy==3.3.0
-pytest==7.4.0
-pytest-django==4.5.2
+pytest==8.2.2
+pytest-django==4.8.0
diff --git a/tests/factories.py b/tests/factories.py
index 408efbb..fc19491 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -69,7 +69,7 @@ class DeviceFactory(DjangoModelFactory):
name = factory.Sequence(lambda n: f"device-{n}")
site = factory.SubFactory(SiteFactory)
device_type = factory.SubFactory(DeviceTypeFactory)
- device_role = factory.SubFactory(DeviceRoleFactory)
+ role = factory.SubFactory(DeviceRoleFactory)
platform = factory.SubFactory(PlatformFactory)
primary_ip4 = factory.SubFactory(IPAddressFactory)
config_template = factory.SubFactory(ConfigTemplateFactory)