diff --git a/development/Dockerfile b/development/Dockerfile index d6f70552..177cd531 100644 --- a/development/Dockerfile +++ b/development/Dockerfile @@ -64,7 +64,7 @@ RUN pip show nautobot | grep "^Version: " | sed -e 's/Version: /nautobot==/' > c # We can't use the entire freeze as it takes forever to resolve with rigidly fixed non-direct dependencies, # especially those that are only direct to Nautobot but the container included versions slightly mismatch RUN poetry export -f requirements.txt --without-hashes --output poetry_freeze_base.txt -RUN poetry export -f requirements.txt --dev --without-hashes --output poetry_freeze_all.txt +RUN poetry export -f requirements.txt --with dev --without-hashes --output poetry_freeze_all.txt RUN sort poetry_freeze_base.txt poetry_freeze_all.txt | uniq -u > poetry_freeze_dev.txt # Install all local project as editable, constrained on Nautobot version, to get any additional diff --git a/development/docker-compose.base.yml b/development/docker-compose.base.yml index 3fbf8f10..26356204 100644 --- a/development/docker-compose.base.yml +++ b/development/docker-compose.base.yml @@ -21,8 +21,9 @@ services: condition: "service_started" db: condition: "service_healthy" - <<: *nautobot-build - <<: *nautobot-base + <<: + - *nautobot-build + - *nautobot-base worker: entrypoint: - "sh" diff --git a/nautobot_golden_config/api/views.py b/nautobot_golden_config/api/views.py index f6a2b18c..39c91d70 100644 --- a/nautobot_golden_config/api/views.py +++ b/nautobot_golden_config/api/views.py @@ -117,7 +117,7 @@ class ConfigToPushViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): class RemediationSettingViewSet(NautobotModelViewSet): # pylint:disable=too-many-ancestors - """API viewset for interacting with ConfigRemove objects.""" + """API viewset for interacting with RemediationSetting objects.""" queryset = models.RemediationSetting.objects.all() serializer_class = serializers.RemediationSettingSerializer diff --git a/nautobot_golden_config/filters.py b/nautobot_golden_config/filters.py index 00f1ea44..02378a7a 100644 --- a/nautobot_golden_config/filters.py +++ b/nautobot_golden_config/filters.py @@ -6,7 +6,7 @@ from nautobot.extras.filters import CustomFieldModelFilterSetMixin, StatusFilter from nautobot.extras.models import JobResult, Status from nautobot.tenancy.models import Tenant, TenantGroup -from nautobot.utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TreeNodeMultipleChoiceFilter, TagFilter +from nautobot.utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter from nautobot_golden_config import models @@ -293,18 +293,18 @@ class RemediationSettingFilterSet(BaseFilterSet, NameSlugSearchFilterSet): method="search", label="Search", ) - remediationsetting_id = django_filters.ModelMultipleChoiceFilter( - queryset=models.RemediationSetting.objects.all(), - label="RemediationSetting ID", - ) platform = django_filters.ModelMultipleChoiceFilter( field_name="platform__name", queryset=Platform.objects.all(), to_field_name="name", label="Platform Name", ) + platform_id = django_filters.ModelMultipleChoiceFilter( + queryset=Platform.objects.all(), + label="Platform ID", + ) remediation_type = django_filters.ModelMultipleChoiceFilter( - field_name="remediationsetting__remediation_type", + field_name="remediation_type", queryset=models.RemediationSetting.objects.all(), to_field_name="remediation_type", label="Remediation Type", @@ -314,7 +314,7 @@ def search(self, queryset, name, value): # pylint: disable=unused-argument """Perform the filtered search.""" if not value.strip(): return queryset - qs_filter = Q(platform__icontains=value) + qs_filter = Q(platform__name__icontains=value) | Q(remediation_type__icontains=value) return queryset.filter(qs_filter) class Meta: diff --git a/nautobot_golden_config/forms.py b/nautobot_golden_config/forms.py index 999e0501..74cfa107 100644 --- a/nautobot_golden_config/forms.py +++ b/nautobot_golden_config/forms.py @@ -6,23 +6,13 @@ import nautobot.extras.forms as extras_forms import nautobot.utilities.forms as utilities_forms from django import forms -from nautobot.dcim.models import ( - Device, - DeviceRole, - DeviceType, - Manufacturer, - Platform, - Rack, - RackGroup, - Region, - Site, -) +from nautobot.dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Rack, RackGroup, Region, Site from nautobot.extras.forms import NautobotBulkEditForm, NautobotFilterForm, NautobotModelForm from nautobot.extras.models import DynamicGroup, GitRepository, JobResult, Status, Tag from nautobot.tenancy.models import Tenant, TenantGroup from nautobot.utilities.forms import add_blank_choice, DatePicker, SlugField, TagFilterField from nautobot_golden_config import models -from nautobot_golden_config.choices import ComplianceRuleConfigTypeChoice, ConfigPlanTypeChoice +from nautobot_golden_config.choices import ComplianceRuleConfigTypeChoice, ConfigPlanTypeChoice, RemediationTypeChoice # ConfigCompliance @@ -416,10 +406,10 @@ class Meta: # Remediation Setting class RemediationSettingForm(NautobotModelForm): - """Filter Form for Line Removal instances.""" + """Filter Form for Remediation Settings instances.""" class Meta: - """Boilerplate form Meta data for removal feature.""" + """Boilerplate form Meta data for Remediation Settings.""" model = models.RemediationSetting fields = ( @@ -430,9 +420,16 @@ class Meta: class RemediationSettingFilterForm(NautobotFilterForm): - """Filter Form for Line Replacement.""" + """Filter Form for Remediation Settings.""" model = models.RemediationSetting + q = forms.CharField(required=False, label="Search") + platform = utilities_forms.DynamicModelMultipleChoiceField( + queryset=Platform.objects.all(), required=False, display_field="name", to_field_name="name" + ) + remediation_type = forms.ChoiceField( + choices=add_blank_choice(RemediationTypeChoice), required=False, label="Remediation Type" + ) class RemediationSettingCSVForm(extras_forms.CustomFieldModelCSVForm): @@ -451,6 +448,7 @@ class RemediationSettingBulkEditForm(NautobotBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=models.RemediationSetting.objects.all(), widget=forms.MultipleHiddenInput ) + remediation_type = forms.ChoiceField(choices=RemediationTypeChoice, label="Remediation Type") class Meta: """Boilerplate form Meta data for RemediationSetting.""" diff --git a/nautobot_golden_config/models.py b/nautobot_golden_config/models.py index 40d6ef85..47459001 100644 --- a/nautobot_golden_config/models.py +++ b/nautobot_golden_config/models.py @@ -18,15 +18,10 @@ from nautobot.utilities.utils import serialize_object, serialize_object_v2 from netutils.config.compliance import feature_compliance from netutils.lib_mapper import HIERCONFIG_LIB_MAPPER_REVERSE -from nautobot_golden_config.choices import ( - ComplianceRuleConfigTypeChoice, - ConfigPlanTypeChoice, - RemediationTypeChoice, -) +from nautobot_golden_config.choices import ComplianceRuleConfigTypeChoice, ConfigPlanTypeChoice, RemediationTypeChoice from nautobot_golden_config.utilities.constant import ENABLE_SOTAGG, PLUGIN_CFG from nautobot_golden_config.utilities.utils import get_platform - LOGGER = logging.getLogger(__name__) GRAPHQL_STR_START = "query ($device_id: ID!)" @@ -298,11 +293,7 @@ class ComplianceRule(PrimaryModel): # pylint: disable=too-many-ancestors @property def remediation_setting(self): """Returns remediation settings for a particular platform.""" - try: - remediation_setting = RemediationSetting.objects.get(platform=self.platform) - except RemediationSetting.DoesNotExist as err: - raise ValidationError(f"Platform {self.platform.slug} has no Remediation Settings defined.") from err - return remediation_setting + return RemediationSetting.objects.filter(platform=self.platform).first() def to_csv(self): """Indicates model fields to return as csv.""" @@ -428,10 +419,10 @@ def remediation_on_save(self): self.remediation = None return - if not FUNC_MAPPER.get(self.rule.remediation_setting.remediation_type): - raise ValidationError( - f"Remediation {self.rule.remediation_setting.remediation_type} has no associated function set." - ) + if not self.rule.remediation_setting: + self.remediation = None + return + remediation_config = FUNC_MAPPER[self.rule.remediation_setting.remediation_type](obj=self) self.remediation = remediation_config @@ -826,6 +817,11 @@ class RemediationSetting(PrimaryModel): # pylint: disable=too-many-ancestors "remediation_type", ] + class Meta: + """Meta information for RemediationSettings model.""" + + ordering = ("platform", "remediation_type") + def to_csv(self): """Indicates model fields to return as csv.""" return ( @@ -835,7 +831,7 @@ def to_csv(self): def __str__(self): """Return a sane string representation of the instance.""" - return self.platform.slug + return str(self.platform.slug) def get_absolute_url(self): """Absolute url for the RemediationRule instance.""" diff --git a/nautobot_golden_config/tests/test_api.py b/nautobot_golden_config/tests/test_api.py index 4d984845..f8a7e7f2 100644 --- a/nautobot_golden_config/tests/test_api.py +++ b/nautobot_golden_config/tests/test_api.py @@ -1,28 +1,26 @@ """Unit tests for nautobot_golden_config.""" from copy import deepcopy + from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType - from django.urls import reverse -from rest_framework import status - from nautobot.dcim.models import Device, Platform +from nautobot.extras.models import DynamicGroup, GitRepository, GraphQLQuery, Status from nautobot.utilities.testing import APITestCase, APIViewTestCases -from nautobot.extras.models import GitRepository, GraphQLQuery, DynamicGroup, Status +from rest_framework import status from nautobot_golden_config.choices import RemediationTypeChoice from nautobot_golden_config.models import ConfigPlan, GoldenConfigSetting, RemediationSetting from .conftest import ( + create_config_compliance, create_device, + create_device_data, create_feature_rule_json, - create_config_compliance, create_git_repos, - create_saved_queries, - create_device_data, create_job_result, + create_saved_queries, ) - User = get_user_model() @@ -312,6 +310,22 @@ def setUpTestData(cls): remediation_type=type_custom, ) + platforms = ( + Platform.objects.create(name="Platform 4", slug="platform-4"), + Platform.objects.create(name="Platform 5", slug="platform-5"), + Platform.objects.create(name="Platform 6", slug="platform-6"), + ) + + cls.create_data = [ + {"platform": platforms[0].pk, "remediation_type": type_cli}, + { + "platform": platforms[1].pk, + "remediation_type": type_cli, + "remediation_options": {"some_option": "some_value"}, + }, + {"platform": platforms[2].pk, "remediation_type": type_custom}, + ] + cls.update_data = { "remediation_type": type_custom, } @@ -324,7 +338,6 @@ def test_list_objects_brief(self): """Skipping test due to brief_fields not implemented.""" - # pylint: disable=too-many-ancestors,too-many-locals class ConfigPlanTest(APIViewTestCases.APIViewTestCase): """Test API for ConfigPlan."""