Skip to content

Commit c8ab3f9

Browse files
authored
Merge pull request #94 from k01ek/develop
Develop
2 parents 2d1cc44 + 508f867 commit c8ab3f9

File tree

11 files changed

+350
-14
lines changed

11 files changed

+350
-14
lines changed

netbox_bgp/api/serializers.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from netbox_bgp.models import (
1313
ASN, ASNStatusChoices, BGPSession, SessionStatusChoices, RoutingPolicy, BGPPeerGroup,
14-
Community
14+
Community, RoutingPolicyRule
1515
)
1616

1717

@@ -161,11 +161,25 @@ def to_representation(self, instance):
161161
)
162162
return ret
163163

164+
class NestedBGPSessionSerializer(WritableNestedSerializer):
165+
url = HyperlinkedIdentityField(view_name='plugins:netbox_bgp:bgpsession')
166+
167+
class Meta:
168+
model = BGPSession
169+
fields = ['id', 'url', 'name', 'description']
170+
validators = []
164171

165172
class CommunitySerializer(NetBoxModelSerializer):
166173
status = ChoiceField(choices=ASNStatusChoices, required=False)
167174
tenant = NestedTenantSerializer(required=False, allow_null=True)
168175

169176
class Meta:
170177
model = Community
171-
fields = ['id', 'value', 'status', 'description', 'tenant', 'tags']
178+
# fields = ['id', 'value', 'status', 'description', 'tenant', 'tags']
179+
fields = '__all__'
180+
181+
182+
class RoutingPolicyRuleSerializer(NetBoxModelSerializer):
183+
class Meta:
184+
model = RoutingPolicyRule
185+
fields = '__all__'

netbox_bgp/api/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
router = routers.DefaultRouter()
99
router.register('asn', ASNViewSet)
1010
router.register('session', BGPSessionViewSet, 'session')
11+
router.register('bgpsession', BGPSessionViewSet, 'bgpsession')
1112
router.register('routing-policy', RoutingPolicyViewSet)
1213
router.register('peer-group', BGPPeerGroupViewSet, 'peergroup')
14+
router.register('bgppeergroup', BGPPeerGroupViewSet, 'bgppeergroup')
1315
router.register('community', CommunityViewSet)
1416

1517

netbox_bgp/forms.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from extras.models import Tag
99
from tenancy.models import Tenant
1010
from dcim.models import Device, Site
11-
from ipam.models import IPAddress
11+
from ipam.models import IPAddress, Prefix
1212
from ipam.formfields import IPNetworkFormField
1313
from utilities.forms import (
1414
DynamicModelChoiceField,
@@ -19,7 +19,7 @@
1919

2020
from .models import (
2121
ASN, ASNStatusChoices, Community, BGPSession,
22-
SessionStatusChoices, RoutingPolicy, BGPPeerGroup
22+
SessionStatusChoices, RoutingPolicy, BGPPeerGroup, RoutingPolicyRule
2323
)
2424

2525

@@ -457,3 +457,38 @@ class BGPPeerGroupForm(NetBoxModelForm):
457457
class Meta:
458458
model = BGPPeerGroup
459459
fields = ['name', 'description', 'import_policies', 'export_policies', 'tags']
460+
461+
462+
class RoutingPolicyRuleForm(NetBoxModelForm):
463+
match_community = DynamicModelMultipleChoiceField(
464+
queryset=Community.objects.all(),
465+
required=False,
466+
)
467+
match_ip = DynamicModelMultipleChoiceField(
468+
queryset=Prefix.objects.all(),
469+
required=False,
470+
label='Match Prefix',
471+
)
472+
match_ip_cond = forms.JSONField(
473+
label='Match filtered prefixes',
474+
help_text='Filter for Prefixes, e.g., {"site__name": "site1", "tenant__name": "tenant1"}',
475+
required=False,
476+
)
477+
match_custom = forms.JSONField(
478+
label='Custom Match',
479+
help_text='Any custom match statements, e.g., {"ip nexthop": "1.1.1.1"}',
480+
required=False,
481+
)
482+
set_actions = forms.JSONField(
483+
label='Set statements',
484+
help_text='Set statements, e.g., {"as-path prepend": [12345,12345]}',
485+
required=False
486+
)
487+
488+
class Meta:
489+
model = RoutingPolicyRule
490+
fields = [
491+
'routing_policy', 'index', 'action', 'match_community',
492+
'match_ip', 'match_ip_cond', 'match_custom',
493+
'set_actions', 'description',
494+
]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Generated by Django 4.0.3 on 2022-05-12 07:42
2+
3+
import django.core.serializers.json
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
import taggit.managers
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('ipam', '0057_created_datetimefield'),
13+
('extras', '0073_journalentry_tags_custom_fields'),
14+
('netbox_bgp', '0021_netbox32_support'),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='RoutingPolicyRule',
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=django.core.serializers.json.DjangoJSONEncoder)),
25+
('index', models.PositiveIntegerField()),
26+
('action', models.CharField(max_length=30)),
27+
('description', models.CharField(blank=True, max_length=500)),
28+
('match_ip_cond', models.JSONField(blank=True, null=True)),
29+
('match_custom', models.JSONField(blank=True, null=True)),
30+
('set_actions', models.JSONField(blank=True, null=True)),
31+
('match_community', models.ManyToManyField(blank=True, related_name='+', to='netbox_bgp.community')),
32+
('match_ip', models.ManyToManyField(blank=True, related_name='+', to='ipam.prefix')),
33+
('routing_policy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='netbox_bgp.routingpolicy')),
34+
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
35+
],
36+
options={
37+
'ordering': ('routing_policy', 'index'),
38+
'unique_together': {('routing_policy', 'index')},
39+
},
40+
),
41+
]

netbox_bgp/models.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from django.urls import reverse
22
from django.db import models
33
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
4+
from django.core.exceptions import FieldError
45
from django.conf import settings
56

67
from taggit.managers import TaggableManager
78

89
from utilities.choices import ChoiceSet
910
from netbox.models import NetBoxModel
1011
from netbox.models.features import ChangeLoggingMixin
12+
from ipam.models import Prefix
1113

1214

1315
class ASNStatusChoices(ChoiceSet):
@@ -38,6 +40,14 @@ class SessionStatusChoices(ChoiceSet):
3840
)
3941

4042

43+
class ActionChoices(ChoiceSet):
44+
45+
CHOICES = [
46+
('permit', 'Permit', 'green'),
47+
('deny', 'Deny', 'red'),
48+
]
49+
50+
4151
class ASNGroup(ChangeLoggingMixin, models.Model):
4252
"""
4353
"""
@@ -306,3 +316,97 @@ def get_status_color(self):
306316

307317
def get_absolute_url(self):
308318
return reverse('plugins:netbox_bgp:bgpsession', args=[self.pk])
319+
320+
321+
class RoutingPolicyRule(NetBoxModel):
322+
routing_policy = models.ForeignKey(
323+
to=RoutingPolicy,
324+
on_delete=models.CASCADE,
325+
related_name='rules'
326+
)
327+
index = models.PositiveIntegerField()
328+
action = models.CharField(
329+
max_length=30,
330+
choices=ActionChoices
331+
)
332+
description = models.CharField(
333+
max_length=500,
334+
blank=True
335+
)
336+
match_community = models.ManyToManyField(
337+
to=Community,
338+
blank=True,
339+
related_name='+'
340+
)
341+
match_ip = models.ManyToManyField(
342+
to='ipam.Prefix',
343+
blank=True,
344+
related_name='+',
345+
)
346+
match_ip_cond = models.JSONField(
347+
blank=True,
348+
null=True,
349+
)
350+
match_custom = models.JSONField(
351+
blank=True,
352+
null=True,
353+
)
354+
set_actions = models.JSONField(
355+
blank=True,
356+
null=True,
357+
)
358+
359+
class Meta:
360+
ordering = ('routing_policy', 'index')
361+
unique_together = ('routing_policy', 'index')
362+
363+
def __str__(self):
364+
return f'{self.routing_policy}: Rule {self.index}'
365+
366+
def get_absolute_url(self):
367+
return reverse('plugins:netbox_bgp:routingpolicyrule', args=[self.pk])
368+
369+
def get_action_color(self):
370+
return ActionChoices.colors.get(self.action)
371+
372+
def get_ip_conditions(self):
373+
queryset = Prefix.objects.none()
374+
if self.match_ip_cond and self.match_ip_cond != {}:
375+
try:
376+
queryset = Prefix.objects.filter(**self.match_ip_cond)
377+
except FieldError:
378+
pass
379+
return queryset
380+
381+
def get_match_custom(self):
382+
# some kind of ckeck?
383+
result = {}
384+
if self.match_custom:
385+
result = self.match_custom
386+
return result
387+
388+
@property
389+
def match_statements(self):
390+
result = {}
391+
# add communities
392+
result.update(
393+
{'community': list(self.match_community.all().values_list('value', flat=True))}
394+
)
395+
result.update(
396+
{'ip address': [str(prefix) for prefix in self.match_ip.all().values_list('prefix', flat=True)]}
397+
)
398+
matched_ip = self.get_ip_conditions()
399+
result['ip address'].extend([str(prefix) for prefix in matched_ip.values_list('prefix', flat=True)])
400+
custom_match = self.get_match_custom()
401+
# update community from custom
402+
result['community'].extend(custom_match.get('community', []))
403+
result['ip address'].extend(custom_match.get('ip address', []))
404+
# remove empty matches
405+
result = {k: v for k, v in result.items() if v}
406+
return result
407+
408+
@property
409+
def set_statements(self):
410+
if self.set_actions:
411+
return self.set_actions
412+
return {}

netbox_bgp/tables.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from netbox.tables import NetBoxTable
66
from netbox.tables.columns import ChoiceFieldColumn, TagColumn
77

8-
from .models import ASN, Community, BGPSession, RoutingPolicy, BGPPeerGroup
8+
from .models import ASN, Community, BGPSession, RoutingPolicy, BGPPeerGroup, RoutingPolicyRule
99

1010
AVAILABLE_LABEL = mark_safe('<span class="label label-success">Available</span>')
1111
COL_TENANT = """
@@ -121,3 +121,20 @@ class Meta(NetBoxTable.Meta):
121121
default_columns = (
122122
'pk', 'name', 'description'
123123
)
124+
125+
126+
class RoutingPolicyRuleTable(NetBoxTable):
127+
routing_policy = tables.Column(
128+
linkify=True
129+
)
130+
index = tables.Column(
131+
linkify=True
132+
)
133+
action = ChoiceFieldColumn()
134+
135+
class Meta(NetBoxTable.Meta):
136+
model = RoutingPolicyRule
137+
fields = (
138+
'pk', 'routing_policy', 'index', 'match_statements',
139+
'set_statements', 'action', 'description'
140+
)

netbox_bgp/templates/netbox_bgp/routingpolicy.html

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
{% endblock %}
1111
{% block controls %}
1212
<div class="pull-right noprint">
13+
{% if perms.netbox_bgp.change_policy %}
14+
<a href="{% url 'plugins:netbox_bgp:routingpolicyrule_add' %}?routing_policy={{ object.pk }}" class="btn btn-success">
15+
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Rule
16+
</a>
17+
{% endif %}
1318
{% if perms.netbox_bgp.change_policy %}
1419
<a href="{% url 'plugins:netbox_bgp:routingpolicy_edit' pk=object.pk %}" class="btn btn-warning">
1520
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
@@ -75,7 +80,15 @@ <h5 class="card-header">
7580
</div>
7681
<div class="row">
7782
<div class="col-md-12">
78-
{% plugin_full_width_page object %}
83+
<div class="card">
84+
<h5 class="card-header">
85+
Rules
86+
</h5>
87+
<div class="card-body">
88+
{% render_table rules_table 'inc/table.html' %}
89+
</div>
90+
{% plugin_full_width_page object %}
91+
</div>
7992
</div>
8093
</div>
8194
{% endblock %}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{% extends 'generic/object.html' %}
2+
{% load helpers %}
3+
{% load static %}
4+
5+
{% block content %}
6+
<div class="row mb-3">
7+
<div class="col col-md-5">
8+
<div class="card">
9+
<h5 class="card-header">Routing Policy Rule</h5>
10+
<div class="card-body">
11+
<table class="table table-hover attr-table">
12+
<tr>
13+
<th scope="row">Routing Policy</th>
14+
<td>
15+
<a href="{{ object.routing_policy.get_absolute_url }}">{{ object.routing_policy }}</a>
16+
</td>
17+
</tr>
18+
<tr>
19+
<th scope="row">Index</th>
20+
<td>{{ object.index }}</td>
21+
</tr>
22+
<tr>
23+
<th scope="row">Action</th>
24+
<td>{% badge object.get_action_display bg_color=object.get_action_color %}</td>
25+
</tr>
26+
<tr>
27+
<th scope="row">Description</th>
28+
<td>{{ object.description|placeholder }}</td>
29+
</tr>
30+
</table>
31+
</div>
32+
</div>
33+
{% include 'inc/panels/custom_fields.html' %}
34+
{% include 'inc/panels/tags.html' %}
35+
</div>
36+
<div class="col col-md-7">
37+
<div class="card">
38+
<div class="card-header">
39+
<h5>Statements</h5>
40+
<div class="float-end">
41+
<div class="btn-group btn-group-sm" role="group">
42+
<a href="?format=json" type="button" class="btn btn-outline-dark{% if format == 'json' %} active{% endif %}">JSON</a>
43+
<a href="?format=yaml" type="button" class="btn btn-outline-dark{% if format == 'yaml' %} active{% endif %}">YAML</a>
44+
</div>
45+
</div>
46+
</div>
47+
<div class="card-body">
48+
Match
49+
<div class="rendered-context-data">
50+
<pre class="block">{% if format == 'json' %}{{ object.match_statements|json }}{% elif format == 'yaml' %}{{ object.match_statements|yaml }}{% else %}{{ object.match_statements }}{% endif %}</pre>
51+
</div>
52+
Set
53+
<div class="rendered-context-data">
54+
<pre class="block">{% if format == 'json' %}{{ object.set_statements|json }}{% elif format == 'yaml' %}{{ object.set_statements|yaml }}{% else %}{{ object.set_statements }}{% endif %}</pre>
55+
</div>
56+
</div>
57+
</div>
58+
</div>
59+
</div>
60+
{% endblock content %}

0 commit comments

Comments
 (0)