Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ COPY ./phonebox_plugin /source/phonebox_plugin/phonebox_plugin/
COPY ./setup.py /source/phonebox_plugin/
COPY ./MANIFEST.in /source/phonebox_plugin/
COPY ./README.md /source/phonebox_plugin/
RUN pip3 install --no-cache-dir /source/phonebox_plugin/
RUN /usr/local/bin/uv pip install --no-cache-dir /source/phonebox_plugin/
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,30 @@

A Telephone Number Management Plugin for [NetBox](https://github.com/netbox-community/netbox) and more.

## Compatibility

| NetBox Version | Plugin Version |
|:-------------------:|:--------------:|
| 4.2.x | 0.0.11 |

>The plugin versions 0.0.1b1-0.0.1b4 support NetBox 2.10.x versions.
>
>Latest plugin version 0.0.1b5 supports NetBox 2.11.0+ versions

I described some general considerations behind the plugin development and future plans in my [blog post](https://idebugall.github.io/phonebox-init/).


## Configuration

This plugin can be configured to use a top level netbox menu:
```
PLUGINS_CONFIG = {
"phonebox_plugin": {
"top_level_menu": True
}
}
```

### Preview

![](docs/media/preview_01.png)
Expand Down
16 changes: 5 additions & 11 deletions phonebox_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
from packaging import version
from django.conf import settings
NETBOX_CURRENT_VERSION = version.parse(settings.VERSION)

if NETBOX_CURRENT_VERSION >= version.parse("4.0.0"):
from netbox.plugins import PluginConfig
else:
from extras.plugins import PluginConfig

import importlib.metadata
from netbox.plugins import PluginConfig

class PhoneBoxConfig(PluginConfig):
name = 'phonebox_plugin'
version = version = importlib.metadata.version('phonebox-plugin')
verbose_name = 'PhoneBox Plugin'
description = 'Telephone Number Management Plugin for NetBox.'
version = 'v0.0.10'
author = 'Igor Korotchenkov'
author_email = 'iDebugAll@gmail.com'
base_url = 'phonebox'
min_version = "4.1.0"
min_version = "4.2.0"
max_version = "4.2.99"
required_settings = []
default_settings = {}
caching_config = {
Expand Down
7 changes: 0 additions & 7 deletions phonebox_plugin/admin.py

This file was deleted.

26 changes: 14 additions & 12 deletions phonebox_plugin/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,33 @@
from rest_framework.validators import UniqueTogetherValidator
from ..models import Number, VoiceCircuit
from tenancy.api.serializers import TenantSerializer
from dcim.api.serializers import RegionSerializer, SiteSerializer
from dcim.api.serializers import SiteSerializer, RegionSerializer, SiteSerializer
from circuits.api.serializers import ProviderSerializer
from extras.api.serializers import TagSerializer
from netbox.api.fields import ContentTypeField
from utilities.api import get_serializer_for_model
from ..choices import VOICE_CIRCUIT_ASSIGNMENT_MODELS
from netbox.api.serializers import NetBoxModelSerializer


class NumberSerializer(TagSerializer, serializers.ModelSerializer):
class NumberSerializer(NetBoxModelSerializer):

label = serializers.CharField(source='number', read_only=True)
tenant = TenantSerializer(required=True, allow_null=False, nested=True)
region = RegionSerializer(required=False, allow_null=True, nested=True)
site = SiteSerializer(required=False, allow_null=True, nested=True)
provider = ProviderSerializer(required=False, allow_null=True, nested=True)
forward_to = serializers.PrimaryKeyRelatedField(queryset=Number.objects.all(), required=False, allow_null=True)
tags = TagSerializer(many=True, required=False, nested=True)


class Meta:
model = Number
fields = [
"id", "label", "number", "tenant", "region", "forward_to", "description", "provider", "tags",
]
fields = (
"id", "url", "display", "label", "number", "tenant", "site", "region", "forward_to", "description", "provider", "tags",
)
brief_fields = ("id", "url", "number", "display")


class VoiceCircuitSerializer(TagSerializer, serializers.ModelSerializer):
class VoiceCircuitSerializer(NetBoxModelSerializer):

label = serializers.CharField(source='voice_circuit', read_only=True)
tenant = TenantSerializer(required=True, allow_null=False, nested=True)
Expand All @@ -41,7 +43,6 @@ class VoiceCircuitSerializer(TagSerializer, serializers.ModelSerializer):
allow_null=False
)
assigned_object = serializers.SerializerMethodField(read_only=True)
tags = TagSerializer(many=True, required=False, nested=True)

@extend_schema_field(serializers.JSONField(allow_null=True))
def get_assigned_object(self, obj):
Expand All @@ -53,8 +54,9 @@ def get_assigned_object(self, obj):

class Meta:
model = VoiceCircuit
fields = [
"id", "label", "name", "voice_circuit_type", "tenant", "region", "site", "description",
fields = (
"id", "url", "label", "display", "name", "voice_circuit_type", "tenant", "region", "site", "description",
'assigned_object_type','assigned_object_id', 'assigned_object',
"sip_source", "sip_target", "provider", "tags",
]
)
brief_fields = ("id", "url", "name", "display")
10 changes: 3 additions & 7 deletions phonebox_plugin/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@
from django.conf import settings
from packaging import version

NETBOX_CURRENT_VERSION = version.parse(settings.VERSION)

if NETBOX_CURRENT_VERSION >= version.parse("3.2"):
from netbox.api.viewsets import NetBoxModelViewSet as ModelViewSet
else:
from netbox.api.views import ModelViewSet
from netbox.api.viewsets import NetBoxModelViewSet


class PhoneBoxPluginRootView(APIRootView):
Expand All @@ -21,12 +17,12 @@ def get_view_name(self):
return 'PhoneBox'


class NumberViewSet(ModelViewSet):
class NumberViewSet(NetBoxModelViewSet):
queryset = Number.objects.prefetch_related('tenant', 'region', 'tags')
serializer_class = serializers.NumberSerializer
filterset_class = filters.NumberFilterSet

class VoiceCircuitsViewSet(ModelViewSet):
class VoiceCircuitsViewSet(NetBoxModelViewSet):
queryset = VoiceCircuit.objects.prefetch_related('tenant', 'region', 'tags')
serializer_class = serializers.VoiceCircuitSerializer
filterset_class = filters.VoiceCircuitFilterSet
15 changes: 8 additions & 7 deletions phonebox_plugin/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,9 @@
from packaging import version
from django.conf import settings

NETBOX_CURRENT_VERSION = version.parse(settings.VERSION)

if NETBOX_CURRENT_VERSION < version.parse("2.11.3"):
from utilities.filters import BaseFilterSet
from utilities.filters import TagFilter
else:
from netbox.filtersets import BaseFilterSet
from extras.filters import TagFilter
from netbox.filtersets import BaseFilterSet
from extras.filters import TagFilter


class NumberFilterSet(BaseFilterSet):
Expand All @@ -41,6 +36,12 @@ class NumberFilterSet(BaseFilterSet):
to_field_name='id',
label='Region (id)',
)
site = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
field_name='site__id',
to_field_name='id',
label='Site (id)',
)
provider = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
field_name='provider__id',
Expand Down
40 changes: 26 additions & 14 deletions phonebox_plugin/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,11 @@
from .models import Number, VoiceCircuit
from .choices import VoiceCircuitTypeChoices

NETBOX_CURRENT_VERSION = version.parse(settings.VERSION)
if NETBOX_CURRENT_VERSION < version.parse("3.5"):
from utilities.forms import (
DynamicModelMultipleChoiceField, DynamicModelChoiceField,
TagFilterField, BulkEditForm, CSVModelForm, CSVModelChoiceField
)
else:
from utilities.forms import BulkEditForm, CSVModelForm
from utilities.forms.fields import (
DynamicModelMultipleChoiceField, DynamicModelChoiceField,
TagFilterField, CSVModelChoiceField
)
from utilities.forms import BulkEditForm, CSVModelForm
from utilities.forms.fields import (
DynamicModelMultipleChoiceField, DynamicModelChoiceField,
TagFilterField, CSVModelChoiceField
)

class AddRemoveTagsForm(forms.Form):

Expand Down Expand Up @@ -57,6 +50,12 @@ class NumberFilterForm(forms.Form):
required=False,
null_option='None',
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='id',
required=False,
null_option='None',
)
provider = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
to_field_name='id',
Expand Down Expand Up @@ -86,7 +85,7 @@ class NumberEditForm(forms.ModelForm):

class Meta:
model = Number
fields = ('number', 'tenant', 'region', 'description', 'provider', 'forward_to', 'tags')
fields = ('number', 'tenant', 'site', 'region', 'description', 'provider', 'forward_to', 'tags')


class NumberBulkEditForm(AddRemoveTagsForm, BulkEditForm):
Expand All @@ -107,6 +106,13 @@ class NumberBulkEditForm(AddRemoveTagsForm, BulkEditForm):
required=False,
null_option='None',
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
to_field_name='id',
required=False,
null_option='None',
)

provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
to_field_name='id',
Expand All @@ -125,7 +131,7 @@ class NumberBulkEditForm(AddRemoveTagsForm, BulkEditForm):
)

class Meta:
nullable_fields = ('region', 'provider', 'forward_to', 'description')
nullable_fields = ('region', 'site', 'provider', 'forward_to', 'description')


class NumberCSVForm(CSVModelForm):
Expand All @@ -147,6 +153,12 @@ class NumberCSVForm(CSVModelForm):
to_field_name='name',
help_text='Assigned region'
)
site = CSVModelChoiceField(
queryset=Site.objects.all(),
required=False,
to_field_name='name',
help_text='Assigned site'
)
forward_to = CSVModelChoiceField(
queryset=Number.objects.all(),
to_field_name="number",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.1.5 on 2025-02-18 23:47

import utilities.json
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('phonebox_plugin', '0004_alter_number_created_alter_number_id_and_more'),
]

operations = [
migrations.AddField(
model_name='number',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
migrations.AddField(
model_name='voicecircuit',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
),
]
20 changes: 20 additions & 0 deletions phonebox_plugin/migrations/0006_number_site.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 5.1.5 on 2025-02-19 00:19

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dcim', '0200_populate_mac_addresses'),
('phonebox_plugin', '0005_number_custom_field_data_and_more'),
]

operations = [
migrations.AddField(
model_name='number',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='site_set', to='dcim.site'),
),
]
24 changes: 17 additions & 7 deletions phonebox_plugin/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
from taggit.managers import TaggableManager
from django.urls import reverse
from .choices import VoiceCircuitTypeChoices, VOICE_CIRCUIT_ASSIGNMENT_MODELS
from netbox.models import NetBoxModel

number_validator = RegexValidator(
r"^\+?[0-9A-D\#\*]*$",
"Numbers can only contain: leading +, digits 0-9; chars A, B, C, D; # and *"
)


class Number(ChangeLoggedModel):
class Number(NetBoxModel):
"""A Number represents a single telephone number of an arbitrary format.
A Number can contain only valid DTMF characters and leading plus sign for E.164 support:
- leading plus ("+") sign (optional)
Expand Down Expand Up @@ -53,6 +54,13 @@ class Number(ChangeLoggedModel):
null=True,
related_name="region_set"
)
site = models.ForeignKey(
to="dcim.Site",
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name="site_set"
)
forward_to = models.ForeignKey(
to="self",
on_delete=models.SET_NULL,
Expand All @@ -64,19 +72,21 @@ class Number(ChangeLoggedModel):

objects = RestrictedQuerySet.as_manager()

csv_headers = ['number', 'tenant', 'region', 'description', 'provider', 'forward_to']
csv_headers = ['number', 'tenant', 'site', 'region', 'description', 'provider', 'forward_to']

class Meta:
unique_together = ("number", "tenant",)

def __str__(self):
return str(self.number)

def get_absolute_url(self):
return reverse("plugins:phonebox_plugin:number_view", kwargs={"pk": self.pk})
return reverse("plugins:phonebox_plugin:number", kwargs={"pk": self.pk})


class Meta:
unique_together = ("number", "tenant",)


class VoiceCircuit(ChangeLoggedModel):
class VoiceCircuit(NetBoxModel):
"""A Voice Circuit represents a single circuit of one of the following types:
- SIP Trunk.
- Digital Voice Circuit (BRI/PRI/etc).
Expand Down Expand Up @@ -157,4 +167,4 @@ def __str__(self):
return str(self.name)

def get_absolute_url(self):
return reverse("plugins:phonebox_plugin:voice_circuit_view", kwargs={"pk": self.pk})
return reverse("plugins:phonebox_plugin:voicecircuit", kwargs={"pk": self.pk})
Loading