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:
@ -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']
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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')
|
||||
)
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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(),
|
||||
|
@ -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(
|
||||
|
@ -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'],
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
57
netbox/ipam/migrations/0052_fhrpgroup.py
Normal file
57
netbox/ipam/migrations/0052_fhrpgroup.py
Normal 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')},
|
||||
},
|
||||
),
|
||||
]
|
@ -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',
|
||||
|
92
netbox/ipam/models/fhrp.py
Normal file
92
netbox/ipam/models/fhrp.py
Normal 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'
|
@ -1,3 +1,4 @@
|
||||
from .fhrp import *
|
||||
from .ip import *
|
||||
from .services import *
|
||||
from .vlans import *
|
||||
|
64
netbox/ipam/tables/fhrp.py
Normal file
64
netbox/ipam/tables/fhrp.py
Normal 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')
|
@ -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,
|
||||
|
@ -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']
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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']),
|
||||
),
|
||||
),
|
||||
|
@ -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>
|
||||
|
49
netbox/templates/inc/panels/nhrp_groups.html
Normal file
49
netbox/templates/inc/panels/nhrp_groups.html
Normal 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>
|
82
netbox/templates/ipam/fhrpgroup.html
Normal file
82
netbox/templates/ipam/fhrpgroup.html
Normal 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 %}
|
@ -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">—</span>
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
Reference in New Issue
Block a user