mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Merge branch 'feature' into 6732-asn-model
This commit is contained in:
commit
db2d71ed9e
@ -17,3 +17,7 @@
|
|||||||
|
|
||||||
{!models/ipam/vrf.md!}
|
{!models/ipam/vrf.md!}
|
||||||
{!models/ipam/routetarget.md!}
|
{!models/ipam/routetarget.md!}
|
||||||
|
|
||||||
|
__
|
||||||
|
|
||||||
|
{!models/ipam/fhrpgroup.md!}
|
||||||
|
14
docs/models/ipam/fhrpgroup.md
Normal file
14
docs/models/ipam/fhrpgroup.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# FHRP Group
|
||||||
|
|
||||||
|
A first-hop redundancy protocol (FHRP) enables multiple physical interfaces to present a virtual IP address in a redundant manner. Example of such protocols include:
|
||||||
|
|
||||||
|
* Hot Standby Router Protocol (HSRP)
|
||||||
|
* Virtual Router Redundancy Protocol (VRRP)
|
||||||
|
* Common Address Redundancy Protocol (CARP)
|
||||||
|
* Gateway Load Balancing Protocol (GLBP)
|
||||||
|
|
||||||
|
NetBox models these redundancy groups by protocol and group ID. Each group may optionally be assigned an authentication type and key. (Note that the authentication key is stored as a plaintext value in NetBox.) Each group may be assigned or more virtual IPv4 and/or IPv6 addresses.
|
||||||
|
|
||||||
|
## FHRP Group Assignments
|
||||||
|
|
||||||
|
Member device and VM interfaces can be assigned to FHRP groups, along with a numeric priority value. For instance, three interfaces, each belonging to a different router, may each be assigned to the same FHRP group to serve a common virtual IP address. Each of these assignments would typically receive a different priority.
|
@ -37,6 +37,10 @@ Dynamic configuration parameters may also still be defined within `configuration
|
|||||||
|
|
||||||
For a complete list of supported parameters, please see the [dynamic configuration documentation](../configuration/dynamic-settings.md).
|
For a complete list of supported parameters, please see the [dynamic configuration documentation](../configuration/dynamic-settings.md).
|
||||||
|
|
||||||
|
#### First Hop Redundancy Protocol (FHRP) Groups ([#6235](https://github.com/netbox-community/netbox/issues/6235))
|
||||||
|
|
||||||
|
A new FHRP group model has been introduced to aid in modeling the configurations of protocols such as HSRP, VRRP, and GLBP. Each FHRP group may be assigned one or more virtual IP addresses, as well as an authentication type and key. Member device and VM interfaces may be associated with one or more FHRP groups, with each assignment receiving a numeric priority designation.
|
||||||
|
|
||||||
#### Conditional Webhooks ([#6238](https://github.com/netbox-community/netbox/issues/6238))
|
#### Conditional Webhooks ([#6238](https://github.com/netbox-community/netbox/issues/6238))
|
||||||
|
|
||||||
Webhooks now include a `conditions` field, which may be used to specify conditions under which a webhook triggers. For example, you may wish to generate outgoing requests for a device webhook only when its status is "active" or "staged". This can be done by declaring conditional logic in JSON:
|
Webhooks now include a `conditions` field, which may be used to specify conditions under which a webhook triggers. For example, you may wish to generate outgoing requests for a device webhook only when its status is "active" or "staged". This can be done by declaring conditional logic in JSON:
|
||||||
|
@ -599,6 +599,12 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
|
|||||||
object_id_field='assigned_object_id',
|
object_id_field='assigned_object_id',
|
||||||
related_query_name='interface'
|
related_query_name='interface'
|
||||||
)
|
)
|
||||||
|
fhrp_group_assignments = GenericRelation(
|
||||||
|
to='ipam.FHRPGroupAssignment',
|
||||||
|
content_type_field='interface_type',
|
||||||
|
object_id_field='interface_id',
|
||||||
|
related_query_name='+'
|
||||||
|
)
|
||||||
|
|
||||||
clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only']
|
clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only']
|
||||||
|
|
||||||
|
@ -1747,7 +1747,7 @@ class InterfaceView(generic.ObjectView):
|
|||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
# Get assigned IP addresses
|
# Get assigned IP addresses
|
||||||
ipaddress_table = InterfaceIPAddressTable(
|
ipaddress_table = AssignedIPAddressesTable(
|
||||||
data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
|
data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
|
@ -6,6 +6,7 @@ from netbox.api import WritableNestedSerializer
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'NestedAggregateSerializer',
|
'NestedAggregateSerializer',
|
||||||
'NestedASNSerializer',
|
'NestedASNSerializer',
|
||||||
|
'NestedFHRPGroupSerializer',
|
||||||
'NestedIPAddressSerializer',
|
'NestedIPAddressSerializer',
|
||||||
'NestedIPRangeSerializer',
|
'NestedIPRangeSerializer',
|
||||||
'NestedPrefixSerializer',
|
'NestedPrefixSerializer',
|
||||||
@ -78,6 +79,18 @@ class NestedAggregateSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'family', 'prefix']
|
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
|
# VLANs
|
||||||
#
|
#
|
||||||
|
@ -110,6 +110,45 @@ class AggregateSerializer(PrimaryModelSerializer):
|
|||||||
read_only_fields = ['family']
|
read_only_fields = ['family']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# FHRP Groups
|
||||||
|
#
|
||||||
|
|
||||||
|
class FHRPGroupSerializer(PrimaryModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
|
||||||
|
ip_addresses = NestedIPAddressSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FHRPGroup
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_addresses',
|
||||||
|
'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
|
||||||
|
interface_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.all()
|
||||||
|
)
|
||||||
|
interface = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FHRPGroupAssignment
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'interface_type', 'interface_id', 'interface', 'priority', 'created',
|
||||||
|
'last_updated',
|
||||||
|
]
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
|
def get_interface(self, obj):
|
||||||
|
if obj.interface is None:
|
||||||
|
return None
|
||||||
|
serializer = get_serializer_for_model(obj.interface, prefix='Nested')
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.interface, context=context).data
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
@ -30,6 +30,10 @@ router.register('ip-ranges', views.IPRangeViewSet)
|
|||||||
# IP addresses
|
# IP addresses
|
||||||
router.register('ip-addresses', views.IPAddressViewSet)
|
router.register('ip-addresses', views.IPAddressViewSet)
|
||||||
|
|
||||||
|
# FHRP groups
|
||||||
|
router.register('fhrp-groups', views.FHRPGroupViewSet)
|
||||||
|
router.register('fhrp-group-assignments', views.FHRPGroupAssignmentViewSet)
|
||||||
|
|
||||||
# VLANs
|
# VLANs
|
||||||
router.register('vlan-groups', views.VLANGroupViewSet)
|
router.register('vlan-groups', views.VLANGroupViewSet)
|
||||||
router.register('vlans', views.VLANViewSet)
|
router.register('vlans', views.VLANViewSet)
|
||||||
|
@ -130,6 +130,22 @@ class IPAddressViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.IPAddressFilterSet
|
filterset_class = filtersets.IPAddressFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# FHRP groups
|
||||||
|
#
|
||||||
|
|
||||||
|
class FHRPGroupViewSet(CustomFieldModelViewSet):
|
||||||
|
queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')
|
||||||
|
serializer_class = serializers.FHRPGroupSerializer
|
||||||
|
filterset_class = filtersets.FHRPGroupFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class FHRPGroupAssignmentViewSet(CustomFieldModelViewSet):
|
||||||
|
queryset = FHRPGroupAssignment.objects.prefetch_related('group', 'interface')
|
||||||
|
serializer_class = serializers.FHRPGroupAssignmentSerializer
|
||||||
|
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLAN groups
|
# 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
|
# VLANs
|
||||||
#
|
#
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from .choices import IPAddressRoleChoices
|
from .choices import FHRPGroupProtocolChoices, IPAddressRoleChoices
|
||||||
|
|
||||||
# BGP ASN bounds
|
# BGP ASN bounds
|
||||||
BGP_ASN_MIN = 1
|
BGP_ASN_MIN = 1
|
||||||
@ -34,6 +34,7 @@ PREFIX_LENGTH_MAX = 127 # IPv6
|
|||||||
|
|
||||||
IPADDRESS_ASSIGNMENT_MODELS = Q(
|
IPADDRESS_ASSIGNMENT_MODELS = Q(
|
||||||
Q(app_label='dcim', model='interface') |
|
Q(app_label='dcim', model='interface') |
|
||||||
|
Q(app_label='ipam', model='fhrpgroup') |
|
||||||
Q(app_label='virtualization', model='vminterface')
|
Q(app_label='virtualization', model='vminterface')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,6 +52,22 @@ IPADDRESS_ROLES_NONUNIQUE = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# FHRP groups
|
||||||
|
#
|
||||||
|
|
||||||
|
FHRPGROUPASSIGNMENT_PRIORITY_MIN = 0
|
||||||
|
FHRPGROUPASSIGNMENT_PRIORITY_MAX = 255
|
||||||
|
|
||||||
|
FHRP_PROTOCOL_ROLE_MAPPINGS = {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError
|
|||||||
|
|
||||||
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
||||||
from extras.filters import TagFilter
|
from extras.filters import TagFilter
|
||||||
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
|
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||||
from tenancy.filtersets import TenancyFilterSet
|
from tenancy.filtersets import TenancyFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
@ -21,6 +21,8 @@ from .models import *
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateFilterSet',
|
'AggregateFilterSet',
|
||||||
'ASNFilterSet',
|
'ASNFilterSet',
|
||||||
|
'FHRPGroupAssignmentFilterSet',
|
||||||
|
'FHRPGroupFilterSet',
|
||||||
'IPAddressFilterSet',
|
'IPAddressFilterSet',
|
||||||
'IPRangeFilterSet',
|
'IPRangeFilterSet',
|
||||||
'PrefixFilterSet',
|
'PrefixFilterSet',
|
||||||
@ -648,6 +650,67 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
return queryset.exclude(assigned_object_id__isnull=value)
|
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
|
||||||
|
)
|
||||||
|
related_ip = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=IPAddress.objects.all(),
|
||||||
|
method='filter_related_ip'
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_related_ip(self, queryset, name, value):
|
||||||
|
"""
|
||||||
|
Filter by VRF & prefix of assigned IP addresses.
|
||||||
|
"""
|
||||||
|
ip_filter = Q()
|
||||||
|
for ipaddress in value:
|
||||||
|
if ipaddress.vrf:
|
||||||
|
q = Q(
|
||||||
|
ip_addresses__address__net_contained_or_equal=ipaddress.address,
|
||||||
|
ip_addresses__vrf=ipaddress.vrf
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
q = Q(
|
||||||
|
ip_addresses__address__net_contained_or_equal=ipaddress.address,
|
||||||
|
ip_addresses__vrf__isnull=True
|
||||||
|
)
|
||||||
|
ip_filter |= q
|
||||||
|
|
||||||
|
return queryset.filter(ip_filter)
|
||||||
|
|
||||||
|
|
||||||
|
class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
|
||||||
|
interface_type = ContentTypeFilter()
|
||||||
|
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=FHRPGroup.objects.all(),
|
||||||
|
label='Group (ID)',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FHRPGroupAssignment
|
||||||
|
fields = ['id', 'group_id', 'interface_type', 'interface_id', 'priority']
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
@ -15,6 +15,7 @@ from utilities.forms import (
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateBulkEditForm',
|
'AggregateBulkEditForm',
|
||||||
'ASNBulkEditForm',
|
'ASNBulkEditForm',
|
||||||
|
'FHRPGroupBulkEditForm',
|
||||||
'IPAddressBulkEditForm',
|
'IPAddressBulkEditForm',
|
||||||
'IPRangeBulkEditForm',
|
'IPRangeBulkEditForm',
|
||||||
'PrefixBulkEditForm',
|
'PrefixBulkEditForm',
|
||||||
@ -314,6 +315,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):
|
class VLANGroupBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
|
@ -13,6 +13,7 @@ from virtualization.models import VirtualMachine, VMInterface
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateCSVForm',
|
'AggregateCSVForm',
|
||||||
'ASNCSVForm',
|
'ASNCSVForm',
|
||||||
|
'FHRPGroupCSVForm',
|
||||||
'IPAddressCSVForm',
|
'IPAddressCSVForm',
|
||||||
'IPRangeCSVForm',
|
'IPRangeCSVForm',
|
||||||
'PrefixCSVForm',
|
'PrefixCSVForm',
|
||||||
@ -303,6 +304,20 @@ class IPAddressCSVForm(CustomFieldModelCSVForm):
|
|||||||
return ipaddress
|
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):
|
class VLANGroupCSVForm(CustomFieldModelCSVForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
scope_type = CSVContentTypeField(
|
scope_type = CSVContentTypeField(
|
||||||
|
@ -17,6 +17,7 @@ from utilities.forms import (
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateFilterForm',
|
'AggregateFilterForm',
|
||||||
'ASNFilterForm',
|
'ASNFilterForm',
|
||||||
|
'FHRPGroupFilterForm',
|
||||||
'IPAddressFilterForm',
|
'IPAddressFilterForm',
|
||||||
'IPRangeFilterForm',
|
'IPRangeFilterForm',
|
||||||
'PrefixFilterForm',
|
'PrefixFilterForm',
|
||||||
@ -386,6 +387,41 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFil
|
|||||||
tag = TagFilterField(model)
|
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):
|
class VLANGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||||
field_groups = [
|
field_groups = [
|
||||||
['q', 'tag'],
|
['q', 'tag'],
|
||||||
|
@ -4,19 +4,24 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
|
from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
|
||||||
from extras.forms import CustomFieldModelForm
|
from extras.forms import CustomFieldModelForm
|
||||||
from extras.models import Tag
|
from extras.models import Tag
|
||||||
|
from ipam.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
|
from ipam.formfields import IPNetworkFormField
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
|
from utilities.exceptions import PermissionsViolation
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
BootstrapMixin, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
add_blank_choice, BootstrapMixin, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField,
|
||||||
NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple,
|
DynamicModelMultipleChoiceField, NumericArrayField, SlugField, StaticSelect, StaticSelectMultiple,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateForm',
|
'AggregateForm',
|
||||||
'ASNForm',
|
'ASNForm',
|
||||||
|
'FHRPGroupForm',
|
||||||
|
'FHRPGroupAssignmentForm',
|
||||||
'IPAddressAssignForm',
|
'IPAddressAssignForm',
|
||||||
'IPAddressBulkAddForm',
|
'IPAddressBulkAddForm',
|
||||||
'IPAddressForm',
|
'IPAddressForm',
|
||||||
@ -518,6 +523,77 @@ 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 = FHRP_PROTOCOL_ROLE_MAPPINGS[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')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
ipaddresses = self.instance.interface.ip_addresses.all()
|
||||||
|
for ipaddress in ipaddresses:
|
||||||
|
self.fields['group'].widget.add_query_param('related_ip', ipaddress.pk)
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
|
class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
scope_type = ContentTypeChoiceField(
|
scope_type = ContentTypeChoiceField(
|
||||||
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
||||||
|
@ -32,6 +32,12 @@ class IPAMQuery(graphene.ObjectType):
|
|||||||
service = ObjectField(ServiceType)
|
service = ObjectField(ServiceType)
|
||||||
service_list = ObjectListField(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 = ObjectField(VLANType)
|
||||||
vlan_list = ObjectListField(VLANType)
|
vlan_list = ObjectListField(VLANType)
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'ASNType',
|
'ASNType',
|
||||||
'AggregateType',
|
'AggregateType',
|
||||||
|
'FHRPGroupType',
|
||||||
|
'FHRPGroupAssignmentType',
|
||||||
'IPAddressType',
|
'IPAddressType',
|
||||||
'IPRangeType',
|
'IPRangeType',
|
||||||
'PrefixType',
|
'PrefixType',
|
||||||
@ -37,6 +39,25 @@ class AggregateType(PrimaryObjectType):
|
|||||||
filterset_class = filtersets.AggregateFilterSet
|
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 IPAddressType(PrimaryObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
58
netbox/ipam/migrations/0052_fhrpgroup.py
Normal file
58
netbox/ipam/migrations/0052_fhrpgroup.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import django.core.serializers.json
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0064_configrevision'),
|
||||||
|
('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)),
|
||||||
|
('interface_id', models.PositiveIntegerField()),
|
||||||
|
('priority', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(255)])),
|
||||||
|
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ipam.fhrpgroup')),
|
||||||
|
('interface_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'FHRP group assignment',
|
||||||
|
'ordering': ('priority', 'pk'),
|
||||||
|
'unique_together': {('interface_type', 'interface_id', 'group')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,4 @@
|
|||||||
|
from .fhrp import *
|
||||||
from .ip import *
|
from .ip import *
|
||||||
from .services import *
|
from .services import *
|
||||||
from .vlans import *
|
from .vlans import *
|
||||||
@ -8,6 +9,8 @@ __all__ = (
|
|||||||
'Aggregate',
|
'Aggregate',
|
||||||
'IPAddress',
|
'IPAddress',
|
||||||
'IPRange',
|
'IPRange',
|
||||||
|
'FHRPGroup',
|
||||||
|
'FHRPGroupAssignment',
|
||||||
'Prefix',
|
'Prefix',
|
||||||
'RIR',
|
'RIR',
|
||||||
'Role',
|
'Role',
|
||||||
|
100
netbox/ipam/models/fhrp.py
Normal file
100
netbox/ipam/models/fhrp.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
|
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 ipam.constants 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):
|
||||||
|
interface_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
interface_id = models.PositiveIntegerField()
|
||||||
|
interface = GenericForeignKey(
|
||||||
|
ct_field='interface_type',
|
||||||
|
fk_field='interface_id'
|
||||||
|
)
|
||||||
|
group = models.ForeignKey(
|
||||||
|
to='ipam.FHRPGroup',
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
priority = models.PositiveSmallIntegerField(
|
||||||
|
validators=(
|
||||||
|
MinValueValidator(FHRPGROUPASSIGNMENT_PRIORITY_MIN),
|
||||||
|
MaxValueValidator(FHRPGROUPASSIGNMENT_PRIORITY_MAX)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('priority', 'pk')
|
||||||
|
unique_together = ('interface_type', 'interface_id', 'group')
|
||||||
|
verbose_name = 'FHRP group assignment'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.interface}: {self.group} ({self.priority})'
|
@ -1,3 +1,4 @@
|
|||||||
|
from .fhrp import *
|
||||||
from .ip import *
|
from .ip import *
|
||||||
from .services import *
|
from .services import *
|
||||||
from .vlans import *
|
from .vlans import *
|
||||||
|
68
netbox/ipam/tables/fhrp.py
Normal file
68
netbox/ipam/tables/fhrp.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import django_tables2 as tables
|
||||||
|
|
||||||
|
from utilities.tables import BaseTable, ButtonsColumn, 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'
|
||||||
|
)
|
||||||
|
interface_count = tables.Column(
|
||||||
|
verbose_name='Interfaces'
|
||||||
|
)
|
||||||
|
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', 'interface_count',
|
||||||
|
'tags',
|
||||||
|
)
|
||||||
|
default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'interface_count')
|
||||||
|
|
||||||
|
|
||||||
|
class FHRPGroupAssignmentTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
object_parent = tables.Column(
|
||||||
|
accessor=tables.A('object.parent_object'),
|
||||||
|
linkify=True,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Parent'
|
||||||
|
)
|
||||||
|
interface = tables.Column(
|
||||||
|
linkify=True,
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
group = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
|
actions = ButtonsColumn(
|
||||||
|
model=FHRPGroupAssignment,
|
||||||
|
buttons=('edit', 'delete', 'foo')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = FHRPGroupAssignment
|
||||||
|
fields = ('pk', 'group', 'object_parent', 'interface', 'priority')
|
@ -13,6 +13,7 @@ __all__ = (
|
|||||||
'AggregateTable',
|
'AggregateTable',
|
||||||
'ASNTable',
|
'ASNTable',
|
||||||
'InterfaceIPAddressTable',
|
'InterfaceIPAddressTable',
|
||||||
|
'AssignedIPAddressesTable',
|
||||||
'IPAddressAssignTable',
|
'IPAddressAssignTable',
|
||||||
'IPAddressTable',
|
'IPAddressTable',
|
||||||
'IPRangeTable',
|
'IPRangeTable',
|
||||||
@ -382,9 +383,9 @@ class IPAddressAssignTable(BaseTable):
|
|||||||
orderable = False
|
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(
|
address = tables.Column(
|
||||||
linkify=True,
|
linkify=True,
|
||||||
|
@ -544,6 +544,47 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase):
|
|||||||
IPAddress.objects.bulk_create(ip_addresses)
|
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):
|
class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
brief_fields = ['display', 'id', 'name', 'slug', 'url', 'vlan_count']
|
brief_fields = ['display', 'id', 'name', 'slug', 'url', 'vlan_count']
|
||||||
|
@ -872,6 +872,33 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
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):
|
class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = VLANGroup.objects.all()
|
queryset = VLANGroup.objects.all()
|
||||||
filterset = VLANGroupFilterSet
|
filterset = VLANGroupFilterSet
|
||||||
|
@ -427,6 +427,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):
|
class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
|
|
||||||
|
@ -119,6 +119,23 @@ urlpatterns = [
|
|||||||
path('ip-addresses/<int:pk>/edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
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'),
|
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
|
# VLAN groups
|
||||||
path('vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
path('vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
||||||
path('vlan-groups/add/', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
|
path('vlan-groups/add/', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.db.models.expressions import RawSQL
|
from django.db.models.expressions import RawSQL
|
||||||
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Site
|
from dcim.models import Device, Interface, Site
|
||||||
from dcim.tables import SiteTable
|
from dcim.tables import SiteTable
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.forms import TableConfigForm
|
|
||||||
from utilities.tables import paginate_table
|
from utilities.tables import paginate_table
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from virtualization.models import VirtualMachine, VMInterface
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
@ -883,6 +885,113 @@ class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.VLANGroupTable
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get assigned interfaces
|
||||||
|
members_table = tables.FHRPGroupAssignmentTable(
|
||||||
|
data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance),
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
members_table.columns.hide('group')
|
||||||
|
|
||||||
|
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
|
||||||
|
template_name = 'ipam/fhrpgroup_edit.html'
|
||||||
|
|
||||||
|
def get_return_url(self, request, obj=None):
|
||||||
|
return_url = super().get_return_url(request, obj)
|
||||||
|
|
||||||
|
# If we're redirecting the user to the FHRPGroupAssignment creation form,
|
||||||
|
# initialize the group field with the FHRPGroup we just saved.
|
||||||
|
if return_url.startswith(reverse('ipam:fhrpgroupassignment_add')):
|
||||||
|
return_url += f'&group={obj.pk}'
|
||||||
|
|
||||||
|
return return_url
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
template_name = 'ipam/fhrpgroupassignment_edit.html'
|
||||||
|
|
||||||
|
def alter_obj(self, instance, request, args, kwargs):
|
||||||
|
if not instance.pk:
|
||||||
|
# Assign the interface based on URL kwargs
|
||||||
|
try:
|
||||||
|
app_label, model = request.GET.get('interface_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.interface = get_object_or_404(content_type.model_class(), pk=request.GET.get('interface_id'))
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def get_return_url(self, request, obj=None):
|
||||||
|
return obj.interface.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.interface.get_absolute_url() if obj else super().get_return_url(request)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
@ -257,8 +257,9 @@ IPAM_MENU = Menu(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
label='Services',
|
label='Other',
|
||||||
items=(
|
items=(
|
||||||
|
get_model_item('ipam', 'fhrpgroup', 'FHRP Groups'),
|
||||||
get_model_item('ipam', 'service', 'Services', actions=['import']),
|
get_model_item('ipam', 'service', 'Services', actions=['import']),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -440,6 +440,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% include 'ipam/inc/panels/fhrp_groups.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -458,7 +459,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if perms.ipam.add_ipaddress %}
|
{% if perms.ipam.add_ipaddress %}
|
||||||
<div class="card-footer text-end noprint">
|
<div class="card-footer text-end noprint">
|
||||||
<a href="{% url 'ipam:ipaddress_add' %}?device={{ object.device.pk }}&interface={{ object.pk }}" class="btn btn-sm btn-primary">
|
<a href="{% url 'ipam:ipaddress_add' %}?device={{ object.device.pk }}&interface={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary">
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
|
||||||
</a>
|
</a>
|
||||||
</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>
|
84
netbox/templates/ipam/fhrpgroup.html
Normal file
84
netbox/templates/ipam/fhrpgroup.html
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{% 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">Virtual 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">
|
||||||
|
{% if ipaddress_table.rows %}
|
||||||
|
{% render_table members_table 'inc/table.html' %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted">None</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
40
netbox/templates/ipam/fhrpgroup_edit.html
Normal file
40
netbox/templates/ipam/fhrpgroup_edit.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{% extends 'generic/object_edit.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">FHRP Group</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.protocol %}
|
||||||
|
{% render_field form.group_id %}
|
||||||
|
{% render_field form.description %}
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Authentication</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.auth_type %}
|
||||||
|
{% render_field form.auth_key %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not form.instance.pk %}
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Virtual IP Address</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.ip_vrf %}
|
||||||
|
{% render_field form.ip_address %}
|
||||||
|
{% render_field form.ip_status %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if form.custom_fields %}
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||||
|
</div>
|
||||||
|
{% render_custom_fields form %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
18
netbox/templates/ipam/fhrpgroupassignment_edit.html
Normal file
18
netbox/templates/ipam/fhrpgroupassignment_edit.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{% extends 'generic/object_edit.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">FHRP Group Assignment</h5>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label class="col-sm-3 col-form-label text-lg-end">Interface</label>
|
||||||
|
<div class="col">
|
||||||
|
<input class="form-control" value="{{ form.instance.interface }}" disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% render_field form.group %}
|
||||||
|
{% render_field form.priority %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
54
netbox/templates/ipam/inc/panels/fhrp_groups.html
Normal file
54
netbox/templates/ipam/inc/panels/fhrp_groups.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
<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>Protocol</th>
|
||||||
|
<th>Virtual IPs</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.group_id }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ assignment.group.get_protocol_display }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for ipaddress in assignment.group.ip_addresses.all %}
|
||||||
|
<a href="{{ ipaddress.get_absolute_url }}">{{ ipaddress }}</a>
|
||||||
|
{% if not forloop.last %}<br />{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ assignment.priority }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-muted">None</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-end noprint">
|
||||||
|
{% if perms.ipam.add_fhrpgroup %}
|
||||||
|
<a href="{% url 'ipam:fhrpgroup_add' %}?return_url={% url 'ipam:fhrpgroupassignment_add' %}%3Finterface_type={{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}%26interface_id={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||||
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Create Group
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.ipam.add_fhrpgroupassignment %}
|
||||||
|
<a href="{% url 'ipam:fhrpgroupassignment_add' %}?interface_type={{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}&interface_id={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||||
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Assign Group
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -73,12 +73,14 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Assignment</th>
|
<th scope="row">Assignment</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.assigned_object %}
|
{% if object.assigned_object %}
|
||||||
<a href="{{ object.assigned_object.parent_object.get_absolute_url }}">{{ object.assigned_object.parent_object }}</a> /
|
{% if object.assigned_object.parent_object %}
|
||||||
<a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}
|
<a href="{{ object.assigned_object.parent_object.get_absolute_url }}">{{ object.assigned_object.parent_object }}</a> /
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -76,11 +76,12 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'ipam/inc/panels/fhrp_groups.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -99,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if perms.ipam.add_ipaddress %}
|
{% if perms.ipam.add_ipaddress %}
|
||||||
<div class="card-footer text-end noprint">
|
<div class="card-footer text-end noprint">
|
||||||
<a href="{% url 'ipam:ipaddress_add' %}?virtual_machine={{ object.virtual_machine.pk }}&vminterface={{ object.pk }}" class="btn btn-sm btn-primary">
|
<a href="{% url 'ipam:ipaddress_add' %}?virtual_machine={{ object.virtual_machine.pk }}&vminterface={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary">
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add IP Address
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -398,6 +398,12 @@ class VMInterface(PrimaryModel, BaseInterface):
|
|||||||
object_id_field='assigned_object_id',
|
object_id_field='assigned_object_id',
|
||||||
related_query_name='vminterface'
|
related_query_name='vminterface'
|
||||||
)
|
)
|
||||||
|
fhrp_group_assignments = GenericRelation(
|
||||||
|
to='ipam.FHRPGroupAssignment',
|
||||||
|
content_type_field='interface_type',
|
||||||
|
object_id_field='interface_id',
|
||||||
|
related_query_name='+'
|
||||||
|
)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from dcim.models import Device
|
|||||||
from dcim.tables import DeviceTable
|
from dcim.tables import DeviceTable
|
||||||
from extras.views import ObjectConfigContextView
|
from extras.views import ObjectConfigContextView
|
||||||
from ipam.models import IPAddress, Service
|
from ipam.models import IPAddress, Service
|
||||||
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.tables import paginate_table
|
from utilities.tables import paginate_table
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
@ -421,7 +421,7 @@ class VMInterfaceView(generic.ObjectView):
|
|||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
# Get assigned IP addresses
|
# Get assigned IP addresses
|
||||||
ipaddress_table = InterfaceIPAddressTable(
|
ipaddress_table = AssignedIPAddressesTable(
|
||||||
data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
|
data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
|
||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user