diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 11f88e1..21db174 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,7 +4,7 @@ Thank you for sharing your work and for opening a PR. (!) IMPORTANT (!): -First make sure that you point your PR to the `devel` branch! +First make sure that you point your PR to the `dev` branch! Now please read the comments carefully and try to provide information on all relevant titles. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcdc166..7907cd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Lint Code Base - uses: github/super-linter/slim@v5 + uses: github/super-linter/slim@v6 env: DEFAULT_BRANCH: dev GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Dockerfile b/Dockerfile index 43a2c3e..8616db2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG NETBOX_VARIANT=v3.7 +ARG NETBOX_VARIANT=v4.0 FROM netboxcommunity/netbox:${NETBOX_VARIANT} diff --git a/README.md b/README.md index 8a72b6b..7460aaf 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Each Plugin Version listed below has been tested with its corresponding NetBox V | NetBox Version | Plugin Version | |:--------------:|:--------------:| +| >= 4.0.2 | 1.6.0 | | 3.7 | 1.5.0 | | 3.6 | 1.4.0 | | 3.5 | 1.3.0 | @@ -79,6 +80,12 @@ PLUGINS_CONFIG = { } ``` +To add the required `netbox-acls` tables to your NetBox database, run the `migrate` manager subcommand in the NetBox virtual environment: +``` +cd /opt/netbox +sudo ./venv/bin/python3 netbox/manage.py migrate +``` + ## Developing ### VSCode + Docker + Dev Containers diff --git a/netbox_acls/CODEOWNERS b/netbox_acls/CODEOWNERS new file mode 100644 index 0000000..f6c3d1c --- /dev/null +++ b/netbox_acls/CODEOWNERS @@ -0,0 +1 @@ +* @ryanmerolle @abhi1693 @cruse1977 @natm \ No newline at end of file diff --git a/netbox_acls/__init__.py b/netbox_acls/__init__.py index 584592f..6e81b98 100644 --- a/netbox_acls/__init__.py +++ b/netbox_acls/__init__.py @@ -2,7 +2,7 @@ Define the NetBox Plugin """ -from extras.plugins import PluginConfig +from netbox.plugins import PluginConfig from .version import __version__ @@ -17,8 +17,8 @@ class NetBoxACLsConfig(PluginConfig): version = __version__ description = "Manage simple ACLs in NetBox" base_url = "access-lists" - min_version = "3.7.0" - max_version = "3.7.99" + min_version = "4.0.2" + max_version = "4.0.99" config = NetBoxACLsConfig diff --git a/netbox_acls/api/serializers.py b/netbox_acls/api/serializers.py index 241134e..3715599 100644 --- a/netbox_acls/api/serializers.py +++ b/netbox_acls/api/serializers.py @@ -75,6 +75,7 @@ class Meta: "last_updated", "rule_count", ) + brief_fields = ("id", "url", "name", "display") @extend_schema_field(serializers.DictField()) def get_assigned_object(self, obj): @@ -139,6 +140,7 @@ class Meta: "created", "last_updated", ) + brief_fields = ("id", "url", "access_list") @extend_schema_field(serializers.DictField()) def get_assigned_object(self, obj): @@ -212,6 +214,7 @@ class Meta: "last_updated", "source_prefix", ) + brief_fields = ("id", "url", "display") def validate(self, data): """ @@ -221,14 +224,17 @@ def validate(self, data): """ 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 data.get("action") == "remark": + # Check if action set to remark, but no remark set. + if 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) @@ -281,7 +287,7 @@ class Meta: "protocol", "remark", ) - + brief_fields = ("id", "url", "display") def validate(self, data): """ Validate the ACLExtendedRule django model's inputs before allowing it to update the instance: @@ -295,34 +301,37 @@ def validate(self, data): """ 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 data.get("action") == "remark": + # Check if action set to remark, but no remark set. + if 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) diff --git a/netbox_acls/filtersets.py b/netbox_acls/filtersets.py index 40728f4..8b384fe 100644 --- a/netbox_acls/filtersets.py +++ b/netbox_acls/filtersets.py @@ -4,6 +4,7 @@ """ import django_filters from dcim.models import Device, Interface, VirtualChassis +from django.db.models import Q from netbox.filtersets import NetBoxModelFilterSet from virtualization.models import VirtualMachine, VMInterface @@ -80,7 +81,16 @@ def search(self, queryset, name, value): """ Override the default search behavior for the django model. """ - return queryset.filter(description__icontains=value) + query = ( + Q(name__icontains=value) + | Q(device__name__icontains=value) + | Q(virtual_chassis__name__icontains=value) + | Q(virtual_machine__name__icontains=value) + | Q(type__icontains=value) + | Q(default_action__icontains=value) + | Q(comments__icontains=value) + ) + return queryset.filter(query) class ACLInterfaceAssignmentFilterSet(NetBoxModelFilterSet): @@ -131,7 +141,13 @@ def search(self, queryset, name, value): """ Override the default search behavior for the django model. """ - return queryset.filter(description__icontains=value) + query = ( + Q(access_list__name__icontains=value) + | Q(direction__icontains=value) + | Q(interface__name__icontains=value) + | Q(vminterface__name__icontains=value) + ) + return queryset.filter(query) class ACLStandardRuleFilterSet(NetBoxModelFilterSet): @@ -151,7 +167,12 @@ def search(self, queryset, name, value): """ Override the default search behavior for the django model. """ - return queryset.filter(description__icontains=value) + query = ( + Q(access_list__name__icontains=value) + | Q(index__icontains=value) + | Q(action__icontains=value) + ) + return queryset.filter(query) class ACLExtendedRuleFilterSet(NetBoxModelFilterSet): @@ -171,4 +192,10 @@ def search(self, queryset, name, value): """ Override the default search behavior for the django model. """ - return queryset.filter(description__icontains=value) + query = ( + Q(access_list__name__icontains=value) + | Q(index__icontains=value) + | Q(action__icontains=value) + | Q(protocol__icontains=value) + ) + return queryset.filter(query) diff --git a/netbox_acls/graphql/__init__.py b/netbox_acls/graphql/__init__.py index dd2a695..f778a80 100644 --- a/netbox_acls/graphql/__init__.py +++ b/netbox_acls/graphql/__init__.py @@ -1,2 +1,9 @@ from .schema import * from .types import * + +schema = [ + schema.NetBoxACLSAccessListQuery, + schema.NetBoxACLSStandardRuleQuery, + schema.NetBoxACLSACLExtendedRuleQuery +] + diff --git a/netbox_acls/graphql/filters.py b/netbox_acls/graphql/filters.py new file mode 100644 index 0000000..1a2e97f --- /dev/null +++ b/netbox_acls/graphql/filters.py @@ -0,0 +1,30 @@ +import strawberry_django +from .. import filtersets, models +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin + +__all__ = ( + 'AccessListFilter', + 'ACLInterfaceAssignmentFilter', + 'ACLExtendedRuleFilter', + 'ACLStandardRuleFilter', +) + +@strawberry_django.filter(models.AccessList, lookups=True) +@autotype_decorator(filtersets.AccessListFilterSet) +class AccessListFilter(BaseFilterMixin): + pass + +@strawberry_django.filter(models.ACLStandardRule, lookups=True) +@autotype_decorator(filtersets.ACLStandardRuleFilterSet) +class ACLStandardRuleFilter(BaseFilterMixin): + pass + +@strawberry_django.filter(models.ACLExtendedRule, lookups=True) +@autotype_decorator(filtersets.ACLExtendedRuleFilterSet) +class ACLExtendedRuleFilter(BaseFilterMixin): + pass + +@strawberry_django.filter(models.ACLInterfaceAssignment, lookups=True) +@autotype_decorator(filtersets.ACLInterfaceAssignmentFilterSet) +class ACLInterfaceAssignmentFilter(BaseFilterMixin): + pass \ No newline at end of file diff --git a/netbox_acls/graphql/schema.py b/netbox_acls/graphql/schema.py index 0dc2dd2..94b707d 100644 --- a/netbox_acls/graphql/schema.py +++ b/netbox_acls/graphql/schema.py @@ -1,22 +1,32 @@ -from graphene import ObjectType -from netbox.graphql.fields import ObjectField, ObjectListField - +import strawberry +import strawberry_django from .types import * +from ..models import * - -class Query(ObjectType): +@strawberry.type +class NetBoxACLSAccessListQuery: """ Defines the queries available to this plugin via the graphql api. """ + @strawberry.field + def access_list(self, id: int) -> AccessListType: + return AccessList.objects.get(pk=id) + access_list_list: list[AccessListType] = strawberry_django.field() + +@strawberry.type +class NetBoxACLSACLExtendedRuleQuery: + @strawberry.field + def acl_extended_rule(self, id: int) -> ACLExtendedRuleType: + return ACLExtendedRule.objects.get(pk=id) + acl_extended_rule_list: list[ACLExtendedRuleType] = strawberry_django.field() - access_list = ObjectField(AccessListType) - access_list_list = ObjectListField(AccessListType) - acl_extended_rule = ObjectField(ACLExtendedRuleType) - acl_extended_rule_list = ObjectListField(ACLExtendedRuleType) - acl_standard_rule = ObjectField(ACLStandardRuleType) - acl_standard_rule_list = ObjectListField(ACLStandardRuleType) +@strawberry.type +class NetBoxACLSStandardRuleQuery: + @strawberry.field + def acl_standard_rule(self, id: int) -> ACLStandardRuleType: + return ACLStandardRule.objects.get(pk=id) + acl_standard_rule_list: list[ACLStandardRuleType] = strawberry_django.field() -schema = Query diff --git a/netbox_acls/graphql/types.py b/netbox_acls/graphql/types.py index f43774c..2cc4d21 100644 --- a/netbox_acls/graphql/types.py +++ b/netbox_acls/graphql/types.py @@ -2,73 +2,112 @@ Define the object types and queries availble via the graphql api. """ -from netbox.graphql.types import NetBoxObjectType +import strawberry +import strawberry_django -from .. import filtersets, models -__all__ = ( - "AccessListType", - "ACLInterfaceAssignmentType", - "ACLExtendedRuleType", - "ACLStandardRuleType", -) +from typing import Annotated, List, Union +from .filters import * +from .. import models +from netbox.graphql.types import OrganizationalObjectType +@strawberry_django.type( + models.AccessList, + fields='__all__', + filters=AccessListFilter, + exclude=('assigned_object_type', 'assigned_object_id') +) -class AccessListType(NetBoxObjectType): +class AccessListType(OrganizationalObjectType): """ Defines the object type for the django model AccessList. """ + assigned_object_type: Annotated["ContentTypeType", strawberry.lazy("netbox.graphql.types")] + assigned_object: Annotated[Union[ + Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')], + Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')], + ], strawberry.union("ACLAssignmentType")] + class Meta: """ Associates the filterset, fields, and model for the django model AccessList. """ - - model = models.AccessList - fields = "__all__" - filterset_class = filtersets.AccessListFilterSet - - -class ACLInterfaceAssignmentType(NetBoxObjectType): + @strawberry_django.field + def accesslists(self) -> List[Annotated["AccessList", strawberry.lazy('accesslists.graphql.types')]]: + return self.accesslists.all() + +@strawberry_django.type( + models.ACLInterfaceAssignment, + fields='__all__', + exclude=('assigned_object_type', 'assigned_object_id'), + filters=ACLInterfaceAssignmentFilter +) +class ACLInterfaceAssignmentType(OrganizationalObjectType): """ Defines the object type for the django model AccessList. """ + access_list: Annotated["AccessListType", strawberry.lazy("netbox_acls.graphql.types")] + assigned_object_type: Annotated["ContentTypeType", strawberry.lazy("netbox.graphql.types")] + assigned_object: Annotated[Union[ + Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')], + Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')], + ], strawberry.union("ACLInterfaceAssignmentType")] + + + class Meta: """ Associates the filterset, fields, and model for the django model ACLInterfaceAssignment. """ + @strawberry_django.field + def aclinterfaceassignments(self) -> List[Annotated["ACLInterfaceAssignment", strawberry.lazy('aclinterfaceassignments.graphql.types')]]: + return self.aclinterfaceassignments.all() + +@strawberry_django.type( + models.ACLExtendedRule, + fields='__all__', + filters=ACLExtendedRuleFilter +) - model = models.ACLInterfaceAssignment - fields = "__all__" - filterset_class = filtersets.ACLInterfaceAssignmentFilterSet - - -class ACLExtendedRuleType(NetBoxObjectType): +class ACLExtendedRuleType(OrganizationalObjectType): """ Defines the object type for the django model ACLExtendedRule. """ + source_ports: List[int] + destination_ports: List[int] + access_list: Annotated["AccessListType", strawberry.lazy("netbox_acls.graphql.types")] + destination_prefix: Annotated["PrefixType", strawberry.lazy("ipam.graphql.types")] + source_prefix: Annotated["PrefixType", strawberry.lazy("ipam.graphql.types")] class Meta: """ Associates the filterset, fields, and model for the django model ACLExtendedRule. """ + @strawberry_django.field + def aclextendedrules(self) -> List[Annotated["ACLExtendedRule", strawberry.lazy('aclextendedrule.graphql.types')]]: + return self.aclextendedrules.all() - model = models.ACLExtendedRule - fields = "__all__" - filterset_class = filtersets.ACLExtendedRuleFilterSet +@strawberry_django.type( + models.ACLStandardRule, + fields='__all__', + filters=ACLStandardRuleFilter +) -class ACLStandardRuleType(NetBoxObjectType): +class ACLStandardRuleType(OrganizationalObjectType): """ Defines the object type for the django model ACLStandardRule. """ + access_list: Annotated["AccessListType", strawberry.lazy("netbox_acls.graphql.types")] + source_prefix: Annotated["PrefixType", strawberry.lazy("ipam.graphql.types")] class Meta: """ - Associates the filterset, fields, and model for the django model ACLStandardRule. + Associates the filterset, fields, and model for the django model ACLExtendedRule. """ + @strawberry_django.field + def aclstandardrules(self) -> List[Annotated["ACLStandardRule", strawberry.lazy('aclstandardrule.graphql.types')]]: + return self.aclstandardrules.all() - model = models.ACLStandardRule - fields = "__all__" - filterset_class = filtersets.ACLStandardRuleFilterSet diff --git a/netbox_acls/navigation.py b/netbox_acls/navigation.py index 9790365..433e824 100644 --- a/netbox_acls/navigation.py +++ b/netbox_acls/navigation.py @@ -3,8 +3,8 @@ """ from django.conf import settings -from extras.plugins import PluginMenu, PluginMenuButton, PluginMenuItem -from utilities.choices import ButtonColorChoices +from netbox.plugins import PluginMenu, PluginMenuButton, PluginMenuItem + plugin_settings = settings.PLUGINS_CONFIG["netbox_acls"] @@ -21,7 +21,6 @@ link="plugins:netbox_acls:accesslist_add", title="Add", icon_class="mdi mdi-plus-thick", - color=ButtonColorChoices.GREEN, permissions=["netbox_acls.add_accesslist"], ), ), @@ -35,7 +34,6 @@ link="plugins:netbox_acls:aclstandardrule_add", title="Add", icon_class="mdi mdi-plus-thick", - color=ButtonColorChoices.GREEN, permissions=["netbox_acls.add_aclstandardrule"], ), ), @@ -49,7 +47,6 @@ link="plugins:netbox_acls:aclextendedrule_add", title="Add", icon_class="mdi mdi-plus-thick", - color=ButtonColorChoices.GREEN, permissions=["netbox_acls.add_aclextendedrule"], ), ), @@ -63,7 +60,6 @@ link="plugins:netbox_acls:aclinterfaceassignment_add", title="Add", icon_class="mdi mdi-plus-thick", - color=ButtonColorChoices.GREEN, permissions=["netbox_acls.add_aclinterfaceassignment"], ), ), diff --git a/netbox_acls/tests/test_api.py b/netbox_acls/tests/test_api.py index 207f1dd..7d077f7 100644 --- a/netbox_acls/tests/test_api.py +++ b/netbox_acls/tests/test_api.py @@ -44,7 +44,7 @@ def setUpTestData(cls): name="Device 1", site=site, device_type=devicetype, - device_role=devicerole, + role=devicerole, ) access_lists = ( diff --git a/netbox_acls/version.py b/netbox_acls/version.py index c179ed2..df44d33 100644 --- a/netbox_acls/version.py +++ b/netbox_acls/version.py @@ -1 +1 @@ -__version__ = "1.5.0" \ No newline at end of file +__version__ = "1.6.0" \ No newline at end of file diff --git a/setup.py b/setup.py index 030dece..2ac195b 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ script_dir = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(script_dir, "README.md"), encoding="utf-8") as fh: - long_description = fh.read().replace("(docs/img/", "(https://raw.githubusercontent.com/ryanmerolle/netbox-acls/release/docs/img/") + long_description = fh.read().replace("(docs/img/", "(https://raw.githubusercontent.com/netbox-community/netbox-acls/release/docs/img/") def read(relative_path): @@ -37,7 +37,7 @@ def get_version(relative_path): description="A NetBox plugin for Access List management", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/ryanmerolle/netbox-acls", + url="https://github.com/netbox-community/netbox-acls", license="Apache 2.0", install_requires=[], python_requires=">=3.10", @@ -61,7 +61,7 @@ def get_version(relative_path): "Topic :: Internet", ], project_urls={ - "Issues": "https://github.com/ryanmerolle/netbox-acls/issues", - "Source": "https://github.com/ryanmerolle/netbox-acls", + "Issues": "https://github.com/netbox-community/netbox-acls/issues", + "Source": "https://github.com/netbox-community/netbox-acls", }, )