├── netbox_tunnels2 ├── api │ ├── __init__.py │ ├── urls.py │ ├── views.py │ └── serializers.py ├── migrations │ ├── __init__.py │ ├── 0003_tunnel_tenant.py │ ├── 0002_alter_tunnel_b_pub_address.py │ ├── 0005_alter_tunnel.py │ ├── 0004_alter_tunnel_side_a_assigned_object_type_and_more.py │ └── 0001_initial.py ├── tests │ ├── __init__.py │ ├── test_api.py │ ├── custom.py │ └── test_models.py ├── version.py ├── constants.py ├── search.py ├── admin.py ├── __init__.py ├── templates │ └── netbox_tunnels2 │ │ ├── interface_extend.html │ │ ├── tunneltype.html │ │ ├── tunnel.html │ │ ├── plugintunnel.html │ │ ├── tunnel_edit.html │ │ └── plugintunnel_edit.html ├── graphql.py ├── navigation.py ├── template_content.py ├── urls.py ├── filtersets.py ├── views.py ├── tables.py ├── models.py └── forms.py ├── docs ├── index.md ├── changelog.md ├── contributing.md ├── img │ ├── tunnel-info.png │ └── tunnel-list.png └── dev │ └── configuration.testing.py ├── .devcontainer ├── initializers │ ├── manufacturers.yml │ ├── device_types.yml │ ├── sites.yml │ ├── users.yml │ ├── device_roles.yml │ ├── devices.yml │ ├── interfaces.yml │ └── ip_addresses.yml ├── requirements-dev.txt ├── docker-compose.override.yml ├── configuration │ ├── plugins.py │ ├── logging.py │ └── configuration.py ├── entrypoint-dev.sh ├── Dockerfile-plugin_dev ├── docker-compose.yml ├── .bashrc ├── .zshrc └── devcontainer.json ├── requirements_dev.txt ├── TODO ├── MANIFEST.in ├── Makefile ├── development ├── configuration │ ├── plugins.py │ ├── logging.py │ └── configuration.py ├── Dockerfile └── docker-compose.yml ├── .github ├── workflows │ ├── mkdocs.yml │ ├── publish.yml │ └── test.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pyproject.toml ├── setup.cfg ├── CHANGELOG.md ├── README.md ├── setup.py ├── mkdocs.yml ├── .gitignore ├── CONTRIBUTING.md └── LICENSE /netbox_tunnels2/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_tunnels2/migrations/__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/contributing.md: -------------------------------------------------------------------------------- 1 | {% 2 | include-markdown "../CONTRIBUTING.md" 3 | %} 4 | -------------------------------------------------------------------------------- /netbox_tunnels2/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit test package for netbox_tunnels2.""" 2 | -------------------------------------------------------------------------------- /netbox_tunnels2/version.py: -------------------------------------------------------------------------------- 1 | __version__="0.2.9" 2 | __min_version__="3.6.0" 3 | __max_version__="3.7.99" -------------------------------------------------------------------------------- /docs/img/tunnel-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertlynch3/netbox-tunnels2/HEAD/docs/img/tunnel-info.png -------------------------------------------------------------------------------- /docs/img/tunnel-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertlynch3/netbox-tunnels2/HEAD/docs/img/tunnel-list.png -------------------------------------------------------------------------------- /.devcontainer/initializers/manufacturers.yml: -------------------------------------------------------------------------------- 1 | - name: Cisco 2 | slug: cisco 3 | - name: Juniper 4 | slug: juniper -------------------------------------------------------------------------------- /.devcontainer/initializers/device_types.yml: -------------------------------------------------------------------------------- 1 | - model: SRX4200 2 | manufacturer: Juniper 3 | slug: srx4200 4 | is_full_depth: True 5 | u_height: 1 -------------------------------------------------------------------------------- /.devcontainer/initializers/sites.yml: -------------------------------------------------------------------------------- 1 | - name: AK 2 | slug: dc1 3 | status: Active 4 | - name: WDC 5 | slug: dc2 6 | status: Active 7 | - name: SF 8 | slug: SF 9 | status: Active -------------------------------------------------------------------------------- /.devcontainer/initializers/users.yml: -------------------------------------------------------------------------------- 1 | admin: 2 | first_name: admin 3 | last_name: user 4 | is_active: True 5 | is_superuser: True 6 | is_staff: True 7 | password: admin -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip==22.3.1 2 | bump2version==0.5.11 3 | wheel==0.33.6 4 | watchdog==0.9.0 5 | flake8==6.0.0 6 | Sphinx==1.8.5 7 | twine==4.0.2 8 | black==22.12.0 9 | isort=5.11.4 10 | -------------------------------------------------------------------------------- /.devcontainer/initializers/device_roles.yml: -------------------------------------------------------------------------------- 1 | - name: switch 2 | slug: switch 3 | color: Grey 4 | - name: router 5 | slug: router 6 | color: Cyan 7 | - name: firewall 8 | slug: firewall 9 | color: Cyan -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - TODO: Logic to validate that the tunnel endpoints are in the same prefix or are /32s 2 | - TODO: Validate the Public IP addresses belong to the Devices 3 | - TODO: Validate the same inside interface is not used by multiple tunnels -------------------------------------------------------------------------------- /.devcontainer/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | autoflake 2 | autopep8 3 | bandit 4 | black 5 | flake8 6 | isort 7 | mypy 8 | pre-commit 9 | pycodestyle 10 | pydocstyle 11 | pylint 12 | pylint-django 13 | wily 14 | yapf 15 | sourcery-analytics 16 | -------------------------------------------------------------------------------- /netbox_tunnels2/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | Constants for filters 3 | """ 4 | from django.db.models import Q 5 | 6 | TUNNEL_INTERFACE_ASSIGNMENT_MODELS = Q( 7 | Q(app_label="dcim", model="interface"), 8 | Q(app_label="virtualization", model="vminterface") 9 | ) -------------------------------------------------------------------------------- /.devcontainer/initializers/devices.yml: -------------------------------------------------------------------------------- 1 | - name: fw1 2 | device_role: firewall 3 | device_type: SRX4200 4 | site: AK 5 | - name: fw2 6 | device_role: firewall 7 | device_type: SRX4200 8 | site: WDC 9 | - name: fw3 10 | device_role: firewall 11 | device_type: SRX4200 12 | site: SF -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTING.md 2 | include LICENSE 3 | include README.md 4 | 5 | recursive-include tests * 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.py[co] 8 | 9 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 10 | recursive-include netbox_tunnels2 *.html -------------------------------------------------------------------------------- /.devcontainer/docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3.4' 3 | services: 4 | netbox: 5 | build: 6 | dockerfile: Dockerfile-plugin_dev 7 | context: . 8 | ports: 9 | - "8000:8080" 10 | volumes: 11 | - ../:/opt/netbox/netbox/netbox-tunnels2 12 | - ~/.gitconfig:/home/vscode/.gitconfig:z,ro 13 | - ~/.ssh:/home/vscode/.ssh 14 | -------------------------------------------------------------------------------- /netbox_tunnels2/api/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Creates API endpoint URLs for the plugin. 3 | """ 4 | from netbox.api.routers import NetBoxRouter 5 | from . import views 6 | 7 | app_name='netbox_tunnels2' 8 | 9 | router = NetBoxRouter() 10 | router.register('tunnels', views.TunnelViewSet) 11 | router.register('tunnel-types', views.TunnelTypeViewSet) 12 | 13 | urlpatterns = router.urls -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | sources = NetBox Tunnels Plugin 2 | 3 | .PHONY: test format lint unittest pre-commit clean 4 | test: format lint unittest 5 | 6 | format: 7 | isort $(sources) tests 8 | black $(sources) tests 9 | 10 | lint: 11 | flake8 $(sources) tests 12 | 13 | pre-commit: 14 | pre-commit run --all-files 15 | 16 | clean: 17 | rm -rf *.egg-info 18 | rm -rf .tox dist site 19 | -------------------------------------------------------------------------------- /development/configuration/plugins.py: -------------------------------------------------------------------------------- 1 | # Add your plugins and plugin settings here. 2 | # Of course uncomment this file out. 3 | 4 | # To learn how to build images with your required plugins 5 | # See https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins 6 | 7 | PLUGINS = [ 8 | "netbox_tunnels2", 9 | "netbox_initializers" 10 | ] 11 | 12 | PLUGINS_CONFIG = { # type: ignore 13 | "netbox_initializers": {}, 14 | "netbox_tunnels2": {}, 15 | } -------------------------------------------------------------------------------- /development/configuration/logging.py: -------------------------------------------------------------------------------- 1 | # Remove first comment(#) on each line to implement this working logging example. 2 | # Add LOGLEVEL environment variable to netbox if you use this example & want a different log level. 3 | from os import environ 4 | 5 | # Set LOGLEVEL in netbox.env or docker-compose.overide.yml to override a logging level of INFO. 6 | LOGLEVEL = environ.get("LOGLEVEL", "INFO") 7 | 8 | LOGGING = { 9 | "version": 1, 10 | "disable_existing_loggers": True, 11 | } -------------------------------------------------------------------------------- /.devcontainer/configuration/plugins.py: -------------------------------------------------------------------------------- 1 | # Add your plugins and plugin settings here. 2 | # Of course uncomment this file out. 3 | 4 | # To learn how to build images with your required plugins 5 | # See https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins 6 | 7 | PLUGINS = [ 8 | "netbox_initializers", # Loads demo data 9 | "netbox_tunnels2", 10 | ] 11 | 12 | PLUGINS_CONFIG = { # type: ignore 13 | "netbox_initializers": {}, 14 | "netbox_tunnels2": {}, 15 | } 16 | -------------------------------------------------------------------------------- /netbox_tunnels2/search.py: -------------------------------------------------------------------------------- 1 | from netbox.search import SearchIndex, register_search 2 | from .models import PluginTunnel, TunnelType 3 | 4 | @register_search 5 | class TunnelTypeIndex(SearchIndex): 6 | model=TunnelType 7 | fields = ( 8 | ('name', 100), 9 | ) 10 | @register_search 11 | class TunnelIndex(SearchIndex): 12 | model=PluginTunnel 13 | fields = ( 14 | ('name', 100), 15 | ('a_pub_address', 150), 16 | ('b_pub_address', 150), 17 | ('comments', 5000), 18 | ) -------------------------------------------------------------------------------- /netbox_tunnels2/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import PluginTunnel, TunnelType 3 | 4 | 5 | @admin.register(PluginTunnel) 6 | class TunnelAdmin(admin.ModelAdmin): 7 | """Administrative view for managing Tunnels instances.""" 8 | 9 | list_display = ("id", "name", "status", "tunnel_type") 10 | 11 | 12 | @admin.register(TunnelType) 13 | class TunnelTypeAdmin(admin.ModelAdmin): 14 | """Administrative view for managing Tunnels to Device instances.""" 15 | 16 | list_display = ("id", "name") 17 | -------------------------------------------------------------------------------- /.github/workflows/mkdocs.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | permissions: 8 | contents: write 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: 3.x 17 | - run: pip install mkdocs-material mkdocs-autorefs mkdocs-material-extensions mkdocstrings mkdocstrings-python-legacy mkdocs-include-markdown-plugin 18 | - run: mkdocs gh-deploy --force 19 | -------------------------------------------------------------------------------- /netbox_tunnels2/tests/test_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests for `netbox_tunnels2` package.""" 4 | from django.urls import reverse 5 | from netbox_tunnels2.tests.custom import APITestCase 6 | 7 | 8 | class AppTest(APITestCase): 9 | """ 10 | Test the availability of the NetBox Tunnels API root 11 | """ 12 | 13 | def test_root(self): 14 | url = reverse("plugins-api:netbox_tunnels2-api:api-root") 15 | response = self.client.get(f"{url}?format=api", **self.header) 16 | 17 | self.assertEqual(response.status_code, 200) -------------------------------------------------------------------------------- /netbox_tunnels2/__init__.py: -------------------------------------------------------------------------------- 1 | from extras.plugins import PluginConfig 2 | from .version import __version__, __min_version__, __max_version__ 3 | 4 | 5 | class TunnelsConfig(PluginConfig): 6 | """This class defines attributes for the NetBox Tunnels Plugin.""" 7 | name = "netbox_tunnels2" 8 | verbose_name = "Network Tunnels" 9 | version = __version__ 10 | description = "Subsystem for tracking IP Tunnels" 11 | base_url = "tunnels" 12 | author = "Robert Lynch" 13 | author_email = "robertlynch3@users.noreply.github.com" 14 | min_version = __min_version__ 15 | max_version = __max_version__ 16 | 17 | 18 | config = TunnelsConfig 19 | -------------------------------------------------------------------------------- /netbox_tunnels2/migrations/0003_tunnel_tenant.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.9 on 2023-07-22 01:18 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tenancy', '0010_tenant_relax_uniqueness'), 11 | ('netbox_tunnels2', '0002_alter_tunnel_b_pub_address'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='tunnel', 17 | name='tenant', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, to='tenancy.tenant'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /development/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-bullseye 2 | 3 | ARG NETBOX_VERSION=develop 4 | 5 | ENV PYTHONUNBUFFERED 1 6 | ENV PYTHONDONTWRITEBYTECODE 1 7 | 8 | RUN apt-get update \ 9 | && apt-get install -y --no-install-recommends git postgresql-client \ 10 | && pip install --upgrade pip 11 | 12 | # Install NetBox 13 | RUN mkdir -p /opt/netbox \ 14 | && git clone --single-branch --branch=${NETBOX_VERSION} https://github.com/netbox-community/netbox.git /opt/netbox/ \ 15 | && pip install -r /opt/netbox/requirements.txt 16 | 17 | # Install Validity 18 | COPY . /plugin/netbox_tunnels2 19 | RUN pip install --editable /plugin/netbox_tunnels2[dev] 20 | 21 | WORKDIR /opt/netbox/netbox/ -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up Python 15 | uses: actions/setup-python@v3 16 | with: 17 | python-version: '3.x' 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install build 22 | - name: Build package 23 | run: python -m build 24 | - name: Publish package 25 | uses: pypa/gh-action-pypi-publish@release/v1 26 | with: 27 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /netbox_tunnels2/migrations/0002_alter_tunnel_b_pub_address.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.8 on 2023-04-20 17:32 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('ipam', '0064_clear_search_cache'), 11 | ('netbox_tunnels2', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='tunnel', 17 | name='b_pub_address', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tunnel_b_pub_address', to='ipam.ipaddress'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /netbox_tunnels2/migrations/0005_alter_tunnel.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-01-03 18:42 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import taggit.managers 6 | import utilities.json 7 | 8 | 9 | class Migration(migrations.Migration): 10 | dependencies = [ 11 | ("ipam", "0069_gfk_indexes"), 12 | ("tenancy", "0013_gfk_indexes"), 13 | ("contenttypes", "0002_remove_content_type_name"), 14 | ("extras", "0105_customfield_min_max_values"), 15 | ("netbox_tunnels2", "0004_alter_tunnel_side_a_assigned_object_type_and_more"), 16 | ] 17 | 18 | operations = [ 19 | migrations.RenameModel('Tunnel','PluginTunnel') 20 | ] 21 | -------------------------------------------------------------------------------- /netbox_tunnels2/templates/netbox_tunnels2/interface_extend.html: -------------------------------------------------------------------------------- 1 | {% if related_tunnels %} 2 | {% load render_table from django_tables2 %} 3 |
4 |
5 | Related VPN Tunnels 6 |
7 |
8 | {% render_table related_tunnels 'inc/table.html' %} 9 |
10 | 15 |
16 | {% endif %} -------------------------------------------------------------------------------- /.devcontainer/initializers/interfaces.yml: -------------------------------------------------------------------------------- 1 | - device: fw1 2 | enabled: true 3 | type: 10gbase-x-sfpp 4 | name: xe-0/0/0 5 | - device: fw1 6 | enabled: true 7 | type: virtual 8 | name: st0.0 9 | - device: fw1 10 | enabled: true 11 | type: virtual 12 | name: st0.1 13 | 14 | - device: fw2 15 | enabled: true 16 | type: 10gbase-x-sfpp 17 | name: xe-0/0/0 18 | - device: fw2 19 | enabled: true 20 | type: virtual 21 | name: st0.0 22 | - device: fw2 23 | enabled: true 24 | type: virtual 25 | name: st0.1 26 | 27 | - device: fw3 28 | enabled: true 29 | type: 10gbase-x-sfpp 30 | name: xe-0/0/0 31 | - device: fw3 32 | enabled: true 33 | type: virtual 34 | name: st0.0 35 | - device: fw3 36 | enabled: true 37 | type: virtual 38 | name: st0.1 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **NetBox version** 11 | What version of NetBox are you currently running? 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /netbox_tunnels2/api/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create views to handle the API logic. 3 | A view set is a single class that can handle the view, add, change, 4 | and delete operations which each require dedicated views under the UI. 5 | """ 6 | from netbox.api.viewsets import NetBoxModelViewSet 7 | 8 | from .. import filtersets, models 9 | from .serializers import TunnelSerializer, TunnelTypeSerializer 10 | 11 | 12 | 13 | class TunnelViewSet(NetBoxModelViewSet): 14 | queryset = models.PluginTunnel.objects.all() 15 | serializer_class = TunnelSerializer 16 | filterset_class = filtersets.TunnelFilterSet 17 | 18 | 19 | class TunnelTypeViewSet(NetBoxModelViewSet): 20 | queryset = models.TunnelType.objects.all() 21 | serializer_class = TunnelTypeSerializer 22 | filterset_class = filtersets.TunnelTypeFilterSet -------------------------------------------------------------------------------- /netbox_tunnels2/graphql.py: -------------------------------------------------------------------------------- 1 | from graphene import ObjectType 2 | from netbox.graphql.types import NetBoxObjectType 3 | from netbox.graphql.fields import ObjectField, ObjectListField 4 | from . import filtersets, models 5 | 6 | 7 | class TunnelType(NetBoxObjectType): 8 | class Meta: 9 | model=models.PluginTunnel 10 | fields = "__all__" 11 | filterset_class = filtersets.TunnelFilterSet 12 | class TunnelTypeType(NetBoxObjectType): 13 | class Meta: 14 | model=models.TunnelType 15 | fields = "__all__" 16 | 17 | 18 | class Query(ObjectType): 19 | tunnel = ObjectField(TunnelType) 20 | tunnel_list = ObjectListField(TunnelType) 21 | 22 | tunnel_type = ObjectField(TunnelTypeType) 23 | tunnel_type_list = ObjectListField(TunnelTypeType) 24 | 25 | 26 | schema = Query -------------------------------------------------------------------------------- /.devcontainer/initializers/ip_addresses.yml: -------------------------------------------------------------------------------- 1 | - address: 10.1.1.1/24 2 | device: fw1 3 | interface: xe-0/0/0 4 | status: active 5 | - address: 169.254.0.1/30 6 | device: fw1 7 | interface: st0.0 8 | status: active 9 | - address: 169.254.3.1/30 10 | device: fw1 11 | interface: st0.1 12 | status: active 13 | 14 | 15 | - address: 10.1.1.2/24 16 | device: fw2 17 | interface: xe-0/0/0 18 | status: active 19 | - address: 169.254.0.2/30 20 | device: fw2 21 | interface: st0.0 22 | status: active 23 | - address: 169.254.1.1/30 24 | device: fw2 25 | interface: st0.1 26 | status: active 27 | 28 | 29 | - address: 10.1.1.3/24 30 | device: fw3 31 | interface: xe-0/0/0 32 | status: active 33 | - address: 169.254.3.2/30 34 | device: fw3 35 | interface: st0.0 36 | status: active 37 | - address: 169.254.1.2/30 38 | device: fw3 39 | interface: st0.1 40 | status: active -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # pyproject.toml 2 | 3 | [build-system] 4 | requires = ["setuptools>=61.0.0", "wheel"] 5 | build-backend = "setuptools.build_meta" 6 | 7 | [project] 8 | name = "netbox-tunnels2" 9 | version = "0.2.9" 10 | description = "Provides the ability track IP Tunnels." 11 | readme = "README.md" 12 | authors = [{ name = "Robert Lynch", email = "robertlynch3@users.noreply.github.com" }] 13 | license = { file = "LICENSE" } 14 | 15 | [project.urls] 16 | Homepage = "https://github.com/robertlynch3/netbox-tunnels2" 17 | 18 | [project.optional-dependencies] 19 | dev = ["black", "bumpver", "isort", "pip-tools", "pytest"] 20 | 21 | [tool.bumpver] 22 | current_version = "0.2.9" 23 | version_pattern = "MAJOR.MINOR.PATCH" 24 | commit_message = "bump version {old_version} -> {new_version}" 25 | commit = false 26 | 27 | [tool.bumpver.file_patterns] 28 | "pyproject.toml" = [ 29 | 'current_version = "{version}"$', 30 | 'version = "{version}"$', 31 | ] 32 | "netbox_tunnels2/version.py" = [ 33 | '__version__="{version}"$', 34 | ] -------------------------------------------------------------------------------- /docs/dev/configuration.testing.py: -------------------------------------------------------------------------------- 1 | ################################################################### 2 | # This file serves as a base configuration for testing purposes # 3 | # 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_tunnels2", 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 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | max-complexity = 18 4 | ignore = E203, E266, W503 5 | per-file-ignores = __init__.py:F401 6 | exclude = .git, 7 | __pycache__, 8 | setup.py, 9 | build, 10 | dist, 11 | docs, 12 | releases, 13 | .venv, 14 | .tox, 15 | .mypy_cache, 16 | .pytest_cache, 17 | .vscode, 18 | .github, 19 | # By default test codes will be linted. 20 | # tests 21 | 22 | [tox:tox] 23 | isolated_build = true 24 | envlist = py38, py39, py310, format, lint, build 25 | 26 | [gh-actions] 27 | python = 28 | 3.10: py310 29 | 3.9: py39 30 | 3.8: py38, format, lint, build 31 | 32 | [bumpversion] 33 | current_version = 0.2.3 34 | commit = True 35 | tag = True 36 | 37 | [bumpversion:file:setup.py] 38 | search = version='{current_version}' 39 | replace = version='{new_version}' 40 | 41 | [bumpversion:file:netbox_tunnels_plugin/__init__.py] 42 | search = __version__ = '{current_version}' 43 | replace = __version__ = '{new_version}' 44 | 45 | [bdist_wheel] 46 | universal = 1 47 | -------------------------------------------------------------------------------- /.devcontainer/entrypoint-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USER=vscode 4 | 5 | # Reconfigure User id if set by user 6 | if [ ! -z "${USER_UID}" ] && [ "${USER_UID}" != "`id -u ${USER}`" ] ; then 7 | echo -n "Update uid for user ${USER} with ${USER_UID}" 8 | usermod -u ${USER_UID} ${USER} 9 | echo "... updated" 10 | else 11 | echo "skipping UID configuration" 12 | fi 13 | 14 | if [ -n "${USER_GID}" ] && [ "${USER_GID}" != "`id -g ${USER}`" ] ; then 15 | echo -n "Update gid for group ${USER} with ${USER_GID}" 16 | usermod -u ${USER_UID} ${USER} 17 | echo "... updated" 18 | else 19 | echo "skipping GID configuration" 20 | fi 21 | 22 | #if [ -z "$SSH_AUTH_SOCK" ]; then 23 | # # Check for a currently running instance of the agent 24 | # RUNNING_AGENT="`ps -ax | grep 'ssh-agent -s' | grep -v grep | wc -l | tr -d '[:space:]'`" 25 | # if [ "$RUNNING_AGENT" = "0" ]; then 26 | # # Launch a new instance of the agent 27 | # ssh-agent -s &> $HOME/.ssh/ssh-agent 28 | # fi 29 | # eval `cat $HOME/.ssh/ssh-agent` 30 | #fi 31 | 32 | python3 /opt/netbox/netbox/manage.py migrate 33 | 34 | /bin/sh -c "while sleep 1000; do :; done" 35 | -------------------------------------------------------------------------------- /netbox_tunnels2/navigation.py: -------------------------------------------------------------------------------- 1 | from extras.plugins import PluginMenuButton, PluginMenuItem 2 | from utilities.choices import ButtonColorChoices 3 | 4 | 5 | menu_items = ( 6 | PluginMenuItem( 7 | link="plugins:netbox_tunnels2:tunnel_list", 8 | link_text="Tunnels", 9 | permissions=["netbox_tunnels2.view_tunnel"], 10 | buttons=(PluginMenuButton( 11 | link='plugins:netbox_tunnels2:plugintunnel_add', 12 | title='Add', 13 | icon_class='mdi mdi-plus-thick', 14 | color=ButtonColorChoices.GREEN, 15 | permissions=["netbox_tunnels2.add_tunnel"] 16 | ),) 17 | ), 18 | PluginMenuItem( 19 | link="plugins:netbox_tunnels2:tunneltype_list", 20 | link_text="Tunnel Types", 21 | permissions=["netbox_tunnels2.view_tunneltype"], 22 | buttons=(PluginMenuButton( 23 | link='plugins:netbox_tunnels2:tunneltype_add', 24 | title='Add', 25 | icon_class='mdi mdi-plus-thick', 26 | color=ButtonColorChoices.GREEN, 27 | permissions=["netbox_tunnels2.add_tunneltype"] 28 | ),) 29 | ), 30 | ) 31 | -------------------------------------------------------------------------------- /netbox_tunnels2/tests/custom.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | 3 | from utilities.testing.api import APITestCase as NetBoxAPITestCase 4 | from utilities.testing.views import ModelViewTestCase as NetBoxModelViewTestCase 5 | 6 | 7 | class ModelViewTestCase(NetBoxModelViewTestCase): 8 | """ 9 | Customized ModelViewTestCase for work with plugins 10 | """ 11 | 12 | def _get_base_url(self): 13 | """ 14 | Return the base format for a URL for the test's model. Override this to test for a model which belongs 15 | to a different app (e.g. testing Interfaces within the virtualization app). 16 | """ 17 | return ( 18 | f"plugins:{self.model._meta.app_label}:{self.model._meta.model_name}_{{}}" 19 | ) 20 | 21 | 22 | class APITestCase(NetBoxAPITestCase): 23 | """ 24 | Customized APITestCase for work with plugins 25 | """ 26 | 27 | def _get_detail_url(self, instance): 28 | viewname = f"plugins-api:{self._get_view_namespace()}:{instance._meta.model_name}-detail" 29 | return reverse(viewname, kwargs={"pk": instance.pk}) 30 | 31 | def _get_list_url(self): 32 | viewname = f"plugins-api:{self._get_view_namespace()}:{self.model._meta.model_name}-list" 33 | return reverse(viewname) -------------------------------------------------------------------------------- /netbox_tunnels2/templates/netbox_tunnels2/tunneltype.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load plugins %} 3 | {% load render_table from django_tables2 %} 4 | {% block content %} 5 |
6 |
7 |
8 |
Tunnel
9 |
10 | 11 | 12 | 13 | 14 | 15 |
Name{{ object.name }}
16 |
17 |
18 | {% include 'inc/panels/custom_fields.html' %} 19 | {% plugin_left_page object %} 20 |
21 |
22 | {% plugin_right_page object %} 23 |
24 |
25 |
26 |
27 |
28 |
Tunnels
29 |
30 | {% render_table tunnel_table %} 31 |
32 |
33 |
34 |
35 |
36 |
37 | {% plugin_full_width_page object %} 38 |
39 |
40 | {% endblock content %} 41 | -------------------------------------------------------------------------------- /netbox_tunnels2/migrations/0004_alter_tunnel_side_a_assigned_object_type_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.10 on 2023-08-22 05:48 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('contenttypes', '0002_remove_content_type_name'), 11 | ('netbox_tunnels2', '0003_tunnel_tenant'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='tunnel', 17 | name='side_a_assigned_object_type', 18 | field=models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'virtualization'), ('model', 'interfaces'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='side_a_assigned_object_type', to='contenttypes.contenttype'), 19 | ), 20 | migrations.AlterField( 21 | model_name='tunnel', 22 | name='side_b_assigned_object_type', 23 | field=models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'virtualization'), ('model', 'interfaces'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='side_b_assigned_object_type', to='contenttypes.contenttype'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /development/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | worker: &worker 3 | build: 4 | context: ../ 5 | dockerfile: ./development/Dockerfile 6 | args: 7 | NETBOX_VERSION: ${NETBOX_VERSION} 8 | command: sh -c "python manage.py rqworker" 9 | depends_on: 10 | - postgres 11 | - redis 12 | env_file: .env 13 | environment: 14 | - POSTGRES_HOST=postgres 15 | - REDIS_CACHE_HOST=redis 16 | - REDIS_HOST=redis 17 | volumes: 18 | - custom_scripts:/opt/netbox/netbox/scripts 19 | - ./configuration.py:/opt/netbox/netbox/netbox/configuration.py 20 | - ../netbox_tunnels2:/plugin/netbox_tunnels2/netbox_tunnels2 21 | - git_repos:/opt/git_repos/ 22 | networks: 23 | - netboxnet 24 | 25 | netbox: 26 | <<: *worker 27 | command: sh -c "python manage.py runserver 0.0.0.0:8000" 28 | ports: 29 | - "8000:8000" 30 | - "5678:5678" 31 | depends_on: 32 | - postgres 33 | - worker 34 | - redis 35 | 36 | postgres: 37 | image: postgres:14-alpine 38 | env_file: .env 39 | volumes: 40 | - pgdata:/var/lib/postgresql/data 41 | networks: 42 | - netboxnet 43 | 44 | redis: 45 | image: redis:7-alpine 46 | command: --appendonly yes --requirepass $REDIS_PASSWORD 47 | env_file: .env 48 | networks: 49 | - netboxnet 50 | 51 | volumes: 52 | pgdata: 53 | git_repos: 54 | custom_scripts: 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## 0.2.9 (2024-01-03) 3 | * Added Netbox 3.7 support 4 | * Renamed the Tunnel model to PluginTunnel to allow for compatibility with Netbox v3.7
5 | Please plan to migrate from `netbox-tunnels2` to the native [Netbox VPN Tunnels model](https://docs.netbox.dev/en/stable/features/vpn-tunnels/) 6 | 7 | ## 0.2.8 (2023-08-30) 8 | * Added Netbox 3.6 support 9 | 10 | ## 0.2.7 (2023-08-30) 11 | * Add related tunnels to side A interfaces by @p-rintz in https://github.com/robertlynch3/netbox-tunnels2/pull/14 12 | * Fix permissions for non-admin users by @p-rintz in https://github.com/robertlynch3/netbox-tunnels2/pull/16 13 | * Allow VM assignment to tunnels by @p-rintz in https://github.com/robertlynch3/netbox-tunnels2/pull/18 14 | 15 | ## 0.2.6 (2023-07-25) 16 | * Add Tenancy support to tunnels by @p-rintz in https://github.com/robertlynch3/netbox-tunnels2/pull/12 17 | * Fix title & align custom fields in template by @p-rintz in https://github.com/robertlynch3/netbox-tunnels2/pull/13 18 | 19 | 20 | ## 0.2.5 (2023-06-23) 21 | * Added the blocks for rendering extra content by registered plugins 22 | 23 | ## 0.2.4 (2023-05-03) 24 | * Resolved [#4](https://github.com/robertlynch3/netbox-tunnels2/issues/4) 25 | 26 | ## 0.2.3 (2023-05-03) 27 | * Added Netbox 3.5 support 28 | 29 | ## 0.2.2 (2023-04-21) 30 | * Resolved [#1](https://github.com/robertlynch3/netbox-tunnels2/issues/1) 31 | 32 | ## 0.2.1 (2023-04-19) 33 | * Initial release of netbox-tunnels2 34 | * Updated for Netbox v3.4+ 35 | * Added tunnel endpoints to denote the inner tunnel interfaces 36 | -------------------------------------------------------------------------------- /netbox_tunnels2/template_content.py: -------------------------------------------------------------------------------- 1 | from extras.plugins import PluginTemplateExtension 2 | from .models import PluginTunnel 3 | from .tables import RelatedTunnelTable 4 | from django.contrib.contenttypes.models import ContentType 5 | 6 | 7 | class InterfaceTunnels(PluginTemplateExtension): 8 | model = 'dcim.interface' 9 | 10 | def full_width_page(self): 11 | obj = self.context.get('object') 12 | types = ContentType.objects.get_for_model(obj) 13 | tunnel = PluginTunnel.objects.filter( 14 | side_a_assigned_object_type=types, 15 | side_a_assigned_object_id=obj.id, 16 | ) 17 | tunnel_table = RelatedTunnelTable(tunnel) 18 | return self.render( 19 | 'netbox_tunnels2/interface_extend.html', 20 | extra_context={ 21 | 'related_tunnels': tunnel_table 22 | } 23 | ) 24 | 25 | 26 | class VMInterfaceTunnels(PluginTemplateExtension): 27 | model = 'virtualization.vminterface' 28 | 29 | def full_width_page(self): 30 | obj = self.context.get('object') 31 | types = ContentType.objects.get_for_model(obj) 32 | tunnel = PluginTunnel.objects.filter( 33 | side_a_assigned_object_type=types, 34 | side_a_assigned_object_id=obj.id, 35 | ) 36 | tunnel_table = RelatedTunnelTable(tunnel) 37 | return self.render( 38 | 'netbox_tunnels2/interface_extend.html', 39 | extra_context={ 40 | 'related_tunnels': tunnel_table 41 | } 42 | ) 43 | 44 | 45 | template_extensions = [InterfaceTunnels, VMInterfaceTunnels] 46 | -------------------------------------------------------------------------------- /netbox_tunnels2/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | from netbox.views.generic import ObjectChangeLogView 4 | 5 | from . import models 6 | 7 | urlpatterns = ( 8 | path("tunnel/", views.ListTunnelView.as_view(), name="tunnel_list"), 9 | path("tunnel/", views.ListTunnelView.as_view(), name="plugintunnel_list"), 10 | path("tunnel//", views.TunnelView.as_view(), name="tunnel"), 11 | path("tunnel//edit/", views.EditTunnelView.as_view(), name="plugintunnel_edit"), 12 | path("tunnel//delete/", views.DeleteTunnelView.as_view(), name="plugintunnel_delete"), 13 | path("tunnel//changelog/", ObjectChangeLogView.as_view(), name="plugintunnel_changelog", kwargs={'model': models.TunnelType}), 14 | path("tunnel/add/", views.CreateTunnelView.as_view(), name="plugintunnel_add"), 15 | path("tunnel/delete/", views.BulkDeleteTunnelView.as_view(), name="plugintunnel_bulk_delete"), 16 | 17 | path("tunnel-type/", views.ListTunnelTypeView.as_view(), name="tunneltype_list"), 18 | path("tunnel-type//", views.TunnelTypeView.as_view(), name="tunneltype"), 19 | path("tunnel-type//edit/", views.EditTunnelTypeView.as_view(), name="tunneltype_edit"), 20 | path("tunnel-type//delete/", views.DeleteTunnelTypeView.as_view(), name="tunneltype_delete"), 21 | path("tunnel-type//changelog/", ObjectChangeLogView.as_view(), name="tunneltype_changelog", kwargs={'model': models.TunnelType}), 22 | path("tunnel-type/add/", views.CreateTunnelTypeView.as_view(), name="tunneltype_add"), 23 | path("tunnel-type/delete/", views.BulkDeleteTunnelTypeView.as_view(), name="tunneltype_bulk_delete") 24 | ) -------------------------------------------------------------------------------- /.devcontainer/configuration/logging.py: -------------------------------------------------------------------------------- 1 | # Remove first comment(#) on each line to implement this working logging example. 2 | # Add LOGLEVEL environment variable to netbox if you use this example & want a different log level. 3 | from os import environ 4 | 5 | # Set LOGLEVEL in netbox.env or docker-compose.overide.yml to override a logging level of INFO. 6 | LOGLEVEL = environ.get("LOGLEVEL", "INFO") 7 | 8 | LOGGING = { 9 | "version": 1, 10 | "disable_existing_loggers": False, 11 | "formatters": { 12 | "verbose": { 13 | "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", 14 | "style": "{", 15 | }, 16 | "simple": { 17 | "format": "{levelname} {message}", 18 | "style": "{", 19 | }, 20 | }, 21 | "filters": { 22 | "require_debug_false": { 23 | "()": "django.utils.log.RequireDebugFalse", 24 | }, 25 | }, 26 | "handlers": { 27 | "console": { 28 | "level": LOGLEVEL, 29 | "filters": ["require_debug_false"], 30 | "class": "logging.StreamHandler", 31 | "formatter": "simple", 32 | }, 33 | "mail_admins": { 34 | "level": "ERROR", 35 | "class": "django.utils.log.AdminEmailHandler", 36 | "filters": ["require_debug_false"], 37 | }, 38 | }, 39 | "loggers": { 40 | "django": { 41 | "handlers": ["console"], 42 | "propagate": True, 43 | }, 44 | "django.request": { 45 | "handlers": ["mail_admins"], 46 | "level": "ERROR", 47 | "propagate": False, 48 | }, 49 | "django_auth_ldap": { 50 | "handlers": ["console"], 51 | "level": LOGLEVEL, 52 | }, 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test NetBox Latest 2 | on: [push, pull_request] 3 | jobs: 4 | test-netbox-latest: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | python-version: ["3.8", "3.9", "3.10", "3.11"] 9 | services: 10 | redis: 11 | image: redis 12 | ports: 13 | - 6379:6379 14 | postgres: 15 | image: postgres 16 | env: 17 | POSTGRES_USER: netbox 18 | POSTGRES_PASSWORD: netbox 19 | options: >- 20 | --health-cmd pg_isready 21 | --health-interval 10s 22 | --health-timeout 5s 23 | --health-retries 5 24 | ports: 25 | - 5432:5432 26 | 27 | steps: 28 | - name: Checkout repo 29 | uses: actions/checkout@v3 30 | with: 31 | path: netbox-tunnels2 32 | 33 | - name: Set up Python ${{ matrix.python-version }} 34 | uses: actions/setup-python@v2 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | 38 | - name: Checkout netbox 39 | uses: actions/checkout@v3 40 | with: 41 | repository: "netbox-community/netbox" 42 | path: netbox 43 | ref: develop 44 | 45 | - name: install netbox_tunnels2 46 | working-directory: netbox-tunnels2 47 | run: | 48 | # include tests directory for test 49 | sed -i 's/exclude/#exclude/g' pyproject.toml 50 | pip install . 51 | - name: Install dependencies & set up configuration 52 | working-directory: netbox 53 | run: | 54 | ln -s $(pwd)/../netbox-tunnels2/docs/dev/configuration.testing.py netbox/netbox/configuration.py 55 | python -m pip install --upgrade pip 56 | pip install -r requirements.txt -U 57 | - name: Run tests 58 | working-directory: netbox 59 | run: | 60 | python netbox/manage.py test netbox_tunnels2 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netbox Tunnels Plugin 2 | This plugin is a fork of [jdrew82/netbox-tunnels-plugin](https://github.com/jdrew82/netbox-tunnels-plugin) and [hiddenman/netbox-tunnels-plugin](https://github.com/hiddenman/netbox-tunnels-plugin) in an effort to support Netbox 3.4+. 3 |


4 | **In Netbox 3.7, official Tunnel support was added. Please transition to using the build in feature.** 5 |


6 | 7 | ## Features 8 | This plugin provide following Models: 9 | * Tunnels 10 | * Tunnel Types 11 | 12 | ## Compatibility 13 | 14 | | NetBox Version | Plugin Version | 15 | |:--------------:|:--------------:| 16 | | NetBox 3.4 | 0.2.2 | 17 | | NetBox 3.5 | 0.2.3 | 18 | | NetBox 3.6 | 0.2.8 | 19 | | NetBox 3.7 > | 0.2.9 | 20 | 21 | 22 | ## Installation 23 | You can install this package from Pip 24 | ``` 25 | pip install netbox-tunnels2 26 | ``` 27 | 28 | To install the package from source 29 | ``` 30 | git clone https://github.com/robertlynch3/netbox-tunnels2.git 31 | cd netbox-tunnels2 32 | source /path/to/netbox/venv/bin/activate 33 | python3 setup.py develop 34 | ``` 35 | 36 | Enable the plugin in /opt/netbox/netbox/netbox/configuration.py: 37 | ``` 38 | PLUGINS = ['netbox_tunnels2'] 39 | ``` 40 | 41 | Apply the migrations with Netbox `manage.py`: 42 | ``` 43 | (venv) $ python manage.py migrate 44 | ``` 45 | 46 | Restart Netbox to apply the changes: 47 | ``` 48 | sudo systemctl restart netbox 49 | ``` 50 | See [NetBox Documentation](https://docs.netbox.dev/en/stable/plugins/#installing-plugins) for details 51 | 52 | ## Screenshots 53 | Tunnel List 54 | ![Tunnel List](https://github.com/robertlynch3/netbox-tunnels2/blob/master/docs/img/tunnel-list.png) 55 | 56 | Tunnel Details 57 | ![Tunnel Details](https://github.com/robertlynch3/netbox-tunnels2/blob/master/docs/img/tunnel-info.png) 58 | 59 | ## TODO 60 | * Validate the Public IP addresses belong to the Devices 61 | * Validate the same inside interface is not used by multiple tunnels -------------------------------------------------------------------------------- /.devcontainer/Dockerfile-plugin_dev: -------------------------------------------------------------------------------- 1 | ARG NETBOX_VARIANT=v3.5 2 | 3 | FROM netboxcommunity/netbox:${NETBOX_VARIANT} 4 | 5 | ARG NETBOX_INITIALIZERS_VARIANT=3.5.* 6 | 7 | ARG DEBIAN_FRONTEND=noninteractive 8 | 9 | # Install APT packages 10 | # hadolint ignore=DL3008 11 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 12 | && apt-get -y install --no-install-recommends curl git make openssh-client python3.10-dev sudo wget zsh vim \ 13 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* 14 | 15 | # Install development & ide dependencies 16 | COPY requirements-dev.txt /tmp/pip-tmp/ 17 | RUN /opt/netbox/venv/bin/python3 -m pip install --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements-dev.txt \ 18 | && rm -rf /tmp/* 19 | 20 | ARG USERNAME=vscode 21 | ARG USER_UID=1000 22 | ARG USER_GID=$USER_UID 23 | 24 | RUN useradd -l -md /home/vscode -s /usr/bin/zsh -u $USER_UID $USERNAME \ 25 | && usermod -aG sudo $USERNAME \ 26 | && echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \ 27 | && mkdir /opt/netbox/netbox/netbox-tunnels2 \ 28 | && chown $USERNAME:$USERNAME /opt/netbox /etc/netbox /opt/unit -R 29 | 30 | USER $USERNAME 31 | 32 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 33 | # Add oh my zsh 34 | RUN wget --quiet https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh || true 35 | 36 | COPY .bashrc /home/vscode/.bashrc 37 | COPY .zshrc /home/vscode/.zshrc 38 | 39 | RUN /opt/netbox/venv/bin/pip install --no-warn-script-location netbox-initializers==${NETBOX_INITIALIZERS_VARIANT} 40 | 41 | WORKDIR /opt/netbox/netbox/netbox-tunnels2 42 | 43 | RUN python3 /opt/netbox/netbox/manage.py migrate 44 | RUN python3 /opt/netbox/netbox/manage.py load_initializer_data --path .devcontainer/initializers/ 45 | 46 | # hadolint ignore=DL3002 47 | USER root 48 | 49 | COPY entrypoint-dev.sh /bin/entrypoint-dev.sh 50 | RUN chmod +x /bin/entrypoint-dev.sh 51 | 52 | CMD ["/bin/entrypoint-dev.sh"] 53 | -------------------------------------------------------------------------------- /netbox_tunnels2/filtersets.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from netbox.filtersets import NetBoxModelFilterSet 3 | from dcim.models import Interface 4 | from tenancy.models import Tenant 5 | from .models import PluginTunnel, TunnelType 6 | 7 | 8 | class TunnelFilterSet(NetBoxModelFilterSet): 9 | side_a_interface = django_filters.ModelMultipleChoiceFilter( 10 | field_name="interface__name", 11 | queryset=Interface.objects.all(), 12 | to_field_name="name", 13 | label="Side A Interface (name)", 14 | ) 15 | side_a_interface_id = django_filters.ModelMultipleChoiceFilter( 16 | field_name="interface", 17 | queryset=Interface.objects.all(), 18 | label="Side A Interface (ID)", 19 | ) 20 | side_b_interface = django_filters.ModelMultipleChoiceFilter( 21 | field_name="interface__name", 22 | queryset=Interface.objects.all(), 23 | to_field_name="name", 24 | label="Side B Interface (name)", 25 | ) 26 | side_b_interface_id = django_filters.ModelMultipleChoiceFilter( 27 | field_name="interface", 28 | queryset=Interface.objects.all(), 29 | label="Side B Interface (ID)", 30 | ) 31 | tenant_id = django_filters.ModelChoiceFilter( 32 | field_name="tenant_id", 33 | queryset=Tenant.objects.all(), 34 | to_field_name="id", 35 | label="Tenant (ID)", 36 | ) 37 | 38 | class Meta: 39 | model = PluginTunnel 40 | fields = ( 41 | "name", 42 | "status", 43 | "tunnel_type", 44 | "a_pub_address", 45 | "b_pub_address", 46 | "side_a_interface", 47 | "side_a_interface_id", 48 | "side_b_interface", 49 | "side_b_interface_id", 50 | ) 51 | 52 | def search(self, queryset, name, value): 53 | return queryset.filter(description__icontains=value) 54 | 55 | 56 | class TunnelTypeFilterSet(NetBoxModelFilterSet): 57 | class Meta: 58 | model = TunnelType 59 | fields = ( 60 | "name", 61 | "slug" 62 | ) 63 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup file for netbox-tunnels plugin. 2 | 3 | (c) 2020 Justin Drew 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | import codecs 15 | import os.path 16 | 17 | from setuptools import find_packages, setup 18 | 19 | 20 | with open("README.md", "r") as fh: 21 | long_description = fh.read() 22 | 23 | 24 | def read(rel_path): 25 | here = os.path.abspath(os.path.dirname(__file__)) 26 | with codecs.open(os.path.join(here, rel_path), 'r') as fp: 27 | return fp.read() 28 | 29 | 30 | def get_version(rel_path): 31 | for line in read(rel_path).splitlines(): 32 | if line.startswith('__version__'): 33 | delim = '"' if '"' in line else "'" 34 | return line.split(delim)[1] 35 | else: 36 | raise RuntimeError("Unable to find version string.") 37 | 38 | def get_min_version(rel_path): 39 | for line in read(rel_path).splitlines(): 40 | if line.startswith('__version__'): 41 | delim = '"' if '"' in line else "'" 42 | return line.split(delim)[1] 43 | else: 44 | raise RuntimeError("Unable to find version string.") 45 | 46 | 47 | setup( 48 | name="netbox-tunnels2", 49 | version=get_min_version('netbox_tunnels2/version.py'), 50 | description="A plugin for NetBox to support documentation of network tunneling protocols, ie IPsec, GRE, L2TP, etc.", 51 | long_description=long_description, 52 | long_description_content_type="text/markdown", 53 | url="https://github.com/robertlynch3/netbox-tunnels2", 54 | author="Robert Lynch", 55 | license="Apache v2.0", 56 | package_data={"": ["LICENSE"],}, 57 | install_requires=[], 58 | min_version=get_min_version('netbox_tunnels2/version.py'), 59 | packages=find_packages(), 60 | include_package_data=True, 61 | ) 62 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: NetBox Tunnels Plugin 2 | site_url: https://robertlynch3.github.io/netbox_tunnels_plugin 3 | repo_url: https://github.com/robertlynch3/netbox_tunnels_plugin 4 | repo_name: robertlynch3/netbox_tunnels_plugin 5 | #strict: true 6 | nav: 7 | - Home: index.md 8 | - Contributing: contributing.md 9 | - Changelog: changelog.md 10 | theme: 11 | name: material 12 | language: en 13 | #logo: assets/logo.png 14 | palette: 15 | scheme: preference 16 | primary: indigo 17 | accent: indigo 18 | features: 19 | - navigation.indexes 20 | - navigation.instant 21 | - navigation.tabs.sticky 22 | markdown_extensions: 23 | - pymdownx.emoji: 24 | emoji_index: !!python/name:materialx.emoji.twemoji 25 | emoji_generator: !!python/name:materialx.emoji.to_svg 26 | - pymdownx.critic 27 | - pymdownx.caret 28 | - pymdownx.mark 29 | - pymdownx.tilde 30 | - pymdownx.tabbed 31 | - attr_list 32 | - pymdownx.arithmatex: 33 | generic: true 34 | - pymdownx.highlight: 35 | linenums: false 36 | - pymdownx.superfences 37 | - pymdownx.inlinehilite 38 | - pymdownx.details 39 | - admonition 40 | - toc: 41 | baselevel: 2 42 | permalink: true 43 | slugify: !!python/name:pymdownx.slugs.uslugify 44 | - meta 45 | plugins: 46 | - include-markdown 47 | - search: 48 | lang: en 49 | - mkdocstrings: 50 | watch: 51 | - netbox_tunnels_plugin 52 | extra: 53 | social: 54 | - icon: fontawesome/brands/twitter 55 | # replace with your own tweet link below 56 | link: https://github.com/netbox-community/cookiecutter-netbox-plugin 57 | name: Tweet 58 | - icon: fontawesome/brands/facebook 59 | # replace with your own facebook link below 60 | link: https://github.com/netbox-community/cookiecutter-netbox-plugin 61 | name: Facebook 62 | - icon: fontawesome/brands/github 63 | link: https://github.com/robertlynch3/netbox_tunnels_plugin 64 | name: Github 65 | - icon: material/email 66 | link: "mailto:robertlynch3@users.noreply.github.com" 67 | # to enable disqus, uncomment the following and put your disqus id below 68 | # disqus: disqus_id 69 | # uncomment the following and put your google tracking id below to enable GA 70 | #google_analytics: 71 | #- UA-xxx 72 | #- auto 73 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3.4' 3 | services: 4 | netbox: &netbox 5 | image: netboxcommunity/netbox:${VARIANT-latest} 6 | depends_on: 7 | - postgres 8 | - redis 9 | #- redis-cache 10 | #- netbox-worker 11 | env_file: env/netbox.env 12 | user: 'unit:root' 13 | healthcheck: 14 | start_period: 60s 15 | timeout: 3s 16 | interval: 15s 17 | test: "curl -f http://localhost:8080/api/ || exit 1" 18 | volumes: 19 | - ./configuration:/etc/netbox/config:z,ro 20 | #- ./reports:/etc/netbox/reports:z,ro 21 | #- ./scripts:/etc/netbox/scripts:z,ro 22 | #- netbox-media-files:/opt/netbox/netbox/media:z 23 | #netbox-worker: 24 | # <<: *netbox 25 | # depends_on: 26 | # netbox: 27 | # condition: service_healthy 28 | # command: 29 | # - /opt/netbox/venv/bin/python 30 | # - /opt/netbox/netbox/manage.py 31 | # - rqworker 32 | # healthcheck: 33 | # start_period: 20s 34 | # timeout: 3s 35 | # interval: 15s 36 | # test: "ps -aux | grep -v grep | grep -q rqworker || exit 1" 37 | #netbox-housekeeping: 38 | # <<: *netbox 39 | # depends_on: 40 | # netbox: 41 | # condition: service_healthy 42 | # command: 43 | # - /opt/netbox/housekeeping.sh 44 | # healthcheck: 45 | # start_period: 20s 46 | # timeout: 3s 47 | # interval: 15s 48 | # test: "ps -aux | grep -v grep | grep -q housekeeping || exit 1" 49 | 50 | # postgres 51 | postgres: 52 | image: postgres:15-alpine 53 | env_file: env/postgres.env 54 | volumes: 55 | - netbox-postgres-data:/var/lib/postgresql/data 56 | 57 | # redis 58 | redis: 59 | image: redis:7-alpine 60 | command: 61 | - sh 62 | - -c # this is to evaluate the $REDIS_PASSWORD from the env 63 | - redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose 64 | env_file: env/redis.env 65 | #volumes: 66 | # - netbox-redis-data:/data 67 | #redis-cache: 68 | # image: redis:7-alpine 69 | # command: 70 | # - sh 71 | # - -c # this is to evaluate the $REDIS_PASSWORD from the env 72 | # - redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose 73 | # env_file: env/redis-cache.env 74 | # volumes: 75 | # - netbox-redis-cache-data:/data 76 | 77 | volumes: 78 | #netbox-media-files: 79 | # driver: local 80 | netbox-postgres-data: 81 | driver: local 82 | #netbox-redis-data: 83 | # driver: local 84 | #netbox-redis-cache-data: 85 | # driver: local 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | #env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Mac store file 132 | .DS_Store 133 | 134 | # VSCode & PyCharm Settings 135 | .idea/.gitignore 136 | .idea/misc.xml 137 | .idea/modules.xml 138 | .idea/netbox_tunnels_plugin.iml 139 | .idea/netbox-tunnels2.iml 140 | .idea/vcs.xml 141 | .idea/inspectionProfiles/Project_Default.xml 142 | .idea/inspectionProfiles/profiles_settings.xml 143 | .vscode/settings.json 144 | 145 | 146 | 147 | ignored/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! Every little bit 4 | helps, and credit will always be given. 5 | 6 | You can contribute in many ways: 7 | 8 | ## Types of Contributions 9 | 10 | ### Report Bugs 11 | 12 | Report bugs at https://github.com/robertlynch3/netbox-tunnels2/issues. 13 | 14 | If you are reporting a bug, please include: 15 | 16 | * Your operating system name and version. 17 | * Any details about your local setup that might be helpful in troubleshooting. 18 | * Detailed steps to reproduce the bug. 19 | 20 | ### Fix Bugs 21 | 22 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 23 | wanted" is open to whoever wants to implement it. 24 | 25 | ### Implement Features 26 | 27 | Look through the GitHub issues for features. Anything tagged with "enhancement" 28 | and "help wanted" is open to whoever wants to implement it. 29 | 30 | ### Write Documentation 31 | 32 | NetBox Tunnels Plugin could always use more documentation, whether as part of the 33 | official NetBox Tunnels Plugin docs, in docstrings, or even on the web in blog posts, 34 | articles, and such. 35 | 36 | ### Submit Feedback 37 | 38 | The best way to send feedback is to file an issue at https://github.com/robertlynch3/netbox-tunnels2/issues. 39 | 40 | If you are proposing a feature: 41 | 42 | * Explain in detail how it would work. 43 | * Keep the scope as narrow as possible, to make it easier to implement. 44 | * Remember that this is a volunteer-driven project, and that contributions 45 | are welcome :) 46 | 47 | ## Get Started! 48 | 49 | Ready to contribute? Here's how to set up `netbox-tunnels2` for local development. 50 | 51 | 1. Fork the `netbox-tunnels2` repo on GitHub. 52 | 2. Clone your fork locally 53 | 54 | ``` 55 | $ git clone git@github.com:your_name_here/netbox-tunnels2.git 56 | ``` 57 | 58 | 3. Install dependencies and start your virtualenv: 59 | 60 | ``` 61 | $ poetry install -E test -E doc -E dev 62 | ``` 63 | 64 | 4. Create a branch for local development: 65 | 66 | ``` 67 | $ git checkout -b name-of-your-bugfix-or-feature 68 | ``` 69 | 70 | Now you can make your changes locally. 71 | 72 | 5. When you're done making changes, check that your changes pass the 73 | tests, including testing other Python versions, with tox: 74 | 75 | ``` 76 | $ poetry run tox 77 | ``` 78 | 79 | 6. Commit your changes and push your branch to GitHub: 80 | 81 | ``` 82 | $ git add . 83 | $ git commit -m "Your detailed description of your changes." 84 | $ git push origin name-of-your-bugfix-or-feature 85 | ``` 86 | 87 | 7. Submit a pull request through the GitHub website. 88 | 89 | ## Pull Request Guidelines 90 | 91 | Before you submit a pull request, check that it meets these guidelines: 92 | 93 | 1. The pull request should include tests. 94 | 2. If the pull request adds functionality, the docs should be updated. Put 95 | your new functionality into a function with a docstring, and add the 96 | feature to the list in README.md. 97 | 3. The pull request should work for Python 3.6, 3.7, 3.8 and 3.9. Check 98 | https://github.com/robertlynch3/netbox-tunnels2/actions 99 | and make sure that the tests pass for all supported Python versions. 100 | 101 | 102 | ## Deploying 103 | 104 | A reminder for the maintainers on how to deploy. 105 | Make sure all your changes are committed (including an entry in CHANGELOG.md). 106 | Then run: 107 | 108 | ``` 109 | $ poetry run bump2version patch # possible: major / minor / patch 110 | $ git push 111 | $ git push --tags 112 | ``` 113 | 114 | GitHub Actions will then deploy to PyPI if tests pass. 115 | -------------------------------------------------------------------------------- /netbox_tunnels2/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-04-18 20:30 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import taggit.managers 6 | import utilities.json 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('extras', '0084_staging'), 15 | ('ipam', '0064_clear_search_cache'), 16 | ('contenttypes', '0002_remove_content_type_name'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='TunnelType', 22 | fields=[ 23 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 24 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 25 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 26 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 27 | ('description', models.CharField(blank=True, max_length=200)), 28 | ('name', models.CharField(max_length=100, unique=True)), 29 | ('slug', models.SlugField(max_length=100, unique=True)), 30 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 31 | ], 32 | options={ 33 | 'ordering': ['name'], 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name='Tunnel', 38 | fields=[ 39 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 40 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 41 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 42 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 43 | ('name', models.CharField(max_length=64)), 44 | ('status', models.CharField(default='pending-config', max_length=30)), 45 | ('side_a_assigned_object_id', models.PositiveBigIntegerField(blank=True, null=True)), 46 | ('side_b_assigned_object_id', models.PositiveBigIntegerField(blank=True, null=True)), 47 | ('description', models.CharField(blank=True, max_length=200)), 48 | ('psk', models.CharField(blank=True, max_length=100)), 49 | ('comments', models.TextField(blank=True)), 50 | ('a_pub_address', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tunnel_a_pub_address', to='ipam.ipaddress')), 51 | ('b_pub_address', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tunnel_b_pub_address', to='ipam.ipaddress')), 52 | ('side_a_assigned_object_type', models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='side_a_assigned_object_type', to='contenttypes.contenttype')), 53 | ('side_b_assigned_object_type', models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface'))), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='side_b_assigned_object_type', to='contenttypes.contenttype')), 54 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 55 | ('tunnel_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tunnels', to='netbox_tunnels2.tunneltype')), 56 | ], 57 | options={ 58 | 'verbose_name_plural': 'Tunnels', 59 | }, 60 | ), 61 | ] -------------------------------------------------------------------------------- /netbox_tunnels2/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.db import IntegrityError, transaction 3 | from django.test import TestCase 4 | from django.contrib.contenttypes.models import ContentType 5 | 6 | from netbox_tunnels2.models import PluginTunnel, TunnelType 7 | from dcim.models import Site, DeviceRole, DeviceType, Manufacturer, Device, Interface 8 | from ipam.models import IPAddress 9 | from virtualization.models import VirtualMachine, VMInterface 10 | from tenancy.models.tenants import Tenant 11 | 12 | 13 | class TunnelTestCase(TestCase): 14 | def setUp(self): 15 | self.tunnelType1 = TunnelType.objects.create(name='GRE', slug='gre') 16 | self.tunnelType2 = TunnelType.objects.create(name='IPSec Tunnel', slug='ipsec') 17 | self.ipAddressA = IPAddress.objects.create(address='1.0.0.1/32') 18 | self.ipAddressB = IPAddress.objects.create(address='1.0.0.2/32') 19 | self.tenant = Tenant.objects.create(name='TestTenant', slug='testtenant') 20 | site = Site.objects.create(name='test', slug='test') 21 | manufacturer = Manufacturer.objects.create(name='TestManufacturer', slug='testmanufacturer') 22 | device_role = DeviceRole.objects.create(name='Firewall', slug='firewall') 23 | device_type = DeviceType.objects.create(slug='devicetype1', model='DeviceType1', manufacturer=manufacturer) 24 | self.device = Device.objects.create( 25 | device_type=device_type, name='device1', device_role=device_role, site=site, 26 | ) 27 | self.vm = VirtualMachine.objects.create( 28 | name='test_vm', 29 | site=site, 30 | ) 31 | self.interface = Interface.objects.create(name='test_intf', device=self.device, type='virtual') 32 | self.vminterface = VMInterface.objects.create(name='test_vm_intf', virtual_machine=self.vm) 33 | self.interface.ip_addresses.add(self.ipAddressA) 34 | self.vminterface.ip_addresses.add(self.ipAddressB) 35 | 36 | def test_tunnel_creation(self): 37 | tunnel1 = PluginTunnel.objects.create(name='Test Tunnel1', 38 | tunnel_type=self.tunnelType1, 39 | a_pub_address=self.ipAddressA, 40 | tenant=self.tenant) 41 | tunnel1.full_clean() 42 | self.assertEqual(tunnel1.a_pub_address, self.ipAddressA) 43 | self.assertEqual(tunnel1.tenant, self.tenant) 44 | 45 | # Tests an (invalid) Null for a_pub_address 46 | with self.assertRaises(IntegrityError): 47 | with transaction.atomic(): 48 | PluginTunnel.objects.create(name='Test Tunnel2', tunnel_type=self.tunnelType1, b_pub_address=self.ipAddressA) 49 | 50 | tunnel3 = PluginTunnel.objects.create(name='Test Tunnel3', tunnel_type=self.tunnelType1, a_pub_address=self.ipAddressA, b_pub_address=self.ipAddressB) 51 | self.assertEqual(tunnel3.a_pub_address, self.ipAddressA) 52 | self.assertEqual(tunnel3.b_pub_address, self.ipAddressB) 53 | 54 | def test_tunnel_creation_sides(self): 55 | tunnel = PluginTunnel.objects.create(name='Test Tunnel1', 56 | tunnel_type=self.tunnelType1, 57 | a_pub_address=self.ipAddressA, 58 | b_pub_address=self.ipAddressB, 59 | side_a_assigned_object_type=ContentType.objects.get_by_natural_key('dcim', 'interface'), 60 | side_a_assigned_object_id=self.interface.id, 61 | side_b_assigned_object_type=ContentType.objects.get_by_natural_key('virtualization', 'vminterface'), 62 | side_b_assigned_object_id=self.vminterface.id, 63 | tenant=self.tenant 64 | ) 65 | tunnel.full_clean(exclude=['side_a_assigned_object_type', 'side_b_assigned_object_type']) 66 | self.assertEqual(tunnel.a_pub_address, self.ipAddressA) 67 | self.assertEqual(tunnel.b_pub_address, self.ipAddressB) 68 | self.assertEqual(tunnel.tenant, self.tenant) 69 | self.assertEqual(tunnel.side_a_assigned_object, self.interface) 70 | self.assertEqual(tunnel.side_b_assigned_object, self.vminterface) 71 | -------------------------------------------------------------------------------- /.devcontainer/.bashrc: -------------------------------------------------------------------------------- 1 | # If you come from bash you might have to change your $PATH. 2 | # export PATH=$HOME/bin:/usr/local/bin:$PATH 3 | 4 | # Path to your oh-my-zsh installation. 5 | export ZSH="$HOME/.oh-my-zsh" 6 | 7 | # Set name of the theme to load --- if set to "random", it will 8 | # load a random theme each time oh-my-zsh is loaded, in which case, 9 | # to know which specific one was loaded, run: echo $RANDOM_THEME 10 | # See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes 11 | ZSH_THEME="robbyrussell" 12 | 13 | # Set list of themes to pick from when loading at random 14 | # Setting this variable when ZSH_THEME=random will cause zsh to load 15 | # a theme from this variable instead of looking in $ZSH/themes/ 16 | # If set to an empty array, this variable will have no effect. 17 | # ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" ) 18 | 19 | # Uncomment the following line to use case-sensitive completion. 20 | # CASE_SENSITIVE="true" 21 | 22 | # Uncomment the following line to use hyphen-insensitive completion. 23 | # Case-sensitive completion must be off. _ and - will be interchangeable. 24 | # HYPHEN_INSENSITIVE="true" 25 | 26 | # Uncomment one of the following lines to change the auto-update behavior 27 | # zstyle ':omz:update' mode disabled # disable automatic updates 28 | # zstyle ':omz:update' mode auto # update automatically without asking 29 | # zstyle ':omz:update' mode reminder # just remind me to update when it's time 30 | 31 | # Uncomment the following line to change how often to auto-update (in days). 32 | # zstyle ':omz:update' frequency 13 33 | 34 | # Uncomment the following line if pasting URLs and other text is messed up. 35 | # DISABLE_MAGIC_FUNCTIONS="true" 36 | 37 | # Uncomment the following line to disable colors in ls. 38 | # DISABLE_LS_COLORS="true" 39 | 40 | # Uncomment the following line to disable auto-setting terminal title. 41 | # DISABLE_AUTO_TITLE="true" 42 | 43 | # Uncomment the following line to enable command auto-correction. 44 | # ENABLE_CORRECTION="true" 45 | 46 | # Uncomment the following line to display red dots whilst waiting for completion. 47 | # You can also set it to another string to have that shown instead of the default red dots. 48 | # e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f" 49 | # Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765) 50 | # COMPLETION_WAITING_DOTS="true" 51 | 52 | # Uncomment the following line if you want to disable marking untracked files 53 | # under VCS as dirty. This makes repository status check for large repositories 54 | # much, much faster. 55 | # DISABLE_UNTRACKED_FILES_DIRTY="true" 56 | 57 | # Uncomment the following line if you want to change the command execution time 58 | # stamp shown in the history command output. 59 | # You can set one of the optional three formats: 60 | # "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd" 61 | # or set a custom format using the strftime function format specifications, 62 | # see 'man strftime' for details. 63 | # HIST_STAMPS="mm/dd/yyyy" 64 | 65 | # Would you like to use another custom folder than $ZSH/custom? 66 | # ZSH_CUSTOM=/path/to/new-custom-folder 67 | 68 | # Which plugins would you like to load? 69 | # Standard plugins can be found in $ZSH/plugins/ 70 | # Custom plugins may be added to $ZSH_CUSTOM/plugins/ 71 | # Example format: plugins=(rails git textmate ruby lighthouse) 72 | # Add wisely, as too many plugins slow down shell startup. 73 | plugins=(common-aliases colored-man-pages colorize docker docker-compose emoji safe-paste git git-auto-fetch git-extras history jsontools pip) 74 | 75 | source $ZSH/oh-my-zsh.sh 76 | 77 | # User configuration 78 | 79 | # export MANPATH="/usr/local/man:$MANPATH" 80 | 81 | # You may need to manually set your language environment 82 | # export LANG=en_US.UTF-8 83 | 84 | # Preferred editor for local and remote sessions 85 | # if [[ -n $SSH_CONNECTION ]]; then 86 | # export EDITOR='vim' 87 | # else 88 | # export EDITOR='mvim' 89 | # fi 90 | 91 | # Compilation flags 92 | # export ARCHFLAGS="-arch x86_64" 93 | 94 | # Set personal aliases, overriding those provided by oh-my-zsh libs, 95 | # plugins, and themes. Aliases can be placed here, though oh-my-zsh 96 | # users are encouraged to define aliases within the ZSH_CUSTOM folder. 97 | # For a full list of active aliases, run `alias`. 98 | # 99 | # Example aliases 100 | # alias zshconfig="mate ~/.zshrc" 101 | # alias ohmyzsh="mate ~/.oh-my-zsh" 102 | 103 | # Activate Python venv in terminal 104 | source /opt/netbox/venv/bin/activate 105 | -------------------------------------------------------------------------------- /.devcontainer/.zshrc: -------------------------------------------------------------------------------- 1 | # If you come from bash you might have to change your $PATH. 2 | # export PATH=$HOME/bin:/usr/local/bin:$PATH 3 | 4 | # Path to your oh-my-zsh installation. 5 | export ZSH="$HOME/.oh-my-zsh" 6 | 7 | # Set name of the theme to load --- if set to "random", it will 8 | # load a random theme each time oh-my-zsh is loaded, in which case, 9 | # to know which specific one was loaded, run: echo $RANDOM_THEME 10 | # See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes 11 | ZSH_THEME="robbyrussell" 12 | 13 | # Set list of themes to pick from when loading at random 14 | # Setting this variable when ZSH_THEME=random will cause zsh to load 15 | # a theme from this variable instead of looking in $ZSH/themes/ 16 | # If set to an empty array, this variable will have no effect. 17 | # ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" ) 18 | 19 | # Uncomment the following line to use case-sensitive completion. 20 | # CASE_SENSITIVE="true" 21 | 22 | # Uncomment the following line to use hyphen-insensitive completion. 23 | # Case-sensitive completion must be off. _ and - will be interchangeable. 24 | # HYPHEN_INSENSITIVE="true" 25 | 26 | # Uncomment one of the following lines to change the auto-update behavior 27 | # zstyle ':omz:update' mode disabled # disable automatic updates 28 | # zstyle ':omz:update' mode auto # update automatically without asking 29 | # zstyle ':omz:update' mode reminder # just remind me to update when it's time 30 | 31 | # Uncomment the following line to change how often to auto-update (in days). 32 | # zstyle ':omz:update' frequency 13 33 | 34 | # Uncomment the following line if pasting URLs and other text is messed up. 35 | # DISABLE_MAGIC_FUNCTIONS="true" 36 | 37 | # Uncomment the following line to disable colors in ls. 38 | # DISABLE_LS_COLORS="true" 39 | 40 | # Uncomment the following line to disable auto-setting terminal title. 41 | # DISABLE_AUTO_TITLE="true" 42 | 43 | # Uncomment the following line to enable command auto-correction. 44 | # ENABLE_CORRECTION="true" 45 | 46 | # Uncomment the following line to display red dots whilst waiting for completion. 47 | # You can also set it to another string to have that shown instead of the default red dots. 48 | # e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f" 49 | # Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765) 50 | # COMPLETION_WAITING_DOTS="true" 51 | 52 | # Uncomment the following line if you want to disable marking untracked files 53 | # under VCS as dirty. This makes repository status check for large repositories 54 | # much, much faster. 55 | # DISABLE_UNTRACKED_FILES_DIRTY="true" 56 | 57 | # Uncomment the following line if you want to change the command execution time 58 | # stamp shown in the history command output. 59 | # You can set one of the optional three formats: 60 | # "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd" 61 | # or set a custom format using the strftime function format specifications, 62 | # see 'man strftime' for details. 63 | # HIST_STAMPS="mm/dd/yyyy" 64 | 65 | # Would you like to use another custom folder than $ZSH/custom? 66 | # ZSH_CUSTOM=/path/to/new-custom-folder 67 | 68 | # Which plugins would you like to load? 69 | # Standard plugins can be found in $ZSH/plugins/ 70 | # Custom plugins may be added to $ZSH_CUSTOM/plugins/ 71 | # Example format: plugins=(rails git textmate ruby lighthouse) 72 | # Add wisely, as too many plugins slow down shell startup. 73 | plugins=(common-aliases colored-man-pages colorize docker docker-compose emoji safe-paste git git-auto-fetch git-extras history jsontools pip) 74 | 75 | source $ZSH/oh-my-zsh.sh 76 | 77 | # User configuration 78 | 79 | # export MANPATH="/usr/local/man:$MANPATH" 80 | 81 | # You may need to manually set your language environment 82 | # export LANG=en_US.UTF-8 83 | 84 | # Preferred editor for local and remote sessions 85 | # if [[ -n $SSH_CONNECTION ]]; then 86 | # export EDITOR='vim' 87 | # else 88 | # export EDITOR='mvim' 89 | # fi 90 | 91 | # Compilation flags 92 | # export ARCHFLAGS="-arch x86_64" 93 | 94 | # Set personal aliases, overriding those provided by oh-my-zsh libs, 95 | # plugins, and themes. Aliases can be placed here, though oh-my-zsh 96 | # users are encouraged to define aliases within the ZSH_CUSTOM folder. 97 | # For a full list of active aliases, run `alias`. 98 | # 99 | # Example aliases 100 | # alias zshconfig="mate ~/.zshrc" 101 | # alias ohmyzsh="mate ~/.oh-my-zsh" 102 | 103 | # Activate Python venv in terminal 104 | source /opt/netbox/venv/bin/activate 105 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/python-3-postgres 3 | // Update the VARIANT arg in docker-compose.yml to pick a Python version 4 | { 5 | "name": "NetBox Plugin Development", 6 | "dockerComposeFile": [ 7 | "docker-compose.yml", 8 | "docker-compose.override.yml" 9 | ], 10 | "service": "netbox", 11 | //"workspaceMount": "source=${localWorkspaceFolder},target=/opt/netbox/netbox/netbox-tunnels2,type=bind,consistency=cached", 12 | "workspaceFolder": "/opt/netbox/netbox/netbox-tunnels2", 13 | 14 | "overrideCommand":false, 15 | 16 | // Configure tool-specific properties. 17 | "customizations": { 18 | // Configure properties specific to VS Code. 19 | "vscode": { 20 | // Set *default* container specific settings.json values on container create. 21 | "settings": { 22 | "editor.experimental.stickyScroll.enabled": true, 23 | //"[python]": { 24 | // "editor.codeActionsOnSave": { 25 | // "source.organizeImports": true 26 | // } 27 | //}, 28 | "isort.args": [ 29 | "--profile=black" 30 | ], 31 | "isort.path": "/opt/netbox/venv/bin/isort", 32 | "python.analysis.typeCheckingMode": "strict", 33 | python.Jedi 34 | "python.analysis.extraPaths": [ 35 | "/opt/netbox/netbox" 36 | ], 37 | "python.autoComplete.extraPaths": [ 38 | "/opt/netbox/netbox" 39 | ], 40 | "python.defaultInterpreterPath": "/opt/netbox/venv/bin/python3", 41 | "python.formatting.autopep8Path": "/opt/netbox/venv/bin/autopep8", 42 | "python.formatting.blackPath": "/opt/netbox/venv/bin/black", 43 | "python.formatting.provider": "black", 44 | "python.formatting.yapfPath": "/opt/netbox/venv/bin/yapf", 45 | "python.linting.banditPath": "/opt/netbox/venv/bin/bandit", 46 | "python.linting.enabled": true, 47 | "python.linting.flake8Path": "/opt/netbox/venv/bin/flake8", 48 | "python.linting.flake8Args": [ 49 | "--max-line-length=160", 50 | "--ignore=E203" 51 | ], 52 | "python.linting.mypyPath": "//opt/netbox/venv/bin/mypy", 53 | "python.linting.pycodestylePath": "/opt/netbox/venv/bin/pycodestyle", 54 | "python.linting.pydocstylePath": "/opt/netbox/venv/bin/pydocstyle", 55 | "python.linting.pylintArgs": [ 56 | "--load-plugins", 57 | "pylint_django", 58 | "--errors-only", 59 | "--load-plugins=pylint_django", 60 | "--django-settings-module=/opt/netbox/netbox/netbox/netbox.settings", 61 | "--enable=W0602,W0611,W0612,W0613,W0614" 62 | ], 63 | "python.linting.pylintEnabled": true, 64 | "python.linting.pylintPath": "/opt/netbox/venv/bin/pylint", 65 | "python.linting.lintOnSave": true, 66 | "python.pythonPath": "/opt/netbox/venv/bin/python3", 67 | "python.terminal.activateEnvironment": true, 68 | "python.venvPath": "/opt/netbox/", 69 | "files.exclude": { 70 | "**/node_modules": true, 71 | "build": true, 72 | "dist": true, 73 | "*egg*": true 74 | } 75 | }, 76 | 77 | // Add the IDs of extensions you want installed when the container is created. 78 | "extensions": [ 79 | "DavidAnson.vscode-markdownlint", 80 | "GitHub.codespaces", 81 | "GitHub.copilot", 82 | "GitHub.copilot-labs", 83 | "GitHub.vscode-pull-request-github", 84 | "Gruntfuggly.todo-tree", 85 | "Tyriar.sort-lines", 86 | "aaron-bond.better-comments", 87 | "batisteo.vscode-django", 88 | "codezombiech.gitignore", 89 | "esbenp.prettier-vscode", 90 | "formulahendry.auto-rename-tag", 91 | "mintlify.document", 92 | "ms-python.python", 93 | "ms-python.vscode-pylance", 94 | "mutantdino.resourcemonitor", 95 | "paulomenezes.duplicated-code", 96 | "searKing.preview-vscode", 97 | "sourcery.sourcery" 98 | ] 99 | } 100 | }, 101 | 102 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 103 | // This can be used to network with other containers or the host. 104 | // "forwardPorts": [5000, 5432], 105 | 106 | // Use 'postCreateCommand' to run commands after the container is created. 107 | // "postCreateCommand": "pip install --user -r requirements-dev.txt", 108 | 109 | //"postAttachCommand": "source /opt/netbox/venv/bin/activate", 110 | 111 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 112 | "remoteUser": "vscode" 113 | 114 | } 115 | -------------------------------------------------------------------------------- /development/configuration/configuration.py: -------------------------------------------------------------------------------- 1 | #### 2 | ## We recommend to not edit this file. 3 | ## Create separate files to overwrite the settings. 4 | ## See `extra.py` as an example. 5 | #### 6 | 7 | from os import environ 8 | from os.path import abspath, dirname 9 | 10 | # For reference see https://netbox.readthedocs.io/en/stable/configuration/ 11 | # Based on https://github.com/netbox-community/netbox/blob/master/netbox/netbox/configuration.example.py 12 | 13 | 14 | # Read secret from file 15 | def _read_secret(secret_name, default=None): 16 | try: 17 | f = open("/run/secrets/" + secret_name, encoding="utf-8") 18 | except OSError: 19 | return default 20 | else: 21 | with f: 22 | return f.readline().strip() 23 | 24 | 25 | _BASE_DIR = dirname(dirname(abspath(__file__))) 26 | 27 | ######################### 28 | # # 29 | # Required settings # 30 | # # 31 | ######################### 32 | 33 | # This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write 34 | # access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. 35 | # 36 | # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] 37 | ALLOWED_HOSTS = environ.get("ALLOWED_HOSTS", "*").split(" ") 38 | 39 | # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: 40 | # https://docs.djangoproject.com/en/stable/ref/settings/#databases 41 | DATABASE = { 42 | "NAME": environ.get("DB_NAME", "netbox"), # Database name 43 | "USER": environ.get("DB_USER", ""), # PostgreSQL username 44 | "PASSWORD": _read_secret("db_password", environ.get("DB_PASSWORD", "")), 45 | # PostgreSQL password 46 | "HOST": environ.get("DB_HOST", "localhost"), # Database server 47 | "PORT": environ.get("DB_PORT", ""), # Database port (leave blank for default) 48 | "OPTIONS": {"sslmode": environ.get("DB_SSLMODE", "prefer")}, 49 | # Database connection SSLMODE 50 | "CONN_MAX_AGE": int(environ.get("DB_CONN_MAX_AGE", "300")), 51 | # Max database connection age 52 | "DISABLE_SERVER_SIDE_CURSORS": environ.get( 53 | "DB_DISABLE_SERVER_SIDE_CURSORS", 54 | "False", 55 | ).lower() 56 | == "true", 57 | # Disable the use of server-side cursors transaction pooling 58 | } 59 | 60 | # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate 61 | # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended 62 | # to use two separate database IDs. 63 | REDIS = { 64 | "tasks": { 65 | "HOST": environ.get("REDIS_HOST", "localhost"), 66 | "PORT": int(environ.get("REDIS_PORT", 6379)), 67 | "PASSWORD": _read_secret("redis_password", environ.get("REDIS_PASSWORD", "")), 68 | "DATABASE": int(environ.get("REDIS_DATABASE", 0)), 69 | "SSL": environ.get("REDIS_SSL", "False").lower() == "true", 70 | "INSECURE_SKIP_TLS_VERIFY": environ.get( 71 | "REDIS_INSECURE_SKIP_TLS_VERIFY", 72 | "False", 73 | ).lower() 74 | == "true", 75 | }, 76 | "caching": { 77 | "HOST": environ.get("REDIS_CACHE_HOST", environ.get("REDIS_HOST", "localhost")), 78 | "PORT": int(environ.get("REDIS_CACHE_PORT", environ.get("REDIS_PORT", 6379))), 79 | "PASSWORD": _read_secret( 80 | "redis_cache_password", 81 | environ.get("REDIS_CACHE_PASSWORD", environ.get("REDIS_PASSWORD", "")), 82 | ), 83 | "DATABASE": int(environ.get("REDIS_CACHE_DATABASE", 1)), 84 | "SSL": environ.get("REDIS_CACHE_SSL", environ.get("REDIS_SSL", "False")).lower() 85 | == "true", 86 | "INSECURE_SKIP_TLS_VERIFY": environ.get( 87 | "REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY", 88 | environ.get("REDIS_INSECURE_SKIP_TLS_VERIFY", "False"), 89 | ).lower() 90 | == "true", 91 | }, 92 | } 93 | 94 | # This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. 95 | # For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and 96 | # symbols. NetBox will not run without this defined. For more information, see 97 | # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY 98 | SECRET_KEY = _read_secret("secret_key", environ.get("SECRET_KEY", "")) 99 | 100 | DEVELOPER = True -------------------------------------------------------------------------------- /netbox_tunnels2/templates/netbox_tunnels2/tunnel.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load plugins %} 3 | {% block content %} 4 |
5 |
6 |
7 |
Tunnel
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
Name{{ object.name }}
Status{% badge object.get_status_display bg_color=object.get_status_color %}
Tunnel Type{{ object.tunnel_type|linkify|placeholder }}
Local Address{{ object.a_pub_address|linkify|placeholder }}
Remote Address{{ object.b_pub_address|linkify|placeholder }}
Pre Shared Key{{ object.psk|placeholder }}
Tenant{{ object.tenant|linkify|placeholder }}
39 |
40 |
41 | {% include 'inc/panels/custom_fields.html' %} 42 | {% plugin_left_page object %} 43 |
44 |
45 | {% include 'inc/panels/tags.html' %} 46 | {% include 'inc/panels/comments.html' %} 47 | {% plugin_right_page object %} 48 |
49 |
50 |
51 |
52 |
53 |
Endpoints
54 |
55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {% if object.side_a_assigned_object.device %} 69 | 72 | {% elif object.side_a_assigned_object.virtual_machine %} 73 | 76 | {% endif %} 77 | 80 | {% if object.side_b_assigned_object.device %} 81 | 84 | {% elif object.side_b_assigned_object.virtual_machine %} 85 | 88 | {% endif %} 89 | 92 | 93 | 94 |
Side A DeviceSide A Inside InterfaceSide B DeviceSide B Inside Interface
70 | {{ object.side_a_assigned_object.device|placeholder }} 71 | 74 | {{ object.side_a_assigned_object.virtual_machine|placeholder }} 75 | 78 | {{ object.side_a_assigned_object|placeholder }} 79 | 82 | {{ object.side_b_assigned_object.device|placeholder }} 83 | 86 | {{ object.side_b_assigned_object.virtual_machine|placeholder }} 87 | 90 | {{ object.side_b_assigned_object|placeholder }} 91 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | 103 | 104 | 105 |
106 |
107 |
108 |
109 | {% plugin_full_width_page object %} 110 |
111 |
112 | {% endblock content %} -------------------------------------------------------------------------------- /netbox_tunnels2/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.mixins import PermissionRequiredMixin 2 | from utilities.utils import count_related 3 | 4 | from netbox.views.generic import BulkDeleteView, BulkImportView, ObjectEditView, ObjectListView, ObjectView, ObjectDeleteView 5 | 6 | from . import forms, models, tables, filtersets 7 | 8 | class TunnelView(PermissionRequiredMixin, ObjectView): 9 | permission_required = "netbox_tunnels2.view_tunnel" 10 | queryset = models.PluginTunnel.objects.all() 11 | 12 | class ListTunnelView(PermissionRequiredMixin, ObjectListView): 13 | """View for listing all Tunnels.""" 14 | 15 | permission_required = "netbox_tunnels2.view_tunnel" 16 | model = models.PluginTunnel 17 | queryset = models.PluginTunnel.objects.all().order_by("id") 18 | filterset = filtersets.TunnelFilterSet 19 | filterset_form = forms.TunnelFilterForm 20 | table = tables.TunnelTable 21 | 22 | 23 | class EditTunnelView(PermissionRequiredMixin, ObjectEditView): 24 | """View for creating a new Tunnels.""" 25 | 26 | permission_required = "netbox_tunnels2.change_tunnel" 27 | model = models.PluginTunnel 28 | queryset = models.PluginTunnel.objects.all() 29 | form = forms.TunnelEditForm 30 | default_return_url = "plugins:netbox_tunnels2:tunnel_list" 31 | template_name = "netbox_tunnels2/tunnel_edit.html" 32 | 33 | class CreateTunnelView(PermissionRequiredMixin, ObjectEditView): 34 | """View for creating a new Tunnels.""" 35 | permission_required = "netbox_tunnels2.add_tunnel" 36 | model = models.PluginTunnel 37 | queryset = models.PluginTunnel.objects.all() 38 | #form = forms.TunnelAddForm 39 | form = forms.TunnelEditForm 40 | default_return_url = "plugins:netbox_tunnels2:tunnel_list" 41 | template_name = "netbox_tunnels2/tunnel_edit.html" 42 | 43 | class DeleteTunnelView(PermissionRequiredMixin,ObjectDeleteView): 44 | permission_required = "netbox_tunnels2.delete_tunnel" 45 | queryset = models.PluginTunnel.objects.all() 46 | default_return_url = "plugins:netbox_tunnels2:tunnel_list" 47 | 48 | class BulkDeleteTunnelView(PermissionRequiredMixin, BulkDeleteView): 49 | """View for deleting one or more Tunnels.""" 50 | 51 | permission_required = "netbox_tunnels2.delete_tunnel" 52 | queryset = models.PluginTunnel.objects.filter() 53 | table = tables.TunnelTable 54 | default_return_url = "plugins:netbox_tunnels2:tunnel_list" 55 | 56 | 57 | 58 | # 59 | # Tunnel Type 60 | # 61 | 62 | class TunnelTypeView(PermissionRequiredMixin, ObjectView): 63 | permission_required = "netbox_tunnels2.view_tunneltype" 64 | queryset = models.TunnelType.objects.all() 65 | def get_extra_context(self, request, instance): 66 | table = tables.TunnelTable(instance.tunnels.all()) 67 | table.configure(request) 68 | return { 69 | 'tunnel_table': table, 70 | } 71 | 72 | class ListTunnelTypeView(PermissionRequiredMixin, ObjectListView): 73 | """View for listing all Tunnels.""" 74 | permission_required = "netbox_tunnels2.view_tunneltype" 75 | model = models.TunnelType 76 | queryset = models.TunnelType.objects.annotate(tunnel_count=count_related(models.PluginTunnel,'tunnel_type')) 77 | table = tables.TunnelTypeTable 78 | 79 | 80 | class EditTunnelTypeView(PermissionRequiredMixin, ObjectEditView): 81 | """View for creating a new Tunnels.""" 82 | 83 | permission_required = "netbox_tunnels2.change_tunneltype" 84 | model = models.TunnelType 85 | queryset = models.TunnelType.objects.all() 86 | form = forms.TunnelTypeEditForm 87 | default_return_url = "plugins:netbox_tunnels2:tunneltype_list" 88 | 89 | class CreateTunnelTypeView(PermissionRequiredMixin, ObjectEditView): 90 | """View for creating a new Tunnels.""" 91 | 92 | permission_required = "netbox_tunnels2.add_tunneltype" 93 | model = models.TunnelType 94 | queryset = models.TunnelType.objects.all() 95 | form = forms.TunnelTypeEditForm 96 | default_return_url = "plugins:netbox_tunnels2:tunneltype_list" 97 | 98 | class DeleteTunnelTypeView(PermissionRequiredMixin,ObjectDeleteView): 99 | permission_required = "netbox_tunnels2.delete_tunneltype" 100 | queryset = models.TunnelType.objects.all() 101 | default_return_url = "plugins:netbox_tunnels2:tunneltype_list" 102 | 103 | 104 | class BulkDeleteTunnelTypeView(PermissionRequiredMixin, BulkDeleteView): 105 | """View for deleting one or more Tunnels.""" 106 | 107 | permission_required = "netbox_tunnels2.delete_tunneltype" 108 | queryset = models.TunnelType.objects.filter() 109 | table = tables.TunnelTable 110 | default_return_url = "plugins:netbox_tunnels2:tunneltype_list" 111 | -------------------------------------------------------------------------------- /netbox_tunnels2/templates/netbox_tunnels2/plugintunnel.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load plugins %} 3 | {% block content %} 4 |
5 |
6 |
7 |
Tunnel
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
Name{{ object.name }}
Status{% badge object.get_status_display bg_color=object.get_status_color %}
Tunnel Type{{ object.tunnel_type|linkify|placeholder }}
Local Address{{ object.a_pub_address|linkify|placeholder }}
Remote Address{{ object.b_pub_address|linkify|placeholder }}
Pre Shared Key{{ object.psk|placeholder }}
Tenant{{ object.tenant|linkify|placeholder }}
39 |
40 |
41 | {% include 'inc/panels/custom_fields.html' %} 42 | {% plugin_left_page object %} 43 |
44 |
45 | {% include 'inc/panels/tags.html' %} 46 | {% include 'inc/panels/comments.html' %} 47 | {% plugin_right_page object %} 48 |
49 |
50 |
51 |
52 |
53 |
Endpoints
54 |
55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {% if object.side_a_assigned_object.device %} 69 | 72 | {% elif object.side_a_assigned_object.virtual_machine %} 73 | 76 | {% endif %} 77 | 80 | {% if object.side_b_assigned_object.device %} 81 | 84 | {% elif object.side_b_assigned_object.virtual_machine %} 85 | 88 | {% endif %} 89 | 92 | 93 | 94 |
Side A DeviceSide A Inside InterfaceSide B DeviceSide B Inside Interface
70 | {{ object.side_a_assigned_object.device|placeholder }} 71 | 74 | {{ object.side_a_assigned_object.virtual_machine|placeholder }} 75 | 78 | {{ object.side_a_assigned_object|placeholder }} 79 | 82 | {{ object.side_b_assigned_object.device|placeholder }} 83 | 86 | {{ object.side_b_assigned_object.virtual_machine|placeholder }} 87 | 90 | {{ object.side_b_assigned_object|placeholder }} 91 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | 103 | 104 | 105 |
106 |
107 |
108 |
109 | {% plugin_full_width_page object %} 110 |
111 |
112 | {% endblock content %} -------------------------------------------------------------------------------- /netbox_tunnels2/tables.py: -------------------------------------------------------------------------------- 1 | """Tables for displaying list of configured Tunnels. 2 | 3 | (c) 2020 Justin Drew 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | import django_tables2 as tables 15 | from netbox.tables import NetBoxTable, ChoiceFieldColumn, columns 16 | from .models import PluginTunnel, TunnelType 17 | 18 | 19 | COL_SIDE_A_HOST_ASSIGNMENT = """ 20 | {% if record.side_a_assigned_object.device %} 21 | {{ record.side_a_assigned_object.device|placeholder }} 22 | {% else %} 23 | {{ record.side_a_assigned_object.virtual_machine|placeholder }} 24 | {% endif %} 25 | """ 26 | COL_SIDE_B_HOST_ASSIGNMENT = """ 27 | {% if record.side_b_assigned_object.device %} 28 | {{ record.side_b_assigned_object.device|placeholder }} 29 | {% else %} 30 | {{ record.side_b_assigned_object.virtual_machine|placeholder }} 31 | {% endif %} 32 | """ 33 | 34 | 35 | class RelatedTunnelTable(NetBoxTable): 36 | """Table for displaying Tunnel instances related to an interface.""" 37 | name = tables.Column( 38 | linkify=True 39 | ) 40 | tunnel_type = tables.Column(linkify=True) 41 | b_pub_address = tables.Column(linkify=True) 42 | status = ChoiceFieldColumn() 43 | tenant = tables.Column(linkify=True) 44 | 45 | side_b_host = tables.TemplateColumn( 46 | template_code=COL_SIDE_B_HOST_ASSIGNMENT, 47 | verbose_name="Side B Host", 48 | ) 49 | side_b_assigned_object = tables.Column( 50 | linkify=True, 51 | orderable=False, 52 | verbose_name="Side B Interface", 53 | ) 54 | 55 | class Meta(NetBoxTable.Meta): 56 | """Class to define what is used for interface_extend.html template to show 57 | tunnels related to an interface""" 58 | 59 | model = PluginTunnel 60 | fields = ( 61 | 'pk', 62 | 'id', 63 | "name", 64 | "tenant", 65 | "tunnel_type", 66 | "side_b_host", 67 | "side_b_assigned_object", 68 | "status", 69 | "b_pub_address" 70 | ) 71 | default_columns = ('name', 'status', 'tunnel_type', 72 | 'side_b_host', 'b_pub_address') 73 | 74 | 75 | class TunnelTable(RelatedTunnelTable): 76 | """Table for displaying configured Tunnel instances.""" 77 | a_pub_address = tables.Column(linkify=True) 78 | side_a_host = tables.TemplateColumn( 79 | template_code=COL_SIDE_A_HOST_ASSIGNMENT, 80 | verbose_name="Side A Host", 81 | ) 82 | side_a_assigned_object = tables.Column( 83 | linkify=True, 84 | orderable=False, 85 | verbose_name="Side A Interface", 86 | ) 87 | 88 | class Meta(RelatedTunnelTable.Meta): 89 | """Class to define what is used for tunnel_lists.html template to show configured tunnels.""" 90 | 91 | RelatedTunnelTable.Meta.fields += ( 92 | "side_a_host", 93 | "side_a_assigned_object", 94 | "a_pub_address", 95 | ) 96 | RelatedTunnelTable.Meta.default_columns += ('side_a_host', 'a_pub_address') 97 | 98 | 99 | class TunnelTypeTable(NetBoxTable): 100 | """Table for displaying configured Tunnel instances.""" 101 | name = tables.Column( 102 | linkify=True 103 | ) 104 | tunnel_count=columns.LinkedCountColumn( 105 | viewname='plugins:netbox_tunnels2:tunnel_list', 106 | url_params={'tunnel_type': 'pk'}, 107 | verbose_name='Tunnels' 108 | ) 109 | class Meta(NetBoxTable.Meta): 110 | """Class to define what is used for tunnel_lists.html template to show configured tunnels.""" 111 | 112 | model = TunnelType 113 | fields = ( 114 | 'pk', 115 | 'id', 116 | "name", 117 | "tunnel_count", 118 | "slug" 119 | ) 120 | default_columns = ('name','tunnel_count',) 121 | 122 | 123 | -------------------------------------------------------------------------------- /netbox_tunnels2/templates/netbox_tunnels2/tunnel_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_edit.html' %} 2 | {% load static %} 3 | {% load form_helpers %} 4 | 5 | {% block form %} 6 | {% render_errors form %} 7 |
8 |

Tunnel Details

9 | {% render_field form.name %} 10 | {% render_field form.status %} 11 | {% render_field form.tunnel_type %} 12 | {% render_field form.psk %} 13 | {% render_field form.tenant %} 14 |
15 |
16 |

Side A Host

17 | 45 |
46 |
47 | {% render_field form.side_a_device %} 48 | {% render_field form.side_a_device_interface %} 49 |
50 |
51 | {% render_field form.side_a_virtual_machine %} 52 | {% render_field form.side_a_vm_interface %} 53 |
54 | {% render_field form.a_pub_VRF %} 55 | {% render_field form.a_pub_address %} 56 |
57 |
58 | 59 |
60 |

Side B Host

61 | 89 |
90 |
91 | {% render_field form.side_b_device %} 92 | {% render_field form.side_b_device_interface %} 93 |
94 |
95 | {% render_field form.side_b_virtual_machine %} 96 | {% render_field form.side_b_vm_interface %} 97 |
98 | {% render_field form.b_pub_VRF %} 99 | {% render_field form.b_pub_address %} 100 |
101 |
102 | 103 | {% if form.custom_fields %} 104 |
105 |

Custom Fields

106 | {% render_custom_fields form %} 107 |
108 | {% endif %} 109 | 110 |
111 |

Comments

112 | {% render_field form.comments %} 113 |
114 | {% endblock %} -------------------------------------------------------------------------------- /netbox_tunnels2/templates/netbox_tunnels2/plugintunnel_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object_edit.html' %} 2 | {% load static %} 3 | {% load form_helpers %} 4 | 5 | {% block form %} 6 | {% render_errors form %} 7 |
8 |

Tunnel Details

9 | {% render_field form.name %} 10 | {% render_field form.status %} 11 | {% render_field form.tunnel_type %} 12 | {% render_field form.psk %} 13 | {% render_field form.tenant %} 14 |
15 |
16 |

Side A Host

17 | 45 |
46 |
47 | {% render_field form.side_a_device %} 48 | {% render_field form.side_a_device_interface %} 49 |
50 |
51 | {% render_field form.side_a_virtual_machine %} 52 | {% render_field form.side_a_vm_interface %} 53 |
54 | {% render_field form.a_pub_VRF %} 55 | {% render_field form.a_pub_address %} 56 |
57 |
58 | 59 |
60 |

Side B Host

61 | 89 |
90 |
91 | {% render_field form.side_b_device %} 92 | {% render_field form.side_b_device_interface %} 93 |
94 |
95 | {% render_field form.side_b_virtual_machine %} 96 | {% render_field form.side_b_vm_interface %} 97 |
98 | {% render_field form.b_pub_VRF %} 99 | {% render_field form.b_pub_address %} 100 |
101 |
102 | 103 | {% if form.custom_fields %} 104 |
105 |

Custom Fields

106 | {% render_custom_fields form %} 107 |
108 | {% endif %} 109 | 110 |
111 |

Comments

112 | {% render_field form.comments %} 113 |
114 | {% endblock %} -------------------------------------------------------------------------------- /netbox_tunnels2/models.py: -------------------------------------------------------------------------------- 1 | """Tunnel Django model. 2 | 3 | (c) 2020 Justin Drew 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | from django.db import models 16 | from django.urls import reverse 17 | from django.contrib.contenttypes.models import ContentType 18 | from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation 19 | 20 | from netbox.models import NetBoxModel, OrganizationalModel 21 | from utilities.querysets import RestrictedQuerySet 22 | from tenancy.models import Tenant 23 | from .constants import TUNNEL_INTERFACE_ASSIGNMENT_MODELS 24 | 25 | from dcim.models import Device, Interface 26 | 27 | 28 | 29 | 30 | from utilities.choices import ChoiceSet 31 | 32 | 33 | class TunnelStatusChoices(ChoiceSet): 34 | CHOICES = [ 35 | ('pending-config', 'Pending Configuration', 'orange'), 36 | ('configured', 'Configured', 'green'), 37 | ('pending-deletion', 'Pending Deletion', 'red'), 38 | ] 39 | 40 | class TunnelType(OrganizationalModel): 41 | name = models.CharField( 42 | max_length=100, 43 | unique=True 44 | ) 45 | slug = models.SlugField( 46 | max_length=100, 47 | unique=True 48 | ) 49 | objects = RestrictedQuerySet.as_manager() 50 | 51 | 52 | class Meta: 53 | ordering = ['name'] 54 | def __str__(self): 55 | return self.name 56 | 57 | def get_absolute_url(self): 58 | return reverse('plugins:netbox_tunnels2:tunneltype', args=[self.pk]) 59 | 60 | 61 | class PluginTunnel(NetBoxModel): 62 | """Tunnel model.""" 63 | name = models.CharField(max_length=64) 64 | status = models.CharField(max_length=30, choices=TunnelStatusChoices, default='pending-config') 65 | tunnel_type = models.ForeignKey( 66 | to='TunnelType', 67 | on_delete=models.PROTECT, 68 | related_name='tunnels' 69 | ) 70 | side_a_assigned_object_type = models.ForeignKey( 71 | to=ContentType, 72 | limit_choices_to=TUNNEL_INTERFACE_ASSIGNMENT_MODELS, 73 | on_delete=models.PROTECT, 74 | null=True, 75 | blank=True, 76 | related_name="side_a_assigned_object_type" 77 | ) 78 | side_a_assigned_object_id = models.PositiveBigIntegerField( 79 | null=True, 80 | blank=True 81 | ) 82 | side_a_assigned_object = GenericForeignKey( 83 | ct_field="side_a_assigned_object_type", 84 | fk_field="side_a_assigned_object_id" 85 | ) 86 | side_b_assigned_object_type = models.ForeignKey( 87 | to=ContentType, 88 | limit_choices_to=TUNNEL_INTERFACE_ASSIGNMENT_MODELS, 89 | on_delete=models.PROTECT, 90 | null=True, 91 | blank=True, 92 | related_name="side_b_assigned_object_type" 93 | ) 94 | side_b_assigned_object_id = models.PositiveBigIntegerField( 95 | null=True, 96 | blank=True 97 | ) 98 | side_b_assigned_object = GenericForeignKey( 99 | ct_field="side_b_assigned_object_type", 100 | fk_field="side_b_assigned_object_id" 101 | ) 102 | a_pub_address = models.ForeignKey( 103 | to='ipam.IPAddress', 104 | on_delete=models.PROTECT, 105 | related_name='tunnel_a_pub_address', 106 | verbose_name="Side A Public Address" 107 | ) 108 | b_pub_address = models.ForeignKey( 109 | to='ipam.IPAddress', 110 | on_delete=models.PROTECT, 111 | related_name='tunnel_b_pub_address', 112 | verbose_name="Side B Public Address", 113 | null=True, 114 | blank=True 115 | ) 116 | description = models.CharField( 117 | max_length=200, 118 | blank=True 119 | ) 120 | tenant = models.ForeignKey( 121 | to=Tenant, 122 | on_delete=models.RESTRICT, 123 | null=True, 124 | blank=True 125 | ) 126 | psk = models.CharField(verbose_name="Pre-shared Key", max_length=100, blank=True) 127 | comments = models.TextField(blank=True) 128 | 129 | class Meta: 130 | """Class to define what will be used to set order based on. Will be using the unique tunnel ID for this purpose.""" 131 | verbose_name_plural='Tunnels' 132 | def __str__(self): 133 | """Class to define what identifies the Tunnel object. Will be using name for this.""" 134 | return self.name 135 | def get_absolute_url(self): 136 | return reverse('plugins:netbox_tunnels2:tunnel', args=[self.pk]) 137 | def get_status_color(self): 138 | return TunnelStatusChoices.colors.get(self.status) 139 | 140 | 141 | GenericRelation( 142 | to=PluginTunnel, 143 | content_type_field="side_a_assigned_object_type", 144 | object_id_field="side_a_assigned_object_id", 145 | related_query_name="interface", 146 | ).contribute_to_class(Interface, "tunnelassignments") 147 | GenericRelation( 148 | to=PluginTunnel, 149 | content_type_field="side_a_assigned_object_type", 150 | object_id_field="side_a_assigned_object_id", 151 | related_query_name="device", 152 | ).contribute_to_class(Device, "tunnels") 153 | GenericRelation( 154 | to=PluginTunnel, 155 | content_type_field="side_b_assigned_object_type", 156 | object_id_field="side_b_assigned_object_id", 157 | related_query_name="interface", 158 | ).contribute_to_class(Interface, "tunnelassignments_b") 159 | GenericRelation( 160 | to=PluginTunnel, 161 | content_type_field="side_b_assigned_object_type", 162 | object_id_field="side_b_assigned_object_id", 163 | related_query_name="device", 164 | ).contribute_to_class(Device, "tunnels_b") -------------------------------------------------------------------------------- /netbox_tunnels2/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from django.core.exceptions import ObjectDoesNotExist 4 | from django.contrib.contenttypes.models import ContentType 5 | from ..constants import TUNNEL_INTERFACE_ASSIGNMENT_MODELS 6 | from netbox.api.fields import ContentTypeField 7 | 8 | from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer 9 | from ..models import PluginTunnel, TunnelType 10 | 11 | 12 | from netbox.constants import NESTED_SERIALIZER_PREFIX 13 | from utilities.api import get_serializer_for_model 14 | from drf_spectacular.utils import extend_schema_field 15 | 16 | class NestedTunnelSerializer(WritableNestedSerializer): 17 | url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_tunnels2-api:tunnel-detail') 18 | 19 | class Meta: 20 | model = PluginTunnel 21 | fields = ( 22 | 'id', 23 | 'url', 24 | 'display', 25 | 'name', 26 | ) 27 | 28 | class TunnelSerializer(NetBoxModelSerializer): 29 | url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_tunnels2-api:tunnel-detail') 30 | side_a_assigned_object_type = ContentTypeField( 31 | queryset=ContentType.objects.filter(TUNNEL_INTERFACE_ASSIGNMENT_MODELS), 32 | allow_null=True 33 | ) 34 | side_b_assigned_object_type = ContentTypeField( 35 | queryset=ContentType.objects.filter(TUNNEL_INTERFACE_ASSIGNMENT_MODELS), 36 | allow_null=True 37 | ) 38 | side_a_assigned_object = serializers.SerializerMethodField(read_only=True) 39 | side_b_assigned_object = serializers.SerializerMethodField(read_only=True) 40 | class Meta: 41 | model = PluginTunnel 42 | fields = ( 43 | 'id', 44 | 'url', 45 | 'display', 46 | 'name', 47 | 'tenant', 48 | 'a_pub_address', 49 | 'b_pub_address', 50 | 'side_a_assigned_object_type', 51 | 'side_a_assigned_object_id', 52 | 'side_a_assigned_object', 53 | 'side_b_assigned_object_type', 54 | 'side_b_assigned_object_id', 55 | 'side_b_assigned_object', 56 | 'status', 57 | 'tunnel_type', 58 | 'psk', 59 | 'tags', 60 | 'custom_fields', 61 | 'created', 62 | 'last_updated', 63 | ) 64 | @extend_schema_field(serializers.DictField) 65 | def get_side_a_assigned_object(self, obj): 66 | if obj.side_a_assigned_object is None: 67 | return None 68 | serializer = get_serializer_for_model( 69 | obj.side_a_assigned_object, 70 | prefix=NESTED_SERIALIZER_PREFIX, 71 | ) 72 | context = {"request": self.context["request"]} 73 | return serializer(obj.side_a_assigned_object, context=context).data 74 | def get_side_a_inner_ip(self, obj): 75 | if obj.side_a_assigned_object is None: 76 | return None 77 | serializer = get_serializer_for_model( 78 | obj.side_a_assigned_object, 79 | prefix=NESTED_SERIALIZER_PREFIX, 80 | ) 81 | context = {"request": self.context["request"]} 82 | return serializer(obj.side_a_assigned_object, context=context).data 83 | def get_side_b_assigned_object(self, obj): 84 | if obj.side_b_assigned_object is None: 85 | return None 86 | serializer = get_serializer_for_model( 87 | obj.side_b_assigned_object, 88 | prefix=NESTED_SERIALIZER_PREFIX, 89 | ) 90 | context = {"request": self.context["request"]} 91 | return serializer(obj.side_b_assigned_object, context=context).data 92 | 93 | def validate(self, data): 94 | """ 95 | Validate the Tunnel django model's inputs before allowing it to update the instance. 96 | - Check that the GFK object is valid. 97 | - TODO: Check to see if the interface is assigned to another Tunnel 98 | """ 99 | error_message = {} 100 | 101 | # Check that the GFK object is valid. 102 | if "side_a_assigned_object_type" in data and "side_a_assigned_object_id" in data: 103 | # TODO: This can removed after https://github.com/netbox-community/netbox/issues/10221 is fixed. 104 | try: 105 | side_a_assigned_object = data[ # noqa: F841 106 | "side_a_assigned_object_type" 107 | ].get_object_for_this_type( 108 | id=data["side_a_assigned_object_id"], 109 | ) 110 | except ObjectDoesNotExist: 111 | # Sets a standard error message for invalid GFK 112 | error_message_invalid_gfk = f"Invalid side_a_assigned_object {data['side_a_assigned_object_type']} ID {data['side_a_assigned_object_id']}" 113 | error_message["side_a_assigned_object_type"] = [error_message_invalid_gfk] 114 | error_message["side_a_assigned_object_id"] = [error_message_invalid_gfk] 115 | if "side_b_assigned_object_type" in data and "side_b_assigned_object_id" in data: 116 | # TODO: This can removed after https://github.com/netbox-community/netbox/issues/10221 is fixed. 117 | try: 118 | side_b_assigned_object = data[ # noqa: F841 119 | "side_a_assigned_object_type" 120 | ].get_object_for_this_type( 121 | id=data["side_b_assigned_object_id"], 122 | ) 123 | except ObjectDoesNotExist: 124 | # Sets a standard error message for invalid GFK 125 | error_message_invalid_gfk = f"Invalid side_b_assigned_object {data['side_b_assigned_object_type']} ID {data['side_b_assigned_object_id']}" 126 | error_message["side_b_assigned_object_type"] = [error_message_invalid_gfk] 127 | error_message["side_b_assigned_object_id"] = [error_message_invalid_gfk] 128 | 129 | if data["side_a_assigned_object_type"].model == "interface": 130 | interface_host = ( 131 | data["side_a_assigned_object_type"] 132 | .get_object_for_this_type(id=data["side_a_assigned_object_id"]) 133 | .device 134 | ) 135 | else: 136 | a_interface_host = None 137 | if data["side_b_assigned_object_type"].model == "interface": 138 | interface_host = ( 139 | data["side_b_assigned_object_type"] 140 | .get_object_for_this_type(id=data["side_b_assigned_object_id"]) 141 | .device 142 | ) 143 | else: 144 | b_interface_host = None 145 | 146 | if error_message: 147 | raise serializers.ValidationError(error_message) 148 | 149 | return super().validate(data) 150 | 151 | # 152 | # Tunnel Type 153 | # 154 | 155 | class NestedTunnelTypeSerializer(WritableNestedSerializer): 156 | url = serializers.HyperlinkedIdentityField(view_name='plugins:netbox_tunnels2:tunneltype') 157 | 158 | class Meta: 159 | model = TunnelType 160 | fields = ('id', 'url', 'display', 'name') 161 | 162 | class TunnelTypeSerializer(NetBoxModelSerializer): 163 | url = serializers.HyperlinkedIdentityField(view_name='plugins:netbox_tunnels2:tunneltype') 164 | 165 | class Meta: 166 | model = TunnelType 167 | fields = ('id', 'url', 'display', 'name',) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2023] [Robert Lynch] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /netbox_tunnels2/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ( 2 | CharField, 3 | ChoiceField, 4 | MultipleChoiceField, 5 | ChoiceField, 6 | PasswordInput, 7 | ModelChoiceField 8 | ) 9 | 10 | from utilities.forms.fields import ( 11 | DynamicModelChoiceField, 12 | SlugField, 13 | DynamicModelMultipleChoiceField 14 | ) 15 | 16 | from dcim.models import Interface, Device 17 | from ipam.models import IPAddress, VRF 18 | from virtualization.models import VMInterface, VirtualMachine 19 | from ipam.formfields import IPNetworkFormField 20 | from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, ValidationError 21 | from django.contrib.contenttypes.models import ContentType 22 | 23 | from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm 24 | from tenancy.models import Tenant 25 | 26 | from .models import PluginTunnel, TunnelStatusChoices, TunnelType 27 | 28 | 29 | class TunnelEditForm(NetBoxModelForm): 30 | """Form for creating a new tunnel.""" 31 | 32 | name = CharField(required=True, label="Name", help_text="Name of tunnel") 33 | 34 | status = ChoiceField( 35 | choices=TunnelStatusChoices, 36 | required=False 37 | ) 38 | 39 | tunnel_type = ModelChoiceField( 40 | queryset=TunnelType.objects.all(), 41 | required=True 42 | ) 43 | a_pub_VRF = DynamicModelChoiceField( 44 | label='Side A Address VRF', 45 | queryset=VRF.objects.all(), 46 | required=False, 47 | selector=True, 48 | query_params={ 49 | "tenant_id": "$tenant", 50 | }, 51 | ) 52 | a_pub_address = DynamicModelChoiceField( 53 | label='Side A Public IP Address', 54 | queryset=IPAddress.objects.all(), 55 | query_params={ 56 | 'vrf_id': '$a_pub_VRF', 57 | 'device_id': '$side_a_device', 58 | 'virtual_machine_id': '$side_a_virtual_machine' 59 | } 60 | ) 61 | b_pub_VRF = DynamicModelChoiceField( 62 | label='Side B Address VRF', 63 | queryset=VRF.objects.all(), 64 | selector=True, 65 | required=False 66 | ) 67 | b_pub_address = DynamicModelChoiceField( 68 | label='Side B Public IP Address', 69 | queryset=IPAddress.objects.all(), 70 | query_params={ 71 | 'vrf_id': '$b_pub_VRF', 72 | 'device_id': '$side_b_device', 73 | 'virtual_machine_id': '$side_b_virtual_machine' 74 | }, 75 | required=False 76 | ) 77 | psk = CharField(required=False, label="Pre-shared Key", help_text="Pre-shared key") 78 | 79 | side_a_virtual_machine = DynamicModelChoiceField( 80 | queryset=VirtualMachine.objects.all(), 81 | label="Site A VM", 82 | required=False, 83 | selector=True, 84 | query_params={ 85 | "tenant_id": "$tenant", 86 | }, 87 | ) 88 | side_a_device = DynamicModelChoiceField( 89 | queryset=Device.objects.all(), 90 | label="Site A Device", 91 | required=False, 92 | selector=True, 93 | query_params={ 94 | "tenant_id": "$tenant", 95 | }, 96 | ) 97 | side_a_device_interface = DynamicModelChoiceField( 98 | queryset=Interface.objects.all(), 99 | label="Site A Interface", 100 | required=False, 101 | query_params={ 102 | "device_id": "$side_a_device", 103 | }, 104 | ) 105 | side_a_vm_interface = DynamicModelChoiceField( 106 | queryset=VMInterface.objects.all(), 107 | label="Site A Interface", 108 | required=False, 109 | query_params={ 110 | "virtual_machine_id": "$side_a_virtual_machine", 111 | }, 112 | ) 113 | side_b_virtual_machine = DynamicModelChoiceField( 114 | queryset=VirtualMachine.objects.all(), 115 | label="Site B VM", 116 | required=False, 117 | selector=True, 118 | query_params={ 119 | "tenant_id": "$tenant", 120 | }, 121 | ) 122 | side_b_device = DynamicModelChoiceField( 123 | queryset=Device.objects.all(), 124 | label="Site B Device", 125 | required=False, 126 | selector=True, 127 | ) 128 | side_b_device_interface = DynamicModelChoiceField( 129 | queryset=Interface.objects.all(), 130 | label="Site B Interface", 131 | required=False, 132 | query_params={ 133 | "device_id": "$side_b_device", 134 | }, 135 | ) 136 | side_b_vm_interface = DynamicModelChoiceField( 137 | queryset=VMInterface.objects.all(), 138 | label="Site B Interface", 139 | required=False, 140 | query_params={ 141 | "virtual_machine_id": "$side_b_virtual_machine", 142 | }, 143 | ) 144 | 145 | def __init__(self, *args, **kwargs): 146 | 147 | # Initialize helper selectors 148 | instance = kwargs.get("instance") 149 | initial = kwargs.get("initial", {}).copy() 150 | if instance: 151 | if type(instance.side_a_assigned_object) is Interface: 152 | initial["side_a_device_interface"] = instance.side_a_assigned_object 153 | initial["side_a_device"] = instance.side_a_assigned_object.device 154 | elif type(instance.side_a_assigned_object) is VMInterface: 155 | initial["side_a_vm_interface"] = instance.side_a_assigned_object 156 | initial["side_a_virtual_machine"] = instance.side_a_assigned_object.virtual_machine 157 | if type(instance.side_b_assigned_object) is Interface: 158 | initial["side_b_device_interface"] = instance.side_b_assigned_object 159 | initial["side_b_device"] = instance.side_b_assigned_object.device 160 | elif type(instance.side_b_assigned_object) is VMInterface: 161 | initial["side_b_vm_interface"] = instance.side_b_assigned_object 162 | initial["side_b_virtual_machine"] = instance.side_b_assigned_object.virtual_machine 163 | if hasattr(instance, 'a_pub_address') and type(instance.a_pub_address) is IPAddress: 164 | initial["a_pub_VRF"] = instance.a_pub_address.vrf 165 | if hasattr(instance, 'b_pub_address') and type(instance.b_pub_address) is IPAddress: 166 | initial["b_pub_VRF"] = instance.b_pub_address.vrf 167 | kwargs["initial"] = initial 168 | 169 | super().__init__(*args, **kwargs) 170 | 171 | class Meta: 172 | """Class to define what is used to create a new network tunnel.""" 173 | 174 | model = PluginTunnel 175 | fields = ( 176 | "name", 177 | "status", 178 | "tunnel_type", 179 | "a_pub_address", 180 | "b_pub_address", 181 | "psk", 182 | 'comments', 183 | 'tags', 184 | 'tenant', 185 | ) 186 | 187 | def clean(self): 188 | super().clean() 189 | error_message = {} 190 | name = self.cleaned_data['name'] 191 | status = self.cleaned_data['status'] 192 | tunnel_type = self.cleaned_data['tunnel_type'] 193 | a_pub_address = self.cleaned_data['a_pub_address'] 194 | b_pub_address = self.cleaned_data['b_pub_address'] 195 | psk = self.cleaned_data['psk'] 196 | side_a_device_interface = self.cleaned_data['side_a_device_interface'] 197 | side_a_vm_interface = self.cleaned_data['side_a_vm_interface'] 198 | side_b_device_interface = self.cleaned_data['side_b_device_interface'] 199 | side_b_vm_interface = self.cleaned_data['side_b_vm_interface'] 200 | 201 | # Check that interface or vminterface are set 202 | # if either the Side A or Side B interfaces are assigned 203 | if side_a_device_interface: 204 | side_a_assigned_object = side_a_device_interface 205 | side_a_assigned_object_type = "interface" 206 | side_a_host_type = "device" 207 | side_a_host = Interface.objects.get(pk=side_a_assigned_object.pk).device 208 | side_a_assigned_object_id = Interface.objects.get(pk=side_a_assigned_object.pk).pk 209 | if side_a_vm_interface: 210 | side_a_assigned_object = side_a_vm_interface 211 | side_a_assigned_object_type = "interfaces" 212 | side_a_host_type = "virtual_machine" 213 | side_a_host = VMInterface.objects.get(pk=side_a_assigned_object.pk).virtual_machine 214 | side_a_assigned_object_id = VMInterface.objects.get(pk=side_a_assigned_object.pk).pk 215 | if side_a_device_interface or side_a_vm_interface: 216 | side_a_assigned_object_type_id = ContentType.objects.get_for_model(side_a_assigned_object, ).pk 217 | if side_b_device_interface: 218 | side_b_assigned_object = side_b_device_interface 219 | side_b_assigned_object_type = "interface" 220 | side_b_host_type = "device" 221 | side_b_host = Interface.objects.get(pk=side_b_assigned_object.pk).device 222 | side_b_assigned_object_id = Interface.objects.get(pk=side_b_assigned_object.pk).pk 223 | if side_b_vm_interface: 224 | side_b_assigned_object = side_b_vm_interface 225 | side_b_assigned_object_type = "interfaces" 226 | side_b_host_type = "virtual_machine" 227 | side_b_host = VMInterface.objects.get(pk=side_b_assigned_object.pk).virtual_machine 228 | side_b_assigned_object_id = VMInterface.objects.get(pk=side_b_assigned_object.pk).pk 229 | if side_b_device_interface or side_b_vm_interface: 230 | side_b_assigned_object_type_id = ContentType.objects.get_for_model(side_b_assigned_object, ).pk 231 | 232 | def save(self, *args, **kwargs): 233 | # Set assigned object 234 | self.instance.side_a_assigned_object = ( 235 | self.cleaned_data.get("side_a_device_interface") 236 | or self.cleaned_data.get("side_a_vm_interface") 237 | ) 238 | self.instance.side_b_assigned_object = ( 239 | self.cleaned_data.get("side_b_device_interface") 240 | or self.cleaned_data.get("side_b_vm_interface") 241 | ) 242 | return super().save(*args, **kwargs) 243 | 244 | 245 | class TunnelAddForm(TunnelEditForm): 246 | tunnel_type = ModelChoiceField( 247 | queryset=TunnelType.objects.all(), 248 | required=True 249 | ) 250 | remote_VRF = DynamicModelChoiceField(label='Remote Address VRF', queryset=VRF.objects.all(), required=False) 251 | b_pub_address = IPNetworkFormField(required=True) 252 | 253 | fields = ( 254 | "name", 255 | "status", 256 | "tunnel_type", 257 | "a_pub_VRF", 258 | "a_pub_address", 259 | "b_pub_VRF", 260 | "b_pub_address", 261 | "psk", 262 | "comments", 263 | "tags", 264 | "tenant", 265 | ) 266 | field_order = ["name", 267 | "status", 268 | "tunnel_type", 269 | "a_pub_VRF", 270 | "a_pub_address", 271 | "b_pub_VRF", 272 | "b_pub_address", 273 | "psk", 274 | "tenant", 275 | "comments", 276 | "tags"] 277 | 278 | def clean_b_pub_address(self): 279 | 280 | if self.data['remote_VRF'] != '': 281 | vrf = VRF.objects.get(id=self.data['remote_VRF']) 282 | else: 283 | vrf = 0 284 | try: 285 | if vrf == 0: 286 | ip = IPAddress.objects.get(address=str(self.cleaned_data['b_pub_address'])) 287 | else: 288 | ip = IPAddress.objects.get(address=str(self.cleaned_data['b_pub_address']), vrf=vrf) 289 | except MultipleObjectsReturned: 290 | if vrf == 0: 291 | ip = IPAddress.objects.filter(address=str(self.cleaned_data['b_pub_address'])).first() 292 | else: 293 | ip = IPAddress.objects.filter(address=str(self.cleaned_data['b_pub_address']), vrf=vrf).first() 294 | except ObjectDoesNotExist: 295 | if vrf == 0: 296 | ip = IPAddress.objects.create(address=str(self.cleaned_data['b_pub_address'])) 297 | else: 298 | ip = IPAddress.objects.create(address=str(self.cleaned_data['b_pub_address']), vrf=vrf) 299 | self.cleaned_data['b_pub_address'] = ip 300 | return self.cleaned_data['b_pub_address'] 301 | 302 | 303 | class TunnelFilterForm(NetBoxModelFilterSetForm): 304 | """Form for filtering Tunnel instances.""" 305 | model = PluginTunnel 306 | status = MultipleChoiceField(choices=TunnelStatusChoices, required=False) 307 | tenant_id = DynamicModelMultipleChoiceField( 308 | required=False, queryset=Tenant.objects.all(), label="Tenant" 309 | ) 310 | tunnel_type = ModelChoiceField( 311 | queryset=TunnelType.objects.all(), 312 | required=False 313 | ) 314 | a_pub_address = DynamicModelMultipleChoiceField( 315 | queryset=IPAddress.objects.all(), 316 | required=False, 317 | label="Local Address", 318 | ) 319 | b_pub_address = DynamicModelMultipleChoiceField( 320 | queryset=IPAddress.objects.all(), 321 | required=False, 322 | label="Remote Address", 323 | ) 324 | 325 | class Meta: 326 | """Class to define what is used for filtering tunnels with the search box.""" 327 | model = PluginTunnel 328 | fields = ( 329 | "a_pub_address", 330 | "b_pub_address", 331 | "psk", 332 | "tunnel_type", 333 | "tenant_id", 334 | ) 335 | 336 | 337 | # 338 | # Tunnel Type 339 | # 340 | class TunnelTypeEditForm(NetBoxModelForm): 341 | """Form for creating a new tunnel.""" 342 | slug = SlugField() 343 | 344 | class Meta: 345 | """Class to define what is used to create a new network tunnel.""" 346 | model = TunnelType 347 | fields = ('name', 'slug') 348 | -------------------------------------------------------------------------------- /.devcontainer/configuration/configuration.py: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/netbox-community/netbox-docker/blob/release/configuration/configuration.py 2 | import re 3 | from os import environ 4 | from os.path import abspath, dirname, join 5 | 6 | 7 | # Read secret from file 8 | def _read_secret(secret_name, default=None): 9 | try: 10 | f = open("/run/secrets/" + secret_name, encoding="utf-8") 11 | except OSError: 12 | return default 13 | else: 14 | with f: 15 | return f.readline().strip() 16 | 17 | 18 | _BASE_DIR = dirname(dirname(abspath(__file__))) 19 | 20 | ######################### 21 | # # 22 | # Required settings # 23 | # # 24 | ######################### 25 | 26 | # This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write 27 | # access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. 28 | # 29 | # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] 30 | ALLOWED_HOSTS = environ.get("ALLOWED_HOSTS", "*").split(" ") 31 | 32 | # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: 33 | # https://docs.djangoproject.com/en/stable/ref/settings/#databases 34 | DATABASE = { 35 | "NAME": environ.get("DB_NAME", "netbox"), # Database name 36 | "USER": environ.get("DB_USER", ""), # PostgreSQL username 37 | "PASSWORD": _read_secret("db_password", environ.get("DB_PASSWORD", "")), 38 | # PostgreSQL password 39 | "HOST": environ.get("DB_HOST", "localhost"), # Database server 40 | "PORT": environ.get("DB_PORT", ""), # Database port (leave blank for default) 41 | "OPTIONS": {"sslmode": environ.get("DB_SSLMODE", "prefer")}, 42 | # Database connection SSLMODE 43 | "CONN_MAX_AGE": int(environ.get("DB_CONN_MAX_AGE", "300")), 44 | # Max database connection age 45 | "DISABLE_SERVER_SIDE_CURSORS": environ.get( 46 | "DB_DISABLE_SERVER_SIDE_CURSORS", 47 | "False", 48 | ).lower() 49 | == "true", 50 | # Disable the use of server-side cursors transaction pooling 51 | } 52 | 53 | # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate 54 | # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended 55 | # to use two separate database IDs. 56 | REDIS = { 57 | "tasks": { 58 | "HOST": environ.get("REDIS_HOST", "localhost"), 59 | "PORT": int(environ.get("REDIS_PORT", 6379)), 60 | "PASSWORD": _read_secret("redis_password", environ.get("REDIS_PASSWORD", "")), 61 | "DATABASE": int(environ.get("REDIS_DATABASE", 0)), 62 | "SSL": environ.get("REDIS_SSL", "False").lower() == "true", 63 | "INSECURE_SKIP_TLS_VERIFY": environ.get( 64 | "REDIS_INSECURE_SKIP_TLS_VERIFY", 65 | "False", 66 | ).lower() 67 | == "true", 68 | }, 69 | "caching": { 70 | "HOST": environ.get("REDIS_CACHE_HOST", environ.get("REDIS_HOST", "localhost")), 71 | "PORT": int(environ.get("REDIS_CACHE_PORT", environ.get("REDIS_PORT", 6379))), 72 | "PASSWORD": _read_secret( 73 | "redis_cache_password", 74 | environ.get("REDIS_CACHE_PASSWORD", environ.get("REDIS_PASSWORD", "")), 75 | ), 76 | "DATABASE": int(environ.get("REDIS_CACHE_DATABASE", 1)), 77 | "SSL": environ.get("REDIS_CACHE_SSL", environ.get("REDIS_SSL", "False")).lower() 78 | == "true", 79 | "INSECURE_SKIP_TLS_VERIFY": environ.get( 80 | "REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY", 81 | environ.get("REDIS_INSECURE_SKIP_TLS_VERIFY", "False"), 82 | ).lower() 83 | == "true", 84 | }, 85 | } 86 | 87 | # This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. 88 | # For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and 89 | # symbols. NetBox will not run without this defined. For more information, see 90 | # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY 91 | SECRET_KEY = _read_secret("secret_key", environ.get("SECRET_KEY", "")) 92 | 93 | 94 | ######################### 95 | # # 96 | # Optional settings # 97 | # # 98 | ######################### 99 | 100 | # Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of 101 | # application errors (assuming correct email settings are provided). 102 | ADMINS = [ 103 | # ['John Doe', 'jdoe@example.com'], 104 | ] 105 | 106 | # URL schemes that are allowed within links in NetBox 107 | ALLOWED_URL_SCHEMES = ( 108 | "file", 109 | "ftp", 110 | "ftps", 111 | "http", 112 | "https", 113 | "irc", 114 | "mailto", 115 | "sftp", 116 | "ssh", 117 | "tel", 118 | "telnet", 119 | "tftp", 120 | "vnc", 121 | "xmpp", 122 | ) 123 | 124 | # Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same 125 | # content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. 126 | BANNER_TOP = environ.get("BANNER_TOP", "") 127 | BANNER_BOTTOM = environ.get("BANNER_BOTTOM", "") 128 | 129 | # Text to include on the login page above the login form. HTML is allowed. 130 | BANNER_LOGIN = environ.get("BANNER_LOGIN", "") 131 | 132 | # Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set: 133 | # BASE_PATH = 'netbox/' 134 | BASE_PATH = environ.get("BASE_PATH", "") 135 | 136 | # Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90) 137 | CHANGELOG_RETENTION = int(environ.get("CHANGELOG_RETENTION", 90)) 138 | 139 | # API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be 140 | # allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or 141 | # CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers 142 | CORS_ORIGIN_ALLOW_ALL = environ.get("CORS_ORIGIN_ALLOW_ALL", "False").lower() == "true" 143 | CORS_ORIGIN_WHITELIST = list( 144 | filter(None, environ.get("CORS_ORIGIN_WHITELIST", "https://localhost").split(" ")), 145 | ) 146 | CORS_ORIGIN_REGEX_WHITELIST = [ 147 | re.compile(r) 148 | for r in list( 149 | filter(None, environ.get("CORS_ORIGIN_REGEX_WHITELIST", "").split(" ")), 150 | ) 151 | ] 152 | 153 | # Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal 154 | # sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging 155 | # on a production system. 156 | DEBUG = environ.get("DEBUG", "False").lower() == "true" 157 | 158 | # Set to True to enable DEVELOPER Mode. WARNING: ONLY netbox developers or plugin developers need this access. 159 | DEVELOPER = environ.get("DEVELOPER_MODE", "False").lower() == "true" 160 | 161 | # Email settings 162 | EMAIL = { 163 | "SERVER": environ.get("EMAIL_SERVER", "localhost"), 164 | "PORT": int(environ.get("EMAIL_PORT", 25)), 165 | "USERNAME": environ.get("EMAIL_USERNAME", ""), 166 | "PASSWORD": _read_secret("email_password", environ.get("EMAIL_PASSWORD", "")), 167 | "USE_SSL": environ.get("EMAIL_USE_SSL", "False").lower() == "true", 168 | "USE_TLS": environ.get("EMAIL_USE_TLS", "False").lower() == "true", 169 | "SSL_CERTFILE": environ.get("EMAIL_SSL_CERTFILE", ""), 170 | "SSL_KEYFILE": environ.get("EMAIL_SSL_KEYFILE", ""), 171 | "TIMEOUT": int(environ.get("EMAIL_TIMEOUT", 10)), # seconds 172 | "FROM_EMAIL": environ.get("EMAIL_FROM", ""), 173 | } 174 | 175 | # Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table 176 | # (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True. 177 | ENFORCE_GLOBAL_UNIQUE = environ.get("ENFORCE_GLOBAL_UNIQUE", "False").lower() == "true" 178 | 179 | # Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and 180 | # by anonymous users. List models in the form `.`. Add '*' to this list to exempt all models. 181 | EXEMPT_VIEW_PERMISSIONS = list( 182 | filter(None, environ.get("EXEMPT_VIEW_PERMISSIONS", "").split(" ")), 183 | ) 184 | 185 | # Enable GraphQL API. 186 | GRAPHQL_ENABLED = environ.get("GRAPHQL_ENABLED", "True").lower() == "true" 187 | 188 | # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: 189 | # https://docs.djangoproject.com/en/stable/topics/logging/ 190 | LOGGING = {} 191 | 192 | # Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users 193 | # are permitted to access most data in NetBox (excluding secrets) but not make any changes. 194 | LOGIN_REQUIRED = environ.get("LOGIN_REQUIRED", "False").lower() == "true" 195 | 196 | # The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to 197 | # re-authenticate. (Default: 1209600 [14 days]) 198 | LOGIN_TIMEOUT = int(environ.get("LOGIN_TIMEOUT", 1209600)) 199 | 200 | # Setting this to True will display a "maintenance mode" banner at the top of every page. 201 | MAINTENANCE_MODE = environ.get("MAINTENANCE_MODE", "False").lower() == "true" 202 | 203 | # An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g. 204 | # "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request 205 | # all objects by specifying "?limit=0". 206 | MAX_PAGE_SIZE = int(environ.get("MAX_PAGE_SIZE", 1000)) 207 | 208 | # The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that 209 | # the default value of this setting is derived from the installed location. 210 | MEDIA_ROOT = environ.get("MEDIA_ROOT", join(_BASE_DIR, "media")) 211 | 212 | # Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics' 213 | METRICS_ENABLED = environ.get("METRICS_ENABLED", "False").lower() == "true" 214 | 215 | # Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM. 216 | NAPALM_USERNAME = environ.get("NAPALM_USERNAME", "") 217 | NAPALM_PASSWORD = _read_secret("napalm_password", environ.get("NAPALM_PASSWORD", "")) 218 | 219 | # NAPALM timeout (in seconds). (Default: 30) 220 | NAPALM_TIMEOUT = int(environ.get("NAPALM_TIMEOUT", 30)) 221 | 222 | # NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must 223 | # be provided as a dictionary. 224 | NAPALM_ARGS = {} 225 | 226 | # Determine how many objects to display per page within a list. (Default: 50) 227 | PAGINATE_COUNT = int(environ.get("PAGINATE_COUNT", 50)) 228 | 229 | # Enable installed plugins. Add the name of each plugin to the list. 230 | PLUGINS = [] 231 | 232 | # Plugins configuration settings. These settings are used by various plugins that the user may have installed. 233 | # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. 234 | PLUGINS_CONFIG = {} 235 | 236 | # When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to 237 | # prefer IPv4 instead. 238 | PREFER_IPV4 = environ.get("PREFER_IPV4", "False").lower() == "true" 239 | 240 | # Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1. 241 | RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = int( 242 | environ.get("RACK_ELEVATION_DEFAULT_UNIT_HEIGHT", 22), 243 | ) 244 | RACK_ELEVATION_DEFAULT_UNIT_WIDTH = int( 245 | environ.get("RACK_ELEVATION_DEFAULT_UNIT_WIDTH", 220), 246 | ) 247 | 248 | # Remote authentication support 249 | REMOTE_AUTH_ENABLED = environ.get("REMOTE_AUTH_ENABLED", "False").lower() == "true" 250 | REMOTE_AUTH_BACKEND = environ.get( 251 | "REMOTE_AUTH_BACKEND", 252 | "netbox.authentication.RemoteUserBackend", 253 | ) 254 | REMOTE_AUTH_HEADER = environ.get("REMOTE_AUTH_HEADER", "HTTP_REMOTE_USER") 255 | REMOTE_AUTH_AUTO_CREATE_USER = ( 256 | environ.get("REMOTE_AUTH_AUTO_CREATE_USER", "True").lower() == "true" 257 | ) 258 | REMOTE_AUTH_DEFAULT_GROUPS = list( 259 | filter(None, environ.get("REMOTE_AUTH_DEFAULT_GROUPS", "").split(" ")), 260 | ) 261 | 262 | # This repository is used to check whether there is a new release of NetBox available. Set to None to disable the 263 | # version check or use the URL below to check for release in the official NetBox repository. 264 | # https://api.github.com/repos/netbox-community/netbox/releases 265 | RELEASE_CHECK_URL = environ.get("RELEASE_CHECK_URL", None) 266 | 267 | # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of 268 | # this setting is derived from the installed location. 269 | REPORTS_ROOT = environ.get("REPORTS_ROOT", "/etc/netbox/reports") 270 | 271 | # Maximum execution time for background tasks, in seconds. 272 | RQ_DEFAULT_TIMEOUT = int(environ.get("RQ_DEFAULT_TIMEOUT", 300)) 273 | 274 | # The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of 275 | # this setting is derived from the installed location. 276 | SCRIPTS_ROOT = environ.get("SCRIPTS_ROOT", "/etc/netbox/scripts") 277 | 278 | # By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use 279 | # local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only 280 | # database access.) Note that the user as which NetBox runs must have read and write permissions to this path. 281 | SESSION_FILE_PATH = environ.get("SESSIONS_ROOT", None) 282 | 283 | # Time zone (default: UTC) 284 | TIME_ZONE = environ.get("TIME_ZONE", "UTC") 285 | 286 | # Date/time formatting. See the following link for supported formats: 287 | # https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date 288 | DATE_FORMAT = environ.get("DATE_FORMAT", "N j, Y") 289 | SHORT_DATE_FORMAT = environ.get("SHORT_DATE_FORMAT", "Y-m-d") 290 | TIME_FORMAT = environ.get("TIME_FORMAT", "g:i a") 291 | SHORT_TIME_FORMAT = environ.get("SHORT_TIME_FORMAT", "H:i:s") 292 | DATETIME_FORMAT = environ.get("DATETIME_FORMAT", "N j, Y g:i a") 293 | SHORT_DATETIME_FORMAT = environ.get("SHORT_DATETIME_FORMAT", "Y-m-d H:i") 294 | --------------------------------------------------------------------------------