├── CODEOWNERS ├── netbox_aci_plugin ├── api │ ├── __init__.py │ ├── serializers │ │ ├── tenant │ │ │ ├── __init__.py │ │ │ ├── tenants.py │ │ │ ├── app_profiles.py │ │ │ ├── vrfs.py │ │ │ ├── contract_filters.py │ │ │ └── bridge_domains.py │ │ └── __init__.py │ └── urls.py ├── tables │ ├── __init__.py │ └── tenant │ │ ├── __init__.py │ │ ├── tenants.py │ │ ├── app_profiles.py │ │ └── vrfs.py ├── forms │ ├── tenant │ │ └── __init__.py │ ├── widgets │ │ └── misc.py │ └── __init__.py ├── migrations │ ├── __init__.py │ └── 0009_alter_aciendpointgroup_options.py ├── models │ ├── tenant │ │ ├── __init__.py │ │ ├── tenants.py │ │ └── app_profiles.py │ ├── __init__.py │ ├── base.py │ └── mixins.py ├── tests │ ├── api │ │ ├── __init__.py │ │ ├── tenant │ │ │ ├── __init__.py │ │ │ ├── test_tenants.py │ │ │ ├── test_app_profiles.py │ │ │ └── test_vrfs.py │ │ └── test_app.py │ ├── forms │ │ ├── __init__.py │ │ └── tenant │ │ │ ├── __init__.py │ │ │ ├── test_vrfs.py │ │ │ ├── test_tenants.py │ │ │ ├── test_app_profiles.py │ │ │ ├── test_contracts.py │ │ │ ├── test_contract_filters.py │ │ │ └── test_bridge_domains.py │ ├── models │ │ ├── __init__.py │ │ └── tenant │ │ │ ├── __init__.py │ │ │ └── test_tenants.py │ ├── __init__.py │ └── test_plugin.py ├── views │ └── tenant │ │ └── __init__.py ├── filtersets │ ├── tenant │ │ ├── __init__.py │ │ ├── tenants.py │ │ ├── app_profiles.py │ │ └── vrfs.py │ └── __init__.py ├── graphql │ ├── filters │ │ ├── tenant │ │ │ ├── __init__.py │ │ │ ├── tenants.py │ │ │ ├── app_profiles.py │ │ │ ├── vrfs.py │ │ │ ├── endpoint_security_groups.py │ │ │ ├── contract_filters.py │ │ │ └── endpoint_groups.py │ │ ├── mixins.py │ │ └── __init__.py │ ├── __init__.py │ ├── filter_lookups.py │ ├── schema.py │ └── enums.py ├── templates │ └── netbox_aci_plugin │ │ ├── widgets │ │ └── textinput_with_options.html │ │ ├── inc │ │ ├── acivrf │ │ │ ├── bridgedomains.html │ │ │ └── contractrelations.html │ │ ├── acitenant │ │ │ ├── vrfs.html │ │ │ ├── contracts.html │ │ │ ├── endpointgroups.html │ │ │ ├── appprofiles.html │ │ │ ├── bridgedomains.html │ │ │ └── endpointsecuritygroups.html │ │ ├── acicontract │ │ │ ├── relations.html │ │ │ └── subjects.html │ │ ├── acicontractfilter │ │ │ └── entries.html │ │ ├── acicontractsubject │ │ │ └── subjectfilters.html │ │ ├── aciappprofile │ │ │ ├── endpointgroups.html │ │ │ ├── usegendpointgroups.html │ │ │ └── endpointsecuritygroups.html │ │ ├── acibridgedomain │ │ │ ├── endpointgroups.html │ │ │ └── subnets.html │ │ ├── aciusegendpointgroup │ │ │ ├── networkattributes.html │ │ │ └── contractrelations.html │ │ ├── aciendpointgroup │ │ │ └── contractrelations.html │ │ └── aciendpointsecuritygroup │ │ │ ├── contractrelations.html │ │ │ ├── epselectors.html │ │ │ └── epgselectors.html │ │ ├── acitenant.html │ │ ├── aciappprofile.html │ │ ├── acicontractrelation.html │ │ ├── acicontractfilter.html │ │ ├── aciendpointsecuritygroup.html │ │ ├── aciesgendpointselector.html │ │ ├── aciesgendpointgroupselector.html │ │ ├── acicontract.html │ │ ├── aciusegnetworkattribute.html │ │ ├── acicontractsubjectfilter.html │ │ ├── aciusegendpointgroup.html │ │ ├── acivrf.html │ │ ├── aciendpointgroup.html │ │ └── acibridgedomainsubnet.html ├── __init__.py ├── constants.py └── validators.py ├── docs ├── index.md ├── changelog.md ├── security.md └── development │ ├── contributing.md │ ├── code_of_conduct.md │ └── releasing.md ├── requirements_dev.txt ├── CODE_OF_CONDUCT.md ├── Makefile ├── .markdownlint.yaml ├── .editorconfig ├── MANIFEST.in ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── housekeeping.yaml │ ├── bug_report.yaml │ └── feature_request.yaml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── mkdocs.yaml │ └── ci.yaml ├── CHANGELOG.md ├── .ci └── configuration.testing.py ├── .pre-commit-config.yaml ├── pyproject.toml ├── mkdocs.yml ├── ruff.toml ├── SECURITY.md └── .gitignore /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @pheus 2 | -------------------------------------------------------------------------------- /netbox_aci_plugin/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tables/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/forms/tenant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/models/tenant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tables/tenant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/forms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/views/tenant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/filtersets/tenant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/api/tenant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/forms/tenant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/api/serializers/tenant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/filters/tenant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/models/tenant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | {% 2 | include-markdown "../README.md" 3 | %} 4 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | {% 2 | include-markdown "../CHANGELOG.md" 3 | %} 4 | -------------------------------------------------------------------------------- /docs/security.md: -------------------------------------------------------------------------------- 1 | {% 2 | include-markdown "../SECURITY.md" 3 | %} 4 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit test package for netbox_aci_plugin.""" 2 | -------------------------------------------------------------------------------- /docs/development/contributing.md: -------------------------------------------------------------------------------- 1 | {% 2 | include-markdown "../../CONTRIBUTING.md" 3 | %} 4 | -------------------------------------------------------------------------------- /docs/development/code_of_conduct.md: -------------------------------------------------------------------------------- 1 | {% 2 | include-markdown "../../CODE_OF_CONDUCT.md" 3 | %} 4 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/__init__.py: -------------------------------------------------------------------------------- 1 | from .schema import NetBoxACIQuery 2 | 3 | schema = [NetBoxACIQuery] 4 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | check-manifest==0.50 2 | pip==25.0.1 3 | pre-commit==4.3.0 4 | pytest==8.4.1 5 | ruff==0.12.11 6 | yamllint==1.37.1 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | This project follows the [Contributor Covenant 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). 3 | For any unacceptable behavior, please open a private email to the maintainers. 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | sources = netbox_aci_plugin 2 | 3 | .PHONY: test format lint unittest pre-commit clean 4 | test: format lint unittest 5 | 6 | format: 7 | ruff format $(sources) tests 8 | 9 | lint: 10 | ruff check $(sources) tests 11 | 12 | pre-commit: 13 | pre-commit run --all-files 14 | 15 | clean: 16 | rm -rf *.egg-info 17 | rm -rf .tox dist site 18 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Default to enable all rules 3 | default: true 4 | 5 | # MD007: unordered-list-indent 6 | MD007: 7 | indent: 4 8 | 9 | # MD013: line-length 10 | MD013: true 11 | 12 | # MD024: no-duplicate-heading 13 | MD024: 14 | siblings_only: true 15 | 16 | # MD033: no-inline-html 17 | MD033: false 18 | 19 | # MD041: first-line-h1 20 | MD041: false 21 | ... 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.htm,*.html}] 14 | indent_size = 2 15 | 16 | [{*.yml,*.yaml}] 17 | indent_size = 2 18 | 19 | [LICENSE] 20 | insert_final_newline = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/filters/tenant/tenants.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import strawberry_django 6 | 7 | from .... import models 8 | from ..mixins import ACIBaseFilterMixin 9 | 10 | __all__ = ("ACITenantFilter",) 11 | 12 | 13 | @strawberry_django.filter(models.ACITenant, lookups=True) 14 | class ACITenantFilter(ACIBaseFilterMixin): 15 | """GraphQL filter definition for the ACITenant model.""" 16 | 17 | pass 18 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/widgets/textinput_with_options.html: -------------------------------------------------------------------------------- 1 |
2 | {% include "django/forms/widgets/input.html" %} 3 | 6 | 13 |
14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTING.md 2 | include LICENSE 3 | include README.md 4 | include CHANGELOG.md 5 | include Makefile 6 | include mkdocs.yml 7 | 8 | # Package assets for NetBox 9 | recursive-include netbox_aci_plugin/locale * 10 | recursive-include netbox_aci_plugin/templates *.html 11 | recursive-include netbox_aci_plugin/static * 12 | 13 | # Documentation 14 | recursive-include docs * 15 | prune docs/site 16 | 17 | # Tests 18 | recursive-include netbox_aci_plugin/tests * 19 | 20 | # Housekeeping 21 | global-exclude __pycache__ *.py[co] .DS_Store .ruff-cache 22 | prune .ci 23 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acivrf/bridgedomains.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acibridgedomain %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add a Bridge Domain" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acitenant/vrfs.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acivrf %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add a VRF" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acicontract/relations.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acicontractrelation %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add a Relation" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Reference: 3 | # https://help.github.com/en/github/building-a-strong-community/\ 4 | # configuring-issue-templates-for-your-repository 5 | blank_issues_enabled: false 6 | contact_links: 7 | - name: 📖 Contributing Policy 8 | url: https://github.com/pheus/netbox-aci-plugin/blob/main/CONTRIBUTING.md 9 | about: > 10 | Please read through our contributing policy before opening an issue or 11 | pull request. 12 | - name: ❓ Discussion 13 | url: https://github.com/pheus/netbox-aci-plugin/discussions 14 | about: > 15 | If you're just looking for help, try starting a discussion instead. 16 | ... 17 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acicontractfilter/entries.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acicontractfilterentry %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add an Entry" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acitenant/contracts.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acicontract %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add a Contract" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/api/test_app.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.urls import reverse 6 | from rest_framework import status 7 | from utilities.testing import APITestCase 8 | 9 | 10 | class AppTest(APITestCase): 11 | """API test case for NetBox ACI plugin.""" 12 | 13 | def test_root(self) -> None: 14 | """Test API root access of the plugin.""" 15 | url = reverse("plugins-api:netbox_aci_plugin-api:api-root") 16 | response = self.client.get(f"{url}?format=api", **self.header) 17 | self.assertEqual(response.status_code, status.HTTP_200_OK) 18 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acitenant/endpointgroups.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_aciendpointgroup %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add an EPG" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acicontract/subjects.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acicontractsubject %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add a Subject" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acicontractsubject/subjectfilters.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acicontractsubjectfilter %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Assign a Filter" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/aciappprofile/endpointgroups.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_aciendpointgroup %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add an EPG" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acitenant/appprofiles.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_aciappprofile %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add an Application Profile" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acitenant/bridgedomains.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acibridgedomain %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add a Bridge Domain" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/aciappprofile/usegendpointgroups.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_aciendpointgroup %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add an EPG" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acibridgedomain/endpointgroups.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_aciendpointgroup %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add an EPG" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/aciusegendpointgroup/networkattributes.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_aciusegnetworkattribute %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add a Network Attribute" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acitenant/endpointsecuritygroups.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_aciendpointsecuritygroup %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add an ESG" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/aciappprofile/endpointsecuritygroups.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_aciendpointsecuritygroup %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add an ESG" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/). 7 | 8 | --- 9 | 10 | ## [0.1.0] – 2025-09-03 11 | 12 | ### Compatibility 13 | - Tested against NetBox v4.3 and NetBox v4.4. 14 | 15 | ### Added 16 | - First PyPI release of the NetBox ACI plugin. 17 | - Models/UI for Tenants, Application Profiles, EPGs, uSeg EPGs, ESGs, 18 | Bridge Domains, VRFs, Contracts, Contract Subjects, and Contract Filters. 19 | 20 | --- 21 | 22 | [0.1.0]: https://github.com/pheus/netbox-aci-plugin/releases/tag/v0.1.0 23 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acivrf/contractrelations.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acicontractrelation %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add a Relation" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/aciendpointgroup/contractrelations.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acicontractrelation %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Assign a Contract" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/aciusegendpointgroup/contractrelations.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acicontractrelation %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Assign a Contract" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/aciendpointsecuritygroup/contractrelations.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acicontractrelation %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Assign a Contract" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/acibridgedomain/subnets.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_acibridgedomainsubnet %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Add a BD Subnet" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/aciendpointsecuritygroup/epselectors.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_aciesgendpointselector %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Assign an Endpoint" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/inc/aciendpointsecuritygroup/epgselectors.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_children.html' %} 2 | {% load helpers %} 3 | {% load i18n %} 4 | 5 | {% block extra_controls %} 6 | {% if perms.netbox_aci_plugin.add_aciesgendpointgroupselector %} 7 | {% with viewname=object|viewname:"" %} 8 | 9 | {% trans "Assign an EPG" %} 10 | 11 | {% endwith %} 12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | ## Summary 13 | 14 | 15 | ## Screenshots (if UI) 16 | 17 | 18 | ## Checklist 19 | - [ ] Linked to an **accepted** and **assigned** issue (Fixes #NNN) 20 | - [ ] Tests added/updated (if applicable) 21 | - [ ] Docs updated (README/docs and/or docstrings) 22 | - [ ] `pre-commit` passes locally (Ruff, etc.) 23 | - [ ] No version/changelog changes (maintainers handle releases) 24 | -------------------------------------------------------------------------------- /netbox_aci_plugin/forms/widgets/misc.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django import forms 6 | 7 | 8 | class TextInputWithOptions(forms.TextInput): 9 | """Text input form with option dropdown.""" 10 | 11 | template_name = "netbox_aci_plugin/widgets/textinput_with_options.html" 12 | 13 | def __init__(self, options, attrs=None) -> None: 14 | """Initialize the widget.""" 15 | self.options = options 16 | super().__init__(attrs) 17 | 18 | def get_context(self, name, value, attrs) -> dict: 19 | """Get the context for the widget and add options to the context.""" 20 | context = super().get_context(name, value, attrs) 21 | context["widget"]["options"] = self.options 22 | return context 23 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/filters/mixins.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from dataclasses import dataclass 6 | 7 | import strawberry_django 8 | from netbox.graphql.filter_mixins import NetBoxModelFilterMixin 9 | from strawberry_django import FilterLookup 10 | from tenancy.graphql.filter_mixins import TenancyFilterMixin 11 | 12 | __all__ = ("ACIBaseFilterMixin",) 13 | 14 | 15 | @dataclass 16 | class ACIBaseFilterMixin(TenancyFilterMixin, NetBoxModelFilterMixin): 17 | """Base GraphQL filter mixin for ACI models.""" 18 | 19 | name: FilterLookup[str] | None = strawberry_django.filter_field() 20 | name_alias: FilterLookup[str] | None = strawberry_django.filter_field() 21 | description: FilterLookup[str] | None = strawberry_django.filter_field() 22 | -------------------------------------------------------------------------------- /netbox_aci_plugin/migrations/0009_alter_aciendpointgroup_options.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | class Migration(migrations.Migration): 5 | dependencies = [ 6 | ("netbox_aci_plugin", "0008_aciendpointsecuritygroup"), 7 | ] 8 | 9 | operations = [ 10 | migrations.AlterModelOptions( 11 | name="aciendpointgroup", 12 | options={ 13 | "default_related_name": "aci_endpoint_groups", 14 | "ordering": ("aci_app_profile", "name"), 15 | }, 16 | ), 17 | migrations.AlterModelOptions( 18 | name="aciusegendpointgroup", 19 | options={ 20 | "default_related_name": "aci_useg_endpoint_groups", 21 | "ordering": ("aci_app_profile", "name"), 22 | }, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /netbox_aci_plugin/models/tenant/tenants.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.db import models 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | from ..base import ACIBaseModel 9 | 10 | 11 | class ACITenant(ACIBaseModel): 12 | """NetBox model for ACI Tenant.""" 13 | 14 | class Meta: 15 | constraints: list[models.UniqueConstraint] = [ 16 | models.UniqueConstraint( 17 | fields=("name",), 18 | name="%(app_label)s_%(class)s_unique_name", 19 | ), 20 | ] 21 | ordering: tuple = ("name",) 22 | verbose_name: str = _("ACI Tenant") 23 | 24 | def parent_object(self) -> ACIBaseModel | None: 25 | """Return the parent object of the instance.""" 26 | return None 27 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/filter_lookups.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import strawberry 6 | from netbox.graphql.filter_lookups import ArrayLookup 7 | 8 | from .enums import ContractFilterTCPRulesEnum 9 | 10 | 11 | @strawberry.input( 12 | one_of=True, 13 | description="Lookup for Array fields. Only one of the lookup fields can be set.", 14 | ) 15 | class TCPRulesArrayLookup(ArrayLookup[ContractFilterTCPRulesEnum]): 16 | """Represents a specialized lookup functionality for TCP rules in an array. 17 | 18 | This class is used to perform lookups on array fields specifically designed 19 | for TCP rules. 20 | It enforces that only one of the lookup fields can be set at a time, 21 | ensuring clearer and more valid querying operations. 22 | """ 23 | 24 | pass 25 | -------------------------------------------------------------------------------- /netbox_aci_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for NetBox ACI Plugin.""" 2 | 3 | __author__ = """Martin Hauser""" 4 | __email__ = "git@pheus.dev" 5 | __version__ = "0.1.0" 6 | 7 | 8 | from netbox.plugins import PluginConfig 9 | 10 | 11 | class ACIConfig(PluginConfig): 12 | """NetBox ACI Plugin specific configuration.""" 13 | 14 | name = "netbox_aci_plugin" 15 | label = "netbox_aci_plugin" 16 | verbose_name = "NetBox ACI" 17 | description = "NetBox plugin for documenting Cisco ACI specific objects." 18 | version = __version__ 19 | author = __author__ 20 | author_email = __email__ 21 | base_url = "aci" 22 | min_version = "4.3.0" 23 | max_version = "4.4.99" 24 | default_settings = { 25 | "create_default_aci_tenants": True, 26 | "create_default_aci_contract_filters": True, 27 | } 28 | 29 | 30 | config = ACIConfig 31 | -------------------------------------------------------------------------------- /.github/workflows/mkdocs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deploy docs 📚 to GitHub pages 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | deploy: 14 | name: Build and deploy docs 📚 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 🧩 19 | uses: actions/checkout@v4 20 | - name: Set up Python 🐍 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.x" 24 | cache: "pip" 25 | - name: Install MKDocs and plugins 📚 26 | run: >- 27 | pip install 28 | mkdocs-material 29 | mkdocs-autorefs 30 | mkdocs-material-extensions 31 | mkdocstrings 32 | mkdocstrings-python-legacy 33 | mkdocs-include-markdown-plugin 34 | - name: Deploy docs to GitHub pages 📚 35 | run: | 36 | mkdocs gh-deploy --force 37 | ... 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/housekeeping.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🏡 Housekeeping 3 | description: A change pertaining to the codebase itself (developers only) 4 | labels: ["type: housekeeping"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: > 9 | **NOTE:** This template is for use by maintainers only. Please do not 10 | submit an issue using this template unless you have been specifically 11 | asked to do so. 12 | - type: textarea 13 | attributes: 14 | label: Proposed Changes 15 | description: > 16 | Describe in detail the new feature or behavior you'd like to propose. 17 | Include any specific changes to work flows, data models, or the user 18 | interface. 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: Justification 24 | description: Please provide justification for the proposed change(s). 25 | validations: 26 | required: true 27 | ... 28 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/filters/tenant/app_profiles.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from typing import TYPE_CHECKING, Annotated 6 | 7 | import strawberry 8 | import strawberry_django 9 | from strawberry.scalars import ID 10 | 11 | from .... import models 12 | from ..mixins import ACIBaseFilterMixin 13 | 14 | if TYPE_CHECKING: 15 | from .tenants import ACITenantFilter 16 | 17 | 18 | __all__ = ("ACIAppProfileFilter",) 19 | 20 | 21 | @strawberry_django.filter(models.ACIAppProfile, lookups=True) 22 | class ACIAppProfileFilter(ACIBaseFilterMixin): 23 | """GraphQL filter definition for the ACIAppProfile model.""" 24 | 25 | aci_tenant: ( 26 | Annotated[ 27 | "ACITenantFilter", 28 | strawberry.lazy("netbox_aci_plugin.graphql.filters"), 29 | ] 30 | | None 31 | ) = strawberry_django.filter_field() 32 | aci_tenant_id: ID | None = strawberry_django.filter_field() 33 | -------------------------------------------------------------------------------- /docs/development/releasing.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | We follow **Semantic Versioning** and **Keep a Changelog**. 4 | 5 | - SemVer → https://semver.org/ 6 | - Keep a Changelog → https://keepachangelog.com/en/1.1.0/ 7 | 8 | ## Checklist 9 | 10 | 1. **Finish code/docs** 11 | - README shows `pip install netbox-aci-plugin`. 12 | - `docs/index.md` Installing & Compatibility are correct. 13 | 2. **Changelog** 14 | - Add `## X.Y.Z – YYYY-MM-DD` with **Added / Changed / Fixed / Compatibility**. 15 | 3. **Version bump** 16 | - `pyproject.toml` → `version = "X.Y.Z"`. 17 | 4. **Tag** 18 | - `git tag vX.Y.Z && git push origin vX.Y.Z` 19 | 5. **CI** 20 | - Build wheel+sdist, `twine check`, publish to PyPI (Trusted Publishing), draft GitHub Release with only the current section from the changelog. 21 | 6. **Publish Release** 22 | - Review the draft notes and click **Publish**. 23 | 7. **Post-release** 24 | - Verify the PyPI page renders the README. 25 | - Announce compatibility (NetBox 4.3) if it changed. 26 | -------------------------------------------------------------------------------- /.ci/configuration.testing.py: -------------------------------------------------------------------------------- 1 | ############################################################### 2 | # This NetBox configuration file serves as a base for testing # 3 | # purposes only. It is not intended for production use. # 4 | ############################################################### 5 | 6 | ALLOWED_HOSTS = ["*"] 7 | 8 | DATABASE = { 9 | "NAME": "netbox", 10 | "USER": "netbox", 11 | "PASSWORD": "netbox", 12 | "HOST": "localhost", 13 | "PORT": "", 14 | "CONN_MAX_AGE": 300, 15 | } 16 | 17 | PLUGINS = [ 18 | "netbox_aci_plugin", 19 | ] 20 | 21 | REDIS = { 22 | "tasks": { 23 | "HOST": "localhost", 24 | "PORT": 6379, 25 | "PASSWORD": "", 26 | "DATABASE": 0, 27 | "SSL": False, 28 | }, 29 | "caching": { 30 | "HOST": "localhost", 31 | "PORT": 6379, 32 | "PASSWORD": "", 33 | "DATABASE": 1, 34 | "SSL": False, 35 | }, 36 | } 37 | 38 | SECRET_KEY = "5i1(eGhHM_!*&E9-7rJ2y8wF8EA3iNvhRU#X&990-WJE&eT@@7" 39 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ci: 3 | autoupdate_commit_msg: "chore: update pre-commit hooks" 4 | autofix_commit_msg: "style: pre-commit fixes" 5 | 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: "v6.0.0" 9 | hooks: 10 | - id: check-docstring-first 11 | - id: check-merge-conflict 12 | - id: check-symlinks 13 | - id: end-of-file-fixer 14 | - id: trailing-whitespace 15 | - id: check-yaml 16 | files: \.(yaml|yml)$ 17 | args: [--unsafe] 18 | - id: check-added-large-files 19 | - id: name-tests-test 20 | args: 21 | - "--django" 22 | - id: requirements-txt-fixer 23 | 24 | - repo: https://github.com/astral-sh/ruff-pre-commit 25 | rev: "v0.12.11" 26 | hooks: 27 | # Run the linter. 28 | - id: ruff-check 29 | args: [--fix] 30 | # Run the formatter. 31 | - id: ruff-format 32 | 33 | - repo: https://github.com/adrienverge/yamllint 34 | rev: "v1.37.1" 35 | hooks: 36 | - id: yamllint 37 | files: \.(yaml|yml)$ 38 | 39 | - repo: https://github.com/mgedmin/check-manifest 40 | rev: "0.50" 41 | hooks: 42 | - id: check-manifest 43 | stages: [manual] 44 | ... 45 | -------------------------------------------------------------------------------- /netbox_aci_plugin/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .tenant.app_profiles import ACIAppProfile 2 | from .tenant.bridge_domains import ACIBridgeDomain, ACIBridgeDomainSubnet 3 | from .tenant.contract_filters import ACIContractFilter, ACIContractFilterEntry 4 | from .tenant.contracts import ( 5 | ACIContract, 6 | ACIContractRelation, 7 | ACIContractSubject, 8 | ACIContractSubjectFilter, 9 | ) 10 | from .tenant.endpoint_groups import ( 11 | ACIEndpointGroup, 12 | ACIUSegEndpointGroup, 13 | ACIUSegNetworkAttribute, 14 | ) 15 | from .tenant.endpoint_security_groups import ( 16 | ACIEndpointSecurityGroup, 17 | ACIEsgEndpointGroupSelector, 18 | ACIEsgEndpointSelector, 19 | ) 20 | from .tenant.tenants import ACITenant 21 | from .tenant.vrfs import ACIVRF 22 | 23 | __all__ = ( 24 | "ACIAppProfile", 25 | "ACIBridgeDomain", 26 | "ACIBridgeDomainSubnet", 27 | "ACIContract", 28 | "ACIContractRelation", 29 | "ACIContractSubject", 30 | "ACIContractSubjectFilter", 31 | "ACIContractFilter", 32 | "ACIContractFilterEntry", 33 | "ACIEndpointGroup", 34 | "ACIEndpointSecurityGroup", 35 | "ACIEsgEndpointGroupSelector", 36 | "ACIEsgEndpointSelector", 37 | "ACITenant", 38 | "ACIUSegEndpointGroup", 39 | "ACIUSegNetworkAttribute", 40 | "ACIVRF", 41 | ) 42 | -------------------------------------------------------------------------------- /netbox_aci_plugin/models/tenant/app_profiles.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.db import models 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | from ..base import ACIBaseModel 9 | from .tenants import ACITenant 10 | 11 | 12 | class ACIAppProfile(ACIBaseModel): 13 | """NetBox model for ACI Application Profile.""" 14 | 15 | aci_tenant = models.ForeignKey( 16 | to=ACITenant, 17 | on_delete=models.PROTECT, 18 | related_name="aci_app_profiles", 19 | verbose_name=_("ACI Tenant"), 20 | ) 21 | 22 | clone_fields: tuple = ACIBaseModel.clone_fields + ("aci_tenant",) 23 | prerequisite_models: tuple = ("netbox_aci_plugin.ACITenant",) 24 | 25 | class Meta: 26 | constraints: list[models.UniqueConstraint] = [ 27 | models.UniqueConstraint( 28 | fields=("aci_tenant", "name"), 29 | name="%(app_label)s_%(class)s_unique_per_aci_tenant", 30 | ), 31 | ] 32 | ordering: tuple = ("aci_tenant", "name") 33 | verbose_name: str = _("ACI Application Profile") 34 | 35 | @property 36 | def parent_object(self) -> ACIBaseModel: 37 | """Return the parent object of the instance.""" 38 | return self.aci_tenant 39 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tables/tenant/tenants.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import django_tables2 as tables 6 | from django.utils.translation import gettext_lazy as _ 7 | from netbox.tables import NetBoxTable, columns 8 | 9 | from ...models.tenant.tenants import ACITenant 10 | 11 | 12 | class ACITenantTable(NetBoxTable): 13 | """NetBox table for the ACI Tenant model.""" 14 | 15 | name = tables.Column( 16 | verbose_name=_("ACI Tenant"), 17 | linkify=True, 18 | ) 19 | name_alias = tables.Column( 20 | verbose_name=_("Alias"), 21 | linkify=True, 22 | ) 23 | nb_tenant = tables.Column( 24 | linkify=True, 25 | ) 26 | tags = columns.TagColumn() 27 | comments = columns.MarkdownColumn() 28 | 29 | class Meta(NetBoxTable.Meta): 30 | model = ACITenant 31 | fields: tuple = ( 32 | "pk", 33 | "id", 34 | "name", 35 | "name_alias", 36 | "nb_tenant", 37 | "description", 38 | "tags", 39 | "comments", 40 | ) 41 | default_columns: tuple = ( 42 | "name", 43 | "name_alias", 44 | "nb_tenant", 45 | "description", 46 | "tags", 47 | ) 48 | -------------------------------------------------------------------------------- /netbox_aci_plugin/api/serializers/tenant/tenants.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from netbox.api.serializers import NetBoxModelSerializer 6 | from rest_framework import serializers 7 | from tenancy.api.serializers import TenantSerializer 8 | 9 | from ....models.tenant.tenants import ACITenant 10 | 11 | 12 | class ACITenantSerializer(NetBoxModelSerializer): 13 | """Serializer for the ACI Tenant model.""" 14 | 15 | url = serializers.HyperlinkedIdentityField( 16 | view_name="plugins-api:netbox_aci_plugin-api:acitenant-detail" 17 | ) 18 | nb_tenant = TenantSerializer(nested=True, required=False, allow_null=True) 19 | 20 | class Meta: 21 | model = ACITenant 22 | fields: tuple = ( 23 | "id", 24 | "url", 25 | "display", 26 | "name", 27 | "name_alias", 28 | "description", 29 | "nb_tenant", 30 | "comments", 31 | "tags", 32 | "custom_fields", 33 | "created", 34 | "last_updated", 35 | ) 36 | brief_fields: tuple = ( 37 | "id", 38 | "url", 39 | "display", 40 | "name", 41 | "name_alias", 42 | "description", 43 | "nb_tenant", 44 | ) 45 | -------------------------------------------------------------------------------- /netbox_aci_plugin/constants.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.db.models import Q 6 | 7 | # 8 | # Contract Relation 9 | # 10 | 11 | # Contract relation to possible ACI object types 12 | CONTRACT_RELATION_OBJECT_TYPES = Q( 13 | app_label="netbox_aci_plugin", 14 | model__in=( 15 | "aciendpointgroup", 16 | "aciendpointsecuritygroup", 17 | "aciusegendpointgroup", 18 | "acivrf", 19 | ), 20 | ) 21 | 22 | 23 | # 24 | # Endpoint Security Group 25 | # 26 | 27 | # Endpoint Group (EPG) Selectors 28 | ESG_ENDPOINT_GROUP_SELECTORS_MODELS = Q( 29 | Q( 30 | app_label="netbox_aci_plugin", 31 | model__in=( 32 | "aciendpointgroup", 33 | "aciusegendpointgroup", 34 | ), 35 | ) 36 | ) 37 | 38 | # IP Subnet Selectors 39 | ESG_ENDPOINT_SELECTORS_MODELS = Q( 40 | Q( 41 | app_label="ipam", 42 | model__in=( 43 | "prefix", 44 | "ipaddress", 45 | ), 46 | ) 47 | ) 48 | 49 | 50 | # 51 | # uSeg Endpoint Group Attributes 52 | # 53 | 54 | # Network Attributes 55 | USEG_NETWORK_ATTRIBUTES_MODELS = Q( 56 | Q( 57 | app_label="ipam", 58 | model__in=( 59 | "prefix", 60 | "ipaddress", 61 | ), 62 | ) 63 | | Q(app_label="dcim", model="macaddress") 64 | ) 65 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/acitenant.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load i18n %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 |

{% trans "ACI Tenant" %}

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 29 |
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "NetBox Tenant" %} 23 | {% if object.nb_tenant.group %} 24 | {{ object.nb_tenant.group|linkify }} / 25 | {% endif %} 26 | {{ object.nb_tenant|linkify|placeholder }} 27 |
30 |
31 | {% include 'inc/panels/custom_fields.html' %} 32 | {% include 'inc/panels/tags.html' %} 33 | {% include 'inc/panels/comments.html' %}
34 |
35 | {% include 'inc/panels/related_objects.html' %} 36 |
37 |
38 | {% endblock content %} 39 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tables/tenant/app_profiles.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import django_tables2 as tables 6 | from django.utils.translation import gettext_lazy as _ 7 | from netbox.tables import NetBoxTable, columns 8 | 9 | from ...models.tenant.app_profiles import ACIAppProfile 10 | 11 | 12 | class ACIAppProfileTable(NetBoxTable): 13 | """NetBox table for the ACI Application Profile model.""" 14 | 15 | name = tables.Column( 16 | verbose_name=_("Application Profile"), 17 | linkify=True, 18 | ) 19 | name_alias = tables.Column( 20 | verbose_name=_("Alias"), 21 | linkify=True, 22 | ) 23 | aci_tenant = tables.Column( 24 | linkify=True, 25 | ) 26 | nb_tenant = tables.Column( 27 | linkify=True, 28 | ) 29 | tags = columns.TagColumn() 30 | comments = columns.MarkdownColumn() 31 | 32 | class Meta(NetBoxTable.Meta): 33 | model = ACIAppProfile 34 | fields: tuple = ( 35 | "pk", 36 | "id", 37 | "name", 38 | "name_alias", 39 | "aci_tenant", 40 | "nb_tenant", 41 | "description", 42 | "tags", 43 | "comments", 44 | ) 45 | default_columns: tuple = ( 46 | "name", 47 | "name_alias", 48 | "aci_tenant", 49 | "nb_tenant", 50 | "description", 51 | "tags", 52 | ) 53 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/filters/__init__.py: -------------------------------------------------------------------------------- 1 | from .tenant.app_profiles import ACIAppProfileFilter 2 | from .tenant.bridge_domains import ( 3 | ACIBridgeDomainFilter, 4 | ACIBridgeDomainSubnetFilter, 5 | ) 6 | from .tenant.contract_filters import ( 7 | ACIContractFilterEntryFilter, 8 | ACIContractFilterFilter, 9 | ) 10 | from .tenant.contracts import ( 11 | ACIContractFilter, 12 | ACIContractRelationFilter, 13 | ACIContractSubjectFilter, 14 | ACIContractSubjectFilterFilter, 15 | ) 16 | from .tenant.endpoint_groups import ( 17 | ACIEndpointGroupFilter, 18 | ACIUSegEndpointGroupFilter, 19 | ACIUSegNetworkAttributeFilter, 20 | ) 21 | from .tenant.endpoint_security_groups import ( 22 | ACIEndpointSecurityGroupFilter, 23 | ACIEsgEndpointGroupSelectorFilter, 24 | ACIEsgEndpointSelectorFilter, 25 | ) 26 | from .tenant.tenants import ACITenantFilter 27 | from .tenant.vrfs import ACIVRFFilter 28 | 29 | __all__ = ( 30 | "ACIAppProfileFilter", 31 | "ACIBridgeDomainFilter", 32 | "ACIBridgeDomainSubnetFilter", 33 | "ACIContractFilterEntryFilter", 34 | "ACIContractFilterFilter", 35 | "ACIContractFilter", 36 | "ACIContractRelationFilter", 37 | "ACIContractSubjectFilter", 38 | "ACIContractSubjectFilterFilter", 39 | "ACIEndpointGroupFilter", 40 | "ACIEndpointSecurityGroupFilter", 41 | "ACIEsgEndpointGroupSelectorFilter", 42 | "ACIEsgEndpointSelectorFilter", 43 | "ACITenantFilter", 44 | "ACIUSegEndpointGroupFilter", 45 | "ACIUSegNetworkAttributeFilter", 46 | "ACIVRFFilter", 47 | ) 48 | -------------------------------------------------------------------------------- /netbox_aci_plugin/api/serializers/tenant/app_profiles.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from netbox.api.serializers import NetBoxModelSerializer 6 | from rest_framework import serializers 7 | from tenancy.api.serializers import TenantSerializer 8 | 9 | from ....models.tenant.app_profiles import ACIAppProfile 10 | from .tenants import ACITenantSerializer 11 | 12 | 13 | class ACIAppProfileSerializer(NetBoxModelSerializer): 14 | """Serializer for the ACI Application Profile model.""" 15 | 16 | url = serializers.HyperlinkedIdentityField( 17 | view_name="plugins-api:netbox_aci_plugin-api:aciappprofile-detail" 18 | ) 19 | aci_tenant = ACITenantSerializer(nested=True, required=True) 20 | nb_tenant = TenantSerializer(nested=True, required=False, allow_null=True) 21 | 22 | class Meta: 23 | model = ACIAppProfile 24 | fields: tuple = ( 25 | "id", 26 | "url", 27 | "display", 28 | "name", 29 | "name_alias", 30 | "description", 31 | "aci_tenant", 32 | "nb_tenant", 33 | "comments", 34 | "tags", 35 | "custom_fields", 36 | "created", 37 | "last_updated", 38 | ) 39 | brief_fields: tuple = ( 40 | "id", 41 | "url", 42 | "display", 43 | "name", 44 | "name_alias", 45 | "description", 46 | "aci_tenant", 47 | "nb_tenant", 48 | ) 49 | -------------------------------------------------------------------------------- /netbox_aci_plugin/filtersets/tenant/tenants.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import django_filters 6 | from django.db.models import Q 7 | from django.utils.translation import gettext_lazy as _ 8 | from netbox.filtersets import NetBoxModelFilterSet 9 | from tenancy.models import Tenant 10 | 11 | from ...models.tenant.tenants import ACITenant 12 | 13 | 14 | class ACITenantFilterSet(NetBoxModelFilterSet): 15 | """Filter set for the ACI Tenant model.""" 16 | 17 | nb_tenant = django_filters.ModelMultipleChoiceFilter( 18 | field_name="nb_tenant__name", 19 | queryset=Tenant.objects.all(), 20 | to_field_name="name", 21 | label=_("NetBox tenant (name)"), 22 | ) 23 | nb_tenant_id = django_filters.ModelMultipleChoiceFilter( 24 | queryset=Tenant.objects.all(), 25 | to_field_name="id", 26 | label=_("NetBox tenant (ID)"), 27 | ) 28 | 29 | class Meta: 30 | model = ACITenant 31 | fields: tuple = ( 32 | "id", 33 | "name", 34 | "name_alias", 35 | "description", 36 | "nb_tenant", 37 | ) 38 | 39 | def search(self, queryset, name, value): 40 | """Return a QuerySet filtered by the model's description.""" 41 | if not value.strip(): 42 | return queryset 43 | queryset_filter: Q = ( 44 | Q(name__icontains=value) 45 | | Q(name_alias__icontains=value) 46 | | Q(description__icontains=value) 47 | ) 48 | return queryset.filter(queryset_filter) 49 | -------------------------------------------------------------------------------- /netbox_aci_plugin/api/urls.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from netbox.api.routers import NetBoxRouter 6 | 7 | from . import views 8 | 9 | app_name = "netbox_aci_plugin" 10 | router = NetBoxRouter() 11 | router.register("tenants", views.ACITenantListViewSet) 12 | router.register("app-profiles", views.ACIAppProfileListViewSet) 13 | router.register("bridge-domains", views.ACIBridgeDomainListViewSet) 14 | router.register("bridge-domain-subnets", views.ACIBridgeDomainSubnetListViewSet) 15 | router.register("endpoint-groups", views.ACIEndpointGroupListViewSet) 16 | router.register("useg-endpoint-groups", views.ACIUSegEndpointGroupListViewSet) 17 | router.register("useg-network-attributes", views.ACIUSegNetworkAttributeListViewSet) 18 | router.register("endpoint-security-groups", views.ACIEndpointSecurityGroupListViewSet) 19 | router.register( 20 | "esg-endpoint-group-selector", views.ACIEsgEndpointGroupSelectorListViewSet 21 | ) 22 | router.register("esg-endpoint-selector", views.ACIEsgEndpointSelectorListViewSet) 23 | router.register("vrfs", views.ACIVRFListViewSet) 24 | router.register("contract-filters", views.ACIContractFilterListViewSet) 25 | router.register("contract-filter-entries", views.ACIContractFilterEntryListViewSet) 26 | router.register("contracts", views.ACIContractListViewSet) 27 | router.register("contract-relations", views.ACIContractRelationListViewSet) 28 | router.register("contract-subjects", views.ACIContractSubjectListViewSet) 29 | router.register("contract-subject-filters", views.ACIContractSubjectFilterListViewSet) 30 | 31 | urlpatterns = router.urls 32 | -------------------------------------------------------------------------------- /netbox_aci_plugin/filtersets/__init__.py: -------------------------------------------------------------------------------- 1 | from .tenant.app_profiles import ACIAppProfileFilterSet 2 | from .tenant.bridge_domains import ( 3 | ACIBridgeDomainFilterSet, 4 | ACIBridgeDomainSubnetFilterSet, 5 | ) 6 | from .tenant.contract_filters import ( 7 | ACIContractFilterEntryFilterSet, 8 | ACIContractFilterFilterSet, 9 | ) 10 | from .tenant.contracts import ( 11 | ACIContractFilterSet, 12 | ACIContractRelationFilterSet, 13 | ACIContractSubjectFilterFilterSet, 14 | ACIContractSubjectFilterSet, 15 | ) 16 | from .tenant.endpoint_groups import ( 17 | ACIEndpointGroupFilterSet, 18 | ACIUSegEndpointGroupFilterSet, 19 | ACIUSegNetworkAttributeFilterSet, 20 | ) 21 | from .tenant.endpoint_security_groups import ( 22 | ACIEndpointSecurityGroupFilterSet, 23 | ACIEsgEndpointGroupSelectorFilterSet, 24 | ACIEsgEndpointSelectorFilterSet, 25 | ) 26 | from .tenant.tenants import ACITenantFilterSet 27 | from .tenant.vrfs import ACIVRFFilterSet 28 | 29 | __all__ = ( 30 | "ACIAppProfileFilterSet", 31 | "ACIBridgeDomainFilterSet", 32 | "ACIBridgeDomainSubnetFilterSet", 33 | "ACIContractFilterSet", 34 | "ACIContractRelationFilterSet", 35 | "ACIContractSubjectFilterSet", 36 | "ACIContractSubjectFilterFilterSet", 37 | "ACIContractFilterFilterSet", 38 | "ACIContractFilterEntryFilterSet", 39 | "ACIEndpointGroupFilterSet", 40 | "ACIEndpointSecurityGroupFilterSet", 41 | "ACIEsgEndpointGroupSelectorFilterSet", 42 | "ACIEsgEndpointSelectorFilterSet", 43 | "ACITenantFilterSet", 44 | "ACIUSegEndpointGroupFilterSet", 45 | "ACIUSegNetworkAttributeFilterSet", 46 | "ACIVRFFilterSet", 47 | ) 48 | -------------------------------------------------------------------------------- /netbox_aci_plugin/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .tenant.app_profiles import ACIAppProfileFilterForm 2 | from .tenant.bridge_domains import ( 3 | ACIBridgeDomainFilterForm, 4 | ACIBridgeDomainSubnetFilterForm, 5 | ) 6 | from .tenant.contract_filters import ( 7 | ACIContractFilterEntryFilterForm, 8 | ACIContractFilterFilterForm, 9 | ) 10 | from .tenant.contracts import ( 11 | ACIContractFilterForm, 12 | ACIContractRelationFilterForm, 13 | ACIContractSubjectFilterFilterForm, 14 | ACIContractSubjectFilterForm, 15 | ) 16 | from .tenant.endpoint_groups import ( 17 | ACIEndpointGroupFilterForm, 18 | ACIUSegEndpointGroupFilterForm, 19 | ACIUSegNetworkAttributeFilterForm, 20 | ) 21 | from .tenant.endpoint_security_groups import ( 22 | ACIEndpointSecurityGroupFilterForm, 23 | ACIEsgEndpointGroupSelectorFilterForm, 24 | ACIEsgEndpointSelectorFilterForm, 25 | ) 26 | from .tenant.tenants import ACITenantFilterForm 27 | from .tenant.vrfs import ACIVRFFilterForm 28 | 29 | __all__ = ( 30 | "ACIAppProfileFilterForm", 31 | "ACIBridgeDomainFilterForm", 32 | "ACIBridgeDomainSubnetFilterForm", 33 | "ACIContractFilterForm", 34 | "ACIContractRelationFilterForm", 35 | "ACIContractSubjectFilterForm", 36 | "ACIContractSubjectFilterFilterForm", 37 | "ACIContractFilterFilterForm", 38 | "ACIContractFilterEntryFilterForm", 39 | "ACIEndpointGroupFilterForm", 40 | "ACIEndpointSecurityGroupFilterForm", 41 | "ACIEsgEndpointGroupSelectorFilterForm", 42 | "ACIEsgEndpointSelectorFilterForm", 43 | "ACITenantFilterForm", 44 | "ACIUSegEndpointGroupFilterForm", 45 | "ACIUSegNetworkAttributeFilterForm", 46 | "ACIVRFFilterForm", 47 | ) 48 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/aciappprofile.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load i18n %} 5 | 6 | {% block breadcrumbs %} 7 | {{ block.super }} 8 | 9 | {% endblock breadcrumbs %} 10 | 11 | {% block content %} 12 |
13 |
14 |
15 |

{% trans "ACI Application Profile" %}

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 37 | 38 |
{% trans "ACI Tenant" %}{{ object.aci_tenant|linkify|placeholder }}
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "NetBox Tenant" %} 32 | {% if object.nb_tenant.group %} 33 | {{ object.nb_tenant.group|linkify }} / 34 | {% endif %} 35 | {{ object.nb_tenant|linkify|placeholder }} 36 |
39 |
40 | {% include 'inc/panels/custom_fields.html' %} 41 |
42 |
43 | {% include 'inc/panels/tags.html' %} 44 | {% include 'inc/panels/comments.html' %} 45 |
46 |
47 | {% endblock content %} 48 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/forms/tenant/test_vrfs.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.test import TestCase 6 | 7 | from ....forms.tenant.vrfs import ACIVRFEditForm 8 | 9 | 10 | class ACIVRFFormTestCase(TestCase): 11 | """Test case for ACIVRF form.""" 12 | 13 | name_error_message: str = ( 14 | "Only alphanumeric characters, hyphens, periods and underscores are allowed." 15 | ) 16 | description_error_message: str = ( 17 | "Only alphanumeric characters and !#$%()*,-./:;@ _{|}~?&+ are allowed." 18 | ) 19 | 20 | def test_invalid_aci_vrf_field_values(self) -> None: 21 | """Test validation of invalid ACI VRF field values.""" 22 | aci_vrf_form = ACIVRFEditForm( 23 | data={ 24 | "name": "ACI VRF Test 1", 25 | "name_alias": "ACI Test Alias 1", 26 | "description": "Invalid Description: ö", 27 | } 28 | ) 29 | self.assertEqual(aci_vrf_form.errors["name"], [self.name_error_message]) 30 | self.assertEqual(aci_vrf_form.errors["name_alias"], [self.name_error_message]) 31 | self.assertEqual( 32 | aci_vrf_form.errors["description"], 33 | [self.description_error_message], 34 | ) 35 | 36 | def test_valid_aci_vrf_field_values(self) -> None: 37 | """Test validation of valid ACI VRF field values.""" 38 | aci_vrf_form = ACIVRFEditForm( 39 | data={ 40 | "name": "ACIVRF1", 41 | "name_alias": "Testing", 42 | "description": "VRF for NetBox ACI Plugin", 43 | } 44 | ) 45 | self.assertEqual(aci_vrf_form.errors.get("name"), None) 46 | self.assertEqual(aci_vrf_form.errors.get("name_alias"), None) 47 | self.assertEqual(aci_vrf_form.errors.get("description"), None) 48 | -------------------------------------------------------------------------------- /netbox_aci_plugin/api/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | from .tenant.app_profiles import ACIAppProfileSerializer 2 | from .tenant.bridge_domains import ( 3 | ACIBridgeDomainSerializer, 4 | ACIBridgeDomainSubnetSerializer, 5 | ) 6 | from .tenant.contract_filters import ( 7 | ACIContractFilterEntrySerializer, 8 | ACIContractFilterSerializer, 9 | ) 10 | from .tenant.contracts import ( 11 | ACIContractRelationSerializer, 12 | ACIContractSerializer, 13 | ACIContractSubjectFilterSerializer, 14 | ACIContractSubjectSerializer, 15 | ) 16 | from .tenant.endpoint_groups import ( 17 | ACIEndpointGroupSerializer, 18 | ACIUSegEndpointGroupSerializer, 19 | ACIUSegNetworkAttributeSerializer, 20 | ) 21 | from .tenant.endpoint_security_groups import ( 22 | ACIEndpointSecurityGroupSerializer, 23 | ACIEsgEndpointGroupSelectorSerializer, 24 | ACIEsgEndpointSelectorSerializer, 25 | ) 26 | from .tenant.tenants import ACITenantSerializer 27 | from .tenant.vrfs import ACIVRFSerializer 28 | 29 | __all__ = ( 30 | # From app_profiles 31 | "ACIAppProfileSerializer", 32 | "ACIEndpointGroupSerializer", 33 | # From bridge_domains 34 | "ACIBridgeDomainSerializer", 35 | "ACIBridgeDomainSubnetSerializer", 36 | # From contract_filters 37 | "ACIContractFilterEntrySerializer", 38 | "ACIContractFilterSerializer", 39 | # From contracts 40 | "ACIContractRelationSerializer", 41 | "ACIContractSerializer", 42 | "ACIContractSubjectFilterSerializer", 43 | "ACIContractSubjectSerializer", 44 | # From endpoint_groups 45 | "ACIEndpointGroupSerializer", 46 | "ACIUSegEndpointGroupSerializer", 47 | "ACIUSegNetworkAttributeSerializer", 48 | # From endpoint_security_groups 49 | "ACIEndpointSecurityGroupSerializer", 50 | "ACIEsgEndpointGroupSelectorSerializer", 51 | "ACIEsgEndpointSelectorSerializer", 52 | # From vrfs 53 | "ACIVRFSerializer", 54 | # From tenants 55 | "ACITenantSerializer", 56 | ) 57 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/forms/tenant/test_tenants.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.test import TestCase 6 | 7 | from ....forms.tenant.tenants import ACITenantEditForm 8 | 9 | 10 | class ACITenantFormTestCase(TestCase): 11 | """Test case for ACITenant form.""" 12 | 13 | name_error_message: str = ( 14 | "Only alphanumeric characters, hyphens, periods and underscores are allowed." 15 | ) 16 | description_error_message: str = ( 17 | "Only alphanumeric characters and !#$%()*,-./:;@ _{|}~?&+ are allowed." 18 | ) 19 | 20 | def test_invalid_aci_tenant_field_values(self) -> None: 21 | """Test validation of invalid ACI Tenant field values.""" 22 | aci_tenant_form = ACITenantEditForm( 23 | data={ 24 | "name": "ACI Test Tenant 1", 25 | "name_alias": "ACI Test Tenant Alias 1", 26 | "description": "Invalid Description: ö", 27 | } 28 | ) 29 | self.assertEqual(aci_tenant_form.errors["name"], [self.name_error_message]) 30 | self.assertEqual( 31 | aci_tenant_form.errors["name_alias"], [self.name_error_message] 32 | ) 33 | self.assertEqual( 34 | aci_tenant_form.errors["description"], 35 | [self.description_error_message], 36 | ) 37 | 38 | def test_valid_aci_tenant_field_values(self) -> None: 39 | """Test validation of valid ACI Tenant field values.""" 40 | aci_tenant_form = ACITenantEditForm( 41 | data={ 42 | "name": "ACITestTenant1", 43 | "name_alias": "TestingTenant", 44 | "description": "Tenant for NetBox ACI Plugin testing", 45 | } 46 | ) 47 | self.assertEqual(aci_tenant_form.errors.get("name"), None) 48 | self.assertEqual(aci_tenant_form.errors.get("name_alias"), None) 49 | self.assertEqual(aci_tenant_form.errors.get("description"), None) 50 | -------------------------------------------------------------------------------- /netbox_aci_plugin/api/serializers/tenant/vrfs.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from ipam.api.serializers import VRFSerializer 6 | from netbox.api.serializers import NetBoxModelSerializer 7 | from rest_framework import serializers 8 | from tenancy.api.serializers import TenantSerializer 9 | 10 | from ....models.tenant.vrfs import ACIVRF 11 | from .tenants import ACITenantSerializer 12 | 13 | 14 | class ACIVRFSerializer(NetBoxModelSerializer): 15 | """Serializer for the ACI VRF model.""" 16 | 17 | url = serializers.HyperlinkedIdentityField( 18 | view_name="plugins-api:netbox_aci_plugin-api:acivrf-detail" 19 | ) 20 | aci_tenant = ACITenantSerializer(nested=True, required=True) 21 | nb_tenant = TenantSerializer(nested=True, required=False, allow_null=True) 22 | nb_vrf = VRFSerializer(nested=True, required=False, allow_null=True) 23 | 24 | class Meta: 25 | model = ACIVRF 26 | fields: tuple = ( 27 | "id", 28 | "url", 29 | "display", 30 | "name", 31 | "name_alias", 32 | "description", 33 | "aci_tenant", 34 | "nb_tenant", 35 | "nb_vrf", 36 | "bd_enforcement_enabled", 37 | "dns_labels", 38 | "ip_data_plane_learning_enabled", 39 | "pc_enforcement_direction", 40 | "pc_enforcement_preference", 41 | "pim_ipv4_enabled", 42 | "pim_ipv6_enabled", 43 | "preferred_group_enabled", 44 | "comments", 45 | "tags", 46 | "custom_fields", 47 | "created", 48 | "last_updated", 49 | ) 50 | brief_fields: tuple = ( 51 | "id", 52 | "url", 53 | "display", 54 | "name", 55 | "name_alias", 56 | "description", 57 | "aci_tenant", 58 | "nb_tenant", 59 | "nb_vrf", 60 | ) 61 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/acicontractrelation.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load helpers %} 5 | {% load i18n %} 6 | 7 | {% block breadcrumbs %} 8 | {{ block.super }} 9 | 10 | 11 | {% endblock breadcrumbs %} 12 | 13 | {% block content %} 14 |
15 |
16 |
17 |

{% trans "ACI Contract Relation" %}

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
{% trans "ACI Tenant" %}{{ object.aci_contract.aci_tenant|linkify|placeholder }}
{% trans "ACI Contract" %}{{ object.aci_contract|linkify|placeholder }}
{% trans "ACI Object Type" %}{{ object.aci_object_type.name|placeholder }}
{% trans "ACI Object" %}{{ object.aci_object|linkify|placeholder }}
{% trans "Role" %}{% badge object.get_role_display bg_color=object.get_role_color %}
40 |
41 | {% include 'inc/panels/custom_fields.html' %} 42 |
43 |
44 | {% include 'inc/panels/tags.html' %} 45 | {% include 'inc/panels/comments.html' %} 46 |
47 |
48 | {% endblock content %} 49 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/forms/tenant/test_app_profiles.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.test import TestCase 6 | 7 | from ....forms.tenant.app_profiles import ACIAppProfileEditForm 8 | 9 | 10 | class ACIAppProfileFormTestCase(TestCase): 11 | """Test case for ACIAppProfile form.""" 12 | 13 | name_error_message: str = ( 14 | "Only alphanumeric characters, hyphens, periods and underscores are allowed." 15 | ) 16 | description_error_message: str = ( 17 | "Only alphanumeric characters and !#$%()*,-./:;@ _{|}~?&+ are allowed." 18 | ) 19 | 20 | def test_invalid_aci_app_profile_field_values(self) -> None: 21 | """Test validation of invalid ACI AppProfile field values.""" 22 | aci_app_profile_form = ACIAppProfileEditForm( 23 | data={ 24 | "name": "ACI App Profile Test 1", 25 | "name_alias": "ACI Test Alias 1", 26 | "description": "Invalid Description: ö", 27 | } 28 | ) 29 | self.assertEqual(aci_app_profile_form.errors["name"], [self.name_error_message]) 30 | self.assertEqual( 31 | aci_app_profile_form.errors["name_alias"], 32 | [self.name_error_message], 33 | ) 34 | self.assertEqual( 35 | aci_app_profile_form.errors["description"], 36 | [self.description_error_message], 37 | ) 38 | 39 | def test_valid_aci_app_profile_field_values(self) -> None: 40 | """Test validation of valid ACI AppProfile field values.""" 41 | aci_app_profile_form = ACIAppProfileEditForm( 42 | data={ 43 | "name": "ACIAppProfile1", 44 | "name_alias": "Testing", 45 | "description": "Application Profile for NetBox ACI Plugin", 46 | } 47 | ) 48 | self.assertEqual(aci_app_profile_form.errors.get("name"), None) 49 | self.assertEqual(aci_app_profile_form.errors.get("name_alias"), None) 50 | self.assertEqual(aci_app_profile_form.errors.get("description"), None) 51 | -------------------------------------------------------------------------------- /netbox_aci_plugin/models/base.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.core.validators import MaxLengthValidator 6 | from django.db import models 7 | from django.utils.translation import gettext_lazy as _ 8 | from netbox.models import NetBoxModel 9 | 10 | from ..validators import ACIPolicyDescriptionValidator, ACIPolicyNameValidator 11 | 12 | 13 | class ACIBaseModel(NetBoxModel): 14 | """NetBox abstract model for ACI classes.""" 15 | 16 | name = models.CharField( 17 | verbose_name=_("name"), 18 | max_length=64, 19 | validators=[ 20 | MaxLengthValidator(64), 21 | ACIPolicyNameValidator, 22 | ], 23 | ) 24 | name_alias = models.CharField( 25 | verbose_name=_("name alias"), 26 | max_length=64, 27 | blank=True, 28 | validators=[ 29 | MaxLengthValidator(64), 30 | ACIPolicyNameValidator, 31 | ], 32 | ) 33 | description = models.CharField( 34 | verbose_name=_("description"), 35 | max_length=128, 36 | blank=True, 37 | validators=[ 38 | MaxLengthValidator(128), 39 | ACIPolicyDescriptionValidator, 40 | ], 41 | ) 42 | nb_tenant = models.ForeignKey( 43 | to="tenancy.Tenant", 44 | on_delete=models.SET_NULL, 45 | related_name="%(class)ss", 46 | verbose_name=_("NetBox tenant"), 47 | blank=True, 48 | null=True, 49 | ) 50 | comments = models.TextField( 51 | verbose_name=_("comments"), 52 | blank=True, 53 | ) 54 | 55 | clone_fields: tuple = ( 56 | "description", 57 | "nb_tenant", 58 | ) 59 | 60 | class Meta: 61 | abstract: bool = True 62 | ordering: tuple = ("name",) 63 | 64 | def __str__(self) -> str: 65 | """Return string representation of the instance.""" 66 | return self.name 67 | 68 | @property 69 | def parent_object(self) -> NetBoxModel | None: 70 | """Return the parent object of the instance.""" 71 | return NotImplemented 72 | -------------------------------------------------------------------------------- /netbox_aci_plugin/filtersets/tenant/app_profiles.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import django_filters 6 | from django.db.models import Q 7 | from django.utils.translation import gettext_lazy as _ 8 | from netbox.filtersets import NetBoxModelFilterSet 9 | from tenancy.models import Tenant 10 | 11 | from ...models.tenant.app_profiles import ACIAppProfile 12 | from ...models.tenant.tenants import ACITenant 13 | 14 | 15 | class ACIAppProfileFilterSet(NetBoxModelFilterSet): 16 | """Filter set for the ACI Application Profile model.""" 17 | 18 | aci_tenant = django_filters.ModelMultipleChoiceFilter( 19 | field_name="aci_tenant__name", 20 | queryset=ACITenant.objects.all(), 21 | to_field_name="name", 22 | label=_("ACI Tenant (name)"), 23 | ) 24 | aci_tenant_id = django_filters.ModelMultipleChoiceFilter( 25 | queryset=ACITenant.objects.all(), 26 | to_field_name="id", 27 | label=_("ACI Tenant (ID)"), 28 | ) 29 | nb_tenant = django_filters.ModelMultipleChoiceFilter( 30 | field_name="nb_tenant__name", 31 | queryset=Tenant.objects.all(), 32 | to_field_name="name", 33 | label=_("NetBox tenant (name)"), 34 | ) 35 | nb_tenant_id = django_filters.ModelMultipleChoiceFilter( 36 | queryset=Tenant.objects.all(), 37 | to_field_name="id", 38 | label=_("NetBox tenant (ID)"), 39 | ) 40 | 41 | class Meta: 42 | model = ACIAppProfile 43 | fields: tuple = ( 44 | "id", 45 | "name", 46 | "name_alias", 47 | "description", 48 | "aci_tenant", 49 | "nb_tenant", 50 | ) 51 | 52 | def search(self, queryset, name, value): 53 | """Return a QuerySet filtered by the model's description.""" 54 | if not value.strip(): 55 | return queryset 56 | queryset_filter: Q = ( 57 | Q(name__icontains=value) 58 | | Q(name_alias__icontains=value) 59 | | Q(description__icontains=value) 60 | ) 61 | return queryset.filter(queryset_filter) 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | description: > 4 | Report a reproducible bug in the current release of NetBox ACI Plugin 5 | labels: ["type: bug"] 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: > 10 | **NOTE:** This form is only for reporting _reproducible bugs_ in a 11 | current NetBox ACI Plugin release. 12 | - type: input 13 | attributes: 14 | label: NetBox ACI Plugin version 15 | description: What version of NetBox ACI Plugin are you currently running? 16 | placeholder: v0.1.0 17 | validations: 18 | required: true 19 | - type: input 20 | attributes: 21 | label: NetBox version 22 | description: What version of NetBox are you currently running? 23 | placeholder: v4.4.0 24 | validations: 25 | required: true 26 | - type: dropdown 27 | attributes: 28 | label: Python version 29 | description: What version of Python are you currently running? 30 | options: 31 | - "3.10" 32 | - "3.11" 33 | - "3.12" 34 | validations: 35 | required: true 36 | - type: textarea 37 | attributes: 38 | label: Steps to Reproduce 39 | description: > 40 | Please provide a minimal working example to demonstrate the bug. Begin 41 | with the initialization of any necessary database objects and clearly 42 | enumerate each operation carried out. Ensure that your example is as 43 | concise as possible while adequately illustrating the issue. 44 | 45 | _Please refrain from including any confidential or sensitive 46 | information in your example._ 47 | validations: 48 | required: true 49 | - type: textarea 50 | attributes: 51 | label: Expected Behavior 52 | description: What did you expect to happen? 53 | placeholder: > 54 | The script should execute without raising any errors or exceptions 55 | validations: 56 | required: true 57 | - type: textarea 58 | attributes: 59 | label: Observed Behavior 60 | description: What happened instead? 61 | placeholder: A TypeError exception was raised 62 | validations: 63 | required: true 64 | ... 65 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # See PEP 518 for the spec of this file 2 | # https://www.python.org/dev/peps/pep-0518/ 3 | 4 | [build-system] 5 | requires = ["setuptools >= 77.0.3"] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [project] 9 | name = "netbox-aci-plugin" 10 | version = "0.1.0" 11 | requires-python = ">=3.10.0" 12 | authors = [ 13 | {name = "Martin Hauser", email = "git@pheus.dev"}, 14 | ] 15 | maintainers = [ 16 | {name = "Martin Hauser", email = "git@pheus.dev"}, 17 | ] 18 | description = "NetBox plugin for Cisco ACI" 19 | readme = { file = "README.md", content-type = "text/markdown" } 20 | license = "GPL-3.0-or-later" 21 | license-files = ["LICENSE"] 22 | keywords = ["netbox", "netbox-plugin", "cisco", "aci"] 23 | classifiers = [ 24 | "Development Status :: 3 - Alpha", 25 | "Framework :: Django", 26 | "Framework :: Django :: 5.2", 27 | "Intended Audience :: Developers", 28 | "Intended Audience :: System Administrators", 29 | "Natural Language :: English", 30 | "Operating System :: OS Independent", 31 | "Programming Language :: Python", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3 :: Only", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Programming Language :: Python :: 3.12", 37 | ] 38 | 39 | [project.optional-dependencies] 40 | docs = [ 41 | "mkdocs==1.6.1", 42 | "mkdocs-material==9.6.18", 43 | "mkdocs-include-markdown-plugin==7.1.7", 44 | ] 45 | test = [ 46 | "check-manifest==0.50", 47 | "pre-commit==4.3.0", 48 | "pytest==8.4.1", 49 | "ruff==0.12.11", 50 | "yamllint==1.37.1", 51 | ] 52 | 53 | [project.urls] 54 | Homepage = "https://github.com/pheus/netbox-aci-plugin" 55 | Documentation = "https://pheus.github.io/netbox-aci-plugin/" 56 | Source = "https://github.com/pheus/netbox-aci-plugin" 57 | Issues = "https://github.com/pheus/netbox-aci-plugin/issues" 58 | Changelog = "https://github.com/pheus/netbox-aci-plugin/releases" 59 | 60 | [tool.check-manifest] 61 | ignore = [ 62 | ".editorconfig", 63 | ".markdownlint.yaml", 64 | ".pre-commit-config.yaml", 65 | ".pypirc", 66 | "requirements_dev.txt", 67 | "ruff.toml", 68 | ] 69 | 70 | [tool.setuptools] 71 | include-package-data = true 72 | 73 | [tool.setuptools.packages.find] 74 | include = ["netbox_aci_plugin*"] 75 | 76 | [tool.setuptools.package-data] 77 | netbox_aci_plugin = [ 78 | "templates/**", 79 | "static/**", 80 | "locale/**/*.mo" 81 | ] 82 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | site_name: NetBox ACI Plugin 3 | site_url: https://pheus.github.io/netbox-aci-plugin 4 | repo_url: https://github.com/pheus/netbox-aci-plugin 5 | repo_name: pheus/netbox-aci-plugin 6 | # strict: true 7 | nav: 8 | - Home: index.md 9 | - Features: 10 | - Tenants: features/tenants.md 11 | - Development: 12 | - Code of Conduct: development/code_of_conduct.md 13 | - Contributing: development/contributing.md 14 | - Releasing: development/releasing.md 15 | - Changelog: changelog.md 16 | - Security: security.md 17 | theme: 18 | name: material 19 | language: en 20 | # logo: assets/logo.png 21 | palette: 22 | # Palette toggle for automatic mode 23 | - media: "(prefers-color-scheme)" 24 | toggle: 25 | icon: material/brightness-auto 26 | name: Switch to light mode 27 | # Palette toggle for light mode 28 | - media: "(prefers-color-scheme: light)" 29 | scheme: default 30 | toggle: 31 | icon: material/brightness-7 32 | name: Switch to dark mode 33 | # Palette toggle for dark mode 34 | - media: "(prefers-color-scheme: dark)" 35 | scheme: slate 36 | toggle: 37 | icon: material/brightness-4 38 | name: Switch to system preference 39 | features: 40 | - content.code.copy 41 | - navigation.indexes 42 | - navigation.instant 43 | - navigation.instant.progress 44 | - navigation.tabs.sticky 45 | markdown_extensions: 46 | - attr_list 47 | - pymdownx.emoji: 48 | emoji_index: !!python/name:material.extensions.emoji.twemoji 49 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 50 | - pymdownx.critic 51 | - pymdownx.caret 52 | - pymdownx.mark 53 | - pymdownx.tilde 54 | - pymdownx.tabbed 55 | - attr_list 56 | - pymdownx.arithmatex: 57 | generic: true 58 | - pymdownx.highlight: 59 | linenums: false 60 | - pymdownx.superfences: 61 | custom_fences: 62 | - name: mermaid 63 | class: mermaid 64 | format: !!python/name:pymdownx.superfences.fence_code_format 65 | - pymdownx.inlinehilite 66 | - pymdownx.details 67 | - admonition 68 | - toc: 69 | baselevel: 1 70 | permalink: '#' 71 | slugify: !!python/object/apply:pymdownx.slugs.slugify {} 72 | - meta 73 | plugins: 74 | - include-markdown 75 | - search: 76 | lang: en 77 | extra: 78 | social: 79 | - icon: fontawesome/brands/github 80 | link: https://github.com/pheus/netbox-aci-plugin 81 | name: Github 82 | watch: 83 | - netbox_aci_plugin 84 | ... 85 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature Request 3 | description: Propose a new NetBox ACI Plugin feature or enhancement 4 | labels: ["type: feature"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: > 9 | **NOTE:** This form is only for submitting well-formed proposals to 10 | extend or modify NetBox ACI Plugin in some way. If you're trying to 11 | solve a problem but can't figure out how, or if you still need time to 12 | work on the details of a proposed new feature, please start a 13 | [discussion](https://github.com/netbox-community/pynetbox/discussions) 14 | instead. 15 | - type: input 16 | attributes: 17 | label: NetBox ACI Plugin version 18 | description: What version of NetBox ACI Plugin are you currently running? 19 | placeholder: v0.1.0 20 | validations: 21 | required: true 22 | - type: input 23 | attributes: 24 | label: NetBox version 25 | description: What version of NetBox are you currently running? 26 | placeholder: v4.4.0 27 | validations: 28 | required: true 29 | - type: dropdown 30 | attributes: 31 | label: Feature type 32 | options: 33 | - Data model extension 34 | - New functionality 35 | - Change to existing functionality 36 | validations: 37 | required: true 38 | - type: textarea 39 | attributes: 40 | label: Proposed functionality 41 | description: > 42 | Describe in detail the new feature or behavior you are proposing. 43 | Include any specific changes to work flows, data models, and/or the user 44 | interface. The more detail you provide here, the greater chance your 45 | proposal has of being discussed. Feature requests which don't include an 46 | actionable implementation plan will be rejected. 47 | validations: 48 | required: true 49 | - type: textarea 50 | attributes: 51 | label: Use case 52 | description: > 53 | Explain how adding this functionality would benefit NetBox ACI Plugin 54 | users. What need does it address? 55 | validations: 56 | required: true 57 | - type: textarea 58 | attributes: 59 | label: External dependencies 60 | description: > 61 | List any new dependencies on external libraries or services that this 62 | new feature would introduce. For example, does the proposal require the 63 | installation of a new Python package? 64 | (Not all new features introduce new dependencies.) 65 | ... 66 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tables/tenant/vrfs.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import django_tables2 as tables 6 | from django.utils.translation import gettext_lazy as _ 7 | from netbox.tables import NetBoxTable, columns 8 | 9 | from ...models.tenant.vrfs import ACIVRF 10 | 11 | 12 | class ACIVRFTable(NetBoxTable): 13 | """NetBox table for the ACI VRF model.""" 14 | 15 | name = tables.Column( 16 | verbose_name=_("ACI VRF"), 17 | linkify=True, 18 | ) 19 | name_alias = tables.Column( 20 | verbose_name=_("Alias"), 21 | linkify=True, 22 | ) 23 | aci_tenant = tables.Column( 24 | linkify=True, 25 | ) 26 | nb_tenant = tables.Column( 27 | linkify=True, 28 | ) 29 | nb_vrf = tables.Column( 30 | linkify=True, 31 | ) 32 | bd_enforcement_enabled = columns.BooleanColumn(verbose_name=_("BD enforcement")) 33 | dns_labels = columns.ArrayColumn() 34 | ip_data_plane_learning_enabled = columns.BooleanColumn( 35 | verbose_name=_("DP learning"), 36 | ) 37 | pc_enforcement_direction = columns.ChoiceFieldColumn( 38 | verbose_name=_("Enforcement direction"), 39 | ) 40 | pc_enforcement_preference = columns.ChoiceFieldColumn( 41 | verbose_name=_("Enforcement preference"), 42 | ) 43 | pim_ipv4_enabled = columns.BooleanColumn( 44 | verbose_name=_("PIM IPv4"), 45 | ) 46 | pim_ipv6_enabled = columns.BooleanColumn( 47 | verbose_name=_("PIM IPv6"), 48 | ) 49 | preferred_group_enabled = columns.BooleanColumn( 50 | verbose_name=_("Preferred group"), 51 | ) 52 | tags = columns.TagColumn() 53 | comments = columns.MarkdownColumn() 54 | 55 | class Meta(NetBoxTable.Meta): 56 | model = ACIVRF 57 | fields: tuple = ( 58 | "pk", 59 | "id", 60 | "name", 61 | "name_alias", 62 | "aci_tenant", 63 | "nb_tenant", 64 | "nb_vrf", 65 | "description", 66 | "bd_enforcement_enabled", 67 | "dns_labels", 68 | "ip_data_plane_learning_enabled", 69 | "pc_enforcement_direction", 70 | "pc_enforcement_preference", 71 | "pim_ipv4_enabled", 72 | "pim_ipv6_enabled", 73 | "preferred_group_enabled", 74 | "tags", 75 | "comments", 76 | ) 77 | default_columns: tuple = ( 78 | "name", 79 | "name_alias", 80 | "aci_tenant", 81 | "nb_tenant", 82 | "description", 83 | "tags", 84 | ) 85 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/acicontractfilter.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load helpers %} 5 | {% load i18n %} 6 | 7 | {% block breadcrumbs %} 8 | {{ block.super }} 9 | 10 | {% endblock breadcrumbs %} 11 | 12 | {% block content %} 13 |
14 |
15 |
16 |

{% trans "ACI Contract Filter" %}

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 38 | 39 |
{% trans "ACI Tenant" %}{{ object.aci_tenant|linkify|placeholder }}
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "NetBox Tenant" %} 33 | {% if object.nb_tenant.group %} 34 | {{ object.nb_tenant.group|linkify }} / 35 | {% endif %} 36 | {{ object.nb_tenant|linkify|placeholder }} 37 |
40 |
41 | {% include 'inc/panels/custom_fields.html' %} 42 |
43 |
44 | {% include 'inc/panels/tags.html' %} 45 | {% include 'inc/panels/comments.html' %} 46 |
47 |
48 |
49 |
50 |
51 |

52 | {% trans "Entries" %} 53 | {% if perms.netbox_aci_plugin.add_acicontractfilterentry %} 54 | 59 | {% endif %} 60 |

61 |
62 | {% render_table contract_filter_entries_table %} 63 |
64 |
65 |
66 |
67 | {% endblock content %} 68 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/filters/tenant/vrfs.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from typing import TYPE_CHECKING, Annotated 6 | 7 | import strawberry 8 | import strawberry_django 9 | from strawberry.scalars import ID 10 | from strawberry_django import FilterLookup 11 | 12 | from .... import models 13 | from ..mixins import ACIBaseFilterMixin 14 | 15 | if TYPE_CHECKING: 16 | from ipam.graphql.filters import VRFFilter 17 | from netbox.graphql.filter_lookups import StringArrayLookup 18 | 19 | from ...enums import ( 20 | VRFPCEnforcementDirectionEnum, 21 | VRFPCEnforcementPreferenceEnum, 22 | ) 23 | from .tenants import ACITenantFilter 24 | 25 | 26 | __all__ = ("ACIVRFFilter",) 27 | 28 | 29 | @strawberry_django.filter(models.ACIVRF, lookups=True) 30 | class ACIVRFFilter(ACIBaseFilterMixin): 31 | """GraphQL filter definition for the ACIVRF model.""" 32 | 33 | aci_tenant: ( 34 | Annotated[ 35 | "ACITenantFilter", 36 | strawberry.lazy("netbox_aci_plugin.graphql.filters"), 37 | ] 38 | | None 39 | ) = strawberry_django.filter_field() 40 | aci_tenant_id: ID | None = strawberry_django.filter_field() 41 | nb_vrf: Annotated["VRFFilter", strawberry.lazy("ipam.graphql.filters")] | None = ( 42 | strawberry_django.filter_field() 43 | ) 44 | nb_vrf_id: ID | None = strawberry_django.filter_field() 45 | bd_enforcement_enabled: FilterLookup[bool] | None = strawberry_django.filter_field() 46 | dns_labels: ( 47 | Annotated[ 48 | "StringArrayLookup", 49 | strawberry.lazy("netbox.graphql.filter_lookups"), 50 | ] 51 | | None 52 | ) = strawberry_django.filter_field() 53 | ip_data_plane_learning_enabled: FilterLookup[bool] | None = ( 54 | strawberry_django.filter_field() 55 | ) 56 | pc_enforcement_direction: ( 57 | Annotated[ 58 | "VRFPCEnforcementDirectionEnum", 59 | strawberry.lazy("netbox_aci_plugin.graphql.enums"), 60 | ] 61 | | None 62 | ) = strawberry_django.filter_field() 63 | pc_enforcement_preference: ( 64 | Annotated[ 65 | "VRFPCEnforcementPreferenceEnum", 66 | strawberry.lazy("netbox_aci_plugin.graphql.enums"), 67 | ] 68 | | None 69 | ) = strawberry_django.filter_field() 70 | pim_ipv4_enabled: FilterLookup[bool] | None = strawberry_django.filter_field() 71 | pim_ipv6_enabled: FilterLookup[bool] | None = strawberry_django.filter_field() 72 | preferred_group_enabled: FilterLookup[bool] | None = ( 73 | strawberry_django.filter_field() 74 | ) 75 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/api/tenant/test_tenants.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from tenancy.models import Tenant 6 | from utilities.testing import APIViewTestCases 7 | 8 | from ....api.urls import app_name 9 | from ....models.tenant.tenants import ACITenant 10 | 11 | 12 | class ACITenantAPIViewTestCase(APIViewTestCases.APIViewTestCase): 13 | """API view test case for ACI Tenant.""" 14 | 15 | model = ACITenant 16 | view_namespace: str = f"plugins-api:{app_name}" 17 | brief_fields: list[str] = [ 18 | "description", 19 | "display", 20 | "id", 21 | "name", 22 | "name_alias", 23 | "nb_tenant", 24 | "url", 25 | ] 26 | 27 | @classmethod 28 | def setUpTestData(cls) -> None: 29 | """Set up ACI Tenants for API view testing.""" 30 | nb_tenant1 = Tenant.objects.create( 31 | name="NetBox Tenant API 1", slug="netbox-tenant-api-1" 32 | ) 33 | nb_tenant2 = Tenant.objects.create( 34 | name="NetBox Tenant API 2", slug="netbox-tenant-api-2" 35 | ) 36 | aci_tenants = ( 37 | ACITenant( 38 | name="ACITestTenantAPI1", 39 | name_alias="TestingTenant1", 40 | description="First ACI Test Tenant", 41 | comments="# ACI Test Tenant 1", 42 | nb_tenant=nb_tenant1, 43 | ), 44 | ACITenant( 45 | name="ACITestTenantAPI2", 46 | name_alias="TestingTenant2", 47 | description="Second ACI Test Tenant", 48 | comments="# ACI Test Tenant 2", 49 | nb_tenant=nb_tenant1, 50 | ), 51 | ACITenant( 52 | name="ACITestTenantAPI3", 53 | name_alias="TestingTenant3", 54 | description="Third ACI Test Tenant", 55 | comments="# ACI Test Tenant 3", 56 | nb_tenant=nb_tenant2, 57 | ), 58 | ) 59 | ACITenant.objects.bulk_create(aci_tenants) 60 | 61 | cls.create_data = [ 62 | { 63 | "name": "ACITestTenantAPI4", 64 | "name_alias": "TestingTenant4", 65 | "description": "Forth ACI Test Tenant", 66 | "comments": "# ACI Test Tenant 4", 67 | "nb_tenant": nb_tenant1.id, 68 | }, 69 | { 70 | "name": "ACITestTenantAPI5", 71 | "name_alias": "TestingTenant5", 72 | "description": "Fifth ACI Test Tenant", 73 | "comments": "# ACI Test Tenant 5", 74 | "nb_tenant": nb_tenant2.id, 75 | }, 76 | ] 77 | cls.bulk_update_data = { 78 | "description": "New description", 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Continuous Integration 🔄 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | - dev 10 | 11 | jobs: 12 | pre-commit: 13 | name: Run pre-commit hooks 🪝 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Set up repository 🧩 17 | uses: actions/checkout@v4 18 | - name: Set up Python 🐍 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.x" 22 | cache: "pip" 23 | - name: Run pre-commit hooks 🪝 24 | uses: pre-commit/action@v3.0.1 25 | 26 | test-plugin-integration: 27 | name: >- 28 | Run tests 🧪 [NB ${{ matrix.netbox-version }} | 29 | Py ${{ matrix.python-version }}] 30 | needs: 31 | - pre-commit 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | python-version: ["3.10", "3.11", "3.12"] 36 | netbox-version: ["v4.3.0", "main"] 37 | services: 38 | redis: 39 | image: redis 40 | ports: 41 | - 6379:6379 42 | postgres: 43 | image: postgres 44 | env: 45 | POSTGRES_USER: netbox 46 | POSTGRES_PASSWORD: netbox 47 | options: >- 48 | --health-cmd pg_isready 49 | --health-interval 10s 50 | --health-timeout 5s 51 | --health-retries 5 52 | ports: 53 | - 5432:5432 54 | 55 | steps: 56 | - name: Checkout repository of the plugin 🧩 57 | uses: actions/checkout@v4 58 | with: 59 | path: netbox_aci_plugin 60 | 61 | - name: Checkout repository of NetBox ${{ matrix.netbox-version }} 📦 62 | uses: actions/checkout@v4 63 | with: 64 | repository: "netbox-community/netbox" 65 | ref: ${{ matrix.netbox-version }} 66 | path: netbox 67 | 68 | - name: Set up Python ${{ matrix.python-version }} 🐍 69 | uses: actions/setup-python@v5 70 | with: 71 | python-version: ${{ matrix.python-version }} 72 | cache: "pip" 73 | 74 | - name: Set up NetBox configuration for testing 🧪 75 | working-directory: netbox 76 | run: | 77 | ln -s $(pwd)/../netbox_aci_plugin/.ci/configuration.testing.py \ 78 | netbox/netbox/configuration.py 79 | 80 | - name: Install dependencies for NetBox 📦 81 | working-directory: netbox 82 | run: | 83 | python -m pip install --upgrade pip 84 | pip install -r requirements.txt -U 85 | 86 | - name: Install Plugin in NetBox environment 🧩 87 | working-directory: netbox_aci_plugin 88 | run: | 89 | pip install -e . 90 | 91 | - name: Run Django tests 🧪 92 | working-directory: netbox 93 | run: | 94 | python netbox/manage.py test netbox_aci_plugin.tests --parallel -v 2 95 | ... 96 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/aciendpointsecuritygroup.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load i18n %} 5 | 6 | {% block breadcrumbs %} 7 | {{ block.super }} 8 | 9 | 10 | 11 | {% endblock breadcrumbs %} 12 | 13 | {% block content %} 14 |
15 |
16 |
17 |

{% trans "ACI Endpoint Security Group" %}

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 47 | 48 |
{% trans "ACI Tenant" %}{{ object.aci_tenant|linkify|placeholder }}
{% trans "ACI Application Profile" %}{{ object.aci_app_profile|linkify|placeholder }}
{% trans "ACI VRF" %}{{ object.aci_vrf|linkify|placeholder }}
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "NetBox Tenant" %} 42 | {% if object.nb_tenant.group %} 43 | {{ object.nb_tenant.group|linkify }} / 44 | {% endif %} 45 | {{ object.nb_tenant|linkify|placeholder }} 46 |
49 |
50 |
51 |

{% trans "Policy Enforcement Settings" %}

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
{% trans "Preferred Group Member enabled" %}{% checkmark object.preferred_group_member_enabled %}
{% trans "Intra-ESG Isolation enabled" %}{% checkmark object.intra_esg_isolation_enabled %}
62 |
63 | {% include 'inc/panels/custom_fields.html' %} 64 |
65 |
66 | {% include 'inc/panels/tags.html' %} 67 | {% include 'inc/panels/comments.html' %} 68 |
69 |
70 | {% endblock content %} 71 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # Ruff configuration 2 | #################### 3 | 4 | # Exclude a variety of commonly ignored directories. 5 | exclude = [ 6 | ".bzr", 7 | ".direnv", 8 | ".eggs", 9 | ".git", 10 | ".git-rewrite", 11 | ".hg", 12 | ".ipynb_checkpoints", 13 | ".mypy_cache", 14 | ".nox", 15 | ".pants.d", 16 | ".pyenv", 17 | ".pytest_cache", 18 | ".pytype", 19 | ".ruff_cache", 20 | ".svn", 21 | ".tox", 22 | ".venv", 23 | ".vscode", 24 | "__pypackages__", 25 | "_build", 26 | "buck-out", 27 | "build", 28 | "dist", 29 | "node_modules", 30 | "site-packages", 31 | "venv", 32 | ] 33 | 34 | # Enforce line length and indent-width 35 | line-length = 88 36 | indent-width = 4 37 | 38 | # Groups diagnostics by file and rule for readability 39 | # output-format = "grouped" 40 | 41 | # Ignores anything in .gitignore 42 | respect-gitignore = true 43 | 44 | # When running with --fix, shows which fixes are applied/available 45 | show-fixes = true 46 | 47 | # Always generate Python 3.10-compatible code. 48 | target-version = "py310" 49 | 50 | [lint] 51 | # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. 52 | select = ["E4", "E7", "E9", "F"] 53 | extend-select = [ 54 | "D", # pydocstyle 55 | "I", # import sorting (isort-equivalent) 56 | "UP", # pyupgrade 57 | "C4", # flake8-comprehensions 58 | "ISC", # implicit str concat checks 59 | "RET", # return semantics 60 | "SIM", # simplifications 61 | "PTH", # pathlib over os.path 62 | "SLF", # flake8-self 63 | "W505", # docstring line-length (paired with max-doc-length) 64 | ] 65 | ignore = [ 66 | "D100", # missing docstring in public module 67 | "D104", # missing docstring in public package (__init__.py) 68 | "D106", # missing docstring in nested classes 69 | "D105", # missing docstring in magic method 70 | "D107", # missing docstring in __init__ 71 | ] 72 | 73 | # Allow fix for all enabled rules (when `--fix`) is provided. 74 | fixable = ["ALL"] 75 | unfixable = [] 76 | 77 | # Allow unused variables when underscore-prefixed. 78 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 79 | 80 | [lint.flake8-self] 81 | ignore-names = ["_meta", "Meta"] 82 | 83 | [lint.per-file-ignores] 84 | "**/tests/*.py" = ["D"] 85 | "**/migrations/*.py" = ["D"] # For Django/NetBox plugin migrations 86 | "**/__init__.py" = ["D104"] # missing docstring in public package (__init__.py) 87 | 88 | [lint.pycodestyle] 89 | max-doc-length = 79 90 | 91 | [lint.pydocstyle] 92 | # Choose a docstring style (one of: "google", "numpy", "pep257"). 93 | convention = "pep257" 94 | 95 | [format] 96 | # Like Black, use double quotes for strings. 97 | quote-style = "double" 98 | 99 | # Like Black, indent with spaces, rather than tabs. 100 | indent-style = "space" 101 | 102 | # Like Black, respect magic trailing commas. 103 | skip-magic-trailing-comma = false 104 | 105 | # Enforce UNIX line ending 106 | line-ending = "lf" 107 | 108 | # Format Python code blocks found inside docstrings 109 | docstring-code-format = true 110 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/aciesgendpointselector.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load i18n %} 5 | 6 | {% block breadcrumbs %} 7 | {{ block.super }} 8 | 9 | 10 | 11 | {% endblock breadcrumbs %} 12 | 13 | {% block content %} 14 |
15 |
16 |
17 |

{% trans "ACI ESG Endpoint Group Selector" %}

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 47 | 48 |
{% trans "ACI Tenant" %}{{ object.aci_tenant|linkify|placeholder }}
{% trans "ACI Application Profile" %}{{ object.aci_app_profile|linkify|placeholder }}
{% trans "ACI Endpoint Security Group" %}{{ object.aci_endpoint_security_group|linkify|placeholder }}
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "NetBox Tenant" %} 42 | {% if object.nb_tenant.group %} 43 | {{ object.nb_tenant.group|linkify }} / 44 | {% endif %} 45 | {{ object.nb_tenant|linkify|placeholder }} 46 |
49 |
50 |
51 |

{% trans "Endpoint Assignment" %}

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
{% trans "Endpoint Object Type" %}{{ object.ep_object_type.name|placeholder }}
{% trans "Endpoint Object" %}{{ object.ep_object|linkify|placeholder }}
62 |
63 | {% include 'inc/panels/custom_fields.html' %} 64 |
65 |
66 | {% include 'inc/panels/tags.html' %} 67 | {% include 'inc/panels/comments.html' %} 68 |
69 |
70 | {% endblock content %} 71 | -------------------------------------------------------------------------------- /netbox_aci_plugin/api/serializers/tenant/contract_filters.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from netbox.api.serializers import NetBoxModelSerializer 6 | from rest_framework import serializers 7 | from tenancy.api.serializers import TenantSerializer 8 | 9 | from ....models.tenant.contract_filters import ( 10 | ACIContractFilter, 11 | ACIContractFilterEntry, 12 | ) 13 | from .tenants import ACITenantSerializer 14 | 15 | 16 | class ACIContractFilterSerializer(NetBoxModelSerializer): 17 | """Serializer for the ACI Contract Filter model.""" 18 | 19 | url = serializers.HyperlinkedIdentityField( 20 | view_name="plugins-api:netbox_aci_plugin-api:acicontractfilter-detail" 21 | ) 22 | aci_tenant = ACITenantSerializer(nested=True, required=True) 23 | nb_tenant = TenantSerializer(nested=True, required=False, allow_null=True) 24 | 25 | class Meta: 26 | model = ACIContractFilter 27 | fields: tuple = ( 28 | "id", 29 | "url", 30 | "display", 31 | "name", 32 | "name_alias", 33 | "description", 34 | "aci_tenant", 35 | "nb_tenant", 36 | "comments", 37 | "tags", 38 | "custom_fields", 39 | "created", 40 | "last_updated", 41 | ) 42 | brief_fields: tuple = ( 43 | "id", 44 | "url", 45 | "display", 46 | "name", 47 | "name_alias", 48 | "description", 49 | "aci_tenant", 50 | "nb_tenant", 51 | ) 52 | 53 | 54 | class ACIContractFilterEntrySerializer(NetBoxModelSerializer): 55 | """Serializer for the ACI Contract Filter Entry model.""" 56 | 57 | url = serializers.HyperlinkedIdentityField( 58 | view_name="plugins-api:netbox_aci_plugin-api:acicontractfilterentry-detail" 59 | ) 60 | aci_contract_filter = ACIContractFilterSerializer(nested=True, required=True) 61 | 62 | class Meta: 63 | model = ACIContractFilterEntry 64 | fields: tuple = ( 65 | "id", 66 | "url", 67 | "display", 68 | "name", 69 | "name_alias", 70 | "description", 71 | "aci_contract_filter", 72 | "arp_opc", 73 | "destination_from_port", 74 | "destination_to_port", 75 | "ether_type", 76 | "icmp_v4_type", 77 | "icmp_v6_type", 78 | "ip_protocol", 79 | "match_dscp", 80 | "match_only_fragments_enabled", 81 | "source_from_port", 82 | "source_to_port", 83 | "stateful_enabled", 84 | "tcp_rules", 85 | "comments", 86 | "tags", 87 | "custom_fields", 88 | "created", 89 | "last_updated", 90 | ) 91 | brief_fields: tuple = ( 92 | "id", 93 | "url", 94 | "display", 95 | "name", 96 | "name_alias", 97 | "description", 98 | "aci_contract_filter", 99 | ) 100 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/api/tenant/test_app_profiles.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from tenancy.models import Tenant 6 | from utilities.testing import APIViewTestCases 7 | 8 | from ....api.urls import app_name 9 | from ....models.tenant.app_profiles import ACIAppProfile 10 | from ....models.tenant.tenants import ACITenant 11 | 12 | 13 | class ACIAppProfileAPIViewTestCase(APIViewTestCases.APIViewTestCase): 14 | """API view test case for ACI AppProfile.""" 15 | 16 | model = ACIAppProfile 17 | view_namespace: str = f"plugins-api:{app_name}" 18 | brief_fields: list[str] = [ 19 | "aci_tenant", 20 | "description", 21 | "display", 22 | "id", 23 | "name", 24 | "name_alias", 25 | "nb_tenant", 26 | "url", 27 | ] 28 | user_permissions = ("netbox_aci_plugin.view_acitenant",) 29 | 30 | @classmethod 31 | def setUpTestData(cls) -> None: 32 | """Set up ACI AppProfile for API view testing.""" 33 | nb_tenant1 = Tenant.objects.create( 34 | name="NetBox Tenant API 1", slug="netbox-tenant-api-1" 35 | ) 36 | nb_tenant2 = Tenant.objects.create( 37 | name="NetBox Tenant API 2", slug="netbox-tenant-api-2" 38 | ) 39 | aci_tenant1 = ACITenant.objects.create(name="ACITestTenantAPI1") 40 | aci_tenant2 = ACITenant.objects.create(name="ACITestTenantAPI2") 41 | 42 | aci_app_profiles = ( 43 | ACIAppProfile( 44 | name="ACIAppProfileTestAPI1", 45 | name_alias="Testing", 46 | description="First ACI Test", 47 | comments="# ACI Test 1", 48 | aci_tenant=aci_tenant1, 49 | nb_tenant=nb_tenant1, 50 | ), 51 | ACIAppProfile( 52 | name="ACIAppProfileTestAPI2", 53 | name_alias="Testing", 54 | description="Second ACI Test", 55 | comments="# ACI Test 2", 56 | aci_tenant=aci_tenant2, 57 | nb_tenant=nb_tenant1, 58 | ), 59 | ACIAppProfile( 60 | name="ACIAppProfileTestAPI3", 61 | name_alias="Testing", 62 | description="Third ACI Test", 63 | comments="# ACI Test 3", 64 | aci_tenant=aci_tenant1, 65 | nb_tenant=nb_tenant2, 66 | ), 67 | ) 68 | ACIAppProfile.objects.bulk_create(aci_app_profiles) 69 | 70 | cls.create_data: list[dict] = [ 71 | { 72 | "name": "ACIAppProfileTestAPI4", 73 | "name_alias": "Testing", 74 | "description": "Forth ACI Test", 75 | "comments": "# ACI Test 4", 76 | "aci_tenant": aci_tenant2.id, 77 | "nb_tenant": nb_tenant1.id, 78 | }, 79 | { 80 | "name": "ACIAppProfileTestAPI5", 81 | "name_alias": "Testing", 82 | "description": "Fifth ACI Test", 83 | "comments": "# ACI Test 5", 84 | "aci_tenant": aci_tenant1.id, 85 | "nb_tenant": nb_tenant2.id, 86 | }, 87 | ] 88 | cls.bulk_update_data = { 89 | "description": "New description", 90 | } 91 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## No Warranty 4 | Per the terms of the **GNU General Public License v3.0** (GPL‑3.0), 5 | the NetBox ACI Plugin is provided “as is,” without a warranty of 6 | any kind. 7 | While maintainers make reasonable efforts to avoid security defects, 8 | you are responsible for evaluating each release for fitness and risk in 9 | your own environment. 10 | 11 | ## Supported Versions 12 | We provide security fixes for the latest patch release of 13 | each supported minor series. 14 | 15 | | Plugin Version | Supported | 16 | |----------------|---------------------------| 17 | | 0.1.x | ✅ Security fixes accepted | 18 | | < 0.1.0 | ❌ End of support | 19 | 20 | > See the [changelog](https://github.com/pheus/netbox-aci-plugin/blob/main/CHANGELOG.md) for compatibility details. 21 | 22 | ## Reporting a Vulnerability 23 | 24 | **Do not open public GitHub issues for security reports.** 25 | 26 | **Preferred:** Use GitHub’s **Security → “Report a vulnerability”** to 27 | contact the maintainers via a private Security Advisory. 28 | 29 | Please include: 30 | - Affected **plugin version** and **NetBox version** 31 | - Environment details (local/Docker/OS/Python) 32 | - Impact and minimal steps to reproduce (PoC if possible) 33 | - Relevant logs (scrub sensitive data) 34 | 35 | ## Scope 36 | 37 | This policy covers vulnerabilities in the **NetBox ACI Plugin** 38 | codebase and its documentation site. 39 | Issues in **NetBox core** or other dependencies should be reported 40 | upstream to their respective maintainers. 41 | 42 | ### Out of Scope (examples) 43 | 44 | - Denial‑of‑service (resource exhaustion, load tests) or spam 45 | - Vulnerabilities require privileged access, physical access, 46 | or a compromised environment 47 | - Clickjacking or missing security headers on non‑sensitive pages 48 | - Best‑practice suggestions without a demonstrable security impact 49 | - Third‑party or platform issues outside this repository’s control 50 | 51 | ### Testing Guidelines 52 | 53 | - Use only your own systems and data; do **not** test against systems 54 | you do not own or have permission to test. 55 | - Avoid privacy violations and service degradation. 56 | - Do not run automated scanners against third‑party deployments. 57 | 58 | ## Our Process & Timelines 59 | 60 | - **Acknowledgment:** within **3 business days** 61 | - **Triage & Reproduction:** within **7 business days** 62 | - **Fix & Release:** target **≤30 days** for high/critical issues and 63 | **≤90 days** otherwise (coordinated disclosure if needed) 64 | - **Credit:** We’re happy to credit reporters unless you prefer anonymity. 65 | 66 | ## Coordinated Disclosure 67 | 68 | After a fix is available, we will publish a GitHub Security Advisory 69 | and update the changelog with upgrade guidance. 70 | If a CVE is appropriate, we’ll request one through GitHub. 71 | 72 | ## Safe Harbor 73 | 74 | We will not pursue or support legal action against researchers who: 75 | - Act in good faith and within this policy’s Scope and Testing Guidelines 76 | - Avoid privacy violations and service disruption 77 | - Give us reasonable time to remediate before public disclosure 78 | 79 | This policy does not authorize testing against systems you do not own 80 | or have permission to test, 81 | nor accessing data that does not belong to you. 82 | 83 | ## Bug Bounties 84 | 85 | We do not operate a bug bounty program at this time. 86 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/filters/tenant/endpoint_security_groups.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from dataclasses import dataclass 6 | from typing import TYPE_CHECKING, Annotated 7 | 8 | import strawberry 9 | import strawberry_django 10 | from core.graphql.filters import ContentTypeFilter 11 | from strawberry.scalars import ID 12 | from strawberry_django import FilterLookup 13 | 14 | from .... import models 15 | from ..mixins import ACIBaseFilterMixin 16 | 17 | if TYPE_CHECKING: 18 | from .app_profiles import ACIAppProfileFilter 19 | from .vrfs import ACIVRFFilter 20 | 21 | 22 | __all__ = ( 23 | "ACIEndpointSecurityGroupFilter", 24 | "ACIEsgEndpointGroupSelectorFilter", 25 | "ACIEsgEndpointSelectorFilter", 26 | ) 27 | 28 | 29 | @strawberry_django.filter(models.ACIEndpointSecurityGroup, lookups=True) 30 | class ACIEndpointSecurityGroupFilter(ACIBaseFilterMixin): 31 | """GraphQL filter definition for the ACIEndpointSecurityGroup model.""" 32 | 33 | aci_app_profile: ( 34 | Annotated[ 35 | "ACIAppProfileFilter", 36 | strawberry.lazy("netbox_aci_plugin.graphql.filters"), 37 | ] 38 | | None 39 | ) = strawberry_django.filter_field() 40 | aci_app_profile_id: ID | None = strawberry_django.filter_field() 41 | aci_vrf: ( 42 | Annotated[ 43 | "ACIVRFFilter", 44 | strawberry.lazy("netbox_aci_plugin.graphql.filters"), 45 | ] 46 | | None 47 | ) = strawberry_django.filter_field() 48 | aci_vrf_id: ID | None = strawberry_django.filter_field() 49 | admin_shutdown: FilterLookup[bool] | None = strawberry_django.filter_field() 50 | intra_esg_isolation_enabled: FilterLookup[bool] | None = ( 51 | strawberry_django.filter_field() 52 | ) 53 | preferred_group_member_enabled: FilterLookup[bool] | None = ( 54 | strawberry_django.filter_field() 55 | ) 56 | 57 | 58 | @dataclass 59 | class ACIEsgSelectorBaseFilterMixin(ACIBaseFilterMixin): 60 | """Base GraphQL filter mixin for ACI ESG Selector models.""" 61 | 62 | aci_endpoint_security_group: ( 63 | Annotated[ 64 | "ACIEndpointSecurityGroupFilter", 65 | strawberry.lazy("netbox_aci_plugin.graphql.filters"), 66 | ] 67 | | None 68 | ) = strawberry_django.filter_field() 69 | aci_endpoint_security_group_id: ID | None = strawberry_django.filter_field() 70 | 71 | 72 | @strawberry_django.filter(models.ACIEsgEndpointGroupSelector, lookups=True) 73 | class ACIEsgEndpointGroupSelectorFilter(ACIEsgSelectorBaseFilterMixin): 74 | """GraphQL filter definition for the ACIEsgEndpointGroupSelector model.""" 75 | 76 | aci_epg_object_type: ( 77 | Annotated["ContentTypeFilter", strawberry.lazy("core.graphql.filters")] | None 78 | ) = strawberry_django.filter_field() 79 | aci_epg_object_id: ID | None = strawberry_django.filter_field() 80 | 81 | 82 | @strawberry_django.filter(models.ACIEsgEndpointSelector, lookups=True) 83 | class ACIEsgEndpointSelectorFilter(ACIEsgSelectorBaseFilterMixin): 84 | """GraphQL filter definition for the ACIEsgEndpointSelector model.""" 85 | 86 | ep_object_type: ( 87 | Annotated["ContentTypeFilter", strawberry.lazy("core.graphql.filters")] | None 88 | ) = strawberry_django.filter_field() 89 | ep_object_id: ID | None = strawberry_django.filter_field() 90 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/aciesgendpointgroupselector.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load i18n %} 5 | 6 | {% block breadcrumbs %} 7 | {{ block.super }} 8 | 9 | 10 | 11 | {% endblock breadcrumbs %} 12 | 13 | {% block content %} 14 |
15 |
16 |
17 |

{% trans "ACI ESG Endpoint Group Selector" %}

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 47 | 48 |
{% trans "ACI Tenant" %}{{ object.aci_tenant|linkify|placeholder }}
{% trans "ACI Application Profile" %}{{ object.aci_app_profile|linkify|placeholder }}
{% trans "ACI Endpoint Security Group" %}{{ object.aci_endpoint_security_group|linkify|placeholder }}
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "NetBox Tenant" %} 42 | {% if object.nb_tenant.group %} 43 | {{ object.nb_tenant.group|linkify }} / 44 | {% endif %} 45 | {{ object.nb_tenant|linkify|placeholder }} 46 |
49 |
50 |
51 |

{% trans "Endpoint Group (EPG) Assignment" %}

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
{% trans "ACI Tenant" %}{{ object.aci_epg_object.aci_tenant|linkify|placeholder }}
{% trans "ACI Application Profile" %}{{ object.aci_epg_object.aci_app_profile|linkify|placeholder }}
{{ object.aci_epg_object_type.name|placeholder }}{{ object.aci_epg_object|linkify|placeholder }}
66 |
67 | {% include 'inc/panels/custom_fields.html' %} 68 |
69 |
70 | {% include 'inc/panels/tags.html' %} 71 | {% include 'inc/panels/comments.html' %} 72 |
73 |
74 | {% endblock content %} 75 | -------------------------------------------------------------------------------- /netbox_aci_plugin/validators.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.core.exceptions import ValidationError 6 | from django.core.validators import RegexValidator 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | from .choices import ( 10 | ContractFilterIPProtocolChoices, 11 | ContractFilterPortChoices, 12 | ContractFilterTCPRulesChoices, 13 | ) 14 | 15 | # 16 | # ACI Policy Validators 17 | # 18 | 19 | ACIPolicyNameValidator = RegexValidator( 20 | regex=r"^[a-zA-Z0-9_.:-]{1,64}$", 21 | message=_( 22 | "Only alphanumeric characters, hyphens, periods and underscores are allowed." 23 | ), 24 | code="invalid", 25 | ) 26 | 27 | ACIPolicyDescriptionValidator = RegexValidator( 28 | regex=r"^[a-zA-Z0-9\\!#$%()*,-./:;@ _{|}~?&+]{1,128}$", 29 | message=_("Only alphanumeric characters and !#$%%()*,-./:;@ _{|}~?&+ are allowed."), 30 | code="invalid", 31 | ) 32 | 33 | 34 | def validate_contract_filter_ip_protocol(value: str) -> None: 35 | """Validate the IP protocol value for ContractFilterEntry.""" 36 | # Check if the protocol value is a valid choice in the ChoiceSet 37 | if value in dict(ContractFilterIPProtocolChoices): 38 | return 39 | 40 | if value in dict(ContractFilterIPProtocolChoices) or value in [ 41 | str(i) for i in range(0, 256) 42 | ]: 43 | return 44 | 45 | # Check if the protocol value is a valid number between 0 and 255 46 | try: 47 | number = int(value) 48 | if 0 <= number <= 255: 49 | return 50 | except (ValueError, TypeError): 51 | pass 52 | 53 | # if neither condition is met, raise a ValidationError 54 | valid_choices = ", ".join(dict(ContractFilterIPProtocolChoices).keys()) 55 | raise ValidationError( 56 | _( 57 | f"IP Protocol must be a number between 0 and 255 or" 58 | f" one of the following values: {valid_choices}" 59 | ) 60 | ) 61 | 62 | 63 | def validate_contract_filter_port(value: str) -> None: 64 | """Validate the layer 4 port value for ContractFilterEntry.""" 65 | # Check if the port value is a valid choice in the ChoiceSet 66 | if value in dict(ContractFilterPortChoices): 67 | return 68 | 69 | # Check if the port value is a valid number between 0 and 65,535 70 | try: 71 | number = int(value) 72 | if 0 <= number <= 65535: 73 | return 74 | except (ValueError, TypeError): 75 | pass 76 | 77 | # if neither condition is met, raise a ValidationError 78 | valid_choices = ", ".join(dict(ContractFilterPortChoices).keys()) 79 | raise ValidationError( 80 | _( 81 | f"Layer 4 Port must be a number between 0 and 65535 or" 82 | f" one of the following values: {valid_choices}" 83 | ) 84 | ) 85 | 86 | 87 | def validate_contract_filter_tcp_rules(value_list: list[str]) -> None: 88 | """Validate the TCP rule combinations for ContractFilterEntry.""" 89 | if ( 90 | ContractFilterTCPRulesChoices.TCP_ESTABLISHED in value_list 91 | and len(value_list) > 1 92 | ): 93 | raise ValidationError(_("TCP rules cannot be combined with 'established'.")) 94 | if ( 95 | ContractFilterTCPRulesChoices.TCP_UNSPECIFIED in value_list 96 | and len(value_list) > 1 97 | ): 98 | raise ValidationError(_("TCP rules cannot be combined with 'unspecified'.")) 99 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/acicontract.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load helpers %} 5 | {% load i18n %} 6 | 7 | {% block breadcrumbs %} 8 | {{ block.super }} 9 | 10 | {% endblock breadcrumbs %} 11 | 12 | {% block content %} 13 |
14 |
15 |
16 |

{% trans "ACI Contract" %}

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 38 | 39 |
{% trans "ACI Tenant" %}{{ object.aci_tenant|linkify|placeholder }}
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "NetBox Tenant" %} 33 | {% if object.nb_tenant.group %} 34 | {{ object.nb_tenant.group|linkify }} / 35 | {% endif %} 36 | {{ object.nb_tenant|linkify|placeholder }} 37 |
40 |
41 |
42 |

{% trans "Scope" %}

43 | 44 | 45 | 46 | 47 | 48 |
{% trans "Scope" %}{% badge object.get_scope_display bg_color=object.get_scope_color %}
49 |
50 |
51 |

{% trans "Priority" %}

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
{% trans "QoS Class" %}{% badge object.get_qos_class_display bg_color=object.get_qos_class_color %}
{% trans "Target DSCP" %}{% badge object.get_target_dscp_display %}
62 |
63 | {% include 'inc/panels/custom_fields.html' %} 64 |
65 |
66 |
67 |

68 | {% trans "Subjects" %} 69 | {% if perms.netbox_aci_plugin.add_acicontractsubject %} 70 | 75 | {% endif %} 76 |

77 |
78 | {% render_table contract_subjects_table %} 79 |
80 |
81 | {% include 'inc/panels/tags.html' %} 82 | {% include 'inc/panels/comments.html' %} 83 |
84 |
85 | {% endblock content %} 86 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/aciusegnetworkattribute.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load i18n %} 5 | 6 | {% block breadcrumbs %} 7 | {{ block.super }} 8 | 9 | 10 | 11 | {% endblock breadcrumbs %} 12 | 13 | {% block content %} 14 |
15 |
16 |
17 |

{% trans "ACI uSeg Network Attribute" %}

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | 52 |
{% trans "ACI Tenant" %}{{ object.aci_tenant|linkify|placeholder }}
{% trans "ACI Application Profile" %}{{ object.aci_app_profile|linkify|placeholder }}
{% trans "ACI uSeg Endpoint Group" %}{{ object.aci_useg_endpoint_group|linkify|placeholder }}
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "Type" %}{% badge object.get_type_display bg_color=object.get_type_color %}
{% trans "NetBox Tenant" %} 46 | {% if object.nb_tenant.group %} 47 | {{ object.nb_tenant.group|linkify }} / 48 | {% endif %} 49 | {{ object.nb_tenant|linkify|placeholder }} 50 |
53 |
54 |
55 |

{% trans "EPG Subnet Settings" %}

56 | 57 | 58 | 59 | 60 | 61 |
{% trans "Use EPG Subnet" %}{% checkmark object.use_epg_subnet %}
62 |
63 |
64 |

{% trans "Attribute Assignment" %}

65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
{% trans "Attribute Object Type" %}{{ object.attr_object_type.name|placeholder }}
{% trans "Attribute Object" %}{{ object.attr_object|linkify|placeholder }}
75 |
76 | {% include 'inc/panels/custom_fields.html' %} 77 |
78 |
79 | {% include 'inc/panels/tags.html' %} 80 | {% include 'inc/panels/comments.html' %} 81 |
82 |
83 | {% endblock content %} 84 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/acicontractsubjectfilter.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load helpers %} 5 | {% load i18n %} 6 | 7 | {% block breadcrumbs %} 8 | {{ block.super }} 9 | 10 | 11 | 12 | {% endblock breadcrumbs %} 13 | 14 | {% block content %} 15 |
16 |
17 |
18 |

{% trans "ACI Contract Subject Filter" %}

19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
{% trans "ACI Tenant" %}{{ object.aci_contract_subject.aci_contract.aci_tenant|linkify|placeholder }}
{% trans "ACI Contract" %}{{ object.aci_contract_subject.aci_contract|linkify|placeholder }}
{% trans "ACI Contract Subject" %}{{ object.aci_contract_subject|linkify|placeholder }}
{% trans "ACI Contract Filter" %}{{ object.aci_contract_filter|linkify|placeholder }}
{% trans "Action" %}{% badge object.get_action_display bg_color=object.get_action_color %}
41 |
42 |
43 |

{% trans "Direction Settings" %}

44 | 45 | 46 | 47 | 48 | 49 |
{% trans "Apply Direction" %}{% badge object.get_apply_direction_display bg_color=object.get_apply_direction_color %}
50 |
51 |
52 |

{% trans "Directives Settings" %}

53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
{% trans "Logging enabled" %}{% checkmark object.log_enabled %}
{% trans "Policy Compression enabled" %}{% checkmark object.policy_compression_enabled %}
63 |
64 |
65 |

{% trans "Priority" %}

66 | 67 | 68 | 69 | 70 | 71 |
{% trans "(Deny) Priority" %}{% badge object.get_priority_display bg_color=object.get_priority_color %}
72 |
73 | {% include 'inc/panels/custom_fields.html' %} 74 |
75 |
76 | {% include 'inc/panels/tags.html' %} 77 | {% include 'inc/panels/comments.html' %} 78 |
79 |
80 | {% endblock content %} 81 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/forms/tenant/test_contracts.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.test import TestCase 6 | 7 | from ....forms.tenant.contracts import ( 8 | ACIContractEditForm, 9 | ACIContractSubjectEditForm, 10 | ) 11 | 12 | 13 | class ACIContractFormTestCase(TestCase): 14 | """Test case for ACIContract form.""" 15 | 16 | name_error_message: str = ( 17 | "Only alphanumeric characters, hyphens, periods and underscores are allowed." 18 | ) 19 | description_error_message: str = ( 20 | "Only alphanumeric characters and !#$%()*,-./:;@ _{|}~?&+ are allowed." 21 | ) 22 | 23 | def test_invalid_aci_contract_field_values(self) -> None: 24 | """Test validation of invalid ACI Contract field values.""" 25 | aci_contract = ACIContractEditForm( 26 | data={ 27 | "name": "ACI Contract Test 1", 28 | "name_alias": "ACI Test Alias 1", 29 | "description": "Invalid Description: ö", 30 | } 31 | ) 32 | self.assertEqual(aci_contract.errors["name"], [self.name_error_message]) 33 | self.assertEqual(aci_contract.errors["name_alias"], [self.name_error_message]) 34 | self.assertEqual( 35 | aci_contract.errors["description"], 36 | [self.description_error_message], 37 | ) 38 | 39 | def test_valid_aci_contract_field_values(self) -> None: 40 | """Test validation of valid ACI Contract field values.""" 41 | aci_contract = ACIContractEditForm( 42 | data={ 43 | "name": "ACIContract1", 44 | "name_alias": "Testing", 45 | "description": "Contract for NetBox ACI Plugin", 46 | } 47 | ) 48 | self.assertEqual(aci_contract.errors.get("name"), None) 49 | self.assertEqual(aci_contract.errors.get("name_alias"), None) 50 | self.assertEqual(aci_contract.errors.get("description"), None) 51 | 52 | 53 | class ACIContractSubjectFormTestCase(TestCase): 54 | """Test case for ACIContractSubject form.""" 55 | 56 | name_error_message: str = ( 57 | "Only alphanumeric characters, hyphens, periods and underscores are allowed." 58 | ) 59 | description_error_message: str = ( 60 | "Only alphanumeric characters and !#$%()*,-./:;@ _{|}~?&+ are allowed." 61 | ) 62 | 63 | def test_invalid_aci_contract_subject_field_values(self) -> None: 64 | """Test validation of invalid ACI Contract Subject field values.""" 65 | aci_contract_subject = ACIContractSubjectEditForm( 66 | data={ 67 | "name": "ACI Contract Subject Test 1", 68 | "name_alias": "ACI Test Alias 1", 69 | "description": "Invalid Description: ö", 70 | } 71 | ) 72 | self.assertEqual(aci_contract_subject.errors["name"], [self.name_error_message]) 73 | self.assertEqual( 74 | aci_contract_subject.errors["name_alias"], 75 | [self.name_error_message], 76 | ) 77 | self.assertEqual( 78 | aci_contract_subject.errors["description"], 79 | [self.description_error_message], 80 | ) 81 | 82 | def test_valid_aci_contract_subject_field_values(self) -> None: 83 | """Test validation of valid ACI Contract Subject field values.""" 84 | aci_contract_subject = ACIContractSubjectEditForm( 85 | data={ 86 | "name": "ACIContractSubject1", 87 | "name_alias": "Testing", 88 | "description": "Contract Subject for NetBox ACI Plugin", 89 | } 90 | ) 91 | self.assertEqual(aci_contract_subject.errors.get("name"), None) 92 | self.assertEqual(aci_contract_subject.errors.get("name_alias"), None) 93 | self.assertEqual(aci_contract_subject.errors.get("description"), None) 94 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.conf import settings 6 | from django.test import TestCase 7 | from netbox.plugins.navigation import PluginMenu, PluginMenuItem 8 | from netbox.registry import registry 9 | 10 | 11 | class PluginTest(TestCase): 12 | """Test case for plugin integration in NetBox.""" 13 | 14 | config_name: str = "ACIConfig" 15 | menu_group_count: int = 4 16 | menu_name: str = "ACI" 17 | # Menu group: Tenants 18 | menu_group_tenants_item_count: int = 1 19 | # Menu group: Tenant Application Profiles 20 | menu_group_tenant_app_profiles_item_count: int = 4 21 | # Menu group: Tenant Contracts 22 | menu_group_tenant_contracts_item_count: int = 4 23 | # Menu group: Tenant Networking 24 | menu_group_tenant_networking_item_count: int = 3 25 | 26 | def test_configuration(self) -> None: 27 | """Test for plugin configuration in NetBox.""" 28 | self.assertIn(f"netbox_aci_plugin.{self.config_name}", settings.INSTALLED_APPS) 29 | 30 | def test_menu(self) -> None: 31 | """Test for the main menu entry of the plugin in NetBox UI.""" 32 | menu_plugin_reg = registry["plugins"]["menus"][0] 33 | self.assertIsInstance(menu_plugin_reg, PluginMenu) 34 | self.assertEqual(menu_plugin_reg.label, self.menu_name) 35 | 36 | def test_menu_group_items(self) -> None: 37 | """Test for submenu entries of the plugin in NetBox UI.""" 38 | menu_plugin_reg_groups = registry["plugins"]["menus"][0].groups 39 | self.assertEqual(len(menu_plugin_reg_groups), self.menu_group_count) 40 | 41 | def test_menu_group_tenants_items(self) -> None: 42 | """Test for group 0 submenu entries of the plugin in NetBox UI.""" 43 | menu_plugin_reg_groups = registry["plugins"]["menus"][0].groups 44 | # Menu group: Tenants 45 | self.assertEqual(menu_plugin_reg_groups[0].label, "Tenants") 46 | self.assertEqual( 47 | len(menu_plugin_reg_groups[0].items), 48 | self.menu_group_tenants_item_count, 49 | ) 50 | self.assertIsInstance(menu_plugin_reg_groups[0].items[0], PluginMenuItem) 51 | 52 | def test_menu_group_tenant_appprofiles_items(self) -> None: 53 | """Test for group 1 submenu entries of the plugin in NetBox UI.""" 54 | menu_plugin_reg_groups = registry["plugins"]["menus"][0].groups 55 | # Menu group: Tenants 56 | self.assertEqual(menu_plugin_reg_groups[1].label, "Tenant Application Profiles") 57 | self.assertEqual( 58 | len(menu_plugin_reg_groups[1].items), 59 | self.menu_group_tenant_app_profiles_item_count, 60 | ) 61 | self.assertIsInstance(menu_plugin_reg_groups[1].items[0], PluginMenuItem) 62 | 63 | def test_menu_group_tenant_networking_items(self) -> None: 64 | """Test for group 2 submenu entries of the plugin in NetBox UI.""" 65 | menu_plugin_reg_groups = registry["plugins"]["menus"][0].groups 66 | # Menu group: Tenants 67 | self.assertEqual(menu_plugin_reg_groups[2].label, "Tenant Networking") 68 | self.assertEqual( 69 | len(menu_plugin_reg_groups[2].items), 70 | self.menu_group_tenant_networking_item_count, 71 | ) 72 | self.assertIsInstance(menu_plugin_reg_groups[2].items[0], PluginMenuItem) 73 | 74 | def test_menu_group_tenant_contracts_items(self) -> None: 75 | """Test for group 3 submenu entries of the plugin in NetBox UI.""" 76 | menu_plugin_reg_groups = registry["plugins"]["menus"][0].groups 77 | # Menu group: Tenants 78 | self.assertEqual(menu_plugin_reg_groups[3].label, "Tenant Contracts") 79 | self.assertEqual( 80 | len(menu_plugin_reg_groups[3].items), 81 | self.menu_group_tenant_contracts_item_count, 82 | ) 83 | self.assertIsInstance(menu_plugin_reg_groups[3].items[0], PluginMenuItem) 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | # ###### 3 | # 4 | # Source: https://github.com/github/gitignore/blob/main/Python.gitignore 5 | # 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | cover/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | .pybuilder/ 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | # For a library or package, you might want to ignore these files since the code is 93 | # intended to run in multiple environments; otherwise, check them in: 94 | # .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/#use-with-ide 116 | .pdm.toml 117 | 118 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 119 | __pypackages__/ 120 | 121 | # Celery stuff 122 | celerybeat-schedule 123 | celerybeat.pid 124 | 125 | # SageMath parsed files 126 | *.sage.py 127 | 128 | # Environments 129 | .env 130 | .venv 131 | env/ 132 | venv/ 133 | ENV/ 134 | env.bak/ 135 | venv.bak/ 136 | 137 | # Spyder project settings 138 | .spyderproject 139 | .spyproject 140 | 141 | # Rope project settings 142 | .ropeproject 143 | 144 | # mkdocs documentation 145 | /site 146 | 147 | # mypy 148 | .mypy_cache/ 149 | .dmypy.json 150 | dmypy.json 151 | 152 | # Pyre type checker 153 | .pyre/ 154 | 155 | # pytype static type analyzer 156 | .pytype/ 157 | 158 | # Cython debug symbols 159 | cython_debug/ 160 | 161 | # PyCharm 162 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 163 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 164 | # and can be added to the global gitignore or merged into this file. For a more nuclear 165 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 166 | #.idea/ 167 | 168 | # Ruff cache 169 | .ruff_cache/ 170 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/forms/tenant/test_contract_filters.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.test import TestCase 6 | 7 | from ....forms.tenant.contract_filters import ( 8 | ACIContractFilterEditForm, 9 | ACIContractFilterEntryEditForm, 10 | ) 11 | 12 | 13 | class ACIContractFilterFormTestCase(TestCase): 14 | """Test case for ACIContractFilter form.""" 15 | 16 | name_error_message: str = ( 17 | "Only alphanumeric characters, hyphens, periods and underscores are allowed." 18 | ) 19 | description_error_message: str = ( 20 | "Only alphanumeric characters and !#$%()*,-./:;@ _{|}~?&+ are allowed." 21 | ) 22 | 23 | def test_invalid_aci_contract_filter_field_values(self) -> None: 24 | """Test validation of invalid ACI Contract Filter field values.""" 25 | aci_contract_filter = ACIContractFilterEditForm( 26 | data={ 27 | "name": "ACI Contract Filter Test 1", 28 | "name_alias": "ACI Test Alias 1", 29 | "description": "Invalid Description: ö", 30 | } 31 | ) 32 | self.assertEqual(aci_contract_filter.errors["name"], [self.name_error_message]) 33 | self.assertEqual( 34 | aci_contract_filter.errors["name_alias"], [self.name_error_message] 35 | ) 36 | self.assertEqual( 37 | aci_contract_filter.errors["description"], 38 | [self.description_error_message], 39 | ) 40 | 41 | def test_valid_aci_contract_filter_field_values(self) -> None: 42 | """Test validation of valid ACI Contract Filter field values.""" 43 | aci_contract_filter = ACIContractFilterEditForm( 44 | data={ 45 | "name": "ACIContractFilter1", 46 | "name_alias": "Testing", 47 | "description": "Contract Filter for NetBox ACI Plugin", 48 | } 49 | ) 50 | self.assertEqual(aci_contract_filter.errors.get("name"), None) 51 | self.assertEqual(aci_contract_filter.errors.get("name_alias"), None) 52 | self.assertEqual(aci_contract_filter.errors.get("description"), None) 53 | 54 | 55 | class ACIContractFilterEntryFormTestCase(TestCase): 56 | """Test case for ACIContractFilterEntry form.""" 57 | 58 | name_error_message: str = ( 59 | "Only alphanumeric characters, hyphens, periods and underscores are allowed." 60 | ) 61 | description_error_message: str = ( 62 | "Only alphanumeric characters and !#$%()*,-./:;@ _{|}~?&+ are allowed." 63 | ) 64 | 65 | def test_invalid_aci_contract_filter_entry_field_values(self) -> None: 66 | """Test validation of invalid Contract Filter Entry field values.""" 67 | aci_contract_filter_entry = ACIContractFilterEntryEditForm( 68 | data={ 69 | "name": "ACI Contract Filter Entry Test 1", 70 | "name_alias": "ACI Test Alias 1", 71 | "description": "Invalid Description: ö", 72 | } 73 | ) 74 | self.assertEqual( 75 | aci_contract_filter_entry.errors["name"], [self.name_error_message] 76 | ) 77 | self.assertEqual( 78 | aci_contract_filter_entry.errors["name_alias"], 79 | [self.name_error_message], 80 | ) 81 | self.assertEqual( 82 | aci_contract_filter_entry.errors["description"], 83 | [self.description_error_message], 84 | ) 85 | 86 | def test_valid_aci_contract_filter_entry_field_values(self) -> None: 87 | """Test validation of valid Contract Filter Entry field values.""" 88 | aci_contract_filter_entry = ACIContractFilterEntryEditForm( 89 | data={ 90 | "name": "ACIContractFilterEntry1", 91 | "name_alias": "Testing", 92 | "description": "Contract Filter Entry for NetBox ACI Plugin", 93 | } 94 | ) 95 | self.assertEqual(aci_contract_filter_entry.errors.get("name"), None) 96 | self.assertEqual(aci_contract_filter_entry.errors.get("name_alias"), None) 97 | self.assertEqual(aci_contract_filter_entry.errors.get("description"), None) 98 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/schema.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | 6 | import strawberry 7 | import strawberry_django 8 | 9 | from .types import ( 10 | ACIAppProfileType, 11 | ACIBridgeDomainSubnetType, 12 | ACIBridgeDomainType, 13 | ACIContractFilterEntryType, 14 | ACIContractFilterType, 15 | ACIContractRelationType, 16 | ACIContractSubjectFilterType, 17 | ACIContractSubjectType, 18 | ACIContractType, 19 | ACIEndpointGroupType, 20 | ACIEndpointSecurityGroupType, 21 | ACIEsgEndpointGroupSelectorType, 22 | ACIEsgEndpointSelectorType, 23 | ACITenantType, 24 | ACIUSegEndpointGroupType, 25 | ACIUSegNetworkAttributeType, 26 | ACIVRFType, 27 | ) 28 | 29 | 30 | @strawberry.type(name="Query") 31 | class NetBoxACIQuery: 32 | """GraphQL query definition for the NetBox ACI Plugin.""" 33 | 34 | aci_tenant: ACITenantType = strawberry_django.field() 35 | aci_tenant_list: list[ACITenantType] = strawberry_django.field() 36 | 37 | aci_application_profile: ACIAppProfileType = strawberry_django.field() 38 | aci_application_profile_list: list[ACIAppProfileType] = strawberry_django.field() 39 | 40 | aci_vrf: ACIVRFType = strawberry_django.field() 41 | aci_vrf_list: list[ACIVRFType] = strawberry_django.field() 42 | 43 | aci_bridge_domain: ACIBridgeDomainType = strawberry_django.field() 44 | aci_bridge_domain_list: list[ACIBridgeDomainType] = strawberry_django.field() 45 | 46 | aci_bridge_domain_subnet: ACIBridgeDomainSubnetType = strawberry_django.field() 47 | aci_bridge_domain_subnet_list: list[ACIBridgeDomainSubnetType] = ( 48 | strawberry_django.field() 49 | ) 50 | 51 | aci_endpoint_group: ACIEndpointGroupType = strawberry_django.field() 52 | aci_endpoint_group_list: list[ACIEndpointGroupType] = strawberry_django.field() 53 | 54 | aci_useg_endpoint_group: ACIUSegEndpointGroupType = strawberry_django.field() 55 | aci_useg_endpoint_group_list: list[ACIUSegEndpointGroupType] = ( 56 | strawberry_django.field() 57 | ) 58 | 59 | aci_useg_network_attribute: ACIUSegNetworkAttributeType = strawberry_django.field() 60 | aci_useg_network_attribute_list: list[ACIUSegNetworkAttributeType] = ( 61 | strawberry_django.field() 62 | ) 63 | 64 | aci_endpoint_security_group: ACIEndpointSecurityGroupType = ( 65 | strawberry_django.field() 66 | ) 67 | aci_endpoint_security_group_list: list[ACIEndpointSecurityGroupType] = ( 68 | strawberry_django.field() 69 | ) 70 | 71 | aci_esg_endpoint_group_selector: ACIEsgEndpointGroupSelectorType = ( 72 | strawberry_django.field() 73 | ) 74 | aci_esg_endpoint_group_selector_list: list[ACIEsgEndpointGroupSelectorType] = ( 75 | strawberry_django.field() 76 | ) 77 | 78 | aci_esg_endpoint_selector: ACIEsgEndpointSelectorType = strawberry_django.field() 79 | aci_esg_endpoint_selector_list: list[ACIEsgEndpointSelectorType] = ( 80 | strawberry_django.field() 81 | ) 82 | 83 | aci_contract_filter: ACIContractFilterType = strawberry_django.field() 84 | aci_contract_filter_list: list[ACIContractFilterType] = strawberry_django.field() 85 | 86 | aci_contract_filter_entry: ACIContractFilterEntryType = strawberry_django.field() 87 | aci_contract_filter_entry_list: list[ACIContractFilterEntryType] = ( 88 | strawberry_django.field() 89 | ) 90 | 91 | aci_contract: ACIContractType = strawberry_django.field() 92 | aci_contract_list: list[ACIContractType] = strawberry_django.field() 93 | 94 | aci_contract_relation: ACIContractRelationType = strawberry_django.field() 95 | aci_contract_relation_list: list[ACIContractRelationType] = ( 96 | strawberry_django.field() 97 | ) 98 | 99 | aci_contract_subject: ACIContractSubjectType = strawberry_django.field() 100 | aci_contract_subject_list: list[ACIContractSubjectType] = strawberry_django.field() 101 | 102 | aci_contract_subject_filter: ACIContractSubjectFilterType = ( 103 | strawberry_django.field() 104 | ) 105 | aci_contract_subject_filter_list: list[ACIContractSubjectFilterType] = ( 106 | strawberry_django.field() 107 | ) 108 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/aciusegendpointgroup.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load i18n %} 5 | 6 | {% block breadcrumbs %} 7 | {{ block.super }} 8 | 9 | 10 | 11 | {% endblock breadcrumbs %} 12 | 13 | {% block content %} 14 |
15 |
16 |
17 |

{% trans "ACI uSeg Endpoint Group" %}

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | 52 |
{% trans "ACI Tenant" %}{{ object.aci_tenant|linkify|placeholder }}
{% trans "ACI Application Profile" %}{{ object.aci_app_profile|linkify|placeholder }}
{% trans "ACI VRF" %}{{ object.aci_vrf|linkify|placeholder }}
{% trans "ACI Bridge Domain" %}{{ object.aci_bridge_domain|linkify|placeholder }}
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "NetBox Tenant" %} 46 | {% if object.nb_tenant.group %} 47 | {{ object.nb_tenant.group|linkify }} / 48 | {% endif %} 49 | {{ object.nb_tenant|linkify|placeholder }} 50 |
53 |
54 |
55 |

{% trans "Policy Enforcement Settings" %}

56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
{% trans "Preferred Group Member enabled" %}{% checkmark object.preferred_group_member_enabled %}
{% trans "Intra-EPG Isolation enabled" %}{% checkmark object.intra_epg_isolation_enabled %}
66 |
67 |
68 |

{% trans "Endpoint Forwarding Settings" %}

69 | 70 | 71 | 72 | 73 | 74 |
{% trans "Flood in Encapsulation enabled" %}{% checkmark object.flood_in_encap_enabled %}
75 |
76 |
77 |

{% trans "Quality of Service (QoS) Settings" %}

78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
{% trans "QoS Class" %}{% badge object.get_qos_class_display bg_color=object.get_qos_class_color %}
{% trans "Custom QoS Policy" %}{{ object.custom_qos_policy_name|placeholder }}
88 |
89 | {% include 'inc/panels/custom_fields.html' %} 90 |
91 |
92 | {% include 'inc/panels/tags.html' %} 93 | {% include 'inc/panels/comments.html' %} 94 |
95 |
96 | {% endblock content %} 97 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/filters/tenant/contract_filters.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from typing import TYPE_CHECKING, Annotated 6 | 7 | import strawberry 8 | import strawberry_django 9 | from strawberry.scalars import ID 10 | from strawberry_django import FilterLookup 11 | 12 | from .... import models 13 | from ...filter_lookups import TCPRulesArrayLookup 14 | from ..mixins import ACIBaseFilterMixin 15 | 16 | if TYPE_CHECKING: 17 | from ...enums import ( 18 | ContractFilterARPOpenPeripheralCodesEnum, 19 | ContractFilterEtherTypeEnum, 20 | ContractFilterICMPv4TypesEnum, 21 | ContractFilterICMPv6TypesEnum, 22 | ContractFilterIPProtocolEnum, 23 | QualityOfServiceDSCPEnum, 24 | ) 25 | from .tenants import ACITenantFilter 26 | 27 | 28 | __all__ = ( 29 | "ACIContractFilterFilter", 30 | "ACIContractFilterEntryFilter", 31 | ) 32 | 33 | 34 | @strawberry_django.filter(models.ACIContractFilter, lookups=True) 35 | class ACIContractFilterFilter(ACIBaseFilterMixin): 36 | """GraphQL filter definition for the ACIContractFilter model.""" 37 | 38 | aci_tenant: ( 39 | Annotated[ 40 | "ACITenantFilter", 41 | strawberry.lazy("netbox_aci_plugin.graphql.filters"), 42 | ] 43 | | None 44 | ) = strawberry_django.filter_field() 45 | aci_tenant_id: ID | None = strawberry_django.filter_field() 46 | 47 | 48 | @strawberry_django.filter(models.ACIContractFilterEntry, lookups=True) 49 | class ACIContractFilterEntryFilter(ACIBaseFilterMixin): 50 | """GraphQL filter definition for the ACIContractFilterEntry model.""" 51 | 52 | aci_contract_filter: ( 53 | Annotated[ 54 | "ACIContractFilterFilter", 55 | strawberry.lazy("netbox_aci_plugin.graphql.filters"), 56 | ] 57 | | None 58 | ) = strawberry_django.filter_field() 59 | aci_contract_filter_id: ID | None = strawberry_django.filter_field() 60 | arp_opc: ( 61 | Annotated[ 62 | "ContractFilterARPOpenPeripheralCodesEnum", 63 | strawberry.lazy("netbox_aci_plugin.graphql.enums"), 64 | ] 65 | | None 66 | ) = strawberry_django.filter_field() 67 | destination_from_port: FilterLookup[str] | None = strawberry_django.filter_field() 68 | destination_to_port: FilterLookup[str] | None = strawberry_django.filter_field() 69 | ether_type: ( 70 | Annotated[ 71 | "ContractFilterEtherTypeEnum", 72 | strawberry.lazy("netbox_aci_plugin.graphql.enums"), 73 | ] 74 | | None 75 | ) = strawberry_django.filter_field() 76 | icmp_v4_type: ( 77 | Annotated[ 78 | "ContractFilterICMPv4TypesEnum", 79 | strawberry.lazy("netbox_aci_plugin.graphql.enums"), 80 | ] 81 | | None 82 | ) = strawberry_django.filter_field() 83 | icmp_v6_type: ( 84 | Annotated[ 85 | "ContractFilterICMPv6TypesEnum", 86 | strawberry.lazy("netbox_aci_plugin.graphql.enums"), 87 | ] 88 | | None 89 | ) = strawberry_django.filter_field() 90 | ip_protocol: ( 91 | Annotated[ 92 | "ContractFilterIPProtocolEnum", 93 | strawberry.lazy("netbox_aci_plugin.graphql.enums"), 94 | ] 95 | | None 96 | ) = strawberry_django.filter_field() 97 | match_dscp: ( 98 | Annotated[ 99 | "QualityOfServiceDSCPEnum", 100 | strawberry.lazy("netbox_aci_plugin.graphql.enums"), 101 | ] 102 | | None 103 | ) = strawberry_django.filter_field() 104 | match_only_fragments_enabled: FilterLookup[bool] | None = ( 105 | strawberry_django.filter_field() 106 | ) 107 | source_from_port: FilterLookup[str] | None = strawberry_django.filter_field() 108 | source_to_port: FilterLookup[str] | None = strawberry_django.filter_field() 109 | stateful_enabled: FilterLookup[bool] | None = strawberry_django.filter_field() 110 | tcp_rules: ( 111 | Annotated[ 112 | "TCPRulesArrayLookup", 113 | strawberry.lazy("netbox_aci_plugin.graphql.filter_lookups"), 114 | ] 115 | | None 116 | ) = strawberry_django.filter_field() 117 | -------------------------------------------------------------------------------- /netbox_aci_plugin/models/mixins.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.core.exceptions import FieldDoesNotExist 6 | from django.core.validators import ValidationError 7 | from django.utils.translation import gettext as _ 8 | 9 | 10 | class UniqueGenericForeignKeyMixin: 11 | """Enforce uniqueness for models with a GenericForeignKey. 12 | 13 | Attributes: 14 | generic_fk_field: Name of the GenericForeignKey attribute (for 15 | example, "aci_object"). 16 | generic_unique_fields: Tuple of additional field names that, 17 | together with the GenericForeignKey, must be unique (for 18 | example, ("aci_contract", "role")). 19 | 20 | Notes: 21 | This mixin assumes the underlying fields for the GenericForeignKey 22 | follow the "_type" and "_id" 23 | naming convention. 24 | """ 25 | 26 | generic_fk_field: str 27 | generic_unique_fields: tuple[str] = () 28 | 29 | def _validate_generic_uniqueness(self) -> None: 30 | """Validate the uniqueness of the instance. 31 | 32 | Ensure that no other instance exists with the same values for the 33 | specified GenericForeignKey and any additional fields defined in 34 | generic_unique_fields. 35 | If a duplicate is found (excluding the current instance on update), 36 | raise a ValidationError. 37 | """ 38 | if not getattr(self, "generic_fk_field", None): 39 | raise NotImplementedError( 40 | _("You must define 'generic_fk_field' in your model.") 41 | ) 42 | 43 | # Determine the underlying fields for the GenericForeignKey. 44 | type_field = f"{self.generic_fk_field}_type" 45 | id_field = f"{self.generic_fk_field}_id" 46 | filter_kwargs = { 47 | type_field: getattr(self, type_field), 48 | id_field: getattr(self, id_field), 49 | } 50 | 51 | # Add additional unique fields to the filter. 52 | for field in self.generic_unique_fields: 53 | filter_kwargs[field] = getattr(self, field) 54 | 55 | # Filter out the current instance (if editing an existing record). 56 | qs = self.__class__.objects.filter(**filter_kwargs) 57 | if self.pk: 58 | qs = qs.exclude(pk=self.pk) 59 | 60 | # If any instance matches these field values, raise an error. 61 | if qs.exists(): 62 | # We'll only include the additional unique fields but also prepend 63 | # the underlying model name from the GFK for context. 64 | fields_in_conflict = tuple(self.generic_unique_fields) 65 | 66 | content_type = getattr(self, type_field, None) 67 | # Safely get the model name or fall back to an "Unknown" label 68 | model_class = content_type.model_class() if content_type else None 69 | model_name = ( 70 | str(model_class._meta.verbose_name) 71 | if model_class and hasattr(model_class, "_meta") 72 | else _("Unknown model") 73 | ) 74 | 75 | # Start the collected verbose names with the model name 76 | field_verbose_names = [model_name] 77 | 78 | # Convert each field to its verbose name if possible 79 | for field_name in fields_in_conflict: 80 | try: 81 | model_field = self._meta.get_field(field_name) 82 | verbose_name = str(model_field.verbose_name) 83 | except FieldDoesNotExist: 84 | # Fallback to the raw field name if it's not a recognized 85 | # model field 86 | verbose_name = field_name 87 | field_verbose_names.append(verbose_name) 88 | 89 | # Build a descriptive, comma-separated list of these field names. 90 | field_names_str = ", ".join(field_verbose_names) 91 | 92 | raise ValidationError( 93 | { 94 | "__all__": _( 95 | "A record already exists using the same values for " 96 | "the following fields: {field_verbose_names}" 97 | ).format(field_verbose_names=field_names_str) 98 | } 99 | ) 100 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/acivrf.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load i18n %} 5 | 6 | {% block breadcrumbs %} 7 | {{ block.super }} 8 | 9 | {% endblock breadcrumbs %} 10 | 11 | {% block content %} 12 |
13 |
14 |
15 |

{% trans "ACI VRF" %}

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 44 |
{% trans "ACI Tenant" %}{{ object.aci_tenant|linkify|placeholder }}
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "NetBox Tenant" %} 32 | {% if object.nb_tenant.group %} 33 | {{ object.nb_tenant.group|linkify }} / 34 | {% endif %} 35 | {{ object.nb_tenant|linkify|placeholder }} 36 |
{% trans "NetBox VRF" %} 41 | {{ object.nb_vrf|linkify|placeholder }} 42 |
45 |
46 |
47 |

{% trans "Policy Control Settings" %}

48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
{% trans "Policy Control Enforcement Direction" %}{% badge object.get_pc_enforcement_direction_display bg_color=object.get_pc_enforcement_direction_color %}
{% trans "Policy Control Enforcement Preference" %}{% badge object.get_pc_enforcement_preference_display bg_color=object.get_pc_enforcement_preference_color %}
{% trans "Bridge Domain Enforcement" %}{% checkmark object.bd_enforcement_enabled %}
{% trans "Preferred Group" %}{% checkmark object.preferred_group_enabled %}
66 |
67 |
68 |

{% trans "Endpoint Learning Settings" %}

69 | 70 | 71 | 72 | 73 | 74 |
{% trans "IP Data Plane Learning" %}{% checkmark object.ip_data_plane_learning_enabled %}
75 |
76 |
77 |

{% trans "Multicast Settings" %}

78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
{% trans "PIM (Multicast) IPv4" %}{% checkmark object.pim_ipv4_enabled %}
{% trans "PIM (Multicast) IPv6" %}{% checkmark object.pim_ipv6_enabled %}
88 |
89 | {% include 'inc/panels/custom_fields.html' %} 90 |
91 |
92 |
93 |

{% trans "Additional Settings" %}

94 | 95 | 96 | 97 | 98 | 99 |
{% trans "DNS Labels" %}{{ object.dns_labels|join:", "|placeholder }}
100 |
101 | {% include 'inc/panels/tags.html' %} 102 | {% include 'inc/panels/comments.html' %} 103 |
104 |
105 | {% endblock content %} 106 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/enums.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import strawberry 6 | 7 | from ..choices import ( 8 | BDMultiDestinationFloodingChoices, 9 | BDUnknownMulticastChoices, 10 | BDUnknownUnicastChoices, 11 | ContractFilterARPOpenPeripheralCodesChoices, 12 | ContractFilterEtherTypeChoices, 13 | ContractFilterICMPv4TypesChoices, 14 | ContractFilterICMPv6TypesChoices, 15 | ContractFilterIPProtocolChoices, 16 | ContractFilterPortChoices, 17 | ContractFilterTCPRulesChoices, 18 | ContractRelationRoleChoices, 19 | ContractScopeChoices, 20 | ContractSubjectFilterActionChoices, 21 | ContractSubjectFilterApplyDirectionChoices, 22 | ContractSubjectFilterPriorityChoices, 23 | QualityOfServiceClassChoices, 24 | QualityOfServiceDSCPChoices, 25 | USegAttributeMatchOperatorChoices, 26 | USegAttributeTypeChoices, 27 | VRFPCEnforcementDirectionChoices, 28 | VRFPCEnforcementPreferenceChoices, 29 | ) 30 | 31 | __all__ = ( 32 | "BDMultiDestinationFloodingEnum", 33 | "BDUnknownMulticastEnum", 34 | "BDUnknownUnicastEnum", 35 | "ContractFilterARPOpenPeripheralCodesEnum", 36 | "ContractFilterEtherTypeEnum", 37 | "ContractFilterICMPv4TypesEnum", 38 | "ContractFilterICMPv6TypesEnum", 39 | "ContractFilterIPProtocolEnum", 40 | "ContractFilterPortEnum", 41 | "ContractFilterTCPRulesEnum", 42 | "ContractRelationRoleEnum", 43 | "ContractScopeEnum", 44 | "ContractSubjectFilterActionEnum", 45 | "ContractSubjectFilterApplyDirectionEnum", 46 | "ContractSubjectFilterPriorityEnum", 47 | "QualityOfServiceClassEnum", 48 | "QualityOfServiceDSCPEnum", 49 | "USegAttributeMatchOperatorEnum", 50 | "USegAttributeTypeEnum", 51 | "VRFPCEnforcementDirectionEnum", 52 | "VRFPCEnforcementPreferenceEnum", 53 | ) 54 | 55 | # 56 | # Bridge Domain 57 | # 58 | 59 | BDMultiDestinationFloodingEnum = strawberry.enum( 60 | BDMultiDestinationFloodingChoices.as_enum() 61 | ) 62 | BDUnknownMulticastEnum = strawberry.enum(BDUnknownMulticastChoices.as_enum()) 63 | BDUnknownUnicastEnum = strawberry.enum(BDUnknownUnicastChoices.as_enum()) 64 | 65 | # 66 | # Contract Filter 67 | # 68 | 69 | ContractFilterARPOpenPeripheralCodesEnum = strawberry.enum( 70 | ContractFilterARPOpenPeripheralCodesChoices.as_enum() 71 | ) 72 | ContractFilterEtherTypeEnum = strawberry.enum(ContractFilterEtherTypeChoices.as_enum()) 73 | ContractFilterICMPv4TypesEnum = strawberry.enum( 74 | ContractFilterICMPv4TypesChoices.as_enum() 75 | ) 76 | ContractFilterICMPv6TypesEnum = strawberry.enum( 77 | ContractFilterICMPv6TypesChoices.as_enum() 78 | ) 79 | ContractFilterIPProtocolEnum = strawberry.enum( 80 | ContractFilterIPProtocolChoices.as_enum() 81 | ) 82 | ContractFilterPortEnum = strawberry.enum(ContractFilterPortChoices.as_enum()) 83 | ContractFilterTCPRulesEnum = strawberry.enum(ContractFilterTCPRulesChoices.as_enum()) 84 | 85 | # 86 | # Contract 87 | # 88 | 89 | ContractScopeEnum = strawberry.enum(ContractScopeChoices.as_enum()) 90 | 91 | # 92 | # Contract Relation 93 | # 94 | 95 | ContractRelationRoleEnum = strawberry.enum(ContractRelationRoleChoices.as_enum()) 96 | 97 | # 98 | # Contract Subject Filter 99 | # 100 | 101 | ContractSubjectFilterActionEnum = strawberry.enum( 102 | ContractSubjectFilterActionChoices.as_enum() 103 | ) 104 | ContractSubjectFilterApplyDirectionEnum = strawberry.enum( 105 | ContractSubjectFilterApplyDirectionChoices.as_enum() 106 | ) 107 | ContractSubjectFilterPriorityEnum = strawberry.enum( 108 | ContractSubjectFilterPriorityChoices.as_enum() 109 | ) 110 | 111 | # 112 | # Quality of Service (QoS) 113 | # 114 | 115 | QualityOfServiceClassEnum = strawberry.enum(QualityOfServiceClassChoices.as_enum()) 116 | QualityOfServiceDSCPEnum = strawberry.enum(QualityOfServiceDSCPChoices.as_enum()) 117 | 118 | # 119 | # uSeg EPG 120 | # 121 | 122 | USegAttributeMatchOperatorEnum = strawberry.enum( 123 | USegAttributeMatchOperatorChoices.as_enum() 124 | ) 125 | 126 | # 127 | # uSeg Attribute 128 | # 129 | 130 | USegAttributeTypeEnum = strawberry.enum(USegAttributeTypeChoices.as_enum()) 131 | 132 | # 133 | # VRF 134 | # 135 | 136 | VRFPCEnforcementDirectionEnum = strawberry.enum( 137 | VRFPCEnforcementDirectionChoices.as_enum() 138 | ) 139 | VRFPCEnforcementPreferenceEnum = strawberry.enum( 140 | VRFPCEnforcementPreferenceChoices.as_enum() 141 | ) 142 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/aciendpointgroup.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load i18n %} 5 | 6 | {% block breadcrumbs %} 7 | {{ block.super }} 8 | 9 | 10 | 11 | {% endblock breadcrumbs %} 12 | 13 | {% block content %} 14 |
15 |
16 |
17 |

{% trans "ACI Endpoint Group" %}

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | 52 |
{% trans "ACI Tenant" %}{{ object.aci_tenant|linkify|placeholder }}
{% trans "ACI Application Profile" %}{{ object.aci_app_profile|linkify|placeholder }}
{% trans "ACI VRF" %}{{ object.aci_vrf|linkify|placeholder }}
{% trans "ACI Bridge Domain" %}{{ object.aci_bridge_domain|linkify|placeholder }}
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "NetBox Tenant" %} 46 | {% if object.nb_tenant.group %} 47 | {{ object.nb_tenant.group|linkify }} / 48 | {% endif %} 49 | {{ object.nb_tenant|linkify|placeholder }} 50 |
53 |
54 |
55 |

{% trans "Policy Enforcement Settings" %}

56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
{% trans "Preferred Group Member enabled" %}{% checkmark object.preferred_group_member_enabled %}
{% trans "Intra-EPG Isolation enabled" %}{% checkmark object.intra_epg_isolation_enabled %}
66 |
67 |
68 |

{% trans "Endpoint Forwarding Settings" %}

69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
{% trans "Flood in Encapsulation enabled" %}{% checkmark object.flood_in_encap_enabled %}
{% trans "Proxy ARP enabled" %}{% checkmark object.proxy_arp_enabled %}
79 |
80 |
81 |

{% trans "Quality of Service (QoS) Settings" %}

82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
{% trans "QoS Class" %}{% badge object.get_qos_class_display bg_color=object.get_qos_class_color %}
{% trans "Custom QoS Policy" %}{{ object.custom_qos_policy_name|placeholder }}
92 |
93 | {% include 'inc/panels/custom_fields.html' %} 94 |
95 |
96 | {% include 'inc/panels/tags.html' %} 97 | {% include 'inc/panels/comments.html' %} 98 |
99 |
100 | {% endblock content %} 101 | -------------------------------------------------------------------------------- /netbox_aci_plugin/filtersets/tenant/vrfs.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import django_filters 6 | from django.contrib.postgres.fields import ArrayField 7 | from django.db.models import Q 8 | from django.utils.translation import gettext_lazy as _ 9 | from drf_spectacular.types import OpenApiTypes 10 | from drf_spectacular.utils import extend_schema_field 11 | from ipam.models import VRF 12 | from netbox.filtersets import NetBoxModelFilterSet 13 | from tenancy.models import Tenant 14 | 15 | from ...choices import ( 16 | VRFPCEnforcementDirectionChoices, 17 | VRFPCEnforcementPreferenceChoices, 18 | ) 19 | from ...models.tenant.tenants import ACITenant 20 | from ...models.tenant.vrfs import ACIVRF 21 | 22 | 23 | class ACIVRFFilterSet(NetBoxModelFilterSet): 24 | """Filter set for the ACI VRF model.""" 25 | 26 | aci_tenant = django_filters.ModelMultipleChoiceFilter( 27 | field_name="aci_tenant__name", 28 | queryset=ACITenant.objects.all(), 29 | to_field_name="name", 30 | label=_("ACI Tenant (name)"), 31 | ) 32 | aci_tenant_id = django_filters.ModelMultipleChoiceFilter( 33 | queryset=ACITenant.objects.all(), 34 | to_field_name="id", 35 | label=_("ACI Tenant (ID)"), 36 | ) 37 | nb_tenant = django_filters.ModelMultipleChoiceFilter( 38 | field_name="nb_tenant__name", 39 | queryset=Tenant.objects.all(), 40 | to_field_name="name", 41 | label=_("NetBox tenant (name)"), 42 | ) 43 | nb_tenant_id = django_filters.ModelMultipleChoiceFilter( 44 | queryset=Tenant.objects.all(), 45 | to_field_name="id", 46 | label=_("NetBox tenant (ID)"), 47 | ) 48 | nb_vrf = django_filters.ModelMultipleChoiceFilter( 49 | field_name="nb_vrf__name", 50 | queryset=VRF.objects.all(), 51 | to_field_name="name", 52 | label=_("NetBox VRF (name)"), 53 | ) 54 | nb_vrf_id = django_filters.ModelMultipleChoiceFilter( 55 | queryset=VRF.objects.all(), 56 | to_field_name="id", 57 | label=_("NetBox VRF (ID)"), 58 | ) 59 | pc_enforcement_direction = django_filters.MultipleChoiceFilter( 60 | choices=VRFPCEnforcementDirectionChoices, 61 | null_value=None, 62 | ) 63 | pc_enforcement_preference = django_filters.MultipleChoiceFilter( 64 | choices=VRFPCEnforcementPreferenceChoices, 65 | null_value=None, 66 | ) 67 | 68 | # Filters extended with a custom filter method 69 | present_in_aci_tenant_or_common_id = django_filters.ModelChoiceFilter( 70 | queryset=ACITenant.objects.all(), 71 | method="filter_present_in_aci_tenant_or_common_id", 72 | label=_("ACI Tenant (ID)"), 73 | ) 74 | 75 | class Meta: 76 | model = ACIVRF 77 | fields: tuple = ( 78 | "id", 79 | "name", 80 | "name_alias", 81 | "description", 82 | "aci_tenant", 83 | "nb_tenant", 84 | "nb_vrf", 85 | "bd_enforcement_enabled", 86 | "dns_labels", 87 | "ip_data_plane_learning_enabled", 88 | "pc_enforcement_direction", 89 | "pc_enforcement_preference", 90 | "pim_ipv4_enabled", 91 | "pim_ipv6_enabled", 92 | "preferred_group_enabled", 93 | ) 94 | filter_overrides = { 95 | ArrayField: { 96 | "filter_class": django_filters.CharFilter, 97 | "extra": lambda f: { 98 | "lookup_expr": "icontains", 99 | }, 100 | } 101 | } 102 | 103 | def search(self, queryset, name, value): 104 | """Return a QuerySet filtered by the model's description.""" 105 | if not value.strip(): 106 | return queryset 107 | queryset_filter: Q = ( 108 | Q(name__icontains=value) 109 | | Q(name_alias__icontains=value) 110 | | Q(description__icontains=value) 111 | ) 112 | return queryset.filter(queryset_filter) 113 | 114 | @extend_schema_field(OpenApiTypes.INT) 115 | def filter_present_in_aci_tenant_or_common_id(self, queryset, name, aci_tenant_id): 116 | """Return a QuerySet filtered by given ACI Tenant or 'common'.""" 117 | if aci_tenant_id is None: 118 | return queryset.none() 119 | return queryset.filter( 120 | Q(aci_tenant=aci_tenant_id) | Q(aci_tenant__name="common") 121 | ) 122 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/forms/tenant/test_bridge_domains.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.test import TestCase 6 | 7 | from ....forms.tenant.bridge_domains import ( 8 | ACIBridgeDomainEditForm, 9 | ACIBridgeDomainSubnetEditForm, 10 | ) 11 | from ....models.tenant.tenants import ACITenant 12 | from ....models.tenant.vrfs import ACIVRF 13 | 14 | 15 | class ACIBridgeDomainFormTestCase(TestCase): 16 | """Test case for ACIBridgeDomain form.""" 17 | 18 | name_error_message: str = ( 19 | "Only alphanumeric characters, hyphens, periods and underscores are allowed." 20 | ) 21 | description_error_message: str = ( 22 | "Only alphanumeric characters and !#$%()*,-./:;@ _{|}~?&+ are allowed." 23 | ) 24 | 25 | @classmethod 26 | def setUp(cls): 27 | """Set up required objects for ACIBridgeDomainForm tests.""" 28 | cls.aci_tenant = ACITenant.objects.create(name="ACITestTenant") 29 | cls.aci_vrf = ACIVRF.objects.create( 30 | name="ACITestVRF", aci_tenant=cls.aci_tenant 31 | ) 32 | 33 | def test_invalid_aci_bridge_domain_field_values(self) -> None: 34 | """Test validation of invalid ACI Bridge Domain field values.""" 35 | aci_bd_form = ACIBridgeDomainEditForm( 36 | data={ 37 | "name": "ACI BD Test 1", 38 | "name_alias": "ACI Test Alias 1", 39 | "description": "Invalid Description: ö", 40 | "aci_tenant": self.aci_tenant, 41 | "aci_vrf": self.aci_vrf, 42 | } 43 | ) 44 | self.assertEqual(aci_bd_form.errors["name"], [self.name_error_message]) 45 | self.assertEqual(aci_bd_form.errors["name_alias"], [self.name_error_message]) 46 | self.assertEqual( 47 | aci_bd_form.errors["description"], 48 | [self.description_error_message], 49 | ) 50 | 51 | def test_valid_aci_bridge_domain_field_values(self) -> None: 52 | """Test validation of valid ACI Bridge Domain field values.""" 53 | aci_bd_form = ACIBridgeDomainEditForm( 54 | data={ 55 | "name": "ACIBD1", 56 | "name_alias": "Testing", 57 | "description": "BD for NetBox ACI Plugin", 58 | "aci_tenant": self.aci_tenant, 59 | "aci_vrf": self.aci_vrf, 60 | } 61 | ) 62 | self.assertEqual(aci_bd_form.errors.get("name"), None) 63 | self.assertEqual(aci_bd_form.errors.get("name_alias"), None) 64 | self.assertEqual(aci_bd_form.errors.get("description"), None) 65 | 66 | 67 | class ACIBridgeDomainSubnetFormTestCase(TestCase): 68 | """Test case for ACIBridgeDomainSubnet form.""" 69 | 70 | name_error_message: str = ( 71 | "Only alphanumeric characters, hyphens, periods and underscores are allowed." 72 | ) 73 | description_error_message: str = ( 74 | "Only alphanumeric characters and !#$%()*,-./:;@ _{|}~?&+ are allowed." 75 | ) 76 | 77 | def test_invalid_aci_bridge_domain_subnet_field_values(self) -> None: 78 | """Test validation of invalid ACI Bridge Domain Subnet field values.""" 79 | aci_bd_subnet_form = ACIBridgeDomainSubnetEditForm( 80 | data={ 81 | "name": "ACI BDSubnet Test 1", 82 | "name_alias": "ACI Test Alias 1", 83 | "description": "Invalid Description: ö", 84 | } 85 | ) 86 | self.assertEqual(aci_bd_subnet_form.errors["name"], [self.name_error_message]) 87 | self.assertEqual( 88 | aci_bd_subnet_form.errors["name_alias"], [self.name_error_message] 89 | ) 90 | self.assertEqual( 91 | aci_bd_subnet_form.errors["description"], 92 | [self.description_error_message], 93 | ) 94 | 95 | def test_valid_aci_bridge_domain_subnet_field_values(self) -> None: 96 | """Test validation of valid ACI Bridge Domain Subnet field values.""" 97 | aci_bd_subnet_form = ACIBridgeDomainSubnetEditForm( 98 | data={ 99 | "name": "ACIBDSubnet1", 100 | "name_alias": "Testing", 101 | "description": "BDSubnet for NetBox ACI Plugin", 102 | } 103 | ) 104 | self.assertEqual(aci_bd_subnet_form.errors.get("name"), None) 105 | self.assertEqual(aci_bd_subnet_form.errors.get("name_alias"), None) 106 | self.assertEqual(aci_bd_subnet_form.errors.get("description"), None) 107 | -------------------------------------------------------------------------------- /netbox_aci_plugin/graphql/filters/tenant/endpoint_groups.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from dataclasses import dataclass 6 | from typing import TYPE_CHECKING, Annotated 7 | 8 | import strawberry 9 | import strawberry_django 10 | from core.graphql.filters import ContentTypeFilter 11 | from strawberry.scalars import ID 12 | from strawberry_django import FilterLookup 13 | 14 | from .... import models 15 | from ..mixins import ACIBaseFilterMixin 16 | 17 | if TYPE_CHECKING: 18 | from ...enums import ( 19 | QualityOfServiceClassEnum, 20 | USegAttributeMatchOperatorEnum, 21 | USegAttributeTypeEnum, 22 | ) 23 | from .app_profiles import ACIAppProfileFilter 24 | from .bridge_domains import ACIBridgeDomainFilter 25 | 26 | 27 | __all__ = ( 28 | "ACIEndpointGroupFilter", 29 | "ACIUSegEndpointGroupFilter", 30 | "ACIUSegNetworkAttributeFilter", 31 | ) 32 | 33 | 34 | @dataclass 35 | class ACIEndpointGroupBaseFilterMixin(ACIBaseFilterMixin): 36 | """Base GraphQL filter mixin for ACI Endpoint Group models.""" 37 | 38 | aci_app_profile: ( 39 | Annotated[ 40 | "ACIAppProfileFilter", 41 | strawberry.lazy("netbox_aci_plugin.graphql.filters"), 42 | ] 43 | | None 44 | ) = strawberry_django.filter_field() 45 | aci_app_profile_id: ID | None = strawberry_django.filter_field() 46 | aci_bridge_domain: ( 47 | Annotated[ 48 | "ACIBridgeDomainFilter", 49 | strawberry.lazy("netbox_aci_plugin.graphql.filters"), 50 | ] 51 | | None 52 | ) = strawberry_django.filter_field() 53 | aci_bridge_domain_id: ID | None = strawberry_django.filter_field() 54 | admin_shutdown: FilterLookup[bool] | None = strawberry_django.filter_field() 55 | custom_qos_policy_name: FilterLookup[str] | None = strawberry_django.filter_field() 56 | flood_in_encap_enabled: FilterLookup[bool] | None = strawberry_django.filter_field() 57 | intra_epg_isolation_enabled: FilterLookup[bool] | None = ( 58 | strawberry_django.filter_field() 59 | ) 60 | qos_class: ( 61 | Annotated[ 62 | "QualityOfServiceClassEnum", 63 | strawberry.lazy("netbox_aci_plugin.graphql.enums"), 64 | ] 65 | | None 66 | ) = strawberry_django.filter_field() 67 | preferred_group_member_enabled: FilterLookup[bool] | None = ( 68 | strawberry_django.filter_field() 69 | ) 70 | 71 | 72 | @strawberry_django.filter(models.ACIEndpointGroup, lookups=True) 73 | class ACIEndpointGroupFilter(ACIEndpointGroupBaseFilterMixin): 74 | """GraphQL filter definition for the ACIEndpointGroup model.""" 75 | 76 | proxy_arp_enabled: FilterLookup[bool] | None = strawberry_django.filter_field() 77 | 78 | 79 | @strawberry_django.filter(models.ACIUSegEndpointGroup, lookups=True) 80 | class ACIUSegEndpointGroupFilter(ACIEndpointGroupBaseFilterMixin): 81 | """GraphQL filter definition for the ACIUSegEndpointGroup model.""" 82 | 83 | match_operator: ( 84 | Annotated[ 85 | "USegAttributeMatchOperatorEnum", 86 | strawberry.lazy("netbox_aci_plugin.graphql.enums"), 87 | ] 88 | | None 89 | ) = strawberry_django.filter_field() 90 | 91 | 92 | @dataclass 93 | class ACIUSegAttributeBaseFilterMixin(ACIBaseFilterMixin): 94 | """Base GraphQL filter mixin for ACI uSeg Attribute models.""" 95 | 96 | aci_useg_endpoint_group: ( 97 | Annotated[ 98 | "ACIUSegEndpointGroupFilter", 99 | strawberry.lazy("netbox_aci_plugin.graphql.filters"), 100 | ] 101 | | None 102 | ) = strawberry_django.filter_field() 103 | aci_useg_endpoint_group_id: ID | None = strawberry_django.filter_field() 104 | type: ( 105 | Annotated[ 106 | "USegAttributeTypeEnum", 107 | strawberry.lazy("netbox_aci_plugin.graphql.enums"), 108 | ] 109 | | None 110 | ) = strawberry_django.filter_field() 111 | 112 | 113 | @strawberry_django.filter(models.ACIUSegNetworkAttribute, lookups=True) 114 | class ACIUSegNetworkAttributeFilter(ACIUSegAttributeBaseFilterMixin): 115 | """GraphQL filter definition for the ACIUSegNetworkAttribute model.""" 116 | 117 | attr_object_type: ( 118 | Annotated["ContentTypeFilter", strawberry.lazy("core.graphql.filters")] | None 119 | ) = strawberry_django.filter_field() 120 | attr_object_id: ID | None = strawberry_django.filter_field() 121 | use_epg_subnet: FilterLookup[bool] | None = strawberry_django.filter_field() 122 | -------------------------------------------------------------------------------- /netbox_aci_plugin/api/serializers/tenant/bridge_domains.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from ipam.api.serializers import IPAddressSerializer 6 | from netbox.api.serializers import NetBoxModelSerializer 7 | from rest_framework import serializers 8 | from tenancy.api.serializers import TenantSerializer 9 | 10 | from ....models.tenant.bridge_domains import ( 11 | ACIBridgeDomain, 12 | ACIBridgeDomainSubnet, 13 | ) 14 | from .tenants import ACITenantSerializer 15 | from .vrfs import ACIVRFSerializer 16 | 17 | 18 | class ACIBridgeDomainSerializer(NetBoxModelSerializer): 19 | """Serializer for the ACI Bridge Domain model.""" 20 | 21 | url = serializers.HyperlinkedIdentityField( 22 | view_name="plugins-api:netbox_aci_plugin-api:acibridgedomain-detail" 23 | ) 24 | aci_tenant = ACITenantSerializer(nested=True, required=True) 25 | aci_vrf = ACIVRFSerializer(nested=True, required=True) 26 | nb_tenant = TenantSerializer(nested=True, required=False, allow_null=True) 27 | mac_address = serializers.CharField( 28 | required=False, default=None, allow_blank=True, allow_null=True 29 | ) 30 | virtual_mac_address = serializers.CharField( 31 | required=False, default=None, allow_blank=True, allow_null=True 32 | ) 33 | 34 | class Meta: 35 | model = ACIBridgeDomain 36 | fields: tuple = ( 37 | "id", 38 | "url", 39 | "display", 40 | "name", 41 | "name_alias", 42 | "description", 43 | "aci_tenant", 44 | "aci_vrf", 45 | "nb_tenant", 46 | "advertise_host_routes_enabled", 47 | "arp_flooding_enabled", 48 | "clear_remote_mac_enabled", 49 | "dhcp_labels", 50 | "ep_move_detection_enabled", 51 | "igmp_interface_policy_name", 52 | "igmp_snooping_policy_name", 53 | "ip_data_plane_learning_enabled", 54 | "limit_ip_learn_enabled", 55 | "mac_address", 56 | "multi_destination_flooding", 57 | "pim_ipv4_enabled", 58 | "pim_ipv4_destination_filter", 59 | "pim_ipv4_source_filter", 60 | "pim_ipv6_enabled", 61 | "unicast_routing_enabled", 62 | "unknown_ipv4_multicast", 63 | "unknown_ipv6_multicast", 64 | "unknown_unicast", 65 | "virtual_mac_address", 66 | "comments", 67 | "tags", 68 | "custom_fields", 69 | "created", 70 | "last_updated", 71 | ) 72 | brief_fields: tuple = ( 73 | "id", 74 | "url", 75 | "display", 76 | "name", 77 | "name_alias", 78 | "description", 79 | "aci_tenant", 80 | "aci_vrf", 81 | "nb_tenant", 82 | ) 83 | 84 | 85 | class ACIBridgeDomainSubnetSerializer(NetBoxModelSerializer): 86 | """Serializer for the ACI Bridge Domain Subnet model.""" 87 | 88 | url = serializers.HyperlinkedIdentityField( 89 | view_name=("plugins-api:netbox_aci_plugin-api:acibridgedomainsubnet-detail") 90 | ) 91 | aci_bridge_domain = ACIBridgeDomainSerializer(nested=True, required=True) 92 | gateway_ip_address = IPAddressSerializer(nested=True, required=True) 93 | nb_tenant = TenantSerializer(nested=True, required=False, allow_null=True) 94 | 95 | class Meta: 96 | model = ACIBridgeDomainSubnet 97 | fields: tuple = ( 98 | "id", 99 | "url", 100 | "display", 101 | "name", 102 | "name_alias", 103 | "description", 104 | "aci_bridge_domain", 105 | "gateway_ip_address", 106 | "nb_tenant", 107 | "advertised_externally_enabled", 108 | "igmp_querier_enabled", 109 | "ip_data_plane_learning_enabled", 110 | "no_default_gateway", 111 | "nd_ra_enabled", 112 | "nd_ra_prefix_policy_name", 113 | "preferred_ip_address_enabled", 114 | "shared_enabled", 115 | "virtual_ip_enabled", 116 | "comments", 117 | "tags", 118 | "custom_fields", 119 | "created", 120 | "last_updated", 121 | ) 122 | brief_fields: tuple = ( 123 | "id", 124 | "url", 125 | "display", 126 | "name", 127 | "name_alias", 128 | "description", 129 | "gateway_ip_address", 130 | "aci_bridge_domain", 131 | "nb_tenant", 132 | ) 133 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/models/tenant/test_tenants.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from django.core.exceptions import ValidationError 6 | from django.db.utils import IntegrityError 7 | from django.test import TestCase 8 | from tenancy.models import Tenant 9 | 10 | from ....models.tenant.tenants import ACITenant 11 | 12 | 13 | class ACITenantTestCase(TestCase): 14 | """Test case for ACITenant model.""" 15 | 16 | @classmethod 17 | def setUpTestData(cls) -> None: 18 | """Set up test data for ACITenant model.""" 19 | cls.aci_tenant_name = "ACITestTenant" 20 | cls.aci_tenant_alias = "ACITestTenantAlias" 21 | cls.aci_tenant_description = "ACI Test Tenant for NetBox ACI Plugin" 22 | cls.aci_tenant_comments = """ 23 | ACI Tenant for NetBox ACI Plugin testing. 24 | """ 25 | cls.nb_tenant_name = "NetBoxTestTenant" 26 | 27 | # Create objects 28 | cls.nb_tenant = Tenant.objects.create(name=cls.nb_tenant_name) 29 | cls.aci_tenant = ACITenant.objects.create( 30 | name=cls.aci_tenant_name, 31 | name_alias=cls.aci_tenant_alias, 32 | description=cls.aci_tenant_description, 33 | comments=cls.aci_tenant_comments, 34 | nb_tenant=cls.nb_tenant, 35 | ) 36 | 37 | def test_aci_tenant_instance(self) -> None: 38 | """Test type of created ACI Tenant.""" 39 | self.assertTrue(isinstance(self.aci_tenant, ACITenant)) 40 | 41 | def test_aci_tenant_str_return_value(self) -> None: 42 | """Test string value of created ACI Tenant.""" 43 | self.assertEqual(self.aci_tenant.__str__(), self.aci_tenant.name) 44 | 45 | def test_aci_tenant_name_alias(self) -> None: 46 | """Test alias of ACI Tenant.""" 47 | self.assertEqual(self.aci_tenant.name_alias, self.aci_tenant_alias) 48 | 49 | def test_aci_tenant_description(self) -> None: 50 | """Test description of ACI Tenant.""" 51 | self.assertEqual(self.aci_tenant.description, self.aci_tenant_description) 52 | 53 | def test_aci_tenant_nb_tenant_instance(self) -> None: 54 | """Test the NetBox tenant associated with ACI Tenant.""" 55 | self.assertTrue(isinstance(self.aci_tenant.nb_tenant, Tenant)) 56 | 57 | def test_aci_tenant_nb_tenant_name(self) -> None: 58 | """Test the NetBox tenant name associated with ACI Tenant.""" 59 | self.assertEqual(self.aci_tenant.nb_tenant.name, self.nb_tenant_name) 60 | 61 | def test_invalid_aci_tenant_name(self) -> None: 62 | """Test validation of ACI Tenant naming.""" 63 | tenant = ACITenant(name="ACI Test Tenant 1") 64 | with self.assertRaises(ValidationError): 65 | tenant.full_clean() 66 | 67 | def test_invalid_aci_tenant_name_length(self) -> None: 68 | """Test validation of ACI Tenant name length.""" 69 | tenant = ACITenant( 70 | name="T" * 65, # Exceeding the maximum length of 64 71 | ) 72 | with self.assertRaises(ValidationError): 73 | tenant.full_clean() 74 | 75 | def test_invalid_aci_tenant_name_alias(self) -> None: 76 | """Test validation of ACI Tenant alias.""" 77 | tenant = ACITenant(name="ACITestTenant1", name_alias="Invalid Alias") 78 | with self.assertRaises(ValidationError): 79 | tenant.full_clean() 80 | 81 | def test_invalid_aci_tenant_name_alias_length(self) -> None: 82 | """Test validation of ACI Tenant name alias length.""" 83 | tenant = ACITenant( 84 | name="ACITestTenant1", 85 | name_alias="T" * 65, # Exceeding the maximum length of 64 86 | ) 87 | with self.assertRaises(ValidationError): 88 | tenant.full_clean() 89 | 90 | def test_invalid_aci_tenant_description(self) -> None: 91 | """Test validation of ACI Tenant description.""" 92 | tenant = ACITenant(name="ACITestTenant1", description="Invalid Description: ö") 93 | with self.assertRaises(ValidationError): 94 | tenant.full_clean() 95 | 96 | def test_invalid_aci_tenant_description_length(self) -> None: 97 | """Test validation of ACI Tenant description length.""" 98 | tenant = ACITenant( 99 | name="ACITestTenant1", 100 | description="T" * 129, # Exceeding the maximum length of 128 101 | ) 102 | with self.assertRaises(ValidationError): 103 | tenant.full_clean() 104 | 105 | def test_constraint_unique_aci_tenant_name(self) -> None: 106 | """Test unique constraint of ACI Tenant name.""" 107 | duplicate_tenant = ACITenant(name=self.aci_tenant_name) 108 | with self.assertRaises(IntegrityError): 109 | duplicate_tenant.save() 110 | -------------------------------------------------------------------------------- /netbox_aci_plugin/templates/netbox_aci_plugin/acibridgedomainsubnet.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'generic/object.html' %} 3 | {% load render_table from django_tables2 %} 4 | {% load i18n %} 5 | 6 | {% block breadcrumbs %} 7 | {{ block.super }} 8 | 9 | 10 | 11 | {% endblock breadcrumbs %} 12 | 13 | {% block content %} 14 |
15 |
16 |
17 |

{% trans "ACI Bridge Domain Subnet" %}

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
{% trans "ACI Tenant" %}{{ object.aci_tenant|linkify|placeholder }}
{% trans "ACI VRF" %}{{ object.aci_vrf|linkify|placeholder }}
{% trans "ACI Bridge Domain" %}{{ object.aci_bridge_domain|linkify|placeholder }}
{% trans "Name Alias" %}{{ object.name_alias|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "NetBox Tenant" %} 42 | {% if object.nb_tenant.group %} 43 | {{ object.nb_tenant.group|linkify }} / 44 | {% endif %} 45 | {{ object.nb_tenant|linkify|placeholder }} 46 |
{% trans "Preferred IP address enabled" %}{% checkmark object.preferred_ip_address_enabled %}
{% trans "Virtual IP enabled" %}{% checkmark object.virtual_ip_enabled %}
57 |
58 |
59 |

{% trans "Scope Settings" %}

60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
{% trans "Advertised externally enabled" %}{% checkmark object.advertised_externally_enabled %}
{% trans "Shared enabled" %}{% checkmark object.shared_enabled %}
70 |
71 |
72 |

{% trans "Subnet Control Settings" %}

73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
{% trans "IGMP Querier enabled" %}{% checkmark object.igmp_querier_enabled %}
{% trans "No Default SVI Gateway" %}{% checkmark object.no_default_gateway %}
83 |
84 |
85 |

{% trans "Endpoint Learning Settings" %}

86 | 87 | 88 | 89 | 90 | 91 |
{% trans "IP Data Plane Learning enabled" %}{% checkmark object.ip_data_plane_learning_enabled %}
92 |
93 |
94 |

{% trans "IPv6 Settings" %}

95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
{% trans "ND RA enabled" %}{% checkmark object.nd_ra_enabled %}
{% trans "ND RA Prefix Policy" %}{{ object.nd_ra_prefix_policy_name|placeholder }}
105 |
106 | {% include 'inc/panels/custom_fields.html' %} 107 |
108 |
109 | {% include 'inc/panels/tags.html' %} 110 | {% include 'inc/panels/comments.html' %} 111 |
112 |
113 | {% endblock content %} 114 | -------------------------------------------------------------------------------- /netbox_aci_plugin/tests/api/tenant/test_vrfs.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Martin Hauser 2 | # 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | from ipam.models import VRF 6 | from tenancy.models import Tenant 7 | from utilities.testing import APIViewTestCases 8 | 9 | from ....api.urls import app_name 10 | from ....models.tenant.tenants import ACITenant 11 | from ....models.tenant.vrfs import ACIVRF 12 | 13 | 14 | class ACIVRFAPIViewTestCase(APIViewTestCases.APIViewTestCase): 15 | """API view test case for ACI VRF.""" 16 | 17 | model = ACIVRF 18 | view_namespace: str = f"plugins-api:{app_name}" 19 | brief_fields: list[str] = [ 20 | "aci_tenant", 21 | "description", 22 | "display", 23 | "id", 24 | "name", 25 | "name_alias", 26 | "nb_tenant", 27 | "nb_vrf", 28 | "url", 29 | ] 30 | user_permissions = ("netbox_aci_plugin.view_acitenant",) 31 | 32 | @classmethod 33 | def setUpTestData(cls) -> None: 34 | """Set up ACI VRF for API view testing.""" 35 | nb_tenant1 = Tenant.objects.create( 36 | name="NetBox Tenant API 1", slug="netbox-tenant-api-1" 37 | ) 38 | nb_tenant2 = Tenant.objects.create( 39 | name="NetBox Tenant API 2", slug="netbox-tenant-api-2" 40 | ) 41 | nb_vrf1 = VRF.objects.create(name="NetBox-VRF-API-1", tenant=nb_tenant1) 42 | nb_vrf2 = VRF.objects.create(name="NetBox-VRF-API-2", tenant=nb_tenant2) 43 | aci_tenant1 = ACITenant.objects.create(name="ACITestTenantAPI5") 44 | aci_tenant2 = ACITenant.objects.create(name="ACITestTenantAPI6") 45 | 46 | aci_vrfs: tuple = ( 47 | ACIVRF( 48 | name="ACIVRFTestAPI1", 49 | name_alias="Testing", 50 | description="First ACI Test", 51 | comments="# ACI Test 1", 52 | aci_tenant=aci_tenant1, 53 | nb_tenant=nb_tenant1, 54 | nb_vrf=nb_vrf1, 55 | bd_enforcement_enabled=False, 56 | dns_labels=["DNS1", "DNS2"], 57 | ip_data_plane_learning_enabled=True, 58 | pc_enforcement_direction="ingress", 59 | pc_enforcement_preference="unenforced", 60 | pim_ipv4_enabled=False, 61 | pim_ipv6_enabled=False, 62 | preferred_group_enabled=False, 63 | ), 64 | ACIVRF( 65 | name="ACIVRFTestAPI2", 66 | name_alias="Testing", 67 | description="Second ACI Test", 68 | comments="# ACI Test 2", 69 | aci_tenant=aci_tenant2, 70 | nb_tenant=nb_tenant1, 71 | nb_vrf=nb_vrf2, 72 | ), 73 | ACIVRF( 74 | name="ACIVRFTestAPI3", 75 | name_alias="Testing", 76 | description="Third ACI Test", 77 | comments="# ACI Test 3", 78 | aci_tenant=aci_tenant1, 79 | nb_tenant=nb_tenant2, 80 | bd_enforcement_enabled=True, 81 | ip_data_plane_learning_enabled=False, 82 | pc_enforcement_direction="egress", 83 | pc_enforcement_preference="enforced", 84 | pim_ipv4_enabled=True, 85 | pim_ipv6_enabled=True, 86 | preferred_group_enabled=True, 87 | ), 88 | ) 89 | ACIVRF.objects.bulk_create(aci_vrfs) 90 | 91 | cls.create_data: list[dict] = [ 92 | { 93 | "name": "ACIVRFTestAPI4", 94 | "name_alias": "Testing", 95 | "description": "Forth ACI Test", 96 | "comments": "# ACI Test 4", 97 | "aci_tenant": aci_tenant2.id, 98 | "nb_tenant": nb_tenant1.id, 99 | "nb_vrf": nb_vrf2.id, 100 | "bd_enforcement_enabled": False, 101 | "ip_data_plane_learning_enabled": True, 102 | "pc_enforcement_direction": "ingress", 103 | "pc_enforcement_preference": "unenforced", 104 | "pim_ipv4_enabled": True, 105 | "preferred_group_enabled": False, 106 | }, 107 | { 108 | "name": "ACIVRFTestAPI5", 109 | "name_alias": "Testing", 110 | "description": "Fifth ACI Test", 111 | "comments": "# ACI Test 5", 112 | "aci_tenant": aci_tenant1.id, 113 | "nb_tenant": nb_tenant2.id, 114 | "nb_vrf": nb_vrf1.id, 115 | "ip_data_plane_learning_enabled": False, 116 | "pc_enforcement_direction": "egress", 117 | "pc_enforcement_preference": "enforced", 118 | "pim_ipv6_enabled": True, 119 | "preferred_group_enabled": False, 120 | }, 121 | ] 122 | cls.bulk_update_data = { 123 | "description": "New description", 124 | } 125 | --------------------------------------------------------------------------------