diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index 0f0c142..4731536 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -21,8 +21,8 @@ jobs: python -m pip install --upgrade pip python -m pip install --upgrade setuptools wheel python -m pip install -r requirements/dev.txt - - name: Run black - run: black --check --diff --color . + - name: Run ruff format + run: ruff format . - name: Run ruff run: ruff . diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index abcf5ca..68ed49a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,13 +5,10 @@ repos: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/psf/black - rev: 23.1.0 - hooks: - - id: black - args: [--check, --diff, --color, .] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.280 + rev: v0.1.2 hooks: - id: ruff args: [netbox_config_diff] + - id: ruff-format + exclude: migrations diff --git a/README.md b/README.md index e26e271..2955f8b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ [![NetBox version](https://img.shields.io/badge/NetBox-3.5|3.6-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) -[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![CI](https://github.com/miaow2/netbox-config-diff/actions/workflows/commit.yaml/badge.svg?branch=develop)](https://github.com/miaow2/netbox-config-diff/actions) @@ -78,6 +77,7 @@ PLUGINS_CONFIG = { "netbox_config_diff": { "USERNAME": "foo", "PASSWORD": "bar", + "AUTH_SECONDARY": "foobar", # define here password for accessing Privileged EXEC mode, this variable is optional }, } ``` @@ -120,6 +120,14 @@ No diff ![Screenshot of the compliance ok](docs/media/screenshots/compliance-ok.png) +Configuration request + +![Screenshot of the CR](docs/media/screenshots/cr-created.png) + +Completed Configuration request + +![Screenshot of the completed CR](docs/media/screenshots/cr-completed.png) + ## Credits Based on the NetBox plugin tutorial: diff --git a/docs/changelog.md b/docs/changelog.md index 4204b52..1766866 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,17 @@ # Changelog +## 2.1.0 (2023-10-26) + +* [#35](https://github.com/miaow2/netbox-config-diff/issues/35) Add ability to define password for accessing priviliged exec mode +* [#37](https://github.com/miaow2/netbox-config-diff/issues/37) Add `DeviceRole` field to `CollectDiffScript` +* [#38](https://github.com/miaow2/netbox-config-diff/issues/38) Remove config template filter for devices filed in forms +* [#39](https://github.com/miaow2/netbox-config-diff/issues/39) Add `Status` field to `CollectDiffScript` +* [#43](https://github.com/miaow2/netbox-config-diff/issues/43) `ConfigDiffScript` does not create empty changelog entries + +## 2.0.1 (2023-10-22) + +* [#33](https://github.com/miaow2/netbox-config-diff/issues/33) Fix failing migrations on fresh database install + ## 2.0.0 (2023-10-18) * [#25](https://github.com/miaow2/netbox-config-diff/issues/25) Add configuration management diff --git a/docs/media/screenshots/cr-completed.png b/docs/media/screenshots/cr-completed.png new file mode 100644 index 0000000..368a75c Binary files /dev/null and b/docs/media/screenshots/cr-completed.png differ diff --git a/docs/media/screenshots/cr-created.png b/docs/media/screenshots/cr-created.png new file mode 100644 index 0000000..5733189 Binary files /dev/null and b/docs/media/screenshots/cr-created.png differ diff --git a/docs/secrets.md b/docs/secrets.md index e15ebc6..978f0f7 100644 --- a/docs/secrets.md +++ b/docs/secrets.md @@ -4,7 +4,8 @@ You can store credentials for devices authentification in NetBox secrets [plugin Read NetBox secrets docs for more info. -In plugin variables define secrets roles for username (`USER_SECRET_ROLE`) and password (`PASSWORD_SECRET_ROLE`). +In plugin variables define secrets roles for username (`USER_SECRET_ROLE`), password (`PASSWORD_SECRET_ROLE`) and + password (`SECOND_AUTH_SECRET_ROLE`) for Privileged EXEC mode. Default values for this variables are: @@ -13,6 +14,7 @@ PLUGINS_CONFIG = { "netbox_config_diff": { "USER_SECRET_ROLE": "Username", "PASSWORD_SECRET_ROLE": "Password", + "SECOND_AUTH_SECRET_ROLE": "Second Auth", }, } ``` diff --git a/netbox_config_diff/__init__.py b/netbox_config_diff/__init__.py index 22a4b4b..3cd5c9e 100644 --- a/netbox_config_diff/__init__.py +++ b/netbox_config_diff/__init__.py @@ -2,7 +2,7 @@ __author__ = "Artem Kotik" __email__ = "miaow2@yandex.ru" -__version__ = "2.0.1" +__version__ = "2.1.0" class ConfigDiffConfig(PluginConfig): @@ -18,6 +18,7 @@ class ConfigDiffConfig(PluginConfig): default_settings = { "USER_SECRET_ROLE": "Username", "PASSWORD_SECRET_ROLE": "Password", + "SECOND_AUTH_SECRET_ROLE": "Second Auth", } diff --git a/netbox_config_diff/compliance/base.py b/netbox_config_diff/compliance/base.py index 0d60752..2e2c3f8 100644 --- a/netbox_config_diff/compliance/base.py +++ b/netbox_config_diff/compliance/base.py @@ -14,7 +14,8 @@ from netutils.config.compliance import diff_network_config from utilities.exceptions import AbortScript -from .models import DeviceDataClass +from netbox_config_diff.models import ConplianceDeviceDataClass + from .secrets import SecretsMixin from .utils import PLATFORM_MAPPING, CustomChoiceVar, exclude_lines, get_unified_diff @@ -106,12 +107,12 @@ def validate_data(self, data: dict) -> Iterable[Device]: self.log_info(f"Working with device(s): {', '.join(d.name for d in devices)}") return devices - def update_in_db(self, devices: list[DeviceDataClass]) -> None: + def update_in_db(self, devices: list[ConplianceDeviceDataClass]) -> None: for device in devices: self.log_results(device) device.send_to_db() - def log_results(self, device: DeviceDataClass) -> None: + def log_results(self, device: ConplianceDeviceDataClass) -> None: if device.error: self.log_failure(f"{device.name} errored") elif device.diff: @@ -119,11 +120,11 @@ def log_results(self, device: DeviceDataClass) -> None: else: self.log_success(f"{device.name} no diff") - def get_devices_with_rendered_configs(self, devices: Iterable[Device]) -> Iterator[DeviceDataClass]: + def get_devices_with_rendered_configs(self, devices: Iterable[Device]) -> Iterator[ConplianceDeviceDataClass]: self.check_netbox_secrets() self.substitutes = {} for device in devices: - username, password = self.get_credentials(device) + username, password, auth_secondary = self.get_credentials(device) rendered_config = None error = None context_data = device.get_config_context() @@ -142,7 +143,7 @@ def get_devices_with_rendered_configs(self, devices: Iterable[Device]) -> Iterat if substitutes := device.platform.platform_setting.substitutes.all(): self.substitutes[platform] = [s.regexp for s in substitutes] - yield DeviceDataClass( + yield ConplianceDeviceDataClass( pk=device.pk, name=device.name, mgmt_ip=str(device.primary_ip.address.ip), @@ -151,11 +152,12 @@ def get_devices_with_rendered_configs(self, devices: Iterable[Device]) -> Iterat exclude_regex=device.platform.platform_setting.exclude_regex, username=username, password=password, + auth_secondary=auth_secondary, rendered_config=rendered_config, error=error, ) - def get_config_from_datasource(self, devices: list[DeviceDataClass]) -> None: + def get_config_from_datasource(self, devices: list[ConplianceDeviceDataClass]) -> None: for device in devices: if df := DataFile.objects.filter(source=self.data["data_source"], path__icontains=device.name).first(): if config := df.data_as_string: @@ -165,14 +167,14 @@ def get_config_from_datasource(self, devices: list[DeviceDataClass]) -> None: else: device.error = f"Not found file in DataSource for device {device.name}" - def get_actual_configs(self, devices: list[DeviceDataClass]) -> None: + def get_actual_configs(self, devices: list[ConplianceDeviceDataClass]) -> None: if self.data["data_source"]: self.get_config_from_datasource(devices) else: loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.gather(*(d.get_actual_config() for d in devices))) - def get_diff(self, devices: list[DeviceDataClass]) -> None: + def get_diff(self, devices: list[ConplianceDeviceDataClass]) -> None: for device in devices: if device.error is not None: continue diff --git a/netbox_config_diff/compliance/secrets.py b/netbox_config_diff/compliance/secrets.py index 998311c..862adae 100644 --- a/netbox_config_diff/compliance/secrets.py +++ b/netbox_config_diff/compliance/secrets.py @@ -15,9 +15,9 @@ class SecretsMixin: def get_session_key(self) -> None: if "netbox_secrets_sessionid" in self.request.COOKIES: - self.session_key = base64.b64decode(self.request.COOKIES['netbox_secrets_sessionid']) + self.session_key = base64.b64decode(self.request.COOKIES["netbox_secrets_sessionid"]) elif "HTTP_X_SESSION_KEY" in self.request.META: - self.session_key = base64.b64decode(self.request.META['HTTP_X_SESSION_KEY']) + self.session_key = base64.b64decode(self.request.META["HTTP_X_SESSION_KEY"]) else: self.session_key = None @@ -45,24 +45,33 @@ def get_secret(self, secret: "Secret") -> str | None: return None return secret.plaintext - def get_credentials(self, device: Device) -> tuple[str, str]: - if self.netbox_secrets_installed: - if secret := device.secrets.filter(role__name=self.user_role).first(): - if value := self.get_secret(secret): - username = value - if secret := device.secrets.filter(role__name=self.password_role).first(): - if value := self.get_secret(secret): - password = value - return username, password + def get_credentials(self, device: Device) -> tuple[str, str, str]: + if not self.netbox_secrets_installed: + return self.username, self.password, self.auth_secondary - return self.username, self.password + if secret := device.secrets.filter(role__name=self.user_role).first(): + username = value if (value := self.get_secret(secret)) else self.username + else: + username = self.username + if secret := device.secrets.filter(role__name=self.password_role).first(): + password = value if (value := self.get_secret(secret)) else self.password + else: + password = self.password + if secret := device.secrets.filter(role__name=self.auth_secondary_role).first(): + auth_secondary = value if (value := self.get_secret(secret)) else self.auth_secondary + else: + auth_secondary = self.auth_secondary + + return username, password, auth_secondary def check_netbox_secrets(self) -> None: if "netbox_secrets" in get_installed_plugins(): self.get_master_key() self.user_role = get_plugin_config("netbox_config_diff", "USER_SECRET_ROLE") self.password_role = get_plugin_config("netbox_config_diff", "PASSWORD_SECRET_ROLE") + self.auth_secondary_role = get_plugin_config("netbox_config_diff", "SECOND_AUTH_SECRET_ROLE") self.netbox_secrets_installed = True - else: - self.username = get_plugin_config("netbox_config_diff", "USERNAME") - self.password = get_plugin_config("netbox_config_diff", "PASSWORD") + + self.username = get_plugin_config("netbox_config_diff", "USERNAME") + self.password = get_plugin_config("netbox_config_diff", "PASSWORD") + self.auth_secondary = get_plugin_config("netbox_config_diff", "AUTH_SECONDARY") diff --git a/netbox_config_diff/configurator/base.py b/netbox_config_diff/configurator/base.py index 3f3913e..a6635a2 100644 --- a/netbox_config_diff/configurator/base.py +++ b/netbox_config_diff/configurator/base.py @@ -13,12 +13,12 @@ from scrapli_cfg.response import ScrapliCfgResponse from utilities.utils import NetBoxFakeRequest -from netbox_config_diff.compliance.models import DeviceDataClass from netbox_config_diff.compliance.secrets import SecretsMixin from netbox_config_diff.compliance.utils import PLATFORM_MAPPING, get_unified_diff from netbox_config_diff.configurator.exceptions import DeviceConfigurationError, DeviceValidationError from netbox_config_diff.configurator.utils import CustomLogger from netbox_config_diff.constants import ACCEPTABLE_DRIVERS +from netbox_config_diff.models import ConfiguratorDeviceDataClass from .factory import AsyncScrapliCfg @@ -27,9 +27,9 @@ class Configurator(SecretsMixin): def __init__(self, devices: Iterable[Device], request: NetBoxFakeRequest) -> None: self.devices = devices self.request = request - self.unprocessed_devices: set[DeviceDataClass] = set() - self.processed_devices: set[DeviceDataClass] = set() - self.failed_devices: set[DeviceDataClass] = set() + self.unprocessed_devices: set[ConfiguratorDeviceDataClass] = set() + self.processed_devices: set[ConfiguratorDeviceDataClass] = set() + self.failed_devices: set[ConfiguratorDeviceDataClass] = set() self.substitutes: dict[str, list] = {} self.logger = CustomLogger() self.connections: dict[str, AsyncScrapliCfgPlatform] = {} @@ -37,7 +37,7 @@ def __init__(self, devices: Iterable[Device], request: NetBoxFakeRequest) -> Non def validate_devices(self) -> None: self.check_netbox_secrets() for device in self.devices: - username, password = self.get_credentials(device) + username, password, auth_secondary = self.get_credentials(device) if device.platform.platform_setting is None: self.logger.log_warning(f"Skipping {device}, add PlatformSetting for {device.platform} platform") elif device.platform.platform_setting.driver not in ACCEPTABLE_DRIVERS: @@ -59,13 +59,14 @@ def validate_devices(self) -> None: error = "Define config template for device" self.logger.log_failure(error) - d = DeviceDataClass( + d = ConfiguratorDeviceDataClass( pk=device.pk, name=device.name, mgmt_ip=str(device.primary_ip.address.ip), platform=device.platform.platform_setting.driver, username=username, password=password, + auth_secondary=auth_secondary, rendered_config=rendered_config, error=error, ) @@ -115,7 +116,7 @@ async def _collect_diffs(self) -> None: await asyncio.gather(*(self._collect_one_diff(d) for d in self.unprocessed_devices)) await self.update_diffs() - async def _collect_one_diff(self, device: DeviceDataClass) -> None: + async def _collect_one_diff(self, device: ConfiguratorDeviceDataClass) -> None: self.logger.log_info(f"Collecting diff on {device.name}") try: conn = self.connections[device.name] @@ -161,7 +162,7 @@ async def _push_configs(self) -> None: devices=", ".join(f"{d.name}: {d.config_error}" for d in self.failed_devices), ) - async def _push_one_config(self, device: DeviceDataClass) -> None: + async def _push_one_config(self, device: ConfiguratorDeviceDataClass) -> None: self.logger.log_info(f"Push config to {device.name}") try: conn = self.connections[device.name] @@ -184,7 +185,11 @@ async def _push_one_config(self, device: DeviceDataClass) -> None: self.failed_devices.add(device) async def abort_config( - self, operation: str, conn: AsyncScrapliCfgPlatform, response: ScrapliCfgResponse, device: DeviceDataClass + self, + operation: str, + conn: AsyncScrapliCfgPlatform, + response: ScrapliCfgResponse, + device: ConfiguratorDeviceDataClass, ) -> None: self.logger.log_failure(f"Failed to {operation} config on {device.name}: {response.result}") device.config_error = response.result @@ -197,7 +202,7 @@ async def rollback(self) -> None: self.logger.log_info(f"Rollback config: {', '.join(d.name for d in self.processed_devices)}") await asyncio.gather(*(self._rollback_one(d) for d in self.processed_devices)) - async def _rollback_one(self, device: DeviceDataClass) -> None: + async def _rollback_one(self, device: ConfiguratorDeviceDataClass) -> None: conn = self.connections[device.name] await conn.load_config(config=device.actual_config, replace=True) await conn.commit_config() diff --git a/netbox_config_diff/configurator/utils.py b/netbox_config_diff/configurator/utils.py index c556f32..cf751cb 100644 --- a/netbox_config_diff/configurator/utils.py +++ b/netbox_config_diff/configurator/utils.py @@ -15,7 +15,7 @@ def _log(self, message: str, log_level: str | None = None) -> None: raise Exception(f"Unknown logging level: {log_level}") if log_level is None: log_level = LogLevelChoices.LOG_DEFAULT - self.log_data.append((timezone.now().strftime('%Y-%m-%d %H:%M:%S'), log_level, message)) + self.log_data.append((timezone.now().strftime("%Y-%m-%d %H:%M:%S"), log_level, message)) def log(self, message: str) -> None: self._log(message, log_level=LogLevelChoices.LOG_DEFAULT) diff --git a/netbox_config_diff/models/__init__.py b/netbox_config_diff/models/__init__.py new file mode 100644 index 0000000..1ddddd2 --- /dev/null +++ b/netbox_config_diff/models/__init__.py @@ -0,0 +1,11 @@ +from .data_models import ConfiguratorDeviceDataClass, ConplianceDeviceDataClass +from .models import ConfigCompliance, ConfigurationRequest, PlatformSetting, Substitute + +__all__ = ( + "ConfigCompliance", + "ConfigurationRequest", + "ConfiguratorDeviceDataClass", + "ConplianceDeviceDataClass", + "PlatformSetting", + "Substitute", +) diff --git a/netbox_config_diff/models/base.py b/netbox_config_diff/models/base.py new file mode 100644 index 0000000..ddb682e --- /dev/null +++ b/netbox_config_diff/models/base.py @@ -0,0 +1,10 @@ +from django.db import models +from django.urls import reverse + + +class AbsoluteURLMixin(models.Model): + class Meta: + abstract = True + + def get_absolute_url(self): + return reverse(f"plugins:netbox_config_diff:{self._meta.model_name}", args=[self.pk]) diff --git a/netbox_config_diff/compliance/models.py b/netbox_config_diff/models/data_models.py similarity index 91% rename from netbox_config_diff/compliance/models.py rename to netbox_config_diff/models/data_models.py index 1ddaa26..43162ce 100644 --- a/netbox_config_diff/compliance/models.py +++ b/netbox_config_diff/models/data_models.py @@ -4,18 +4,18 @@ from scrapli import AsyncScrapli from netbox_config_diff.choices import ConfigComplianceStatusChoices -from netbox_config_diff.models import ConfigCompliance + +from .models import ConfigCompliance @dataclass -class DeviceDataClass: +class BaseDeviceDataClass: pk: int name: str mgmt_ip: str platform: str username: str password: str - command: str | None = None exclude_regex: str | None = None rendered_config: str | None = None actual_config: str | None = None @@ -25,14 +25,12 @@ class DeviceDataClass: error: str = "" config_error: str | None = None auth_strict_key: bool = False + auth_secondary: str | None = None transport: str = "asyncssh" def __str__(self) -> str: return self.name - def __hash__(self) -> int: - return hash(self.name) - def to_scrapli(self) -> dict: return { "host": self.mgmt_ip, @@ -40,6 +38,7 @@ def to_scrapli(self) -> dict: "auth_password": self.password, "platform": self.platform, "auth_strict_key": self.auth_strict_key, + "auth_secondary": self.auth_secondary, "transport": self.transport, "transport_options": { "asyncssh": { @@ -111,6 +110,14 @@ def send_to_db(self) -> None: except ConfigCompliance.DoesNotExist: ConfigCompliance.objects.create(**self.to_db()) + +class ConplianceDeviceDataClass(BaseDeviceDataClass): + command: str + + def __init__(self, command: str, **kwargs) -> None: + super().__init__(**kwargs) + self.command = command + async def get_actual_config(self) -> None: if self.error is not None: return @@ -123,3 +130,8 @@ async def get_actual_config(self) -> None: self.actual_config = result.result except Exception: self.error = traceback.format_exc() + + +class ConfiguratorDeviceDataClass(BaseDeviceDataClass): + def __hash__(self) -> int: + return hash(self.name) diff --git a/netbox_config_diff/models.py b/netbox_config_diff/models/models.py similarity index 88% rename from netbox_config_diff/models.py rename to netbox_config_diff/models/models.py index 0e4c4e9..4bb0dfd 100644 --- a/netbox_config_diff/models.py +++ b/netbox_config_diff/models/models.py @@ -5,7 +5,6 @@ from django.conf import settings from django.core.validators import RegexValidator from django.db import models -from django.urls import reverse from django.utils import timezone from django.utils.module_loading import import_string from django.utils.translation import gettext as _ @@ -18,8 +17,10 @@ from netbox_config_diff.choices import ConfigComplianceStatusChoices, ConfigurationRequestStatusChoices +from .base import AbsoluteURLMixin -class ConfigCompliance(ChangeLoggingMixin, models.Model): + +class ConfigCompliance(AbsoluteURLMixin, ChangeLoggingMixin, models.Model): device = models.OneToOneField( to="dcim.Device", on_delete=models.CASCADE, @@ -57,9 +58,6 @@ class Meta: def __str__(self) -> str: return self.device.name - def get_absolute_url(self): - return reverse("plugins:netbox_config_diff:configcompliance", args=[self.pk]) - def get_status_color(self) -> str: return ConfigComplianceStatusChoices.colors.get(self.status) @@ -72,7 +70,7 @@ def update(self, commit: bool = False, **kwargs) -> None: self.save() -class PlatformSetting(NetBoxModel): +class PlatformSetting(AbsoluteURLMixin, NetBoxModel): description = models.CharField( max_length=200, blank=True, @@ -107,29 +105,26 @@ class Meta: def __str__(self) -> str: return f"{self.platform} {self.driver}" - def get_absolute_url(self): - return reverse("plugins:netbox_config_diff:platformsetting", args=[self.pk]) - -class ConfigurationRequest(JobsMixin, PrimaryModel): +class ConfigurationRequest(AbsoluteURLMixin, JobsMixin, PrimaryModel): created_by = models.ForeignKey( to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, - related_name='+', + related_name="+", blank=True, null=True, ) approved_by = models.ForeignKey( to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, - related_name='+', + related_name="+", blank=True, null=True, ) scheduled_by = models.ForeignKey( to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, - related_name='+', + related_name="+", blank=True, null=True, ) @@ -161,9 +156,6 @@ class Meta: def __str__(self) -> str: return f"CR #{self.pk}" - def get_absolute_url(self): - return reverse("plugins:netbox_config_diff:configurationrequest", args=[self.pk]) - def get_status_color(self) -> str: return ConfigurationRequestStatusChoices.colors.get(self.status) @@ -210,7 +202,7 @@ def terminate(self, job: Job, status: str = ConfigurationRequestStatusChoices.CO self.save() -class Substitute(NetBoxModel): +class Substitute(AbsoluteURLMixin, NetBoxModel): platform_setting = models.ForeignKey( to="netbox_config_diff.PlatformSetting", on_delete=models.CASCADE, @@ -221,12 +213,12 @@ class Substitute(NetBoxModel): unique=True, validators=( RegexValidator( - regex=r'^[a-z0-9_]+$', + regex=r"^[a-z0-9_]+$", message=_("Only alphanumeric characters and underscores are allowed."), flags=re.IGNORECASE, ), RegexValidator( - regex=r'__', + regex=r"__", message=_("Double underscores are not permitted in names."), flags=re.IGNORECASE, inverse_match=True, @@ -234,7 +226,6 @@ class Substitute(NetBoxModel): ), ) description = models.CharField( - verbose_name=_('description'), max_length=200, blank=True, ) @@ -247,6 +238,3 @@ class Meta: def __str__(self) -> str: return self.name - - def get_absolute_url(self): - return reverse("plugins:netbox_config_diff:substitute", args=[self.pk]) diff --git a/netbox_config_diff/navigation.py b/netbox_config_diff/navigation.py index 0e08eb1..ddada5d 100644 --- a/netbox_config_diff/navigation.py +++ b/netbox_config_diff/navigation.py @@ -1,6 +1,3 @@ -from django import forms -from extras.dashboard.utils import register_widget -from extras.dashboard.widgets import DashboardWidget, WidgetConfigForm from extras.plugins import PluginMenuButton, PluginMenuItem from utilities.choices import ButtonColorChoices @@ -47,15 +44,3 @@ def get_add_button(model: str) -> PluginMenuButton: permissions=["netbox_config_diff.view_substitute"], ), ) - - -@register_widget -class ReminderWidget(DashboardWidget): - default_title = 'Reminder' - description = 'Add a virtual sticky note' - - class ConfigForm(WidgetConfigForm): - content = forms.CharField(widget=forms.Textarea()) - - def render(self, request): - return self.config.get('content') diff --git a/netbox_config_diff/views/base.py b/netbox_config_diff/views/base.py new file mode 100644 index 0000000..43cc8de --- /dev/null +++ b/netbox_config_diff/views/base.py @@ -0,0 +1,13 @@ +from django.urls import reverse +from netbox.views.generic import ObjectDeleteView, ObjectEditView + + +class BaseObjectDeleteView(ObjectDeleteView): + def get_return_url(self, request, obj=None): + return reverse(f"plugins:netbox_config_diff:{self.queryset.model._meta.model_name}_list") + + +class BaseObjectEditView(ObjectEditView): + @property + def default_return_url(self) -> str: + return f"plugins:netbox_config_diff:{self.queryset.model._meta.model_name}_list" diff --git a/netbox_config_diff/views/compliance.py b/netbox_config_diff/views/compliance.py index f6cfd40..fbe5c95 100644 --- a/netbox_config_diff/views/compliance.py +++ b/netbox_config_diff/views/compliance.py @@ -14,6 +14,8 @@ from netbox_config_diff.models import ConfigCompliance, PlatformSetting from netbox_config_diff.tables import ConfigComplianceTable, PlatformSettingTable +from .base import BaseObjectDeleteView, BaseObjectEditView + class BaseConfigComplianceConfigView(generic.ObjectView): config_field = None @@ -119,7 +121,7 @@ class ConfigComplianceListView(generic.ObjectListView): @register_model_view(ConfigCompliance, "delete") -class ConfigComplianceDeleteView(generic.ObjectDeleteView): +class ConfigComplianceDeleteView(BaseObjectDeleteView): queryset = ConfigCompliance.objects.all() @@ -142,13 +144,13 @@ class PlatformSettingListView(generic.ObjectListView): @register_model_view(PlatformSetting, "edit") -class PlatformSettingEditView(generic.ObjectEditView): +class PlatformSettingEditView(BaseObjectEditView): queryset = PlatformSetting.objects.all() form = PlatformSettingForm @register_model_view(PlatformSetting, "delete") -class PlatformSettingDeleteView(generic.ObjectDeleteView): +class PlatformSettingDeleteView(BaseObjectDeleteView): queryset = PlatformSetting.objects.all() diff --git a/netbox_config_diff/views/configuration.py b/netbox_config_diff/views/configuration.py index ce8ba4f..f4beefb 100644 --- a/netbox_config_diff/views/configuration.py +++ b/netbox_config_diff/views/configuration.py @@ -30,6 +30,8 @@ from netbox_config_diff.models import ConfigurationRequest, Substitute from netbox_config_diff.tables import ConfigurationRequestTable, SubstituteTable +from .base import BaseObjectDeleteView, BaseObjectEditView + @register_model_view(ConfigurationRequest) class ConfigurationRequestView(generic.ObjectView): @@ -55,7 +57,7 @@ class ConfigurationRequestListView(generic.ObjectListView): @register_model_view(ConfigurationRequest, "edit") -class ConfigurationRequestEditView(generic.ObjectEditView): +class ConfigurationRequestEditView(BaseObjectEditView): queryset = ConfigurationRequest.objects.all() form = ConfigurationRequestForm @@ -86,7 +88,7 @@ def get(self, request, *args, **kwargs): @register_model_view(ConfigurationRequest, "delete") -class ConfigurationRequestDeleteView(generic.ObjectDeleteView): +class ConfigurationRequestDeleteView(BaseObjectDeleteView): queryset = ConfigurationRequest.objects.all() @@ -173,7 +175,7 @@ def post(self, request, pk): @register_model_view(ConfigurationRequest, "schedule") -class ConfigurationRequestScheduleView(generic.ObjectEditView): +class ConfigurationRequestScheduleView(BaseObjectEditView): queryset = ConfigurationRequest.objects.all() form = ConfigurationRequestScheduleForm @@ -320,11 +322,11 @@ class SubstituteListView(generic.ObjectListView): @register_model_view(Substitute, "edit") -class SubstituteEditView(generic.ObjectEditView): +class SubstituteEditView(BaseObjectEditView): queryset = Substitute.objects.all() form = SubstituteForm @register_model_view(Substitute, "delete") -class SubstituteDeleteView(generic.ObjectDeleteView): +class SubstituteDeleteView(BaseObjectDeleteView): queryset = Substitute.objects.all() diff --git a/pyproject.toml b/pyproject.toml index 24b96ea..0ae9cf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,17 +51,6 @@ dependencies = {file = ["requirements/base.txt"]} optional-dependencies.dev = { file = ["requirements/dev.txt"] } optional-dependencies.test = { file = ["requirements/test.txt"] } -[tool.black] -line-length = 120 -skip-string-normalization = true -exclude = ''' -( - /( - migrations - )/ -) -''' - [tool.ruff] exclude = ["migrations", "__pycache__"] select = ["C", "E", "F", "I"] diff --git a/requirements/dev.txt b/requirements/dev.txt index 844be90..e06651c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,2 +1 @@ -black==23.10.0 -ruff==0.1.0 +ruff==0.1.2 diff --git a/tests/conftest.py b/tests/conftest.py index 6c44751..692d0a9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from typing_extensions import Unpack from netbox_config_diff.compliance.base import ConfigDiffBase -from netbox_config_diff.compliance.models import DeviceDataClass +from netbox_config_diff.models import ConplianceDeviceDataClass from tests.factories import DataSourceFactory @@ -97,6 +97,6 @@ def factory(**fields: Unpack["DeviceDataClassData"]) -> "DeviceDataClassData": @pytest.fixture() -def devicedataclass_data(devicedataclass_factory: "DeviceDataClassDataFactory") -> DeviceDataClass: +def devicedataclass_data(devicedataclass_factory: "DeviceDataClassDataFactory") -> ConplianceDeviceDataClass: data = devicedataclass_factory() - return DeviceDataClass(**data) + return ConplianceDeviceDataClass(**data) diff --git a/tests/test_compliance.py b/tests/test_compliance.py index e111ab1..04f2ff8 100644 --- a/tests/test_compliance.py +++ b/tests/test_compliance.py @@ -4,8 +4,7 @@ from dcim.models import Device from utilities.exceptions import AbortScript -from netbox_config_diff.compliance.models import DeviceDataClass -from netbox_config_diff.models import ConfigCompliance +from netbox_config_diff.models import ConfigCompliance, ConplianceDeviceDataClass from tests.factories import ConfigComplianceFactory, DeviceFactory, PlatformSettingFactory if TYPE_CHECKING: @@ -91,6 +90,7 @@ def test_devicedataclass_to_scrapli(devicedataclass_data: "DeviceDataClassData") "auth_password": devicedataclass_data.password, "platform": devicedataclass_data.platform, "auth_strict_key": devicedataclass_data.auth_strict_key, + "auth_secondary": devicedataclass_data.auth_secondary, "transport": devicedataclass_data.transport, "transport_options": { "asyncssh": { @@ -144,7 +144,7 @@ def test_devicedataclass_to_db( devicedataclass_factory: "DeviceDataClassDataFactory", diff: str, error: str, status: str ) -> None: data = devicedataclass_factory(**{"diff": diff, "error": error}) - d = DeviceDataClass(**data) + d = ConplianceDeviceDataClass(**data) assert d.to_db() == { "device_id": d.pk,