mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into develop-2.5
This commit is contained in:
@ -17,6 +17,15 @@ v2.5.0 (FUTURE)
|
||||
|
||||
---
|
||||
|
||||
v2.4.7 (FUTURE)
|
||||
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
v2.4.6 (2018-10-05)
|
||||
|
||||
## Enhancements
|
||||
|
@ -4,6 +4,14 @@ A webhook defines an HTTP request that is sent to an external application when c
|
||||
|
||||
An optional secret key can be configured for each webhook. This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. This digest can be used by the receiver to authenticate the request's content.
|
||||
|
||||
## Installation
|
||||
|
||||
If you are upgrading from a previous version of Netbox and want to enable the webhook feature, please follow the directions listed in the sections below.
|
||||
|
||||
* [Install Redis server and djano-rq package](../installation/2-netbox/#install-python-packages)
|
||||
* [Modify configuration to enable webhooks](../installation/2-netbox/#webhooks-configuration)
|
||||
* [Create supervisord program to run the rqworker process](../installation/3-http-daemon/#supervisord-installation)
|
||||
|
||||
## Requests
|
||||
|
||||
The webhook POST request is structured as so (assuming `application/json` as the Content-Type):
|
||||
|
@ -58,7 +58,7 @@ A device is said to be full depth if its installation on one rack face prevents
|
||||
|
||||
## Device Roles
|
||||
|
||||
Devices can be organized by functional roles. These roles are fully cusomizable. For example, you might create roles for core switches, distribution switches, and access switches.
|
||||
Devices can be organized by functional roles. These roles are fully customizable. For example, you might create roles for core switches, distribution switches, and access switches.
|
||||
|
||||
---
|
||||
|
||||
@ -101,7 +101,7 @@ Device bays represent the ability of a device to house child devices. For exampl
|
||||
|
||||
A platform defines the type of software running on a device or virtual machine. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of the same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
|
||||
|
||||
The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform. See the [API documentation](api/napalm-integration.md) for more information on NAPALM integration.
|
||||
The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform.
|
||||
|
||||
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
|
||||
|
||||
|
@ -71,7 +71,7 @@ Checking connectivity... done.
|
||||
|
||||
`# chown -R netbox:netbox /opt/netbox/netbox/media/`
|
||||
|
||||
## Install Python Packages
|
||||
# Install Python Packages
|
||||
|
||||
Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
|
||||
|
||||
@ -82,7 +82,7 @@ Install the required Python packages using pip. (If you encounter any compilatio
|
||||
!!! note
|
||||
If you encounter errors while installing the required packages, check that you're running a recent version of pip (v9.0.1 or higher) with the command `pip3 -V`.
|
||||
|
||||
### NAPALM Automation (Optional)
|
||||
## NAPALM Automation (Optional)
|
||||
|
||||
NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3:
|
||||
|
||||
@ -90,7 +90,7 @@ NetBox supports integration with the [NAPALM automation](https://napalm-automati
|
||||
# pip3 install napalm
|
||||
```
|
||||
|
||||
### Webhooks (Optional)
|
||||
## Webhooks (Optional)
|
||||
|
||||
[Webhooks](../data-model/extras/#webhooks) allow NetBox to integrate with external services by pushing out a notification each time a relevant object is created, updated, or deleted. Enabling the webhooks feature requires [Redis](https://redis.io/), a lightweight in-memory database. You may opt to install a Redis sevice locally (see below) or connect to an external one.
|
||||
|
||||
|
@ -470,10 +470,14 @@ class ConsoleServerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
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
|
||||
|
||||
|
||||
#
|
||||
@ -493,10 +497,14 @@ class ConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class NestedConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
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
|
||||
|
||||
|
||||
#
|
||||
@ -516,10 +524,14 @@ class PowerOutletSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
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
|
||||
|
||||
|
||||
#
|
||||
@ -539,23 +551,43 @@ class PowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class NestedPowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
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
|
||||
#
|
||||
|
||||
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)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'url', 'device', 'name']
|
||||
fields = ['id', 'url', 'device', 'name', 'is_connected']
|
||||
|
||||
|
||||
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
|
||||
@ -585,7 +617,7 @@ class InterfaceVLANSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'vid', 'name', 'display_name']
|
||||
|
||||
|
||||
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
class InterfaceSerializer(TaggitSerializer, IsConnectedMixin, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
@ -629,19 +661,6 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
|
||||
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):
|
||||
if obj.connection:
|
||||
context = {
|
||||
|
@ -1326,7 +1326,7 @@ class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelF
|
||||
label='Port',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
|
||||
disabled_indicator='connected_console',
|
||||
disabled_indicator='is_connected',
|
||||
)
|
||||
)
|
||||
|
||||
@ -1417,7 +1417,7 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.
|
||||
label='Port',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/console-ports/?device_id={{device}}',
|
||||
disabled_indicator='cs_port'
|
||||
disabled_indicator='is_connected'
|
||||
)
|
||||
)
|
||||
connection_status = forms.BooleanField(
|
||||
@ -1595,7 +1595,7 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
||||
label='Outlet',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
|
||||
disabled_indicator='connected_port'
|
||||
disabled_indicator='is_connected'
|
||||
)
|
||||
)
|
||||
|
||||
@ -1686,7 +1686,7 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||
label='Port',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/power-ports/?device_id={{device}}',
|
||||
disabled_indicator='power_outlet'
|
||||
disabled_indicator='is_connected'
|
||||
)
|
||||
)
|
||||
connection_status = forms.BooleanField(
|
||||
|
@ -1976,25 +1976,44 @@ class InterfaceConnection(models.Model):
|
||||
csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
||||
|
||||
def clean(self):
|
||||
try:
|
||||
if self.interface_a == self.interface_b:
|
||||
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(
|
||||
self.interface_a.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(
|
||||
self.interface_b.get_form_factor_display()
|
||||
)
|
||||
})
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
# An interface cannot be connected to itself
|
||||
if self.interface_a == self.interface_b:
|
||||
raise ValidationError({
|
||||
'interface_b': "Cannot connect an interface to itself."
|
||||
})
|
||||
|
||||
# Only connectable interface types are permitted
|
||||
if self.interface_a.form_factor in NONCONNECTABLE_IFACE_TYPES:
|
||||
raise ValidationError({
|
||||
'interface_a': '{} is not a connectable interface type.'.format(
|
||||
self.interface_a.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(
|
||||
self.interface_b.get_form_factor_display()
|
||||
)
|
||||
})
|
||||
|
||||
# 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):
|
||||
return (
|
||||
|
@ -1953,7 +1953,7 @@ class ConsolePortTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['device', 'id', 'name', 'url']
|
||||
['device', 'id', 'is_connected', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_consoleport(self):
|
||||
@ -2068,7 +2068,7 @@ class ConsoleServerPortTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['device', 'id', 'name', 'url']
|
||||
['device', 'id', 'is_connected', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_consoleserverport(self):
|
||||
@ -2179,7 +2179,7 @@ class PowerPortTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['device', 'id', 'name', 'url']
|
||||
['device', 'id', 'is_connected', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_powerport(self):
|
||||
@ -2294,7 +2294,7 @@ class PowerOutletTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['device', 'id', 'name', 'url']
|
||||
['device', 'id', 'is_connected', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_poweroutlet(self):
|
||||
@ -2430,7 +2430,7 @@ class InterfaceTest(APITestCase):
|
||||
|
||||
self.assertEqual(
|
||||
sorted(response.data['results'][0]),
|
||||
['device', 'id', 'name', 'url']
|
||||
['device', 'id', 'is_connected', 'name', 'url']
|
||||
)
|
||||
|
||||
def test_create_interface(self):
|
||||
|
@ -23,8 +23,9 @@ admin_site.register(User, UserAdmin)
|
||||
admin_site.register(Tag, TagAdmin)
|
||||
|
||||
# Modify the template to include an RQ link if django_rq is installed (see RQ_SHOW_ADMIN_LINK)
|
||||
try:
|
||||
import django_rq
|
||||
admin_site.index_template = 'django_rq/index.html'
|
||||
except ImportError:
|
||||
pass
|
||||
if settings.WEBHOOKS_ENABLED:
|
||||
try:
|
||||
import django_rq
|
||||
admin_site.index_template = 'django_rq/index.html'
|
||||
except ImportError:
|
||||
pass
|
||||
|
Reference in New Issue
Block a user