Allow ACL associations to interfaces (#43)

This commit is contained in:
Ryan Merolle 2022-07-28 09:28:50 -04:00 committed by GitHub
parent b0288037a2
commit 63a5188862
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1080 additions and 156 deletions

View File

@ -1,6 +1,6 @@
ARG VARIANT=latest
FROM tgenannt/netbox:${VARIANT}
FROM netboxcommunity/netbox:${VARIANT}
ARG DEBIAN_FRONTEND=noninteractive

View File

@ -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:

View File

@ -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

View File

@ -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}

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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():

View File

@ -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.

View File

@ -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('<b>*Note:</b> 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('<b>*Note:</b> 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('<b>*Note:</b> 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('<b>*Note:</b> 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('<b>*Note:</b> 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('<b>*Note:</b> 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

View File

@ -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.

View File

@ -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=[

View File

@ -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'

View File

@ -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',

View File

@ -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
),
)

View File

@ -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 %}
<a href="{{ record.assigned_object.device.get_absolute_url }}">{{ record.assigned_object.device|placeholder }}</a>
{% else %}
<a href="{{ record.assigned_object.virtual_machine.get_absolute_url }}">{{ record.assigned_object.virtual_machine|placeholder }}</a>
{% 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.

View File

@ -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]

View File

@ -7,15 +7,15 @@
<th>Default Action</th>
<th>Rule Count</th>
</tr>
{% for access_list in access_lists %}
{% for object in access_lists %}
<tr>
<td>{{ access_list|linkify }}</td>
<td>{{ access_list.type|title }}</td>
<td>{{ access_list.default_action|title }}</td>
{% if access_list.type == 'standard' %}
<td>{{ access_list.aclstandardrules.count|placeholder }}</td>
{% elif access_list.type == 'extended' %}
<td>{{ access_list.aclextendedrules.count|placeholder }}</td>
<td>{{ object|linkify }}</td>
<td>{{ object.type|title }}</td>
<td>{{ object.default_action|title }}</td>
{% if object.type == 'standard' %}
<td>{{ object.aclstandardrules.count|placeholder }}</td>
{% elif object.type == 'extended' %}
<td>{{ object.aclextendedrules.count|placeholder }}</td>
{% endif %}
</tr>
{% endfor %}
@ -24,4 +24,4 @@
<div class="text-muted">
None found
</div>
{% endif %}
{% endif %}

View File

@ -0,0 +1,13 @@
<div class="card">
<h5 class="card-header">
Access Lists
</h5>
<div class="card-body">
{% include 'inc/assigned_interface/assigned_access_lists.html' %}
</div>
<div class="card-footer text-end noprint">
<a href="{% url 'plugins:netbox_access_lists:aclinterfaceassignment_add' %}?{{ type }}={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary">
<i class="mdi mdi-plus-thick"></i> Assign Access List
</a>
</div>
</div>

View File

@ -0,0 +1,29 @@
{% if acl_interface_assignments %}
<table class="table table-hover">
<tr>
<th>Name</th>
<th>Type</th>
<th>Default Action</th>
<th>Rule Count</th>
<th>Direction</th>
</tr>
{% for object in acl_interface_assignments %}
<tr>
<td>{{ object.access_list|linkify }}</td>
<td>{{ object.access_list.type|title }}</td>
<td>{{ object.access_list.default_action|title }}</td>
{% if object.access_list.type == 'standard' %}
<td>{{ object.access_list.aclstandardrules.count|placeholder }}</td>
{% elif object.access_list.type == 'extended' %}
<td>{{ object.access_list.aclextendedrules.count|placeholder }}</td>
{% endif %}
<td>{{ object.direction|title }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="text-muted">
None
</div>
{% endif %}

View File

@ -2,7 +2,7 @@
{% load static %}
{% load form_helpers %}
{% block title %}{% if obj.pk %}Editing {{ obj }}{% else %}Add a Access List{% endif %}{% endblock %}
{% block title %}{% if obj.pk %}Editing {{ obj }}{% else %}Add an Access List{% endif %}{% endblock %}
{% block form %}
{% render_errors form %}

View File

@ -0,0 +1,81 @@
{% extends 'generic/object.html' %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'plugins:netbox_access_lists:aclinterfaceassignment_list' %}">ACL Interface Assignments</a></li>
{% endblock %}
{% block controls %}
<div class="pull-right noprint">
{% if perms.netbox_access_lists.change_policy %}
<a href="{% url 'plugins:netbox_access_lists:aclinterfaceassignment_edit' pk=object.pk %}" class="btn btn-warning">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
</a>
{% endif %}
{% if perms.netbox_access_lists.delete_policy %}
<a href="{% url 'plugins:netbox_access_lists:aclinterfaceassignment_delete' pk=object.pk %}" class="btn btn-danger">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete
</a>
{% endif %}
</div>
{% endblock controls %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
{% block tab_items %}
<li class="nav-item" role="presentation">
<a class="nav-link{% if not active_tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
</li>
{% endblock tab_items %}
{% if perms.extras.view_objectchange %}
<li role="presentation" class="nav-item">
<a href="{% url 'plugins:netbox_access_lists:aclinterfaceassignment_changelog' pk=object.pk %}" class="nav-link{% if active_tab == 'changelog'%} active{% endif %}">Change Log</a>
</li>
{% endif %}
</ul>
{% endblock tabs %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">ACL Interface Assignment</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Host</th>
<td>
{% if object.assigned_object.device %}
<a href="{{ object.assigned_object.device.get_absolute_url }}">{{ object.assigned_object.device|placeholder }}</a>
{% else %}
<a href="{{ object.assigned_object.virtual_machine.get_absolute_url }}">{{ object.assigned_object.virtual_machine|placeholder }}</a>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Interface</th>
<td>
<a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}</a>
</td>
</tr>
<tr>
<th scope="row">Access List</th>
<td>
<a href="{{ object.access_list.get_absolute_url }}">{{ object.access_list }}</a>
</td>
</tr>
<tr>
<th scope="row">Direction</th>
<td>
{{ object.direction|title }}
</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,68 @@
{% extends 'generic/object_edit.html' %}
{% load static %}
{% load form_helpers %}
{% block title %}{% if obj.pk %}Editing {{ obj }}{% else %}Add an Access List to an Interface{% endif %}{% endblock %}
{% block form %}
{% render_errors form %}
<div class="field-group">
<h4>Access List Details</h4>
{% render_field form.access_list %}
{% render_field form.direction %}
{% render_field form.tags %}
</div>
<div class="field-group">
<h4>Interface Assignment</h4>
<ul class="nav nav-pills" role="tablist">
<li class="nav-item" role="presentation">
<button
role="tab"
type="button"
id="device_tab"
data-bs-toggle="tab"
class="nav-link {% if not form.initial.virtual_chassis and not form.initial.virtual_machine %}active{% endif %}"
data-bs-target="#device"
aria-controls="device"
>
Device
</button>
</li>
<li class="nav-item" role="presentation">
<button
role="tab"
type="button"
id="vm_tab"
data-bs-toggle="tab"
class="nav-link {% if form.initial.virtual_machine %}active{% endif %}"
data-bs-target="#virtualmachine"
aria-controls="virtualmachine"
>
Virtual Machine
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane{% if not form.initial.virtual_chassis and not form.initial.virtualmachine %} active{% endif %}" id="device">
{% render_field form.device %}
{% render_field form.interface %}
</div>
<div class="tab-pane{% if form.initial.virtual_machine %} active{% endif %}" id="virtualmachine">
{% render_field form.virtual_machine %}
{% render_field form.vminterface %}
</div>
</div>
</div>
<div class="field-group">
<h4>Comments</h4>
{% render_field form.comments %}
</div>
{% if form.custom_fields %}
<div class="card">
<h5 class="card-header">Custom Fields</h5>
<div class="card-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -21,7 +21,19 @@ urlpatterns = (
'model': models.AccessList
}),
# Standard Access List rules
# Access List Interface Assignments
path('interface-assignments/', views.ACLInterfaceAssignmentListView.as_view(), name='aclinterfaceassignment_list'),
path('interface-assignments/add/', views.ACLInterfaceAssignmentEditView.as_view(), name='aclinterfaceassignment_add'),
#path('interface-assignments/edit/', views.ACLInterfaceAssignmentBulkEditView.as_view(), name='aclinterfaceassignment_bulk_edit'),
path('interface-assignments/delete/', views.ACLInterfaceAssignmentBulkDeleteView.as_view(), name='aclinterfaceassignment_bulk_delete'),
path('interface-assignments/<int:pk>/', views.ACLInterfaceAssignmentView.as_view(), name='aclinterfaceassignment'),
path('interface-assignments/<int:pk>/edit/', views.ACLInterfaceAssignmentEditView.as_view(), name='aclinterfaceassignment_edit'),
path('interface-assignments/<int:pk>/delete/', views.ACLInterfaceAssignmentDeleteView.as_view(), name='aclinterfaceassignment_delete'),
path('interface-assignments/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aclinterfaceassignment_changelog', kwargs={
'model': models.ACLInterfaceAssignment
}),
# Standard Access List Rules
path('standard-rules/', views.ACLStandardRuleListView.as_view(), name='aclstandardrule_list'),
path('standard-rules/add/', views.ACLStandardRuleEditView.as_view(), name='aclstandardrule_add'),
path('standard-rules/delete/', views.ACLStandardRuleBulkDeleteView.as_view(), name='aclstandardrule_bulk_delete'),
@ -32,7 +44,7 @@ urlpatterns = (
'model': models.ACLStandardRule
}),
# Extended Access List rules
# Extended Access List Rules
path('extended-rules/', views.ACLExtendedRuleListView.as_view(), name='aclextendedrule_list'),
path('extended-rules/add/', views.ACLExtendedRuleEditView.as_view(), name='aclextendedrule_add'),
path('extended-rules/delete/', views.ACLExtendedRuleBulkDeleteView.as_view(), name='aclextendedrule_bulk_delete'),

View File

@ -14,6 +14,11 @@ __all__ = (
'AccessListEditView',
'AccessListDeleteView',
'AccessListBulkDeleteView',
'ACLInterfaceAssignmentView',
'ACLInterfaceAssignmentListView',
'ACLInterfaceAssignmentEditView',
'ACLInterfaceAssignmentDeleteView',
'ACLInterfaceAssignmentBulkDeleteView',
'ACLStandardRuleView',
'ACLStandardRuleListView',
'ACLStandardRuleEditView',
@ -85,17 +90,48 @@ class AccessListBulkDeleteView(generic.BulkDeleteView):
filterset = filtersets.AccessListFilterSet
table = tables.AccessListTable
#
# ACLInterfaceAssignment views
#
class ACLInterfaceAssignmentView(generic.ObjectView):
"""
Defines the view for the ACLInterfaceAssignments django model.
"""
queryset = models.ACLInterfaceAssignment.objects.all()
class ACLInterfaceAssignmentListView(generic.ObjectListView):
"""
Defines the list view for the ACLInterfaceAssignments django model.
"""
queryset = models.ACLInterfaceAssignment.objects.all()
table = tables.ACLInterfaceAssignmentTable
filterset = filtersets.ACLInterfaceAssignmentFilterSet
filterset_form = forms.ACLInterfaceAssignmentFilterForm
class ACLInterfaceAssignmentEditView(generic.ObjectEditView):
"""
Defines the edit view for the ACLInterfaceAssignments django model.
"""
queryset = models.ACLInterfaceAssignment.objects.all()
form = forms.ACLInterfaceAssignmentForm
template_name = 'netbox_access_lists/aclinterfaceassignment_edit.html'
class ACLInterfaceAssignmentDeleteView(generic.ObjectDeleteView):
"""
Defines the delete view for the ACLInterfaceAssignments django model.
"""
queryset = models.ACLInterfaceAssignment.objects.all()
class ACLInterfaceAssignmentBulkDeleteView(generic.BulkDeleteView):
queryset = models.ACLInterfaceAssignment.objects.all()
filterset = filtersets.ACLInterfaceAssignmentFilterSet
table = tables.ACLInterfaceAssignmentTable
#class AccessListBulkEditView(generic.BulkEditView):
# """
# Defines the bulk edit view for the AccessList django model.
# """
# queryset = models.AccessList.objects.annotate(
# rule_count=Count('aclextendedrules') + Count('aclstandardrules')
# )
# table = tables.AccessListTable
# filterset = filtersets.AccessListFilterSet
# form = forms.AccessListBulkEditForm
#
# ACLStandardRule views