diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a56c043..437c4a2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -27,30 +27,40 @@ //"python.sortImports.args": [ // "--profile=black" //], - //"python.sortImports.path": "/opt/netbox/venv/bin/isort", + "python.sortImports.path": "/opt/netbox/venv/bin/isort", "python.analysis.typeCheckingMode": "strict", "python.analysis.extraPaths": [ "/opt/netbox/", "/opt/netbox/netbox" ], "python.autoComplete.extraPaths": [ - "/opt/netbox/", - "/opt/netbox/netbox" + "/opt/netbox/netbox/", + "/opt/netbox/netbox/**", + "/opt/netbox/netbox/**/**" + ], - "python.defaultInterpreterPath": "/opt/netbox/venv/bin/python", - "python.formatting.autopep8Path": "/opt/netbox/venv/lib/python3.9/site-packages/autopep8", - "python.formatting.blackPath": "/opt/netbox/venv/lib/python3.9/site-packages/black", + "python.defaultInterpreterPath": "/opt/netbox/venv/bin/python3", + "python.formatting.autopep8Path": "/opt/netbox/venv/bin/autopep8", + "python.formatting.blackPath": "/opt/netbox/venv/bin/black", "python.formatting.provider": "black", - "python.formatting.yapfPath": "/opt/netbox/venv/lib/python3.9/site-packages/yapf", - "python.linting.banditPath": "/opt/netbox/venv/lib/python3.9/site-packages/bandit", + "python.formatting.yapfPath": "/opt/netbox/venv/bin/yapf", + "python.linting.banditPath": "/opt/netbox/venv/bin/bandit", "python.linting.enabled": true, - "python.linting.flake8Path": "/opt/netbox/venv/lib/python3.9/site-packages/flake8", - "python.linting.mypyPath": "/opt/netbox/venv/lib/python3.9/site-packages/mypy", - "python.linting.pycodestylePath": "/opt/netbox/venv/lib/python3.9/site-packages/pycodestyle", - "python.linting.pydocstylePath": "/opt/netbox/venv/lib/python3.9/site-packages/pydocstyle", + "python.linting.flake8Path": "/opt/netbox/venv/bin/flake8", + "python.linting.mypyPath": "//opt/netbox/venv/bin/mypy", + "python.linting.pycodestylePath": "/opt/netbox/venv/bin/pycodestyle", + "python.linting.pydocstylePath": "/opt/netbox/venv/bin/pydocstyle", + "python.linting.pylintArgs": [ + "--load-plugins", + "pylint_django", + "--errors-only", + "--load-plugins=pylint_django", + "--django-settings-module=/opt/netbox/netbox/netbox/netbox.settings", + "--enable=W0602,W0611,W0612,W0613,W0614" + ], "python.linting.pylintEnabled": true, - "python.linting.pylintPath": "/opt/netbox/venv/lib/python3.9/site-packages/pylint", - "python.pythonPath": "/opt/netbox/venv/bin/python", + "python.linting.pylintPath": "/opt/netbox/venv/bin/pylint", + "python.pythonPath": "/opt/netbox/venv/bin/python3", "python.terminal.activateEnvironment": true, "python.venvPath": "/opt/netbox/" }, @@ -65,7 +75,11 @@ "aaron-bond.better-comments", "GitHub.codespaces", "codezombiech.gitignore", - "Tyriar.sort-lines" + "Tyriar.sort-lines", + "GitHub.vscode-pull-request-github", + "sourcery.sourcery", + "mintlify.document", + "batisteo.vscode-django" ] } }, diff --git a/.devcontainer/initializers/prefixes.yml b/.devcontainer/initializers/prefixes.yml index 6aea9c5..941f63c 100644 --- a/.devcontainer/initializers/prefixes.yml +++ b/.devcontainer/initializers/prefixes.yml @@ -27,3 +27,11 @@ status: active tenant: tenant2 vlan: vlan2 +- description: prefix3 + prefix: 192.168.1.0/24 + site: AMS 1 + status: active +- description: prefix4 + prefix: 192.168.11.0/24 + site: AMS 2 + status: active diff --git a/.devcontainer/requirements-dev.txt b/.devcontainer/requirements-dev.txt index 9f60b96..093cf52 100644 --- a/.devcontainer/requirements-dev.txt +++ b/.devcontainer/requirements-dev.txt @@ -8,4 +8,5 @@ pre-commit pycodestyle pydocstyle pylint +pylint-django yapf diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/documentation_change.yaml index 84ded74..2f8656e 100644 --- a/.github/ISSUE_TEMPLATE/documentation_change.yaml +++ b/.github/ISSUE_TEMPLATE/documentation_change.yaml @@ -1,6 +1,6 @@ --- name: 📖 Documentation Change -description: Suggest an addition or modification to the NetBox access-list plugin documentation +description: Suggest an addition or modification to the NetBox Access Lists plugin documentation title: "[Docs]: " labels: ["documentation"] body: diff --git a/README.md b/README.md index 093cd9f..daaa042 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # netbox-access-lists -A NetBox plugin for Access-List management +A NetBox plugin for Access List management ## Origin diff --git a/netbox_access_lists/__init__.py b/netbox_access_lists/__init__.py index 3d9dc1c..3b49054 100644 --- a/netbox_access_lists/__init__.py +++ b/netbox_access_lists/__init__.py @@ -1,12 +1,15 @@ +""" +Define the NetBox Plugin +""" + from extras.plugins import PluginConfig class NetBoxAccessListsConfig(PluginConfig): name = 'netbox_access_lists' - verbose_name = ' NetBox Access Lists' + verbose_name = 'Access Lists' description = 'Manage simple ACLs in NetBox' version = '0.1' base_url = 'access-lists' - config = NetBoxAccessListsConfig diff --git a/netbox_access_lists/api/serializers.py b/netbox_access_lists/api/serializers.py index a5b92db..c34d193 100644 --- a/netbox_access_lists/api/serializers.py +++ b/netbox_access_lists/api/serializers.py @@ -1,40 +1,77 @@ +""" +Serializers control the translation of client data to and from Python objects, +while Django itself handles the database abstraction. +""" + +from dcim.api.nested_serializers import NestedDeviceSerializer +from ipam.api.serializers import NestedPrefixSerializer +from netbox.api.serializers import (NetBoxModelSerializer, + WritableNestedSerializer) from rest_framework import serializers -from ipam.api.serializers import NestedPrefixSerializer -from dcim.api.serializers import NestedDeviceSerializer -from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer -from ..models import AccessList, AccessListRule - +from ..models import AccessList, ACLExtendedRule, ACLStandardRule # # Nested serializers # + class NestedAccessListSerializer(WritableNestedSerializer): + """ + Defines the nested serializer for the django AccessList model & associates it to a view. + """ url = serializers.HyperlinkedIdentityField( view_name='plugins-api:netbox_access_lists-api:accesslist-detail' ) class Meta: + """ + Associates the django model ACLStandardRule & fields to the nested serializer. + """ model = AccessList fields = ('id', 'url', 'display', 'name', 'device') -class NestedAccessListRuleSerializer(WritableNestedSerializer): +class NestedACLStandardRuleSerializer(WritableNestedSerializer): + """ + Defines the nested serializer for the django ACLStandardRule model & associates it to a view. + """ url = serializers.HyperlinkedIdentityField( - view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail' + view_name='plugins-api:netbox_access_lists-api:aclstandardrule-detail' ) class Meta: - model = AccessListRule + """ + Associates the django model ACLStandardRule & fields to the nested serializer. + """ + model = ACLStandardRule fields = ('id', 'url', 'display', 'index') +class NestedACLExtendedRuleSerializer(WritableNestedSerializer): + """ + Defines the nested serializer for the django ACLExtendedRule model & associates it to a view. + """ + url = serializers.HyperlinkedIdentityField( + view_name='plugins-api:netbox_access_lists-api:aclextendedrule-detail' + ) + + class Meta: + """ + Associates the django model ACLExtendedRule & fields to the nested serializer. + """ + model = ACLExtendedRule + fields = ('id', 'url', 'display', 'index') + # # Regular serializers # + class AccessListSerializer(NetBoxModelSerializer): + """ + Defines the serializer for the django AccessList model & associates it to a view. + """ url = serializers.HyperlinkedIdentityField( view_name='plugins-api:netbox_access_lists-api:accesslist-detail' ) @@ -42,25 +79,90 @@ class AccessListSerializer(NetBoxModelSerializer): device = NestedDeviceSerializer() class Meta: + """ + Associates the django model AccessList & fields to the serializer. + """ model = AccessList fields = ( 'id', 'url', 'display', 'name', 'device', 'type', 'default_action', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', 'rule_count', + 'last_updated', 'rule_count' ) + def validate(self, data): + """ + Validate the AccessList django model model's inputs before allowing it to update the instance. + """ + if self.instance.rule_count > 0: + raise serializers.ValidationError({ + 'type': 'This ACL has ACL rules already associated, CANNOT change ACL type!!' + }) -class AccessListRuleSerializer(NetBoxModelSerializer): + return super().validate(data) + + +class ACLStandardRuleSerializer(NetBoxModelSerializer): + """ + Defines the serializer for the django ACLStandardRule model & associates it to a view. + """ url = serializers.HyperlinkedIdentityField( - view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail' + view_name='plugins-api:netbox_access_lists-api:aclstandardrule-detail' + ) + access_list = NestedAccessListSerializer() + source_prefix = NestedPrefixSerializer() + + class Meta: + """ + Associates the django model ACLStandardRule & fields to the serializer. + """ + model = ACLStandardRule + fields = ( + 'id', 'url', 'display', 'access_list', 'index', 'action', 'tags', 'description', + 'created', 'custom_fields', 'last_updated', 'source_prefix' + ) + + def validate(self, data): + """ + Validate the ACLStandardRule django model model's inputs before allowing it to update the instance. + """ + access_list = data.get('access_list') + if access_list.type == 'extended': + raise serializers.ValidationError({ + 'access_list': 'CANNOT associated standard ACL rules to an extended ACL!!' + }) + + return super().validate(data) + + +class ACLExtendedRuleSerializer(NetBoxModelSerializer): + """ + Defines the serializer for the django ACLExtendedRule model & associates it to a view. + """ + url = serializers.HyperlinkedIdentityField( + view_name='plugins-api:netbox_access_lists-api:aclextendedrule-detail' ) access_list = NestedAccessListSerializer() source_prefix = NestedPrefixSerializer() destination_prefix = NestedPrefixSerializer() class Meta: - model = AccessListRule + """ + Associates the django model ACLExtendedRule & fields to the serializer. + """ + model = ACLExtendedRule fields = ( - 'id', 'url', 'display', 'access_list', 'index', 'protocol', 'source_prefix', 'source_ports', - 'destination_prefix', 'destination_ports', 'action', 'tags', 'custom_fields', 'created', - 'last_updated', + 'id', 'url', 'display', 'access_list', 'index', 'action', 'tags', 'description', + 'created', 'custom_fields', 'last_updated', 'source_prefix', 'source_ports', + 'destination_prefix', 'destination_ports', 'protocol' ) + + def validate(self, data): + """ + Validate the ACLExtendedRule django model model's inputs before allowing it to update the instance. + """ + access_list = data.get('access_list') + if access_list.type == 'standard': + raise serializers.ValidationError({ + 'access_list': 'CANNOT associated extended ACL rules to a standard ACL!!' + }) + + return super().validate(data) diff --git a/netbox_access_lists/api/urls.py b/netbox_access_lists/api/urls.py index 7afe0a2..20fb28b 100644 --- a/netbox_access_lists/api/urls.py +++ b/netbox_access_lists/api/urls.py @@ -1,11 +1,16 @@ -from netbox.api.routers import NetBoxRouter -from . import views +""" +Creates API endpoint URLs for the plugin. +""" +from netbox.api.routers import NetBoxRouter + +from . import views app_name = 'netbox_access_list' router = NetBoxRouter() router.register('access-lists', views.AccessListViewSet) -router.register('access-list-rules', views.AccessListRuleViewSet) +router.register('standard-acl-rules', views.ACLStandardRuleViewSet) +router.register('extended-acl-rules', views.ACLExtendedRuleViewSet) urlpatterns = router.urls diff --git a/netbox_access_lists/api/views.py b/netbox_access_lists/api/views.py index fa477dc..9617d1d 100644 --- a/netbox_access_lists/api/views.py +++ b/netbox_access_lists/api/views.py @@ -1,24 +1,47 @@ -from django.db.models import Count +""" +Create views to handle the API logic. +A view set is a single class that can handle the view, add, change, +and delete operations which each require dedicated views under the UI. +""" +from django.db.models import Count from netbox.api.viewsets import NetBoxModelViewSet from .. import filtersets, models -from .serializers import AccessListSerializer, AccessListRuleSerializer +from .serializers import (AccessListSerializer, ACLExtendedRuleSerializer, + ACLStandardRuleSerializer) class AccessListViewSet(NetBoxModelViewSet): + """ + Defines the view set for the django AccessList model & associates it to a view. + """ queryset = models.AccessList.objects.prefetch_related( 'device', 'tags' ).annotate( - rule_count=Count('rules') + rule_count=Count('aclextendedrules') + Count('aclstandardrules') ) serializer_class = AccessListSerializer filterset_class = filtersets.AccessListFilterSet -class AccessListRuleViewSet(NetBoxModelViewSet): - queryset = models.AccessListRule.objects.prefetch_related( - 'access_list', 'source_prefix', 'destination_prefix', 'tags' +class ACLStandardRuleViewSet(NetBoxModelViewSet): + """ + Defines the view set for the django ACLStandardRule model & associates it to a view. + """ + queryset = models.ACLStandardRule.objects.prefetch_related( + 'access_list', 'tags', 'source_prefix' ) - serializer_class = AccessListRuleSerializer - filterset_class = filtersets.AccessListRuleFilterSet + serializer_class = ACLStandardRuleSerializer + filterset_class = filtersets.ACLStandardRuleFilterSet + + +class ACLExtendedRuleViewSet(NetBoxModelViewSet): + """ + Defines the view set for the django ACLExtendedRule model & associates it to a view. + """ + queryset = models.ACLExtendedRule.objects.prefetch_related( + 'access_list', 'tags', 'source_prefix', 'destination_prefix', + ) + serializer_class = ACLExtendedRuleSerializer + filterset_class = filtersets.ACLExtendedRuleFilterSet diff --git a/netbox_access_lists/choices.py b/netbox_access_lists/choices.py new file mode 100644 index 0000000..086c665 --- /dev/null +++ b/netbox_access_lists/choices.py @@ -0,0 +1,63 @@ +""" +Defines the various choices to be used by the models, forms, and other plugin specifics. +""" + +from utilities.choices import ChoiceSet + +__all__ = ( + 'ACLActionChoices', + 'ACLRuleActionChoices', + 'ACLTypeChoices', + 'ACLProtocolChoices', +) + + +class ACLActionChoices(ChoiceSet): + """ + Defines the choices availble for the Access Lists plugin specific to ACL default_action. + """ + ACTION_DENY = 'deny' + ACTION_PERMIT = 'permit' + ACTION_REJECT = 'reject' + + CHOICES = [ + (ACTION_DENY, 'Deny', 'red'), + (ACTION_PERMIT, 'Permit', 'green'), + (ACTION_REJECT, 'Reject (Reset)', 'orange'), + ] + + +class ACLRuleActionChoices(ChoiceSet): + """ + Defines the choices availble for the Access Lists plugin specific to ACL rule actions. + """ + ACTION_DENY = 'deny' + ACTION_PERMIT = 'permit' + ACTION_REMARK = 'remark' + + CHOICES = [ + (ACTION_DENY, 'Deny', 'red'), + (ACTION_PERMIT, 'Permit', 'green'), + (ACTION_REMARK, 'Remark', 'blue'), + ] + + +class ACLTypeChoices(ChoiceSet): + """ + Defines the choices availble for the Access Lists plugin specific to ACL type. + """ + CHOICES = [ + ('extended', 'Extended', 'purple'), + ('standard', 'Standard', 'blue'), + ] + + +class ACLProtocolChoices(ChoiceSet): + """ + Defines the choices availble for the Access Lists plugin specific to ACL Rule protocol. + """ + CHOICES = [ + ('icmp', 'ICMP', 'purple'), + ('tcp', 'TCP', 'blue'), + ('udp', 'UDP', 'orange'), + ] diff --git a/netbox_access_lists/filtersets.py b/netbox_access_lists/filtersets.py index a588dbf..5254349 100644 --- a/netbox_access_lists/filtersets.py +++ b/netbox_access_lists/filtersets.py @@ -1,21 +1,71 @@ +""" +Filters enable users to request only a specific subset of objects matching a query; +when filtering the sites list by status or region, for instance. +""" + from netbox.filtersets import NetBoxModelFilterSet -from .models import AccessList, AccessListRule + +from .models import * + +__all__ = ( + 'AccessListFilterSet', + 'ACLStandardRuleFilterSet', + 'ACLExtendedRuleFilterSet', +) class AccessListFilterSet(NetBoxModelFilterSet): + """ + Define the filter set for the django model AccessList. + """ + class Meta: + """ + Associates the django model AccessList & fields to the filter set. + """ model = AccessList fields = ('id', 'name', 'device', 'type', 'default_action', 'comments') def search(self, queryset, name, value): + """ + Override the default search behavior for the django model. + """ return queryset.filter(description__icontains=value) -class AccessListRuleFilterSet(NetBoxModelFilterSet): +class ACLStandardRuleFilterSet(NetBoxModelFilterSet): + """ + Define the filter set for the django model ACLStandardRule. + """ class Meta: - model = AccessListRule - fields = ('id', 'access_list', 'index', 'protocol', 'action', 'remark') + """ + Associates the django model ACLStandardRule & fields to the filter set. + """ + model = ACLStandardRule + fields = ('id', 'access_list', 'index', 'action') def search(self, queryset, name, value): + """ + Override the default search behavior for the django model. + """ + return queryset.filter(description__icontains=value) + + +class ACLExtendedRuleFilterSet(NetBoxModelFilterSet): + """ + Define the filter set for the django model ACLExtendedRule. + """ + + class Meta: + """ + Associates the django model ACLExtendedRule & fields to the filter set. + """ + model = ACLExtendedRule + fields = ('id', 'access_list', 'index', 'action', 'protocol') + + def search(self, queryset, name, value): + """ + Override the default search behavior for the django model. + """ return queryset.filter(description__icontains=value) diff --git a/netbox_access_lists/forms.py b/netbox_access_lists/forms.py deleted file mode 100644 index 8c43f75..0000000 --- a/netbox_access_lists/forms.py +++ /dev/null @@ -1,80 +0,0 @@ -from django import forms - -from extras.models import Tag -from ipam.models import Prefix -from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm -from utilities.forms import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField -from .models import AccessList, AccessListRule, AccessListActionChoices, AccessListProtocolChoices, AccessListTypeChoices - - -class AccessListForm(NetBoxModelForm): - comments = CommentField() - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = AccessList - fields = ('name', 'device', 'type', 'default_action', 'comments', 'tags') - - -class AccessListFilterForm(NetBoxModelFilterSetForm): - model = AccessList - type = forms.MultipleChoiceField( - choices=AccessListTypeChoices, - required=False, - widget=StaticSelectMultiple() - ) - default_action = forms.MultipleChoiceField( - choices=AccessListActionChoices, - required=False, - widget=StaticSelectMultiple() - ) - tag = TagFilterField(model) - - -class AccessListRuleForm(NetBoxModelForm): - access_list = DynamicModelChoiceField( - queryset=AccessList.objects.all() - ) - source_prefix = DynamicModelChoiceField( - queryset=Prefix.objects.all() - ) - destination_prefix = DynamicModelChoiceField( - queryset=Prefix.objects.all() - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class Meta: - model = AccessListRule - fields = ( - 'access_list', 'index', 'remark', 'source_prefix', 'source_ports', 'destination_prefix', - 'destination_ports', 'protocol', 'action', 'tags', - ) - - -class AccessListRuleFilterForm(NetBoxModelFilterSetForm): - model = AccessListRule - access_list = forms.ModelMultipleChoiceField( - queryset=AccessList.objects.all(), - required=False, - widget=StaticSelectMultiple() - ) - index = forms.IntegerField( - required=False - ) - protocol = forms.MultipleChoiceField( - choices=AccessListProtocolChoices, - required=False, - widget=StaticSelectMultiple() - ) - action = forms.MultipleChoiceField( - choices=AccessListActionChoices, - required=False, - widget=StaticSelectMultiple() - ) - tag = TagFilterField(model) diff --git a/netbox_access_lists/forms/__init__.py b/netbox_access_lists/forms/__init__.py new file mode 100644 index 0000000..01f1bbd --- /dev/null +++ b/netbox_access_lists/forms/__init__.py @@ -0,0 +1,13 @@ +""" +Import each of the directory's scripts. +""" + +#from .bulk_create import * +from .bulk_edit import * +#from .bulk_import import * +#from .connections import * +from .filtersets import * +#from .formsets import * +from .models import * +#from .object_create import * +#from .object_import import * diff --git a/netbox_access_lists/forms/bulk_edit.py b/netbox_access_lists/forms/bulk_edit.py new file mode 100644 index 0000000..56c1f34 --- /dev/null +++ b/netbox_access_lists/forms/bulk_edit.py @@ -0,0 +1,86 @@ +""" +Draft for a possible BulkEditForm, but may not be worth wile. +""" + +from dcim.models import Device, Region, Site, SiteGroup +from django import forms +from django.core.exceptions import ValidationError +from django.utils.safestring import mark_safe +from netbox.forms import NetBoxModelBulkEditForm +from utilities.forms import (ChoiceField, DynamicModelChoiceField, + StaticSelect, add_blank_choice) + +from ..choices import ACLActionChoices, ACLTypeChoices +from ..models import AccessList + +#__all__ = ( +# 'AccessListBulkEditForm', +#) + + +#class AccessListBulkEditForm(NetBoxModelBulkEditForm): +# model = AccessList +# +# region = DynamicModelChoiceField( +# queryset=Region.objects.all(), +# required=False, +# ) +# site_group = DynamicModelChoiceField( +# queryset=SiteGroup.objects.all(), +# required=False, +# label='Site Group' +# ) +# site = DynamicModelChoiceField( +# queryset=Site.objects.all(), +# required=False +# ) +# device = DynamicModelChoiceField( +# queryset=Device.objects.all(), +# query_params={ +# 'region': '$region', +# 'group_id': '$site_group', +# 'site_id': '$site', +# }, +# required=False, +# ) +# type = ChoiceField( +# choices=add_blank_choice(ACLTypeChoices), +# required=False, +# widget=StaticSelect(), +# ) +# default_action = ChoiceField( +# choices=add_blank_choice(ACLActionChoices), +# required=False, +# widget=StaticSelect(), +# label='Default Action', +# ) +# +# fieldsets = [ +# ('Host Details', ('region', 'site_group', 'site', 'device')), +# ('Access List Details', ('type', 'default_action', 'add_tags', 'remove_tags')), +# ] +# +# +# class Meta: +# model = AccessList +# fields = ('region', 'site_group', 'site', 'device', 'type', 'default_action', 'add_tags', 'remove_tags') +# help_texts = { +# 'default_action': 'The default behavior of the ACL.', +# 'name': 'The name uniqueness per device is case insensitive.', +# 'type': mark_safe('*Note: CANNOT be changed if ACL Rules are assoicated to this Access List.'), +# } +# +# def clean(self): # Not working given you are bulkd editing multiple forms +# cleaned_data = super().clean() +# if self.errors.get('name'): +# return cleaned_data +# name = cleaned_data.get('name') +# device = cleaned_data.get('device') +# type = cleaned_data.get('type') +# if ('name' in self.changed_data or 'device' in self.changed_data) and AccessList.objects.filter(name__iexact=name, device=device).exists(): +# raise forms.ValidationError('An ACL with this name (case insensitive) is already associated to this device.') +# if type == 'extended' and self.cleaned_data['aclstandardrules'].exists(): +# raise forms.ValidationError('This ACL has Standard ACL rules already associated, CANNOT change ACL type!!') +# elif type == 'standard' and self.cleaned_data['aclextendedrules'].exists(): +# raise forms.ValidationError('This ACL has Extended ACL rules already associated, CANNOT change ACL type!!') +# return cleaned_data diff --git a/netbox_access_lists/forms/filtersets.py b/netbox_access_lists/forms/filtersets.py new file mode 100644 index 0000000..78d88d0 --- /dev/null +++ b/netbox_access_lists/forms/filtersets.py @@ -0,0 +1,134 @@ +""" +Defines each django model's GUI filter/search options. +""" + +from dcim.models import Device, Region, Site, SiteGroup +from django import forms +from ipam.models import Prefix +from netbox.forms import NetBoxModelFilterSetForm +from utilities.forms import (ChoiceField, DynamicModelChoiceField, + StaticSelect, StaticSelectMultiple, + TagFilterField, add_blank_choice) + +from ..choices import (ACLActionChoices, ACLProtocolChoices, + ACLRuleActionChoices, ACLTypeChoices) +from ..models import AccessList, ACLExtendedRule, ACLStandardRule + +__all__ = ( + 'AccessListFilterForm', + 'ACLStandardRuleFilterForm', + 'ACLExtendedRuleFilterForm', +) + + +class AccessListFilterForm(NetBoxModelFilterSetForm): + """ + GUI filter form to search the django AccessList model. + """ + model = AccessList + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + ) + site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label='Site Group' + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False + ) + device = DynamicModelChoiceField( + queryset=Device.objects.all(), + query_params={ + 'region': '$region', + 'group_id': '$site_group', + 'site_id': '$site', + }, + required=False + ) + type = ChoiceField( + choices=add_blank_choice(ACLTypeChoices), + required=False, + initial='', + widget=StaticSelect(), + ) + default_action = ChoiceField( + choices=add_blank_choice(ACLActionChoices), + required=False, + initial='', + widget=StaticSelect(), + label='Default Action', + ) + tag = TagFilterField(model) + + fieldsets = ( + (None, ('q', 'tag')), + ('Host Details', ('region', 'site_group', 'site', 'device')), + ('ACL Details', ('type', 'default_action')), + ) + + +class ACLStandardRuleFilterForm(NetBoxModelFilterSetForm): + """ + GUI filter form to search the django ACLStandardRule model. + """ + model = ACLStandardRule + tag = TagFilterField(model) + source_prefix = forms.ModelMultipleChoiceField( + queryset=Prefix.objects.all(), + required=False, + widget=StaticSelectMultiple(), + label='Source Prefix', + ) + action = forms.ChoiceField( + choices=add_blank_choice(ACLRuleActionChoices), + required=False, + initial='', + widget=StaticSelect(), + ) + fieldsets = ( + (None, ('q', 'tag')), + ('Rule Details', ('action', 'source_prefix',)), + ) + + +class ACLExtendedRuleFilterForm(NetBoxModelFilterSetForm): + """ + GUI filter form to search the django ACLExtendedRule model. + """ + model = ACLExtendedRule + index = forms.IntegerField( + required=False + ) + tag = TagFilterField(model) + action = forms.ChoiceField( + choices=add_blank_choice(ACLRuleActionChoices), + required=False, + widget=StaticSelect(), + initial='', + ) + source_prefix = forms.ModelMultipleChoiceField( + queryset=Prefix.objects.all(), + required=False, + widget=StaticSelectMultiple(), + label='Source Prefix', + ) + desintation_prefix = forms.ModelMultipleChoiceField( + queryset=Prefix.objects.all(), + required=False, + widget=StaticSelectMultiple(), + label='Destination Prefix', + ) + protocol = ChoiceField( + choices=add_blank_choice(ACLProtocolChoices), + required=False, + widget=StaticSelect(), + initial='', + ) + + fieldsets = ( + (None, ('q', 'tag')), + ('Rule Details', ('action', 'source_prefix', 'desintation_prefix', 'protocol')), + ) diff --git a/netbox_access_lists/forms/models.py b/netbox_access_lists/forms/models.py new file mode 100644 index 0000000..c2e645d --- /dev/null +++ b/netbox_access_lists/forms/models.py @@ -0,0 +1,229 @@ +""" +Defines each django model's GUI form to add or edit objects for each django model. +""" + +from dcim.models import Device, Region, Site, SiteGroup +from django import forms +from django.utils.safestring import mark_safe +from extras.models import Tag +from ipam.models import Prefix +from netbox.forms import NetBoxModelForm +from utilities.forms import (CommentField, DynamicModelChoiceField, + DynamicModelMultipleChoiceField) + +from ..models import AccessList, ACLExtendedRule, ACLStandardRule + +__all__ = ( + 'AccessListForm', + 'ACLStandardRuleForm', + 'ACLExtendedRuleForm', +) + +# Sets a standard mark_save help_text value to be used by the various classes +acl_rule_logic_help = mark_safe('*Note: CANNOT be set if action is set to remark.') + + +class AccessListForm(NetBoxModelForm): + """ + GUI form to add or edit an AccessList. + Requires a device, a name, a type, and a default_action. + """ + region = DynamicModelChoiceField( + queryset=Region.objects.all(), + required=False, + ) + site_group = DynamicModelChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label='Site Group' + ) + site = DynamicModelChoiceField( + queryset=Site.objects.all(), + required=False + ) + device = DynamicModelChoiceField( + queryset=Device.objects.all(), + query_params={ + 'region': '$region', + 'group_id': '$site_group', + 'site_id': '$site', + }, + ) + comments = CommentField() + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + fieldsets = [ + ('Host Details', ('region', 'site_group', 'site', 'device')), + ('Access List Details', ('name', 'type', 'default_action', 'tags')), + ] + + class Meta: + model = AccessList + fields = ('region', 'site_group', 'site', 'device', 'name', 'type', 'default_action', 'comments', 'tags') + help_texts = { + 'default_action': 'The default behavior of the ACL.', + 'name': 'The name uniqueness per device is case insensitive.', + 'type': mark_safe('*Note: CANNOT be changed if ACL Rules are assoicated to this Access List.'), + } + + def clean(self): + """ + Validates form inputs before submitting. + """ + cleaned_data = super().clean() + error_message = {} + if self.errors.get('name'): + return cleaned_data + type = cleaned_data.get('type') + if type == 'extended' and self.instance.aclstandardrules.exists(): + error_message.update({'type': ['This ACL has Standard ACL rules already associated, CANNOT change ACL type!!']}) + elif type == 'standard' and self.instance.aclextendedrules.exists(): + error_message.update({'type': ['This ACL has Extended ACL rules already associated, CANNOT change ACL type!!']}) + if len(error_message) > 0: + raise forms.ValidationError(error_message) + + return cleaned_data + + +class ACLStandardRuleForm(NetBoxModelForm): + """ + GUI form to add or edit Standard Access List. + Requires an access_list, an index, and ACL rule type. + See the clean function for logic on other field requirements. + """ + access_list = DynamicModelChoiceField( + queryset=AccessList.objects.all(), + query_params={ + 'type': 'standard' + }, + help_text=mark_safe('*Note: This field will only display Standard ACLs.'), + label='Access List', + ) + source_prefix = DynamicModelChoiceField( + queryset=Prefix.objects.all(), + required=False, + help_text=acl_rule_logic_help, + label='Source Prefix', + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + fieldsets = ( + ('Access List Details', ('access_list', 'description', 'tags')), + ('Rule Definition', ('index', 'action', 'remark', 'source_prefix')), + ) + + class Meta: + model = ACLStandardRule + fields = ( + 'access_list', 'index', 'action', 'remark', 'source_prefix', + 'tags', 'description' + ) + help_texts = { + 'index': 'Determines the order of the rule in the ACL processing.', + 'remark': mark_safe('*Note: CANNOT be set if source prefix OR action is set.'), + } + + def clean(self): + """ + Validates form inputs before submitting. + If action is set to remark, remark needs to be set. + If action is set to remark, source_prefix cannot be set. + If action is not set to remark, remark cannot be set. + """ + cleaned_data = super().clean() + error_message = {} + if cleaned_data.get('action') == 'remark': + if cleaned_data.get('remark') is None: + error_message.update({'remark': ['Action is set to remark, you MUST add a remark.']}) + if cleaned_data.get('source_prefix'): + error_message.update({'source_prefix': ['Action is set to remark, Source Prefix CANNOT be set.']}) + elif cleaned_data.get('remark'): + error_message.update({'remark': ['CANNOT set remark unless action is set to remark, .']}) + if len(error_message) > 0: + raise forms.ValidationError(error_message) + return cleaned_data + + +class ACLExtendedRuleForm(NetBoxModelForm): + """ + GUI form to add or edit Extended Access List. + Requires an access_list, an index, and ACL rule type. + See the clean function for logic on other field requirements. + """ + access_list = DynamicModelChoiceField( + queryset=AccessList.objects.all(), + query_params={ + 'type': 'extended' + }, + help_text=mark_safe('*Note: This field will only display Extended ACLs.'), + label='Access List', + ) + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + source_prefix = DynamicModelChoiceField( + queryset=Prefix.objects.all(), + required=False, + help_text=acl_rule_logic_help, + label='Source Prefix', + ) + destination_prefix = DynamicModelChoiceField( + queryset=Prefix.objects.all(), + required=False, + help_text=acl_rule_logic_help, + label='Destination Prefix', + ) + fieldsets = ( + ('Access List Details', ('access_list', 'description', 'tags')), + ('Rule Definition', ('index', 'action', 'remark', 'source_prefix', 'source_ports', 'destination_prefix', 'destination_ports', 'protocol',)), + ) + + class Meta: + model = ACLExtendedRule + fields = ( + 'access_list', 'index', 'action', 'remark', 'source_prefix', + 'source_ports', 'destination_prefix', 'destination_ports', 'protocol', + 'tags', 'description' + ) + help_texts = { + 'destination_ports': acl_rule_logic_help, + 'index': 'Determines the order of the rule in the ACL processing.', + 'protocol': acl_rule_logic_help, + 'remark': mark_safe('*Note: CANNOT be set if action is not set to remark.'), + 'source_ports': acl_rule_logic_help, + } + + def clean(self): + """ + Validates form inputs before submitting. + If action is set to remark, remark needs to be set. + If action is set to remark, source_prefix, source_ports, desintation_prefix, destination_ports, or protocol cannot be set. + If action is not set to remark, remark cannot be set. + """ + cleaned_data = super().clean() + error_message = {} + if cleaned_data.get('action') == 'remark': + if cleaned_data.get('remark') is None: + error_message.update({'remark': ['Action is set to remark, you MUST add a remark.']}) + if cleaned_data.get('source_prefix'): + error_message.update({'source_prefix': ['Action is set to remark, Source Prefix CANNOT be set.']}) + if cleaned_data.get('source_ports'): + error_message.update({'source_ports': ['Action is set to remark, Source Ports CANNOT be set.']}) + if cleaned_data.get('destination_prefix'): + error_message.update({'destination_prefix': ['Action is set to remark, Destination Prefix CANNOT be set.']}) + if cleaned_data.get('destination_ports'): + error_message.update({'destination_ports': ['Action is set to remark, Destination Ports CANNOT be set.']}) + if cleaned_data.get('protocol'): + error_message.update({'protocol': ['Action is set to remark, Protocol CANNOT be set.']}) + elif cleaned_data.get('remark'): + error_message.update({'remark': ['CANNOT set remark unless action is set to remark, .']}) + if len(error_message) > 0: + raise forms.ValidationError(error_message) + return cleaned_data diff --git a/netbox_access_lists/graphql.py b/netbox_access_lists/graphql.py index fe84a0b..7295323 100644 --- a/netbox_access_lists/graphql.py +++ b/netbox_access_lists/graphql.py @@ -1,39 +1,85 @@ +""" +Define the object types and queries availble via the graphql api. +""" + from graphene import ObjectType -from netbox.graphql.types import NetBoxObjectType from netbox.graphql.fields import ObjectField, ObjectListField +from netbox.graphql.types import NetBoxObjectType + from . import filtersets, models +__all__ = ( + 'AccessListType', + 'ACLExtendedRuleType', + 'ACLStandardRuleType', +) # # Object types # + class AccessListType(NetBoxObjectType): + """ + Defines the object type for the django model AccessList. + """ class Meta: + """ + Associates the filterset, fields, and model for the django model AccessList. + """ model = models.AccessList fields = '__all__' filterset_class = filtersets.AccessListFilterSet -class AccessListRuleType(NetBoxObjectType): +class ACLExtendedRuleType(NetBoxObjectType): + """ + Defines the object type for the django model ACLExtendedRule. + """ class Meta: - model = models.AccessListRule + """ + Associates the filterset, fields, and model for the django model ACLExtendedRule. + """ + model = models.ACLExtendedRule fields = '__all__' - filterset_class = filtersets.AccessListRuleFilterSet + filterset_class = filtersets.ACLExtendedRuleFilterSet +class ACLStandardRuleType(NetBoxObjectType): + """ + Defines the object type for the django model ACLStandardRule. + """ + + class Meta: + """ + Associates the filterset, fields, and model for the django model ACLStandardRule. + """ + model = models.ACLStandardRule + fields = '__all__' + filterset_class = filtersets.ACLStandardRuleFilterSet + # # Queries # + class Query(ObjectType): + """ + Defines the queries availible to this plugin via the graphql api. + """ + access_list = ObjectField(AccessListType) + access_lists = ObjectListField(AccessListType) access_list_list = ObjectListField(AccessListType) - access_list_rule = ObjectField(AccessListRuleType) - access_list_rule_list = ObjectListField(AccessListRuleType) + acl_extended_rule = ObjectField(ACLExtendedRuleType) + acl_extended_rules = ObjectListField(ACLExtendedRuleType) + acl_extended_rule_list = ObjectListField(ACLExtendedRuleType) + acl_standard_rule = ObjectField(ACLStandardRuleType) + acl_standard_rules = ObjectListField(ACLStandardRuleType) + acl_standard_rule_list = ObjectListField(ACLStandardRuleType) schema = Query diff --git a/netbox_access_lists/migrations/0001_initial.py b/netbox_access_lists/migrations/0001_initial.py index e1f7f4d..e95f69f 100644 --- a/netbox_access_lists/migrations/0001_initial.py +++ b/netbox_access_lists/migrations/0001_initial.py @@ -1,11 +1,21 @@ +""" +Defines the migrations for propogating django models into the database schemea. +""" + import django.contrib.postgres.fields import django.core.serializers.json -from django.db import migrations, models import django.db.models.deletion import taggit.managers +from django.db import migrations, models +__all__ = ( + 'Migration', +) class Migration(migrations.Migration): + """ + Defines the migrations required for the initial setup of the access lists plugin and its associated django models. + """ initial = True @@ -25,36 +35,60 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=100)), ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_lists', to='dcim.device')), ('type', models.CharField(max_length=100)), - ('default_action', models.CharField(max_length=30)), + ('default_action', models.CharField(default='deny', max_length=30)), ('comments', models.TextField(blank=True)), ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), ], options={ 'ordering': ('name', 'device'), 'unique_together': {('name', 'device')}, + 'verbose_name': 'Access List', }, ), migrations.CreateModel( - name='AccessListRule', + name='ACLStandardRule', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), ('created', models.DateTimeField(auto_now_add=True, null=True)), ('last_updated', models.DateTimeField(auto_now=True, null=True)), ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('access_list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aclstandardrules', to='netbox_access_lists.accesslist')), ('index', models.PositiveIntegerField()), - ('protocol', models.CharField(blank=True, max_length=30)), - ('source_ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, null=True, size=None)), - ('destination_ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, null=True, size=None)), + ('description', models.CharField(blank=True, max_length=500)), ('action', models.CharField(max_length=30)), ('remark', models.CharField(blank=True, null=True, max_length=500)), - ('access_list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='netbox_access_lists.accesslist')), - ('destination_prefix', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='ipam.prefix')), ('source_prefix', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='ipam.prefix')), - ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), ], options={ 'ordering': ('access_list', 'index'), 'unique_together': {('access_list', 'index')}, + 'verbose_name': 'ACL Standard Rule', + }, + ), + migrations.CreateModel( + name='ACLExtendedRule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('access_list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aclstandardrules', to='netbox_access_lists.accesslist')), + ('index', models.PositiveIntegerField()), + ('description', models.CharField(blank=True, max_length=500)), + ('action', models.CharField(max_length=30)), + ('remark', models.CharField(blank=True, null=True, max_length=500)), + ('source_prefix', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='ipam.prefix')), + ('source_ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, null=True, size=None)), + ('destination_prefix', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='ipam.prefix')), + ('destination_ports', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, null=True, size=None)), + ('protocol', models.CharField(blank=True, max_length=30)), + ], + options={ + 'ordering': ('access_list', 'index'), + 'unique_together': {('access_list', 'index')}, + 'verbose_name': 'ACL Extended Rule', }, ), ] diff --git a/netbox_access_lists/models.py b/netbox_access_lists/models.py deleted file mode 100644 index 0d0d6b6..0000000 --- a/netbox_access_lists/models.py +++ /dev/null @@ -1,144 +0,0 @@ -from django.contrib.postgres.fields import ArrayField -from django.db import models -from django.urls import reverse - -from netbox.models import NetBoxModel -from utilities.choices import ChoiceSet - - -class AccessListActionChoices(ChoiceSet): - key = 'AccessListRule.action' - ACTION_DENY = 'deny' - ACTION_PERMIT = 'permit' - ACTION_REJECT = 'reject' - - CHOICES = [ - (ACTION_DENY, 'Deny', 'red'), - (ACTION_PERMIT, 'Permit', 'green'), - (ACTION_REJECT, 'Reject (Reset)', 'orange'), - ] - - -class AccessListTypeChoices(ChoiceSet): - - CHOICES = [ - ('extended', 'Extended', 'purple'), - ('standard', 'Standard', 'blue'), - ] - - -class AccessListProtocolChoices(ChoiceSet): - - CHOICES = [ - ('icmp', 'ICMP', 'purple'), - ('tcp', 'TCP', 'blue'), - ('udp', 'UDP', 'orange'), - ] - - -class AccessList(NetBoxModel): - name = models.CharField( - max_length=100 - ) - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='access_lists' - ) - type = models.CharField( - max_length=30, - choices=AccessListTypeChoices - ) - default_action = models.CharField( - default=AccessListActionChoices.ACTION_DENY, - max_length=30, - choices=AccessListActionChoices, - verbose_name='Default Action' - ) - comments = models.TextField( - blank=True - ) - - class Meta: - ordering = ('name', 'device') - - def __str__(self): - return self.name - - def get_absolute_url(self): - return reverse('plugins:netbox_access_lists:accesslist', args=[self.pk]) - - def get_default_action_color(self): - return AccessListActionChoices.colors.get(self.default_action) - - def get_type_color(self): - return AccessListTypeChoices.colors.get(self.type) - - -class AccessListRule(NetBoxModel): - access_list = models.ForeignKey( - on_delete=models.CASCADE, - related_name='rules', - to=AccessList, - verbose_name='Access List' - ) - index = models.PositiveIntegerField() - protocol = models.CharField( - blank=True, - choices=AccessListProtocolChoices, - max_length=30, - ) - source_prefix = models.ForeignKey( - blank=True, - null=True, - on_delete=models.PROTECT, - related_name='+', - to='ipam.Prefix', - verbose_name='Source Prefix' - ) - source_ports = ArrayField( - base_field=models.PositiveIntegerField(), - blank=True, - null=True, - verbose_name='Soure Ports' - ) - destination_prefix = models.ForeignKey( - blank=True, - null=True, - on_delete=models.PROTECT, - related_name='+', - to='ipam.Prefix', - verbose_name='Destination Prefix' - ) - destination_ports = ArrayField( - base_field=models.PositiveIntegerField(), - blank=True, - null=True, - verbose_name='Destination Ports' - ) - action = models.CharField( - choices=AccessListActionChoices, - default=AccessListActionChoices.ACTION_PERMIT, - max_length=30, - ) - remark = models.CharField( - max_length=200, - blank=True, - null=True - ) - - class Meta: - ordering = ('access_list', 'index') - unique_together = ('access_list', 'index') - - def __str__(self): - return f'{self.access_list}: Rule {self.index}' - - def get_absolute_url(self): - return reverse('plugins:netbox_access_lists:accesslistrule', args=[self.pk]) - - def get_protocol_color(self): - return AccessListProtocolChoices.colors.get(self.protocol) - - def get_action_color(self): - return AccessListActionChoices.colors.get(self.action) diff --git a/netbox_access_lists/models/__init__.py b/netbox_access_lists/models/__init__.py new file mode 100644 index 0000000..99d911d --- /dev/null +++ b/netbox_access_lists/models/__init__.py @@ -0,0 +1,6 @@ +""" +Import each of the directory's scripts. +""" + +from .access_list_rules import * +from .access_lists import * diff --git a/netbox_access_lists/models/access_list_rules.py b/netbox_access_lists/models/access_list_rules.py new file mode 100644 index 0000000..417f720 --- /dev/null +++ b/netbox_access_lists/models/access_list_rules.py @@ -0,0 +1,144 @@ +""" +Define the django models for this plugin. +""" + +from django.contrib.postgres.fields import ArrayField +from django.db import models +from django.urls import reverse +from netbox.models import NetBoxModel + +from ..choices import * +from .access_lists import AccessList + +__all__ = ( + 'ACLRule', + 'ACLStandardRule', + 'ACLExtendedRule', +) + + +class ACLRule(NetBoxModel): + """ + Abstract model for ACL Rules. + Inherrited by both ACLStandardRule and ACLExtendedRule. + """ + access_list = models.ForeignKey( + on_delete=models.CASCADE, + to=AccessList, + verbose_name='Access List', + ) + index = models.PositiveIntegerField() + remark = models.CharField( + max_length=200, + blank=True, + null=True + ) + description = models.CharField( + max_length=500, + blank=True + ) + action = models.CharField( + choices=ACLRuleActionChoices, + max_length=30, + ) + source_prefix = models.ForeignKey( + blank=True, + null=True, + on_delete=models.PROTECT, + related_name='+', + to='ipam.Prefix', + verbose_name='Source Prefix' + ) + + def __str__(self): + return f'{self.access_list}: Rule {self.index}' + + def get_action_color(self): + return ACLRuleActionChoices.colors.get(self.action) + + class Meta: + """ + Define the common model properties: + - as an abstract model + - ordering + - unique together + """ + abstract = True + ordering = ('access_list', 'index') + unique_together = ('access_list', 'index') + + +class ACLStandardRule(ACLRule): + """ + Inherits ACLRule. + """ + + def get_absolute_url(self): + """ + The method is a Django convention; although not strictly required, + it conveniently returns the absolute URL for any particular object. + """ + return reverse('plugins:netbox_access_lists:aclstandardrule', args=[self.pk]) + + class Meta(ACLRule.Meta): + """ + Define the model properties adding to or overriding the inherited class: + - default_related_name for any FK relationships + - verbose name (for displaying in the GUI) + - verbose name plural (for displaying in the GUI) + """ + default_related_name='aclstandardrules' + verbose_name='ACL Standard Rule' + verbose_name_plural='ACL Standard Rules' + +class ACLExtendedRule(ACLRule): + """ + Inherits ACLRule. + Add ACLExtendedRule specific fields: source_ports, desintation_prefix, destination_ports, and protocol + """ + source_ports = ArrayField( + base_field=models.PositiveIntegerField(), + blank=True, + null=True, + verbose_name='Soure Ports' + ) + destination_prefix = models.ForeignKey( + blank=True, + null=True, + on_delete=models.PROTECT, + related_name='+', + to='ipam.Prefix', + verbose_name='Destination Prefix' + ) + destination_ports = ArrayField( + base_field=models.PositiveIntegerField(), + blank=True, + null=True, + verbose_name='Destination Ports' + ) + protocol = models.CharField( + blank=True, + choices=ACLProtocolChoices, + max_length=30, + ) + + def get_absolute_url(self): + """ + The method is a Django convention; although not strictly required, + it conveniently returns the absolute URL for any particular object. + """ + return reverse('plugins:netbox_access_lists:aclextendedrule', args=[self.pk]) + + def get_protocol_color(self): + return ACLProtocolChoices.colors.get(self.protocol) + + class Meta(ACLRule.Meta): + """ + Define the model properties adding to or overriding the inherited class: + - default_related_name for any FK relationships + - verbose name (for displaying in the GUI) + - verbose name plural (for displaying in the GUI) + """ + default_related_name='aclextendedrules' + verbose_name='ACL Extended Rule' + verbose_name_plural='ACL Extended Rules' diff --git a/netbox_access_lists/models/access_lists.py b/netbox_access_lists/models/access_lists.py new file mode 100644 index 0000000..6572600 --- /dev/null +++ b/netbox_access_lists/models/access_lists.py @@ -0,0 +1,63 @@ +""" +Define the django models for this plugin. +""" + +from django.db import models +from django.urls import reverse +from netbox.models import NetBoxModel + +from ..choices import * + +__all__ = ( + 'AccessList', +) + + +class AccessList(NetBoxModel): + """ + Model defintion for Access Lists. + """ + name = models.CharField( + max_length=100 + ) + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='access_lists' + ) + type = models.CharField( + max_length=30, + choices=ACLTypeChoices + ) + default_action = models.CharField( + default=ACLActionChoices.ACTION_DENY, + max_length=30, + choices=ACLActionChoices, + verbose_name='Default Action' + ) + comments = models.TextField( + blank=True + ) + + class Meta: + ordering = ('name', 'device') + unique_together = ('name', 'device') + default_related_name='accesslists' + verbose_name='Access List' + verbose_name_plural='Access Lists' + + def __str__(self): + return self.name + + def get_absolute_url(self): + """ + The method is a Django convention; although not strictly required, + it conveniently returns the absolute URL for any particular object. + """ + return reverse('plugins:netbox_access_lists:accesslist', args=[self.pk]) + + def get_default_action_color(self): + return ACLActionChoices.colors.get(self.default_action) + + def get_type_color(self): + return ACLTypeChoices.colors.get(self.type) diff --git a/netbox_access_lists/navigation.py b/netbox_access_lists/navigation.py index 5fbe6ec..0ba5610 100644 --- a/netbox_access_lists/navigation.py +++ b/netbox_access_lists/navigation.py @@ -1,6 +1,13 @@ +""" +Define the plugin menu buttons & the plugin navigation bar enteries. +""" + from extras.plugins import PluginMenuButton, PluginMenuItem from utilities.choices import ButtonColorChoices +# +# Define plugin menu buttons +# accesslist_buttons = [ PluginMenuButton( @@ -11,25 +18,44 @@ accesslist_buttons = [ ) ] -accesslistrule_butons = [ +aclstandardrule_butons = [ PluginMenuButton( - link='plugins:netbox_access_lists:accesslistrule_add', + link='plugins:netbox_access_lists:aclstandardrule_add', title='Add', icon_class='mdi mdi-plus-thick', color=ButtonColorChoices.GREEN ) ] +aclextendedrule_butons = [ + PluginMenuButton( + link='plugins:netbox_access_lists:aclextendedrule_add', + title='Add', + icon_class='mdi mdi-plus-thick', + color=ButtonColorChoices.GREEN + ) +] + +# +# Define navigation bar links including the above buttons defined. +# + menu_items = ( PluginMenuItem( link='plugins:netbox_access_lists:accesslist_list', link_text='Access Lists', buttons=accesslist_buttons ), - # # Comment out Access List Rule to force creation in the ACL view - # PluginMenuItem( - # link='plugins:netbox_access_lists:accesslistrule_list', - # link_text='Access List Rules', - # buttons=accesslistrule_butons - # ), + # Comment out Standard Access List rule to force creation in the ACL view + PluginMenuItem( + link='plugins:netbox_access_lists:aclstandardrule_list', + link_text='ACL Standard Rule', + buttons=aclstandardrule_butons + ), + # Comment out Extended Access List rule to force creation in the ACL view + PluginMenuItem( + link='plugins:netbox_access_lists:aclextendedrule_list', + link_text='ACL Extended Rules', + buttons=aclextendedrule_butons + ), ) diff --git a/netbox_access_lists/tables.py b/netbox_access_lists/tables.py index aa6b5e9..f9e8e18 100644 --- a/netbox_access_lists/tables.py +++ b/netbox_access_lists/tables.py @@ -1,10 +1,23 @@ -import django_tables2 as tables +""" +Define the object lists / table view for each of the plugin models. +""" -from netbox.tables import NetBoxTable, columns, ChoiceFieldColumn -from .models import AccessList, AccessListRule +import django_tables2 as tables +from netbox.tables import ChoiceFieldColumn, NetBoxTable, columns + +from .models import AccessList, ACLExtendedRule, ACLStandardRule + +__all__ = ( + 'AccessListTable', + 'ACLStandardRuleTable', + 'ACLExtendedRuleTable', +) class AccessListTable(NetBoxTable): + """ + Defines the table view for the AccessList model. + """ name = tables.Column( linkify=True ) @@ -26,26 +39,54 @@ class AccessListTable(NetBoxTable): default_columns = ('name', 'device', 'type', 'rule_count', 'default_action', 'tags') -class AccessListRuleTable(NetBoxTable): +class ACLStandardRuleTable(NetBoxTable): + """ + Defines the table view for the ACLStandardRule model. + """ access_list = tables.Column( linkify=True ) index = tables.Column( linkify=True ) - protocol = ChoiceFieldColumn() action = ChoiceFieldColumn() tags = columns.TagColumn( - url_name='plugins:netbox_access_lists:accesslistrule_list' + url_name='plugins:netbox_access_lists:aclstandardrule_list' ) class Meta(NetBoxTable.Meta): - model = AccessListRule + model = ACLStandardRule fields = ( - 'pk', 'id', 'access_list', 'index', 'source_prefix', 'source_ports', 'destination_prefix', - 'destination_ports', 'protocol', 'action', 'remark', 'actions', 'tags' + 'pk', 'id', 'access_list', 'index', 'action', 'actions', 'remark', 'tags', 'description', ) default_columns = ( - 'access_list', 'index', 'remark', 'source_prefix', 'source_ports', 'destination_prefix', - 'destination_ports', 'protocol', 'action', 'actions', 'tags' + 'access_list', 'index', 'action', 'actions', 'remark', 'tags' + ) + + +class ACLExtendedRuleTable(NetBoxTable): + """ + Defines the table view for the ACLExtendedRule model. + """ + access_list = tables.Column( + linkify=True + ) + index = tables.Column( + linkify=True + ) + action = ChoiceFieldColumn() + tags = columns.TagColumn( + url_name='plugins:netbox_access_lists:aclextendedrule_list' + ) + protocol = ChoiceFieldColumn() + + class Meta(NetBoxTable.Meta): + model = ACLExtendedRule + fields = ( + 'pk', 'id', 'access_list', 'index', 'action', 'actions', 'remark', 'tags', 'description', + 'source_prefix', 'source_ports', 'destination_prefix', 'destination_ports', 'protocol' + ) + default_columns = ( + 'access_list', 'index', 'action', 'actions', 'remark', 'tags', + 'source_prefix', 'source_ports', 'destination_prefix', 'destination_ports', 'protocol' ) diff --git a/netbox_access_lists/template_content.py b/netbox_access_lists/template_content.py index 3fb339c..1f3bd7f 100644 --- a/netbox_access_lists/template_content.py +++ b/netbox_access_lists/template_content.py @@ -1,9 +1,14 @@ from django.contrib.contenttypes.models import ContentType - from extras.plugins import PluginTemplateExtension + from .models import AccessList +__all__ = ( + 'AccessLists', + 'DeviceAccessLists', +) + class AccessLists(PluginTemplateExtension): @@ -17,7 +22,7 @@ class AccessLists(PluginTemplateExtension): #elif ctype.model == 'virtualmachine': # access_lists = AccessList.objects.filter(device=obj.pk) - return self.render('inc/device_access_lists.html', extra_context={ + return self.render('inc/device/access_lists.html', extra_context={ 'access_lists': access_lists, 'type': ctype.model if ctype.model == 'device' else ctype.name.replace(' ', '_'), }) diff --git a/netbox_access_lists/templates/inc/device_access_lists.html b/netbox_access_lists/templates/inc/device/access_lists.html similarity index 87% rename from netbox_access_lists/templates/inc/device_access_lists.html rename to netbox_access_lists/templates/inc/device/access_lists.html index 074d028..7bdb9b9 100644 --- a/netbox_access_lists/templates/inc/device_access_lists.html +++ b/netbox_access_lists/templates/inc/device/access_lists.html @@ -3,7 +3,7 @@ Access Lists