Merge pull request #2332 from digitalocean/develop

Release v2.4.2
This commit is contained in:
Jeremy Stretch 2018-08-08 09:16:50 -04:00 committed by GitHub
commit 242cb7c7cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 75 additions and 34 deletions

View File

@ -7,10 +7,18 @@ NetBox uses [PostgreSQL](https://www.postgresql.org/) for its database, so gener
## Export the Database ## Export the Database
Use the `pg_dump` utility to export the entire database to a file:
```no-highlight ```no-highlight
pg_dump netbox > netbox.sql 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 ## Load an Exported Database
!!! warning !!! warning

@ -1 +0,0 @@
Subproject commit b3a449437792668041d5cfb9cd6d025e1a5b3470

View File

@ -123,7 +123,7 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
group = NestedRackGroupSerializer(required=False, allow_null=True) group = NestedRackGroupSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
role = NestedRackRoleSerializer(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) width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
@ -223,7 +223,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
manufacturer = NestedManufacturerSerializer() manufacturer = NestedManufacturerSerializer()
interface_ordering = ChoiceField(choices=IFACE_ORDERING_CHOICES, required=False) 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) instance_count = serializers.IntegerField(source='instances.count', read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
@ -396,7 +396,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
platform = NestedPlatformSerializer(required=False, allow_null=True) platform = NestedPlatformSerializer(required=False, allow_null=True)
site = NestedSiteSerializer() site = NestedSiteSerializer()
rack = NestedRackSerializer(required=False, allow_null=True) 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) status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
primary_ip = DeviceIPAddressSerializer(read_only=True) primary_ip = DeviceIPAddressSerializer(read_only=True)
primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True) primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
@ -576,7 +576,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
is_connected = serializers.SerializerMethodField(read_only=True) is_connected = serializers.SerializerMethodField(read_only=True)
interface_connection = serializers.SerializerMethodField(read_only=True) interface_connection = serializers.SerializerMethodField(read_only=True)
circuit_termination = InterfaceCircuitTerminationSerializer(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) untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField( tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),

View File

@ -7,7 +7,7 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.postgres.fields import ArrayField, JSONField 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.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist from django.db.models import Count, Q, ObjectDoesNotExist
@ -1933,11 +1933,20 @@ class Interface(ComponentModel):
""" """
Include the connected Interface (if any). 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( ObjectChange(
user=user, user=user,
request_id=request_id, request_id=request_id,
changed_object=self, changed_object=self,
related_object=self.get_component_parent(), related_object=parent_obj,
action=action, action=action,
object_data=serialize_object(self, extra={ object_data=serialize_object(self, extra={
'connected_interface': self.connected_interface.pk if self.connection else None, 'connected_interface': self.connected_interface.pk if self.connection else None,

View File

@ -1,9 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
import importlib import importlib
import inspect import inspect
import pkgutil import pkgutil
from collections import OrderedDict import sys
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
@ -23,10 +24,29 @@ def get_report(module_name, report_name):
""" """
Return a specific report from within a module. 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) report = getattr(module, report_name, None)
if report is None: if report is None:
return None return None
return report() return report()

View File

@ -2,7 +2,6 @@ import datetime
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from extras.models import Webhook from extras.models import Webhook
from extras.constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE 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: if not settings.WEBHOOKS_ENABLED or instance._meta.model_name not in WEBHOOK_MODELS:
return return
type_create = action == OBJECTCHANGE_ACTION_CREATE # Retrieve any applicable Webhooks
type_update = action == OBJECTCHANGE_ACTION_UPDATE action_flag = {
type_delete = action == OBJECTCHANGE_ACTION_DELETE OBJECTCHANGE_ACTION_CREATE: 'type_create',
OBJECTCHANGE_ACTION_UPDATE: 'type_update',
# Find assigned webhooks OBJECTCHANGE_ACTION_DELETE: 'type_delete',
}[action]
obj_type = ContentType.objects.get_for_model(instance.__class__) obj_type = ContentType.objects.get_for_model(instance.__class__)
webhooks = Webhook.objects.filter( webhooks = Webhook.objects.filter(obj_type=obj_type, enabled=True, **{action_flag: True})
Q(enabled=True) &
(
Q(type_create=type_create) |
Q(type_update=type_update) |
Q(type_delete=type_delete)
) &
Q(obj_type=obj_type)
)
if webhooks: if webhooks.exists():
# Get the Model's API serializer class and serialize the object # Get the Model's API serializer class and serialize the object
serializer_class = get_serializer_for_model(instance.__class__) serializer_class = get_serializer_for_model(instance.__class__)
serializer_context = { serializer_context = {

View File

@ -37,8 +37,12 @@ def process_webhook(webhook, data, model_class, event, timestamp):
prepared_request = requests.Request(**params).prepare() prepared_request = requests.Request(**params).prepare()
if webhook.secret != '': if webhook.secret != '':
# sign the request with the secret # Sign the request with a hash of the secret key and its content.
hmac_prep = hmac.new(bytearray(webhook.secret, 'utf8'), prepared_request.body, digestmod=hashlib.sha512) 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() prepared_request.headers['X-Hook-Signature'] = hmac_prep.hexdigest()
with requests.Session() as session: with requests.Session() as session:

View File

@ -258,7 +258,7 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
vrf = NestedVRFSerializer(required=False, allow_null=True) vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False) 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) interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)

View File

@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
DeprecationWarning DeprecationWarning
) )
VERSION = '2.4.1' VERSION = '2.4.2'
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

View File

View File

@ -194,10 +194,13 @@
</small> </small>
</div> </div>
{% endwith %} {% endwith %}
{% if forloop.last %}
<div class="list-group-item text-right">
<a href="{% url 'extras:objectchange_list' %}">View All Changes</a>
</div>
{% endif %}
{% empty %} {% empty %}
<div class="list-group-item"> <div class="list-group-item text-muted">No change history found</div>
Welcome to NetBox! {% if perms.add_site %} <a href="{% url 'dcim:site_add' %}">Add a site</a> to get started.{% endif %}
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -40,7 +40,7 @@
{% include 'inc/created_updated.html' with obj=vrf %} {% include 'inc/created_updated.html' with obj=vrf %}
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li role="presentation"{% if not active_tab %} class="active"{% endif %}> <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
<a href="{{ aggregate.get_absolute_url }}">VRF</a> <a href="{{ vrf.get_absolute_url }}">VRF</a>
</li> </li>
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}> <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
<a href="{% url 'ipam:vrf_changelog' pk=vrf.pk %}">Changelog</a> <a href="{% url 'ipam:vrf_changelog' pk=vrf.pk %}">Changelog</a>

View File

@ -74,6 +74,12 @@ class ChoiceField(Field):
return {'value': obj, 'label': self._choices[obj]} return {'value': obj, 'label': self._choices[obj]}
def to_internal_value(self, data): 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 return data

View File

@ -146,7 +146,7 @@ class InterfaceVLANSerializer(WritableNestedSerializer):
class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer): class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
virtual_machine = NestedVirtualMachineSerializer() virtual_machine = NestedVirtualMachineSerializer()
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, default=IFACE_FF_VIRTUAL, required=False) 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) untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField( tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),