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
1 change: 1 addition & 0 deletions changes/249.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added new columns to Peerings Table and added sorting + filtering functionality
29 changes: 25 additions & 4 deletions nautobot_bgp_models/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
BaseFilterSet,
CreatedUpdatedModelFilterSetMixin,
CustomFieldModelFilterSetMixin,
MultiValueCharFilter,
NaturalKeyOrPKMultipleChoiceFilter,
NautobotFilterSet,
SearchFilter,
StatusModelFilterSetMixin,
)
from nautobot.circuits.models import Provider
from nautobot.dcim.models import Device
from nautobot.extras.filters.mixins import RoleModelFilterSetMixin
from nautobot.extras.models import Role
from nautobot.ipam.models import VRF
from nautobot.ipam.models import VRF, IPAddress

from . import choices, models

Expand Down Expand Up @@ -197,9 +200,6 @@
):
"""Filtering of Peering records."""

# TODO(mzb): Add in-memory filtering for Provider, ASN, IP Address, ...
# this requires to consider inheritance methods.

q = SearchFilter(
filter_predicates={
"endpoints__routing_instance__device__name": "icontains",
Expand Down Expand Up @@ -234,6 +234,27 @@
label="Peer Endpoint Role (name)",
)

endpoint_ip = MultiValueCharFilter(
method="filter_endpoint_ip",
label="Endpoint IP Address",
)

autonomous_system = NaturalKeyOrPKMultipleChoiceFilter(
queryset=models.AutonomousSystem.objects.all(),
label="Autonomous System Number",
)

provider = NaturalKeyOrPKMultipleChoiceFilter(
queryset=Provider.objects.all(),
label="Provider",
)

def filter_endpoint_ip(self, queryset, _name, value):
"""Filter for IP address."""
matching_ips = IPAddress.objects.net_in(value)

return queryset.filter(endpoints__source_interface__ip_addresses__in=matching_ips).distinct()

Check warning on line 256 in nautobot_bgp_models/filters.py

View workflow job for this annotation

GitHub Actions / unittest_report (3.12, postgresql, stable)

Missing coverage

Missing coverage on lines 254-256

class Meta:
model = models.Peering
fields = ["id"]
Expand Down
253 changes: 253 additions & 0 deletions nautobot_bgp_models/table_columns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
"""Custom table columns for nautobot_bgp_models."""

import django_tables2 as tables
from django.db import models as django_models
from django.urls import reverse
from django.utils.html import format_html
from nautobot.core.templatetags.helpers import hyperlinked_object

from nautobot_bgp_models.models import PeerEndpoint


class BaseEndpointColumn(tables.Column):
"""Base class for endpoint-related columns."""

def __init__(self, *args, **kwargs):
"""Initialize BaseEndpointColumn."""
kwargs.setdefault("empty_values", ())
super().__init__(*args, **kwargs)


class ADeviceColumn(BaseEndpointColumn):
"""Column for A Side Device."""

def render(self, record): # pylint: disable=arguments-renamed
"""Render A Side Device."""
if record.endpoint_a and record.endpoint_a.routing_instance and record.endpoint_a.routing_instance.device:
device = record.endpoint_a.routing_instance.device
return hyperlinked_object(device)
return None

def order(self, queryset, is_descending):
"""Custom ordering for A Side Device."""
# Use a subquery to get specifically the first endpoint's device name
first_endpoint_device = (
PeerEndpoint.objects.filter(peering=django_models.OuterRef("pk"))
.order_by("pk")
.values("routing_instance__device__name")[:1]
)

queryset = queryset.annotate(a_device_name=django_models.Subquery(first_endpoint_device)).order_by(
("-" if is_descending else "") + "a_device_name"
)

return queryset, True


class ZDeviceColumn(BaseEndpointColumn):
"""Column for Z Side Device."""

def render(self, record): # pylint: disable=arguments-renamed
"""Render Z Side Device."""
if record.endpoint_z and record.endpoint_z.routing_instance and record.endpoint_z.routing_instance.device:
device = record.endpoint_z.routing_instance.device
return hyperlinked_object(device)
return None

def order(self, queryset, is_descending):
"""Custom ordering for Z Side Device."""
# Use a subquery to get specifically the second endpoint's device name
second_endpoint_device = (
PeerEndpoint.objects.filter(peering=django_models.OuterRef("pk"))
.order_by("pk")
.values("routing_instance__device__name")[1:2]
)

queryset = queryset.annotate(z_device_name=django_models.Subquery(second_endpoint_device)).order_by(
("-" if is_descending else "") + "z_device_name"
)

return queryset, True


class AEndpointIPColumn(BaseEndpointColumn):
"""Column for A Endpoint IP."""

def render(self, record): # pylint: disable=arguments-renamed
"""Render A Endpoint IP."""
if record.endpoint_a and record.endpoint_a.local_ip:
return hyperlinked_object(record.endpoint_a)
return None

def order(self, queryset, is_descending):
"""Custom ordering for A Endpoint IP."""
first_endpoint_interface_ip = (
PeerEndpoint.objects.filter(peering=django_models.OuterRef("pk"))
.order_by("pk")
.values(
"source_interface__ip_addresses__mask_length",
"source_interface__ip_addresses__ip_version",
"source_interface__ip_addresses__host",
)[:1]
)

queryset = queryset.annotate(
a_endpoint_mask=django_models.Subquery(
first_endpoint_interface_ip.values("source_interface__ip_addresses__mask_length")
),
a_endpoint_ip_version=django_models.Subquery(
first_endpoint_interface_ip.values("source_interface__ip_addresses__ip_version")
),
a_endpoint_host=django_models.Subquery(
first_endpoint_interface_ip.values("source_interface__ip_addresses__host")
),
)

if is_descending:
order_fields = ["a_endpoint_mask", "-a_endpoint_ip_version", "-a_endpoint_host"]
else:
order_fields = ["-a_endpoint_mask", "a_endpoint_ip_version", "a_endpoint_host"]

return queryset.order_by(*order_fields), True


class ZEndpointIPColumn(BaseEndpointColumn):
"""Column for Z Endpoint IP."""

def render(self, record): # pylint: disable=arguments-renamed
"""Render Z Endpoint IP."""
if record.endpoint_z and record.endpoint_z.local_ip:
return hyperlinked_object(record.endpoint_z)
return None

def order(self, queryset, is_descending):
"""Custom ordering for Z Endpoint IP."""
second_endpoint_interface_ip = (
PeerEndpoint.objects.filter(peering=django_models.OuterRef("pk"))
.order_by("pk")
.values(
"source_interface__ip_addresses__mask_length",
"source_interface__ip_addresses__ip_version",
"source_interface__ip_addresses__host",
)[1:2]
)

queryset = queryset.annotate(
z_endpoint_mask=django_models.Subquery(
second_endpoint_interface_ip.values("source_interface__ip_addresses__mask_length")
),
z_endpoint_ip_version=django_models.Subquery(
second_endpoint_interface_ip.values("source_interface__ip_addresses__ip_version")
),
z_endpoint_host=django_models.Subquery(
second_endpoint_interface_ip.values("source_interface__ip_addresses__host")
),
)

if is_descending:
order_fields = ["z_endpoint_mask", "-z_endpoint_ip_version", "-z_endpoint_host"]
else:
order_fields = ["-z_endpoint_mask", "z_endpoint_ip_version", "z_endpoint_host"]

return queryset.order_by(*order_fields), True


class AASNColumn(BaseEndpointColumn):
"""Column for A Side ASN."""

def render(self, record): # pylint: disable=arguments-renamed
"""Render A Side ASN using inherited autonomous system."""
if record.endpoint_a:
asn, _, _ = record.endpoint_a.get_inherited_field("autonomous_system")
if asn:
return hyperlinked_object(asn)
return None

def order(self, queryset, is_descending):
"""Custom ordering for A Side ASN."""
first_endpoint_asn = (
PeerEndpoint.objects.filter(peering=django_models.OuterRef("pk"))
.order_by("pk")
.values("routing_instance__autonomous_system__asn")[:1]
)

queryset = queryset.annotate(a_side_asn_value=django_models.Subquery(first_endpoint_asn))

order_field = "-a_side_asn_value" if is_descending else "a_side_asn_value"
return queryset.order_by(order_field), True


class ZASNColumn(BaseEndpointColumn):
"""Column for Z Side ASN."""

def render(self, record): # pylint: disable=arguments-renamed
"""Render Z Side ASN using inherited autonomous system."""
if record.endpoint_z:
asn, _, _ = record.endpoint_z.get_inherited_field("autonomous_system")
if asn:
url = reverse("plugins:nautobot_bgp_models:autonomoussystem", args=[asn.pk])
return format_html('<a href="{}">{}</a>', url, asn.asn)
return None

def order(self, queryset, is_descending):
"""Custom ordering for Z Side ASN."""
second_endpoint_asn = (
PeerEndpoint.objects.filter(peering=django_models.OuterRef("pk"))
.order_by("pk")
.values("routing_instance__autonomous_system__asn")[1:2]
)

queryset = queryset.annotate(z_side_asn_value=django_models.Subquery(second_endpoint_asn))

order_field = "-z_side_asn_value" if is_descending else "z_side_asn_value"
return queryset.order_by(order_field), True


class AProviderColumn(BaseEndpointColumn):
"""Column for A Side Provider."""

def render(self, record): # pylint: disable=arguments-renamed
"""Render Provider A using inherited autonomous system."""
if record.endpoint_a:
asn, _, _ = record.endpoint_a.get_inherited_field("autonomous_system")
if asn and asn.provider:
return hyperlinked_object(asn.provider)
return None

def order(self, queryset, is_descending):
"""Custom ordering for A Side Provider."""
first_endpoint_provider = (
PeerEndpoint.objects.filter(peering=django_models.OuterRef("pk"))
.order_by("pk")
.values("routing_instance__autonomous_system__provider__name")[:1]
)

queryset = queryset.annotate(a_side_provider_name=django_models.Subquery(first_endpoint_provider))

order_field = "-a_side_provider_name" if is_descending else "a_side_provider_name"
return queryset.order_by(order_field), True


class ZProviderColumn(BaseEndpointColumn):
"""Column for Z Side Provider."""

def render(self, record): # pylint: disable=arguments-renamed
"""Render Provider Z using inherited autonomous system."""
if record.endpoint_z:
asn, _, _ = record.endpoint_z.get_inherited_field("autonomous_system")
if asn and asn.provider:
return hyperlinked_object(asn.provider)
return None

def order(self, queryset, is_descending):
"""Custom ordering for Z Side Provider."""
second_endpoint_provider = (
PeerEndpoint.objects.filter(peering=django_models.OuterRef("pk"))
.order_by("pk")
.values("routing_instance__autonomous_system__provider__name")[1:2]
)

queryset = queryset.annotate(z_side_provider_name=django_models.Subquery(second_endpoint_provider))

order_field = "-z_side_provider_name" if is_descending else "z_side_provider_name"
return queryset.order_by(order_field), True
37 changes: 26 additions & 11 deletions nautobot_bgp_models/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@
)

from . import models
from .table_columns import (
AASNColumn,
ADeviceColumn,
AEndpointIPColumn,
AProviderColumn,
ZASNColumn,
ZDeviceColumn,
ZEndpointIPColumn,
ZProviderColumn,
)

ASN_LINK = """
{% if record.present_in_database %}
Expand Down Expand Up @@ -222,32 +232,37 @@ class Meta(BaseTable.Meta):
class PeeringTable(StatusTableMixin, BaseTable):
"""Table representation of Peering records."""

# TODO(mzb): Add columns: Device_A, Device_B, Provider_A, Provider_Z

pk = ToggleColumn()
peering = tables.LinkColumn(
viewname="plugins:nautobot_bgp_models:peering",
args=[A("pk")],
text=str,
orderable=False,
)
a_side_device = ADeviceColumn(verbose_name="A Side Device")
a_endpoint = AEndpointIPColumn(verbose_name="A Endpoint")
a_side_asn = AASNColumn(verbose_name="A Side ASN")
a_provider = AProviderColumn(verbose_name="A Provider")
z_side_device = ZDeviceColumn(verbose_name="Z Side Device")
z_endpoint = ZEndpointIPColumn(verbose_name="Z Endpoint")
z_side_asn = ZASNColumn(verbose_name="Z Side ASN")
z_provider = ZProviderColumn(verbose_name="Z Provider")

endpoint_a = tables.LinkColumn(
verbose_name="Endpoint", text=lambda x: str(x.endpoint_a.local_ip) if x.endpoint_a else None, orderable=False
)

endpoint_z = tables.LinkColumn(
verbose_name="Endpoint", text=lambda x: str(x.endpoint_z.local_ip) if x.endpoint_z else None, orderable=False
)
actions = ButtonsColumn(model=models.Peering)

class Meta(BaseTable.Meta):
model = models.Peering
fields = (
"pk",
"peering",
"endpoint_a",
"endpoint_z",
"a_side_device",
"a_endpoint",
"a_side_asn",
"a_provider",
"z_side_device",
"z_endpoint",
"z_side_asn",
"z_provider",
"status",
)

Expand Down
Loading
Loading