diff --git a/.devcontainer/Dockerfile-plugin_dev b/.devcontainer/Dockerfile-plugin_dev
index 22c8134..442c6e7 100644
--- a/.devcontainer/Dockerfile-plugin_dev
+++ b/.devcontainer/Dockerfile-plugin_dev
@@ -1,6 +1,6 @@
ARG VARIANT=latest
-FROM tgenannt/netbox:${VARIANT}
+FROM netboxcommunity/netbox:${VARIANT}
ARG DEBIAN_FRONTEND=noninteractive
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index 47ed4f9..1614fcc 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -1,7 +1,7 @@
version: '3.4'
services:
netbox: &netbox
- image: tgenannt/netbox:${VARIANT-latest}
+ image: netboxcommunity/netbox:${VARIANT-latest}
depends_on:
- postgres
- redis
@@ -13,15 +13,15 @@ services:
- ./initializers:/opt/netbox/initializers:z,ro
- ./configuration:/etc/netbox/config:z,ro
#- netbox-media-files:/opt/netbox/netbox/media:z
- netbox-worker:
- <<: *netbox
- depends_on:
- - redis
- - postgres
- command:
- - /opt/netbox/venv/bin/python
- - /opt/netbox/netbox/manage.py
- - rqworker
+ #netbox-worker:
+ # <<: *netbox
+ # depends_on:
+ # - redis
+ # - postgres
+ # command:
+ # - /opt/netbox/venv/bin/python
+ # - /opt/netbox/netbox/manage.py
+ # - rqworker
#netbox-housekeeping:
# <<: *netbox
# depends_on:
diff --git a/.devcontainer/env/netbox.env b/.devcontainer/env/netbox.env
index ef8607a..65849ae 100644
--- a/.devcontainer/env/netbox.env
+++ b/.devcontainer/env/netbox.env
@@ -1,10 +1,12 @@
ALLOWED_HOSTS=*
-CORS_ORIGIN_ALLOW_ALL=True
+CORS_ORIGIN_ALLOW_ALL=true
DB_HOST=postgres
DB_NAME=netbox
DB_PASSWORD=J5brHrAXFLQSif0K
DB_USER=netbox
DEBUG=true
+ENFORCE_GLOBAL_UNIQUE=true
+LOGIN_REQUIRED=false
GRAPHQL_ENABLED=true
MAX_PAGE_SIZE=1000
MEDIA_ROOT=/opt/netbox/netbox/media
diff --git a/Makefile b/Makefile
index 2012c6b..86ce924 100644
--- a/Makefile
+++ b/Makefile
@@ -24,6 +24,11 @@ cleanup: ## Clean associated docker resources.
# in VS Code Devcontianer
+.PHONY: nbshell
+nbshell: ## Run nbshell
+ ${VENV_PY_PATH} ${NETBOX_MANAGE_PATH} nbshell
+ from netbox_access_lists.models import *
+
.PHONY: setup
setup: ## Copy plugin settings. Setup NetBox plugin.
-${VENV_PY_PATH} -m pip install --disable-pip-version-check --no-cache-dir -e ${REPO_PATH}
diff --git a/netbox_access_lists/api/nested_serializers.py b/netbox_access_lists/api/nested_serializers.py
index e12c648..a5910da 100644
--- a/netbox_access_lists/api/nested_serializers.py
+++ b/netbox_access_lists/api/nested_serializers.py
@@ -6,10 +6,12 @@ while Django itself handles the database abstraction.
from netbox.api.serializers import WritableNestedSerializer
from rest_framework import serializers
-from ..models import AccessList, ACLExtendedRule, ACLStandardRule
+from ..models import (AccessList, ACLExtendedRule, ACLInterfaceAssignment,
+ ACLStandardRule)
__all__ = [
'NestedAccessListSerializer',
+ 'NestedACLInterfaceAssignmentSerializer',
'NestedACLStandardRuleSerializer',
'NestedACLExtendedRuleSerializer'
]
@@ -30,6 +32,22 @@ class NestedAccessListSerializer(WritableNestedSerializer):
fields = ('id', 'url', 'display', 'name')
+class NestedACLInterfaceAssignmentSerializer(WritableNestedSerializer):
+ """
+ Defines the nested serializer for the django ACLInterfaceAssignment model & associates it to a view.
+ """
+ url = serializers.HyperlinkedIdentityField(
+ view_name='plugins-api:netbox_access_lists-api:aclinterfaceassignment-detail'
+ )
+
+ class Meta:
+ """
+ Associates the django model ACLInterfaceAssignment & fields to the nested serializer.
+ """
+ model = ACLInterfaceAssignment
+ fields = ('id', 'url', 'display', 'access_list')
+
+
class NestedACLStandardRuleSerializer(WritableNestedSerializer):
"""
Defines the nested serializer for the django ACLStandardRule model & associates it to a view.
diff --git a/netbox_access_lists/api/serializers.py b/netbox_access_lists/api/serializers.py
index fcc1495..efb6adb 100644
--- a/netbox_access_lists/api/serializers.py
+++ b/netbox_access_lists/api/serializers.py
@@ -6,23 +6,37 @@ while Django itself handles the database abstraction.
from django.contrib.contenttypes.models import ContentType
from drf_yasg.utils import swagger_serializer_method
from ipam.api.serializers import NestedPrefixSerializer
+from django.core.exceptions import ObjectDoesNotExist
from netbox.api import ContentTypeField
from netbox.api.serializers import NetBoxModelSerializer
from rest_framework import serializers
from utilities.api import get_serializer_for_model
-from ..constants import ACL_HOST_ASSIGNMENT_MODELS
-from ..models import AccessList, ACLExtendedRule, ACLStandardRule
+from ..constants import (ACL_HOST_ASSIGNMENT_MODELS,
+ ACL_INTERFACE_ASSIGNMENT_MODELS)
+from ..models import (AccessList, ACLExtendedRule, ACLInterfaceAssignment,
+ ACLStandardRule)
from .nested_serializers import (NestedAccessListSerializer,
NestedACLExtendedRuleSerializer,
+ NestedACLInterfaceAssignmentSerializer,
NestedACLStandardRuleSerializer)
__all__ = [
- 'NestedAccessListSerializer',
- 'NestedACLStandardRuleSerializer',
- 'NestedACLExtendedRuleSerializer'
+ 'AccessListSerializer',
+ 'ACLInterfaceAssignmentSerializer',
+ 'ACLStandardRuleSerializer',
+ 'ACLExtendedRuleSerializer'
]
+# Sets a standard error message for ACL rules with an action of remark, but no remark set.
+error_message_no_remark = 'Action is set to remark, you MUST add a remark.'
+# Sets a standard error message for ACL rules with an action of remark, but no source_prefix is set.
+error_message_action_remark_source_prefix_set = 'Action is set to remark, Source Prefix CANNOT be set.'
+# Sets a standard error message for ACL rules with an action not set to remark, but no remark is set.
+error_message_remark_without_action_remark = 'CANNOT set remark unless action is set to remark.'
+# Sets a standard error message for ACL rules no associated to an ACL of the same type.
+error_message_acl_type = 'Provided parent Access List is not of right type.'
+
class AccessListSerializer(NetBoxModelSerializer):
"""
@@ -55,16 +69,95 @@ class AccessListSerializer(NetBoxModelSerializer):
def validate(self, data):
"""
- Validate the AccessList django model model's inputs before allowing it to update the instance.
+ Validates api inputs before processing:
+ - Check that the GFK object is valid.
+ - Check if Access List has no existing rules before change the Access List's type.
"""
- if self.instance.rule_count > 0:
- raise serializers.ValidationError({
- 'type': 'This ACL has ACL rules already associated, CANNOT change ACL type!!'
- })
+ error_message = {}
+
+ # Check that the GFK object is valid.
+ if 'assigned_object_type' in data and 'assigned_object_id' in data:
+ try:
+ assigned_object = data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
+ except ObjectDoesNotExist:
+ # Sets a standard error message for invalid GFK
+ error_message_invalid_gfk = f"Invalid assigned_object {data['assigned_object_type']} ID {data['assigned_object_id']}"
+ error_message['assigned_object_type'] = [error_message_invalid_gfk]
+ error_message['assigned_object_id'] = [error_message_invalid_gfk]
+
+ # Check if Access List has no existing rules before change the Access List's type.
+ if self.instance and self.instance.type != data.get('type') and self.instance.rule_count > 0:
+ error_message['type'] = ['This ACL has ACL rules associated, CANNOT change ACL type.']
+
+ if error_message:
+ raise serializers.ValidationError(error_message)
return super().validate(data)
+class ACLInterfaceAssignmentSerializer(NetBoxModelSerializer):
+ """
+ Defines the serializer for the django ACLInterfaceAssignment model & associates it to a view.
+ """
+ url = serializers.HyperlinkedIdentityField(
+ view_name='plugins-api:netbox_access_lists-api:aclinterfaceassignment-detail'
+ )
+ assigned_object_type = ContentTypeField(
+ queryset=ContentType.objects.filter(ACL_INTERFACE_ASSIGNMENT_MODELS)
+ )
+ assigned_object = serializers.SerializerMethodField(read_only=True)
+
+ class Meta:
+ """
+ Associates the django model ACLInterfaceAssignment & fields to the serializer.
+ """
+ model = ACLInterfaceAssignment
+ fields = (
+ 'id', 'url', 'access_list', 'direction', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'comments', 'tags', 'custom_fields', 'created',
+ 'last_updated'
+ )
+
+ @swagger_serializer_method(serializer_or_field=serializers.DictField)
+ def get_assigned_object(self, obj):
+ serializer = get_serializer_for_model(obj.assigned_object, prefix='Nested')
+ context = {'request': self.context['request']}
+ return serializer(obj.assigned_object, context=context).data
+
+ def validate(self, data):
+ """
+ Validate the AccessList django model model's inputs before allowing it to update the instance.
+ - Check that the GFK object is valid.
+ - Check that the associated interface's parent host has the selected ACL defined.
+ """
+ error_message = {}
+ acl_host = data['access_list'].assigned_object
+
+ # Check that the GFK object is vlaid.
+ if 'assigned_object_type' in data and 'assigned_object_id' in data:
+ try:
+ assigned_object = data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
+ except ObjectDoesNotExist:
+ # Sets a standard error message for invalid GFK
+ error_message_invalid_gfk = f"Invalid assigned_object {data['assigned_object_type']} ID {data['assigned_object_id']}"
+ error_message['assigned_object_type'] = [error_message_invalid_gfk]
+ error_message['assigned_object_id'] = [error_message_invalid_gfk]
+
+
+ if data['assigned_object_type'].model == 'interface':
+ interface_host = data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id']).device
+ elif data['assigned_object_type'].model == 'vminterface':
+ interface_host = data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id']).virtual_machine
+ # Check that the associated interface's parent host has the selected ACL defined.
+ if acl_host != interface_host:
+ error_acl_not_assigned_to_host = "Access List not present on the selected interface's host."
+ error_message['access_list'] = [error_acl_not_assigned_to_host]
+ error_message['assigned_object_id'] = [error_acl_not_assigned_to_host]
+
+ if error_message:
+ raise serializers.ValidationError(error_message)
+
+ return super().validate(data)
+
class ACLStandardRuleSerializer(NetBoxModelSerializer):
"""
Defines the serializer for the django ACLStandardRule model & associates it to a view.
@@ -73,7 +166,11 @@ class ACLStandardRuleSerializer(NetBoxModelSerializer):
view_name='plugins-api:netbox_access_lists-api:aclstandardrule-detail'
)
access_list = NestedAccessListSerializer()
- source_prefix = NestedPrefixSerializer()
+ source_prefix = NestedPrefixSerializer(
+ required=False,
+ allow_null=True,
+ default=None
+ )
class Meta:
"""
@@ -82,18 +179,26 @@ class ACLStandardRuleSerializer(NetBoxModelSerializer):
model = ACLStandardRule
fields = (
'id', 'url', 'display', 'access_list', 'index', 'action', 'tags', 'description',
- 'created', 'custom_fields', 'last_updated', 'source_prefix'
+ 'remark', '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.
+ Validate the ACLStandardRule django model model's inputs before allowing it to update the instance:
+ - Check if action set to remark, but no remark set.
+ - Check if action set to remark, but source_prefix set.
"""
- 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!!'
- })
+ error_message = {}
+
+ # Check if action set to remark, but no remark set.
+ if data.get('action') == 'remark' and data.get('remark') is None:
+ error_message['remark'] = [error_message_no_remark]
+ # Check if action set to remark, but source_prefix set.
+ if data.get('source_prefix'):
+ error_message['source_prefix'] = [error_message_action_remark_source_prefix_set]
+
+ if error_message:
+ raise serializers.ValidationError(error_message)
return super().validate(data)
@@ -106,8 +211,16 @@ class ACLExtendedRuleSerializer(NetBoxModelSerializer):
view_name='plugins-api:netbox_access_lists-api:aclextendedrule-detail'
)
access_list = NestedAccessListSerializer()
- source_prefix = NestedPrefixSerializer()
- destination_prefix = NestedPrefixSerializer()
+ source_prefix = NestedPrefixSerializer(
+ required=False,
+ allow_null=True,
+ default=None
+ )
+ destination_prefix = NestedPrefixSerializer(
+ required=False,
+ allow_null=True,
+ default=None
+ )
class Meta:
"""
@@ -117,17 +230,42 @@ class ACLExtendedRuleSerializer(NetBoxModelSerializer):
fields = (
'id', 'url', 'display', 'access_list', 'index', 'action', 'tags', 'description',
'created', 'custom_fields', 'last_updated', 'source_prefix', 'source_ports',
- 'destination_prefix', 'destination_ports', 'protocol'
+ 'destination_prefix', 'destination_ports', 'protocol', 'remark',
)
def validate(self, data):
"""
- Validate the ACLExtendedRule django model model's inputs before allowing it to update the instance.
+ Validate the ACLExtendedRule django model model's inputs before allowing it to update the instance:
+ - Check if action set to remark, but no remark set.
+ - Check if action set to remark, but source_prefix set.
+ - Check if action set to remark, but source_ports set.
+ - Check if action set to remark, but destination_prefix set.
+ - Check if action set to remark, but destination_ports set.
+ - Check if action set to remark, but protocol set.
+ - Check if action set to remark, but protocol set.
"""
- 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!!'
- })
+ error_message = {}
+
+ # Check if action set to remark, but no remark set.
+ if data.get('action') == 'remark' and data.get('remark') is None:
+ error_message['remark'] = [error_message_no_remark]
+ # Check if action set to remark, but source_prefix set.
+ if data.get('source_prefix'):
+ error_message['source_prefix'] = [error_message_action_remark_source_prefix_set]
+ # Check if action set to remark, but source_ports set.
+ if data.get('source_ports'):
+ error_message['source_ports'] = ['Action is set to remark, Source Ports CANNOT be set.']
+ # Check if action set to remark, but destination_prefix set.
+ if data.get('destination_prefix'):
+ error_message['destination_prefix'] = ['Action is set to remark, Destination Prefix CANNOT be set.']
+ # Check if action set to remark, but destination_ports set.
+ if data.get('destination_ports'):
+ error_message['destination_ports'] = ['Action is set to remark, Destination Ports CANNOT be set.']
+ # Check if action set to remark, but protocol set.
+ if data.get('protocol'):
+ error_message['protocol'] = ['Action is set to remark, Protocol CANNOT be set.']
+
+ if error_message:
+ raise serializers.ValidationError(error_message)
return super().validate(data)
diff --git a/netbox_access_lists/api/urls.py b/netbox_access_lists/api/urls.py
index 20fb28b..f38d1d9 100644
--- a/netbox_access_lists/api/urls.py
+++ b/netbox_access_lists/api/urls.py
@@ -10,6 +10,7 @@ app_name = 'netbox_access_list'
router = NetBoxRouter()
router.register('access-lists', views.AccessListViewSet)
+router.register('interface-assignments', views.ACLInterfaceAssignmentViewSet)
router.register('standard-acl-rules', views.ACLStandardRuleViewSet)
router.register('extended-acl-rules', views.ACLExtendedRuleViewSet)
diff --git a/netbox_access_lists/api/views.py b/netbox_access_lists/api/views.py
index b2c9056..d4bd19b 100644
--- a/netbox_access_lists/api/views.py
+++ b/netbox_access_lists/api/views.py
@@ -9,11 +9,13 @@ from netbox.api.viewsets import NetBoxModelViewSet
from .. import filtersets, models
from .serializers import (AccessListSerializer, ACLExtendedRuleSerializer,
+ ACLInterfaceAssignmentSerializer,
ACLStandardRuleSerializer)
__all__ = [
'AccessListViewSet',
'ACLStandardRuleViewSet',
+ 'ACLInterfaceAssignmentViewSet'
'ACLExtendedRuleViewSet',
]
@@ -29,6 +31,15 @@ class AccessListViewSet(NetBoxModelViewSet):
filterset_class = filtersets.AccessListFilterSet
+class ACLInterfaceAssignmentViewSet(NetBoxModelViewSet):
+ """
+ Defines the view set for the django ACLInterfaceAssignment model & associates it to a view.
+ """
+ queryset = models.ACLInterfaceAssignment.objects.prefetch_related('access_list', 'tags')
+ serializer_class = ACLInterfaceAssignmentSerializer
+ filterset_class = filtersets.ACLInterfaceAssignmentFilterSet
+
+
class ACLStandardRuleViewSet(NetBoxModelViewSet):
"""
Defines the view set for the django ACLStandardRule model & associates it to a view.
diff --git a/netbox_access_lists/choices.py b/netbox_access_lists/choices.py
index 086c665..6531e9b 100644
--- a/netbox_access_lists/choices.py
+++ b/netbox_access_lists/choices.py
@@ -6,6 +6,8 @@ from utilities.choices import ChoiceSet
__all__ = (
'ACLActionChoices',
+ 'ACLAssignmentDirectionChoices',
+ 'ACLProtocolChoices',
'ACLRuleActionChoices',
'ACLTypeChoices',
'ACLProtocolChoices',
@@ -42,6 +44,17 @@ class ACLRuleActionChoices(ChoiceSet):
]
+class ACLAssignmentDirectionChoices(ChoiceSet):
+ """
+ Defines the direction of the application of the ACL on an associated interface.
+ """
+
+ CHOICES = [
+ ('ingress', 'Ingress', 'blue'),
+ ('egress', 'Egress', 'purple'),
+ ]
+
+
class ACLTypeChoices(ChoiceSet):
"""
Defines the choices availble for the Access Lists plugin specific to ACL type.
diff --git a/netbox_access_lists/filtersets.py b/netbox_access_lists/filtersets.py
index 4a87988..d03b16e 100644
--- a/netbox_access_lists/filtersets.py
+++ b/netbox_access_lists/filtersets.py
@@ -3,15 +3,16 @@ Filters enable users to request only a specific subset of objects matching a que
when filtering the sites list by status or region, for instance.
"""
import django_filters
-from dcim.models import Device, VirtualChassis
+from dcim.models import Device, Interface, VirtualChassis
from netbox.filtersets import NetBoxModelFilterSet
-from virtualization.models import VirtualMachine
+from virtualization.models import VirtualMachine, VMInterface
from .models import *
__all__ = (
'AccessListFilterSet',
'ACLStandardRuleFilterSet',
+ 'ACLInterfaceAssignmentFilterSet',
'ACLExtendedRuleFilterSet',
)
@@ -68,6 +69,47 @@ class AccessListFilterSet(NetBoxModelFilterSet):
return queryset.filter(description__icontains=value)
+class ACLInterfaceAssignmentFilterSet(NetBoxModelFilterSet):
+ """
+ Define the filter set for the django model ACLInterfaceAssignment.
+ """
+ interface = django_filters.ModelMultipleChoiceFilter(
+ field_name='interface__name',
+ queryset=Interface.objects.all(),
+ to_field_name='name',
+ label='Interface (name)',
+ )
+ interface_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='interface',
+ queryset=Interface.objects.all(),
+ label='Interface (ID)',
+ )
+ vminterface = django_filters.ModelMultipleChoiceFilter(
+ field_name='vminterface__name',
+ queryset=VMInterface.objects.all(),
+ to_field_name='name',
+ label='VM Interface (name)',
+ )
+ vminterface_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='vminterface',
+ queryset=VMInterface.objects.all(),
+ label='VM Interface (ID)',
+ )
+
+ class Meta:
+ """
+ Associates the django model ACLInterfaceAssignment & fields to the filter set.
+ """
+ model = ACLInterfaceAssignment
+ fields = ('id', 'access_list', 'direction', 'interface', 'interface_id', 'vminterface', 'vminterface_id')
+
+ def search(self, queryset, name, value):
+ """
+ Override the default search behavior for the django model.
+ """
+ return queryset.filter(description__icontains=value)
+
+
class ACLStandardRuleFilterSet(NetBoxModelFilterSet):
"""
Define the filter set for the django model ACLStandardRule.
diff --git a/netbox_access_lists/forms/bulk_edit.py b/netbox_access_lists/forms/bulk_edit.py
index 07403d9..bf9d72d 100644
--- a/netbox_access_lists/forms/bulk_edit.py
+++ b/netbox_access_lists/forms/bulk_edit.py
@@ -77,7 +77,7 @@ from ..models import AccessList
# return cleaned_data
# name = cleaned_data.get('name')
# device = cleaned_data.get('device')
-# type = cleaned_data.get('type')
+# 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():
diff --git a/netbox_access_lists/forms/filtersets.py b/netbox_access_lists/forms/filtersets.py
index 074f1cc..96d09b9 100644
--- a/netbox_access_lists/forms/filtersets.py
+++ b/netbox_access_lists/forms/filtersets.py
@@ -2,21 +2,25 @@
Defines each django model's GUI filter/search options.
"""
-from dcim.models import Device, Region, Site, SiteGroup, VirtualChassis
+from dcim.models import (Device, Interface, Region, Site, SiteGroup,
+ VirtualChassis)
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 virtualization.models import VirtualMachine
+from virtualization.models import VirtualMachine, VMInterface
-from ..choices import (ACLActionChoices, ACLProtocolChoices,
- ACLRuleActionChoices, ACLTypeChoices)
-from ..models import AccessList, ACLExtendedRule, ACLStandardRule
+from ..choices import (ACLActionChoices, ACLAssignmentDirectionChoices,
+ ACLProtocolChoices, ACLRuleActionChoices,
+ ACLTypeChoices)
+from ..models import (AccessList, ACLExtendedRule, ACLInterfaceAssignment,
+ ACLStandardRule)
__all__ = (
'AccessListFilterForm',
+ 'ACLInterfaceAssignmentFilterForm',
'ACLStandardRuleFilterForm',
'ACLExtendedRuleFilterForm',
)
@@ -71,6 +75,75 @@ class AccessListFilterForm(NetBoxModelFilterSetForm):
)
+class ACLInterfaceAssignmentFilterForm(NetBoxModelFilterSetForm):
+ """
+ GUI filter form to search the django AccessList model.
+ """
+ model = ACLInterfaceAssignment
+ 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
+ )
+ interface = DynamicModelChoiceField(
+ queryset=Interface.objects.all(),
+ required=False,
+ query_params={
+ 'device_id': '$device'
+ }
+ )
+ virtual_machine = DynamicModelChoiceField(
+ queryset=VirtualMachine.objects.all(),
+ required=False,
+ label='Virtual Machine',
+ )
+ vminterface = DynamicModelChoiceField(
+ queryset=VMInterface.objects.all(),
+ required=False,
+ query_params={
+ 'virtual_machine_id': '$virtual_machine'
+ },
+ label='Interface'
+ )
+ access_list = DynamicModelChoiceField(
+ queryset=AccessList.objects.all(),
+ query_params={
+ 'assigned_object': '$device',
+ },
+ label='Access List',
+ )
+ direction = ChoiceField(
+ choices=add_blank_choice(ACLAssignmentDirectionChoices),
+ required=False,
+ initial='',
+ widget=StaticSelect(),
+ )
+ 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.
diff --git a/netbox_access_lists/forms/models.py b/netbox_access_lists/forms/models.py
index 66cd728..7a479bc 100644
--- a/netbox_access_lists/forms/models.py
+++ b/netbox_access_lists/forms/models.py
@@ -2,26 +2,41 @@
Defines each django model's GUI form to add or edit objects for each django model.
"""
-from dcim.models import Device, Region, Site, SiteGroup, VirtualChassis
+from dcim.models import (Device, Interface, Region, Site, SiteGroup,
+ VirtualChassis)
from django import forms
+from django.contrib.contenttypes.models import ContentType
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 virtualization.models import VirtualMachine
+from virtualization.models import VirtualMachine, VMInterface
-from ..models import AccessList, ACLExtendedRule, ACLStandardRule
+from ..models import (AccessList, ACLExtendedRule, ACLInterfaceAssignment,
+ ACLStandardRule)
__all__ = (
'AccessListForm',
+ 'ACLInterfaceAssignmentForm',
'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.')
+# Sets a standard mark_safe help_text value to be used by the various classes
+help_text_acl_rule_logic = mark_safe('*Note: CANNOT be set if action is set to remark.')
+# Sets a standard help_text value to be used by the various classes for acl action
+help_text_acl_action = 'Action the rule will take (remark, deny, or allow).'
+# Sets a standard help_text value to be used by the various classes for acl index
+help_text_acl_rule_index = 'Determines the order of the rule in the ACL processing. AKA Sequence Number.'
+
+# Sets a standard error message for ACL rules with an action of remark, but no remark set.
+error_message_no_remark = 'Action is set to remark, you MUST add a remark.'
+# Sets a standard error message for ACL rules with an action of remark, but no source_prefix is set.
+error_message_action_remark_source_prefix_set = 'Action is set to remark, Source Prefix CANNOT be set.'
+# Sets a standard error message for ACL rules with an action not set to remark, but no remark is set.
+error_message_remark_without_action_remark = 'CANNOT set remark unless action is set to remark.'
class AccessListForm(NetBoxModelForm):
@@ -94,47 +109,50 @@ class AccessListForm(NetBoxModelForm):
def clean(self):
"""
- Validates form inputs before submitting.
+ Validates form inputs before submitting:
+ - Check if more than one host type selected.
+ - Check if no hosts selected.
+ - Check if duplicate entry. (Because of GFK.)
+ - Check if Access List has no existing rules before change the Access List's type.
"""
cleaned_data = super().clean()
error_message = {}
if self.errors.get('name'):
return cleaned_data
name = cleaned_data.get('name')
- type = cleaned_data.get('type')
+ acl_type = cleaned_data.get('type')
device = cleaned_data.get('device')
virtual_chassis = cleaned_data.get('virtual_chassis')
virtual_machine = cleaned_data.get('virtual_machine')
+
+
+ # Check if more than one host type selected.
if (device and virtual_chassis) or (device and virtual_machine) or (virtual_chassis and virtual_machine):
- raise forms.ValidationError('Access Lists must be assigned to one host (either a device, virtual chassis or virtual machine).')
+ raise forms.ValidationError('Access Lists must be assigned to one host (either a device, virtual chassis or virtual machine) at a time.')
+ # Check if no hosts selected.
if not device and not virtual_chassis and not virtual_machine:
raise forms.ValidationError('Access Lists must be assigned to a device, virtual chassis or virtual machine.')
- if ('name' in self.changed_data or 'device' in self.changed_data) and device and AccessList.objects.filter(name__iexact=name, device=device).exists():
- error_message.update(
- {
- 'device': ['An ACL with this name (case insensitive) is already associated to this host.'],
- 'name': ['An ACL with this name (case insensitive) is already associated to this host.'],
- }
- )
- if ('name' in self.changed_data or 'virtual_chassis' in self.changed_data) and virtual_chassis and AccessList.objects.filter(name__iexact=name, virtual_chassis=virtual_chassis).exists():
- error_message.update(
- {
- 'virtual_chassis': ['An ACL with this name (case insensitive) is already associated to this host.'],
- 'name': ['An ACL with this name (case insensitive) is already associated to this host.'],
- }
- )
- if ('name' in self.changed_data or 'virtual_machine' in self.changed_data) and virtual_machine and AccessList.objects.filter(name__iexact=name, virtual_machine=virtual_machine).exists():
- error_message.update(
- {
- 'virtual_machine': ['An ACL with this name (case insensitive) is already associated to this host.'],
- 'name': ['An ACL with this name (case insensitive) is already associated to this host.'],
- }
- )
- 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:
+
+ if device:
+ host_type = 'device'
+ existing_acls = AccessList.objects.filter(name=name, device=device).exists()
+ elif virtual_chassis:
+ host_type = 'virtual_chassis'
+ existing_acls = AccessList.objects.filter(name=name, virtual_chassis=virtual_chassis).exists()
+ elif virtual_machine:
+ host_type = 'virtual_machine'
+ existing_acls = AccessList.objects.filter(name=name, virtual_machine=virtual_machine).exists()
+ host = cleaned_data.get(host_type)
+
+ # Check if duplicate entry.
+ if ('name' in self.changed_data or host_type in self.changed_data) and existing_acls:
+ error_same_acl_name = 'An ACL with this name is already associated to this host.'
+ error_message |= {host_type: [error_same_acl_name], 'name': [error_same_acl_name]}
+ # Check if Access List has no existing rules before change the Access List's type.
+ if (acl_type == 'extended' and self.instance.aclstandardrules.exists()) or (acl_type == 'standard' and self.instance.aclextendedrules.exists()):
+ error_message['type'] = ['This ACL has ACL rules associated, CANNOT change ACL type.']
+
+ if error_message:
raise forms.ValidationError(error_message)
return cleaned_data
@@ -146,6 +164,150 @@ class AccessListForm(NetBoxModelForm):
return super().save(*args, **kwargs)
+class ACLInterfaceAssignmentForm(NetBoxModelForm):
+ """
+ GUI form to add or edit ACL Host Object assignments
+ Requires an access_list, a name, a type, and a default_action.
+ """
+ device = DynamicModelChoiceField(
+ queryset=Device.objects.all(),
+ required=False,
+ query_params={
+ # Need to pass ACL device to it
+ },
+ )
+ interface = DynamicModelChoiceField(
+ queryset=Interface.objects.all(),
+ required=False,
+ query_params={
+ 'device_id': '$device'
+ }
+ )
+ virtual_machine = DynamicModelChoiceField(
+ queryset=VirtualMachine.objects.all(),
+ required=False,
+ label='Virtual Machine',
+ )
+ vminterface = DynamicModelChoiceField(
+ queryset=VMInterface.objects.all(),
+ required=False,
+ query_params={
+ 'virtual_machine_id': '$virtual_machine'
+ },
+ label='VM Interface'
+ )
+ #virtual_chassis = DynamicModelChoiceField(
+ # queryset=VirtualChassis.objects.all(),
+ # required=False,
+ # label='Virtual Chassis',
+ #)
+ access_list = DynamicModelChoiceField(
+ queryset=AccessList.objects.all(),
+ #query_params={
+ # 'assigned_object': '$device',
+ # 'assigned_object': '$virtual_machine',
+ #},
+ label='Access List',
+ help_text=mark_safe('*Note: Access List must be present on the device already.')
+ )
+ comments = CommentField()
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
+ required=False
+ )
+
+ def __init__(self, *args, **kwargs):
+
+ # Initialize helper selectors
+ instance = kwargs.get('instance')
+ initial = kwargs.get('initial', {}).copy()
+ if instance:
+ if type(instance.assigned_object) is Interface:
+ initial['interface'] = instance.assigned_object
+ initial['device'] = 'device'
+ elif type(instance.assigned_object) is VMInterface:
+ initial['vminterface'] = instance.assigned_object
+ initial['virtual_machine'] = 'virtual_machine'
+ kwargs['initial'] = initial
+
+ super().__init__(*args, **kwargs)
+
+
+ class Meta:
+ model = ACLInterfaceAssignment
+ fields = (
+ 'access_list', 'direction', 'device', 'interface', 'virtual_machine',
+ 'vminterface', 'comments', 'tags',
+ )
+ help_texts = {
+ 'direction': mark_safe('*Note: CANNOT assign 2 ACLs to the same interface & direction.'),
+ }
+
+ def clean(self):
+ """
+ Validates form inputs before submitting:
+ - Check if both interface and vminterface are set.
+ - Check if neither interface or vminterface are set.
+ - Check that an interface's parent device/virtual_machine is assigned to the Access List.
+ - Check that an interface's parent device/virtual_machine is assigned to the Access List.
+ - Check for duplicate entry. (Because of GFK)
+ - Check that the interface does not have an existing ACL applied in the direction already.
+ """
+ cleaned_data = super().clean()
+ error_message = {}
+ access_list = cleaned_data.get('access_list')
+ direction = cleaned_data.get('direction')
+ interface = cleaned_data.get('interface')
+ vminterface = cleaned_data.get('vminterface')
+ assigned_object = cleaned_data.get('assigned_object')
+ if interface:
+ assigned_object = interface
+ assigned_object_type = 'interface'
+ host_type = 'device'
+ host = Interface.objects.get(pk=assigned_object.pk).device
+ elif vminterface:
+ assigned_object = vminterface
+ assigned_object_type = 'vminterface'
+ host_type = 'virtual_machine'
+ host = VMInterface.objects.get(pk=assigned_object.pk).virtual_machine
+ if interface or vminterface:
+ assigned_object_id = VMInterface.objects.get(pk=assigned_object.pk).pk
+ assigned_object_type_id = ContentType.objects.get_for_model(assigned_object).pk
+ access_list_host = AccessList.objects.get(pk=access_list.pk).assigned_object
+
+ # Check if both interface and vminterface are set.
+ if interface and vminterface:
+ error_too_many_interfaces = 'Access Lists must be assigned to one type of interface at a time (VM interface or physical interface)'
+ error_too_many_hosts = 'Access Lists must be assigned to one type of device at a time (VM or physical device).'
+ error_message |= {'device': [error_too_many_hosts], 'interface': [error_too_many_interfaces], 'virtual_machine': [error_too_many_hosts], 'vminterface': [error_too_many_interfaces]}
+ # Check if neither interface or vminterface are set.
+ elif not (interface or vminterface):
+ error_no_interface = 'An Access List assignment but specify an Interface or VM Interface.'
+ error_message |= {'interface': [error_no_interface], 'vminterface': [error_no_interface]}
+ # Check that an interface's parent device/virtual_machine is assigned to the Access List.
+ elif access_list_host != host:
+ error_acl_not_assigned_to_host = 'Access List not present on selected host.'
+ error_message |= {'access_list': [error_acl_not_assigned_to_host], assigned_object_type: [error_acl_not_assigned_to_host], host_type: [error_acl_not_assigned_to_host]}
+ # Check for duplicate entry.
+ elif ACLInterfaceAssignment.objects.filter(access_list=access_list, assigned_object_id=assigned_object_id, assigned_object_type=assigned_object_type_id, direction=direction).exists():
+ error_duplicate_entry = 'An ACL with this name is already associated to this interface & direction.'
+ error_message |= {'access_list': [error_duplicate_entry], 'direction': [error_duplicate_entry], assigned_object_type: [error_duplicate_entry]}
+ # Check that the interface does not have an existing ACL applied in the direction already.
+ elif ACLInterfaceAssignment.objects.filter(assigned_object_id=assigned_object_id, assigned_object_type=assigned_object_type_id, direction=direction).exists():
+ error_interface_already_assigned = 'Interfaces can only have 1 Access List assigned in each direction.'
+ error_message |= {'direction': [error_interface_already_assigned], assigned_object_type: [error_interface_already_assigned]}
+
+ if error_message:
+ raise forms.ValidationError(error_message)
+ return cleaned_data
+
+ def save(self, *args, **kwargs):
+ # Set assigned object
+ self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
+
+ return super().save(*args, **kwargs)
+
+
class ACLStandardRuleForm(NetBoxModelForm):
"""
GUI form to add or edit Standard Access List.
@@ -163,7 +325,7 @@ class ACLStandardRuleForm(NetBoxModelForm):
source_prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
- help_text=acl_rule_logic_help,
+ help_text=help_text_acl_rule_logic,
label='Source Prefix',
)
tags = DynamicModelMultipleChoiceField(
@@ -183,27 +345,35 @@ class ACLStandardRuleForm(NetBoxModelForm):
'tags', 'description'
)
help_texts = {
- 'index': 'Determines the order of the rule in the ACL processing.',
+ 'index': help_text_acl_rule_index,
+ 'action': help_text_acl_action,
'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.
+ Validates form inputs before submitting:
+ - Check if action set to remark, but no remark set.
+ - Check if action set to remark, but source_prefix set.
+ - Check remark set, but action not set to remark.
"""
cleaned_data = super().clean()
error_message = {}
+
+ # No need to check for unique_together since there is no usage of GFK
+
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.']})
+ # Check if action set to remark, but no remark set.
+ if not cleaned_data.get('remark'):
+ error_message['remark'] = [error_message_no_remark]
+ # Check if action set to remark, but source_prefix set.
if cleaned_data.get('source_prefix'):
- error_message.update({'source_prefix': ['Action is set to remark, Source Prefix CANNOT be set.']})
+ error_message['source_prefix'] = [error_message_action_remark_source_prefix_set]
+ # Check remark set, but action not set to remark.
elif cleaned_data.get('remark'):
- error_message.update({'remark': ['CANNOT set remark unless action is set to remark, .']})
- if len(error_message) > 0:
+ error_message['remark'] = [error_message_remark_without_action_remark]
+
+ if error_message:
raise forms.ValidationError(error_message)
return cleaned_data
@@ -229,13 +399,13 @@ class ACLExtendedRuleForm(NetBoxModelForm):
source_prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
- help_text=acl_rule_logic_help,
+ help_text=help_text_acl_rule_logic,
label='Source Prefix',
)
destination_prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
- help_text=acl_rule_logic_help,
+ help_text=help_text_acl_rule_logic,
label='Destination Prefix',
)
fieldsets = (
@@ -251,37 +421,54 @@ class ACLExtendedRuleForm(NetBoxModelForm):
'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,
+ 'action': help_text_acl_action,
+ 'destination_ports': help_text_acl_rule_logic,
+ 'index': help_text_acl_rule_index,
+ 'protocol': help_text_acl_rule_logic,
'remark': mark_safe('*Note: CANNOT be set if action is not set to remark.'),
- 'source_ports': acl_rule_logic_help,
+ 'source_ports': help_text_acl_rule_logic,
}
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.
+ Validates form inputs before submitting:
+ - Check if action set to remark, but no remark set.
+ - Check if action set to remark, but source_prefix set.
+ - Check if action set to remark, but source_ports set.
+ - Check if action set to remark, but destination_prefix set.
+ - Check if action set to remark, but destination_ports set.
+ - Check if action set to remark, but destination_ports set.
+ - Check if action set to remark, but protocol set.
+ - Check remark set, but action not set to remark.
"""
cleaned_data = super().clean()
error_message = {}
+
+ # No need to check for unique_together since there is no usage of GFK
+
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.']})
+ # Check if action set to remark, but no remark set.
+ if not cleaned_data.get('remark'):
+ error_message['remark'] = [error_message_no_remark]
+ # Check if action set to remark, but source_prefix set.
if cleaned_data.get('source_prefix'):
- error_message.update({'source_prefix': ['Action is set to remark, Source Prefix CANNOT be set.']})
+ error_message['source_prefix'] = [error_message_action_remark_source_prefix_set]
+ # Check if action set to remark, but source_ports set.
if cleaned_data.get('source_ports'):
- error_message.update({'source_ports': ['Action is set to remark, Source Ports CANNOT be set.']})
+ error_message['source_ports'] = ['Action is set to remark, Source Ports CANNOT be set.']
+ # Check if action set to remark, but destination_prefix set.
if cleaned_data.get('destination_prefix'):
- error_message.update({'destination_prefix': ['Action is set to remark, Destination Prefix CANNOT be set.']})
+ error_message['destination_prefix'] = ['Action is set to remark, Destination Prefix CANNOT be set.']
+ # Check if action set to remark, but destination_ports set.
if cleaned_data.get('destination_ports'):
- error_message.update({'destination_ports': ['Action is set to remark, Destination Ports CANNOT be set.']})
+ error_message['destination_ports'] = ['Action is set to remark, Destination Ports CANNOT be set.']
+ # Check if action set to remark, but protocol set.
if cleaned_data.get('protocol'):
- error_message.update({'protocol': ['Action is set to remark, Protocol CANNOT be set.']})
+ error_message['protocol'] = ['Action is set to remark, Protocol CANNOT be set.']
+ # Check if action not set to remark, but remark set.
elif cleaned_data.get('remark'):
- error_message.update({'remark': ['CANNOT set remark unless action is set to remark, .']})
- if len(error_message) > 0:
+ error_message['remark'] = [error_message_remark_without_action_remark]
+
+ if error_message:
raise forms.ValidationError(error_message)
return cleaned_data
diff --git a/netbox_access_lists/graphql.py b/netbox_access_lists/graphql.py
index 3c410e4..e1eac6d 100644
--- a/netbox_access_lists/graphql.py
+++ b/netbox_access_lists/graphql.py
@@ -10,6 +10,7 @@ from . import filtersets, models
__all__ = (
'AccessListType',
+ 'ACLInterfaceAssignmentType',
'ACLExtendedRuleType',
'ACLStandardRuleType',
)
@@ -33,6 +34,20 @@ class AccessListType(NetBoxObjectType):
filterset_class = filtersets.AccessListFilterSet
+class ACLInterfaceAssignmentType(NetBoxObjectType):
+ """
+ Defines the object type for the django model AccessList.
+ """
+
+ class Meta:
+ """
+ Associates the filterset, fields, and model for the django model ACLInterfaceAssignment.
+ """
+ model = models.ACLInterfaceAssignment
+ fields = '__all__'
+ filterset_class = filtersets.ACLInterfaceAssignmentFilterSet
+
+
class ACLExtendedRuleType(NetBoxObjectType):
"""
Defines the object type for the django model ACLExtendedRule.
diff --git a/netbox_access_lists/migrations/0001_initial.py b/netbox_access_lists/migrations/0001_initial.py
index 156d445..60e10ac 100644
--- a/netbox_access_lists/migrations/0001_initial.py
+++ b/netbox_access_lists/migrations/0001_initial.py
@@ -32,7 +32,7 @@ class Migration(migrations.Migration):
('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)),
- ('name', models.CharField(max_length=100)),
+ ('name', models.CharField(max_length=500)),
('assigned_object_id', models.PositiveIntegerField()),
('assigned_object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')),
('type', models.CharField(max_length=100)),
@@ -46,6 +46,26 @@ class Migration(migrations.Migration):
'verbose_name': 'Access List',
},
),
+ migrations.CreateModel(
+ name='ACLInterfaceAssignment',
+ 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)),
+ ('access_list', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='aclinterfaceassignment', to='netbox_access_lists.accesslist')),
+ ('direction', models.CharField(max_length=100)),
+ ('assigned_object_id', models.PositiveIntegerField()),
+ ('assigned_object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')),
+ ('comments', models.TextField(blank=True)),
+ ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+ ],
+ options={
+ 'ordering': ('access_list', 'assigned_object_type', 'assigned_object_id', 'direction'),
+ 'unique_together': {('assigned_object_type', 'assigned_object_id', 'access_list', 'direction')},
+ 'verbose_name': 'ACL Interface Assignment',
+ },
+ ),
migrations.CreateModel(
name='ACLStandardRule',
fields=[
diff --git a/netbox_access_lists/models/access_list_rules.py b/netbox_access_lists/models/access_list_rules.py
index 79ec008..7ac017d 100644
--- a/netbox_access_lists/models/access_list_rules.py
+++ b/netbox_access_lists/models/access_list_rules.py
@@ -26,12 +26,12 @@ class ACLRule(NetBoxModel):
on_delete=models.CASCADE,
to=AccessList,
verbose_name='Access List',
+ related_name='rules',
)
index = models.PositiveIntegerField()
remark = models.CharField(
- max_length=200,
- blank=True,
- null=True
+ max_length=500,
+ blank=True
)
description = models.CharField(
max_length=500,
@@ -72,6 +72,13 @@ class ACLStandardRule(ACLRule):
"""
Inherits ACLRule.
"""
+ access_list = models.ForeignKey(
+ on_delete=models.CASCADE,
+ to=AccessList,
+ verbose_name='Standard Access List',
+ limit_choices_to={'type': 'standard'},
+ related_name='aclstandardrules',
+ )
def get_absolute_url(self):
"""
@@ -87,7 +94,6 @@ class ACLStandardRule(ACLRule):
- 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'
@@ -96,6 +102,13 @@ class ACLExtendedRule(ACLRule):
Inherits ACLRule.
Add ACLExtendedRule specific fields: source_ports, desintation_prefix, destination_ports, and protocol
"""
+ access_list = models.ForeignKey(
+ on_delete=models.CASCADE,
+ to=AccessList,
+ verbose_name='Extended Access List',
+ limit_choices_to={'type': 'extended'},
+ related_name='aclextendedrules',
+ )
source_ports = ArrayField(
base_field=models.PositiveIntegerField(),
blank=True,
@@ -139,6 +152,5 @@ class ACLExtendedRule(ACLRule):
- 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
index 7f9a859..f0486e3 100644
--- a/netbox_access_lists/models/access_lists.py
+++ b/netbox_access_lists/models/access_lists.py
@@ -2,29 +2,35 @@
Define the django models for this plugin.
"""
-from dcim.models import Device, VirtualChassis
+from dcim.models import Device, Interface, VirtualChassis
from django.contrib.contenttypes.fields import (GenericForeignKey,
GenericRelation)
from django.contrib.contenttypes.models import ContentType
+from django.core.validators import RegexValidator
from django.db import models
from django.urls import reverse
from netbox.models import NetBoxModel
-from virtualization.models import VirtualMachine
+from virtualization.models import VirtualMachine, VMInterface
from ..choices import *
-from ..constants import ACL_HOST_ASSIGNMENT_MODELS
+from ..constants import (ACL_HOST_ASSIGNMENT_MODELS,
+ ACL_INTERFACE_ASSIGNMENT_MODELS)
__all__ = (
'AccessList',
+ 'ACLInterfaceAssignment',
)
+alphanumeric_plus = RegexValidator(r'^[0-9a-zA-Z,-,_]*$', 'Only alphanumeric, hyphens, and underscores characters are allowed.')
+
class AccessList(NetBoxModel):
"""
Model defintion for Access Lists.
"""
name = models.CharField(
- max_length=100
+ max_length=500,
+ validators=[alphanumeric_plus]
)
assigned_object_type = models.ForeignKey(
to=ContentType,
@@ -53,7 +59,8 @@ class AccessList(NetBoxModel):
class Meta:
unique_together = ['assigned_object_type', 'assigned_object_id', 'name']
ordering = ['assigned_object_type', 'assigned_object_id', 'name']
- verbose_name = "Access List"
+ verbose_name = 'Access List'
+ verbose_name_plural = 'Access Lists'
def __str__(self):
return self.name
@@ -72,6 +79,68 @@ class AccessList(NetBoxModel):
return ACLTypeChoices.colors.get(self.type)
+class ACLInterfaceAssignment(NetBoxModel):
+ """
+ Model defintion for Access Lists associations with other Host interfaces:
+ - VM interfaces
+ - device interface
+ - tbd on more
+ """
+
+ access_list = models.ForeignKey(
+ on_delete=models.CASCADE,
+ to=AccessList,
+ verbose_name='Access List',
+ )
+ direction = models.CharField(
+ max_length=30,
+ choices=ACLAssignmentDirectionChoices
+ )
+ assigned_object_type = models.ForeignKey(
+ to=ContentType,
+ limit_choices_to=ACL_INTERFACE_ASSIGNMENT_MODELS,
+ on_delete=models.PROTECT
+ )
+ assigned_object_id = models.PositiveBigIntegerField()
+ assigned_object = GenericForeignKey(
+ ct_field='assigned_object_type',
+ fk_field='assigned_object_id'
+ )
+ comments = models.TextField(
+ blank=True
+ )
+
+ class Meta:
+ unique_together = ['assigned_object_type', 'assigned_object_id', 'access_list', 'direction']
+ ordering = ['assigned_object_type', 'assigned_object_id', 'access_list', 'direction']
+ verbose_name = 'ACL Interface Assignment'
+ verbose_name_plural = 'ACL Interface Assignments'
+
+ 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:aclinterfaceassignment', args=[self.pk])
+
+ def get_direction_color(self):
+ return ACLAssignmentDirectionChoices.colors.get(self.direction)
+
+
+GenericRelation(
+ to=ACLInterfaceAssignment,
+ content_type_field='assigned_object_type',
+ object_id_field='assigned_object_id',
+ related_query_name='interface'
+).contribute_to_class(Interface, 'accesslistassignments')
+
+GenericRelation(
+ to=ACLInterfaceAssignment,
+ content_type_field='assigned_object_type',
+ object_id_field='assigned_object_id',
+ related_query_name='vminterface'
+).contribute_to_class(VMInterface, 'accesslistassignments')
+
GenericRelation(
to=AccessList,
content_type_field='assigned_object_type',
diff --git a/netbox_access_lists/navigation.py b/netbox_access_lists/navigation.py
index 0ba5610..2131f4e 100644
--- a/netbox_access_lists/navigation.py
+++ b/netbox_access_lists/navigation.py
@@ -36,6 +36,15 @@ aclextendedrule_butons = [
)
]
+accesslistassignment_buttons = [
+ PluginMenuButton(
+ link='plugins:netbox_access_lists:aclinterfaceassignment_add',
+ title='Add',
+ icon_class='mdi mdi-plus-thick',
+ color=ButtonColorChoices.GREEN
+ )
+]
+
#
# Define navigation bar links including the above buttons defined.
#
@@ -49,7 +58,7 @@ menu_items = (
# 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',
+ link_text='ACL Standard Rules',
buttons=aclstandardrule_butons
),
# Comment out Extended Access List rule to force creation in the ACL view
@@ -58,4 +67,9 @@ menu_items = (
link_text='ACL Extended Rules',
buttons=aclextendedrule_butons
),
+ PluginMenuItem(
+ link='plugins:netbox_access_lists:aclinterfaceassignment_list',
+ link_text='ACL Interface Assignments',
+ buttons=accesslistassignment_buttons
+ ),
)
diff --git a/netbox_access_lists/tables.py b/netbox_access_lists/tables.py
index 9284b81..fda26ec 100644
--- a/netbox_access_lists/tables.py
+++ b/netbox_access_lists/tables.py
@@ -3,23 +3,34 @@ Define the object lists / table view for each of the plugin models.
"""
import django_tables2 as tables
-from netbox.tables import ChoiceFieldColumn, NetBoxTable, columns
+from netbox.tables import (ChoiceFieldColumn, NetBoxTable, TemplateColumn,
+ columns)
-from .models import AccessList, ACLExtendedRule, ACLStandardRule
+from .models import (AccessList, ACLExtendedRule, ACLInterfaceAssignment,
+ ACLStandardRule)
__all__ = (
'AccessListTable',
+ 'ACLInterfaceAssignmentTable',
'ACLStandardRuleTable',
'ACLExtendedRuleTable',
)
+COL_HOST_ASSIGNMENT = """
+ {% if record.assigned_object.device %}
+ {{ record.assigned_object.device|placeholder }}
+ {% else %}
+ {{ record.assigned_object.virtual_machine|placeholder }}
+ {% endif %}
+ """
+
class AccessListTable(NetBoxTable):
"""
Defines the table view for the AccessList model.
"""
pk = columns.ToggleColumn()
- id = tables.Column( # Provides a link to the secret
+ id = tables.Column(
linkify=True
)
assigned_object = tables.Column(
@@ -48,6 +59,37 @@ class AccessListTable(NetBoxTable):
default_columns = ('name', 'assigned_object', 'type', 'rule_count', 'default_action', 'tags')
+class ACLInterfaceAssignmentTable(NetBoxTable):
+ """
+ Defines the table view for the AccessList model.
+ """
+ pk = columns.ToggleColumn()
+ id = tables.Column(
+ linkify=True
+ )
+ access_list = tables.Column(
+ linkify=True
+ )
+ direction = ChoiceFieldColumn()
+ host = tables.TemplateColumn(
+ template_code=COL_HOST_ASSIGNMENT
+
+ )
+ assigned_object = tables.Column(
+ linkify=True,
+ orderable=False,
+ verbose_name='Assigned Interface'
+ )
+ tags = columns.TagColumn(
+ url_name='plugins:netbox_access_lists:aclinterfaceassignment_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = ACLInterfaceAssignment
+ fields = ('pk', 'id', 'access_list', 'direction', 'host', 'assigned_object', 'tags')
+ default_columns = ('id', 'access_list', 'direction', 'host', 'assigned_object', 'tags')
+
+
class ACLStandardRuleTable(NetBoxTable):
"""
Defines the table view for the ACLStandardRule model.
diff --git a/netbox_access_lists/template_content.py b/netbox_access_lists/template_content.py
index 528f537..0d1eeee 100644
--- a/netbox_access_lists/template_content.py
+++ b/netbox_access_lists/template_content.py
@@ -2,16 +2,35 @@
from django.contrib.contenttypes.models import ContentType
from extras.plugins import PluginTemplateExtension
-from .models import AccessList
+from .models import AccessList, ACLInterfaceAssignment
__all__ = (
'AccessLists',
+ "ACLInterfaceAssignments",
'DeviceAccessLists',
'VirtualChassisAccessLists',
'VMAccessLists',
+ 'DeviceACLInterfaceAssignments',
+ 'VMAACLInterfaceAssignments',
)
+class ACLInterfaceAssignments(PluginTemplateExtension):
+
+ def right_page(self):
+ obj = self.context['object']
+
+ acl_interface_assignments = None
+ ctype = ContentType.objects.get_for_model(obj)
+ if ctype.model in ['interface', 'vminterface']:
+ acl_interface_assignments = ACLInterfaceAssignment.objects.filter(assigned_object_id=obj.pk, assigned_object_type=ctype)
+
+ return self.render('inc/assigned_interface/access_lists.html', extra_context={
+ 'acl_interface_assignments': acl_interface_assignments,
+ 'type': ctype.model if ctype.model == 'device' else ctype.name.replace(' ', '_'),
+ })
+
+
class AccessLists(PluginTemplateExtension):
def right_page(self):
@@ -19,11 +38,7 @@ class AccessLists(PluginTemplateExtension):
access_lists = None
ctype = ContentType.objects.get_for_model(obj)
- if ctype.model == 'device':
- access_lists = AccessList.objects.filter(assigned_object_id=obj.pk, assigned_object_type=ctype)
- elif ctype.model == 'virtualchassis':
- access_lists = AccessList.objects.filter(assigned_object_id=obj.pk, assigned_object_type=ctype)
- elif ctype.model == 'virtualmachine':
+ if ctype.model in ['device', 'virtualchassis', 'virtualmachine']:
access_lists = AccessList.objects.filter(assigned_object_id=obj.pk, assigned_object_type=ctype)
return self.render('inc/assigned_host/access_lists.html', extra_context={
@@ -44,4 +59,12 @@ class VMAccessLists(AccessLists):
model = 'virtualization.virtualmachine'
-template_extensions = [DeviceAccessLists, VirtualChassisAccessLists, VMAccessLists]
+class DeviceACLInterfaceAssignments(ACLInterfaceAssignments):
+ model = 'dcim.interface'
+
+
+class VMAACLInterfaceAssignments(ACLInterfaceAssignments):
+ model = 'virtualization.vminterface'
+
+
+template_extensions = [DeviceAccessLists, VirtualChassisAccessLists, VMAccessLists, DeviceACLInterfaceAssignments, VMAACLInterfaceAssignments]
diff --git a/netbox_access_lists/templates/inc/assigned_host/assigned_access_lists.html b/netbox_access_lists/templates/inc/assigned_host/assigned_access_lists.html
index adb99c4..f7111dc 100644
--- a/netbox_access_lists/templates/inc/assigned_host/assigned_access_lists.html
+++ b/netbox_access_lists/templates/inc/assigned_host/assigned_access_lists.html
@@ -7,15 +7,15 @@
Default Action
Rule Count
- {% for access_list in access_lists %}
+ {% for object in access_lists %}