From 507a023f41115fdefd080130af23d2cfde7a8e1c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 Aug 2018 09:26:17 -0400 Subject: [PATCH 01/12] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index b161c4823..f86898ead 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.4.1' +VERSION = '2.4.2-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From f59682a7c955baa6b336ebe927d69c26f725ad67 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 Aug 2018 12:10:14 -0400 Subject: [PATCH 02/12] Fixes #2318: ImportError when viewing a report --- netbox/_reports | 1 - netbox/extras/reports.py | 24 ++++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) delete mode 160000 netbox/_reports diff --git a/netbox/_reports b/netbox/_reports deleted file mode 160000 index b3a449437..000000000 --- a/netbox/_reports +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b3a449437792668041d5cfb9cd6d025e1a5b3470 diff --git a/netbox/extras/reports.py b/netbox/extras/reports.py index a516a0b09..52883063c 100644 --- a/netbox/extras/reports.py +++ b/netbox/extras/reports.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals +from collections import OrderedDict import importlib import inspect import pkgutil -from collections import OrderedDict +import sys from django.conf import settings from django.utils import timezone @@ -23,10 +24,29 @@ def get_report(module_name, report_name): """ Return a specific report from within a module. """ - module = importlib.import_module(module_name) + file_path = '{}/{}.py'.format(settings.REPORTS_ROOT, module_name) + + # Python 3.5+ + if sys.version_info >= (3, 5): + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + except FileNotFoundError: + return None + + # Python 2.7 + else: + import imp + try: + module = imp.load_source(module_name, file_path) + except IOError: + return None + report = getattr(module, report_name, None) if report is None: return None + return report() From f4485dc72add8d4ab6207668286f0b33250450ae Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 Aug 2018 13:47:36 -0400 Subject: [PATCH 03/12] Restore reports directory --- netbox/reports/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 netbox/reports/__init__.py diff --git a/netbox/reports/__init__.py b/netbox/reports/__init__.py new file mode 100644 index 000000000..e69de29bb From c51c20a301686e589d3c0d2e017f1a0ce2154222 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 Aug 2018 13:48:29 -0400 Subject: [PATCH 04/12] Fixes #2319: Extend ChoiceField to properly handle true/false choice keys --- netbox/utilities/api.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 0ce207d6e..d753954aa 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -74,6 +74,12 @@ class ChoiceField(Field): return {'value': obj, 'label': self._choices[obj]} def to_internal_value(self, data): + # Hotwiring boolean values + if hasattr(data, 'lower'): + if data.lower() == 'true': + return True + if data.lower() == 'false': + return False return data From db7148350ea6f6602a7dcbedd0e852bf4a65652a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 Aug 2018 14:05:07 -0400 Subject: [PATCH 05/12] Fixes #2321: Allow explicitly setting a null value on nullable ChoiceFields --- netbox/dcim/api/serializers.py | 8 ++++---- netbox/ipam/api/serializers.py | 2 +- netbox/virtualization/api/serializers.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d1bfa535d..bc9179673 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -123,7 +123,7 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer): group = NestedRackGroupSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) role = NestedRackRoleSerializer(required=False, allow_null=True) - type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False) + type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True) width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False) tags = TagListSerializerField(required=False) @@ -223,7 +223,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer): class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): manufacturer = NestedManufacturerSerializer() interface_ordering = ChoiceField(choices=IFACE_ORDERING_CHOICES, required=False) - subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False) + subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True) instance_count = serializers.IntegerField(source='instances.count', read_only=True) tags = TagListSerializerField(required=False) @@ -396,7 +396,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer): platform = NestedPlatformSerializer(required=False, allow_null=True) site = NestedSiteSerializer() rack = NestedRackSerializer(required=False, allow_null=True) - face = ChoiceField(choices=RACK_FACE_CHOICES, required=False) + face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True) status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False) primary_ip = DeviceIPAddressSerializer(read_only=True) primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True) @@ -576,7 +576,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer): is_connected = serializers.SerializerMethodField(read_only=True) interface_connection = serializers.SerializerMethodField(read_only=True) circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True) - mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False) + mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True) untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( queryset=VLAN.objects.all(), diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 021495a2c..4ba62e8da 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -258,7 +258,7 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer): vrf = NestedVRFSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False) - role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False) + role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False, allow_null=True) interface = IPAddressInterfaceSerializer(required=False, allow_null=True) tags = TagListSerializerField(required=False) diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 218578d1a..f4e0d5e5c 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -146,7 +146,7 @@ class InterfaceVLANSerializer(WritableNestedSerializer): class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer): virtual_machine = NestedVirtualMachineSerializer() form_factor = ChoiceField(choices=IFACE_FF_CHOICES, default=IFACE_FF_VIRTUAL, required=False) - mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False) + mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True) untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( queryset=VLAN.objects.all(), From d5239191fed304b50d6bab0a01f8556a33fde19d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 Aug 2018 14:19:46 -0400 Subject: [PATCH 06/12] Fixes #2320: TypeError when dispatching a webhook with a secret key configured --- netbox/extras/webhooks_worker.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/netbox/extras/webhooks_worker.py b/netbox/extras/webhooks_worker.py index 91e95baa1..2122d1154 100644 --- a/netbox/extras/webhooks_worker.py +++ b/netbox/extras/webhooks_worker.py @@ -37,8 +37,12 @@ def process_webhook(webhook, data, model_class, event, timestamp): prepared_request = requests.Request(**params).prepare() if webhook.secret != '': - # sign the request with the secret - hmac_prep = hmac.new(bytearray(webhook.secret, 'utf8'), prepared_request.body, digestmod=hashlib.sha512) + # Sign the request with a hash of the secret key and its content. + hmac_prep = hmac.new( + key=webhook.secret.encode('utf8'), + msg=prepared_request.body.encode('utf8'), + digestmod=hashlib.sha512 + ) prepared_request.headers['X-Hook-Signature'] = hmac_prep.hexdigest() with requests.Session() as session: From f9911bff0d052571398a32757822613ab10ce2c2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 Aug 2018 15:19:01 -0400 Subject: [PATCH 07/12] Added a "view all" link to the changelog panel --- netbox/templates/home.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/netbox/templates/home.html b/netbox/templates/home.html index 090be1350..d6af56458 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -194,10 +194,13 @@ {% endwith %} + {% if forloop.last %} + + {% endif %} {% empty %} -
- Welcome to NetBox! {% if perms.add_site %} Add a site to get started.{% endif %} -
+
No change history found
{% endfor %} From d5488ca7da5209fd2cb36e427a938312841971c2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 Aug 2018 15:41:31 -0400 Subject: [PATCH 08/12] Fixes #2322: Webhooks firing on non-enabled event types --- netbox/extras/webhooks.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/netbox/extras/webhooks.py b/netbox/extras/webhooks.py index e1269a3a5..a0c927b64 100644 --- a/netbox/extras/webhooks.py +++ b/netbox/extras/webhooks.py @@ -2,7 +2,6 @@ import datetime from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.db.models import Q from extras.models import Webhook from extras.constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE @@ -18,23 +17,16 @@ def enqueue_webhooks(instance, action): if not settings.WEBHOOKS_ENABLED or instance._meta.model_name not in WEBHOOK_MODELS: return - type_create = action == OBJECTCHANGE_ACTION_CREATE - type_update = action == OBJECTCHANGE_ACTION_UPDATE - type_delete = action == OBJECTCHANGE_ACTION_DELETE - - # Find assigned webhooks + # Retrieve any applicable Webhooks + action_flag = { + OBJECTCHANGE_ACTION_CREATE: 'type_create', + OBJECTCHANGE_ACTION_UPDATE: 'type_update', + OBJECTCHANGE_ACTION_DELETE: 'type_delete', + }[action] obj_type = ContentType.objects.get_for_model(instance.__class__) - webhooks = Webhook.objects.filter( - Q(enabled=True) & - ( - Q(type_create=type_create) | - Q(type_update=type_update) | - Q(type_delete=type_delete) - ) & - Q(obj_type=obj_type) - ) + webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True}) - if webhooks: + if webhooks.exists(): # Get the Model's API serializer class and serialize the object serializer_class = get_serializer_for_model(instance.__class__) serializer_context = { From 91b5f6d79960a8fbaa30affc24a8d553a5dfb093 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 7 Aug 2018 17:30:26 -0400 Subject: [PATCH 09/12] Fixes #2323: DoesNotExist raised when deleting devices or virtual machines --- netbox/dcim/models.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index ca18cbb25..564e6fa74 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -7,7 +7,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericRelation from django.contrib.postgres.fields import ArrayField, JSONField -from django.core.exceptions import ValidationError +from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import Count, Q, ObjectDoesNotExist @@ -1933,11 +1933,20 @@ class Interface(ComponentModel): """ Include the connected Interface (if any). """ + + # It's possible that an Interface can be deleted _after_ its parent Device/VM, in which case trying to resolve + # the component parent will raise DoesNotExist. For more discussion, see + # https://github.com/digitalocean/netbox/issues/2323 + try: + parent_obj = self.get_component_parent() + except ObjectDoesNotExist: + parent_obj = None + ObjectChange( user=user, request_id=request_id, changed_object=self, - related_object=self.get_component_parent(), + related_object=parent_obj, action=action, object_data=serialize_object(self, extra={ 'connected_interface': self.connected_interface.pk if self.connection else None, From cfab9a6a0a69e9d53403b25715235c45cb3793d0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 8 Aug 2018 08:49:23 -0400 Subject: [PATCH 10/12] Fixes #2330: Incorrect tab link in VRF changelog view --- netbox/templates/ipam/vrf.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index f75b8c7d1..a69ccd0f5 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -40,7 +40,7 @@ {% include 'inc/created_updated.html' with obj=vrf %}