├── netbox_vlan_manager ├── api │ ├── __init__.py │ ├── urls.py │ ├── views.py │ └── serializers.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── view_helpers.py ├── version.py ├── __init__.py ├── navigation.py ├── forms.py ├── templates │ └── netbox_vlan_manager │ │ ├── inc │ │ └── toggle_available.html │ │ ├── vlangroupset_vlans.html │ │ └── vlangroupset.html ├── urls.py ├── models.py ├── views.py └── tables.py ├── MANIFEST.in ├── docs └── img │ ├── vlan_group_overview.png │ ├── vlan_group_set_list.png │ └── vlan_group_set_vlans.png ├── develop ├── dev.env ├── Dockerfile ├── docker-compose.yml └── configuration.py ├── .github └── workflows │ └── pub-pypi.yml ├── setup.py ├── README.md ├── .gitignore └── LICENSE /netbox_vlan_manager/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_vlan_manager/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_vlan_manager/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netbox_vlan_manager/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.0' 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include netbox_vlan_manager/templates *.html -------------------------------------------------------------------------------- /docs/img/vlan_group_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyuk/netbox-vlan-manager/HEAD/docs/img/vlan_group_overview.png -------------------------------------------------------------------------------- /docs/img/vlan_group_set_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyuk/netbox-vlan-manager/HEAD/docs/img/vlan_group_set_list.png -------------------------------------------------------------------------------- /docs/img/vlan_group_set_vlans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miyuk/netbox-vlan-manager/HEAD/docs/img/vlan_group_set_vlans.png -------------------------------------------------------------------------------- /netbox_vlan_manager/templatetags/view_helpers.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | register = template.Library() 3 | 4 | 5 | @register.filter 6 | def get_vlan_by_group(record, vlan_group): 7 | vlan = ([x for x in record['vlans'] if x.group == vlan_group] or [None])[0] 8 | return vlan 9 | -------------------------------------------------------------------------------- /develop/dev.env: -------------------------------------------------------------------------------- 1 | ALLOWED_HOSTS=* 2 | DB_NAME=netbox 3 | DB_USER=netbox 4 | DB_PASSWORD=opinwegnzongepqg 5 | PGPASSWORD=opinwegnzongepqg 6 | DB_HOST=postgres 7 | SECRET_KEY=VMxcovTeDUGSFRRzVdFkwRHEuCeXKSUc4H8VBigkr1aUyOSmQhL8sIEvOnMgJnEC 8 | POSTGRES_USER=netbox 9 | POSTGRES_PASSWORD=opinwegnzongepqg 10 | POSTGRES_DB=netbox 11 | REDIS_HOST=redis 12 | REDIS_PORT=6379 13 | REDIS_SSL=False 14 | REDIS_PASSWORD=opinwegnzongepqg 15 | -------------------------------------------------------------------------------- /netbox_vlan_manager/__init__.py: -------------------------------------------------------------------------------- 1 | from netbox.plugins import PluginConfig 2 | from .version import __version__ 3 | 4 | 5 | class VLANManager(PluginConfig): 6 | name = 'netbox_vlan_manager' 7 | verbose_name = 'NetBox VLAN Manager' 8 | description = 'NetBox VLAN Manager for multitple VLAN Groups' 9 | author = 'miyuk' 10 | author_email = 'miyuk@miyuk.net' 11 | version = __version__ 12 | base_url = 'vlan-manager' 13 | 14 | 15 | config = VLANManager 16 | -------------------------------------------------------------------------------- /netbox_vlan_manager/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from netbox.api.routers import NetBoxRouter 3 | from . import views 4 | 5 | app_name = 'netbox_vlan_manager' 6 | 7 | router = NetBoxRouter() 8 | router.register('vlan-group-sets', views.VLANGroupSetListViewSet) 9 | 10 | urlpatterns = [ 11 | path( 12 | 'vlan-group-sets//available-vlans/', 13 | views.AvailableVLANsView.as_view(), 14 | name='vlangroupset-available-vlans' 15 | ), 16 | ] 17 | urlpatterns += router.urls 18 | -------------------------------------------------------------------------------- /netbox_vlan_manager/navigation.py: -------------------------------------------------------------------------------- 1 | from netbox.plugins import PluginMenuItem, PluginMenuButton, PluginMenuItem 2 | from netbox.choices import ButtonColorChoices 3 | 4 | vlan_group_manager_buttons = [ 5 | PluginMenuButton( 6 | link=f'plugins:netbox_vlan_manager:vlangroupset_add', 7 | title='Add', 8 | icon_class='mdi mdi-plus-thick', 9 | color=ButtonColorChoices.GREEN 10 | ) 11 | ] 12 | 13 | menu_items = ( 14 | PluginMenuItem( 15 | link=f'plugins:netbox_vlan_manager:vlangroupset_list', 16 | link_text='VLAN Group Set', 17 | buttons=vlan_group_manager_buttons 18 | ), 19 | ) 20 | -------------------------------------------------------------------------------- /netbox_vlan_manager/forms.py: -------------------------------------------------------------------------------- 1 | from netbox.forms import NetBoxModelForm 2 | from utilities.forms.fields import CommentField, DynamicModelMultipleChoiceField 3 | from ipam.models import VLANGroup 4 | from .models import VLANGroupSet 5 | 6 | 7 | class VLANGroupSetForm(NetBoxModelForm): 8 | vlan_groups = DynamicModelMultipleChoiceField( 9 | queryset=VLANGroup.objects.all(), 10 | required=False 11 | ) 12 | comments = CommentField() 13 | 14 | class Meta: 15 | model = VLANGroupSet 16 | fields = ( 17 | 'name', 18 | 'vlan_groups', 19 | 'description', 20 | 'comments', 21 | 'tags' 22 | ) 23 | -------------------------------------------------------------------------------- /develop/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG python_ver=3.8 2 | FROM python:${python_ver} 3 | 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | ENV PYTHONUNBUFFERED 1 6 | ENV APP_HOME=/opt/netbox/netbox 7 | 8 | # Install NetBox 9 | ARG netbox_ver=master 10 | RUN mkdir -p /opt 11 | RUN git clone --single-branch --branch ${netbox_ver} https://github.com/netbox-community/netbox.git /opt/netbox/ 12 | RUN pip install --upgrade pip && \ 13 | pip install -r /opt/netbox/requirements.txt 14 | 15 | # Install Netbox Plugin 16 | RUN mkdir -p /opt/plugin 17 | COPY . /opt/plugin 18 | RUN mkdir -p /opt/plugin && \ 19 | cd /opt/plugin && \ 20 | python /opt/plugin/setup.py develop 21 | 22 | # Set Work Directory 23 | WORKDIR $APP_HOME 24 | -------------------------------------------------------------------------------- /.github/workflows/pub-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | publish: 10 | name: Publish 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@main 14 | 15 | - name: Setup Python 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: 3.x 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install -U pip 23 | pip install build 24 | 25 | - name: Build a binary wheel and a source tarball 26 | run: | 27 | python -m build 28 | 29 | - name: Publish distribution to PyPI 30 | uses: pypa/gh-action-pypi-publish@release/v1 31 | with: 32 | user: __token__ 33 | password: ${{ secrets.PYPI_API_TOKEN }} 34 | -------------------------------------------------------------------------------- /netbox_vlan_manager/templates/netbox_vlan_manager/inc/toggle_available.html: -------------------------------------------------------------------------------- 1 | {% load helpers %} 2 | 3 | {% if show_assigned is not None or show_available is not None %} 4 |
5 | 7 | Show Assigned 8 | 9 | 11 | Show Available 12 | 13 | 15 | Show All 16 | 17 |
18 | {% endif %} -------------------------------------------------------------------------------- /develop/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | services: 4 | netbox: 5 | build: 6 | context: ../ 7 | dockerfile: develop/Dockerfile 8 | command: sh -c "./manage.py migrate && ./manage.py runserver 0.0.0.0:8000" 9 | ports: 10 | - '8000:8000' 11 | depends_on: 12 | - postgres 13 | - redis 14 | env_file: 15 | - ./dev.env 16 | volumes: 17 | - ./configuration.py:/opt/netbox/netbox/netbox/configuration.py 18 | - ../netbox_vlan_manager:/opt/plugin/netbox_vlan_manager 19 | worker: 20 | build: 21 | context: ../ 22 | dockerfile: develop/Dockerfile 23 | command: sh -c "./manage.py rqworker" 24 | depends_on: 25 | - netbox 26 | env_file: 27 | - ./dev.env 28 | volumes: 29 | - ./configuration.py:/opt/netbox/netbox/netbox/configuration.py 30 | - ../netbox_vlan_manager:/opt/plugin/netbox_vlan_manager 31 | postgres: 32 | image: postgres:12 33 | env_file: dev.env 34 | volumes: 35 | - pgdata_netbox_vlan_group_manager:/var/lib/postgresql/data 36 | redis: 37 | image: redis:5 38 | command: sh -c "redis-server --requirepass $$REDIS_PASSWORD" 39 | env_file: ./dev.env 40 | volumes: 41 | pgdata_netbox_vlan_group_manager: 42 | -------------------------------------------------------------------------------- /netbox_vlan_manager/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from netbox.views.generic import ObjectChangeLogView 3 | from . import models, views 4 | 5 | urlpatterns = ( 6 | path( 7 | 'vlan-group-sets/', 8 | views.VLANGroupSetListView.as_view(), 9 | name='vlangroupset_list' 10 | ), 11 | path( 12 | 'vlan-group-sets/add/', 13 | views.VLANGroupSetEditView.as_view(), 14 | name='vlangroupset_add' 15 | ), 16 | path( 17 | 'vlan-group-sets//', 18 | views.VLANGroupSetView.as_view(), 19 | name='vlangroupset' 20 | ), 21 | path( 22 | 'vlan-group-sets//edit/', 23 | views.VLANGroupSetEditView.as_view(), 24 | name='vlangroupset_edit' 25 | ), 26 | path( 27 | 'vlan-group-sets//delete/', 28 | views.VLANGroupSetDeleteView.as_view(), 29 | name='vlangroupset_delete' 30 | ), 31 | path( 32 | 'vlan-group-sets//changelog/', 33 | ObjectChangeLogView.as_view(), 34 | name='vlangroupset_changelog', 35 | kwargs={ 36 | 'model': models.VLANGroupSet 37 | } 38 | ), 39 | path( 40 | 'vlan-group-sets//export-vlans/', 41 | views.VLANGroupSetExportVLANs.as_view(), 42 | name='export_vlans' 43 | ), 44 | ) 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os.path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | 7 | def read(rel_path): 8 | here = os.path.abspath(os.path.dirname(__file__)) 9 | with codecs.open(os.path.join(here, rel_path), 'r') as fp: 10 | return fp.read() 11 | 12 | 13 | def get_version(rel_path): 14 | for line in read(rel_path).splitlines(): 15 | if line.startswith('__version__'): 16 | delim = '"' if '"' in line else "'" 17 | return line.split(delim)[1] 18 | else: 19 | raise RuntimeError("Unable to find version string.") 20 | 21 | 22 | long_description = read("README.md") 23 | 24 | setup( 25 | name='netbox-vlan-manager', 26 | version=get_version('netbox_vlan_manager/version.py'), 27 | description='VLAN Manager for multiple VLAN Groups', 28 | long_description=long_description, 29 | long_description_content_type="text/markdown", 30 | url="https://github.com/miyuk/netbox-vlan-manager/", 31 | author='miyuk', 32 | author_email='miyuk@miyuk.net', 33 | packages=find_packages(), 34 | include_package_data=True, 35 | install_requires=[], 36 | zip_safe=False, 37 | classifiers=[ 38 | 'Development Status :: 4 - Beta', 39 | 'Framework :: Django', 40 | 'Programming Language :: Python :: 3', 41 | ] 42 | ) 43 | -------------------------------------------------------------------------------- /netbox_vlan_manager/templates/netbox_vlan_manager/vlangroupset_vlans.html: -------------------------------------------------------------------------------- 1 | {% load django_tables2 %} 2 | 3 | 4 | {% if table.show_header %} 5 | 6 | 7 | {% for column in table.columns %} 8 | {% if column.orderable %} 9 | 11 | {% else %} 12 | 13 | {% endif %} 14 | {% endfor %} 15 | 16 | 17 | {% endif %} 18 | 19 | {% for row in table.page.object_list|default:table.rows %} 20 | 21 | {% for column, cell in row.items %} 22 | 23 | {% endfor %} 24 | 25 | {% empty %} 26 | {% if table.empty_text %} 27 | 28 | 30 | 31 | {% endif %} 32 | {% endfor %} 33 | 34 | {% if table.has_footer %} 35 | 36 | 37 | {% for column in table.columns %} 38 | 39 | {% endfor %} 40 | 41 | 42 | {% endif %} 43 |
{{ column.header }}{{ column.header }}
{{ cell }}
— {{ table.empty_text }} — 29 |
{{ column.footer }}
-------------------------------------------------------------------------------- /netbox_vlan_manager/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-18 19:41 2 | 3 | from django.db import migrations, models 4 | import taggit.managers 5 | import utilities.json 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('extras', '0084_staging'), 14 | ('ipam', '0063_standardize_description_comments'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='VLANGroupSet', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 22 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 23 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 24 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 25 | ('name', models.CharField(max_length=100, unique=True)), 26 | ('description', models.CharField(blank=True, max_length=200)), 27 | ('comments', models.TextField(blank=True)), 28 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 29 | ('vlan_groups', models.ManyToManyField(blank=True, related_name='vlan_group_sets', to='ipam.vlangroup')), 30 | ], 31 | options={ 32 | 'ordering': ('name',), 33 | }, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /netbox_vlan_manager/api/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Count 2 | from django.shortcuts import get_object_or_404 3 | from rest_framework.response import Response 4 | from rest_framework.views import APIView 5 | from netbox.config import get_config 6 | from netbox.api.viewsets import NetBoxModelViewSet 7 | from .serializers import VLANGroupSetSerializer, AvailableVLANSerializer 8 | from ..models import VLANGroupSet 9 | 10 | 11 | def get_results_limit(request): 12 | config = get_config() 13 | try: 14 | limit = int(request.query_params.get( 15 | 'limit', config.PAGINATE_COUNT)) or config.MAX_PAGE_SIZE 16 | except ValueError: 17 | limit = config.PAGINATE_COUNT 18 | if config.MAX_PAGE_SIZE: 19 | limit = min(limit, config.MAX_PAGE_SIZE) 20 | 21 | return limit 22 | 23 | 24 | class VLANGroupSetListViewSet(NetBoxModelViewSet): 25 | queryset = VLANGroupSet.objects.prefetch_related('tags').annotate( 26 | vlan_group_count=Count('vlan_groups') 27 | ) 28 | serializer_class = VLANGroupSetSerializer 29 | 30 | 31 | class AvailableVLANsView(APIView): 32 | queryset = VLANGroupSet.objects 33 | 34 | def get(self, request, pk): 35 | vlan_group_set = get_object_or_404(VLANGroupSet, pk=pk) 36 | limit = get_results_limit(request) 37 | 38 | available_vlans = vlan_group_set.get_available_vids()[:limit] 39 | serializer = AvailableVLANSerializer(available_vlans, many=True, context={ 40 | 'request': request, 41 | 'group_set': vlan_group_set, 42 | }) 43 | 44 | return Response(serializer.data) 45 | -------------------------------------------------------------------------------- /netbox_vlan_manager/templates/netbox_vlan_manager/vlangroupset.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load render_table from django_tables2 %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |
VLAN Group Set
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Name{{ object.name }}
Description{{ object.description }}
VLAN Groups{{ object.vlan_groups.count }}
24 |
25 |
26 | {% include 'inc/panels/custom_fields.html' %} 27 |
28 |
29 | {% include 'inc/panels/tags.html' %} 30 | {% include 'inc/panels/comments.html' %} 31 |
32 |
33 |
34 |
35 |
36 |
37 |
VLAN Groups
38 |
39 |
40 | {% include 'netbox_vlan_manager/inc/toggle_available.html' %} 41 | 42 |  Export 43 | 44 |
45 |
46 |
47 |
48 | {% render_table vlans_table %} 49 | {% include 'inc/paginator.html' with paginator=vlans_table.paginator page=vlans_table.page %} 50 |
51 |
52 |
53 |
54 | {% endblock content %} -------------------------------------------------------------------------------- /netbox_vlan_manager/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer 3 | from ..models import VLANGroupSet 4 | 5 | 6 | class VLANGroupSetSerializer(NetBoxModelSerializer): 7 | url = serializers.HyperlinkedIdentityField( 8 | view_name=f'plugins-api:netbox_vlan_manager-api:vlangroupset-detail' 9 | ) 10 | vlan_group_count = serializers.IntegerField(read_only=True) 11 | 12 | class Meta: 13 | model = VLANGroupSet 14 | fields = ( 15 | 'id', 16 | 'url', 17 | 'display', 18 | 'name', 19 | 'description', 20 | 'comments', 21 | 'tags', 22 | 'custom_fields', 23 | 'created', 24 | 'last_updated', 25 | 'vlan_group_count', 26 | ) 27 | 28 | 29 | class NestedVLANGroupSetSerializer(WritableNestedSerializer): 30 | url = serializers.HyperlinkedIdentityField( 31 | view_name=f'plugins-api:netbox_vlan_manager-api:vlangroupset-detail' 32 | ) 33 | vlan_group_count = serializers.IntegerField(read_only=True) 34 | 35 | class Meta: 36 | model = VLANGroupSet 37 | fields = ( 38 | 'id', 39 | 'url', 40 | 'display', 41 | 'name', 42 | 'description', 43 | 'vlan_group_count', 44 | ) 45 | 46 | 47 | class AvailableVLANSerializer(serializers.Serializer): 48 | vid = serializers.IntegerField(read_only=True) 49 | group_set = NestedVLANGroupSetSerializer(read_only=True) 50 | 51 | def to_representation(self, instance): 52 | return { 53 | 'vid': instance, 54 | 'group_set': NestedVLANGroupSetSerializer( 55 | self.context['group_set'], 56 | context={'request': self.context['request']} 57 | ).data, 58 | } 59 | -------------------------------------------------------------------------------- /netbox_vlan_manager/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from netbox.models import NetBoxModel 4 | from ipam.models import VLAN 5 | 6 | 7 | class VLANGroupSet(NetBoxModel): 8 | name = models.CharField( 9 | max_length=100, 10 | unique=True 11 | ) 12 | vlan_groups = models.ManyToManyField( 13 | to='ipam.VLANGroup', 14 | related_name='vlan_group_sets', 15 | blank=True 16 | ) 17 | description = models.CharField( 18 | max_length=200, 19 | blank=True 20 | ) 21 | comments = models.TextField( 22 | blank=True 23 | ) 24 | 25 | class Meta: 26 | ordering = ('name',) 27 | 28 | def __str__(self): 29 | return self.name 30 | 31 | def get_absolute_url(self): 32 | return reverse(f'plugins:netbox_vlan_manager:vlangroupset', kwargs={'pk': self.pk}) 33 | 34 | @property 35 | def min_vid(self): 36 | return min(self.vlan_groups.all(), key=(lambda x: x.min_vid)).min_vid 37 | 38 | @property 39 | def max_vid(self): 40 | return max(self.vlan_groups.all(), key=(lambda x: x.max_vid)).max_vid 41 | 42 | @property 43 | def vlans(self): 44 | vlan_groups = self.vlan_groups.all() 45 | group_vlans = VLAN.objects.filter( 46 | group__in=vlan_groups).select_related('group') 47 | 48 | vlan_group_vlans = [] 49 | for vid in range(self.min_vid, self.max_vid + 1): 50 | item = {} 51 | item['vid'] = vid 52 | vlans = [x for x in group_vlans if x.vid == vid] 53 | item['vlans'] = vlans 54 | item['status'] = 'Available' if not vlans else 'Assigned' 55 | vlan_group_vlans.append(item) 56 | return vlan_group_vlans 57 | 58 | def get_available_vids(self): 59 | available_vlans = set([x['vid'] 60 | for x in self.vlans if x['status'] == 'Available']) 61 | 62 | return sorted(available_vlans) 63 | 64 | def get_next_available_vid(self): 65 | available_vids = self.get_available_vids() 66 | if available_vids: 67 | return available_vids[0] 68 | return None 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetBox VLAN Manager 2 | 3 | NetBox plugin for viewer of multiple VLAN Group spaces. 4 | 5 | ![PyPI](https://img.shields.io/pypi/v/netbox-vlan-manager) 6 | ![publish PyPI workflow](https://github.com/miyuk/netbox-vlan-manager/actions/workflows/pub-pypi.yml/badge.svg) 7 | 8 | ## Purpose 9 | 10 | Enterprise environment has a lot of routers or switches. 11 | These devices manage VLAN each other. 12 | 13 | In many cases, these manage as one VLAN group. 14 | On the other hand, complex network has multiple VLAN groups 15 | 16 | For example, below cases. 17 | 18 | - Manage multi site VLAN groups 19 | - Visualize Cisco ACI Leaf Switch VLANs 20 | 21 | NetBox can manage VLAN space as `VLAN Group`. 22 | However, it can one VLAN Group only. 23 | 24 | NetBox VLAN Manager manage multiple `VLAN Group` as `VLAN Group Set`, and visualize status in tabular form such as below image. 25 | 26 | ![VLAN Group Set VLANs](https://raw.githubusercontent.com/miyuk/netbox-vlan-manager/main/docs/img/vlan_group_overview.png) 27 | 28 | ## Features 29 | 30 | ### Models 31 | 32 | This plugin provides following Models: 33 | 34 | - VLAN Group Set 35 | - Manage multiple VLAN Group 36 | 37 | ### API 38 | 39 | This plugin provides following API: 40 | 41 | - Available VLAN 42 | - Extract none used VID searching all VLAN Groups 43 | 44 | ## Compatibility 45 | 46 | This plugin requires NetBox `v3.4.0` or later because has migration scripts compatibility. 47 | 48 | The compatibility table between plugin versions and netbox is as follows. 49 | 50 | |NetBox version|Plugin version| 51 | |---|---| 52 | |3.x.x|0.1.x| 53 | |4.x.x|0.2.x| 54 | 55 | ## Installation 56 | 57 | The plugin is available as a Python package in PyPI and can be installed with pip: 58 | 59 | ```bash 60 | pip install netbox-vlan-manager 61 | ``` 62 | 63 | Enable the plugin in /opt/netbox/netbox/netbox/configuration.py 64 | 65 | ```python 66 | PLUGINS = ['netbox_vlan_manager'] 67 | ``` 68 | 69 | Restart NetBox 70 | 71 | ## Configuration 72 | 73 | Currently, this plugin is not necessary plugin configuration 74 | 75 | ## Screenshots 76 | 77 | VLAN Group Set List 78 | ![VLAN Group Set List](https://raw.githubusercontent.com/miyuk/netbox-vlan-manager/main/docs/img/vlan_group_set_list.png) 79 | 80 | VLAN Group View Set with VLANs 81 | ![VLAN Group Set VLANs](https://raw.githubusercontent.com/miyuk/netbox-vlan-manager/main/docs/img/vlan_group_set_vlans.png) 82 | -------------------------------------------------------------------------------- /netbox_vlan_manager/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Count 2 | from django_tables2.export.export import TableExport 3 | from netbox.views import generic 4 | from ipam.models import VLAN 5 | from .models import VLANGroupSet 6 | from .forms import VLANGroupSetForm 7 | from .tables import VLANGroupSetVLANTable, VLANGroupSetTable 8 | 9 | 10 | class VLANGroupSetView(generic.ObjectView): 11 | queryset = VLANGroupSet.objects.all() 12 | 13 | def get_extra_context(self, request, instance): 14 | show_available = bool(request.GET.get('show_available', 'true') == 'true') 15 | show_assigned = bool(request.GET.get('show_assigned', 'true') == 'true') 16 | vlan_groups = instance.vlan_groups.all() 17 | vlan_group_vlans = instance.vlans 18 | requested_vlans = [ 19 | x for x in vlan_group_vlans 20 | if show_available and x['status'] == 'Available' or show_assigned and x['status'] == 'Assigned' 21 | ] 22 | vlans_table = VLANGroupSetVLANTable(requested_vlans, vlan_groups=vlan_groups) 23 | vlans_table.configure(request) 24 | 25 | return { 26 | 'vlans_table': vlans_table, 27 | 'show_available': show_available, 28 | 'show_assigned': show_assigned, 29 | } 30 | 31 | 32 | class VLANGroupSetListView(generic.ObjectListView): 33 | queryset = VLANGroupSet.objects.annotate( 34 | vlan_group_count=Count('vlan_groups') 35 | ) 36 | table = VLANGroupSetTable 37 | 38 | 39 | class VLANGroupSetEditView(generic.ObjectEditView): 40 | queryset = VLANGroupSet.objects.all() 41 | form = VLANGroupSetForm 42 | 43 | 44 | class VLANGroupSetDeleteView(generic.ObjectDeleteView): 45 | queryset = VLANGroupSet.objects.all() 46 | 47 | 48 | class VLANGroupSetExportVLANs(generic.ObjectView): 49 | queryset = VLANGroupSet.objects.all() 50 | 51 | def get(self, request, **kwargs): 52 | instance = self.get_object(**kwargs) 53 | vlan_groups = instance.vlan_groups.all() 54 | vlan_group_vlans = instance.vlans 55 | vlans_table = VLANGroupSetVLANTable( 56 | vlan_group_vlans, vlan_groups=vlan_groups) 57 | vlans_table.configure(request) 58 | 59 | vlans_table = VLANGroupSetVLANTable( 60 | vlan_group_vlans, vlan_groups=vlan_groups) 61 | vlans_table.configure(request) 62 | exporter = TableExport('csv', vlans_table) 63 | return exporter.response(f'VLANGroupSetVLANs_{instance.name}.csv') 64 | -------------------------------------------------------------------------------- /netbox_vlan_manager/tables.py: -------------------------------------------------------------------------------- 1 | from netbox.models import NetBoxModel 2 | from django.urls import reverse 3 | import django_tables2 as tables 4 | from netbox.tables import NetBoxTable, BaseTable 5 | from .models import VLANGroupSet 6 | 7 | VLAN_STATUS = """ 8 | {% if record.status == 'Available' %} 9 | {{ record.status }} 10 | {% else %} 11 | {{ record.status }} 12 | {% endif %} 13 | """ 14 | 15 | VLAN_DATA = """ 16 | {% load view_helpers %} 17 | {% with vlan=record|get_vlan_by_group:vlan_group %} 18 | {% if vlan %} 19 | 20 | {{ vlan.name }} 21 | 22 | {% else %} 23 | 24 | Add VLAN 25 | 26 | {% endif %} 27 | {% endwith %} 28 | """ 29 | 30 | 31 | class VLANGroupSetTable(NetBoxTable): 32 | name = tables.Column( 33 | linkify=True 34 | ) 35 | vlan_group_count = tables.Column() 36 | 37 | class Meta(NetBoxTable.Meta): 38 | model = VLANGroupSet 39 | fields = ( 40 | 'pk', 41 | 'id', 42 | 'name', 43 | 'vlan_group_count', 44 | 'description', 45 | 'comments' 46 | ) 47 | default_columns = ( 48 | 'name', 49 | 'vlan_group_count', 50 | 'description' 51 | ) 52 | 53 | 54 | class VLANGroupSetVLANTable(BaseTable): 55 | vid = tables.Column( 56 | verbose_name='VID', 57 | linkify=lambda record: f'{reverse("ipam:vlan_list")}?vid={record["vid"]}' 58 | ) 59 | status = tables.TemplateColumn(template_code=VLAN_STATUS) 60 | 61 | class Meta(BaseTable.Meta): 62 | model = NetBoxModel 63 | template_name = 'netbox_vlan_manager/vlangroupset_vlans.html' 64 | empty_text = 'No VLANs found' 65 | fields = ( 66 | 'vid', 67 | 'status', 68 | ) 69 | row_attrs = { 70 | 'class': lambda record: 'success' if record['status'] == 'Available' else '', 71 | } 72 | 73 | def __init__(self, *args, vlan_groups=None, **kwargs): 74 | if vlan_groups is None: 75 | vlan_groups = [] 76 | 77 | self.Meta.fields = list(self.Meta.fields) 78 | extra_columns = [] 79 | for vlan_group in vlan_groups: 80 | column_name = f'vlangroup_{vlan_group.id}' 81 | column = ( 82 | column_name, 83 | tables.TemplateColumn( 84 | verbose_name=f'{vlan_group.name}', 85 | template_code=VLAN_DATA, 86 | extra_context={'vlan_group': vlan_group}, 87 | ) 88 | ) 89 | extra_columns.append(column) 90 | # TODO: There's probably a more clever way to accomplish this 91 | if column_name not in self.Meta.fields: 92 | self.Meta.fields.append(column_name) 93 | 94 | super().__init__(*args, extra_columns=extra_columns, **kwargs) 95 | -------------------------------------------------------------------------------- /.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 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # ignore .vscode 163 | .vscode/ -------------------------------------------------------------------------------- /develop/configuration.py: -------------------------------------------------------------------------------- 1 | import os 2 | from distutils.util import strtobool 3 | import socket 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | 7 | ######################### 8 | # # 9 | # Required settings # 10 | # # 11 | ######################### 12 | 13 | # This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write 14 | # access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. 15 | # 16 | # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] 17 | ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(' ') 18 | 19 | # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: 20 | # https://docs.djangoproject.com/en/stable/ref/settings/#databases 21 | DATABASE = { 22 | 'ENGINE': 'django.db.backends.postgresql', # Database engine 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 | 'HOST': os.environ.get('DB_HOST', 'localhost'), # Database server 27 | 'PORT': os.environ.get('DB_PORT', ''), 28 | } 29 | 30 | # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate 31 | # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended 32 | # to use two separate database IDs. 33 | REDIS = { 34 | 'tasks': { 35 | 'HOST': os.environ.get('REDIS_HOST', 'redis'), 36 | 'PORT': int(os.environ.get('REDIS_PORT', 6379)), 37 | 'PASSWORD': os.environ.get('REDIS_PASSWORD', ''), 38 | 'DATABASE': 0, 39 | 'SSL': bool(strtobool(os.environ.get('REDIS_SSL', False))), 40 | }, 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(strtobool(os.environ.get('REDIS_SSL', False))), 47 | }, 48 | } 49 | # This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. 50 | # For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and 51 | # symbols. NetBox will not run without this defined. For more information, see 52 | # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY 53 | SECRET_KEY = os.environ.get('SECRET_KEY', '') 54 | 55 | 56 | ######################### 57 | # # 58 | # Optional settings # 59 | # # 60 | ######################### 61 | 62 | # Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of 63 | # application errors (assuming correct email settings are provided). 64 | ADMINS = [ 65 | # ('John Doe', 'jdoe@example.com'), 66 | ] 67 | 68 | # Permit the retrieval of API tokens after their creation. 69 | ALLOW_TOKEN_RETRIEVAL = False 70 | 71 | # Enable any desired validators for local account passwords below. For a list of included validators, please see the 72 | # Django documentation at https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation. 73 | AUTH_PASSWORD_VALIDATORS = [ 74 | # { 75 | # 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 76 | # 'OPTIONS': { 77 | # 'min_length': 10, 78 | # } 79 | # }, 80 | ] 81 | 82 | # Base URL path if accessing NetBox within a directory. For example, if installed at https://example.com/netbox/, set: 83 | # BASE_PATH = 'netbox/' 84 | BASE_PATH = os.environ.get('BASE_PATH', '') 85 | 86 | # API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be 87 | # allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or 88 | # CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers 89 | CORS_ORIGIN_ALLOW_ALL = False 90 | CORS_ORIGIN_WHITELIST = [ 91 | # 'https://hostname.example.com', 92 | ] 93 | CORS_ORIGIN_REGEX_WHITELIST = [ 94 | # r'^(https?://)?(\w+\.)?example\.com$', 95 | ] 96 | 97 | # The name to use for the CSRF token cookie. 98 | CSRF_COOKIE_NAME = 'csrftoken' 99 | 100 | # Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal 101 | # sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging 102 | # on a production system. 103 | DEBUG = True 104 | DEVELOPER = True 105 | 106 | # Set the default preferred language/locale 107 | DEFAULT_LANGUAGE = 'en-us' 108 | 109 | # Email settings 110 | EMAIL = { 111 | 'SERVER': 'localhost', 112 | 'PORT': 25, 113 | 'USERNAME': '', 114 | 'PASSWORD': '', 115 | 'USE_SSL': False, 116 | 'USE_TLS': False, 117 | 'TIMEOUT': 10, # seconds 118 | 'FROM_EMAIL': '', 119 | } 120 | 121 | # Localization 122 | ENABLE_LOCALIZATION = False 123 | 124 | # Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and 125 | # by anonymous users. List models in the form `.`. Add '*' to this list to exempt all models. 126 | EXEMPT_VIEW_PERMISSIONS = [ 127 | # 'dcim.site', 128 | # 'dcim.region', 129 | # 'ipam.prefix', 130 | ] 131 | 132 | # HTTP proxies NetBox should use when sending outbound HTTP requests (e.g. for webhooks). 133 | # HTTP_PROXIES = { 134 | # 'http': 'http://10.10.1.10:3128', 135 | # 'https': 'http://10.10.1.10:1080', 136 | # } 137 | 138 | # IP addresses recognized as internal to the system. The debugging toolbar will be available only to clients accessing 139 | # NetBox from an internal IP. 140 | INTERNAL_IPS = ['127.0.0.1', '::1'] 141 | 142 | # Add container network first address because client connection is NATed by Docker 143 | container_ips = socket.gethostbyname_ex(socket.gethostname())[2] 144 | INTERNAL_IPS += ['.'.join(x.split('.')[:3] + ['1']) for x in container_ips] 145 | 146 | # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: 147 | # https://docs.djangoproject.com/en/stable/topics/logging/ 148 | LOGGING = {} 149 | 150 | # Automatically reset the lifetime of a valid session upon each authenticated request. Enables users to remain 151 | # authenticated to NetBox indefinitely. 152 | LOGIN_PERSISTENCE = False 153 | 154 | # Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users 155 | # are permitted to access most data in NetBox but not make any changes. 156 | LOGIN_REQUIRED = False 157 | 158 | # The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to 159 | # re-authenticate. (Default: 1209600 [14 days]) 160 | LOGIN_TIMEOUT = None 161 | 162 | # The view name or URL to which users are redirected after logging out. 163 | LOGOUT_REDIRECT_URL = 'home' 164 | 165 | # The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that 166 | # the default value of this setting is derived from the installed location. 167 | # MEDIA_ROOT = '/opt/netbox/netbox/media' 168 | 169 | # Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics' 170 | METRICS_ENABLED = False 171 | 172 | # Enable installed plugins. Add the name of each plugin to the list. 173 | PLUGINS = [ 174 | 'netbox_vlan_manager' 175 | ] 176 | 177 | # Plugins configuration settings. These settings are used by various plugins that the user may have installed. 178 | # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. 179 | # PLUGINS_CONFIG = { 180 | # 'my_plugin': { 181 | # 'foo': 'bar', 182 | # 'buzz': 'bazz' 183 | # } 184 | # } 185 | 186 | # Remote authentication support 187 | REMOTE_AUTH_ENABLED = False 188 | REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend' 189 | REMOTE_AUTH_HEADER = 'HTTP_REMOTE_USER' 190 | REMOTE_AUTH_USER_FIRST_NAME = 'HTTP_REMOTE_USER_FIRST_NAME' 191 | REMOTE_AUTH_USER_LAST_NAME = 'HTTP_REMOTE_USER_LAST_NAME' 192 | REMOTE_AUTH_USER_EMAIL = 'HTTP_REMOTE_USER_EMAIL' 193 | REMOTE_AUTH_AUTO_CREATE_USER = True 194 | REMOTE_AUTH_DEFAULT_GROUPS = [] 195 | REMOTE_AUTH_DEFAULT_PERMISSIONS = {} 196 | 197 | # This repository is used to check whether there is a new release of NetBox available. Set to None to disable the 198 | # version check or use the URL below to check for release in the official NetBox repository. 199 | RELEASE_CHECK_URL = None 200 | # RELEASE_CHECK_URL = 'https://api.github.com/repos/netbox-community/netbox/releases' 201 | 202 | # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of 203 | # this setting is derived from the installed location. 204 | # REPORTS_ROOT = '/opt/netbox/netbox/reports' 205 | 206 | # Maximum execution time for background tasks, in seconds. 207 | RQ_DEFAULT_TIMEOUT = 300 208 | 209 | # The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of 210 | # this setting is derived from the installed location. 211 | # SCRIPTS_ROOT = '/opt/netbox/netbox/scripts' 212 | 213 | # The name to use for the session cookie. 214 | SESSION_COOKIE_NAME = 'sessionid' 215 | 216 | # By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use 217 | # local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only 218 | # database access.) Note that the user as which NetBox runs must have read and write permissions to this path. 219 | SESSION_FILE_PATH = None 220 | 221 | # By default, uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the 222 | # class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example: 223 | # STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage' 224 | # STORAGE_CONFIG = { 225 | # 'AWS_ACCESS_KEY_ID': 'Key ID', 226 | # 'AWS_SECRET_ACCESS_KEY': 'Secret', 227 | # 'AWS_STORAGE_BUCKET_NAME': 'netbox', 228 | # 'AWS_S3_REGION_NAME': 'eu-west-1', 229 | # } 230 | 231 | # Time zone (default: UTC) 232 | TIME_ZONE = 'UTC' 233 | 234 | # Date/time formatting. See the following link for supported formats: 235 | # https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date 236 | DATE_FORMAT = 'N j, Y' 237 | SHORT_DATE_FORMAT = 'Y-m-d' 238 | TIME_FORMAT = 'g:i a' 239 | SHORT_TIME_FORMAT = 'H:i:s' 240 | DATETIME_FORMAT = 'N j, Y g:i a' 241 | SHORT_DATETIME_FORMAT = 'Y-m-d H:i' 242 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------