├── netbox_bgppeering ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── api │ ├── urls.py │ ├── views.py │ └── serializers.py ├── admin.py ├── __init__.py ├── icon_classes.py ├── tables.py ├── urls.py ├── navigation.py ├── release.py ├── templates │ └── netbox_bgppeering │ │ ├── bgppeering_delete.html │ │ ├── bgppeering_edit.html │ │ ├── bgppeering_list.html │ │ └── bgppeering.html ├── filters.py ├── models.py ├── forms.py └── views.py ├── development ├── netbox_master │ └── configuration.py ├── dev.env ├── docker-compose.yml ├── Dockerfile └── base_configuration.py ├── pyproject.toml ├── NOTICE ├── README.md ├── .gitignore ├── LICENSE ├── tasks.py └── poetry.lock /netbox_bgppeering/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_bgppeering/api/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework import routers 2 | from .views import BgpPeeringView 3 | 4 | router = routers.DefaultRouter() 5 | 6 | router.register(r"bgppeering", BgpPeeringView) 7 | 8 | urlpatterns = router.urls 9 | -------------------------------------------------------------------------------- /netbox_bgppeering/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import BgpPeering 3 | 4 | 5 | @admin.register(BgpPeering) 6 | class BgpPeeringAdmin(admin.ModelAdmin): 7 | list_display = ("device", "peer_name", "remote_as", "remote_ip") 8 | -------------------------------------------------------------------------------- /development/netbox_master/configuration.py: -------------------------------------------------------------------------------- 1 | """NetBox configuration file overrides specific to the latest MASTER version.""" 2 | from .base_configuration import * # pylint: disable=relative-beyond-top-level, wildcard-import 3 | 4 | # Overrides specific to this version go here 5 | -------------------------------------------------------------------------------- /development/dev.env: -------------------------------------------------------------------------------- 1 | ALLOWED_HOSTS=* 2 | DB_NAME=netbox 3 | DB_USER=netbox 4 | DB_PASSWORD=decinablesprewad 5 | PGPASSWORD=decinablesprewad 6 | DB_HOST=postgres 7 | NAPALM_TIMEOUT=5 8 | MAX_PAGE_SIZE=0 9 | SECRET_KEY=bqn8nn4qmjvx4hv2u5qr4pp46s3w9skbb63y 10 | POSTGRES_USER=netbox 11 | POSTGRES_PASSWORD=decinablesprewad 12 | POSTGRES_DB=netbox 13 | CHANGELOG_RETENTION=0 14 | REDIS_HOST=redis 15 | REDIS_PORT=6379 16 | REDIS_PASSWORD=decinablesprewad 17 | -------------------------------------------------------------------------------- /netbox_bgppeering/__init__.py: -------------------------------------------------------------------------------- 1 | from extras.plugins import PluginConfig 2 | 3 | 4 | class BgpPeering(PluginConfig): 5 | name = "netbox_bgppeering" 6 | verbose_name = "BGP Peering" 7 | description = "Manages BGP peer connections" 8 | version = "0.1" 9 | author = "Przemek Rogala " 10 | author_email = "pr@ttl255.com" 11 | base_url = "bgp-peering" 12 | min_version = "2.9" 13 | required_settings = [] 14 | default_settings = {} 15 | 16 | 17 | config = BgpPeering 18 | -------------------------------------------------------------------------------- /netbox_bgppeering/icon_classes.py: -------------------------------------------------------------------------------- 1 | from .release import NETBOX_RELEASE_CURRENT, NETBOX_RELEASE_210 2 | 3 | 4 | if NETBOX_RELEASE_CURRENT >= NETBOX_RELEASE_210: 5 | icon_classes = { 6 | "plus": "mdi mdi-plus-thick", 7 | "search": "mdi mdi-magnify", 8 | "remove": "mdi mdi-close-thick", 9 | "trash": "mdi mdi-trash-can-outline", 10 | "pencil": "mdi mdi-pencil", 11 | } 12 | else: 13 | icon_classes = { 14 | "plus": "fa fa-plus", 15 | "search": "fa fa-search", 16 | "remove": "fa fa-remove", 17 | "trash": "fa fa-trash", 18 | "pencil": "fa fa-pencil", 19 | } 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ttl255-netbox-plugin-bgppeering" 3 | version = "0.1.0" 4 | description = "NetBox Plugins - adds BGP Peering model" 5 | authors = ["Przemek Rogala "] 6 | license = "Apache-2.0" 7 | packages = [ 8 | { include = "netbox_bgppeering" }, 9 | ] 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.7 || ^3.8" 13 | 14 | [tool.poetry.dev-dependencies] 15 | bandit = "^1.6.3" 16 | black = "^20.8b1" 17 | invoke = "^1.4.1" 18 | pylint = "^2.6.0" 19 | pylint-django = "^2.3.0" 20 | pydocstyle = "^5.1.1" 21 | yamllint = "^1.25.0" 22 | 23 | [build-system] 24 | requires = ["poetry-core>=1.0.0"] 25 | build-backend = "poetry.core.masonry.api" 26 | -------------------------------------------------------------------------------- /netbox_bgppeering/tables.py: -------------------------------------------------------------------------------- 1 | import django_tables2 as tables 2 | from utilities.tables import BaseTable 3 | from .models import BgpPeering 4 | 5 | 6 | class BgpPeeringTable(BaseTable): 7 | """Table for displaying BGP Peering objects.""" 8 | 9 | id = tables.LinkColumn() 10 | site = tables.LinkColumn() 11 | device = tables.LinkColumn() 12 | local_ip = tables.LinkColumn() 13 | 14 | class Meta(BaseTable.Meta): 15 | model = BgpPeering 16 | fields = ( 17 | "id", 18 | "site", 19 | "device", 20 | "local_ip", 21 | "peer_name", 22 | "remote_ip", 23 | "remote_as", 24 | ) 25 | -------------------------------------------------------------------------------- /netbox_bgppeering/api/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import mixins, viewsets 2 | 3 | from netbox_bgppeering.models import BgpPeering 4 | from netbox_bgppeering.filters import BgpPeeringFilter 5 | 6 | from .serializers import BgpPeeringSerializer 7 | 8 | 9 | class BgpPeeringView( 10 | mixins.CreateModelMixin, 11 | mixins.DestroyModelMixin, 12 | mixins.ListModelMixin, 13 | mixins.RetrieveModelMixin, 14 | mixins.UpdateModelMixin, 15 | viewsets.GenericViewSet, 16 | ): 17 | """Create, check status of, update, and delete BgpPeering object.""" 18 | 19 | queryset = BgpPeering.objects.all() 20 | filterset_class = BgpPeeringFilter 21 | serializer_class = BgpPeeringSerializer 22 | -------------------------------------------------------------------------------- /netbox_bgppeering/urls.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.urls import path 3 | 4 | from .views import ( 5 | BgpPeeringCreateView, 6 | BgpPeeringDeleteView, 7 | BgpPeeringEditView, 8 | BgpPeeringListView, 9 | BgpPeeringView, 10 | ) 11 | 12 | 13 | urlpatterns = [ 14 | path("", BgpPeeringListView.as_view(), name="bgppeering_list"), 15 | path("/", BgpPeeringView.as_view(), name="bgppeering"), 16 | path("add/", BgpPeeringCreateView.as_view(), name="bgppeering_add"), 17 | path("/delete/", BgpPeeringDeleteView.as_view(), name="bgppeering_delete"), 18 | path("/edit/", BgpPeeringEditView.as_view(), name="bgppeering_edit"), 19 | ] 20 | -------------------------------------------------------------------------------- /netbox_bgppeering/navigation.py: -------------------------------------------------------------------------------- 1 | from extras.plugins import PluginMenuButton, PluginMenuItem 2 | from utilities.choices import ButtonColorChoices 3 | 4 | from .icon_classes import icon_classes 5 | 6 | 7 | menu_items = ( 8 | PluginMenuItem( 9 | link="plugins:netbox_bgppeering:bgppeering_list", 10 | link_text="BGP Peerings", 11 | buttons=( 12 | PluginMenuButton( 13 | link="plugins:netbox_bgppeering:bgppeering_add", 14 | title="Add", 15 | icon_class=icon_classes.get("plus"), 16 | color=ButtonColorChoices.GREEN, 17 | permissions=["netbox_bgppeering.add_bgppeering"], 18 | ), 19 | ), 20 | ), 21 | ) 22 | -------------------------------------------------------------------------------- /netbox_bgppeering/release.py: -------------------------------------------------------------------------------- 1 | """Release variables of the NetBox. 2 | (c) 2020 Network To Code 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | """ 13 | 14 | from packaging import version 15 | from django.conf import settings 16 | 17 | NETBOX_RELEASE_CURRENT = version.parse(settings.VERSION) 18 | NETBOX_RELEASE_28 = version.parse("2.8") 19 | NETBOX_RELEASE_29 = version.parse("2.9") 20 | NETBOX_RELEASE_210 = version.parse("2.10") 21 | -------------------------------------------------------------------------------- /netbox_bgppeering/templates/netbox_bgppeering/bgppeering_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load form_helpers %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 | {% csrf_token %} 9 |
10 |
Delete BGP Peering?
11 |
12 |

Are you sure you want to delete BGP Peering {{ object }}?

13 |
14 | 15 | Cancel 16 |
17 |
18 |
19 |
20 |
21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /netbox_bgppeering/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from ipam.api.nested_serializers import ( 4 | NestedIPAddressSerializer, 5 | ) 6 | from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer 7 | 8 | from netbox_bgppeering.models import BgpPeering 9 | 10 | 11 | class BgpPeeringSerializer(serializers.ModelSerializer): 12 | """Serializer for the BgpPeering model.""" 13 | 14 | site = NestedSiteSerializer( 15 | many=False, 16 | read_only=False, 17 | required=False, 18 | help_text="BgpPeering Site", 19 | ) 20 | 21 | device = NestedDeviceSerializer( 22 | many=False, 23 | read_only=False, 24 | required=True, 25 | help_text="BgpPeering Device", 26 | ) 27 | 28 | local_ip = NestedIPAddressSerializer( 29 | many=False, 30 | read_only=False, 31 | required=True, 32 | help_text="Local peering IP", 33 | ) 34 | 35 | class Meta: 36 | model = BgpPeering 37 | fields = [ 38 | "id", 39 | "site", 40 | "device", 41 | "local_ip", 42 | "local_as", 43 | "remote_ip", 44 | "remote_as", 45 | "peer_name", 46 | "description", 47 | ] 48 | -------------------------------------------------------------------------------- /netbox_bgppeering/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django.db.models import Q 3 | 4 | from dcim.models import Device, Site 5 | 6 | from .models import BgpPeering 7 | 8 | 9 | class BgpPeeringFilter(django_filters.FilterSet): 10 | """Filter capabilities for BgpPeering instances.""" 11 | 12 | q = django_filters.CharFilter( 13 | method="search", 14 | label="Search", 15 | ) 16 | 17 | site = django_filters.ModelMultipleChoiceFilter( 18 | field_name="site__slug", 19 | queryset=Site.objects.all(), 20 | to_field_name="slug", 21 | ) 22 | 23 | device = django_filters.ModelMultipleChoiceFilter( 24 | field_name="device__name", 25 | queryset=Device.objects.all(), 26 | to_field_name="name", 27 | ) 28 | 29 | peer_name = django_filters.CharFilter( 30 | lookup_expr="icontains", 31 | ) 32 | 33 | class Meta: 34 | model = BgpPeering 35 | 36 | fields = [ 37 | "local_as", 38 | "remote_as", 39 | "peer_name", 40 | ] 41 | 42 | def search(self, queryset, name, value): 43 | """Perform the filtered search.""" 44 | if not value.strip(): 45 | return queryset 46 | qs_filter = Q(peer_name__icontains=value) | Q(description__icontains=value) 47 | return queryset.filter(qs_filter) 48 | -------------------------------------------------------------------------------- /netbox_bgppeering/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | 4 | from dcim.fields import ASNField 5 | from extras.models import ChangeLoggedModel 6 | from ipam.fields import IPAddressField 7 | 8 | from utilities.querysets import RestrictedQuerySet 9 | 10 | 11 | class BgpPeering(ChangeLoggedModel): 12 | site = models.ForeignKey( 13 | to="dcim.Site", on_delete=models.SET_NULL, blank=True, null=True 14 | ) 15 | device = models.ForeignKey(to="dcim.Device", on_delete=models.PROTECT) 16 | local_ip = models.ForeignKey( 17 | to="ipam.IPAddress", on_delete=models.PROTECT, verbose_name="Local IP" 18 | ) 19 | local_as = ASNField(help_text="32-bit ASN used locally", verbose_name="Local ASN") 20 | remote_ip = IPAddressField( 21 | help_text="IPv4 or IPv6 address (with mask)", verbose_name="Remote IP" 22 | ) 23 | remote_as = ASNField(help_text="32-bit ASN used by peer", verbose_name="Remote ASN") 24 | peer_name = models.CharField(max_length=64, blank=True, verbose_name="Peer Name") 25 | description = models.CharField(max_length=200, blank=True) 26 | 27 | def __str__(self): 28 | return f"{self.device}:{self.remote_as}" 29 | 30 | def get_absolute_url(self): 31 | """Provide absolute URL to a Bgp Peering object.""" 32 | return reverse("plugins:netbox_bgppeering:bgppeering", kwargs={"pk": self.pk}) 33 | 34 | objects = RestrictedQuerySet.as_manager() 35 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Przemek Rogala 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | This software contains open source components pursuant to the following licenses: 16 | 17 | ---- 18 | 19 | ntc-netbox-plugin-onboarding\tasks.py 20 | ntc-netbox-plugin-onboarding\development\* 21 | 22 | https://github.com/networktocode/ntc-netbox-plugin-onboarding/blob/develop/LICENSE 23 | 24 | Copyright 2020 Network to Code 25 | Network to Code, LLC 26 | 27 | Licensed under the Apache License, Version 2.0 (the "License"); 28 | you may not use this file except in compliance with the License. 29 | 30 | You may obtain a copy of the License at 31 | 32 | http://www.apache.org/licenses/LICENSE-2.0 33 | 34 | Unless required by applicable law or agreed to in writing, software 35 | distributed under the License is distributed on an "AS IS" BASIS, 36 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | See the License for the specific language governing permissions and 38 | limitations under the License. -------------------------------------------------------------------------------- /development/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | services: 4 | netbox: 5 | build: 6 | context: ../ 7 | dockerfile: development/Dockerfile 8 | image: "ttl255-netbox-plugin-bgppeering/netbox:${NETBOX_VER}-py${PYTHON_VER}" 9 | command: > 10 | sh -c "python manage.py migrate && 11 | python manage.py runserver 0.0.0.0:8000" 12 | ports: 13 | - "8000:8000" 14 | depends_on: 15 | - postgres 16 | - redis 17 | env_file: 18 | - ./dev.env 19 | volumes: 20 | - ./base_configuration.py:/opt/netbox/netbox/netbox/base_configuration.py 21 | - ./netbox_${NETBOX_VER}/configuration.py:/opt/netbox/netbox/netbox/configuration.py 22 | - ../:/source 23 | tty: true 24 | worker: 25 | build: 26 | context: ../ 27 | dockerfile: development/Dockerfile 28 | image: "ttl255-netbox-plugin-bgppeering/netbox:${NETBOX_VER}-py${PYTHON_VER}" 29 | command: sh -c "python manage.py rqworker" 30 | depends_on: 31 | - netbox 32 | env_file: 33 | - ./dev.env 34 | volumes: 35 | - ./base_configuration.py:/opt/netbox/netbox/netbox/base_configuration.py 36 | - ./netbox_${NETBOX_VER}/configuration.py:/opt/netbox/netbox/netbox/configuration.py 37 | - ../netbox_bgppeering:/source/netbox_bgppeering 38 | tty: true 39 | postgres: 40 | image: postgres:10 41 | env_file: dev.env 42 | volumes: 43 | - pgdata_netbox_bgppeering:/var/lib/postgresql/data 44 | redis: 45 | image: redis:5-alpine 46 | command: 47 | - sh 48 | - -c # this is to evaluate the $REDIS_PASSWORD from the env 49 | - redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose 50 | env_file: ./dev.env 51 | volumes: 52 | pgdata_netbox_bgppeering: 53 | -------------------------------------------------------------------------------- /development/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | ARG python_ver=3.8.6 3 | FROM python:${python_ver} 4 | 5 | ARG netbox_ver=master 6 | # Play nicely with Docker logging 7 | ENV PYTHONUNBUFFERED 1 8 | # Don't generate .pyc files 9 | ENV PYTHONDONTWRITEBYTECODE 1 10 | 11 | RUN mkdir -p /opt 12 | 13 | RUN pip install --upgrade pip\ 14 | && pip install poetry 15 | 16 | # ------------------------------------------------------------------------------------- 17 | # Install NetBox 18 | # ------------------------------------------------------------------------------------- 19 | # Remove redis==3.4.1 from the requirements.txt file as a workaround to #4910 20 | # https://github.com/netbox-community/netbox/issues/4910, required for version 2.8.8 and earlier 21 | RUN git clone --single-branch --branch ${netbox_ver} https://github.com/netbox-community/netbox.git /opt/netbox/ && \ 22 | cd /opt/netbox/ && \ 23 | sed -i '/^redis\=\=/d' /opt/netbox/requirements.txt && \ 24 | pip install -r /opt/netbox/requirements.txt 25 | 26 | # Make the django-debug-toolbar always visible when DEBUG is enabled, 27 | # except when we're running Django unit-tests. 28 | RUN echo "import sys" >> /opt/netbox/netbox/netbox/settings.py && \ 29 | echo "TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'" >> /opt/netbox/netbox/netbox/settings.py && \ 30 | echo "DEBUG_TOOLBAR_CONFIG = {'SHOW_TOOLBAR_CALLBACK': lambda _: DEBUG and not TESTING }" >> /opt/netbox/netbox/netbox/settings.py 31 | 32 | # Work around https://github.com/rq/django-rq/issues/421 33 | RUN pip install django-rq==2.3.2 34 | 35 | # ------------------------------------------------------------------------------------- 36 | # Install Netbox Plugin 37 | # ------------------------------------------------------------------------------------- 38 | RUN mkdir -p /source 39 | WORKDIR /source 40 | COPY . /source 41 | RUN poetry config virtualenvs.create false \ 42 | && poetry install --no-interaction --no-ansi 43 | 44 | WORKDIR /opt/netbox/netbox/ 45 | -------------------------------------------------------------------------------- /netbox_bgppeering/forms.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | 3 | from django import forms 4 | 5 | from dcim.models import Device, Site 6 | from utilities.forms import BootstrapMixin 7 | 8 | from .models import BgpPeering 9 | 10 | 11 | class BgpPeeringForm(BootstrapMixin, forms.ModelForm): 12 | """Form for creating a new BgpPeering object.""" 13 | 14 | class Meta: 15 | model = BgpPeering 16 | fields = [ 17 | "site", 18 | "device", 19 | "local_as", 20 | "local_ip", 21 | "peer_name", 22 | "remote_as", 23 | "remote_ip", 24 | "description", 25 | ] 26 | 27 | def clean(self): 28 | cleaned_data = super().clean() 29 | local_ip = cleaned_data.get("local_ip") 30 | remote_ip = cleaned_data.get("remote_ip") 31 | 32 | if local_ip and remote_ip: 33 | # We can trust these are valid IP addresses. Format validation done in .super() 34 | if ( 35 | ipaddress.ip_interface(str(local_ip)).network 36 | != ipaddress.ip_interface(str(remote_ip)).network 37 | ): 38 | msg = "Local IP and Remote IP must be in the same network." 39 | self.add_error("local_ip", msg) 40 | self.add_error("remote_ip", msg) 41 | 42 | 43 | class BgpPeeringFilterForm(BootstrapMixin, forms.ModelForm): 44 | """Form for filtering BgpPeering instances.""" 45 | 46 | q = forms.CharField(required=False, label="Search") 47 | 48 | site = forms.ModelChoiceField( 49 | queryset=Site.objects.all(), required=False, to_field_name="slug" 50 | ) 51 | 52 | device = forms.ModelChoiceField( 53 | queryset=Device.objects.all(), 54 | to_field_name="name", 55 | required=False, 56 | ) 57 | 58 | local_as = forms.IntegerField( 59 | required=False, 60 | label="Local ASN", 61 | ) 62 | 63 | remote_as = forms.IntegerField(required=False, label="Remote ASN") 64 | 65 | peer_name = forms.CharField( 66 | required=False, 67 | label="Peer Name", 68 | ) 69 | 70 | class Meta: 71 | model = BgpPeering 72 | fields = [] 73 | -------------------------------------------------------------------------------- /netbox_bgppeering/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-12-10 19:24 2 | 3 | import dcim.fields 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import ipam.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ("ipam", "0037_ipaddress_assignment"), 15 | ("dcim", "0116_rearport_max_positions"), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name="BgpPeering", 21 | fields=[ 22 | ( 23 | "id", 24 | models.AutoField( 25 | auto_created=True, primary_key=True, serialize=False 26 | ), 27 | ), 28 | ("created", models.DateField(auto_now_add=True, null=True)), 29 | ("last_updated", models.DateTimeField(auto_now=True, null=True)), 30 | ("local_as", dcim.fields.ASNField()), 31 | ("remote_ip", ipam.fields.IPAddressField()), 32 | ("remote_as", dcim.fields.ASNField()), 33 | ("peer_name", models.CharField(blank=True, max_length=64)), 34 | ("description", models.CharField(blank=True, max_length=200)), 35 | ( 36 | "device", 37 | models.ForeignKey( 38 | on_delete=django.db.models.deletion.PROTECT, to="dcim.device" 39 | ), 40 | ), 41 | ( 42 | "local_ip", 43 | models.ForeignKey( 44 | on_delete=django.db.models.deletion.PROTECT, to="ipam.ipaddress" 45 | ), 46 | ), 47 | ( 48 | "site", 49 | models.ForeignKey( 50 | blank=True, 51 | null=True, 52 | on_delete=django.db.models.deletion.SET_NULL, 53 | to="dcim.site", 54 | ), 55 | ), 56 | ], 57 | options={ 58 | "abstract": False, 59 | }, 60 | ), 61 | ] 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetBox BGP Peering Plugin 2 | 3 | This repository contains source code of NetBox BGP Peering plugin. 4 | 5 | There are multiple branches in this repository. Each branch represents different stage of the development of the plugin. These stages are describred in detail in Developing NetBox plugin tutorial @ ttl255.com. 6 | 7 | This plugin is currently undergoing development and should not be used in production. 8 | 9 | # Branches 10 | 11 | | Name | Description | Tutorial part | 12 | | --- | --- | --- | 13 | | initial-plugin | Dev environment setup and plugin config | [^Part 1] | 14 | | minimal-plugin | Minimal interactive plugin | [^Part 1]| 15 | | adding-model | Adds model and exposes it in admin panel | [^Part 1] | 16 | | model-migrations | Adds Django model migrations | [^Part 1] | 17 | | bgppeering-view-init | Adds web UI page for displaying details of single object | [^Part 2] | 18 | | bgppeering-list-view-init | Adds web UI page for displaying list of objects | [^Part 2] | 19 | | bgppeering-create-view-init | Adds web UI page with form for creating objects | [^Part 2] | 20 | | bgppeering-list-search | Adds search/filter side form | [^Part 3] | 21 | | small-improvements | Improves functionality and look of the plugin | [^Part 4] | 22 | | adding-permissions | Adds object permissions | [^Part 5] | 23 | | adding-api | Adds API endpoints | [^Part 5] | 24 | 25 | \ 26 | Follow the below links to the tutorial if you'd like to see detailed walkthrough on how I built this plugin: 27 | 28 | [Developing NetBox Plugin - Part 1 - Setup and initial build](https://ttl255.com/developing-netbox-plugin-part-1-setup-and-initial-build/) 29 | 30 | [Developing NetBox Plugin - Part 2 - Adding web UI pages](https://ttl255.com/developing-netbox-plugin-part-2-adding-ui-pages/) 31 | 32 | [Developing NetBox Plugin - Part 3 - Adding search panel](https://ttl255.com/developing-netbox-plugin-part-3-adding-search/) 33 | 34 | [Developing NetBox Plugin - Part 4 - Small improvements](https://ttl255.com/developing-netbox-plugin-part-4-small-improvements/) 35 | 36 | [Developing NetBox Plugin - Part 5 - Permissions and API](https://ttl255.com/developing-netbox-plugin-part-5-permissions-and-api/) 37 | 38 | 39 | [^Part 1]: https://ttl255.com/developing-netbox-plugin-part-1-setup-and-initial-build/ 40 | [^Part 2]: https://ttl255.com/developing-netbox-plugin-part-2-adding-ui-pages/ 41 | [^Part 3]: https://ttl255.com/developing-netbox-plugin-part-3-adding-search/ 42 | [^Part 4]: https://ttl255.com/developing-netbox-plugin-part-4-small-improvements/ 43 | [^Part 5]: https://ttl255.com/developing-netbox-plugin-part-5-permissions-and-api/ -------------------------------------------------------------------------------- /netbox_bgppeering/templates/netbox_bgppeering/bgppeering_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 | {% csrf_token %} 6 |
7 |
8 |

9 | {% block title %} 10 | {% if object.pk %} 11 | Editing BGP Peering - {{ object }} 12 | {% else %} 13 | Add a new BGP Peering 14 | {% endif %} 15 | {% endblock %} 16 |

17 |
18 |
BGP Peering
19 |
20 | {% for field in form %} 21 |
22 | 25 |
26 | {{ field }} 27 | {% if field.help_text %} 28 | {{ field.help_text|safe }} 29 | {% endif %} 30 | {% if field.errors %} 31 |
    32 | {% for error in field.errors %} 33 |
  • {{ error }}
  • 34 | {% endfor %} 35 |
36 | {% endif %} 37 |
38 |
39 | {% endfor %} 40 |
41 |
42 |
43 |
44 |
45 |
46 | {% if object.pk %} 47 | 48 | Cancel 49 | {% else %} 50 | 51 | Cancel 52 | {% endif %} 53 |
54 |
55 |
56 | {% endblock %} -------------------------------------------------------------------------------- /netbox_bgppeering/templates/netbox_bgppeering/bgppeering_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load render_table from django_tables2 %} 3 | 4 | {% block content %} 5 |
6 | {% if perms.netbox_bgppeering.add_bgppeering %} 7 | 8 | Add 9 | 10 | {% endif %} 11 |
12 |

{% block title %}BGP Peerings{% endblock %}

13 |
14 |
15 | {% render_table table %} 16 |
17 |
18 |
19 |
20 | 21 | Search 22 |
23 |
24 |
25 | {% for field in filter_form.visible_fields %} 26 |
27 | {% if field.name == "q" %} 28 |
29 | 31 | 32 | 35 | 36 |
37 | {% else %} 38 | {{ field.label_tag }} 39 | {{ field }} 40 | {% endif %} 41 |
42 | {% endfor %} 43 |
44 | 47 | 48 | Clear 49 | 50 |
51 |
52 |
53 |
54 |
55 |
56 | {% endblock %} -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | LICENSE 141 | .vscode -------------------------------------------------------------------------------- /netbox_bgppeering/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.mixins import PermissionRequiredMixin 2 | from django.shortcuts import get_object_or_404, render 3 | from django.urls import reverse_lazy 4 | from django.views import View 5 | from django.views.generic.edit import CreateView, DeleteView, UpdateView 6 | from django_tables2 import LazyPaginator, RequestConfig, SingleTableView 7 | 8 | from .icon_classes import icon_classes 9 | from .filters import BgpPeeringFilter 10 | from .forms import BgpPeeringForm, BgpPeeringFilterForm 11 | from .models import BgpPeering 12 | from .tables import BgpPeeringTable 13 | 14 | 15 | class BgpPeeringView(PermissionRequiredMixin, View): 16 | """Display BGP Peering details""" 17 | 18 | permission_required = "netbox_bgppeering.view_bgppeering" 19 | 20 | queryset = BgpPeering.objects.all() 21 | 22 | def get(self, request, pk): 23 | """Get request.""" 24 | bgppeering_obj = get_object_or_404(self.queryset, pk=pk) 25 | 26 | return render( 27 | request, 28 | "netbox_bgppeering/bgppeering.html", 29 | { 30 | "bgppeering": bgppeering_obj, 31 | "icon_classes": icon_classes, 32 | }, 33 | ) 34 | 35 | 36 | class BgpPeeringListView(PermissionRequiredMixin, View): 37 | """View for listing all existing BGP Peerings.""" 38 | 39 | permission_required = "netbox_bgppeering.view_bgppeering" 40 | 41 | queryset = BgpPeering.objects.all() 42 | filterset = BgpPeeringFilter 43 | filterset_form = BgpPeeringFilterForm 44 | 45 | def get(self, request): 46 | """Get request.""" 47 | 48 | self.queryset = self.filterset(request.GET, self.queryset).qs.order_by("pk") 49 | 50 | table = BgpPeeringTable(self.queryset) 51 | RequestConfig(request, paginate={"per_page": 25}).configure(table) 52 | 53 | return render( 54 | request, 55 | "netbox_bgppeering/bgppeering_list.html", 56 | { 57 | "table": table, 58 | "filter_form": self.filterset_form(request.GET), 59 | "icon_classes": icon_classes, 60 | }, 61 | ) 62 | 63 | 64 | class BgpPeeringCreateView(PermissionRequiredMixin, CreateView): 65 | """View for creating a new BgpPeering instance.""" 66 | 67 | permission_required = "netbox_bgppeering.add_bgppeering" 68 | 69 | form_class = BgpPeeringForm 70 | template_name = "netbox_bgppeering/bgppeering_edit.html" 71 | 72 | 73 | class BgpPeeringDeleteView(PermissionRequiredMixin, DeleteView): 74 | """View for deleting a BgpPeering instance.""" 75 | 76 | permission_required = "netbox_bgppeering.delete_bgppeering" 77 | 78 | model = BgpPeering 79 | success_url = reverse_lazy("plugins:netbox_bgppeering:bgppeering_list") 80 | template_name = "netbox_bgppeering/bgppeering_delete.html" 81 | 82 | 83 | class BgpPeeringEditView(PermissionRequiredMixin, UpdateView): 84 | """View for editing a BgpPeering instance.""" 85 | 86 | permission_required = "netbox_bgppeering.change_bgppeering" 87 | 88 | model = BgpPeering 89 | form_class = BgpPeeringForm 90 | template_name = "netbox_bgppeering/bgppeering_edit.html" 91 | -------------------------------------------------------------------------------- /netbox_bgppeering/templates/netbox_bgppeering/bgppeering.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load helpers %} 3 | 4 | {% block header %} 5 |
6 |
7 | 11 |
12 |
13 |
14 |
15 | {% if perms.netbox_bgppeering.change_bgppeering %} 16 | 17 | Edit 18 | 19 | {% endif %} 20 | {% if perms.netbox_bgppeering.delete_bgppeering %} 21 | 22 | Delete 23 | 24 | {% endif %} 25 |
26 |
27 |
28 |

{% block title %}{{ bgppeering }}{% endblock %}

29 |

30 | Created {{ bgppeering.created }} · Updated {{ bgppeering.last_updated|timesince }} ago 32 |

33 |
34 | 35 | {% endblock %} 36 | 37 | 38 | {% block content %} 39 |
40 |
41 |
42 |
43 | BGP Peering 44 |
45 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
Site 49 | {% if bgppeering.site %} 50 | {{ bgppeering.site }} 51 | {% else %} 52 | None 53 | {% endif %} 54 |
Device 59 | {{ bgppeering.device }} 60 |
Local BGP AS{{ bgppeering.local_as }}
Local peering IP address 69 | {{ bgppeering.local_ip }} 70 |
Remote BGP AS{{ bgppeering.remote_as }}
Remote peering IP address{{ bgppeering.remote_ip }}
Peer name{{ bgppeering.peer_name|placeholder }}
Description{{ bgppeering.description|placeholder }}
89 |
90 |
91 |
92 | {% endblock %} -------------------------------------------------------------------------------- /development/base_configuration.py: -------------------------------------------------------------------------------- 1 | """NetBox configuration file.""" 2 | import os 3 | 4 | # For reference see http://netbox.readthedocs.io/en/latest/configuration/mandatory-settings/ 5 | # Based on https://github.com/digitalocean/netbox/blob/develop/netbox/netbox/configuration.example.py 6 | 7 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | 9 | ######################### 10 | # # 11 | # Required settings # 12 | # # 13 | ######################### 14 | 15 | # This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write 16 | # access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. 17 | # 18 | # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] 19 | ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(" ") 20 | 21 | # PostgreSQL database configuration. 22 | DATABASE = { 23 | "NAME": os.environ.get("DB_NAME", "netbox"), # Database name 24 | "USER": os.environ.get("DB_USER", ""), # PostgreSQL username 25 | "PASSWORD": os.environ.get("DB_PASSWORD", ""), 26 | # PostgreSQL password 27 | "HOST": os.environ.get("DB_HOST", "localhost"), # Database server 28 | "PORT": os.environ.get("DB_PORT", ""), # Database port (leave blank for default) 29 | } 30 | 31 | # This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. 32 | # For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and 33 | # symbols. NetBox will not run without this defined. For more information, see 34 | # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECRET_KEY 35 | SECRET_KEY = os.environ.get("SECRET_KEY", "") 36 | 37 | # Redis database settings. The Redis database is used for caching and background processing such as webhooks 38 | # Seperate sections for webhooks and caching allow for connecting to seperate Redis instances/datbases if desired. 39 | # Full connection details are required in both sections, even if they are the same. 40 | REDIS = { 41 | "caching": { 42 | "HOST": os.environ.get("REDIS_HOST", "redis"), 43 | "PORT": int(os.environ.get("REDIS_PORT", 6379)), 44 | "PASSWORD": os.environ.get("REDIS_PASSWORD", ""), 45 | "DATABASE": 1, 46 | "SSL": bool(os.environ.get("REDIS_SSL", False)), 47 | }, 48 | "tasks": { 49 | "HOST": os.environ.get("REDIS_HOST", "redis"), 50 | "PORT": int(os.environ.get("REDIS_PORT", 6379)), 51 | "PASSWORD": os.environ.get("REDIS_PASSWORD", ""), 52 | "DATABASE": 0, 53 | "SSL": bool(os.environ.get("REDIS_SSL", False)), 54 | }, 55 | } 56 | 57 | RQ_DEFAULT_TIMEOUT = 300 58 | 59 | 60 | ######################### 61 | # # 62 | # Optional settings # 63 | # # 64 | ######################### 65 | 66 | # Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of 67 | # application errors (assuming correct email settings are provided). 68 | ADMINS = [ 69 | # ['John Doe', 'jdoe@example.com'], 70 | ] 71 | 72 | # Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same 73 | # content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. 74 | BANNER_TOP = os.environ.get("BANNER_TOP", None) 75 | BANNER_BOTTOM = os.environ.get("BANNER_BOTTOM", None) 76 | 77 | # Text to include on the login page above the login form. HTML is allowed. 78 | BANNER_LOGIN = os.environ.get("BANNER_LOGIN", "") 79 | 80 | # Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set: 81 | # BASE_PATH = 'netbox/' 82 | BASE_PATH = os.environ.get("BASE_PATH", "") 83 | 84 | # Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90) 85 | CHANGELOG_RETENTION = int(os.environ.get("CHANGELOG_RETENTION", 0)) 86 | 87 | # API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be 88 | # allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or 89 | # CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers 90 | CORS_ORIGIN_ALLOW_ALL = True 91 | CORS_ORIGIN_WHITELIST = [] 92 | CORS_ORIGIN_REGEX_WHITELIST = [] 93 | 94 | # Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal 95 | # sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging 96 | # on a production system. 97 | DEBUG = True 98 | DEVELOPER = True 99 | 100 | # Email settings 101 | EMAIL = { 102 | "SERVER": "localhost", 103 | "PORT": 25, 104 | "USERNAME": "", 105 | "PASSWORD": "", 106 | "TIMEOUT": 10, 107 | "FROM_EMAIL": "", 108 | } 109 | 110 | # Enforcement of unique IP space can be toggled on a per-VRF basis. 111 | # To enforce unique IP space within the global table (all prefixes and IP addresses not assigned to a VRF), 112 | # set ENFORCE_GLOBAL_UNIQUE to True. 113 | ENFORCE_GLOBAL_UNIQUE = False 114 | 115 | # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: 116 | # https://docs.djangoproject.com/en/1.11/topics/logging/ 117 | LOGGING = { 118 | "version": 1, 119 | "disable_existing_loggers": False, 120 | "formatters": {"rq_console": {"format": "%(asctime)s %(message)s", "datefmt": "%H:%M:%S",},}, 121 | "handlers": { 122 | "rq_console": { 123 | "level": "DEBUG", 124 | "class": "rq.utils.ColorizingStreamHandler", 125 | "formatter": "rq_console", 126 | "exclude": ["%(asctime)s"], 127 | }, 128 | }, 129 | "loggers": {"rq.worker": {"handlers": ["rq_console"], "level": "DEBUG"},}, 130 | } 131 | 132 | # Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users 133 | # are permitted to access most data in NetBox (excluding secrets) but not make any changes. 134 | LOGIN_REQUIRED = False 135 | 136 | # Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set: 137 | # BASE_PATH = 'netbox/' 138 | BASE_PATH = os.environ.get("BASE_PATH", "") 139 | 140 | # Setting this to True will display a "maintenance mode" banner at the top of every page. 141 | MAINTENANCE_MODE = os.environ.get("MAINTENANCE_MODE", False) 142 | 143 | # An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g. 144 | # "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request 145 | # all objects by specifying "?limit=0". 146 | MAX_PAGE_SIZE = int(os.environ.get("MAX_PAGE_SIZE", 1000)) 147 | 148 | # The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that 149 | # the default value of this setting is derived from the installed location. 150 | MEDIA_ROOT = os.environ.get("MEDIA_ROOT", os.path.join(BASE_DIR, "media")) 151 | 152 | NAPALM_USERNAME = os.environ.get("NAPALM_USERNAME", "") 153 | NAPALM_PASSWORD = os.environ.get("NAPALM_PASSWORD", "") 154 | 155 | # NAPALM timeout (in seconds). (Default: 30) 156 | NAPALM_TIMEOUT = os.environ.get("NAPALM_TIMEOUT", 30) 157 | 158 | # NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must 159 | # be provided as a dictionary. 160 | NAPALM_ARGS = { 161 | "secret": NAPALM_PASSWORD, 162 | # Include any additional args here 163 | } 164 | 165 | # Determine how many objects to display per page within a list. (Default: 50) 166 | PAGINATE_COUNT = os.environ.get("PAGINATE_COUNT", 50) 167 | 168 | # Enable installed plugins. Add the name of each plugin to the list. 169 | PLUGINS = ["netbox_bgppeering"] 170 | 171 | PLUGINS_CONFIG = {"netbox_bgppeering": {}} 172 | # Plugins configuration settings. These settings are used by various plugins that the user may have installed. 173 | # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. 174 | # PLUGINS_CONFIG = {} 175 | 176 | # When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to 177 | # prefer IPv4 instead. 178 | PREFER_IPV4 = os.environ.get("PREFER_IPV4", False) 179 | 180 | # Remote authentication support 181 | REMOTE_AUTH_ENABLED = False 182 | REMOTE_AUTH_BACKEND = "netbox.authentication.RemoteUserBackend" 183 | REMOTE_AUTH_HEADER = "HTTP_REMOTE_USER" 184 | REMOTE_AUTH_AUTO_CREATE_USER = True 185 | REMOTE_AUTH_DEFAULT_GROUPS = [] 186 | REMOTE_AUTH_DEFAULT_PERMISSIONS = {} 187 | 188 | # This determines how often the GitHub API is called to check the latest release of NetBox. Must be at least 1 hour. 189 | RELEASE_CHECK_TIMEOUT = 24 * 3600 190 | 191 | # This repository is used to check whether there is a new release of NetBox available. Set to None to disable the 192 | # version check or use the URL below to check for release in the official NetBox repository. 193 | RELEASE_CHECK_URL = None 194 | # RELEASE_CHECK_URL = 'https://api.github.com/repos/netbox-community/netbox/releases' 195 | 196 | SESSION_FILE_PATH = None 197 | 198 | # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of 199 | # this setting is derived from the installed location. 200 | REPORTS_ROOT = os.environ.get("REPORTS_ROOT", os.path.join(BASE_DIR, "reports")) 201 | 202 | # Time zone (default: UTC) 203 | TIME_ZONE = os.environ.get("TIME_ZONE", "UTC") 204 | 205 | # Date/time formatting. See the following link for supported formats: 206 | # https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date 207 | DATE_FORMAT = os.environ.get("DATE_FORMAT", "N j, Y") 208 | SHORT_DATE_FORMAT = os.environ.get("SHORT_DATE_FORMAT", "Y-m-d") 209 | TIME_FORMAT = os.environ.get("TIME_FORMAT", "g:i a") 210 | SHORT_TIME_FORMAT = os.environ.get("SHORT_TIME_FORMAT", "H:i:s") 211 | DATETIME_FORMAT = os.environ.get("DATETIME_FORMAT", "N j, Y g:i a") 212 | SHORT_DATETIME_FORMAT = os.environ.get("SHORT_DATETIME_FORMAT", "Y-m-d H:i") 213 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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. 202 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | """Tasks for use with Invoke. 2 | 3 | (c) 2020 Network To Code 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 | (c) 2020 Przemek Rogala 15 | 16 | Modifications to the name of the plugin. Changing few hardcoded strings to vars. 17 | 18 | 2020-12-15 Added `refresh` task 19 | """ 20 | 21 | import os 22 | from invoke import task 23 | 24 | PYTHON_VER = os.getenv("PYTHON_VER", "3.8") 25 | NETBOX_VER = os.getenv("NETBOX_VER", "master") 26 | 27 | # Name of the docker image/container 28 | NAME = os.getenv("IMAGE_NAME", "ttl255-netbox-plugin-bgppeering") 29 | PWD = os.getcwd() 30 | 31 | COMPOSE_FILE = "development/docker-compose.yml" 32 | BUILD_NAME = "netbox_bgppeering" 33 | 34 | 35 | # ------------------------------------------------------------------------------ 36 | # BUILD 37 | # ------------------------------------------------------------------------------ 38 | @task 39 | def build(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 40 | """Build all docker images. 41 | 42 | Args: 43 | context (obj): Used to run specific commands 44 | netbox_ver (str): NetBox version to use to build the container 45 | python_ver (str): Will use the Python version docker image to build from 46 | """ 47 | context.run( 48 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} build --build-arg netbox_ver={netbox_ver} --build-arg python_ver={python_ver}", 49 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 50 | ) 51 | 52 | 53 | # ------------------------------------------------------------------------------ 54 | # START / STOP / DEBUG 55 | # ------------------------------------------------------------------------------ 56 | @task 57 | def debug(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 58 | """Start NetBox and its dependencies in debug mode. 59 | 60 | Args: 61 | context (obj): Used to run specific commands 62 | netbox_ver (str): NetBox version to use to build the container 63 | python_ver (str): Will use the Python version docker image to build from 64 | """ 65 | print("Starting Netbox .. ") 66 | context.run( 67 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} up", 68 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 69 | ) 70 | 71 | 72 | @task 73 | def start(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 74 | """Start NetBox and its dependencies in detached mode. 75 | 76 | Args: 77 | context (obj): Used to run specific commands 78 | netbox_ver (str): NetBox version to use to build the container 79 | python_ver (str): Will use the Python version docker image to build from 80 | """ 81 | print("Starting Netbox in detached mode.. ") 82 | context.run( 83 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} up -d", 84 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 85 | ) 86 | 87 | 88 | @task 89 | def stop(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 90 | """Stop NetBox and its dependencies. 91 | 92 | Args: 93 | context (obj): Used to run specific commands 94 | netbox_ver (str): NetBox version to use to build the container 95 | python_ver (str): Will use the Python version docker image to build from 96 | """ 97 | print("Stopping Netbox .. ") 98 | context.run( 99 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} down", 100 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 101 | ) 102 | 103 | 104 | @task 105 | def destroy(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 106 | """Destroy all containers and volumes. 107 | 108 | Args: 109 | context (obj): Used to run specific commands 110 | netbox_ver (str): NetBox version to use to build the container 111 | python_ver (str): Will use the Python version docker image to build from 112 | """ 113 | context.run( 114 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} down", 115 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 116 | ) 117 | context.run( 118 | f"docker volume rm -f {BUILD_NAME}_pgdata_{BUILD_NAME}", 119 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 120 | ) 121 | 122 | 123 | @task 124 | def refresh(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 125 | """Builds package, then stops and starts NetBox and its dependencies. 126 | 127 | Args: 128 | context (obj): Used to run specific commands 129 | netbox_ver (str): NetBox version to use to build the container 130 | python_ver (str): Will use the Python version docker image to build from 131 | """ 132 | build(context, netbox_ver, python_ver) 133 | stop(context, netbox_ver, python_ver) 134 | start(context, netbox_ver, python_ver) 135 | 136 | 137 | # ------------------------------------------------------------------------------ 138 | # ACTIONS 139 | # ------------------------------------------------------------------------------ 140 | @task 141 | def nbshell(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 142 | """Launch a nbshell session. 143 | 144 | Args: 145 | context (obj): Used to run specific commands 146 | netbox_ver (str): NetBox version to use to build the container 147 | python_ver (str): Will use the Python version docker image to build from 148 | """ 149 | context.run( 150 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox python manage.py nbshell", 151 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 152 | pty=True, 153 | ) 154 | 155 | 156 | @task 157 | def cli(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 158 | """Launch a bash shell inside the running NetBox container. 159 | 160 | Args: 161 | context (obj): Used to run specific commands 162 | netbox_ver (str): NetBox version to use to build the container 163 | python_ver (str): Will use the Python version docker image to build from 164 | """ 165 | context.run( 166 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox bash", 167 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 168 | pty=True, 169 | ) 170 | 171 | 172 | @task 173 | def create_user(context, user="admin", netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 174 | """Create a new user in django (default: admin), will prompt for password. 175 | 176 | Args: 177 | context (obj): Used to run specific commands 178 | user (str): name of the superuser to create 179 | netbox_ver (str): NetBox version to use to build the container 180 | python_ver (str): Will use the Python version docker image to build from 181 | """ 182 | context.run( 183 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox python manage.py createsuperuser --username {user}", 184 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 185 | pty=True, 186 | ) 187 | 188 | 189 | @task 190 | def makemigrations( 191 | context, app_name=BUILD_NAME, name="", netbox_ver=NETBOX_VER, python_ver=PYTHON_VER 192 | ): 193 | """Run Make Migration in Django. 194 | 195 | Args: 196 | context (obj): Used to run specific commands 197 | app_name (str): Name of the app for which to run migration 198 | name (str): Name of the migration to be created 199 | netbox_ver (str): NetBox version to use to build the container 200 | python_ver (str): Will use the Python version docker image to build from 201 | """ 202 | context.run( 203 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} up -d postgres", 204 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 205 | ) 206 | 207 | if name: 208 | context.run( 209 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox python manage.py makemigrations {app_name} --name {name}", 210 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 211 | ) 212 | else: 213 | context.run( 214 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox python manage.py makemigrations {app_name}", 215 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 216 | ) 217 | 218 | context.run( 219 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} down", 220 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 221 | ) 222 | 223 | 224 | # ------------------------------------------------------------------------------ 225 | # TESTS / LINTING 226 | # ------------------------------------------------------------------------------ 227 | @task 228 | def unittest(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 229 | """Run Django unit tests for the plugin. 230 | 231 | Args: 232 | context (obj): Used to run specific commands 233 | netbox_ver (str): NetBox version to use to build the container 234 | python_ver (str): Will use the Python version docker image to build from 235 | """ 236 | docker = f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox" 237 | context.run( 238 | f'{docker} sh -c "python manage.py test {BUILD_NAME}"', 239 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 240 | pty=True, 241 | ) 242 | 243 | 244 | @task 245 | def pylint(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 246 | """Run pylint code analysis. 247 | 248 | Args: 249 | context (obj): Used to run specific commands 250 | netbox_ver (str): NetBox version to use to build the container 251 | python_ver (str): Will use the Python version docker image to build from 252 | """ 253 | docker = f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox" 254 | # We exclude the /migrations/ directory since it is autogenerated code 255 | context.run( 256 | f"{docker} sh -c \"cd /source && find . -name '*.py' -not -path '*/migrations/*' | " 257 | 'PYTHONPATH=/opt/netbox/netbox DJANGO_SETTINGS_MODULE=netbox.settings xargs pylint"', 258 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 259 | pty=True, 260 | ) 261 | 262 | 263 | @task 264 | def black(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 265 | """Run black to check that Python files adhere to its style standards. 266 | 267 | Args: 268 | context (obj): Used to run specific commands 269 | netbox_ver (str): NetBox version to use to build the container 270 | python_ver (str): Will use the Python version docker image to build from 271 | """ 272 | docker = f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox" 273 | context.run( 274 | f'{docker} sh -c "cd /source && black --check --diff ."', 275 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 276 | pty=True, 277 | ) 278 | 279 | 280 | @task 281 | def pydocstyle(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 282 | """Run pydocstyle to validate docstring formatting adheres to NTC defined standards. 283 | 284 | Args: 285 | context (obj): Used to run specific commands 286 | netbox_ver (str): NetBox version to use to build the container 287 | python_ver (str): Will use the Python version docker image to build from 288 | """ 289 | docker = f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox" 290 | # We exclude the /migrations/ directory since it is autogenerated code 291 | context.run( 292 | f"{docker} sh -c \"cd /source && find . -name '*.py' -not -path '*/migrations/*' | xargs pydocstyle\"", 293 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 294 | pty=True, 295 | ) 296 | 297 | 298 | @task 299 | def bandit(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 300 | """Run bandit to validate basic static code security analysis. 301 | 302 | Args: 303 | context (obj): Used to run specific commands 304 | netbox_ver (str): NetBox version to use to build the container 305 | python_ver (str): Will use the Python version docker image to build from 306 | """ 307 | docker = f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox" 308 | context.run( 309 | f'{docker} sh -c "cd /source && bandit --configfile .bandit.yml --recursive ./"', 310 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 311 | pty=True, 312 | ) 313 | 314 | 315 | @task 316 | def tests(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 317 | """Run all tests for this plugin. 318 | 319 | Args: 320 | context (obj): Used to run specific commands 321 | netbox_ver (str): NetBox version to use to build the container 322 | python_ver (str): Will use the Python version docker image to build from 323 | """ 324 | # Sorted loosely from fastest to slowest 325 | print("Running black...") 326 | black(context, netbox_ver=netbox_ver, python_ver=python_ver) 327 | print("Running bandit...") 328 | bandit(context, netbox_ver=netbox_ver, python_ver=python_ver) 329 | print("Running pydocstyle...") 330 | pydocstyle(context, netbox_ver=netbox_ver, python_ver=python_ver) 331 | print("Running pylint...") 332 | pylint(context, netbox_ver=netbox_ver, python_ver=python_ver) 333 | print("Running unit tests...") 334 | unittest(context, netbox_ver=netbox_ver, python_ver=python_ver) 335 | # print("Running yamllint...") 336 | # yamllint(context, NAME, python_ver) 337 | 338 | print("All tests have passed!") 339 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "astroid" 11 | version = "2.4.2" 12 | description = "An abstract syntax tree for Python with inference support." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=3.5" 16 | 17 | [package.dependencies] 18 | lazy-object-proxy = ">=1.4.0,<1.5.0" 19 | six = ">=1.12,<2.0" 20 | typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} 21 | wrapt = ">=1.11,<2.0" 22 | 23 | [[package]] 24 | name = "bandit" 25 | version = "1.6.3" 26 | description = "Security oriented static analyser for python code." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=3.5" 30 | 31 | [package.dependencies] 32 | colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} 33 | GitPython = ">=1.0.1" 34 | PyYAML = ">=5.3.1" 35 | six = ">=1.10.0" 36 | stevedore = ">=1.20.0" 37 | 38 | [[package]] 39 | name = "black" 40 | version = "20.8b1" 41 | description = "The uncompromising code formatter." 42 | category = "dev" 43 | optional = false 44 | python-versions = ">=3.6" 45 | 46 | [package.dependencies] 47 | appdirs = "*" 48 | click = ">=7.1.2" 49 | mypy-extensions = ">=0.4.3" 50 | pathspec = ">=0.6,<1" 51 | regex = ">=2020.1.8" 52 | toml = ">=0.10.1" 53 | typed-ast = ">=1.4.0" 54 | typing-extensions = ">=3.7.4" 55 | 56 | [package.extras] 57 | colorama = ["colorama (>=0.4.3)"] 58 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 59 | 60 | [[package]] 61 | name = "click" 62 | version = "7.1.2" 63 | description = "Composable command line interface toolkit" 64 | category = "dev" 65 | optional = false 66 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 67 | 68 | [[package]] 69 | name = "colorama" 70 | version = "0.4.4" 71 | description = "Cross-platform colored terminal text." 72 | category = "dev" 73 | optional = false 74 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 75 | 76 | [[package]] 77 | name = "gitdb" 78 | version = "4.0.5" 79 | description = "Git Object Database" 80 | category = "dev" 81 | optional = false 82 | python-versions = ">=3.4" 83 | 84 | [package.dependencies] 85 | smmap = ">=3.0.1,<4" 86 | 87 | [[package]] 88 | name = "gitpython" 89 | version = "3.1.11" 90 | description = "Python Git Library" 91 | category = "dev" 92 | optional = false 93 | python-versions = ">=3.4" 94 | 95 | [package.dependencies] 96 | gitdb = ">=4.0.1,<5" 97 | 98 | [[package]] 99 | name = "importlib-metadata" 100 | version = "3.1.1" 101 | description = "Read metadata from Python packages" 102 | category = "dev" 103 | optional = false 104 | python-versions = ">=3.6" 105 | 106 | [package.dependencies] 107 | zipp = ">=0.5" 108 | 109 | [package.extras] 110 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 111 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 112 | 113 | [[package]] 114 | name = "invoke" 115 | version = "1.4.1" 116 | description = "Pythonic task execution" 117 | category = "dev" 118 | optional = false 119 | python-versions = "*" 120 | 121 | [[package]] 122 | name = "isort" 123 | version = "5.6.4" 124 | description = "A Python utility / library to sort Python imports." 125 | category = "dev" 126 | optional = false 127 | python-versions = ">=3.6,<4.0" 128 | 129 | [package.extras] 130 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 131 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 132 | colors = ["colorama (>=0.4.3,<0.5.0)"] 133 | 134 | [[package]] 135 | name = "lazy-object-proxy" 136 | version = "1.4.3" 137 | description = "A fast and thorough lazy object proxy." 138 | category = "dev" 139 | optional = false 140 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 141 | 142 | [[package]] 143 | name = "mccabe" 144 | version = "0.6.1" 145 | description = "McCabe checker, plugin for flake8" 146 | category = "dev" 147 | optional = false 148 | python-versions = "*" 149 | 150 | [[package]] 151 | name = "mypy-extensions" 152 | version = "0.4.3" 153 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 154 | category = "dev" 155 | optional = false 156 | python-versions = "*" 157 | 158 | [[package]] 159 | name = "pathspec" 160 | version = "0.8.1" 161 | description = "Utility library for gitignore style pattern matching of file paths." 162 | category = "dev" 163 | optional = false 164 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 165 | 166 | [[package]] 167 | name = "pbr" 168 | version = "5.5.1" 169 | description = "Python Build Reasonableness" 170 | category = "dev" 171 | optional = false 172 | python-versions = ">=2.6" 173 | 174 | [[package]] 175 | name = "pydocstyle" 176 | version = "5.1.1" 177 | description = "Python docstring style checker" 178 | category = "dev" 179 | optional = false 180 | python-versions = ">=3.5" 181 | 182 | [package.dependencies] 183 | snowballstemmer = "*" 184 | 185 | [[package]] 186 | name = "pylint" 187 | version = "2.6.0" 188 | description = "python code static checker" 189 | category = "dev" 190 | optional = false 191 | python-versions = ">=3.5.*" 192 | 193 | [package.dependencies] 194 | astroid = ">=2.4.0,<=2.5" 195 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 196 | isort = ">=4.2.5,<6" 197 | mccabe = ">=0.6,<0.7" 198 | toml = ">=0.7.1" 199 | 200 | [[package]] 201 | name = "pylint-django" 202 | version = "2.3.0" 203 | description = "A Pylint plugin to help Pylint understand the Django web framework" 204 | category = "dev" 205 | optional = false 206 | python-versions = "*" 207 | 208 | [package.dependencies] 209 | pylint = ">=2.0" 210 | pylint-plugin-utils = ">=0.5" 211 | 212 | [package.extras] 213 | for_tests = ["coverage", "django-tables2", "factory-boy", "pytest"] 214 | with_django = ["django"] 215 | 216 | [[package]] 217 | name = "pylint-plugin-utils" 218 | version = "0.6" 219 | description = "Utilities and helpers for writing Pylint plugins" 220 | category = "dev" 221 | optional = false 222 | python-versions = "*" 223 | 224 | [package.dependencies] 225 | pylint = ">=1.7" 226 | 227 | [[package]] 228 | name = "pyyaml" 229 | version = "5.3.1" 230 | description = "YAML parser and emitter for Python" 231 | category = "dev" 232 | optional = false 233 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 234 | 235 | [[package]] 236 | name = "regex" 237 | version = "2020.11.13" 238 | description = "Alternative regular expression module, to replace re." 239 | category = "dev" 240 | optional = false 241 | python-versions = "*" 242 | 243 | [[package]] 244 | name = "six" 245 | version = "1.15.0" 246 | description = "Python 2 and 3 compatibility utilities" 247 | category = "dev" 248 | optional = false 249 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 250 | 251 | [[package]] 252 | name = "smmap" 253 | version = "3.0.4" 254 | description = "A pure Python implementation of a sliding window memory map manager" 255 | category = "dev" 256 | optional = false 257 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 258 | 259 | [[package]] 260 | name = "snowballstemmer" 261 | version = "2.0.0" 262 | description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." 263 | category = "dev" 264 | optional = false 265 | python-versions = "*" 266 | 267 | [[package]] 268 | name = "stevedore" 269 | version = "3.3.0" 270 | description = "Manage dynamic plugins for Python applications" 271 | category = "dev" 272 | optional = false 273 | python-versions = ">=3.6" 274 | 275 | [package.dependencies] 276 | importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} 277 | pbr = ">=2.0.0,<2.1.0 || >2.1.0" 278 | 279 | [[package]] 280 | name = "toml" 281 | version = "0.10.2" 282 | description = "Python Library for Tom's Obvious, Minimal Language" 283 | category = "dev" 284 | optional = false 285 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 286 | 287 | [[package]] 288 | name = "typed-ast" 289 | version = "1.4.1" 290 | description = "a fork of Python 2 and 3 ast modules with type comment support" 291 | category = "dev" 292 | optional = false 293 | python-versions = "*" 294 | 295 | [[package]] 296 | name = "typing-extensions" 297 | version = "3.7.4.3" 298 | description = "Backported and Experimental Type Hints for Python 3.5+" 299 | category = "dev" 300 | optional = false 301 | python-versions = "*" 302 | 303 | [[package]] 304 | name = "wrapt" 305 | version = "1.12.1" 306 | description = "Module for decorators, wrappers and monkey patching." 307 | category = "dev" 308 | optional = false 309 | python-versions = "*" 310 | 311 | [[package]] 312 | name = "yamllint" 313 | version = "1.25.0" 314 | description = "A linter for YAML files." 315 | category = "dev" 316 | optional = false 317 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 318 | 319 | [package.dependencies] 320 | pathspec = ">=0.5.3" 321 | pyyaml = "*" 322 | 323 | [[package]] 324 | name = "zipp" 325 | version = "3.4.0" 326 | description = "Backport of pathlib-compatible object wrapper for zip files" 327 | category = "dev" 328 | optional = false 329 | python-versions = ">=3.6" 330 | 331 | [package.extras] 332 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 333 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 334 | 335 | [metadata] 336 | lock-version = "1.1" 337 | python-versions = "^3.7 || ^3.8" 338 | content-hash = "02457d722ccd61d96c37ab6ceb18347e6a66aa651d775b4fe3583ef411854dd8" 339 | 340 | [metadata.files] 341 | appdirs = [ 342 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 343 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 344 | ] 345 | astroid = [ 346 | {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, 347 | {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, 348 | ] 349 | bandit = [ 350 | {file = "bandit-1.6.3-py2.py3-none-any.whl", hash = "sha256:2ff3fe35fe3212c0be5fc9c4899bd0108e2b5239c5ff62fb174639e4660fe958"}, 351 | {file = "bandit-1.6.3.tar.gz", hash = "sha256:d02dfe250f4aa2d166c127ad81d192579e2bfcdb8501717c0e2005e35a6bcf60"}, 352 | ] 353 | black = [ 354 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 355 | ] 356 | click = [ 357 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 358 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 359 | ] 360 | colorama = [ 361 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 362 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 363 | ] 364 | gitdb = [ 365 | {file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"}, 366 | {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"}, 367 | ] 368 | gitpython = [ 369 | {file = "GitPython-3.1.11-py3-none-any.whl", hash = "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b"}, 370 | {file = "GitPython-3.1.11.tar.gz", hash = "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"}, 371 | ] 372 | importlib-metadata = [ 373 | {file = "importlib_metadata-3.1.1-py3-none-any.whl", hash = "sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013"}, 374 | {file = "importlib_metadata-3.1.1.tar.gz", hash = "sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170"}, 375 | ] 376 | invoke = [ 377 | {file = "invoke-1.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"}, 378 | {file = "invoke-1.4.1-py3-none-any.whl", hash = "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132"}, 379 | {file = "invoke-1.4.1.tar.gz", hash = "sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d"}, 380 | ] 381 | isort = [ 382 | {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, 383 | {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, 384 | ] 385 | lazy-object-proxy = [ 386 | {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, 387 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, 388 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, 389 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, 390 | {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, 391 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, 392 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, 393 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, 394 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, 395 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, 396 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, 397 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, 398 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, 399 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, 400 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, 401 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, 402 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, 403 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, 404 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, 405 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, 406 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, 407 | ] 408 | mccabe = [ 409 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 410 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 411 | ] 412 | mypy-extensions = [ 413 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 414 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 415 | ] 416 | pathspec = [ 417 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 418 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 419 | ] 420 | pbr = [ 421 | {file = "pbr-5.5.1-py2.py3-none-any.whl", hash = "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"}, 422 | {file = "pbr-5.5.1.tar.gz", hash = "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9"}, 423 | ] 424 | pydocstyle = [ 425 | {file = "pydocstyle-5.1.1-py3-none-any.whl", hash = "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"}, 426 | {file = "pydocstyle-5.1.1.tar.gz", hash = "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325"}, 427 | ] 428 | pylint = [ 429 | {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, 430 | {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, 431 | ] 432 | pylint-django = [ 433 | {file = "pylint-django-2.3.0.tar.gz", hash = "sha256:b8dcb6006ae9fa911810aba3bec047b9410b7d528f89d5aca2506b03c9235a49"}, 434 | {file = "pylint_django-2.3.0-py3-none-any.whl", hash = "sha256:770e0c55fb054c6378e1e8bb3fe22c7032a2c38ba1d1f454206ee9c6591822d7"}, 435 | ] 436 | pylint-plugin-utils = [ 437 | {file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"}, 438 | {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, 439 | ] 440 | pyyaml = [ 441 | {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, 442 | {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, 443 | {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, 444 | {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, 445 | {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, 446 | {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, 447 | {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, 448 | {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, 449 | {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, 450 | {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, 451 | {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, 452 | {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, 453 | {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, 454 | ] 455 | regex = [ 456 | {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, 457 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, 458 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, 459 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, 460 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, 461 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, 462 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, 463 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, 464 | {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, 465 | {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, 466 | {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, 467 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, 468 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, 469 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, 470 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, 471 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, 472 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, 473 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, 474 | {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, 475 | {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, 476 | {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, 477 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, 478 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, 479 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, 480 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, 481 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, 482 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, 483 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, 484 | {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, 485 | {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, 486 | {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, 487 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, 488 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, 489 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, 490 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, 491 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, 492 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, 493 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, 494 | {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, 495 | {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, 496 | {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, 497 | ] 498 | six = [ 499 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 500 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 501 | ] 502 | smmap = [ 503 | {file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"}, 504 | {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"}, 505 | ] 506 | snowballstemmer = [ 507 | {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, 508 | {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, 509 | ] 510 | stevedore = [ 511 | {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"}, 512 | {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"}, 513 | ] 514 | toml = [ 515 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 516 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 517 | ] 518 | typed-ast = [ 519 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 520 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 521 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 522 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 523 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 524 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 525 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 526 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, 527 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 528 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 529 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 530 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 531 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 532 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, 533 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 534 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 535 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 536 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 537 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 538 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, 539 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 540 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 541 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 542 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, 543 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, 544 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, 545 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, 546 | {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, 547 | {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, 548 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 549 | ] 550 | typing-extensions = [ 551 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 552 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 553 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 554 | ] 555 | wrapt = [ 556 | {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, 557 | ] 558 | yamllint = [ 559 | {file = "yamllint-1.25.0-py2.py3-none-any.whl", hash = "sha256:c7be4d0d2584a1b561498fa9acb77ad22eb434a109725c7781373ae496d823b3"}, 560 | {file = "yamllint-1.25.0.tar.gz", hash = "sha256:b1549cbe5b47b6ba67bdeea31720f5c51431a4d0c076c1557952d841f7223519"}, 561 | ] 562 | zipp = [ 563 | {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, 564 | {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, 565 | ] 566 | --------------------------------------------------------------------------------