mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Fixes #2514: Prevent new connections to already connected interfaces
This commit is contained in:
parent
409a9256a1
commit
0bb5d229e8
@ -2,6 +2,7 @@ v2.4.7 (FUTURE)
|
|||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
|
* [#2514](https://github.com/digitalocean/netbox/issues/2514) - Prevent new connections to already connected interfaces
|
||||||
* [#2515](https://github.com/digitalocean/netbox/issues/2515) - Only use django-rq admin tmeplate if webhooks are enabled
|
* [#2515](https://github.com/digitalocean/netbox/issues/2515) - Only use django-rq admin tmeplate if webhooks are enabled
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -472,10 +472,14 @@ class ConsoleServerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
|||||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = ['id', 'url', 'device', 'name']
|
fields = ['id', 'url', 'device', 'name', 'is_connected']
|
||||||
|
|
||||||
|
def get_is_connected(self, obj):
|
||||||
|
return hasattr(obj, 'connected_console') and obj.connected_console is not None
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -495,10 +499,14 @@ class ConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
|||||||
class NestedConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
class NestedConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = ['id', 'url', 'device', 'name']
|
fields = ['id', 'url', 'device', 'name', 'is_connected']
|
||||||
|
|
||||||
|
def get_is_connected(self, obj):
|
||||||
|
return obj.cs_port is not None
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -518,10 +526,14 @@ class PowerOutletSerializer(TaggitSerializer, ValidatedModelSerializer):
|
|||||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = ['id', 'url', 'device', 'name']
|
fields = ['id', 'url', 'device', 'name', 'is_connected']
|
||||||
|
|
||||||
|
def get_is_connected(self, obj):
|
||||||
|
return hasattr(obj, 'connected_port') and obj.connected_port is not None
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -541,23 +553,43 @@ class PowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
|||||||
class NestedPowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
class NestedPowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = ['id', 'url', 'device', 'name']
|
fields = ['id', 'url', 'device', 'name', 'is_connected']
|
||||||
|
|
||||||
|
def get_is_connected(self, obj):
|
||||||
|
return obj.power_outlet is not None
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
class IsConnectedMixin(object):
|
||||||
|
"""
|
||||||
|
Provide a method for setting is_connected on Interface serializers.
|
||||||
|
"""
|
||||||
|
def get_is_connected(self, obj):
|
||||||
|
"""
|
||||||
|
Return True if the interface has a connected interface or circuit.
|
||||||
|
"""
|
||||||
|
if obj.connection:
|
||||||
|
return True
|
||||||
|
if hasattr(obj, 'circuit_termination') and obj.circuit_termination is not None:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class NestedInterfaceSerializer(IsConnectedMixin, WritableNestedSerializer):
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||||
|
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['id', 'url', 'device', 'name']
|
fields = ['id', 'url', 'device', 'name', 'is_connected']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
|
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
|
||||||
@ -587,7 +619,7 @@ class InterfaceVLANSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
class InterfaceSerializer(TaggitSerializer, IsConnectedMixin, ValidatedModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
|
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
|
||||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
@ -631,19 +663,6 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
|||||||
|
|
||||||
return super(InterfaceSerializer, self).validate(data)
|
return super(InterfaceSerializer, self).validate(data)
|
||||||
|
|
||||||
def get_is_connected(self, obj):
|
|
||||||
"""
|
|
||||||
Return True if the interface has a connected interface or circuit termination.
|
|
||||||
"""
|
|
||||||
if obj.connection:
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
circuit_termination = obj.circuit_termination
|
|
||||||
return True
|
|
||||||
except CircuitTermination.DoesNotExist:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_interface_connection(self, obj):
|
def get_interface_connection(self, obj):
|
||||||
if obj.connection:
|
if obj.connection:
|
||||||
context = {
|
context = {
|
||||||
|
@ -1328,7 +1328,7 @@ class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelF
|
|||||||
label='Port',
|
label='Port',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
|
api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
|
||||||
disabled_indicator='connected_console',
|
disabled_indicator='is_connected',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1419,7 +1419,7 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.
|
|||||||
label='Port',
|
label='Port',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/console-ports/?device_id={{device}}',
|
api_url='/api/dcim/console-ports/?device_id={{device}}',
|
||||||
disabled_indicator='cs_port'
|
disabled_indicator='is_connected'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
connection_status = forms.BooleanField(
|
connection_status = forms.BooleanField(
|
||||||
@ -1597,7 +1597,7 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
|||||||
label='Outlet',
|
label='Outlet',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
|
api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
|
||||||
disabled_indicator='connected_port'
|
disabled_indicator='is_connected'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1688,7 +1688,7 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
|||||||
label='Port',
|
label='Port',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/power-ports/?device_id={{device}}',
|
api_url='/api/dcim/power-ports/?device_id={{device}}',
|
||||||
disabled_indicator='power_outlet'
|
disabled_indicator='is_connected'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
connection_status = forms.BooleanField(
|
connection_status = forms.BooleanField(
|
||||||
|
@ -2035,25 +2035,44 @@ class InterfaceConnection(models.Model):
|
|||||||
csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
try:
|
|
||||||
if self.interface_a == self.interface_b:
|
# An interface cannot be connected to itself
|
||||||
raise ValidationError({
|
if self.interface_a == self.interface_b:
|
||||||
'interface_b': "Cannot connect an interface to itself."
|
raise ValidationError({
|
||||||
})
|
'interface_b': "Cannot connect an interface to itself."
|
||||||
if self.interface_a.form_factor in NONCONNECTABLE_IFACE_TYPES:
|
})
|
||||||
raise ValidationError({
|
|
||||||
'interface_a': '{} is not a connectable interface type.'.format(
|
# Only connectable interface types are permitted
|
||||||
self.interface_a.get_form_factor_display()
|
if self.interface_a.form_factor in NONCONNECTABLE_IFACE_TYPES:
|
||||||
)
|
raise ValidationError({
|
||||||
})
|
'interface_a': '{} is not a connectable interface type.'.format(
|
||||||
if self.interface_b.form_factor in NONCONNECTABLE_IFACE_TYPES:
|
self.interface_a.get_form_factor_display()
|
||||||
raise ValidationError({
|
)
|
||||||
'interface_b': '{} is not a connectable interface type.'.format(
|
})
|
||||||
self.interface_b.get_form_factor_display()
|
if self.interface_b.form_factor in NONCONNECTABLE_IFACE_TYPES:
|
||||||
)
|
raise ValidationError({
|
||||||
})
|
'interface_b': '{} is not a connectable interface type.'.format(
|
||||||
except ObjectDoesNotExist:
|
self.interface_b.get_form_factor_display()
|
||||||
pass
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Prevent the A side of one connection from being the B side of another
|
||||||
|
interface_a_connections = InterfaceConnection.objects.filter(
|
||||||
|
Q(interface_a=self.interface_a) |
|
||||||
|
Q(interface_b=self.interface_a)
|
||||||
|
).exclude(pk=self.pk)
|
||||||
|
if interface_a_connections.exists():
|
||||||
|
raise ValidationError({
|
||||||
|
'interface_a': "This interface is already connected."
|
||||||
|
})
|
||||||
|
interface_b_connections = InterfaceConnection.objects.filter(
|
||||||
|
Q(interface_a=self.interface_b) |
|
||||||
|
Q(interface_b=self.interface_b)
|
||||||
|
).exclude(pk=self.pk)
|
||||||
|
if interface_b_connections.exists():
|
||||||
|
raise ValidationError({
|
||||||
|
'interface_b': "This interface is already connected."
|
||||||
|
})
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
|
@ -1955,7 +1955,7 @@ class ConsolePortTest(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(response.data['results'][0]),
|
sorted(response.data['results'][0]),
|
||||||
['device', 'id', 'name', 'url']
|
['device', 'id', 'is_connected', 'name', 'url']
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_consoleport(self):
|
def test_create_consoleport(self):
|
||||||
@ -2070,7 +2070,7 @@ class ConsoleServerPortTest(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(response.data['results'][0]),
|
sorted(response.data['results'][0]),
|
||||||
['device', 'id', 'name', 'url']
|
['device', 'id', 'is_connected', 'name', 'url']
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_consoleserverport(self):
|
def test_create_consoleserverport(self):
|
||||||
@ -2181,7 +2181,7 @@ class PowerPortTest(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(response.data['results'][0]),
|
sorted(response.data['results'][0]),
|
||||||
['device', 'id', 'name', 'url']
|
['device', 'id', 'is_connected', 'name', 'url']
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_powerport(self):
|
def test_create_powerport(self):
|
||||||
@ -2296,7 +2296,7 @@ class PowerOutletTest(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(response.data['results'][0]),
|
sorted(response.data['results'][0]),
|
||||||
['device', 'id', 'name', 'url']
|
['device', 'id', 'is_connected', 'name', 'url']
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_poweroutlet(self):
|
def test_create_poweroutlet(self):
|
||||||
@ -2432,7 +2432,7 @@ class InterfaceTest(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sorted(response.data['results'][0]),
|
sorted(response.data['results'][0]),
|
||||||
['device', 'id', 'name', 'url']
|
['device', 'id', 'is_connected', 'name', 'url']
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_interface(self):
|
def test_create_interface(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user