Merge branch 'develop' into develop-2.7

This commit is contained in:
Jeremy Stretch 2020-01-13 13:49:22 -05:00
commit 473d67354f
10 changed files with 99 additions and 35 deletions

View File

@ -1,19 +1,22 @@
# v2.6.12 (FUTURE) # v2.6.12 (2020-01-13)
## Enhancements ## Enhancements
* [#1982](https://github.com/netbox-community/netbox/issues/1982) - Improved NAPALM method documentation in Swagger * [#1982](https://github.com/netbox-community/netbox/issues/1982) - Improved NAPALM method documentation in Swagger (OpenAPI)
* [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link * [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering over the link
* [#2113](https://github.com/netbox-community/netbox/issues/2113) - Allow NAPALM driver settings to be changed with request headers * [#2113](https://github.com/netbox-community/netbox/issues/2113) - Allow NAPALM driver settings to be changed with request headers
* [#2589](https://github.com/netbox-community/netbox/issues/2589) - Toggle for showing available prefixes/ip addresses * [#2589](https://github.com/netbox-community/netbox/issues/2589) - Toggle the display of child prefixes/IP addresses
* [#3009](https://github.com/netbox-community/netbox/issues/3009) - Search by description when assigning IP address * [#3009](https://github.com/netbox-community/netbox/issues/3009) - Search by description when assigning IP address to interfaces
* [#3090](https://github.com/netbox-community/netbox/issues/3090) - Add filter field for device interfaces * [#3021](https://github.com/netbox-community/netbox/issues/3021) - Add `tenant` filter field for cables
* [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations * [#3090](https://github.com/netbox-community/netbox/issues/3090) - Enable filtering of interfaces by name on the device view
* [#3393](https://github.com/netbox-community/netbox/issues/3393) - Paginate the circuits at the provider details view * [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations view
* [#3440](https://github.com/netbox-community/netbox/issues/3440) - Add total length to cable trace * [#3393](https://github.com/netbox-community/netbox/issues/3393) - Paginate assigned circuits at the provider details view
* [#3623](https://github.com/netbox-community/netbox/issues/3623) - Add word expansion during interface creation * [#3440](https://github.com/netbox-community/netbox/issues/3440) - Add total path length to cable trace
* [#3668](https://github.com/netbox-community/netbox/issues/3668) - Search by DNS name when assigning IP address * [#3491](https://github.com/netbox-community/netbox/issues/3491) - Include content of response on webhook error
* [#3623](https://github.com/netbox-community/netbox/issues/3623) - Enable word expansion during interface creation
* [#3668](https://github.com/netbox-community/netbox/issues/3668) - Enable searching by DNS name when assigning IP address
* [#3851](https://github.com/netbox-community/netbox/issues/3851) - Allow passing initial data to custom script forms * [#3851](https://github.com/netbox-community/netbox/issues/3851) - Allow passing initial data to custom script forms
* [#3891](https://github.com/netbox-community/netbox/issues/3891) - Add `local_context_data` filter for virtual machines
## Bug Fixes ## Bug Fixes
@ -21,12 +24,14 @@
* [#3849](https://github.com/netbox-community/netbox/issues/3849) - Fix ordering of models when dumping data to JSON * [#3849](https://github.com/netbox-community/netbox/issues/3849) - Fix ordering of models when dumping data to JSON
* [#3853](https://github.com/netbox-community/netbox/issues/3853) - Fix device role link on config context view * [#3853](https://github.com/netbox-community/netbox/issues/3853) - Fix device role link on config context view
* [#3856](https://github.com/netbox-community/netbox/issues/3856) - Allow filtering VM interfaces by multiple MAC addresses * [#3856](https://github.com/netbox-community/netbox/issues/3856) - Allow filtering VM interfaces by multiple MAC addresses
* [#3857](https://github.com/netbox-community/netbox/issues/3857) - Fix group custom links rendering * [#3857](https://github.com/netbox-community/netbox/issues/3857) - Fix rendering of grouped custom links
* [#3862](https://github.com/netbox-community/netbox/issues/3862) - Allow filtering device components by multiple device names * [#3862](https://github.com/netbox-community/netbox/issues/3862) - Allow filtering device components by multiple device names
* [#3864](https://github.com/netbox-community/netbox/issues/3864) - Disallow /0 masks * [#3864](https://github.com/netbox-community/netbox/issues/3864) - Disallow /0 masks for prefixes and IP addresses
* [#3872](https://github.com/netbox-community/netbox/issues/3872) - Paginate related IPs of an address * [#3872](https://github.com/netbox-community/netbox/issues/3872) - Paginate related IPs on the IP address view
* [#3876](https://github.com/netbox-community/netbox/issues/3876) - Fixed min/max to ASN input field at the site creation page * [#3876](https://github.com/netbox-community/netbox/issues/3876) - Fix minimum/maximum value rendering for site ASN field
* [#3882](https://github.com/netbox-community/netbox/issues/3882) - Fix filtering of devices by rack group * [#3882](https://github.com/netbox-community/netbox/issues/3882) - Fix filtering of devices by rack group
* [#3898](https://github.com/netbox-community/netbox/issues/3898) - Fix references to deleted cables without a label
* [#3905](https://github.com/netbox-community/netbox/issues/3905) - Fix divide-by-zero on power feeds with low power values
--- ---

View File

@ -1108,6 +1108,14 @@ class CableFilterSet(django_filters.FilterSet):
method='filter_device', method='filter_device',
field_name='device__site__slug' field_name='device__site__slug'
) )
tenant_id = MultiValueNumberFilter(
method='filter_device',
field_name='device__tenant_id'
)
tenant = MultiValueNumberFilter(
method='filter_device',
field_name='device__tenant__slug'
)
class Meta: class Meta:
model = Cable model = Cable

View File

@ -764,7 +764,7 @@ class RackElevationFilterForm(RackFilterForm):
# Filter the rack field based on the site and group # Filter the rack field based on the site and group
self.fields['site'].widget.add_filter_for('id', 'site') self.fields['site'].widget.add_filter_for('id', 'site')
self.fields['rack_group_id'].widget.add_filter_for('id', 'group_id') self.fields['group_id'].widget.add_filter_for('id', 'group_id')
# #
@ -3242,6 +3242,7 @@ class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, f
termination_b_provider = forms.ModelChoiceField( termination_b_provider = forms.ModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
label='Provider', label='Provider',
required=False,
widget=APISelect( widget=APISelect(
api_url='/api/circuits/providers/', api_url='/api/circuits/providers/',
filter_for={ filter_for={
@ -3295,6 +3296,7 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.Mode
termination_b_site = forms.ModelChoiceField( termination_b_site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site', label='Site',
required=False,
widget=APISelect( widget=APISelect(
api_url='/api/dcim/sites/', api_url='/api/dcim/sites/',
display_field='cid', display_field='cid',
@ -3326,6 +3328,7 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.Mode
('rack_group', 'termination_b_rackgroup'), ('rack_group', 'termination_b_rackgroup'),
), ),
label='Power Panel', label='Power Panel',
required=False,
widget=APISelect( widget=APISelect(
api_url='/api/dcim/power-panels/', api_url='/api/dcim/power-panels/',
filter_for={ filter_for={
@ -3557,6 +3560,17 @@ class CableFilterForm(BootstrapMixin, forms.Form):
} }
) )
) )
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field='slug',
filter_for={
'device_id': 'tenant',
}
)
)
rack_id = FilterChoiceField( rack_id = FilterChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label='Rack', label='Rack',

View File

@ -1949,15 +1949,14 @@ class Cable(ChangeLoggedModel):
('termination_b_type', 'termination_b_id'), ('termination_b_type', 'termination_b_id'),
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# A copy of the PK to be used by __str__ in case the object is deleted
self._pk = self.pk
def __str__(self): def __str__(self):
if self.label: return self.label or '#{}'.format(self._pk)
return self.label
# Save a copy of the PK on the instance since it's nullified if .delete() is called
if not hasattr(self, 'id_string'):
self.id_string = '#{}'.format(self.pk)
return self.id_string
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:cable', args=[self.pk]) return reverse('dcim:cable', args=[self.pk])
@ -2064,6 +2063,9 @@ class Cable(ChangeLoggedModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
# Update the private pk used in __str__ in case this is a new object (i.e. just got its pk)
self._pk = self.pk
def to_csv(self): def to_csv(self):
return ( return (
'{}.{}'.format(self.termination_a_type.app_label, self.termination_a_type.model), '{}.{}'.format(self.termination_a_type.app_label, self.termination_a_type.model),

View File

@ -11,6 +11,7 @@ from dcim.models import (
VirtualChassis, VirtualChassis,
) )
from ipam.models import IPAddress from ipam.models import IPAddress
from tenancy.models import Tenant
from virtualization.models import Cluster, ClusterType from virtualization.models import Cluster, ClusterType
@ -1100,7 +1101,7 @@ class DeviceTestCase(TestCase):
Cluster.objects.bulk_create(clusters) Cluster.objects.bulk_create(clusters)
devices = ( devices = (
Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0]), Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}),
Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]), Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]),
Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]), Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]),
) )
@ -1328,6 +1329,12 @@ class DeviceTestCase(TestCase):
# params = {'device_bays': 'false'} # params = {'device_bays': 'false'}
# self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) # self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_local_context_data(self):
params = {'local_context_data': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'local_context_data': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ConsolePortTestCase(TestCase): class ConsolePortTestCase(TestCase):
queryset = ConsolePort.objects.all() queryset = ConsolePort.objects.all()
@ -2121,6 +2128,12 @@ class CableTestCase(TestCase):
) )
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
tenants = (
Tenant(name='Tenant 1', slug='tenant-1'),
Tenant(name='Tenant 2', slug='tenant-2'),
)
Tenant.objects.bulk_create(tenants)
racks = ( racks = (
Rack(name='Rack 1', site=sites[0]), Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]), Rack(name='Rack 2', site=sites[1]),
@ -2133,9 +2146,9 @@ class CableTestCase(TestCase):
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1') device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
devices = ( devices = (
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1), Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1, tenant=tenants[0]),
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2), Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2, tenant=tenants[0]),
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1), Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1, tenant=tenants[1]),
Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=2), Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=2),
Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=1), Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=1),
Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=2), Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=2),
@ -2218,6 +2231,13 @@ class CableTestCase(TestCase):
params = {'site': [site[0].slug, site[1].slug]} params = {'site': [site[0].slug, site[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
def test_tenant(self):
tenant = Tenant.objects.all()[:2]
params = {'tenant_id': [tenant[0].pk, tenant[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'tenant': [tenant[0].slug, tenant[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class PowerPanelTestCase(TestCase): class PowerPanelTestCase(TestCase):
queryset = PowerPanel.objects.all() queryset = PowerPanel.objects.all()

View File

@ -365,9 +365,12 @@ class CableTestCase(TestCase):
def test_cable_deletion(self): def test_cable_deletion(self):
""" """
When a Cable is deleted, the `cable` field on its termination points must be nullified. When a Cable is deleted, the `cable` field on its termination points must be nullified. The str() method
should still return the PK of the string even after being nullified.
""" """
self.cable.delete() self.cable.delete()
self.assertIsNone(self.cable.pk)
self.assertNotEqual(str(self.cable), '#None')
interface1 = Interface.objects.get(pk=self.interface1.pk) interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertIsNone(interface1.cable) self.assertIsNone(interface1.cable)
interface2 = Interface.objects.get(pk=self.interface2.pk) interface2 = Interface.objects.get(pk=self.interface2.pk)

View File

@ -61,5 +61,5 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque
return 'Status {} returned, webhook successfully processed.'.format(response.status_code) return 'Status {} returned, webhook successfully processed.'.format(response.status_code)
else: else:
raise requests.exceptions.RequestException( raise requests.exceptions.RequestException(
"Status {} returned, webhook FAILED to process.".format(response.status_code) "Status {} returned with content '{}', webhook FAILED to process.".format(response.status_code, response.content)
) )

View File

@ -110,7 +110,9 @@
{% if utilization %} {% if utilization %}
<td> <td>
{{ utilization.allocated }}VA / {{ powerfeed.available_power }}VA {{ utilization.allocated }}VA / {{ powerfeed.available_power }}VA
{% utilization_graph utilization.allocated|percentage:powerfeed.available_power %} {% if powerfeed.available_power > 0 %}
{% utilization_graph utilization.allocated|percentage:powerfeed.available_power %}
{% endif %}
</td> </td>
{% else %} {% else %}
<td class="text-muted">N/A</td> <td class="text-muted">N/A</td>

View File

@ -2,7 +2,7 @@ import django_filters
from django.db.models import Q from django.db.models import Q
from dcim.models import DeviceRole, Interface, Platform, Region, Site from dcim.models import DeviceRole, Interface, Platform, Region, Site
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet
from tenancy.filters import TenancyFilterSet from tenancy.filters import TenancyFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import ( from utilities.filters import (
@ -11,7 +11,6 @@ from utilities.filters import (
from .choices import * from .choices import *
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
__all__ = ( __all__ = (
'ClusterFilterSet', 'ClusterFilterSet',
'ClusterGroupFilterSet', 'ClusterGroupFilterSet',
@ -104,7 +103,12 @@ class ClusterFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
) )
class VirtualMachineFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class VirtualMachineFilterSet(
LocalConfigContextFilterSet,
TenancyFilterSet,
CustomFieldFilterSet,
CreatedUpdatedFilterSet
):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

View File

@ -203,7 +203,7 @@ class VirtualMachineTestCase(TestCase):
DeviceRole.objects.bulk_create(roles) DeviceRole.objects.bulk_create(roles)
vms = ( vms = (
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], platform=platforms[0], role=roles[0], status=VirtualMachineStatusChoices.STATUS_ACTIVE, vcpus=1, memory=1, disk=1), VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], platform=platforms[0], role=roles[0], status=VirtualMachineStatusChoices.STATUS_ACTIVE, vcpus=1, memory=1, disk=1, local_context_data={"foo": 123}),
VirtualMachine(name='Virtual Machine 2', cluster=clusters[1], platform=platforms[1], role=roles[1], status=VirtualMachineStatusChoices.STATUS_STAGED, vcpus=2, memory=2, disk=2), VirtualMachine(name='Virtual Machine 2', cluster=clusters[1], platform=platforms[1], role=roles[1], status=VirtualMachineStatusChoices.STATUS_STAGED, vcpus=2, memory=2, disk=2),
VirtualMachine(name='Virtual Machine 3', cluster=clusters[2], platform=platforms[2], role=roles[2], status=VirtualMachineStatusChoices.STATUS_OFFLINE, vcpus=3, memory=3, disk=3), VirtualMachine(name='Virtual Machine 3', cluster=clusters[2], platform=platforms[2], role=roles[2], status=VirtualMachineStatusChoices.STATUS_OFFLINE, vcpus=3, memory=3, disk=3),
) )
@ -300,6 +300,12 @@ class VirtualMachineTestCase(TestCase):
params = {'mac_address': ['00-00-00-00-00-01', '00-00-00-00-00-02']} params = {'mac_address': ['00-00-00-00-00-01', '00-00-00-00-00-02']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_local_context_data(self):
params = {'local_context_data': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'local_context_data': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class InterfaceTestCase(TestCase): class InterfaceTestCase(TestCase):
queryset = Interface.objects.all() queryset = Interface.objects.all()