diff --git a/docs/miscellaneous/webhook-backend.md b/docs/miscellaneous/webhook-backend.md index c6a4d5e10..b1d9b1135 100644 --- a/docs/miscellaneous/webhook-backend.md +++ b/docs/miscellaneous/webhook-backend.md @@ -47,19 +47,19 @@ The webhook POST request is structured as so (assuming `application/json` as the "event": "created", "signal_received_timestamp": 1508769597, "model": "Site" - "instance": { + "data": { ... } } ``` -`instance` is the serialized representation of the model instance from the event. The same serializers from the NetBox API are used. So an example of the payload for a Site delete event would be: +`data` is the serialized representation of the model instance(s) from the event. The same serializers from the NetBox API are used. So an example of the payload for a Site delete event would be: ``` { "event": "deleted", "signal_received_timestamp": 1508781858.544069, "model": "Site", - "instance": { + "data": { "asn": None, "comments": "", "contact_email": "", diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 933da9f68..0a36ba366 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -61,6 +61,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel): csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments'] + serializer = 'circuits.api.serializers.ProviderSerializer' + class Meta: ordering = ['name'] @@ -82,10 +84,6 @@ class Provider(CreatedUpdatedModel, CustomFieldModel): self.comments, ) - @property - def serializer(self): - return 'circuits.api.serializers.ProviderSerializer' - @python_2_unicode_compatible class CircuitType(models.Model): @@ -179,6 +177,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', ] + serializer = 'circuits.api.serializers.CircuitSerializer' + class Meta: ordering = ['provider', 'cid'] unique_together = ['provider', 'cid'] @@ -219,10 +219,6 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): def termination_z(self): return self._get_termination('Z') - @property - def serializer(self): - return 'circuits.api.serializers.CircuitSerializer' - @python_2_unicode_compatible class CircuitTermination(models.Model): diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 9c13a144a..19ba4a872 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -169,6 +169,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel): 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', ] + serializer = 'dcim.api.serializers.SiteSerializer' + class Meta: ordering = ['name'] @@ -220,10 +222,6 @@ class Site(CreatedUpdatedModel, CustomFieldModel): def count_circuits(self): return Circuit.objects.filter(terminations__site=self).count() - @property - def serializer(self): - return 'dcim.api.serializers.SiteSerializer' - @property def count_vms(self): from virtualization.models import VirtualMachine @@ -253,6 +251,8 @@ class RackGroup(models.Model): csv_headers = ['site', 'name', 'slug'] + serializer = 'dcim.api.serializers.RackGroupSerializer' + class Meta: ordering = ['site', 'name'] unique_together = [ @@ -273,10 +273,6 @@ class RackGroup(models.Model): self.slug, ) - @property - def serializer(self): - return 'dcim.api.serializers.RackGroupSerializer' - @python_2_unicode_compatible class RackRole(models.Model): @@ -405,6 +401,8 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): 'desc_units', 'comments', ] + serializer = 'dcim.api.serializers.RackSerializer' + class Meta: ordering = ['site', 'group', 'name'] unique_together = [ @@ -574,10 +572,6 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): u_available = len(self.get_available_units()) return int(float(self.u_height - u_available) / self.u_height * 100) - @property - def serializer(self): - return 'dcim.api.serializers.RackSerializer' - @python_2_unicode_compatible class RackReservation(models.Model): @@ -1255,6 +1249,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel): 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments', ] + serializer = 'dcim.api.serializers.DeviceSerializer' + class Meta: ordering = ['name'] unique_together = [ @@ -1491,10 +1487,6 @@ class Device(CreatedUpdatedModel, CustomFieldModel): return None return RPC_CLIENTS.get(self.platform.rpc_client) - @property - def serializer(self): - return 'dcim.api.serializers.DeviceSerializer' - # # Console ports @@ -1784,6 +1776,8 @@ class Interface(models.Model): objects = InterfaceQuerySet.as_manager() + serializer = 'dcim.api.serializers.InterfaceSerializer' + class Meta: ordering = ['device', 'name'] unique_together = ['device', 'name'] @@ -1914,10 +1908,6 @@ class Interface(models.Model): pass return None - @property - def serializer(self): - return 'dcim.api.serializers.InterfaceSerializer' - class InterfaceConnection(models.Model): """ diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 0517918a5..e96ae9ac8 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -40,8 +40,8 @@ class WebhookForm(forms.ModelForm): @admin.register(Webhook) class WebhookAdmin(admin.ModelAdmin): list_display = [ - 'name', 'models', 'payload_url', 'content_type', 'type_create', 'type_update', 'type_delete', - 'secret', 'enabled', 'insecure_ssl', + 'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', + 'type_delete', 'ssl_verification', ] form = WebhookForm diff --git a/netbox/extras/migrations/0012_auto_20180523_1635.py b/netbox/extras/migrations/0012_webhooks.py similarity index 82% rename from netbox/extras/migrations/0012_auto_20180523_1635.py rename to netbox/extras/migrations/0012_webhooks.py index ccdad0a0a..8ccee6ce8 100644 --- a/netbox/extras/migrations/0012_auto_20180523_1635.py +++ b/netbox/extras/migrations/0012_webhooks.py @@ -17,15 +17,15 @@ class Migration(migrations.Migration): name='Webhook', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, unique=True)), + ('name', models.CharField(max_length=150, unique=True)), ('type_create', models.BooleanField(default=False, help_text='A POST will be sent to the URL when the object type(s) is created.')), ('type_update', models.BooleanField(default=False, help_text='A POST will be sent to the URL when the object type(s) is updated.')), ('type_delete', models.BooleanField(default=False, help_text='A POST will be sent to the URL when the object type(s) is deleted.')), ('payload_url', models.CharField(max_length=500, verbose_name='A POST will be sent to this URL based on the webhook criteria.')), - ('content_type', models.PositiveSmallIntegerField(choices=[(1, 'application/json'), (2, 'application/x-www-form-urlencoded')], default=1)), + ('http_content_type', models.PositiveSmallIntegerField(choices=[(1, 'application/json'), (2, 'application/x-www-form-urlencoded')], default=1)), ('secret', models.CharField(blank=True, help_text="When provided the request will include a 'X-Hook-Signature' header which is a HMAC hex digest of the payload body using the secret as the key. The secret is not transmitted in the request.", max_length=255)), ('enabled', models.BooleanField(default=True)), - ('insecure_ssl', models.BooleanField(default=False, help_text='When enabled, secure SSL verification will be ignored. Use with caution!')), + ('ssl_verification', models.BooleanField(default=True, help_text='By default, use of proper SSL is verified. Disable with caution!')), ('obj_type', models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object(s)')), ], ), diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 3bd7b22d1..1a1e13ec5 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -39,7 +39,7 @@ class Webhook(models.Model): help_text="The object(s) to which this Webhook applies." ) name = models.CharField( - max_length=50, + max_length=150, unique=True ) type_create = models.BooleanField( @@ -58,7 +58,7 @@ class Webhook(models.Model): max_length=500, verbose_name="A POST will be sent to this URL based on the webhook criteria." ) - content_type = models.PositiveSmallIntegerField( + http_content_type = models.PositiveSmallIntegerField( choices=WEBHOOK_CT_CHOICES, default=WEBHOOK_CT_JSON ) @@ -73,10 +73,9 @@ class Webhook(models.Model): enabled = models.BooleanField( default=True ) - insecure_ssl = models.BooleanField( - default=False, - help_text="When enabled, secure SSL verification will be ignored. Use with " - "caution!" + ssl_verification = models.BooleanField( + default=True, + help_text="By default, use of proper SSL is verified. Disable with caution!" ) class Meta: @@ -95,11 +94,6 @@ class Webhook(models.Model): "You must select at least one type. Either create, update, or delete." ) - if self.insecure_ssl and not self.payload_url.startswith("https"): - raise ValidationError({ - 'insecure_ssl': 'Only applies to HTTPS payload urls.' - }) - # # Custom fields diff --git a/netbox/extras/webhooks.py b/netbox/extras/webhooks.py index 83f6d94d4..3560bece5 100644 --- a/netbox/extras/webhooks.py +++ b/netbox/extras/webhooks.py @@ -122,7 +122,7 @@ def bulk_operation_receiver(sender, **kwargs): enqueue_webhooks(webhooks, sender, list(kwargs['instances']), event, signal_received_timestamp) -# the bulk operation signal is used to overcome signals not being send for bulk model changes +# the bulk operation signal is used to overcome signals not being sent for bulk model changes bulk_operation_signal = Signal(providing_args=["instances", "event"]) bulk_operation_signal.connect(bulk_operation_receiver) diff --git a/netbox/extras/webhooks_worker.py b/netbox/extras/webhooks_worker.py index 298edbaf8..6d346a51f 100644 --- a/netbox/extras/webhooks_worker.py +++ b/netbox/extras/webhooks_worker.py @@ -9,18 +9,18 @@ from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED @job('default') -def process_webhook(webhook, data, model_class, event, signal_received_timestamp): +def process_webhook(webhook, data, model_class, event, timestamp): """ Make a POST request to the defined Webhook """ payload = { 'event': event, - 'signal_received_timestamp': signal_received_timestamp, + 'timestamp': timestamp, 'model': model_class.__name__, 'data': data } headers = { - 'Content-Type': webhook.get_content_type_display(), + 'Content-Type': webhook.get_http_content_type_display(), } params = { 'method': 'POST', @@ -28,9 +28,9 @@ def process_webhook(webhook, data, model_class, event, signal_received_timestamp 'headers': headers } - if webhook.content_type == WEBHOOK_CT_JSON: + if webhook.http_content_type == WEBHOOK_CT_JSON: params.update({'json': payload}) - elif webhook.content_type == WEBHOOK_CT_X_WWW_FORM_ENCODED: + elif webhook.http_content_type == WEBHOOK_CT_X_WWW_FORM_ENCODED: params.update({'data': payload}) prepared_request = requests.Request(**params).prepare() @@ -41,7 +41,7 @@ def process_webhook(webhook, data, model_class, event, signal_received_timestamp prepared_request.headers['X-Hook-Signature'] = hmac_prep.hexdigest() with requests.Session() as session: - session.very = not webhook.insecure_ssl + session.verify = webhook.ssl_verification response = session.send(prepared_request) if response.status_code >= 200 and response.status_code <= 299: diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 222228b97..fe32baaf5 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -61,6 +61,8 @@ class VRF(CreatedUpdatedModel, CustomFieldModel): csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description'] + serializer = 'ipam.api.serializers.VRFSerializer' + class Meta: ordering = ['name', 'rd'] verbose_name = 'VRF' @@ -87,10 +89,6 @@ class VRF(CreatedUpdatedModel, CustomFieldModel): return "{} ({})".format(self.name, self.rd) return None - @property - def serializer(self): - return 'ipam.api.serializers.VRFSerializer' - @python_2_unicode_compatible class RIR(models.Model): @@ -166,6 +164,8 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): csv_headers = ['prefix', 'rir', 'date_added', 'description'] + serializer = 'ipam.api.serializers.AggregateSerializer' + class Meta: ordering = ['family', 'prefix'] @@ -226,10 +226,6 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): child_prefixes = netaddr.IPSet([p.prefix for p in queryset]) return int(float(child_prefixes.size) / self.prefix.size * 100) - @property - def serializer(self): - return 'ipam.api.serializers.AggregateSerializer' - @python_2_unicode_compatible class Role(models.Model): @@ -344,6 +340,8 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description', ] + serializer = 'ipam.api.serializers.PrefixSerializer' + class Meta: ordering = ['vrf', 'family', 'prefix'] verbose_name_plural = 'prefixes' @@ -499,10 +497,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) return None - @property - def serializer(self): - return 'ipam.api.serializers.PrefixSerializer' - class IPAddressManager(models.Manager): @@ -599,6 +593,8 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): 'description', ] + serializer = 'ipam.api.serializers.IPAddressSerializer' + class Meta: ordering = ['family', 'address'] verbose_name = 'IP address' @@ -672,10 +668,6 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): def get_status_class(self): return STATUS_CHOICE_CLASSES[self.status] - @property - def serializer(self): - return 'ipam.api.serializers.IPAddressSerializer' - def get_role_class(self): return ROLE_CHOICE_CLASSES[self.role] @@ -699,6 +691,8 @@ class VLANGroup(models.Model): csv_headers = ['name', 'slug', 'site'] + serializer = 'ipam.api.serializers.VLANGroupSerializer' + class Meta: ordering = ['site', 'name'] unique_together = [ @@ -731,10 +725,6 @@ class VLANGroup(models.Model): return i return None - @property - def serializer(self): - return 'ipam.api.serializers.VLANGroupSerializer' - @python_2_unicode_compatible class VLAN(CreatedUpdatedModel, CustomFieldModel): @@ -800,6 +790,8 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description'] + serializer = 'ipam.api.serializers.VLANSerializer' + class Meta: ordering = ['site', 'group', 'vid'] unique_together = [ @@ -851,10 +843,6 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): Q(tagged_vlans=self.pk) ) - @property - def serializer(self): - return 'ipam.api.serializers.VLANSerializer' - @python_2_unicode_compatible class Service(CreatedUpdatedModel): @@ -898,6 +886,8 @@ class Service(CreatedUpdatedModel): blank=True ) + serializer = 'ipam.api.serializers.ServiceSerializer' + class Meta: ordering = ['protocol', 'port'] @@ -915,7 +905,3 @@ class Service(CreatedUpdatedModel): raise ValidationError("A service cannot be associated with both a device and a virtual machine.") if not self.device and not self.virtual_machine: raise ValidationError("A service must be associated with either a device or a virtual machine.") - - @property - def serializer(self): - return 'ipam.api.serializers.ServiceSerializer' diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 9839b1278..1cfcfcf56 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -122,13 +122,15 @@ PREFER_IPV4 = False # on first enabling the required components for the webhook backend. WEBHOOK_BACKEND_ENABLED = False -# Redis settings. Redis is used in webhook backend so WEBHOOK_BACKEND_ENABLED must be enabled for these mean anything. -# Please refer to the netbox documentation on the webhook backend. -REDIS_HOST = 'localhost' -REDIS_PORT = 6379 -REDIS_DEFAULT_TIMEOUT = 300 -REDIS_PASSWORD = '' -REDIS_DB = 0 +# Redis settings. Redis is used in webhook backend so WEBHOOK_BACKEND_ENABLED must be enabled for these +# to mean anything. Please refer to the netbox documentation on the webhook backend. +REDIS = { + 'HOST': 'localhost', + 'PORT': 6379, + 'DEFAULT_TIMEOUT': 300, + 'PASSWORD': '', + 'DB': 0, +} # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of # this setting is derived from the installed location. diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 784e89f86..f5205a810 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -65,11 +65,12 @@ PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50) PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/') WEBHOOK_BACKEND_ENABLED = getattr(configuration, 'WEBHOOK_BACKEND_ENABLED', False) -REDIS_HOST = getattr(configuration, 'REDIS_HOST', 'localhost') -REDIS_PORT = getattr(configuration, 'REDIS_PORT', 6379) -REDIS_DEFAULT_TIMEOUT = getattr(configuration, 'REDIS_DEFAULT_TIMEOUT', 300) -REDIS_PASSWORD = getattr(configuration, 'REDIS_PASSWORD', '') -REDIS_DB = getattr(configuration, 'REDIS_DB', 0) +REDIS = getattr(configuration, 'REDIS', {}) +REDIS_HOST = REDIS.get('REDIS_HOST', 'localhost') +REDIS_PORT = REDIS.get('REDIS_PORT', 6379) +REDIS_DEFAULT_TIMEOUT = REDIS.get('REDIS_DEFAULT_TIMEOUT', 300) +REDIS_PASSWORD = REDIS.get('REDIS_PASSWORD', '') +REDIS_DB = REDIS.get('REDIS_DB', 0) SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 79af5791f..bc87ccd8c 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -25,6 +25,8 @@ class TenantGroup(models.Model): csv_headers = ['name', 'slug'] + serializer = 'tenancy.api.serializers.TenantGroupSerializer' + class Meta: ordering = ['name'] @@ -40,10 +42,6 @@ class TenantGroup(models.Model): self.slug, ) - @property - def serializer(self): - return 'tenancy.api.serializers.TenantGroupSerializer' - @python_2_unicode_compatible class Tenant(CreatedUpdatedModel, CustomFieldModel): @@ -83,6 +81,8 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel): csv_headers = ['name', 'slug', 'group', 'description', 'comments'] + serializer = 'tenancy.api.serializers.TenantSerializer' + class Meta: ordering = ['group', 'name'] @@ -100,7 +100,3 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel): self.description, self.comments, ) - - @property - def serializer(self): - return 'tenancy.api.serializers.TenantSerializer' diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 6890afbf9..42b6591f4 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -68,6 +68,8 @@ class ClusterGroup(models.Model): csv_headers = ['name', 'slug'] + serializer = 'virtualization.api.serializers.ClusterGroupSerializer' + class Meta: ordering = ['name'] @@ -83,10 +85,6 @@ class ClusterGroup(models.Model): self.slug, ) - @property - def serializer(self): - return 'virtualization.api.serializers.ClusterGroupSerializer' - # # Clusters @@ -133,6 +131,8 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel): csv_headers = ['name', 'type', 'group', 'site', 'comments'] + serializer = 'virtualization.api.serializers.ClusterSerializer' + class Meta: ordering = ['name'] @@ -163,10 +163,6 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel): self.comments, ) - @property - def serializer(self): - return 'virtualization.api.serializers.ClusterSerializer' - # # Virtual machines @@ -259,6 +255,8 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel): 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', ] + serializer = 'virtualization.api.serializers.VirtualMachineSerializer' + class Meta: ordering = ['name'] @@ -285,10 +283,6 @@ class VirtualMachine(CreatedUpdatedModel, CustomFieldModel): def get_status_class(self): return VM_STATUS_CLASSES[self.status] - @property - def serializer(self): - return 'virtualization.api.serializers.VirtualMachineSerializer' - @property def primary_ip(self): if settings.PREFER_IPV4 and self.primary_ip4: