diff --git a/docs/administration/replicating-netbox.md b/docs/administration/replicating-netbox.md index c4a916865..08e11fe56 100644 --- a/docs/administration/replicating-netbox.md +++ b/docs/administration/replicating-netbox.md @@ -7,10 +7,18 @@ NetBox uses [PostgreSQL](https://www.postgresql.org/) for its database, so gener ## Export the Database +Use the `pg_dump` utility to export the entire database to a file: + ```no-highlight pg_dump netbox > netbox.sql ``` +When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data. + +```no-highlight +pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql +``` + ## Load an Exported Database !!! warning 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/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/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, 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() 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 = { 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: 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/netbox/settings.py b/netbox/netbox/settings.py index b161c4823..c2b313b56 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' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/netbox/reports/__init__.py b/netbox/reports/__init__.py new file mode 100644 index 000000000..e69de29bb 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 %} +