Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ASN Range models #183

Merged
merged 1 commit into from
Apr 4, 2024
Merged
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
1 change: 1 addition & 0 deletions changes/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!.gitignore
1 change: 1 addition & 0 deletions changes/117.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added ASN Range model.
15 changes: 15 additions & 0 deletions nautobot_bgp_models/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ class Meta:
fields = "__all__"


class AutonomousSystemRangeSerializer(
NautobotModelSerializer,
TaggedModelSerializerMixin,
):
"""REST API serializer for AutonomousSystemRange records."""

url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:nautobot_bgp_models-api:autonomoussystemrange-detail"
)

class Meta:
model = models.AutonomousSystemRange
fields = "__all__"


class InheritableFieldsSerializerMixin:
"""Common mixin for Serializers that support an additional `include_inherited` query parameter."""

Expand Down
1 change: 1 addition & 0 deletions nautobot_bgp_models/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
router = OrderedDefaultRouter()

router.register("autonomous-systems", views.AutonomousSystemViewSet)
router.register("autonomous-system-ranges", views.AutonomousSystemRangeViewSet)
router.register("peer-groups", views.PeerGroupViewSet)
router.register("peer-group-templates", views.PeerGroupTemplateViewSet)
router.register("peer-endpoints", views.PeerEndpointViewSet)
Expand Down
8 changes: 8 additions & 0 deletions nautobot_bgp_models/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ class AutonomousSystemViewSet(NautobotModelViewSet):
filterset_class = filters.AutonomousSystemFilterSet


class AutonomousSystemRangeViewSet(NautobotModelViewSet):
"""REST API viewset for AutonomousSystemRange records."""

queryset = models.AutonomousSystemRange.objects.all()
serializer_class = serializers.AutonomousSystemRangeSerializer
filterset_class = filters.AutonomousSystemRangeFilterSet


include_inherited = OpenApiParameter(
name="include_inherited",
required=False,
Expand Down
26 changes: 26 additions & 0 deletions nautobot_bgp_models/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,32 @@ class Meta:
fields = ["id", "asn", "status", "tags"]


class AutonomousSystemRangeFilterSet(
BaseFilterSet,
CreatedUpdatedModelFilterSetMixin,
CustomFieldModelFilterSetMixin,
):
"""Filtering of AutonomousSystemRange records."""

q = django_filters.CharFilter(
method="search",
label="Search",
)

def search(self, queryset, name, value): # pylint: disable=unused-argument
mzbroch marked this conversation as resolved.
Show resolved Hide resolved
"""Free-text search method implementation."""
if not value.strip():
return queryset

return queryset.filter(
Q(name=value) | Q(asn_max__icontains=value) | Q(asn_min__icontains=value) | Q(description__icontains=value)
).distinct()

class Meta:
model = models.AutonomousSystemRange
fields = ["id", "name", "asn_min", "asn_max", "tags"]


class BGPRoutingInstanceFilterSet(
BaseFilterSet, CreatedUpdatedModelFilterSetMixin, CustomFieldModelFilterSetMixin, StatusModelFilterSetMixin
):
Expand Down
34 changes: 34 additions & 0 deletions nautobot_bgp_models/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from nautobot.extras.forms import NautobotFilterForm, RoleModelFilterFormMixin
from nautobot.extras.models import Tag, Secret, Role
from nautobot.ipam.models import VRF, IPAddress
from nautobot.tenancy.models import Tenant

from . import choices, models

Expand Down Expand Up @@ -53,6 +54,39 @@ class Meta:
]


class AutonomousSystemRangeForm(NautobotModelForm):
"""Form for creating/updating AutonomousSystem records."""

tags = DynamicModelMultipleChoiceField(queryset=Tag.objects.all(), required=False)
tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)

class Meta:
model = models.AutonomousSystemRange
fields = ("name", "asn_min", "asn_max", "description", "tenant", "tags")


class AutonomousSystemRangeFilterForm(NautobotFilterForm):
"""Form for filtering AutonomousSystem records in combination with AutonomousSystemFilterSet."""

model = models.AutonomousSystemRange
field_order = ["name", "asn_min", "asn_max", "tenant", "tags"]
tag = TagFilterField(model)


class AutonomousSystemRangeBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm):
"""Form for bulk-editing multiple AutonomousSystem records."""

pk = forms.ModelMultipleChoiceField(
queryset=models.AutonomousSystemRange.objects.all(), widget=forms.MultipleHiddenInput()
)
description = forms.CharField(max_length=200, required=False)

class Meta:
nullable_fields = [
"description",
]


class BGPRoutingInstanceForm(NautobotModelForm):
"""Form for creating/updating BGPRoutingInstance records."""

Expand Down
30 changes: 30 additions & 0 deletions nautobot_bgp_models/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""BGP helper functions."""


def add_available_asns(instance, asns):
"""Create fake records for all gaps between used Autonomous Systems."""
new_list = []
last_asn = None
for asn_val in asns:
if asn_val.asn == instance.asn_min:
new_list.append(asn_val)
last_asn = asn_val.asn
elif not last_asn:
new_list.append({"asn": instance.asn_min, "available": asn_val.asn - instance.asn_min})
new_list.append(asn_val)
last_asn = asn_val.asn
elif instance.asn_min <= asn_val.asn <= instance.asn_max:
if asn_val.asn - last_asn > 1:
new_list.append({"asn": last_asn + 1, "available": asn_val.asn - last_asn - 1})
new_list.append(asn_val)
last_asn = asn_val.asn
elif asn_val.asn == instance.asn_max:
new_list.append(asn_val)
last_asn = asn_val.asn

if not asns:
new_list.append({"asn": instance.asn_min, "available": instance.asn_max - instance.asn_min + 1})
elif last_asn < instance.asn_max:
new_list.append({"asn": last_asn + 1, "available": instance.asn_max - last_asn})

return new_list
58 changes: 58 additions & 0 deletions nautobot_bgp_models/migrations/0009_autonomoussystemrange.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# pylint: disable=missing-module-docstring,missing-function-docstring,missing-class-docstring,invalid-name

import uuid
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import nautobot.core.models.fields
import nautobot.dcim.fields
import nautobot.extras.models.mixins


class Migration(migrations.Migration):

dependencies = [
("tenancy", "0008_tagsfield"),
("extras", "0098_rename_data_jobresult_result"),
("nautobot_bgp_models", "0008_nautobotv2_updates"),
]

operations = [
migrations.CreateModel(
name="AutonomousSystemRange",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True
),
),
("created", models.DateTimeField(auto_now_add=True, null=True)),
("last_updated", models.DateTimeField(auto_now=True, null=True)),
(
"_custom_field_data",
models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
("name", models.CharField(max_length=255, unique=True)),
("asn_min", nautobot.dcim.fields.ASNField()),
("asn_max", nautobot.dcim.fields.ASNField()),
("description", models.CharField(blank=True, max_length=255)),
("tags", nautobot.core.models.fields.TagsField(through="extras.TaggedItem", to="extras.Tag")),
(
"tenant",
models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to="tenancy.tenant"
),
),
],
options={
"verbose_name": "Autonomous System Range",
"ordering": ["asn_min"],
},
bases=(
models.Model,
nautobot.extras.models.mixins.DynamicGroupMixin,
nautobot.extras.models.mixins.NotesMixin,
),
),
]
44 changes: 44 additions & 0 deletions nautobot_bgp_models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from nautobot.apps.models import extras_features
from nautobot.ipam.models import IPAddress, IPAddressToInterface
from nautobot.core.utils.data import deepmerge
from nautobot.tenancy.models import Tenant

from nautobot_bgp_models.choices import AFISAFIChoices

Expand Down Expand Up @@ -143,6 +144,49 @@ def __str__(self):
return f"AS {self.asn}"


@extras_features(
"custom_fields",
"custom_links",
"custom_validators",
"export_templates",
"graphql",
"relationships",
"webhooks",
)
class AutonomousSystemRange(PrimaryModel):
"""Autonomous System Range information."""

name = models.CharField(max_length=255, unique=True, blank=False)
asn_min = ASNField(verbose_name="Start", help_text="Min value for 32-bit autonomous system number")
asn_max = ASNField(verbose_name="End", help_text="Max value for 32-bit autonomous system number")
description = models.CharField(max_length=255, blank=True)
tenant = models.ForeignKey(to=Tenant, on_delete=models.PROTECT, blank=True, null=True)
mzbroch marked this conversation as resolved.
Show resolved Hide resolved

class Meta:
ordering = ["asn_min"]
verbose_name = "Autonomous System Range"

def __str__(self):
"""String representation of an AutonomousSystemRange."""
return f"ASN Range {self.asn_min}-{self.asn_max}"

def clean(self):
"""Clean."""
if self.asn_min >= self.asn_max:
raise ValidationError("asn_min value must be lower than asn_max value.")

def get_next_available_asn(self):
"""Return the first available ASN number in the range, or None if none are available."""
asn_nums = AutonomousSystem.objects.filter(asn__gte=self.asn_min, asn__lte=self.asn_max).values_list(
"asn", flat=True
)
for i in range(self.asn_min, self.asn_max + 1):
if i not in asn_nums:
return i

return None


@extras_features(
"custom_fields",
"custom_links",
Expand Down
15 changes: 15 additions & 0 deletions nautobot_bgp_models/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@
),
),
),
NavMenuItem(
link="plugins:nautobot_bgp_models:autonomoussystemrange_list",
name="Autonomous System Ranges",
permissions=["nautobot_bgp_models.view_autonomoussystemrange"],
buttons=(
NavMenuAddButton(
link="plugins:nautobot_bgp_models:autonomoussystemrange_add",
permissions=["nautobot_bgp_models.add_autonomoussystemrange"],
),
NavMenuImportButton(
link="plugins:nautobot_bgp_models:autonomoussystemrange_import",
permissions=["nautobot_bgp_models.add_autonomoussystemrange"],
),
mzbroch marked this conversation as resolved.
Show resolved Hide resolved
),
),
NavMenuItem(
link="plugins:nautobot_bgp_models:peergrouptemplate_list",
name="Peer Group Templates",
Expand Down
31 changes: 30 additions & 1 deletion nautobot_bgp_models/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,25 @@

from . import models

ASN_LINK = """
{% if record.present_in_database %}
<a href="{{ record.get_absolute_url }}">{{ record.asn }}</a>
{% elif perms.nautobot_bgp_models.autonomoussystem_add %}
<a href="\
{% url 'plugins:nautobot_bgp_models:autonomoussystem_add' %}\
?asn={{ record.asn }}\
" class="btn btn-xs btn-success">{{ record.available }} ASN{{ record.available|pluralize }} available</a>\
{% else %}
{{ record.available }} ASN{{ record.available|pluralize }} available
{% endif %}
"""


class AutonomousSystemTable(StatusTableMixin, BaseTable):
"""Table representation of AutonomousSystem records."""

pk = ToggleColumn()
asn = tables.LinkColumn()
asn = tables.TemplateColumn(template_code=ASN_LINK, verbose_name="ASN")
provider = tables.LinkColumn()
tags = TagColumn(url_name="plugins:nautobot_bgp_models:autonomoussystem_list")
actions = ButtonsColumn(model=models.AutonomousSystem)
Expand All @@ -30,6 +43,22 @@ class Meta(BaseTable.Meta):
fields = ("pk", "asn", "status", "provider", "description", "tags")


class AutonomousSystemRangeTable(StatusTableMixin, BaseTable):
"""Table representation of AutonomousSystem records."""

pk = ToggleColumn()
name = tables.LinkColumn()
asn_min = tables.LinkColumn()
asn_max = tables.LinkColumn()
tenant = tables.LinkColumn()
tags = TagColumn(url_name="plugins:nautobot_bgp_models:autonomoussystemrange_list")
actions = ButtonsColumn(model=models.AutonomousSystemRange)

class Meta(BaseTable.Meta):
model = models.AutonomousSystemRange
fields = ("pk", "name", "asn_min", "asn_max", "tenant", "description", "tags")


class BGPRoutingInstanceTable(StatusTableMixin, BaseTable):
"""Table representation of BGPRoutingInstance records."""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% extends 'generic/object_detail.html' %}
{% load helpers %}

{% block content_left_page %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>BGP Autonomous System Range</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Start ASN</td>
<td>{{ object.asn_min }}</td>
</tr>
<tr>
<td>End ASN</td>
<td>{{ object.asn_max }}</td>
</tr>
<tr>
<td>Tenant</td>
<td>{{ object.tenant | hyperlinked_object }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description }}</td>
</tr>
</table>
</div>
{% endblock content_left_page %}

{% block content_right_page %}
{% include 'utilities/obj_table.html' with table=asn_range_table table_template='panel_table.html' heading='ASNs' bulk_edit_url='plugins:nautobot_bgp_models:autonomoussystem_bulk_edit' bulk_delete_url='plugins:nautobot_bgp_models:autonomoussystem_bulk_delete' %}
{% endblock content_right_page %}
Loading