├── 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 |
11 |
12 |
13 | | {% trans "Name Alias" %} |
14 | {{ object.name_alias|placeholder }} |
15 |
16 |
17 | | {% trans "Description" %} |
18 | {{ object.description|placeholder }} |
19 |
20 |
21 | | {% trans "NetBox Tenant" %} |
22 |
23 | {% if object.nb_tenant.group %}
24 | {{ object.nb_tenant.group|linkify }} /
25 | {% endif %}
26 | {{ object.nb_tenant|linkify|placeholder }}
27 | |
28 |
29 |
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 | {{ object.aci_tenant }}
9 | {% endblock breadcrumbs %}
10 |
11 | {% block content %}
12 |
13 |
14 |
15 |
16 |
17 |
18 | | {% trans "ACI Tenant" %} |
19 | {{ object.aci_tenant|linkify|placeholder }} |
20 |
21 |
22 | | {% trans "Name Alias" %} |
23 | {{ object.name_alias|placeholder }} |
24 |
25 |
26 | | {% trans "Description" %} |
27 | {{ object.description|placeholder }} |
28 |
29 |
30 | | {% trans "NetBox Tenant" %} |
31 |
32 | {% if object.nb_tenant.group %}
33 | {{ object.nb_tenant.group|linkify }} /
34 | {% endif %}
35 | {{ object.nb_tenant|linkify|placeholder }}
36 | |
37 |
38 |
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 | {{ object.aci_contract.aci_tenant }}
10 | {{ object.aci_contract }}
11 | {% endblock breadcrumbs %}
12 |
13 | {% block content %}
14 |
15 |
16 |
17 |
18 |
19 |
20 | | {% trans "ACI Tenant" %} |
21 | {{ object.aci_contract.aci_tenant|linkify|placeholder }} |
22 |
23 |
24 | | {% trans "ACI Contract" %} |
25 | {{ object.aci_contract|linkify|placeholder }} |
26 |
27 |
28 | | {% trans "ACI Object Type" %} |
29 | {{ object.aci_object_type.name|placeholder }} |
30 |
31 |
32 | | {% trans "ACI Object" %} |
33 | {{ object.aci_object|linkify|placeholder }} |
34 |
35 |
36 | | {% trans "Role" %} |
37 | {% badge object.get_role_display bg_color=object.get_role_color %} |
38 |
39 |
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 | {{ object.aci_tenant }}
10 | {% endblock breadcrumbs %}
11 |
12 | {% block content %}
13 |
14 |
15 |
16 |
17 |
18 |
19 | | {% trans "ACI Tenant" %} |
20 | {{ object.aci_tenant|linkify|placeholder }} |
21 |
22 |
23 | | {% trans "Name Alias" %} |
24 | {{ object.name_alias|placeholder }} |
25 |
26 |
27 | | {% trans "Description" %} |
28 | {{ object.description|placeholder }} |
29 |
30 |
31 | | {% trans "NetBox Tenant" %} |
32 |
33 | {% if object.nb_tenant.group %}
34 | {{ object.nb_tenant.group|linkify }} /
35 | {% endif %}
36 | {{ object.nb_tenant|linkify|placeholder }}
37 | |
38 |
39 |
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 |
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 | {{ object.aci_tenant }}
9 | {{ object.aci_app_profile }}
10 | {{ object.aci_vrf }}
11 | {% endblock breadcrumbs %}
12 |
13 | {% block content %}
14 |
15 |
16 |
17 |
18 |
19 |
20 | | {% trans "ACI Tenant" %} |
21 | {{ object.aci_tenant|linkify|placeholder }} |
22 |
23 |
24 | | {% trans "ACI Application Profile" %} |
25 | {{ object.aci_app_profile|linkify|placeholder }} |
26 |
27 |
28 | | {% trans "ACI VRF" %} |
29 | {{ object.aci_vrf|linkify|placeholder }} |
30 |
31 |
32 | | {% trans "Name Alias" %} |
33 | {{ object.name_alias|placeholder }} |
34 |
35 |
36 | | {% trans "Description" %} |
37 | {{ object.description|placeholder }} |
38 |
39 |
40 | | {% trans "NetBox Tenant" %} |
41 |
42 | {% if object.nb_tenant.group %}
43 | {{ object.nb_tenant.group|linkify }} /
44 | {% endif %}
45 | {{ object.nb_tenant|linkify|placeholder }}
46 | |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | | {% trans "Preferred Group Member enabled" %} |
55 | {% checkmark object.preferred_group_member_enabled %} |
56 |
57 |
58 | | {% trans "Intra-ESG Isolation enabled" %} |
59 | {% checkmark object.intra_esg_isolation_enabled %} |
60 |
61 |
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 | {{ object.aci_tenant }}
9 | {{ object.aci_app_profile }}
10 | {{ object.aci_endpoint_security_group }}
11 | {% endblock breadcrumbs %}
12 |
13 | {% block content %}
14 |
15 |
16 |
17 |
18 |
19 |
20 | | {% trans "ACI Tenant" %} |
21 | {{ object.aci_tenant|linkify|placeholder }} |
22 |
23 |
24 | | {% trans "ACI Application Profile" %} |
25 | {{ object.aci_app_profile|linkify|placeholder }} |
26 |
27 |
28 | | {% trans "ACI Endpoint Security Group" %} |
29 | {{ object.aci_endpoint_security_group|linkify|placeholder }} |
30 |
31 |
32 | | {% trans "Name Alias" %} |
33 | {{ object.name_alias|placeholder }} |
34 |
35 |
36 | | {% trans "Description" %} |
37 | {{ object.description|placeholder }} |
38 |
39 |
40 | | {% trans "NetBox Tenant" %} |
41 |
42 | {% if object.nb_tenant.group %}
43 | {{ object.nb_tenant.group|linkify }} /
44 | {% endif %}
45 | {{ object.nb_tenant|linkify|placeholder }}
46 | |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | | {% trans "Endpoint Object Type" %} |
55 | {{ object.ep_object_type.name|placeholder }} |
56 |
57 |
58 | | {% trans "Endpoint Object" %} |
59 | {{ object.ep_object|linkify|placeholder }} |
60 |
61 |
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 | {{ object.aci_tenant }}
9 | {{ object.aci_app_profile }}
10 | {{ object.aci_endpoint_security_group }}
11 | {% endblock breadcrumbs %}
12 |
13 | {% block content %}
14 |
15 |
16 |
17 |
18 |
19 |
20 | | {% trans "ACI Tenant" %} |
21 | {{ object.aci_tenant|linkify|placeholder }} |
22 |
23 |
24 | | {% trans "ACI Application Profile" %} |
25 | {{ object.aci_app_profile|linkify|placeholder }} |
26 |
27 |
28 | | {% trans "ACI Endpoint Security Group" %} |
29 | {{ object.aci_endpoint_security_group|linkify|placeholder }} |
30 |
31 |
32 | | {% trans "Name Alias" %} |
33 | {{ object.name_alias|placeholder }} |
34 |
35 |
36 | | {% trans "Description" %} |
37 | {{ object.description|placeholder }} |
38 |
39 |
40 | | {% trans "NetBox Tenant" %} |
41 |
42 | {% if object.nb_tenant.group %}
43 | {{ object.nb_tenant.group|linkify }} /
44 | {% endif %}
45 | {{ object.nb_tenant|linkify|placeholder }}
46 | |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | | {% trans "ACI Tenant" %} |
55 | {{ object.aci_epg_object.aci_tenant|linkify|placeholder }} |
56 |
57 |
58 | | {% trans "ACI Application Profile" %} |
59 | {{ object.aci_epg_object.aci_app_profile|linkify|placeholder }} |
60 |
61 |
62 | | {{ object.aci_epg_object_type.name|placeholder }} |
63 | {{ object.aci_epg_object|linkify|placeholder }} |
64 |
65 |
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 | {{ object.aci_tenant }}
10 | {% endblock breadcrumbs %}
11 |
12 | {% block content %}
13 |
14 |
15 |
16 |
17 |
18 |
19 | | {% trans "ACI Tenant" %} |
20 | {{ object.aci_tenant|linkify|placeholder }} |
21 |
22 |
23 | | {% trans "Name Alias" %} |
24 | {{ object.name_alias|placeholder }} |
25 |
26 |
27 | | {% trans "Description" %} |
28 | {{ object.description|placeholder }} |
29 |
30 |
31 | | {% trans "NetBox Tenant" %} |
32 |
33 | {% if object.nb_tenant.group %}
34 | {{ object.nb_tenant.group|linkify }} /
35 | {% endif %}
36 | {{ object.nb_tenant|linkify|placeholder }}
37 | |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | | {% trans "Scope" %} |
46 | {% badge object.get_scope_display bg_color=object.get_scope_color %} |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | | {% trans "QoS Class" %} |
55 | {% badge object.get_qos_class_display bg_color=object.get_qos_class_color %} |
56 |
57 |
58 | | {% trans "Target DSCP" %} |
59 | {% badge object.get_target_dscp_display %} |
60 |
61 |
62 |
63 | {% include 'inc/panels/custom_fields.html' %}
64 |
65 |
66 |
67 |
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 | {{ object.aci_tenant }}
9 | {{ object.aci_app_profile }}
10 | {{ object.aci_useg_endpoint_group }}
11 | {% endblock breadcrumbs %}
12 |
13 | {% block content %}
14 |
15 |
16 |
17 |
18 |
19 |
20 | | {% trans "ACI Tenant" %} |
21 | {{ object.aci_tenant|linkify|placeholder }} |
22 |
23 |
24 | | {% trans "ACI Application Profile" %} |
25 | {{ object.aci_app_profile|linkify|placeholder }} |
26 |
27 |
28 | | {% trans "ACI uSeg Endpoint Group" %} |
29 | {{ object.aci_useg_endpoint_group|linkify|placeholder }} |
30 |
31 |
32 | | {% trans "Name Alias" %} |
33 | {{ object.name_alias|placeholder }} |
34 |
35 |
36 | | {% trans "Description" %} |
37 | {{ object.description|placeholder }} |
38 |
39 |
40 | | {% trans "Type" %} |
41 | {% badge object.get_type_display bg_color=object.get_type_color %} |
42 |
43 |
44 | | {% trans "NetBox Tenant" %} |
45 |
46 | {% if object.nb_tenant.group %}
47 | {{ object.nb_tenant.group|linkify }} /
48 | {% endif %}
49 | {{ object.nb_tenant|linkify|placeholder }}
50 | |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | | {% trans "Use EPG Subnet" %} |
59 | {% checkmark object.use_epg_subnet %} |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | | {% trans "Attribute Object Type" %} |
68 | {{ object.attr_object_type.name|placeholder }} |
69 |
70 |
71 | | {% trans "Attribute Object" %} |
72 | {{ object.attr_object|linkify|placeholder }} |
73 |
74 |
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 | {{ object.aci_contract_subject.aci_contract.aci_tenant }}
10 | {{ object.aci_contract_subject.aci_contract }}
11 | {{ object.aci_contract_subject }}
12 | {% endblock breadcrumbs %}
13 |
14 | {% block content %}
15 |
16 |
17 |
18 |
19 |
20 |
21 | | {% trans "ACI Tenant" %} |
22 | {{ object.aci_contract_subject.aci_contract.aci_tenant|linkify|placeholder }} |
23 |
24 |
25 | | {% trans "ACI Contract" %} |
26 | {{ object.aci_contract_subject.aci_contract|linkify|placeholder }} |
27 |
28 |
29 | | {% trans "ACI Contract Subject" %} |
30 | {{ object.aci_contract_subject|linkify|placeholder }} |
31 |
32 |
33 | | {% trans "ACI Contract Filter" %} |
34 | {{ object.aci_contract_filter|linkify|placeholder }} |
35 |
36 |
37 | | {% trans "Action" %} |
38 | {% badge object.get_action_display bg_color=object.get_action_color %} |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | | {% trans "Apply Direction" %} |
47 | {% badge object.get_apply_direction_display bg_color=object.get_apply_direction_color %} |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | | {% trans "Logging enabled" %} |
56 | {% checkmark object.log_enabled %} |
57 |
58 |
59 | | {% trans "Policy Compression enabled" %} |
60 | {% checkmark object.policy_compression_enabled %} |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | | {% trans "(Deny) Priority" %} |
69 | {% badge object.get_priority_display bg_color=object.get_priority_color %} |
70 |
71 |
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 | {{ object.aci_tenant }}
9 | {{ object.aci_app_profile }}
10 | {{ object.aci_bridge_domain }}
11 | {% endblock breadcrumbs %}
12 |
13 | {% block content %}
14 |
15 |
16 |
17 |
18 |
19 |
20 | | {% trans "ACI Tenant" %} |
21 | {{ object.aci_tenant|linkify|placeholder }} |
22 |
23 |
24 | | {% trans "ACI Application Profile" %} |
25 | {{ object.aci_app_profile|linkify|placeholder }} |
26 |
27 |
28 | | {% trans "ACI VRF" %} |
29 | {{ object.aci_vrf|linkify|placeholder }} |
30 |
31 |
32 | | {% trans "ACI Bridge Domain" %} |
33 | {{ object.aci_bridge_domain|linkify|placeholder }} |
34 |
35 |
36 | | {% trans "Name Alias" %} |
37 | {{ object.name_alias|placeholder }} |
38 |
39 |
40 | | {% trans "Description" %} |
41 | {{ object.description|placeholder }} |
42 |
43 |
44 | | {% trans "NetBox Tenant" %} |
45 |
46 | {% if object.nb_tenant.group %}
47 | {{ object.nb_tenant.group|linkify }} /
48 | {% endif %}
49 | {{ object.nb_tenant|linkify|placeholder }}
50 | |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | | {% trans "Preferred Group Member enabled" %} |
59 | {% checkmark object.preferred_group_member_enabled %} |
60 |
61 |
62 | | {% trans "Intra-EPG Isolation enabled" %} |
63 | {% checkmark object.intra_epg_isolation_enabled %} |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | | {% trans "Flood in Encapsulation enabled" %} |
72 | {% checkmark object.flood_in_encap_enabled %} |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | | {% trans "QoS Class" %} |
81 | {% badge object.get_qos_class_display bg_color=object.get_qos_class_color %} |
82 |
83 |
84 | | {% trans "Custom QoS Policy" %} |
85 | {{ object.custom_qos_policy_name|placeholder }} |
86 |
87 |
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 | {{ object.aci_tenant }}
9 | {% endblock breadcrumbs %}
10 |
11 | {% block content %}
12 |
13 |
14 |
15 |
16 |
17 |
18 | | {% trans "ACI Tenant" %} |
19 | {{ object.aci_tenant|linkify|placeholder }} |
20 |
21 |
22 | | {% trans "Name Alias" %} |
23 | {{ object.name_alias|placeholder }} |
24 |
25 |
26 | | {% trans "Description" %} |
27 | {{ object.description|placeholder }} |
28 |
29 |
30 | | {% trans "NetBox Tenant" %} |
31 |
32 | {% if object.nb_tenant.group %}
33 | {{ object.nb_tenant.group|linkify }} /
34 | {% endif %}
35 | {{ object.nb_tenant|linkify|placeholder }}
36 | |
37 |
38 |
39 | | {% trans "NetBox VRF" %} |
40 |
41 | {{ object.nb_vrf|linkify|placeholder }}
42 | |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | | {% trans "Policy Control Enforcement Direction" %} |
51 | {% badge object.get_pc_enforcement_direction_display bg_color=object.get_pc_enforcement_direction_color %} |
52 |
53 |
54 | | {% trans "Policy Control Enforcement Preference" %} |
55 | {% badge object.get_pc_enforcement_preference_display bg_color=object.get_pc_enforcement_preference_color %} |
56 |
57 |
58 | | {% trans "Bridge Domain Enforcement" %} |
59 | {% checkmark object.bd_enforcement_enabled %} |
60 |
61 |
62 | | {% trans "Preferred Group" %} |
63 | {% checkmark object.preferred_group_enabled %} |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | | {% trans "IP Data Plane Learning" %} |
72 | {% checkmark object.ip_data_plane_learning_enabled %} |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | | {% trans "PIM (Multicast) IPv4" %} |
81 | {% checkmark object.pim_ipv4_enabled %} |
82 |
83 |
84 | | {% trans "PIM (Multicast) IPv6" %} |
85 | {% checkmark object.pim_ipv6_enabled %} |
86 |
87 |
88 |
89 | {% include 'inc/panels/custom_fields.html' %}
90 |
91 |
92 |
93 |
94 |
95 |
96 | | {% trans "DNS Labels" %} |
97 | {{ object.dns_labels|join:", "|placeholder }} |
98 |
99 |
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 | {{ object.aci_tenant }}
9 | {{ object.aci_app_profile }}
10 | {{ object.aci_bridge_domain }}
11 | {% endblock breadcrumbs %}
12 |
13 | {% block content %}
14 |
15 |
16 |
17 |
18 |
19 |
20 | | {% trans "ACI Tenant" %} |
21 | {{ object.aci_tenant|linkify|placeholder }} |
22 |
23 |
24 | | {% trans "ACI Application Profile" %} |
25 | {{ object.aci_app_profile|linkify|placeholder }} |
26 |
27 |
28 | | {% trans "ACI VRF" %} |
29 | {{ object.aci_vrf|linkify|placeholder }} |
30 |
31 |
32 | | {% trans "ACI Bridge Domain" %} |
33 | {{ object.aci_bridge_domain|linkify|placeholder }} |
34 |
35 |
36 | | {% trans "Name Alias" %} |
37 | {{ object.name_alias|placeholder }} |
38 |
39 |
40 | | {% trans "Description" %} |
41 | {{ object.description|placeholder }} |
42 |
43 |
44 | | {% trans "NetBox Tenant" %} |
45 |
46 | {% if object.nb_tenant.group %}
47 | {{ object.nb_tenant.group|linkify }} /
48 | {% endif %}
49 | {{ object.nb_tenant|linkify|placeholder }}
50 | |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | | {% trans "Preferred Group Member enabled" %} |
59 | {% checkmark object.preferred_group_member_enabled %} |
60 |
61 |
62 | | {% trans "Intra-EPG Isolation enabled" %} |
63 | {% checkmark object.intra_epg_isolation_enabled %} |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | | {% trans "Flood in Encapsulation enabled" %} |
72 | {% checkmark object.flood_in_encap_enabled %} |
73 |
74 |
75 | | {% trans "Proxy ARP enabled" %} |
76 | {% checkmark object.proxy_arp_enabled %} |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | | {% trans "QoS Class" %} |
85 | {% badge object.get_qos_class_display bg_color=object.get_qos_class_color %} |
86 |
87 |
88 | | {% trans "Custom QoS Policy" %} |
89 | {{ object.custom_qos_policy_name|placeholder }} |
90 |
91 |
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 | {{ object.aci_tenant }}
9 | {{ object.aci_vrf }}
10 | {{ object.aci_bridge_domain }}
11 | {% endblock breadcrumbs %}
12 |
13 | {% block content %}
14 |
15 |
16 |
17 |
18 |
19 |
20 | | {% trans "ACI Tenant" %} |
21 | {{ object.aci_tenant|linkify|placeholder }} |
22 |
23 |
24 | | {% trans "ACI VRF" %} |
25 | {{ object.aci_vrf|linkify|placeholder }} |
26 |
27 |
28 | | {% trans "ACI Bridge Domain" %} |
29 | {{ object.aci_bridge_domain|linkify|placeholder }} |
30 |
31 |
32 | | {% trans "Name Alias" %} |
33 | {{ object.name_alias|placeholder }} |
34 |
35 |
36 | | {% trans "Description" %} |
37 | {{ object.description|placeholder }} |
38 |
39 |
40 | | {% trans "NetBox Tenant" %} |
41 |
42 | {% if object.nb_tenant.group %}
43 | {{ object.nb_tenant.group|linkify }} /
44 | {% endif %}
45 | {{ object.nb_tenant|linkify|placeholder }}
46 | |
47 |
48 |
49 | | {% trans "Preferred IP address enabled" %} |
50 | {% checkmark object.preferred_ip_address_enabled %} |
51 |
52 |
53 | | {% trans "Virtual IP enabled" %} |
54 | {% checkmark object.virtual_ip_enabled %} |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | | {% trans "Advertised externally enabled" %} |
63 | {% checkmark object.advertised_externally_enabled %} |
64 |
65 |
66 | | {% trans "Shared enabled" %} |
67 | {% checkmark object.shared_enabled %} |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | | {% trans "IGMP Querier enabled" %} |
76 | {% checkmark object.igmp_querier_enabled %} |
77 |
78 |
79 | | {% trans "No Default SVI Gateway" %} |
80 | {% checkmark object.no_default_gateway %} |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | | {% trans "IP Data Plane Learning enabled" %} |
89 | {% checkmark object.ip_data_plane_learning_enabled %} |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | | {% trans "ND RA enabled" %} |
98 | {% checkmark object.nd_ra_enabled %} |
99 |
100 |
101 | | {% trans "ND RA Prefix Policy" %} |
102 | {{ object.nd_ra_prefix_policy_name|placeholder }} |
103 |
104 |
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 |
--------------------------------------------------------------------------------