diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index a3fc26f..2d3b513 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -78,7 +78,10 @@
"extensions": [
"DavidAnson.vscode-markdownlint",
"GitHub.codespaces",
+ "GitHub.copilot",
+ "GitHub.copilot-labs",
"GitHub.vscode-pull-request-github",
+ "Gruntfuggly.todo-tree",
"Tyriar.sort-lines",
"aaron-bond.better-comments",
"batisteo.vscode-django",
diff --git a/.devcontainer/requirements-dev.txt b/.devcontainer/requirements-dev.txt
index 13a832d..d4a50d5 100644
--- a/.devcontainer/requirements-dev.txt
+++ b/.devcontainer/requirements-dev.txt
@@ -12,3 +12,4 @@ pylint
pylint-django
wily
yapf
+sourcery-analytics
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9ebc05c..efe8170 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -19,11 +19,13 @@ repos:
- id: isort
args:
- "--profile=black"
+ exclude: ^.devcontainer/
- repo: https://github.com/psf/black
rev: 22.12.0
hooks:
- id: black
language_version: python3
+ exclude: ^.devcontainer/
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.4.0
hooks:
@@ -34,6 +36,7 @@ repos:
rev: 6.0.0
hooks:
- id: flake8
+ exclude: ^.devcontainer/
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
hooks:
@@ -44,6 +47,12 @@ repos:
rev: v1.29.0
hooks:
- id: yamllint
+ - repo: https://github.com/econchick/interrogate
+ rev: 1.5.0
+ hooks:
+ - id: interrogate
+ args: [--fail-under=90, --verbose]
+ exclude: (^.devcontainer/|^netbox_acls/migrations/)
#- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
# rev: v1.1.2
# hooks:
diff --git a/Makefile b/Makefile
index aa6c2f9..fb3ba16 100644
--- a/Makefile
+++ b/Makefile
@@ -70,9 +70,13 @@ start:
.PHONY: all ## Run all PLUGIN DEV targets
all: setup makemigrations migrate collectstatic initializers start
-#.PHONY: test
-#test:
-# ${VENV_PY_PATH} /opt/netbox/netbox/manage.py runserver test ${PLUGIN_NAME}
+.PHONY: rebuild ## Run PLUGIN DEV targets to rebuild
+rebuild: setup makemigrations migrate collectstatic start
+
+.PHONY: test
+test: setup
+ ${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py makemigrations ${PLUGIN_NAME} --check
+ ${VENV_PY_PATH} ${NETBOX_MANAGE_PATH}/manage.py test ${PLUGIN_NAME}
#relpatch:
# $(eval GSTATUS := $(shell git status --porcelain))
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..5218b1d
--- /dev/null
+++ b/TODO
@@ -0,0 +1,7 @@
+- TODO: ACL Form Bubble/ICON Extended/Standard
+- TODO: Add an Access List to an Interface Custom Fields after comments - DONE
+- TODO: ACL rules, look at last number and increment to next 10
+- TODO: Clone for ACL Interface should include device
+- TODO: Inconsistent errors for add/edit (where model is using a generic page)
+- TODO: Check Constants across codebase for consistency.
+- TODO: Test API, Forms, & Models - https://github.com/k01ek/netbox-bgp/tree/main/netbox_bgp/tests , https://github.com/DanSheps/netbox-secretstore/tree/develop/netbox_secretstore/tests & https://github.com/FlxPeters/netbox-plugin-prometheus-sd/tree/main/netbox_prometheus_sd/tests
diff --git a/netbox_acls/api/serializers.py b/netbox_acls/api/serializers.py
index b84a0bd..46f24cb 100644
--- a/netbox_acls/api/serializers.py
+++ b/netbox_acls/api/serializers.py
@@ -19,6 +19,7 @@
ACLExtendedRule,
ACLInterfaceAssignment,
ACLStandardRule,
+ BaseACLRule,
)
from .nested_serializers import NestedAccessListSerializer
@@ -29,18 +30,40 @@
"ACLExtendedRuleSerializer",
]
+# TODO: Check Constants across codebase for consistency.
# 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."
+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 = (
+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 = (
+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."
+ERROR_MESSAGE_ACL_TYPE = "Provided parent Access List is not of right type."
+
+
+def validate_gfk(data):
+ """
+ Check that the GFK object is valid.
+ """
+ # TODO: This can removed after https://github.com/netbox-community/netbox/issues/10221 is fixed.
+ try:
+ assigned_object = data[ # noqa: F841
+ "assigned_object_type"
+ ].get_object_for_this_type(
+ id=data["assigned_object_id"],
+ )
+ except ObjectDoesNotExist as e:
+ error_message_invalid_gfk = f"Invalid assigned_object {data['assigned_object_type']} ID {data['assigned_object_id']}"
+ raise serializers.ValidationError(
+ {
+ "assigned_object_type": [error_message_invalid_gfk],
+ "assigned_object_id": [error_message_invalid_gfk],
+ }
+ ) from e
class AccessListSerializer(NetBoxModelSerializer):
@@ -83,6 +106,9 @@ class Meta:
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_assigned_object(self, obj):
+ """
+ Returns the assigned object for the Access List.
+ """
serializer = get_serializer_for_model(
obj.assigned_object,
prefix=NESTED_SERIALIZER_PREFIX,
@@ -93,25 +119,13 @@ def get_assigned_object(self, obj):
def validate(self, data):
"""
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.
+ - Check that the GFK object is valid.
+ - Check if Access List has no existing rules before change the Access List's type.
"""
error_message = {}
# Check that the GFK object is valid.
- if "assigned_object_type" in data and "assigned_object_id" in data:
- # TODO: This can removed after https://github.com/netbox-community/netbox/issues/10221 is fixed.
- try:
- assigned_object = data[ # noqa: F841
- "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]
+ assigned_object = validate_gfk(data)
# Check if Access List has no existing rules before change the Access List's type.
if (
@@ -119,9 +133,9 @@ def validate(self, data):
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.",
- ]
+ raise serializers.ValidationError(
+ {"type": ["This ACL has ACL rules associated, CANNOT change ACL type."]}
+ )
if error_message:
raise serializers.ValidationError(error_message)
@@ -166,6 +180,9 @@ class Meta:
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_assigned_object(self, obj):
+ """
+ Returns the assigned object for the ACLInterfaceAssignment.
+ """
serializer = get_serializer_for_model(
obj.assigned_object,
prefix=NESTED_SERIALIZER_PREFIX,
@@ -173,51 +190,51 @@ def get_assigned_object(self, obj):
context = {"request": self.context["request"]}
return serializer(obj.assigned_object, context=context).data
+ def _validate_get_interface_host(self, data, assigned_object):
+ """
+ Check that the associated interface's parent host has the selected ACL defined.
+ """
+ MODEL_MAPPING = {
+ "interface": "device",
+ "vminterface": "virtual_machine",
+ }
+
+ assigned_object_model = data["assigned_object_type"].model
+
+ return getattr(assigned_object, MODEL_MAPPING.get(assigned_object_model, None))
+
+ def _validate_acl_host(self, acl_host, interface_host):
+ """
+ Check that the associated interface's parent host has the selected ACL defined.
+ """
+ if acl_host == interface_host:
+ return {}
+
+ error_acl_not_assigned_to_host = (
+ "Access List not present on the selected interface's host."
+ )
+ return {
+ "access_list": [error_acl_not_assigned_to_host],
+ "assigned_object_id": [error_acl_not_assigned_to_host],
+ }
+
def validate(self, data):
"""
Validate the AccessList django 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.
+ - Check that the GFK object is valid.
+ - Check that the associated interface's parent host has the selected ACL defined.
"""
+
+ # Check that the GFK object is valid.
+ assigned_object = validate_gfk(data)
+
error_message = {}
acl_host = data["access_list"].assigned_object
- # Check that the GFK object is valid.
- if "assigned_object_type" in data and "assigned_object_id" in data:
- # TODO: This can removed after https://github.com/netbox-community/netbox/issues/10221 is fixed.
- try:
- assigned_object = data[ # noqa: F841
- "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
- )
- else:
- interface_host = None
- # 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]
+ interface_host = self._validate_get_interface_host(data, assigned_object)
+ acl_host = data["access_list"].assigned_object
+
+ error_message |= self._validate_acl_host(acl_host, interface_host)
if error_message:
raise serializers.ValidationError(error_message)
@@ -225,14 +242,11 @@ def validate(self, data):
return super().validate(data)
-class ACLStandardRuleSerializer(NetBoxModelSerializer):
+class BaseACLRuleSerializer(NetBoxModelSerializer):
"""
- Defines the serializer for the django ACLStandardRule model & associates it to a view.
+ Defines the serializer for the django BaseACLRule model & associates it to a view.
"""
- url = serializers.HyperlinkedIdentityField(
- view_name="plugins-api:netbox_acls-api:aclstandardrule-detail",
- )
access_list = NestedAccessListSerializer()
source_prefix = NestedPrefixSerializer(
required=False,
@@ -242,10 +256,11 @@ class ACLStandardRuleSerializer(NetBoxModelSerializer):
class Meta:
"""
- Associates the django model ACLStandardRule & fields to the serializer.
+ Associates the django model BaseACLRule & fields to the serializer.
"""
- model = ACLStandardRule
+ abstract = True
+ model = BaseACLRule
fields = (
"id",
"url",
@@ -264,19 +279,19 @@ class Meta:
def validate(self, data):
"""
- Validate the ACLStandardRule django 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.
+ Validate the BaseACLRule django 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.
"""
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]
+ 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,
+ ERROR_MESSAGE_ACTION_REMARK_SOURCE_PREFIX_SET,
]
if error_message:
@@ -285,7 +300,24 @@ def validate(self, data):
return super().validate(data)
-class ACLExtendedRuleSerializer(NetBoxModelSerializer):
+class ACLStandardRuleSerializer(BaseACLRuleSerializer):
+ """
+ Defines the serializer for the django ACLStandardRule model & associates it to a view.
+ """
+
+ url = serializers.HyperlinkedIdentityField(
+ view_name="plugins-api:netbox_acls-api:aclstandardrule-detail",
+ )
+
+ class Meta(BaseACLRuleSerializer.Meta):
+ """
+ Associates the django model ACLStandardRule & fields to the serializer.
+ """
+
+ model = ACLStandardRule
+
+
+class ACLExtendedRuleSerializer(BaseACLRuleSerializer):
"""
Defines the serializer for the django ACLExtendedRule model & associates it to a view.
"""
@@ -293,37 +325,21 @@ class ACLExtendedRuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:netbox_acls-api:aclextendedrule-detail",
)
- access_list = NestedAccessListSerializer()
- source_prefix = NestedPrefixSerializer(
- required=False,
- allow_null=True,
- default=None,
- )
destination_prefix = NestedPrefixSerializer(
required=False,
allow_null=True,
default=None,
)
- class Meta:
+ class Meta(BaseACLRuleSerializer.Meta):
"""
Associates the django model ACLExtendedRule & fields to the serializer.
"""
model = ACLExtendedRule
- fields = (
- "id",
- "url",
- "display",
- "access_list",
- "index",
- "action",
- "tags",
- "description",
- "created",
- "custom_fields",
- "last_updated",
- "source_prefix",
+
+ # Add the additional fields to the serializer to support Extended ACL Rules.
+ fields = BaseACLRuleSerializer.Meta.fields + (
"source_ports",
"destination_prefix",
"destination_ports",
@@ -334,44 +350,33 @@ class Meta:
def validate(self, data):
"""
Validate the ACLExtendedRule django 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.
+ - 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.
"""
error_message = {}
+ rule_attributes = [
+ "source_prefix",
+ "source_ports",
+ "destination_prefix",
+ "destination_ports",
+ "protocol",
+ ]
# 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.",
- ]
+ error_message["remark"] = [ERROR_MESSAGE_NO_REMARK]
+
+ # Check if action set to remark, but other fields set.
+ for attribute in rule_attributes:
+ if data.get(attribute):
+ error_message[attribute] = [
+ f'Action is set to remark, {attribute.replace("_", " ").title()} CANNOT be set.'
+ ]
if error_message:
raise serializers.ValidationError(error_message)
diff --git a/netbox_acls/forms/constants.py b/netbox_acls/forms/constants.py
new file mode 100644
index 0000000..dac7a8e
--- /dev/null
+++ b/netbox_acls/forms/constants.py
@@ -0,0 +1,25 @@
+"""
+Constants for forms
+"""
+from django.utils.safestring import mark_safe
+
+# 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 not set to remark, but no remark is set.
+ERROR_MESSAGE_REMARK_WITHOUT_ACTION_REMARK = (
+ "CANNOT set remark unless action is set to remark."
+)
diff --git a/netbox_acls/forms/filtersets.py b/netbox_acls/forms/filtersets.py
index f227e3f..a833e7b 100644
--- a/netbox_acls/forms/filtersets.py
+++ b/netbox_acls/forms/filtersets.py
@@ -27,6 +27,7 @@
ACLExtendedRule,
ACLInterfaceAssignment,
ACLStandardRule,
+ BaseACLRule,
)
__all__ = (
@@ -37,11 +38,18 @@
)
-class AccessListFilterForm(NetBoxModelFilterSetForm):
+class BaseACLFilterForm(NetBoxModelFilterSetForm):
"""
- GUI filter form to search the django AccessList model.
+ GUI filter inherited base form to search the django ACL and ACL Interface Assignment models.
"""
+ class Meta:
+ """
+ Sets the parent class as an abstract class to be inherited by other classes.
+ """
+
+ abstract = True
+
model = AccessList
region = DynamicModelChoiceField(
queryset=Region.objects.all(),
@@ -70,6 +78,14 @@ class AccessListFilterForm(NetBoxModelFilterSetForm):
queryset=VirtualMachine.objects.all(),
required=False,
)
+
+
+class AccessListFilterForm(BaseACLFilterForm):
+ """
+ GUI filter form to search the django AccessList model.
+ """
+
+ model = AccessList
virtual_chassis = DynamicModelChoiceField(
queryset=VirtualChassis.objects.all(),
required=False,
@@ -94,43 +110,20 @@ class AccessListFilterForm(NetBoxModelFilterSetForm):
"site_group",
"site",
"device",
- "virtual_chassis",
"virtual_machine",
+ "virtual_chassis",
),
),
("ACL Details", ("type", "default_action")),
)
-class ACLInterfaceAssignmentFilterForm(NetBoxModelFilterSetForm):
+class ACLInterfaceAssignmentFilterForm(BaseACLFilterForm):
"""
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,
- query_params={"region_id": "$region", "group_id": "$site_group"},
- )
- device = DynamicModelChoiceField(
- queryset=Device.objects.all(),
- query_params={
- "region_id": "$region",
- "group_id": "$site_group",
- "site_id": "$site",
- },
- required=False,
- )
interface = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
@@ -138,25 +131,13 @@ class ACLInterfaceAssignmentFilterForm(NetBoxModelFilterSetForm):
"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",
+ label="VM Interface",
)
direction = ChoiceField(
choices=add_blank_choice(ACLAssignmentDirectionChoices),
@@ -164,63 +145,75 @@ class ACLInterfaceAssignmentFilterForm(NetBoxModelFilterSetForm):
)
tag = TagFilterField(model)
- # fieldsets = (
- # (None, ('q', 'tag')),
- # ('Host Details', ('region', 'site_group', 'site', 'device')),
- # ('ACL Details', ('type', 'default_action')),
- # )
+ fieldsets = (
+ (None, ("q", "tag")),
+ (
+ "Host Details",
+ (
+ "region",
+ "site_group",
+ "site",
+ "device",
+ "virtual_machine",
+ ),
+ ),
+ ("Interface Details", ("interface", "vminterface")),
+ )
-class ACLStandardRuleFilterForm(NetBoxModelFilterSetForm):
+class BaseACLRuleFilterForm(NetBoxModelFilterSetForm):
"""
- GUI filter form to search the django ACLStandardRule model.
+ GUI filter inherited base form to search the django ACL rule models.
"""
- model = ACLStandardRule
- tag = TagFilterField(model)
+ class Meta:
+ """
+ Sets the parent class as an abstract class to be inherited by other classes.
+ """
+
+ abstract = True
+
+ model = BaseACLRule
+ index = forms.IntegerField(
+ required=False,
+ )
access_list = DynamicModelMultipleChoiceField(
queryset=AccessList.objects.all(),
required=False,
)
+ action = ChoiceField(
+ choices=add_blank_choice(ACLRuleActionChoices),
+ required=False,
+ )
source_prefix = DynamicModelMultipleChoiceField(
queryset=Prefix.objects.all(),
required=False,
label="Source Prefix",
)
- action = ChoiceField(
- choices=add_blank_choice(ACLRuleActionChoices),
- required=False,
- )
fieldsets = (
(None, ("q", "tag")),
("Rule Details", ("access_list", "action", "source_prefix")),
)
-class ACLExtendedRuleFilterForm(NetBoxModelFilterSetForm):
+class ACLStandardRuleFilterForm(BaseACLRuleFilterForm):
+ """
+ GUI filter form to search the django ACLStandardRule model.
+ """
+
+ model = ACLStandardRule
+
+ tag = TagFilterField(model)
+
+
+class ACLExtendedRuleFilterForm(BaseACLRuleFilterForm):
"""
GUI filter form to search the django ACLExtendedRule model.
"""
model = ACLExtendedRule
- index = forms.IntegerField(
- required=False,
- )
tag = TagFilterField(model)
- access_list = DynamicModelMultipleChoiceField(
- queryset=AccessList.objects.all(),
- required=False,
- )
- action = ChoiceField(
- choices=add_blank_choice(ACLRuleActionChoices),
- required=False,
- )
- source_prefix = DynamicModelMultipleChoiceField(
- queryset=Prefix.objects.all(),
- required=False,
- label="Source Prefix",
- )
- desintation_prefix = DynamicModelMultipleChoiceField(
+ destination_prefix = DynamicModelMultipleChoiceField(
queryset=Prefix.objects.all(),
required=False,
label="Destination Prefix",
@@ -229,17 +222,9 @@ class ACLExtendedRuleFilterForm(NetBoxModelFilterSetForm):
choices=add_blank_choice(ACLProtocolChoices),
required=False,
)
-
- fieldsets = (
- (None, ("q", "tag")),
+ fieldsets = BaseACLRuleFilterForm.fieldsets[:-1] + (
(
"Rule Details",
- (
- "access_list",
- "action",
- "source_prefix",
- "desintation_prefix",
- "protocol",
- ),
+ BaseACLRuleFilterForm.fieldsets[-1][1] + ("destination_prefix", "protocol"),
),
)
diff --git a/netbox_acls/forms/models.py b/netbox_acls/forms/models.py
index 4dc1738..8c1c4c0 100644
--- a/netbox_acls/forms/models.py
+++ b/netbox_acls/forms/models.py
@@ -23,6 +23,14 @@
ACLExtendedRule,
ACLInterfaceAssignment,
ACLStandardRule,
+ BaseACLRule,
+)
+from .constants import (
+ ERROR_MESSAGE_NO_REMARK,
+ ERROR_MESSAGE_REMARK_WITHOUT_ACTION_REMARK,
+ HELP_TEXT_ACL_ACTION,
+ HELP_TEXT_ACL_RULE_INDEX,
+ HELP_TEXT_ACL_RULE_LOGIC,
)
__all__ = (
@@ -32,28 +40,6 @@
"ACLExtendedRuleForm",
)
-# 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):
"""
@@ -133,6 +119,10 @@ class AccessListForm(NetBoxModelForm):
comments = CommentField()
class Meta:
+ """
+ Defines the Model and fields to be used by the form.
+ """
+
model = AccessList
fields = (
"region",
@@ -156,16 +146,19 @@ class Meta:
}
def __init__(self, *args, **kwargs):
+ """
+ Initializes the form
+ """
# Initialize helper selectors
instance = kwargs.get("instance")
initial = kwargs.get("initial", {}).copy()
if instance:
- if type(instance.assigned_object) is Device:
+ if isinstance(instance.assigned_object, Device):
initial["device"] = instance.assigned_object
- elif type(instance.assigned_object) is VirtualChassis:
+ elif isinstance(instance.assigned_object, VirtualChassis):
initial["virtual_chassis"] = instance.assigned_object
- elif type(instance.assigned_object) is VirtualMachine:
+ elif isinstance(instance.assigned_object, VirtualMachine):
initial["virtual_machine"] = instance.assigned_object
kwargs["initial"] = initial
@@ -174,52 +167,69 @@ def __init__(self, *args, **kwargs):
def clean(self):
"""
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.
+ - 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.
"""
+ # TODO: Refactor this method to fix error message logic.
cleaned_data = super().clean()
- error_message = {}
if self.errors.get("name"):
return cleaned_data
+
name = cleaned_data.get("name")
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)
- ):
+ host_types = self._get_host_types()
+
+ # Check if no hosts selected.
+ self._clean_check_host_types(host_types)
+
+ host_type, host = host_types[0]
+
+ # Check if duplicate entry.
+ self._clean_check_duplicate_entry(name, host_type, host)
+ # Check if Access List has no existing rules before change the Access List's type.
+ self._clean_check_acl_type_change(acl_type)
+
+ return cleaned_data
+
+ def _get_host_types(self):
+ """
+ Get host type assigned to the Access List.
+ """
+ device = self.cleaned_data.get("device")
+ virtual_chassis = self.cleaned_data.get("virtual_chassis")
+ virtual_machine = self.cleaned_data.get("virtual_machine")
+ host_types = [
+ ("device", device),
+ ("virtual_chassis", virtual_chassis),
+ ("virtual_machine", virtual_machine),
+ ]
+ return [x for x in host_types if x[1]]
+
+ def _clean_check_host_types(self, host_types):
+ """
+ Used by parent class's clean method. Check number of host types selected.
+ """
+ if len(host_types) > 1:
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:
+ if not host_types:
raise forms.ValidationError(
"Access Lists must be assigned to a device, virtual chassis or virtual machine.",
)
- if device:
- host_type = "device"
- existing_acls = AccessList.objects.filter(name=name, device=device).exists()
- elif virtual_machine:
- host_type = "virtual_machine"
- existing_acls = AccessList.objects.filter(
- name=name,
- virtual_machine=virtual_machine,
- ).exists()
- else:
- host_type = "virtual_chassis"
- existing_acls = AccessList.objects.filter(
- name=name,
- virtual_chassis=virtual_chassis,
- ).exists()
-
+ def _clean_check_duplicate_entry(self, name, host_type, host):
+ """
+ Used by parent class's clean method. Check if duplicate entry. (Because of GFK.)
+ """
+ existing_acls = AccessList.objects.filter(
+ name=name, **{host_type: host}
+ ).exists()
# Check if duplicate entry.
if (
"name" in self.changed_data or host_type in self.changed_data
@@ -227,30 +237,45 @@ def clean(self):
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],
- }
+ raise forms.ValidationError(
+ {
+ host_type: [error_same_acl_name],
+ "name": [error_same_acl_name],
+ }
+ )
+
+ def _clean_check_acl_type_change(self, acl_type):
+ """
+ Used by parent class's clean method. Check if Access List has no existing rules before change the Access List's type.
+ """
if self.instance.pk:
- # Check if Access List has no existing rules before change the Access List's type.
+ ERROR_MESSAGE_EXISTING_RULES = (
+ "This ACL has ACL rules associated, CANNOT change ACL type.",
+ )
if (
acl_type == ACLTypeChoices.TYPE_EXTENDED
and self.instance.aclstandardrules.exists()
- ) or (
+ ):
+ raise forms.ValidationError(
+ {
+ "type": ERROR_MESSAGE_EXISTING_RULES,
+ }
+ )
+
+ if (
acl_type == ACLTypeChoices.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
+ raise forms.ValidationError(
+ {
+ "type": ERROR_MESSAGE_EXISTING_RULES,
+ }
+ )
def save(self, *args, **kwargs):
- # Set assigned object
+ """
+ Set assigned object
+ """
self.instance.assigned_object = (
self.cleaned_data.get("device")
or self.cleaned_data.get("virtual_chassis")
@@ -315,15 +340,17 @@ class ACLInterfaceAssignmentForm(NetBoxModelForm):
comments = CommentField()
def __init__(self, *args, **kwargs):
+ """
+ Initialize helper selectors
+ """
- # Initialize helper selectors
instance = kwargs.get("instance")
initial = kwargs.get("initial", {}).copy()
if instance:
- if type(instance.assigned_object) is Interface:
+ if isinstance(instance.assigned_object, Interface):
initial["interface"] = instance.assigned_object
initial["device"] = "device"
- elif type(instance.assigned_object) is VMInterface:
+ elif isinstance(instance.assigned_object, VMInterface):
initial["vminterface"] = instance.assigned_object
initial["virtual_machine"] = "virtual_machine"
kwargs["initial"] = initial
@@ -331,6 +358,10 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class Meta:
+ """
+ Defines the Model and fields to be used by the form.
+ """
+
model = ACLInterfaceAssignment
fields = (
"access_list",
@@ -351,109 +382,149 @@ class Meta:
def clean(self):
"""
Validates form inputs before submitting:
- - Check if both interface and vminterface are set.
- - Check if neither interface nor 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.
+ - Check if both interface and vminterface are set.
+ - Check if neither interface nor 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.
"""
+ # Call the parent class's `clean` method
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")
-
- # 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_message |= {
- "interface": [error_too_many_interfaces],
- "vminterface": [error_too_many_interfaces],
- }
- elif not (interface or vminterface):
- error_no_interface = (
- "An Access List assignment but specify an Interface or VM Interface."
+
+ # Get the interface types assigned to the Access List
+ interface_types = self._clean_get_interface_types()
+
+ # Initialize an error message variable
+ self._clean_check_interface_types(interface_types)
+
+ # Get the assigned interface & interface type
+ assigned_object_type, assigned_object = interface_types[0]
+
+ # Get the parent host (device or virtual machine) of the assigned interface
+ if assigned_object_type == "interface":
+ assigned_object_id = Interface.objects.get(pk=assigned_object.pk).pk
+ else:
+ assigned_object_id = VMInterface.objects.get(pk=assigned_object.pk).pk
+
+ # Get the ContentType id for the assigned object
+ assigned_object_type_id = ContentType.objects.get_for_model(assigned_object).pk
+
+ # Check if the parent host is assigned to the Access List
+ self._clean_check_if_interface_parent_is_assigned_to_access_list(
+ cleaned_data.get("access_list"), assigned_object_type, assigned_object
+ )
+
+ # Check for duplicate entries in the Access List
+ self._clean_check_if_interface_already_has_acl_in_direction(
+ cleaned_data.get("access_list"),
+ assigned_object_id,
+ assigned_object_type,
+ assigned_object_type_id,
+ cleaned_data.get("direction"),
+ )
+
+ return cleaned_data
+
+ def _clean_get_interface_types(self):
+ """
+ Used by parent class's clean method. Get interface type/model assigned to the Access List.
+ """
+ interface = self.cleaned_data.get("interface")
+ vminterface = self.cleaned_data.get("vminterface")
+ interface_types = [
+ ("interface", interface),
+ ("vminterface", vminterface),
+ ]
+ return [x for x in interface_types if x[1]]
+
+ def _clean_check_interface_types(self, interface_types):
+ """
+ Used by parent class's clean method. Check if number of interface type selected is 1.
+ """
+ # Check if more than 1 hosts selected.
+ if len(interface_types) > 1:
+ raise forms.ValidationError(
+ "Assignment can only be to one interface at a time (either a interface or vminterface)."
)
- error_message |= {
- "interface": [error_no_interface],
- "vminterface": [error_no_interface],
- }
+ # Check if no hosts selected.
+ elif not interface_types:
+ raise forms.ValidationError("No interface or vminterface selected.")
+
+ def _clean_check_if_interface_parent_is_assigned_to_access_list(
+ self, access_list, assigned_object_type, assigned_object
+ ):
+ """
+ Used by parent class's clean method. Check that an interface's parent device/virtual_machine is assigned to the Access List.
+ """
+ access_list_host = AccessList.objects.get(pk=access_list.pk).assigned_object
+ host_type = (
+ "device" if assigned_object_type == "interface" else "virtual_machine"
+ )
+ if assigned_object_type == "interface":
+ host = Interface.objects.get(pk=assigned_object.pk).device
else:
- if interface:
- assigned_object = interface
- assigned_object_type = "interface"
- host_type = "device"
- host = Interface.objects.get(pk=assigned_object.pk).device
- assigned_object_id = Interface.objects.get(pk=assigned_object.pk).pk
- else:
- assigned_object = vminterface
- assigned_object_type = "vminterface"
- host_type = "virtual_machine"
- host = VMInterface.objects.get(pk=assigned_object.pk).virtual_machine
- 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 that an interface's parent device/virtual_machine is assigned to the Access List.
- if 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.
- if 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],
+ host = VMInterface.objects.get(pk=assigned_object.pk).virtual_machine
+
+ if access_list_host != host:
+ ERROR_ACL_NOT_ASSIGNED_TO_HOST = "Access List not present on selected host."
+ raise forms.ValidationError(
+ {
+ "access_list": [ERROR_ACL_NOT_ASSIGNED_TO_HOST],
+ host_type: [ERROR_ACL_NOT_ASSIGNED_TO_HOST],
+ assigned_object_type: [ERROR_ACL_NOT_ASSIGNED_TO_HOST],
}
- # Check that the interface does not have an existing ACL applied in the direction already.
- if 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 |= {
+ )
+
+ def _clean_check_if_interface_already_has_acl_in_direction(
+ self,
+ access_list,
+ assigned_object_id,
+ assigned_object_type,
+ assigned_object_type_id,
+ direction,
+ ):
+ """
+ Check that the interface does not have an existing ACL applied in the direction already.
+ """
+
+ # Check for duplicate entry. (Because of GFK)
+ if ACLInterfaceAssignment.objects.filter(
+ access_list=access_list,
+ assigned_object_id=assigned_object_id,
+ assigned_object_type=assigned_object_type_id,
+ direction=direction,
+ ).exists():
+ raise forms.ValidationError({"access_list": ["Duplicate entry."]})
+
+ # Check that the interface does not have an existing ACL applied in the direction already.
+ if 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."
+ )
+ raise forms.ValidationError(
+ {
"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
+ """
+ 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.
- Requires an access_list, an index, and ACL rule type.
- See the clean function for logic on other field requirements.
- """
+class BaseACLRuleForm(NetBoxModelForm):
+ """GUI form to add or edit Access List Rules to be inherited by other classes"""
access_list = DynamicModelChoiceField(
queryset=AccessList.objects.all(),
@@ -461,14 +532,14 @@ class ACLStandardRuleForm(NetBoxModelForm):
"type": ACLTypeChoices.TYPE_STANDARD,
},
help_text=mark_safe(
- "*Note: This field will only display Standard ACLs.",
+ "*Note: This field will only display Standard ACLs."
),
label="Access List",
)
source_prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
- help_text=help_text_acl_rule_logic,
+ help_text=HELP_TEXT_ACL_RULE_LOGIC,
label="Source Prefix",
)
@@ -478,55 +549,98 @@ class ACLStandardRuleForm(NetBoxModelForm):
)
class Meta:
- model = ACLStandardRule
+ """Defines the Model and fields to be used by the form."""
+
+ abstract = True
+ model = BaseACLRule
fields = (
"access_list",
"index",
"action",
"remark",
- "source_prefix",
"tags",
"description",
)
+ # Adds a help text to the form fields where the field is not defined in the form class.
help_texts = {
- "index": help_text_acl_rule_index,
- "action": help_text_acl_action,
+ "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 source prefix OR action is set.",
+ "*Note: CANNOT be set if action is not set to remark."
),
+ "source_ports": HELP_TEXT_ACL_RULE_LOGIC,
}
- def clean(self):
+ def clean_check_remark_rule(self, cleaned_data, error_message, rule_type):
"""
- 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.
+ Used by parent class's clean method. Checks form inputs before submitting:
+ - Check if action set to remark, but no remark set.
+ - Check if action set to remark, but other rule attributes set.
"""
+ # Check if action set to remark, but no remark set.
+ if not cleaned_data.get("remark"):
+ error_message["remark"] = [ERROR_MESSAGE_NO_REMARK]
+
+ # list all the fields of a rule besides the remark
+ if rule_type == "extended":
+ rule_attributes = [
+ "source_prefix",
+ "source_ports",
+ "destination_prefix",
+ "destination_ports",
+ "protocol",
+ ]
+ else:
+ rule_attributes = ["source_prefix"]
+
+ # Check if action set to remark, but other fields set.
+ for attribute in rule_attributes:
+ if cleaned_data.get(attribute):
+ error_message[attribute] = [
+ f'Action is set to remark, {attribute.replace("_", " ").title()} CANNOT be set.'
+ ]
+
+ if error_message:
+ forms.ValidationError(error_message)
+
+
+class ACLStandardRuleForm(BaseACLRuleForm):
+ """
+ GUI form to add or edit Standard Access List.
+ Requires an access_list, an index, and ACL rule type.
+ See the clean function for logic on other field requirements.
+ """
+
+ class Meta(BaseACLRuleForm.Meta):
+ """Defines the Model and fields to be used by the form."""
+
+ model = ACLStandardRule
+ # Need to add source_prefix to the tuple here instead of base or it will cause a ValueError
+ fields = BaseACLRuleForm.Meta.fields + ("source_prefix",)
+
+ def clean(self):
cleaned_data = super().clean()
error_message = {}
- # No need to check for unique_together since there is no usage of GFK
-
+ # Check if action set to remark, but no remark set OR other fields set.
if cleaned_data.get("action") == "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["source_prefix"] = [
- error_message_action_remark_source_prefix_set,
- ]
+ BaseACLRuleForm.clean_check_remark_rule(
+ self, cleaned_data, error_message, rule_type="standard"
+ )
+
# Check remark set, but action not set to remark.
elif cleaned_data.get("remark"):
- error_message["remark"] = [error_message_remark_without_action_remark]
+ error_message["remark"] = [ERROR_MESSAGE_REMARK_WITHOUT_ACTION_REMARK]
if error_message:
raise forms.ValidationError(error_message)
+
return cleaned_data
-class ACLExtendedRuleForm(NetBoxModelForm):
+class ACLExtendedRuleForm(BaseACLRuleForm):
"""
GUI form to add or edit Extended Access List.
Requires an access_list, an index, and ACL rule type.
@@ -535,120 +649,54 @@ class ACLExtendedRuleForm(NetBoxModelForm):
access_list = DynamicModelChoiceField(
queryset=AccessList.objects.all(),
- query_params={
- "type": ACLTypeChoices.TYPE_EXTENDED,
- },
+ query_params={"type": ACLTypeChoices.TYPE_EXTENDED},
help_text=mark_safe(
"*Note: This field will only display Extended ACLs.",
),
label="Access List",
)
- source_prefix = DynamicModelChoiceField(
- queryset=Prefix.objects.all(),
- required=False,
- help_text=help_text_acl_rule_logic,
- label="Source Prefix",
- )
destination_prefix = DynamicModelChoiceField(
queryset=Prefix.objects.all(),
required=False,
- help_text=help_text_acl_rule_logic,
+ help_text=HELP_TEXT_ACL_RULE_LOGIC,
label="Destination Prefix",
)
- fieldsets = (
- ("Access List Details", ("access_list", "description", "tags")),
+ fieldsets = BaseACLRuleForm.fieldsets[:-1] + (
(
"Rule Definition",
- (
- "index",
- "action",
- "remark",
- "source_prefix",
- "source_ports",
- "destination_prefix",
- "destination_ports",
- "protocol",
- ),
+ BaseACLRuleForm.fieldsets[-1][1]
+ + ("source_ports", "destination_prefix", "destination_ports", "protocol"),
),
)
- class Meta:
+ class Meta(BaseACLRuleForm.Meta):
+ """Defines the Model and fields to be used by the form."""
+
model = ACLExtendedRule
- fields = (
- "access_list",
- "index",
- "action",
- "remark",
+ fields = BaseACLRuleForm.Meta.fields + (
"source_prefix",
"source_ports",
"destination_prefix",
"destination_ports",
"protocol",
- "tags",
- "description",
)
- help_texts = {
- "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": help_text_acl_rule_logic,
- }
def clean(self):
- """
- 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
-
+ # Check if action set to remark, but no remark set OR other fields set.
if cleaned_data.get("action") == "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["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["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["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["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["protocol"] = [
- "Action is set to remark, Protocol CANNOT be set.",
- ]
- # Check if action not set to remark, but remark set.
+ BaseACLRuleForm.clean_check_remark_rule(
+ self, cleaned_data, error_message, rule_type="extended"
+ )
+
+ # Check remark set, but action not set to remark.
elif cleaned_data.get("remark"):
- error_message["remark"] = [error_message_remark_without_action_remark]
+ error_message["remark"] = [ERROR_MESSAGE_REMARK_WITHOUT_ACTION_REMARK]
if error_message:
raise forms.ValidationError(error_message)
+
return cleaned_data
diff --git a/netbox_acls/migrations/0002_alter_accesslist_options_and_more.py b/netbox_acls/migrations/0002_alter_accesslist_options_and_more.py
index ea5ab86..902248f 100644
--- a/netbox_acls/migrations/0002_alter_accesslist_options_and_more.py
+++ b/netbox_acls/migrations/0002_alter_accesslist_options_and_more.py
@@ -6,7 +6,6 @@
class Migration(migrations.Migration):
-
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("netbox_acls", "0001_initial"),
diff --git a/netbox_acls/migrations/0003_netbox_acls.py b/netbox_acls/migrations/0003_netbox_acls.py
index 562e0f3..0af5aef 100644
--- a/netbox_acls/migrations/0003_netbox_acls.py
+++ b/netbox_acls/migrations/0003_netbox_acls.py
@@ -5,7 +5,6 @@
class Migration(migrations.Migration):
-
dependencies = [
("netbox_acls", "0002_alter_accesslist_options_and_more"),
]
diff --git a/netbox_acls/migrations/0004_netbox_acls.py b/netbox_acls/migrations/0004_netbox_acls.py
new file mode 100644
index 0000000..aeac436
--- /dev/null
+++ b/netbox_acls/migrations/0004_netbox_acls.py
@@ -0,0 +1,26 @@
+# Generated by Django 4.1.5 on 2023-02-01 22:47
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("netbox_acls", "0003_netbox_acls"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="accesslist",
+ name="name",
+ field=models.CharField(
+ max_length=500,
+ validators=[
+ django.core.validators.RegexValidator(
+ "^[a-zA-Z0-9-_]+$",
+ "Only alphanumeric, hyphens, and underscores characters are allowed.",
+ )
+ ],
+ ),
+ ),
+ ]
diff --git a/netbox_acls/models/access_list_rules.py b/netbox_acls/models/access_list_rules.py
index 5656720..465f562 100644
--- a/netbox_acls/models/access_list_rules.py
+++ b/netbox_acls/models/access_list_rules.py
@@ -12,13 +12,13 @@
from .access_lists import AccessList
__all__ = (
- "ACLRule",
+ "BaseACLRule",
"ACLStandardRule",
"ACLExtendedRule",
)
-class ACLRule(NetBoxModel):
+class BaseACLRule(NetBoxModel):
"""
Abstract model for ACL Rules.
Inherrited by both ACLStandardRule and ACLExtendedRule.
@@ -55,13 +55,22 @@ class ACLRule(NetBoxModel):
clone_fields = ("access_list", "action", "source_prefix")
def __str__(self):
+ """
+ Return a string representation of the model.
+ """
return f"{self.access_list}: Rule {self.index}"
def get_action_color(self):
+ """
+ Return the color for the action.
+ """
return ACLRuleActionChoices.colors.get(self.action)
@classmethod
def get_prerequisite_models(cls):
+ """
+ Return a list of prerequisite models for this model.
+ """
return [apps.get_model("ipam.Prefix"), AccessList]
class Meta:
@@ -77,9 +86,9 @@ class Meta:
unique_together = ["access_list", "index"]
-class ACLStandardRule(ACLRule):
+class ACLStandardRule(BaseACLRule):
"""
- Inherits ACLRule.
+ Inherits BaseACLRule.
"""
access_list = models.ForeignKey(
@@ -99,9 +108,12 @@ def get_absolute_url(self):
@classmethod
def get_prerequisite_models(cls):
+ """
+ Return a list of prerequisite models for this model.
+ """
return [AccessList]
- class Meta(ACLRule.Meta):
+ class Meta(BaseACLRule.Meta):
"""
Define the model properties adding to or overriding the inherited class:
- default_related_name for any FK relationships
@@ -113,9 +125,9 @@ class Meta(ACLRule.Meta):
verbose_name_plural = "ACL Standard Rules"
-class ACLExtendedRule(ACLRule):
+class ACLExtendedRule(BaseACLRule):
"""
- Inherits ACLRule.
+ Inherits BaseACLRule.
Add ACLExtendedRule specific fields: source_ports, desintation_prefix, destination_ports, and protocol
"""
@@ -123,7 +135,7 @@ class ACLExtendedRule(ACLRule):
on_delete=models.CASCADE,
to=AccessList,
verbose_name="Extended Access List",
- limit_choices_to={"type": "extended"},
+ limit_choices_to={"type": ACLTypeChoices.TYPE_EXTENDED},
related_name="aclextendedrules",
)
source_ports = ArrayField(
@@ -160,13 +172,19 @@ def get_absolute_url(self):
return reverse("plugins:netbox_acls:aclextendedrule", args=[self.pk])
def get_protocol_color(self):
+ """
+ Return the color for the protocol.
+ """
return ACLProtocolChoices.colors.get(self.protocol)
@classmethod
def get_prerequisite_models(cls):
+ """
+ Return a list of prerequisite models for this model.
+ xs"""
return [apps.get_model("ipam.Prefix"), AccessList]
- class Meta(ACLRule.Meta):
+ class Meta(BaseACLRule.Meta):
"""
Define the model properties adding to or overriding the inherited class:
- default_related_name for any FK relationships
diff --git a/netbox_acls/models/access_lists.py b/netbox_acls/models/access_lists.py
index 1feb635..33c6112 100644
--- a/netbox_acls/models/access_lists.py
+++ b/netbox_acls/models/access_lists.py
@@ -21,7 +21,7 @@
alphanumeric_plus = RegexValidator(
- r"^[0-9a-zA-Z,-,_]*$",
+ r"^[a-zA-Z0-9-_]+$",
"Only alphanumeric, hyphens, and underscores characters are allowed.",
)
@@ -65,12 +65,19 @@ class AccessList(NetBoxModel):
)
class Meta:
+ """
+ Define the ordering, unique constraints, and verbose names for the model.
+ """
+
unique_together = ["assigned_object_type", "assigned_object_id", "name"]
ordering = ["assigned_object_type", "assigned_object_id", "name"]
verbose_name = "Access List"
verbose_name_plural = "Access Lists"
def __str__(self):
+ """
+ Return a string representation of the model.
+ """
return self.name
def get_absolute_url(self):
@@ -81,9 +88,15 @@ def get_absolute_url(self):
return reverse("plugins:netbox_acls:accesslist", args=[self.pk])
def get_default_action_color(self):
+ """
+ Define the color of the default action.
+ """
return ACLActionChoices.colors.get(self.default_action)
def get_type_color(self):
+ """
+ Define the color of the type.
+ """
return ACLTypeChoices.colors.get(self.type)
@@ -121,6 +134,10 @@ class ACLInterfaceAssignment(NetBoxModel):
clone_fields = ("access_list", "direction")
class Meta:
+ """
+ Define the ordering, unique constraints, and verbose names for the model.
+ """
+
unique_together = [
"assigned_object_type",
"assigned_object_id",
@@ -148,9 +165,15 @@ def get_absolute_url(self):
@classmethod
def get_prerequisite_models(cls):
+ """
+ Return a list of models that must be defined before this model.
+ """
return [AccessList]
def get_direction_color(self):
+ """
+ Define the color of the direction.
+ """
return ACLAssignmentDirectionChoices.colors.get(self.direction)
diff --git a/netbox_acls/tables.py b/netbox_acls/tables.py
index 8552d78..a47988f 100644
--- a/netbox_acls/tables.py
+++ b/netbox_acls/tables.py
@@ -54,6 +54,10 @@ class AccessListTable(NetBoxTable):
)
class Meta(NetBoxTable.Meta):
+ """
+ Defines the table view for the AccessList model.
+ """
+
model = AccessList
fields = (
"pk",
@@ -103,6 +107,10 @@ class ACLInterfaceAssignmentTable(NetBoxTable):
)
class Meta(NetBoxTable.Meta):
+ """
+ Defines the table view for the ACLInterfaceAssignment model.
+ """
+
model = ACLInterfaceAssignment
fields = (
"pk",
@@ -140,6 +148,10 @@ class ACLStandardRuleTable(NetBoxTable):
)
class Meta(NetBoxTable.Meta):
+ """
+ Defines the table view for the ACLStandardRule model.
+ """
+
model = ACLStandardRule
fields = (
"pk",
@@ -182,6 +194,10 @@ class ACLExtendedRuleTable(NetBoxTable):
protocol = ChoiceFieldColumn()
class Meta(NetBoxTable.Meta):
+ """
+ Defines the table view for the ACLExtendedRule model.
+ """
+
model = ACLExtendedRule
fields = (
"pk",
diff --git a/netbox_acls/templates/inc/aclrule.html b/netbox_acls/templates/inc/aclrule.html
new file mode 100644
index 0000000..7ef1278
--- /dev/null
+++ b/netbox_acls/templates/inc/aclrule.html
@@ -0,0 +1,84 @@
+{% extends 'generic/object.html' %}
+
+{% block content %}
+
+
+
+
+
+
+ {{ object|meta:"verbose_name"|bettertitle }}
+
+ Access List |
+
+ {{ object.access_list }}
+ |
+
+
+ Index |
+ {{ object.index }} |
+
+
+ Description |
+ {{ object.description|placeholder }} |
+
+
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/tags.html' %}
+
+
+
+
+
+
+ Details
+
+ Remark |
+ {{ object.get_remark_display|placeholder }} |
+
+
+ Protocol |
+ {{ object.get_protocol_display|placeholder }} |
+
+
+ Source Prefix |
+
+ {% if object.source_prefix %}
+ {{ object.source_prefix }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
+
+ {% if "extended" in object|meta:"verbose_name"|lower %}
+
+ Source Ports |
+ {{ object.source_ports|join:", "|placeholder }} |
+
+
+ Destination Prefix |
+
+ {% if object.destination_prefix %}
+ {{ object.destination_prefix }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
+
+
+ Destination Ports |
+ {{ object.destination_ports|join:", "|placeholder }} |
+
+ {% endif %}
+
+ Action |
+ {% badge object.get_action_display bg_color=object.get_action_color %} |
+
+
+
+
+
+
+{% endblock content %}
diff --git a/netbox_acls/templates/inc/common_edit.html b/netbox_acls/templates/inc/common_edit.html
new file mode 100644
index 0000000..cca6207
--- /dev/null
+++ b/netbox_acls/templates/inc/common_edit.html
@@ -0,0 +1,11 @@
+{% load form_helpers %}
+{% if form.custom_fields %}
+
+
Custom Fields
+ {% render_custom_fields form %}
+
+{% endif %}
+
+
Comments
+ {% render_field form.comments %}
+
diff --git a/netbox_acls/templates/netbox_acls/accesslist_edit.html b/netbox_acls/templates/netbox_acls/accesslist_edit.html
index a197daa..4edd576 100644
--- a/netbox_acls/templates/netbox_acls/accesslist_edit.html
+++ b/netbox_acls/templates/netbox_acls/accesslist_edit.html
@@ -74,16 +74,5 @@ Host Assignment
-
-
Comments
- {% render_field form.comments %}
-
-{% if form.custom_fields %}
-
-
-
- {% render_custom_fields form %}
-
-
-{% endif %}
+{% include "inc/common_edit.html" %}
{% endblock %}
diff --git a/netbox_acls/templates/netbox_acls/aclextendedrule.html b/netbox_acls/templates/netbox_acls/aclextendedrule.html
index d4abbdf..3749a5e 100644
--- a/netbox_acls/templates/netbox_acls/aclextendedrule.html
+++ b/netbox_acls/templates/netbox_acls/aclextendedrule.html
@@ -1,82 +1 @@
-{% extends 'generic/object.html' %}
-
-{% block content %}
-
-
-
-
-
-
- ACL Extended Rule
-
- Access List |
-
- {{ object.access_list }}
- |
-
-
- Description |
- {{ object.description|placeholder }} |
-
-
- Index |
- {{ object.index }} |
-
-
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
-
-
-
-
-
-
- Details
-
- Remark |
- {{ object.get_remark_display|placeholder }} |
-
-
- Protocol |
- {{ object.get_protocol_display|placeholder }} |
-
-
- Source Prefix |
-
- {% if object.source_prefix %}
- {{ object.source_prefix }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- Source Ports |
- {{ object.source_ports|join:", "|placeholder }} |
-
-
- Destination Prefix |
-
- {% if object.destination_prefix %}
- {{ object.destination_prefix }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- Destination Ports |
- {{ object.destination_ports|join:", "|placeholder }} |
-
-
- Action |
- {% badge object.get_action_display bg_color=object.get_action_color %} |
-
-
-
-
-
-
-{% endblock content %}
+{% include "inc/aclrule.html" %}
diff --git a/netbox_acls/templates/netbox_acls/aclinterfaceassignment_edit.html b/netbox_acls/templates/netbox_acls/aclinterfaceassignment_edit.html
index 4eae6f0..7f7c255 100644
--- a/netbox_acls/templates/netbox_acls/aclinterfaceassignment_edit.html
+++ b/netbox_acls/templates/netbox_acls/aclinterfaceassignment_edit.html
@@ -53,16 +53,5 @@ Interface Assignment
-
-
Comments
- {% render_field form.comments %}
-
-{% if form.custom_fields %}
-
-
-
- {% render_custom_fields form %}
-
-
-{% endif %}
+{% include "inc/common_edit.html" %}
{% endblock %}
diff --git a/netbox_acls/templates/netbox_acls/aclstandardrule.html b/netbox_acls/templates/netbox_acls/aclstandardrule.html
index c8e2562..3749a5e 100644
--- a/netbox_acls/templates/netbox_acls/aclstandardrule.html
+++ b/netbox_acls/templates/netbox_acls/aclstandardrule.html
@@ -1,64 +1 @@
-{% extends 'generic/object.html' %}
-
-{% block content %}
-
-
-
-
-
-
- ACL Standard Rule
-
- Access List |
-
- {{ object.access_list }}
- |
-
-
- Index |
- {{ object.index }} |
-
-
- Description |
- {{ object.description|placeholder }} |
-
-
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
-
-
-
-
-
-
- Details
-
- Remark |
- {{ object.get_remark_display|placeholder }} |
-
-
- Protocol |
- {{ object.get_protocol_display|placeholder }} |
-
-
- Source Prefix |
-
- {% if object.source_prefix %}
- {{ object.source_prefix }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- Action |
- {% badge object.get_action_display bg_color=object.get_action_color %} |
-
-
-
-
-
-
-{% endblock content %}
+{% include "inc/aclrule.html" %}
diff --git a/netbox_acls/tests/test_api.py b/netbox_acls/tests/test_api.py
index afbd4fa..2dd5745 100644
--- a/netbox_acls/tests/test_api.py
+++ b/netbox_acls/tests/test_api.py
@@ -9,21 +9,30 @@
class AppTest(APITestCase):
+ """
+ Test the API root
+ """
+
def test_root(self):
+ """
+ Test the API root
+ """
url = reverse("plugins-api:netbox_acls-api:api-root")
- response = self.client.get(f"{url}?format=api", **self.header)
+ response = self.client.get("{}?format=api".format(url), **self.header)
self.assertEqual(response.status_code, status.HTTP_200_OK)
class ACLTestCase(
+ APIViewTestCases.CreateObjectViewTestCase,
+ APIViewTestCases.DeleteObjectViewTestCase,
APIViewTestCases.GetObjectViewTestCase,
APIViewTestCases.ListObjectsViewTestCase,
- APIViewTestCases.CreateObjectViewTestCase,
APIViewTestCases.UpdateObjectViewTestCase,
- APIViewTestCases.DeleteObjectViewTestCase,
):
- """Test the AccessList Test"""
+ """
+ Test the AccessList Test
+ """
model = AccessList
view_namespace = "plugins-api:netbox_acls"
@@ -31,6 +40,9 @@ class ACLTestCase(
@classmethod
def setUpTestData(cls):
+ """
+ Create a test site, manufacturer, device type, device role, and device
+ """
site = Site.objects.create(name="Site 1", slug="site-1")
manufacturer = Manufacturer.objects.create(
name="Manufacturer 1",
@@ -53,49 +65,67 @@ def setUpTestData(cls):
access_lists = (
AccessList(
- name="testacl1",
+ name="testacl-standard-deny-1",
assigned_object_type=ContentType.objects.get_for_model(Device),
assigned_object_id=device.id,
type=ACLTypeChoices.TYPE_STANDARD,
default_action=ACLActionChoices.ACTION_DENY,
+ comments="Comment Test 1",
),
AccessList(
- name="testacl2",
+ name="testacl-standard-permit-1",
assigned_object_type=ContentType.objects.get_for_model(Device),
assigned_object_id=device.id,
type=ACLTypeChoices.TYPE_STANDARD,
- default_action=ACLActionChoices.ACTION_DENY,
+ default_action=ACLActionChoices.ACTION_PERMIT,
+ comments="Comment Test 2",
),
AccessList(
- name="testacl3",
+ name="testacl-extended-deny-1",
assigned_object_type=ContentType.objects.get_for_model(Device),
assigned_object_id=device.id,
- type=ACLTypeChoices.TYPE_STANDARD,
+ type=ACLTypeChoices.TYPE_EXTENDED,
default_action=ACLActionChoices.ACTION_DENY,
),
+ AccessList(
+ name="testacl-extended-permit-1",
+ assigned_object_type=ContentType.objects.get_for_model(Device),
+ assigned_object_id=device.id,
+ type=ACLTypeChoices.TYPE_EXTENDED,
+ default_action=ACLActionChoices.ACTION_PERMIT,
+ ),
)
AccessList.objects.bulk_create(access_lists)
cls.create_data = [
{
- "name": "testacl4",
+ "name": "testacl-standard-deny-2",
"assigned_object_type": "dcim.device",
"assigned_object_id": device.id,
"type": ACLTypeChoices.TYPE_STANDARD,
"default_action": ACLActionChoices.ACTION_DENY,
+ "comments": "Comment Test 3",
},
{
- "name": "testacl5",
+ "name": "testacl-standard-permit-2",
+ "assigned_object_type": "dcim.device",
+ "assigned_object_id": device.id,
+ "type": ACLTypeChoices.TYPE_STANDARD,
+ "default_action": ACLActionChoices.ACTION_DENY,
+ "comments": "Comment Test 4",
+ },
+ {
+ "name": "testacl-extended-deny-2",
"assigned_object_type": "dcim.device",
"assigned_object_id": device.id,
"type": ACLTypeChoices.TYPE_EXTENDED,
"default_action": ACLActionChoices.ACTION_DENY,
},
{
- "name": "testacl6",
+ "name": "testacl-extended-permit-2",
"assigned_object_type": "dcim.device",
"assigned_object_id": device.id,
- "type": ACLTypeChoices.TYPE_STANDARD,
+ "type": ACLTypeChoices.TYPE_EXTENDED,
"default_action": ACLActionChoices.ACTION_DENY,
},
]
diff --git a/netbox_acls/tests/test_models.py b/netbox_acls/tests/test_models.py
new file mode 100644
index 0000000..31fe5af
--- /dev/null
+++ b/netbox_acls/tests/test_models.py
@@ -0,0 +1,220 @@
+from dcim.models import (
+ Device,
+ DeviceRole,
+ DeviceType,
+ Interface,
+ Manufacturer,
+ Site,
+ VirtualChassis,
+)
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ValidationError
+from django.test import TestCase
+from ipam.models import Prefix
+from netaddr import IPNetwork
+from virtualization.models import Cluster, ClusterType, VirtualMachine, VMInterface
+
+from netbox_acls.choices import *
+from netbox_acls.models import *
+
+
+class BaseTestCase(TestCase):
+ """
+ Base test case for netbox_acls models.
+ """
+
+ @classmethod
+ def setUpTestData(cls):
+ """
+ Create base data to test using including:
+ - 1 of each of the following: test site, manufacturer, device type, device role, cluster type, cluster, virtual_chassis, & virtual machine
+ - 2 devices, prefixes, 2 interfaces, and 2 vminterfaces
+ """
+
+ site = Site.objects.create(name="Site 1", slug="site-1")
+ manufacturer = Manufacturer.objects.create(
+ name="Manufacturer 1",
+ slug="manufacturer-1",
+ )
+ devicetype = DeviceType.objects.create(
+ manufacturer=manufacturer,
+ model="Device Type 1",
+ )
+ devicerole = DeviceRole.objects.create(
+ name="Device Role 1",
+ slug="device-role-1",
+ )
+ device = Device.objects.create(
+ name="Device 1",
+ site=site,
+ device_type=devicetype,
+ device_role=devicerole,
+ )
+ virtual_chassis = VirtualChassis.objects.create(name="Virtual Chassis 1")
+ virtual_chassis_member = Device.objects.create(
+ name="VC Device",
+ site=site,
+ device_type=devicetype,
+ device_role=devicerole,
+ virtual_chassis=virtual_chassis,
+ vc_position=1,
+ )
+ cluster_member = Device.objects.create(
+ name="Cluster Device",
+ site=site,
+ device_type=devicetype,
+ device_role=devicerole,
+ )
+ clustertype = ClusterType.objects.create(name="Cluster Type 1")
+ cluster = Cluster.objects.create(
+ name="Cluster 1",
+ type=clustertype,
+ )
+ virtual_machine = VirtualMachine.objects.create(name="VirtualMachine 1")
+
+
+class TestAccessList(BaseTestCase):
+ """
+ Test AccessList model.
+ """
+
+ def test_alphanumeric_plus_success(self):
+ """
+ Test that AccessList names with alphanumeric characters, '_', or '-' pass validation.
+ """
+ acl_good_name = AccessList(
+ name="Testacl-good_name-1",
+ assigned_object_type=ContentType.objects.get_for_model(Device),
+ assigned_object_id=1,
+ type=ACLTypeChoices.TYPE_EXTENDED,
+ default_action=ACLActionChoices.ACTION_PERMIT,
+ )
+ acl_good_name.full_clean()
+
+ def test_duplicate_name_success(self):
+ """
+ Test that AccessList names can be non-unique if associated to different devices.
+ """
+
+ params = {
+ "name": "GOOD-DUPLICATE-ACL",
+ "type": ACLTypeChoices.TYPE_STANDARD,
+ "default_action": ACLActionChoices.ACTION_PERMIT,
+ }
+ AccessList.objects.create(
+ **params,
+ assigned_object_type=ContentType.objects.get_for_model(Device),
+ assigned_object_id=1,
+ )
+ vm_acl = AccessList(
+ **params,
+ assigned_object_type=ContentType.objects.get_for_model(VirtualMachine),
+ assigned_object_id=1,
+ )
+ vm_acl.full_clean()
+ vc_acl = AccessList(
+ **params,
+ assigned_object_type=ContentType.objects.get_for_model(VirtualChassis),
+ assigned_object_id=1,
+ )
+ vc_acl.full_clean()
+
+ def test_alphanumeric_plus_fail(self):
+ """
+ Test that AccessList names with non-alphanumeric (exluding '_' and '-') characters fail validation.
+ """
+ non_alphanumeric_plus_chars = " !@#$%^&*()[]{};:,./<>?\|~=+"
+
+ for i, char in enumerate(non_alphanumeric_plus_chars, start=1):
+ bad_acl_name = AccessList(
+ name=f"Testacl-bad_name_{i}_{char}",
+ assigned_object_type=ContentType.objects.get_for_model(Device),
+ assigned_object_id=1,
+ type=ACLTypeChoices.TYPE_EXTENDED,
+ default_action=ACLActionChoices.ACTION_PERMIT,
+ comments=f'ACL with "{char}" in name',
+ )
+ with self.assertRaises(ValidationError):
+ bad_acl_name.full_clean()
+
+ def test_duplicate_name_fail(self):
+ """
+ Test that AccessList names must be unique per device.
+ """
+ params = {
+ "name": "FAIL-DUPLICATE-ACL",
+ "assigned_object_type": ContentType.objects.get_for_model(Device),
+ "assigned_object_id": 1,
+ "type": ACLTypeChoices.TYPE_STANDARD,
+ "default_action": ACLActionChoices.ACTION_PERMIT,
+ }
+ acl_1 = AccessList.objects.create(**params)
+ acl_1.save()
+ acl_2 = AccessList(**params)
+ with self.assertRaises(ValidationError):
+ acl_2.full_clean()
+ # TODO: test_duplicate_name_fail - VM & VC
+
+ # TODO: Test choices for AccessList Model
+
+
+class TestACLInterfaceAssignment(BaseTestCase):
+ """
+ Test ACLInterfaceAssignment model.
+ """
+
+ @classmethod
+ def setUpTestData(cls):
+ """
+ Extend BaseTestCase's setUpTestData() to create additional data for testing.
+ """
+ super().setUpTestData()
+
+ interfaces = Interface.objects.bulk_create(
+ (
+ Interface(name="Interface 1", device=device, type="1000baset"),
+ Interface(name="Interface 2", device=device, type="1000baset"),
+ )
+ )
+ vminterfaces = VMInterface.objects.bulk_create(
+ (
+ VMInterface(name="Interface 1", virtual_machine=virtual_machine),
+ VMInterface(name="Interface 2", virtual_machine=virtual_machine),
+ )
+ )
+ # prefixes = Prefix.objects.bulk_create(
+ # (
+ # Prefix(prefix=IPNetwork("10.0.0.0/24")),
+ # Prefix(prefix=IPNetwork("192.168.1.0/24")),
+ # )
+ # )
+
+ def test_acl_interface_assignment_success(self):
+ """
+ Test that ACLInterfaceAssignment passes validation if the ACL is assigned to the host and not already assigned to the interface and direction.
+ """
+ pass
+ # TODO: test_acl_interface_assignment_success - VM & Device
+
+ def test_acl_interface_assignment_fail(self):
+ """
+ Test that ACLInterfaceAssignment fails validation if the ACL is not assigned to the parent host.
+ """
+ pass
+ # TODO: test_acl_interface_assignment_fail - VM & Device
+
+ def test_duplicate_assignment_fail(self):
+ """
+ Test that ACLInterfaceAssignment fails validation if the ACL already is assigned to the same interface and direction.
+ """
+ pass
+ # TODO: test_duplicate_assignment_fail - VM & Device
+
+ def test_acl_already_assinged_fail(self):
+ """
+ Test that ACLInterfaceAssignment fails validation if the interface already has an ACL assigned in the same direction.
+ """
+ pass
+ ## TODO: test_acl_already_assinged_fail - VM & Device
+
+ # TODO: Test choices for ACLInterfaceAssignment Model
diff --git a/netbox_acls/views.py b/netbox_acls/views.py
index d986945..95eca55 100644
--- a/netbox_acls/views.py
+++ b/netbox_acls/views.py
@@ -104,6 +104,10 @@ class AccessListDeleteView(generic.ObjectDeleteView):
class AccessListBulkDeleteView(generic.BulkDeleteView):
+ """
+ Defines the bulk delete view for the AccessLists django model.
+ """
+
queryset = models.AccessList.objects.prefetch_related("tags")
filterset = filtersets.AccessListFilterSet
table = tables.AccessListTable
@@ -120,6 +124,9 @@ class AccessListChildView(generic.ObjectChildrenView):
template_name = "inc/view_tab.html"
def get_extra_context(self, request, instance):
+ """
+ Returns the extra context for the child view.
+ """
return {
"table_config": self.table.__name__,
"model_type": self.queryset.model._meta.verbose_name.replace(" ", "_"),
@@ -127,6 +134,9 @@ def get_extra_context(self, request, instance):
}
def prep_table_data(self, request, queryset, parent):
+ """
+ Prepares the table data for the child view.
+ """
return queryset.annotate(
rule_count=Count("aclextendedrules") + Count("aclstandardrules"),
)
@@ -134,6 +144,10 @@ def prep_table_data(self, request, queryset, parent):
@register_model_view(Device, "access_lists")
class DeviceAccessListView(AccessListChildView):
+ """
+ Defines the child view for the AccessLists model.
+ """
+
queryset = Device.objects.prefetch_related("tags")
tab = ViewTab(
label="Access Lists",
@@ -142,6 +156,9 @@ class DeviceAccessListView(AccessListChildView):
)
def get_children(self, request, parent):
+ """
+ Returns the children of the parent object.
+ """
return self.child_model.objects.restrict(request.user, "view").filter(
device=parent,
)
@@ -149,6 +166,10 @@ def get_children(self, request, parent):
@register_model_view(VirtualChassis, "access_lists")
class VirtualChassisAccessListView(AccessListChildView):
+ """
+ Defines the child view for the AccessLists model.
+ """
+
queryset = VirtualChassis.objects.prefetch_related("tags")
tab = ViewTab(
label="Access Lists",
@@ -157,6 +178,9 @@ class VirtualChassisAccessListView(AccessListChildView):
)
def get_children(self, request, parent):
+ """
+ Returns the children of the parent object.
+ """
return self.child_model.objects.restrict(request.user, "view").filter(
virtual_chassis=parent,
)
@@ -164,6 +188,10 @@ def get_children(self, request, parent):
@register_model_view(VirtualMachine, "access_lists")
class VirtualMachineAccessListView(AccessListChildView):
+ """
+ Defines the child view for the AccessLists model.
+ """
+
queryset = VirtualMachine.objects.prefetch_related("tags")
tab = ViewTab(
label="Access Lists",
@@ -172,6 +200,9 @@ class VirtualMachineAccessListView(AccessListChildView):
)
def get_children(self, request, parent):
+ """
+ Returns the children of the parent object.
+ """
return self.child_model.objects.restrict(request.user, "view").filter(
virtual_machine=parent,
)
@@ -246,6 +277,10 @@ class ACLInterfaceAssignmentDeleteView(generic.ObjectDeleteView):
class ACLInterfaceAssignmentBulkDeleteView(generic.BulkDeleteView):
+ """
+ Defines the bulk delete view for the ACLInterfaceAssignments django model.
+ """
+
queryset = models.ACLInterfaceAssignment.objects.prefetch_related(
"access_list",
"tags",
@@ -265,6 +300,9 @@ class ACLInterfaceAssignmentChildView(generic.ObjectChildrenView):
template_name = "inc/view_tab.html"
def get_extra_context(self, request, instance):
+ """
+ Returns the extra context for the view.
+ """
return {
"table_config": self.table.__name__,
"model_type": self.queryset.model._meta.verbose_name.replace(" ", "_"),
@@ -274,6 +312,10 @@ def get_extra_context(self, request, instance):
@register_model_view(Interface, "acl_interface_assignments")
class InterfaceACLInterfaceAssignmentView(ACLInterfaceAssignmentChildView):
+ """
+ Defines the child view for the ACLInterfaceAssignments model.
+ """
+
queryset = Interface.objects.prefetch_related("device", "tags")
tab = ViewTab(
label="ACL Interface Assignments",
@@ -284,6 +326,9 @@ class InterfaceACLInterfaceAssignmentView(ACLInterfaceAssignmentChildView):
)
def get_children(self, request, parent):
+ """
+ Returns the children of the parent object.
+ """
return self.child_model.objects.restrict(request.user, "view").filter(
interface=parent,
)
@@ -293,6 +338,10 @@ def get_children(self, request, parent):
class VirtualMachineInterfaceACLInterfaceAssignmentView(
ACLInterfaceAssignmentChildView,
):
+ """
+ Defines the child view for the ACLInterfaceAssignments model.
+ """
+
queryset = VMInterface.objects.prefetch_related("virtual_machine", "tags")
tab = ViewTab(
label="ACL Interface Assignments",
@@ -303,6 +352,9 @@ class VirtualMachineInterfaceACLInterfaceAssignmentView(
)
def get_children(self, request, parent):
+ """
+ Returns the children of the parent object.
+ """
return self.child_model.objects.restrict(request.user, "view").filter(
vminterface=parent,
)
@@ -379,6 +431,10 @@ class ACLStandardRuleDeleteView(generic.ObjectDeleteView):
class ACLStandardRuleBulkDeleteView(generic.BulkDeleteView):
+ """
+ Defines the bulk delete view for the ACLStandardRule django model.
+ """
+
queryset = models.ACLStandardRule.objects.prefetch_related(
"access_list",
"tags",
@@ -463,6 +519,10 @@ class ACLExtendedRuleDeleteView(generic.ObjectDeleteView):
class ACLExtendedRuleBulkDeleteView(generic.BulkDeleteView):
+ """
+ Defines bulk delete view for the ACLExtendedRules django model.
+ """
+
queryset = models.ACLExtendedRule.objects.prefetch_related(
"access_list",
"tags",
diff --git a/setup.py b/setup.py
index 50d4055..5059faa 100644
--- a/setup.py
+++ b/setup.py
@@ -1,21 +1,30 @@
+"""
+Configuration for setuptools.
+"""
import codecs
import os.path
from setuptools import find_packages, setup
-here = os.path.abspath(os.path.dirname(__file__))
+script_dir = os.path.abspath(os.path.dirname(__file__))
-with open(os.path.join(here, "README.md"), encoding="utf-8") as fh:
+with open(os.path.join(script_dir, "README.md"), encoding="utf-8") as fh:
long_description = fh.read()
-def read(rel_path):
- with codecs.open(os.path.join(here, rel_path), "r") as fp:
+def read(relative_path):
+ """
+ Read a file and return its contents.
+ """
+ with codecs.open(os.path.join(script_dir, relative_path), "r") as fp:
return fp.read()
-def get_version(rel_path):
- for line in read(rel_path).splitlines():
+def get_version(relative_path):
+ """
+ Extract the version number from a file without importing it.
+ """
+ for line in read(relative_path).splitlines():
if not line.startswith("__version__"):
raise RuntimeError("Unable to find version string.")
delim = '"' if '"' in line else "'"