Merge pull request #2316 from digitalocean/develop

Release v2.4.1
This commit is contained in:
Jeremy Stretch 2018-08-07 09:25:10 -04:00 committed by GitHub
commit ea7386b04b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 61 additions and 25 deletions

1
netbox/_reports Submodule

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

View File

@ -362,7 +362,7 @@ class NestedPlatformSerializer(WritableNestedSerializer):
# #
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency # Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
class DeviceIPAddressSerializer(serializers.ModelSerializer): class DeviceIPAddressSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
class Meta: class Meta:
@ -371,7 +371,7 @@ class DeviceIPAddressSerializer(serializers.ModelSerializer):
# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency # Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
class NestedClusterSerializer(serializers.ModelSerializer): class NestedClusterSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
class Meta: class Meta:
@ -380,7 +380,7 @@ class NestedClusterSerializer(serializers.ModelSerializer):
# Cannot import NestedVirtualChassisSerializer due to circular dependency # Cannot import NestedVirtualChassisSerializer due to circular dependency
class DeviceVirtualChassisSerializer(serializers.ModelSerializer): class DeviceVirtualChassisSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer() master = NestedDeviceSerializer()

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.urls import reverse from django.urls import reverse
from netaddr import IPNetwork
from rest_framework import status from rest_framework import status
from dcim.constants import ( from dcim.constants import (
@ -13,9 +14,10 @@ from dcim.models import (
InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VirtualChassis, RackReservation, RackRole, Region, Site, VirtualChassis,
) )
from ipam.models import VLAN from ipam.models import IPAddress, VLAN
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.testing import APITestCase from utilities.testing import APITestCase
from virtualization.models import Cluster, ClusterType
class RegionTest(APITestCase): class RegionTest(APITestCase):
@ -1680,14 +1682,28 @@ class DeviceTest(APITestCase):
self.devicerole2 = DeviceRole.objects.create( self.devicerole2 = DeviceRole.objects.create(
name='Test Device Role 2', slug='test-device-role-2', color='00ff00' name='Test Device Role 2', slug='test-device-role-2', color='00ff00'
) )
cluster_type = ClusterType.objects.create(name='Test Cluster Type 1', slug='test-cluster-type-1')
self.cluster1 = Cluster.objects.create(name='Test Cluster 1', type=cluster_type)
self.device1 = Device.objects.create( self.device1 = Device.objects.create(
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 1', site=self.site1 device_type=self.devicetype1,
device_role=self.devicerole1,
name='Test Device 1',
site=self.site1,
cluster=self.cluster1
) )
self.device2 = Device.objects.create( self.device2 = Device.objects.create(
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 2', site=self.site1 device_type=self.devicetype1,
device_role=self.devicerole1,
name='Test Device 2',
site=self.site1,
cluster=self.cluster1
) )
self.device3 = Device.objects.create( self.device3 = Device.objects.create(
device_type=self.devicetype1, device_role=self.devicerole1, name='Test Device 3', site=self.site1 device_type=self.devicetype1,
device_role=self.devicerole1,
name='Test Device 3',
site=self.site1,
cluster=self.cluster1
) )
def test_get_device(self): def test_get_device(self):
@ -1696,6 +1712,8 @@ class DeviceTest(APITestCase):
response = self.client.get(url, **self.header) response = self.client.get(url, **self.header)
self.assertEqual(response.data['name'], self.device1.name) self.assertEqual(response.data['name'], self.device1.name)
self.assertEqual(response.data['device_role']['id'], self.devicerole1.pk)
self.assertEqual(response.data['cluster']['id'], self.cluster1.pk)
def test_list_devices(self): def test_list_devices(self):
@ -1711,6 +1729,7 @@ class DeviceTest(APITestCase):
'device_role': self.devicerole1.pk, 'device_role': self.devicerole1.pk,
'name': 'Test Device 4', 'name': 'Test Device 4',
'site': self.site1.pk, 'site': self.site1.pk,
'cluster': self.cluster1.pk,
} }
url = reverse('dcim-api:device-list') url = reverse('dcim-api:device-list')
@ -1722,7 +1741,8 @@ class DeviceTest(APITestCase):
self.assertEqual(device4.device_type_id, data['device_type']) self.assertEqual(device4.device_type_id, data['device_type'])
self.assertEqual(device4.device_role_id, data['device_role']) self.assertEqual(device4.device_role_id, data['device_role'])
self.assertEqual(device4.name, data['name']) self.assertEqual(device4.name, data['name'])
self.assertEqual(device4.site_id, data['site']) self.assertEqual(device4.site.pk, data['site'])
self.assertEqual(device4.cluster.pk, data['cluster'])
def test_create_device_bulk(self): def test_create_device_bulk(self):
@ -1758,11 +1778,17 @@ class DeviceTest(APITestCase):
def test_update_device(self): def test_update_device(self):
interface = Interface.objects.create(name='Test Interface 1', device=self.device1)
ip4_address = IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), interface=interface)
ip6_address = IPAddress.objects.create(address=IPNetwork('2001:db8::1/64'), interface=interface)
data = { data = {
'device_type': self.devicetype2.pk, 'device_type': self.devicetype2.pk,
'device_role': self.devicerole2.pk, 'device_role': self.devicerole2.pk,
'name': 'Test Device X', 'name': 'Test Device X',
'site': self.site2.pk, 'site': self.site2.pk,
'primary_ip4': ip4_address.pk,
'primary_ip6': ip6_address.pk,
} }
url = reverse('dcim-api:device-detail', kwargs={'pk': self.device1.pk}) url = reverse('dcim-api:device-detail', kwargs={'pk': self.device1.pk})
@ -1774,7 +1800,9 @@ class DeviceTest(APITestCase):
self.assertEqual(device1.device_type_id, data['device_type']) self.assertEqual(device1.device_type_id, data['device_type'])
self.assertEqual(device1.device_role_id, data['device_role']) self.assertEqual(device1.device_role_id, data['device_role'])
self.assertEqual(device1.name, data['name']) self.assertEqual(device1.name, data['name'])
self.assertEqual(device1.site_id, data['site']) self.assertEqual(device1.site.pk, data['site'])
self.assertEqual(device1.primary_ip4.pk, data['primary_ip4'])
self.assertEqual(device1.primary_ip6.pk, data['primary_ip6'])
def test_delete_device(self): def test_delete_device(self):

View File

@ -16,16 +16,14 @@ def is_report(obj):
""" """
Returns True if the given object is a Report. Returns True if the given object is a Report.
""" """
if obj in Report.__subclasses__(): return obj in Report.__subclasses__()
return True
return False
def get_report(module_name, report_name): 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('reports.{}'.format(module_name)) module = importlib.import_module(module_name)
report = getattr(module, report_name, None) report = getattr(module, report_name, None)
if report is None: if report is None:
return None return None

View File

@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
DeprecationWarning DeprecationWarning
) )
VERSION = '2.4.0' VERSION = '2.4.1'
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

@ -553,7 +553,7 @@
</button> </button>
{% endif %} {% endif %}
{% if interfaces and perms.dcim.delete_interface %} {% if interfaces and perms.dcim.delete_interface %}
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}" class="btn btn-danger btn-xs"> <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}

View File

@ -9,12 +9,12 @@
<div class="panel-footer"> <div class="panel-footer">
{% if table.rows %} {% if table.rows %}
{% if edit_url %} {% if edit_url %}
<button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}" class="btn btn-xs btn-warning"> <button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
</button> </button>
{% endif %} {% endif %}
{% if delete_url %} {% if delete_url %}
<button type="submit" name="_delete" formaction="{% url delete_url pk=devicetype.pk %}" class="btn btn-xs btn-danger"> <button type="submit" name="_delete" formaction="{% url delete_url pk=devicetype.pk %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
</button> </button>
{% endif %} {% endif %}

View File

@ -111,7 +111,7 @@
</a> </a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}" class="btn btn-info btn-xs" title="Edit interface"> <a href="{% if iface.device_id %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i> <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}

View File

@ -1,4 +1,4 @@
{% with custom_fields=obj.custom_fields %} {% with custom_fields=obj.get_custom_fields %}
{% if custom_fields %} {% if custom_fields %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">

View File

@ -282,12 +282,12 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=virtualmachine.pk %}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=virtualmachine.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
{% endif %} {% endif %}
{% if interfaces and perms.dcim.delete_interface %} {% if interfaces and perms.dcim.delete_interface %}
<button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=virtualmachine.pk %}" class="btn btn-danger btn-xs"> <button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=virtualmachine.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}

View File

@ -101,8 +101,8 @@ def serialize_object(obj, extra=None):
} }
# Include any tags # Include any tags
# if hasattr(obj, 'tags'): if hasattr(obj, 'tags'):
# data['tags'] = [tag.name for tag in obj.tags.all()] data['tags'] = [tag.name for tag in obj.tags.all()]
# Append any extra data # Append any extra data
if extra is not None: if extra is not None:

View File

@ -82,7 +82,7 @@ class NestedClusterSerializer(WritableNestedSerializer):
# #
# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency # Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
class VirtualMachineIPAddressSerializer(serializers.ModelSerializer): class VirtualMachineIPAddressSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
class Meta: class Meta:

View File

@ -1,11 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.urls import reverse from django.urls import reverse
from netaddr import IPNetwork
from rest_framework import status from rest_framework import status
from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_TAGGED from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_TAGGED
from dcim.models import Interface from dcim.models import Interface
from ipam.models import VLAN from ipam.models import IPAddress, VLAN
from utilities.testing import APITestCase from utilities.testing import APITestCase
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
@ -367,6 +368,10 @@ class VirtualMachineTest(APITestCase):
def test_update_virtualmachine(self): def test_update_virtualmachine(self):
interface = Interface.objects.create(name='Test Interface 1', virtual_machine=self.virtualmachine1)
ip4_address = IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), interface=interface)
ip6_address = IPAddress.objects.create(address=IPNetwork('2001:db8::1/64'), interface=interface)
cluster2 = Cluster.objects.create( cluster2 = Cluster.objects.create(
name='Test Cluster 2', name='Test Cluster 2',
type=ClusterType.objects.first(), type=ClusterType.objects.first(),
@ -375,6 +380,8 @@ class VirtualMachineTest(APITestCase):
data = { data = {
'name': 'Test Virtual Machine X', 'name': 'Test Virtual Machine X',
'cluster': cluster2.pk, 'cluster': cluster2.pk,
'primary_ip4': ip4_address.pk,
'primary_ip6': ip6_address.pk,
} }
url = reverse('virtualization-api:virtualmachine-detail', kwargs={'pk': self.virtualmachine1.pk}) url = reverse('virtualization-api:virtualmachine-detail', kwargs={'pk': self.virtualmachine1.pk})
@ -385,6 +392,8 @@ class VirtualMachineTest(APITestCase):
virtualmachine1 = VirtualMachine.objects.get(pk=response.data['id']) virtualmachine1 = VirtualMachine.objects.get(pk=response.data['id'])
self.assertEqual(virtualmachine1.name, data['name']) self.assertEqual(virtualmachine1.name, data['name'])
self.assertEqual(virtualmachine1.cluster.pk, data['cluster']) self.assertEqual(virtualmachine1.cluster.pk, data['cluster'])
self.assertEqual(virtualmachine1.primary_ip4.pk, data['primary_ip4'])
self.assertEqual(virtualmachine1.primary_ip6.pk, data['primary_ip6'])
def test_delete_virtualmachine(self): def test_delete_virtualmachine(self):