1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Initial work on #6235

This commit is contained in:
jeremystretch
2021-11-01 16:14:44 -04:00
parent e0230ed104
commit bb4f3e1789
33 changed files with 959 additions and 17 deletions

View File

@ -599,6 +599,12 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
object_id_field='assigned_object_id',
related_query_name='interface'
)
fhrp_group_assignments = GenericRelation(
to='ipam.FHRPGroupAssignment',
content_type_field='content_type',
object_id_field='object_id',
related_query_name='interface'
)
clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only']

View File

@ -15,7 +15,7 @@ from django.views.generic import View
from circuits.models import Circuit
from extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectJournalView
from ipam.models import IPAddress, Prefix, Service, VLAN
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
from netbox.views import generic
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count
@ -1741,7 +1741,7 @@ class InterfaceView(generic.ObjectView):
def get_extra_context(self, request, instance):
# Get assigned IP addresses
ipaddress_table = InterfaceIPAddressTable(
ipaddress_table = AssignedIPAddressesTable(
data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
orderable=False
)

View File

@ -5,6 +5,7 @@ from netbox.api import WritableNestedSerializer
__all__ = [
'NestedAggregateSerializer',
'NestedFHRPGroupSerializer',
'NestedIPAddressSerializer',
'NestedIPRangeSerializer',
'NestedPrefixSerializer',
@ -65,6 +66,18 @@ class NestedAggregateSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'family', 'prefix']
#
# FHRP groups
#
class NestedFHRPGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
class Meta:
model = models.FHRPGroup
fields = ['id', 'url', 'display', 'protocol', 'group_id']
#
# VLANs
#

View File

@ -92,6 +92,43 @@ class AggregateSerializer(PrimaryModelSerializer):
read_only_fields = ['family']
#
# FHRP Groups
#
class FHRPGroupSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
class Meta:
model = FHRPGroup
fields = [
'id', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'tags',
'custom_fields', 'created', 'last_updated',
]
class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
content_type = ContentTypeField(
queryset=ContentType.objects.all()
)
object = serializers.SerializerMethodField(read_only=True)
class Meta:
model = FHRPGroupAssignment
fields = [
'id', 'url', 'display', 'content_type', 'object_id', 'object', 'priority', 'created', 'last_updated',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_object(self, obj):
if obj.object is None:
return None
serializer = get_serializer_for_model(obj.object, prefix='Nested')
context = {'request': self.context['request']}
return serializer(obj.object, context=context).data
#
# VLANs
#

View File

@ -27,6 +27,10 @@ router.register('ip-ranges', views.IPRangeViewSet)
# IP addresses
router.register('ip-addresses', views.IPAddressViewSet)
# FHRP groups
router.register('fhrp-groups', views.FHRPGroupViewSet)
router.register('fhrp-group-assignments', views.FHRPGroupAssignmentViewSet)
# VLANs
router.register('vlan-groups', views.VLANGroupViewSet)
router.register('vlans', views.VLANViewSet)

View File

@ -119,6 +119,22 @@ class IPAddressViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.IPAddressFilterSet
#
# FHRP groups
#
class FHRPGroupViewSet(CustomFieldModelViewSet):
queryset = FHRPGroup.objects.prefetch_related('tags')
serializer_class = serializers.FHRPGroupSerializer
filterset_class = filtersets.FHRPGroupFilterSet
class FHRPGroupAssignmentViewSet(CustomFieldModelViewSet):
queryset = FHRPGroupAssignment.objects.prefetch_related('group')
serializer_class = serializers.FHRPGroupAssignmentSerializer
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
#
# VLAN groups
#

View File

@ -124,6 +124,38 @@ class IPAddressRoleChoices(ChoiceSet):
}
#
# FHRP
#
class FHRPGroupProtocolChoices(ChoiceSet):
PROTOCOL_VRRP2 = 'vrrp2'
PROTOCOL_VRRP3 = 'vrrp3'
PROTOCOL_HSRP = 'hsrp'
PROTOCOL_GLBP = 'glbp'
PROTOCOL_CARP = 'carp'
CHOICES = (
(PROTOCOL_VRRP2, 'VRRPv2'),
(PROTOCOL_VRRP3, 'VRRPv3'),
(PROTOCOL_HSRP, 'HSRP'),
(PROTOCOL_GLBP, 'GLBP'),
(PROTOCOL_CARP, 'CARP'),
)
class FHRPGroupAuthTypeChoices(ChoiceSet):
AUTHENTICATION_PLAINTEXT = 'plaintext'
AUTHENTICATION_MD5 = 'md5'
CHOICES = (
(AUTHENTICATION_PLAINTEXT, 'Plaintext'),
(AUTHENTICATION_MD5, 'MD5'),
)
#
# VLANs
#

View File

@ -34,6 +34,7 @@ PREFIX_LENGTH_MAX = 127 # IPv6
IPADDRESS_ASSIGNMENT_MODELS = Q(
Q(app_label='dcim', model='interface') |
Q(app_label='ipam', model='fhrpgroup') |
Q(app_label='virtualization', model='vminterface')
)

View File

@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError
from dcim.models import Device, Interface, Region, Site, SiteGroup
from extras.filters import TagFilter
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
@ -19,6 +19,8 @@ from .models import *
__all__ = (
'AggregateFilterSet',
'FHRPGroupAssignmentFilterSet',
'FHRPGroupFilterSet',
'IPAddressFilterSet',
'IPRangeFilterSet',
'PrefixFilterSet',
@ -611,6 +613,39 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
return queryset.exclude(assigned_object_id__isnull=value)
class FHRPGroupFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
protocol = django_filters.MultipleChoiceFilter(
choices=FHRPGroupProtocolChoices
)
auth_type = django_filters.MultipleChoiceFilter(
choices=FHRPGroupAuthTypeChoices
)
tag = TagFilter()
class Meta:
model = FHRPGroup
fields = ['id', 'protocol', 'group_id', 'auth_type']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(description__icontains=value)
)
class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
content_type = ContentTypeFilter()
class Meta:
model = FHRPGroupAssignment
fields = ['id', 'content_type_id', 'priority']
class VLANGroupFilterSet(OrganizationalModelFilterSet):
q = django_filters.CharFilter(
method='search',

View File

@ -13,6 +13,7 @@ from utilities.forms import (
__all__ = (
'AggregateBulkEditForm',
'FHRPGroupBulkEditForm',
'IPAddressBulkEditForm',
'IPRangeBulkEditForm',
'PrefixBulkEditForm',
@ -280,6 +281,41 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
]
class FHRPGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=FHRPGroup.objects.all(),
widget=forms.MultipleHiddenInput()
)
protocol = forms.ChoiceField(
choices=add_blank_choice(FHRPGroupProtocolChoices),
required=False,
widget=StaticSelect()
)
group_id = forms.IntegerField(
min_value=0,
required=False,
label='Group ID'
)
auth_type = forms.ChoiceField(
choices=add_blank_choice(FHRPGroupAuthTypeChoices),
required=False,
widget=StaticSelect(),
label='Authentication type'
)
auth_key = forms.CharField(
max_length=255,
required=False,
label='Authentication key'
)
description = forms.CharField(
max_length=200,
required=False
)
class Meta:
nullable_fields = ['auth_type', 'auth_key', 'description']
class VLANGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=VLANGroup.objects.all(),

View File

@ -12,6 +12,7 @@ from virtualization.models import VirtualMachine, VMInterface
__all__ = (
'AggregateCSVForm',
'FHRPGroupCSVForm',
'IPAddressCSVForm',
'IPRangeCSVForm',
'PrefixCSVForm',
@ -283,6 +284,20 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
return ipaddress
class FHRPGroupCSVForm(CustomFieldModelCSVForm):
protocol = CSVChoiceField(
choices=FHRPGroupProtocolChoices
)
auth_type = CSVChoiceField(
choices=FHRPGroupAuthTypeChoices,
required=False
)
class Meta:
model = FHRPGroup
fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'description')
class VLANGroupCSVForm(CustomFieldModelCSVForm):
slug = SlugField()
scope_type = CSVContentTypeField(

View File

@ -14,6 +14,7 @@ from utilities.forms import (
__all__ = (
'AggregateFilterForm',
'FHRPGroupFilterForm',
'IPAddressFilterForm',
'IPRangeFilterForm',
'PrefixFilterForm',
@ -356,6 +357,41 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFil
tag = TagFilterField(model)
class FHRPGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
model = FHRPGroup
field_groups = (
('q', 'tag'),
('protocol', 'group_id'),
('auth_type', 'auth_key'),
)
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
protocol = forms.MultipleChoiceField(
choices=FHRPGroupProtocolChoices,
required=False,
widget=StaticSelectMultiple()
)
group_id = forms.IntegerField(
min_value=0,
required=False,
label='Group ID'
)
auth_type = forms.MultipleChoiceField(
choices=FHRPGroupAuthTypeChoices,
required=False,
widget=StaticSelectMultiple(),
label='Authentication type'
)
auth_key = forms.CharField(
required=False,
label='Authentication key'
)
tag = TagFilterField(model)
class VLANGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
field_groups = [
['q', 'tag'],

View File

@ -4,17 +4,22 @@ from django.contrib.contenttypes.models import ContentType
from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
from extras.forms import CustomFieldModelForm
from extras.models import Tag
from ipam.choices import *
from ipam.constants import *
from ipam.formfields import IPNetworkFormField
from ipam.models import *
from tenancy.forms import TenancyForm
from utilities.exceptions import PermissionsViolation
from utilities.forms import (
BootstrapMixin, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple,
add_blank_choice, BootstrapMixin, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple,
)
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
__all__ = (
'AggregateForm',
'FHRPGroupForm',
'FHRPGroupAssignmentForm',
'IPAddressAssignForm',
'IPAddressBulkAddForm',
'IPAddressForm',
@ -472,6 +477,76 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
)
class FHRPGroupForm(BootstrapMixin, CustomFieldModelForm):
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
# Optionally create a new IPAddress along with the NHRPGroup
ip_vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF'
)
ip_address = IPNetworkFormField(
required=False,
label='Address'
)
ip_status = forms.ChoiceField(
choices=add_blank_choice(IPAddressStatusChoices),
required=False,
label='Status'
)
class Meta:
model = FHRPGroup
fields = (
'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_vrf', 'ip_address', 'ip_status', 'tags',
)
fieldsets = (
('FHRP Group', ('protocol', 'group_id', 'description', 'tags')),
('Authentication', ('auth_type', 'auth_key')),
('Virtual IP Address', ('ip_vrf', 'ip_address', 'ip_status'))
)
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
# Check if we need to create a new IPAddress for the group
if self.cleaned_data.get('ip_address'):
ipaddress = IPAddress(
vrf=self.cleaned_data['ip_vrf'],
address=self.cleaned_data['ip_address'],
status=self.cleaned_data['ip_status'],
assigned_object=instance
)
ipaddress.role = {
FHRPGroupProtocolChoices.PROTOCOL_VRRP2: IPAddressRoleChoices.ROLE_VRRP,
FHRPGroupProtocolChoices.PROTOCOL_VRRP3: IPAddressRoleChoices.ROLE_VRRP,
FHRPGroupProtocolChoices.PROTOCOL_HSRP: IPAddressRoleChoices.ROLE_HSRP,
FHRPGroupProtocolChoices.PROTOCOL_GLBP: IPAddressRoleChoices.ROLE_GLBP,
FHRPGroupProtocolChoices.PROTOCOL_CARP: IPAddressRoleChoices.ROLE_CARP,
}[self.cleaned_data['protocol']]
ipaddress.save()
# Check that the new IPAddress conforms with any assigned object-level permissions
if not IPAddress.objects.filter(pk=ipaddress.pk).first():
raise PermissionsViolation()
return instance
class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm):
group = DynamicModelChoiceField(
queryset=FHRPGroup.objects.all()
)
class Meta:
model = FHRPGroupAssignment
fields = ('group', 'priority')
class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
scope_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),

View File

@ -29,6 +29,12 @@ class IPAMQuery(graphene.ObjectType):
service = ObjectField(ServiceType)
service_list = ObjectListField(ServiceType)
fhrp_group = ObjectField(FHRPGroupType)
fhrp_group_list = ObjectListField(FHRPGroupType)
fhrp_group_assignment = ObjectField(FHRPGroupAssignmentType)
fhrp_group_assignment_list = ObjectListField(FHRPGroupAssignmentType)
vlan = ObjectField(VLANType)
vlan_list = ObjectListField(VLANType)

View File

@ -3,6 +3,8 @@ from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
__all__ = (
'AggregateType',
'FHRPGroupType',
'FHRPGroupAssignmentType',
'IPAddressType',
'IPRangeType',
'PrefixType',
@ -24,6 +26,25 @@ class AggregateType(PrimaryObjectType):
filterset_class = filtersets.AggregateFilterSet
class FHRPGroupType(PrimaryObjectType):
class Meta:
model = models.FHRPGroup
fields = '__all__'
filterset_class = filtersets.FHRPGroupFilterSet
def resolve_auth_type(self, info):
return self.auth_type or None
class FHRPGroupAssignmentType(PrimaryObjectType):
class Meta:
model = models.FHRPGroupAssignment
fields = '__all__'
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
class IPAddressType(PrimaryObjectType):
class Meta:

View File

@ -0,0 +1,57 @@
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('extras', '0064_configrevision'),
('contenttypes', '0002_remove_content_type_name'),
('ipam', '0051_extend_tag_support'),
]
operations = [
migrations.CreateModel(
name='FHRPGroup',
fields=[
('created', models.DateField(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)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('group_id', models.PositiveSmallIntegerField()),
('protocol', models.CharField(max_length=50)),
('auth_type', models.CharField(blank=True, max_length=50)),
('auth_key', models.CharField(blank=True, max_length=255)),
('description', models.CharField(blank=True, max_length=200)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'verbose_name': 'FHRP group',
'ordering': ['protocol', 'group_id', 'pk'],
},
),
migrations.AlterField(
model_name='ipaddress',
name='assigned_object_type',
field=models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'ipam'), ('model', 'fhrpgroup')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype'),
),
migrations.CreateModel(
name='FHRPGroupAssignment',
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('object_id', models.PositiveIntegerField()),
('priority', models.PositiveSmallIntegerField(blank=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ipam.fhrpgroup')),
],
options={
'verbose_name': 'FHRP group assignment',
'ordering': ('priority', 'pk'),
'unique_together': {('content_type', 'object_id', 'group')},
},
),
]

View File

@ -1,3 +1,4 @@
from .fhrp import *
from .ip import *
from .services import *
from .vlans import *
@ -7,6 +8,8 @@ __all__ = (
'Aggregate',
'IPAddress',
'IPRange',
'FHRPGroup',
'FHRPGroupAssignment',
'Prefix',
'RIR',
'Role',

View File

@ -0,0 +1,92 @@
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.urls import reverse
from extras.utils import extras_features
from netbox.models import ChangeLoggedModel, PrimaryModel
from ipam.choices import *
from utilities.querysets import RestrictedQuerySet
__all__ = (
'FHRPGroup',
'FHRPGroupAssignment',
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class FHRPGroup(PrimaryModel):
"""
A grouping of next hope resolution protocol (FHRP) peers. (For instance, VRRP or HSRP.)
"""
group_id = models.PositiveSmallIntegerField(
verbose_name='Group ID'
)
protocol = models.CharField(
max_length=50,
choices=FHRPGroupProtocolChoices
)
auth_type = models.CharField(
max_length=50,
choices=FHRPGroupAuthTypeChoices,
blank=True,
verbose_name='Authentication type'
)
auth_key = models.CharField(
max_length=255,
blank=True,
verbose_name='Authentication key'
)
description = models.CharField(
max_length=200,
blank=True
)
ip_addresses = GenericRelation(
to='ipam.IPAddress',
content_type_field='assigned_object_type',
object_id_field='assigned_object_id',
related_query_name='nhrp_group'
)
objects = RestrictedQuerySet.as_manager()
clone_fields = [
'protocol', 'auth_type', 'auth_key'
]
class Meta:
ordering = ['protocol', 'group_id', 'pk']
verbose_name = 'FHRP group'
def __str__(self):
return f'{self.get_protocol_display()} group {self.group_id}'
def get_absolute_url(self):
return reverse('ipam:fhrpgroup', args=[self.pk])
@extras_features('webhooks')
class FHRPGroupAssignment(ChangeLoggedModel):
content_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE
)
object_id = models.PositiveIntegerField()
object = GenericForeignKey(
ct_field='content_type',
fk_field='object_id'
)
group = models.ForeignKey(
to='ipam.FHRPGroup',
on_delete=models.CASCADE
)
priority = models.PositiveSmallIntegerField(
blank=True,
)
objects = RestrictedQuerySet.as_manager()
class Meta:
ordering = ('priority', 'pk')
unique_together = ('content_type', 'object_id', 'group')
verbose_name = 'FHRP group assignment'

View File

@ -1,3 +1,4 @@
from .fhrp import *
from .ip import *
from .services import *
from .vlans import *

View File

@ -0,0 +1,64 @@
import django_tables2 as tables
from utilities.tables import (
BaseTable, ContentTypeColumn, MarkdownColumn, TagColumn, ToggleColumn,
)
from ipam.models import *
__all__ = (
'FHRPGroupTable',
'FHRPGroupAssignmentTable',
)
IPADDRESSES = """
{% for ip in record.ip_addresses.all %}
<a href="{{ ip.get_absolute_url }}">{{ ip }}</a><br />
{% endfor %}
"""
class FHRPGroupTable(BaseTable):
pk = ToggleColumn()
group_id = tables.Column(
linkify=True
)
comments = MarkdownColumn()
ip_addresses = tables.TemplateColumn(
template_code=IPADDRESSES,
orderable=False,
verbose_name='IP Addresses'
)
member_count = tables.Column(
verbose_name='Members'
)
tags = TagColumn(
url_name='ipam:fhrpgroup_list'
)
class Meta(BaseTable.Meta):
model = FHRPGroup
fields = (
'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'member_count',
'tags',
)
default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'member_count')
class FHRPGroupAssignmentTable(BaseTable):
pk = ToggleColumn()
content_type = ContentTypeColumn(
verbose_name='Object Type'
)
object = tables.Column(
linkify=True,
orderable=False
)
group = tables.Column(
linkify=True
)
class Meta(BaseTable.Meta):
model = FHRPGroupAssignment
fields = ('pk', 'content_type', 'object', 'group', 'priority')
default_columns = ('pk', 'content_type', 'object', 'group', 'priority')

View File

@ -11,7 +11,7 @@ from ipam.models import *
__all__ = (
'AggregateTable',
'InterfaceIPAddressTable',
'AssignedIPAddressesTable',
'IPAddressAssignTable',
'IPAddressTable',
'IPRangeTable',
@ -359,9 +359,9 @@ class IPAddressAssignTable(BaseTable):
orderable = False
class InterfaceIPAddressTable(BaseTable):
class AssignedIPAddressesTable(BaseTable):
"""
List IP addresses assigned to a specific Interface.
List IP addresses assigned to an object.
"""
address = tables.Column(
linkify=True,

View File

@ -491,6 +491,47 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase):
IPAddress.objects.bulk_create(ip_addresses)
class FHRPGroupTest(APIViewTestCases.APIViewTestCase):
model = FHRPGroup
brief_fields = ['display', 'group_id', 'id', 'protocol', 'url']
bulk_update_data = {
'protocol': FHRPGroupProtocolChoices.PROTOCOL_GLBP,
'group_id': 200,
'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5,
'auth_key': 'foobarbaz999',
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
fhrp_groups = (
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=10, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT, auth_key='foobar123'),
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3, group_id=20, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, auth_key='foobar123'),
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP, group_id=30),
)
FHRPGroup.objects.bulk_create(fhrp_groups)
cls.create_data = [
{
'protocol': FHRPGroupProtocolChoices.PROTOCOL_VRRP2,
'group_id': 110,
'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT,
'auth_key': 'foobar123',
},
{
'protocol': FHRPGroupProtocolChoices.PROTOCOL_VRRP3,
'group_id': 120,
'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5,
'auth_key': 'barfoo456',
},
{
'protocol': FHRPGroupProtocolChoices.PROTOCOL_GLBP,
'group_id': 130,
},
]
class VLANGroupTest(APIViewTestCases.APIViewTestCase):
model = VLANGroup
brief_fields = ['display', 'id', 'name', 'slug', 'url', 'vlan_count']

View File

@ -795,6 +795,33 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = FHRPGroup.objects.all()
filterset = FHRPGroupFilterSet
@classmethod
def setUpTestData(cls):
fhrp_groups = (
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=10, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT, auth_key='foobar123'),
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3, group_id=20, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, auth_key='foobar123'),
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP, group_id=30),
)
FHRPGroup.objects.bulk_create(fhrp_groups)
def test_protocol(self):
params = {'protocol': [FHRPGroupProtocolChoices.PROTOCOL_VRRP2, FHRPGroupProtocolChoices.PROTOCOL_VRRP3]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_group_id(self):
params = {'group_id': [10, 20]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_auth_type(self):
params = {'auth_type': [FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT, FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VLANGroup.objects.all()
filterset = VLANGroupFilterSet

View File

@ -372,6 +372,41 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
class FHRPGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = FHRPGroup
@classmethod
def setUpTestData(cls):
FHRPGroup.objects.bulk_create((
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=10, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT, auth_key='foobar123'),
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3, group_id=20, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, auth_key='foobar123'),
FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP, group_id=30),
))
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'protocol': FHRPGroupProtocolChoices.PROTOCOL_VRRP2,
'group_id': 99,
'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5,
'auth_key': 'abc123def456',
'description': 'Blah blah blah',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"protocol,group_id,auth_type,auth_key,description",
"vrrp2,40,plaintext,foobar123,Foo",
"vrrp3,50,md5,foobar123,Bar",
"hsrp,60,,,",
)
cls.bulk_edit_data = {
'protocol': FHRPGroupProtocolChoices.PROTOCOL_CARP,
}
class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = VLANGroup

View File

@ -107,6 +107,23 @@ urlpatterns = [
path('ip-addresses/<int:pk>/edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
path('ip-addresses/<int:pk>/delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
# FHRP groups
path('fhrp-groups/', views.FHRPGroupListView.as_view(), name='fhrpgroup_list'),
path('fhrp-groups/add/', views.FHRPGroupEditView.as_view(), name='fhrpgroup_add'),
path('fhrp-groups/import/', views.FHRPGroupBulkImportView.as_view(), name='fhrpgroup_import'),
path('fhrp-groups/edit/', views.FHRPGroupBulkEditView.as_view(), name='fhrpgroup_bulk_edit'),
path('fhrp-groups/delete/', views.FHRPGroupBulkDeleteView.as_view(), name='fhrpgroup_bulk_delete'),
path('fhrp-groups/<int:pk>/', views.FHRPGroupView.as_view(), name='fhrpgroup'),
path('fhrp-groups/<int:pk>/edit/', views.FHRPGroupEditView.as_view(), name='fhrpgroup_edit'),
path('fhrp-groups/<int:pk>/delete/', views.FHRPGroupDeleteView.as_view(), name='fhrpgroup_delete'),
path('fhrp-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='fhrpgroup_changelog', kwargs={'model': FHRPGroup}),
path('fhrp-groups/<int:pk>/journal/', ObjectJournalView.as_view(), name='fhrpgroup_journal', kwargs={'model': FHRPGroup}),
# FHRP group assignments
path('fhrp-group-assignments/add/', views.FHRPGroupAssignmentEditView.as_view(), name='fhrpgroupassignment_add'),
path('fhrp-group-assignments/<int:pk>/edit/', views.FHRPGroupAssignmentEditView.as_view(), name='fhrpgroupassignment_edit'),
path('fhrp-group-assignments/<int:pk>/delete/', views.FHRPGroupAssignmentDeleteView.as_view(), name='fhrpgroupassignment_delete'),
# VLAN groups
path('vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'),
path('vlan-groups/add/', views.VLANGroupEditView.as_view(), name='vlangroup_add'),

View File

@ -1,10 +1,11 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import Prefetch
from django.db.models.expressions import RawSQL
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from dcim.models import Device, Interface
from netbox.views import generic
from utilities.forms import TableConfigForm
from utilities.tables import paginate_table
from utilities.utils import count_related
from virtualization.models import VirtualMachine, VMInterface
@ -825,6 +826,101 @@ class VLANGroupBulkDeleteView(generic.BulkDeleteView):
table = tables.VLANGroupTable
#
# FHRP groups
#
class FHRPGroupListView(generic.ObjectListView):
queryset = FHRPGroup.objects.annotate(
member_count=count_related(FHRPGroupAssignment, 'group')
)
filterset = filtersets.FHRPGroupFilterSet
filterset_form = forms.FHRPGroupFilterForm
table = tables.FHRPGroupTable
class FHRPGroupView(generic.ObjectView):
queryset = FHRPGroup.objects.all()
def get_extra_context(self, request, instance):
# Get assigned IP addresses
ipaddress_table = tables.AssignedIPAddressesTable(
data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
orderable=False
)
group_assignments = FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(
group=instance
)
members_table = tables.FHRPGroupAssignmentTable(group_assignments)
members_table.columns.hide('group')
paginate_table(members_table, request)
return {
'ipaddress_table': ipaddress_table,
'members_table': members_table,
'member_count': FHRPGroupAssignment.objects.filter(group=instance).count(),
}
class FHRPGroupEditView(generic.ObjectEditView):
queryset = FHRPGroup.objects.all()
model_form = forms.FHRPGroupForm
class FHRPGroupDeleteView(generic.ObjectDeleteView):
queryset = FHRPGroup.objects.all()
class FHRPGroupBulkImportView(generic.BulkImportView):
queryset = FHRPGroup.objects.all()
model_form = forms.FHRPGroupCSVForm
table = tables.FHRPGroupTable
class FHRPGroupBulkEditView(generic.BulkEditView):
queryset = FHRPGroup.objects.all()
filterset = filtersets.FHRPGroupFilterSet
table = tables.FHRPGroupTable
form = forms.FHRPGroupBulkEditForm
class FHRPGroupBulkDeleteView(generic.BulkDeleteView):
queryset = FHRPGroup.objects.all()
filterset = filtersets.FHRPGroupFilterSet
table = tables.FHRPGroupTable
#
# FHRP group assignments
#
class FHRPGroupAssignmentEditView(generic.ObjectEditView):
queryset = FHRPGroupAssignment.objects.all()
model_form = forms.FHRPGroupAssignmentForm
def alter_obj(self, instance, request, args, kwargs):
if not instance.pk:
# Assign the object based on URL kwargs
try:
app_label, model = request.GET.get('content_type').split('.')
except (AttributeError, ValueError):
raise Http404("Content type not specified")
content_type = get_object_or_404(ContentType, app_label=app_label, model=model)
instance.object = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id'))
return instance
def get_return_url(self, request, obj=None):
return obj.object.get_absolute_url() if obj else super().get_return_url(request)
class FHRPGroupAssignmentDeleteView(generic.ObjectDeleteView):
queryset = FHRPGroupAssignment.objects.all()
def get_return_url(self, request, obj=None):
return obj.object.get_absolute_url() if obj else super().get_return_url(request)
#
# VLANs
#

View File

@ -251,8 +251,9 @@ IPAM_MENU = Menu(
),
),
MenuGroup(
label='Services',
label='Other',
items=(
get_model_item('ipam', 'fhrpgroup', 'FHRP Groups'),
get_model_item('ipam', 'service', 'Services', actions=['import']),
),
),

View File

@ -440,6 +440,42 @@
</div>
</div>
{% endif %}
<div class="card">
<h5 class="card-header">NHRP Groups</h5>
<div class="card-body">
<table class="table table-hover table-headings">
<thead>
<tr>
<th>Group</th>
<th>Priority</th>
</tr>
</thead>
<tbody>
{% for assignment in object.fhrp_group_assignments.all %}
<tr>
<td>
<a href="{{ assignment.group.get_absolute_url }}">{{ assignment.group }}</a>
</td>
<td>
{{ assignment.priority }}
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-muted">None</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if perms.ipam.add_fhrpgroupassignment %}
<div class="card-footer text-end noprint">
<a href="{% url 'ipam:fhrpgroupassignment_add' %}?content_type=dcim.interface&object_id={{ object.pk }}" class="btn btn-sm btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> New Assignment
</a>
</div>
{% endif %}
</div>
{% plugin_right_page object %}
</div>
</div>

View File

@ -0,0 +1,49 @@
{% load helpers %}
<div class="card">
<h5 class="card-header">Contacts</h5>
<div class="card-body">
{% with fhrp_groups=object.fhrp_group_assignments.all %}
{% if contacts.exists %}
<table class="table table-hover">
<tr>
<th>Protocol</th>
<th>Group ID</th>
<th>Priority</th>
<th></th>
</tr>
{% for contact in contacts %}
<tr>
<td>
<a href="{{ contact.contact.get_absolute_url }}">{{ contact.contact }}</a>
</td>
<td>{{ contact.role|placeholder }}</td>
<td>{{ contact.get_priority_display|placeholder }}</td>
<td class="text-end noprint">
{% if perms.tenancy.change_contactassignment %}
<a href="{% url 'tenancy:contactassignment_edit' pk=contact.pk %}" class="btn btn-warning btn-sm lh-1" title="Edit">
<i class="mdi mdi-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.tenancy.delete_contactassignment %}
<a href="{% url 'extras:imageattachment_delete' pk=contact.pk %}" class="btn btn-danger btn-sm lh-1" title="Delete">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="text-muted">None</div>
{% endif %}
{% endwith %}
</div>
{% if perms.tenancy.add_contactassignment %}
<div class="card-footer text-end noprint">
<a href="{% url 'tenancy:contactassignment_add' %}?content_type={{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}&object_id={{ object.pk }}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a contact
</a>
</div>
{% endif %}
</div>

View File

@ -0,0 +1,82 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'ipam:fhrpgroup_list' %}?protocol={{ object.protocol }}">{{ object.get_protocol_display }}</a></li>
{% endblock breadcrumbs %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">FHRP Group</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<td>Protocol</td>
<td>{{ object.get_protocol_display }}</td>
</tr>
<tr>
<td>Group ID</td>
<td>{{ object.group_id }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">Members</th>
<td>{{ member_count }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/tags.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">Authentication</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<td>Authentication Type</td>
<td>{{ object.get_auth_type_display|placeholder }}</td>
</tr>
<tr>
<td>Authentication Key</td>
<td>{{ object.auth_key|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">IP Addresses</h5>
<div class="card-body">
{% if ipaddress_table.rows %}
{% render_table ipaddress_table 'inc/table.html' %}
{% else %}
<div class="text-muted">None</div>
{% endif %}
</div>
</div>
<div class="card">
<h5 class="card-header">Members</h5>
<div class="card-body">
{% include 'inc/table.html' with table=members_table %}
</div>
</div>
{% include 'inc/paginator.html' with paginator=members_table.paginator page=members_table.page %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -74,7 +74,9 @@
<th scope="row">Assignment</th>
<td>
{% if object.assigned_object %}
{% if object.assigned_object.parent_object %}
<a href="{{ object.assigned_object.parent_object.get_absolute_url }}">{{ object.assigned_object.parent_object }}</a> /
{% endif %}
<a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}
{% else %}
<span class="text-muted">&mdash;</span>

View File

@ -398,6 +398,12 @@ class VMInterface(PrimaryModel, BaseInterface):
object_id_field='assigned_object_id',
related_query_name='vminterface'
)
fhrp_group_assignments = GenericRelation(
to='ipam.FHRPGroupAssignment',
content_type_field='content_type',
object_id_field='object_id',
related_query_name='vminterface'
)
objects = RestrictedQuerySet.as_manager()

View File

@ -8,7 +8,7 @@ from dcim.models import Device
from dcim.tables import DeviceTable
from extras.views import ObjectConfigContextView
from ipam.models import IPAddress, Service
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
from netbox.views import generic
from utilities.tables import paginate_table
from utilities.utils import count_related
@ -421,7 +421,7 @@ class VMInterfaceView(generic.ObjectView):
def get_extra_context(self, request, instance):
# Get assigned IP addresses
ipaddress_table = InterfaceIPAddressTable(
ipaddress_table = AssignedIPAddressesTable(
data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
orderable=False
)