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 %}
-
-
-
- {% copy_content config_field %} - - Download - -
-
{{ header }}
-
- {% 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 @@
{{ instance|meta:"verbose_name"|bettertitle }}
-
+ {% if version|first == "3" %}
{% endif %} @@ -17,16 +17,16 @@
{{ instance|meta:"verbose_name"|bettertitle }}
Device {% badge instance.get_status_display bg_color=instance.get_status_color %}
-
+ {% if version|first == "3" %}
{% endif %}
{% if instance.error %}
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 @@
{{ object|meta:"verbose_name"|bettertitle }}
-
+ {% if version|first == "3" %}
{% endif %} @@ -60,18 +60,18 @@
{{ object|meta:"verbose_name"|bettertitle }}
- + - + - +
Status
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 @@
{{ object|meta:"verbose_name"|bettertitle }}
Devices
-
+ {% if version|first == "3" %}
{% endif %} @@ -103,7 +103,7 @@
{% endfor %}
-
+ {% 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 %}
{% csrf_token %} -
@@ -19,7 +23,7 @@ {% if not object.approved_by %}
{% csrf_token %} -
@@ -27,19 +31,19 @@ {% if object.approved_by %}
{% csrf_token %} -
{% if object.scheduled_by %}
{% csrf_token %} -
{% 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 @@
Job
-
+ {% if version|first == "3" %}
{% endif %} @@ -25,28 +25,28 @@
Job
Name {{ job.user|placeholder }}
-
+ {% if version|first == "3" %}
{% endif %}
Time
-
+ {% 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 @@
-
-
- {% copy_content pre_id %} - - Download - + {% if version|first == "3" %} +
+
+ {% copy_content pre_id %} + + Download + +
+
{{ header }}
-
{{ header }}
-
+ {% else %} +
+ {{ header }} +
+ {% copy_content pre_id %} + + Download + +
+
+ {% endif %} {% if data %} -
{{ data }}
+
+
{{ data }}
+
{% else %} -
No commands
+
None
{% endif %}
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 @@
Job Log
-
+ {% if version|first == "3" %}
{% endif %} @@ -23,5 +23,5 @@
Job Log
{% endfor %}
Time
-
+ {% 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 @@
{{ object|meta:"verbose_name"|bettertitle }}
-
+ {% if version|first == "3" %}
{% endif %} @@ -36,7 +36,7 @@
{{ object|meta:"verbose_name"|bettertitle }}
Platform
-
+ {% 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 @@
{{ object|meta:"verbose_name"|bettertitle }}
-
+ {% if version|first == "3" %}
{% endif %} @@ -26,7 +26,7 @@
{{ object|meta:"verbose_name"|bettertitle }}
Platform Setting
{{ 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)