mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge branch 'develop' into develop-2.7
This commit is contained in:
commit
473d67354f
@ -1,19 +1,22 @@
|
||||
# v2.6.12 (FUTURE)
|
||||
# v2.6.12 (2020-01-13)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#1982](https://github.com/netbox-community/netbox/issues/1982) - Improved NAPALM method documentation in Swagger
|
||||
* [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link
|
||||
* [#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 over the link
|
||||
* [#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
|
||||
* [#3009](https://github.com/netbox-community/netbox/issues/3009) - Search by description when assigning IP address
|
||||
* [#3090](https://github.com/netbox-community/netbox/issues/3090) - Add filter field for device interfaces
|
||||
* [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations
|
||||
* [#3393](https://github.com/netbox-community/netbox/issues/3393) - Paginate the circuits at the provider details view
|
||||
* [#3440](https://github.com/netbox-community/netbox/issues/3440) - Add total length to cable trace
|
||||
* [#3623](https://github.com/netbox-community/netbox/issues/3623) - Add word expansion during interface creation
|
||||
* [#3668](https://github.com/netbox-community/netbox/issues/3668) - Search by DNS name when assigning IP address
|
||||
* [#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 to interfaces
|
||||
* [#3021](https://github.com/netbox-community/netbox/issues/3021) - Add `tenant` filter field for cables
|
||||
* [#3090](https://github.com/netbox-community/netbox/issues/3090) - Enable filtering of interfaces by name on the device view
|
||||
* [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations view
|
||||
* [#3393](https://github.com/netbox-community/netbox/issues/3393) - Paginate assigned circuits at the provider details view
|
||||
* [#3440](https://github.com/netbox-community/netbox/issues/3440) - Add total path length to cable trace
|
||||
* [#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
|
||||
* [#3891](https://github.com/netbox-community/netbox/issues/3891) - Add `local_context_data` filter for virtual machines
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
@ -21,12 +24,14 @@
|
||||
* [#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
|
||||
* [#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
|
||||
* [#3864](https://github.com/netbox-community/netbox/issues/3864) - Disallow /0 masks
|
||||
* [#3872](https://github.com/netbox-community/netbox/issues/3872) - Paginate related IPs of an address
|
||||
* [#3876](https://github.com/netbox-community/netbox/issues/3876) - Fixed min/max to ASN input field at the site creation page
|
||||
* [#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 on the IP address view
|
||||
* [#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
|
||||
* [#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
|
||||
|
||||
---
|
||||
|
||||
|
@ -1108,6 +1108,14 @@ class CableFilterSet(django_filters.FilterSet):
|
||||
method='filter_device',
|
||||
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:
|
||||
model = Cable
|
||||
|
@ -764,7 +764,7 @@ class RackElevationFilterForm(RackFilterForm):
|
||||
|
||||
# Filter the rack field based on the site and group
|
||||
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(
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/circuits/providers/',
|
||||
filter_for={
|
||||
@ -3295,6 +3296,7 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.Mode
|
||||
termination_b_site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/sites/',
|
||||
display_field='cid',
|
||||
@ -3326,6 +3328,7 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.Mode
|
||||
('rack_group', 'termination_b_rackgroup'),
|
||||
),
|
||||
label='Power Panel',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/power-panels/',
|
||||
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(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
|
@ -1949,15 +1949,14 @@ class Cable(ChangeLoggedModel):
|
||||
('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):
|
||||
if self.label:
|
||||
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
|
||||
return self.label or '#{}'.format(self._pk)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:cable', args=[self.pk])
|
||||
@ -2064,6 +2063,9 @@ class Cable(ChangeLoggedModel):
|
||||
|
||||
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):
|
||||
return (
|
||||
'{}.{}'.format(self.termination_a_type.app_label, self.termination_a_type.model),
|
||||
|
@ -11,6 +11,7 @@ from dcim.models import (
|
||||
VirtualChassis,
|
||||
)
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.models import Tenant
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
|
||||
|
||||
@ -1100,7 +1101,7 @@ class DeviceTestCase(TestCase):
|
||||
Cluster.objects.bulk_create(clusters)
|
||||
|
||||
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 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'}
|
||||
# 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):
|
||||
queryset = ConsolePort.objects.all()
|
||||
@ -2121,6 +2128,12 @@ class CableTestCase(TestCase):
|
||||
)
|
||||
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 = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
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')
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1),
|
||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2),
|
||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], 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, tenant=tenants[0]),
|
||||
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 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),
|
||||
@ -2218,6 +2231,13 @@ class CableTestCase(TestCase):
|
||||
params = {'site': [site[0].slug, site[1].slug]}
|
||||
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):
|
||||
queryset = PowerPanel.objects.all()
|
||||
|
@ -365,9 +365,12 @@ class CableTestCase(TestCase):
|
||||
|
||||
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.assertIsNone(self.cable.pk)
|
||||
self.assertNotEqual(str(self.cable), '#None')
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertIsNone(interface1.cable)
|
||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||
|
@ -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)
|
||||
else:
|
||||
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)
|
||||
)
|
||||
|
@ -110,7 +110,9 @@
|
||||
{% if utilization %}
|
||||
<td>
|
||||
{{ 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>
|
||||
{% else %}
|
||||
<td class="text-muted">N/A</td>
|
||||
|
@ -2,7 +2,7 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
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.models import Tenant
|
||||
from utilities.filters import (
|
||||
@ -11,7 +11,6 @@ from utilities.filters import (
|
||||
from .choices import *
|
||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
|
||||
__all__ = (
|
||||
'ClusterFilterSet',
|
||||
'ClusterGroupFilterSet',
|
||||
@ -104,7 +103,12 @@ class ClusterFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class VirtualMachineFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
class VirtualMachineFilterSet(
|
||||
LocalConfigContextFilterSet,
|
||||
TenancyFilterSet,
|
||||
CustomFieldFilterSet,
|
||||
CreatedUpdatedFilterSet
|
||||
):
|
||||
id__in = NumericInFilter(
|
||||
field_name='id',
|
||||
lookup_expr='in'
|
||||
|
@ -203,7 +203,7 @@ class VirtualMachineTestCase(TestCase):
|
||||
DeviceRole.objects.bulk_create(roles)
|
||||
|
||||
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 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']}
|
||||
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):
|
||||
queryset = Interface.objects.all()
|
||||
|
Loading…
Reference in New Issue
Block a user