1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into feature

This commit is contained in:
jeremystretch
2021-05-25 13:19:17 -04:00
37 changed files with 220 additions and 93 deletions

View File

@ -17,7 +17,7 @@ body:
What version of NetBox are you currently running? (If you don't have access to the most
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
before opening a bug report to see if your issue has already been addressed.)
placeholder: v2.11.3
placeholder: v2.11.4
validations:
required: true
- type: dropdown
@ -40,7 +40,8 @@ body:
creation of any necessary database objects and call out every operation being
performed explicitly. If reporting a bug in the REST API, be sure to reconstruct
the raw HTTP request(s) being made: Don't rely on a client library such as
pynetbox."
pynetbox. Additionally, **do not rely on the demo instance** for reproducing
suspected bugs, as its data is prone to modification or deletion at any time.
placeholder: |
1. Click on "create widget"
2. Set foo to 12 and bar to G

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v2.11.3
placeholder: v2.11.4
validations:
required: true
- type: dropdown

View File

@ -22,6 +22,8 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
<div align="center">
<h4>Thank you to our sponsors!</h4>
[![DigitalOcean](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/digitalocean.png)](https://try.digitalocean.com/developer-cloud)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com/)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech/)

View File

@ -6,7 +6,7 @@ If a change is made to any of the objects returned by the query within that time
## Invalidating Cached Data
Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object my its type and numeric ID:
Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object by its type and numeric ID:
```no-highlight
$ python netbox/manage.py invalidate dcim.Device.34

View File

@ -1,6 +1,6 @@
# Power Feed
A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power pot (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.
A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power port (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.
Each power feed is assigned an operational type (primary or redundant) and one of the following statuses:

View File

@ -1,5 +1,28 @@
# NetBox v2.11
## v2.11.4 (2021-05-25)
### Enhancements
* [#5121](https://github.com/netbox-community/netbox/issues/5121) - Add content type filters for tags
* [#6358](https://github.com/netbox-community/netbox/issues/6358) - Add search field for VLAN groups
* [#6393](https://github.com/netbox-community/netbox/issues/6393) - Add `description` filter for IP addresses
* [#6400](https://github.com/netbox-community/netbox/issues/6400) - Add cyan color choice for plugin buttons
* [#6422](https://github.com/netbox-community/netbox/issues/6422) - Enable filtering users by group under admin UI
* [#6441](https://github.com/netbox-community/netbox/issues/6441) - Improve UI paginator to optimize page object count
### Bug Fixes
* [#6376](https://github.com/netbox-community/netbox/issues/6376) - Fix assignment of VLAN groups to clusters, cluster groups via REST API
* [#6398](https://github.com/netbox-community/netbox/issues/6398) - Avoid exception when deleting device connected to self via circuit
* [#6426](https://github.com/netbox-community/netbox/issues/6426) - Allow assigning virtual chassis member interfaces to LAG on VC master
* [#6438](https://github.com/netbox-community/netbox/issues/6438) - Fix missing descriptions and label for device type imports and exports
* [#6465](https://github.com/netbox-community/netbox/issues/6465) - Fix typo in installed plugins REST API endpoint
* [#6467](https://github.com/netbox-community/netbox/issues/6467) - Fix access to metrics on custom `BASE_PATH` when login is required
* [#6468](https://github.com/netbox-community/netbox/issues/6468) - Disable ordering VLAN groups list by scope object
---
## v2.11.3 (2021-05-07)
### Enhancements

View File

@ -20,7 +20,7 @@ __all__ = (
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Provider(PrimaryModel):
"""
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
@ -96,7 +96,7 @@ class Provider(PrimaryModel):
# Provider networks
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ProviderNetwork(PrimaryModel):
"""
This represents a provider network which exists outside of NetBox, the details of which are unknown or
@ -189,7 +189,7 @@ class CircuitType(OrganizationalModel):
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Circuit(PrimaryModel):
"""
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple

View File

@ -1818,7 +1818,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = ConsolePortTemplate
fields = [
'device_type', 'name', 'label', 'type',
'device_type', 'name', 'label', 'type', 'description',
]
@ -1827,7 +1827,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = ConsoleServerPortTemplate
fields = [
'device_type', 'name', 'label', 'type',
'device_type', 'name', 'label', 'type', 'description',
]
@ -1836,7 +1836,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = PowerPortTemplate
fields = [
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
]
@ -1850,7 +1850,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = PowerOutletTemplate
fields = [
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
]
@ -1862,7 +1862,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = InterfaceTemplate
fields = [
'device_type', 'name', 'label', 'type', 'mgmt_only',
'device_type', 'name', 'label', 'type', 'mgmt_only', 'description',
]
@ -1879,7 +1879,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = FrontPortTemplate
fields = [
'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
]
@ -1891,7 +1891,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = RearPortTemplate
fields = [
'device_type', 'name', 'type', 'positions',
'device_type', 'name', 'type', 'positions', 'label', 'description',
]
@ -1900,7 +1900,7 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
class Meta:
model = DeviceBayTemplate
fields = [
'device_type', 'name',
'device_type', 'name', 'label', 'description',
]
@ -3150,8 +3150,12 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device
# Restrict parent/LAG interface assignment by device
# Restrict parent/LAG interface assignment by device/VC
self.fields['parent'].widget.add_query_param('device_id', device.pk)
if device.virtual_chassis and device.virtual_chassis.master:
# Get available LAG interfaces by VirtualChassis master
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
else:
self.fields['lag'].widget.add_query_param('device_id', device.pk)
# Limit VLAN choices by device

View File

@ -30,7 +30,7 @@ __all__ = (
# Cables
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Cable(PrimaryModel):
"""
A physical connection between two endpoints.

View File

@ -211,7 +211,7 @@ class PathEndpoint(models.Model):
# Console ports
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
"""
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@ -254,7 +254,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
# Console server ports
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
"""
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
@ -297,7 +297,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
# Power ports
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class PowerPort(ComponentModel, CableTermination, PathEndpoint):
"""
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@ -408,7 +408,7 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
# Power outlets
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
"""
A physical power outlet (output) within a Device which provides power to a PowerPort.
@ -512,7 +512,7 @@ class BaseInterface(models.Model):
return self.ip_addresses.count()
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
"""
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
@ -683,7 +683,7 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
# Pass-through ports
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class FrontPort(ComponentModel, CableTermination):
"""
A pass-through port on the front of a Device.
@ -748,7 +748,7 @@ class FrontPort(ComponentModel, CableTermination):
})
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class RearPort(ComponentModel, CableTermination):
"""
A pass-through port on the rear of a Device.
@ -801,7 +801,7 @@ class RearPort(ComponentModel, CableTermination):
# Device bays
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class DeviceBay(ComponentModel):
"""
An empty space within a Device which can house a child device
@ -860,7 +860,7 @@ class DeviceBay(ComponentModel):
# Inventory items
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class InventoryItem(MPTTModel, ComponentModel):
"""
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.

View File

@ -75,7 +75,7 @@ class Manufacturer(OrganizationalModel):
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class DeviceType(PrimaryModel):
"""
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
@ -183,6 +183,8 @@ class DeviceType(PrimaryModel):
{
'name': c.name,
'type': c.type,
'label': c.label,
'description': c.description,
}
for c in self.consoleporttemplates.all()
]
@ -191,6 +193,8 @@ class DeviceType(PrimaryModel):
{
'name': c.name,
'type': c.type,
'label': c.label,
'description': c.description,
}
for c in self.consoleserverporttemplates.all()
]
@ -201,6 +205,8 @@ class DeviceType(PrimaryModel):
'type': c.type,
'maximum_draw': c.maximum_draw,
'allocated_draw': c.allocated_draw,
'label': c.label,
'description': c.description,
}
for c in self.powerporttemplates.all()
]
@ -211,6 +217,8 @@ class DeviceType(PrimaryModel):
'type': c.type,
'power_port': c.power_port.name if c.power_port else None,
'feed_leg': c.feed_leg,
'label': c.label,
'description': c.description,
}
for c in self.poweroutlettemplates.all()
]
@ -220,6 +228,8 @@ class DeviceType(PrimaryModel):
'name': c.name,
'type': c.type,
'mgmt_only': c.mgmt_only,
'label': c.label,
'description': c.description,
}
for c in self.interfacetemplates.all()
]
@ -230,6 +240,8 @@ class DeviceType(PrimaryModel):
'type': c.type,
'rear_port': c.rear_port.name,
'rear_port_position': c.rear_port_position,
'label': c.label,
'description': c.description,
}
for c in self.frontporttemplates.all()
]
@ -239,6 +251,8 @@ class DeviceType(PrimaryModel):
'name': c.name,
'type': c.type,
'positions': c.positions,
'label': c.label,
'description': c.description,
}
for c in self.rearporttemplates.all()
]
@ -246,6 +260,8 @@ class DeviceType(PrimaryModel):
data['device-bays'] = [
{
'name': c.name,
'label': c.label,
'description': c.description,
}
for c in self.devicebaytemplates.all()
]
@ -448,7 +464,7 @@ class Platform(OrganizationalModel):
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Device(PrimaryModel, ConfigContextModel):
"""
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
@ -891,7 +907,7 @@ class Device(PrimaryModel, ConfigContextModel):
# Virtual chassis
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class VirtualChassis(PrimaryModel):
"""
A collection of Devices which operate with a shared control plane (e.g. a switch stack).

View File

@ -21,7 +21,7 @@ __all__ = (
# Power
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class PowerPanel(PrimaryModel):
"""
A distribution point for electrical power; e.g. a data center RPP.
@ -71,7 +71,7 @@ class PowerPanel(PrimaryModel):
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
"""
An electrical circuit delivered from a PowerPanel.

View File

@ -78,7 +78,7 @@ class RackRole(OrganizationalModel):
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Rack(PrimaryModel):
"""
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
@ -463,7 +463,7 @@ class Rack(PrimaryModel):
return int(allocated_draw_total / available_power_total * 100)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class RackReservation(PrimaryModel):
"""
One or more reserved units within a Rack.

View File

@ -130,7 +130,7 @@ class SiteGroup(NestedGroupModel):
# Sites
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Site(PrimaryModel):
"""
A Site represents a geographic location within a network; typically a building or campus. The optional facility

View File

@ -31,8 +31,9 @@ def rebuild_paths(obj):
with transaction.atomic():
for cp in cable_paths:
invalidate_obj(cp.origin)
cp.delete()
if cp.origin:
invalidate_obj(cp.origin)
create_cablepath(cp.origin)

View File

@ -7,5 +7,6 @@ EXTRAS_FEATURES = [
'custom_links',
'export_templates',
'job_results',
'tags',
'webhooks'
]

View File

@ -6,7 +6,7 @@ from django.db.models import Q
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet
from tenancy.models import Tenant, TenantGroup
from utilities.filters import ContentTypeFilter
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
from virtualization.models import Cluster, ClusterGroup
from .choices import *
from .models import *
@ -114,6 +114,12 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
method='search',
label='Search',
)
content_type = MultiValueCharFilter(
method='_content_type'
)
content_type_id = MultiValueNumberFilter(
method='_content_type_id'
)
class Meta:
model = Tag
@ -127,6 +133,32 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
Q(slug__icontains=value)
)
def _content_type(self, queryset, name, values):
ct_filter = Q()
# Compile list of app_label & model pairings
for value in values:
try:
app_label, model = value.lower().split('.')
ct_filter |= Q(
app_label=app_label,
model=model
)
except ValueError:
pass
# Get ContentType instances
content_types = ContentType.objects.filter(ct_filter)
return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct()
def _content_type_id(self, queryset, name, values):
# Get ContentType instances
content_types = ContentType.objects.filter(pk__in=values)
return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct()
class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter(

View File

@ -8,12 +8,13 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
CommentField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
BOOLEAN_WITH_BLANK_CHOICES,
CommentField, ContentTypeMultipleChoiceField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField,
JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
)
from virtualization.models import Cluster, ClusterGroup
from .choices import *
from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
from .utils import FeatureQuery
#
@ -177,6 +178,15 @@ class AddRemoveTagsForm(forms.Form):
class TagFilterForm(BootstrapMixin, forms.Form):
model = Tag
q = forms.CharField(
required=False,
label=_('Search')
)
content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
required=False,
label=_('Tagged object type')
)
class TagBulkEditForm(BootstrapMixin, BulkEditForm):

View File

@ -42,7 +42,7 @@ class InstalledPluginsAPIView(APIView):
'author': plugin_app_config.author,
'author_email': plugin_app_config.author_email,
'description': plugin_app_config.description,
'verison': plugin_app_config.version
'version': plugin_app_config.version
}
def get(self, request, format=None):

View File

@ -5,6 +5,7 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from circuits.models import Provider
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
from extras.filtersets import *
@ -537,6 +538,13 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Tag.objects.bulk_create(tags)
# Apply some tags so we can filter by content type
site = Site.objects.create(name='Site 1', slug='site-1')
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
site.tags.set(tags[0])
provider.tags.set(tags[1])
def test_name(self):
params = {'name': ['Tag 1', 'Tag 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -549,6 +557,14 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'color': ['ff0000', '00ff00']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_type(self):
params = {'content_type': ['dcim.site', 'circuits.provider']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
site_ct = ContentType.objects.get_for_model(Site).pk
provider_ct = ContentType.objects.get_for_model(Provider).pk
params = {'content_type_id': [site_ct, provider_ct]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
queryset = ObjectChange.objects.all()

View File

@ -7,7 +7,7 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
from ipam.choices import *
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import OrganizationalModelSerializer
@ -115,8 +115,7 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
scope_type = ContentTypeField(
queryset=ContentType.objects.filter(
app_label='dcim',
model__in=['region', 'sitegroup', 'site', 'location', 'rack']
model__in=VLANGROUP_SCOPE_TYPES
),
required=False
)

View File

@ -468,7 +468,7 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = IPAddress
fields = ['id', 'dns_name']
fields = ['id', 'dns_name', 'description']
def search(self, queryset, name, value):
if not value.strip():
@ -536,6 +536,10 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class VLANGroupFilterSet(OrganizationalModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
scope_type = ContentTypeFilter()
region = django_filters.NumberFilter(
method='filter_scope'
@ -563,6 +567,15 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
model = VLANGroup
fields = ['id', 'name', 'slug', 'description', 'scope_id']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(description__icontains=value)
)
return queryset.filter(qs_filter)
def filter_scope(self, queryset, name, value):
return queryset.filter(
scope_type=ContentType.objects.get(model=name),

View File

@ -1291,6 +1291,10 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
['region', 'sitegroup', 'site'],
['location', 'rack']
]
q = forms.CharField(
required=False,
label=_('Search')
)
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,

View File

@ -77,7 +77,7 @@ class RIR(OrganizationalModel):
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Aggregate(PrimaryModel):
"""
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
@ -228,7 +228,7 @@ class Role(OrganizationalModel):
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Prefix(PrimaryModel):
"""
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
@ -489,7 +489,7 @@ class Prefix(PrimaryModel):
return int(float(child_count) / prefix_size * 100)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class IPAddress(PrimaryModel):
"""
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is

View File

@ -17,7 +17,7 @@ __all__ = (
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Service(PrimaryModel):
"""
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may

View File

@ -100,7 +100,7 @@ class VLANGroup(OrganizationalModel):
return None
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class VLAN(PrimaryModel):
"""
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned

View File

@ -13,7 +13,7 @@ __all__ = (
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class VRF(PrimaryModel):
"""
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
@ -88,7 +88,7 @@ class VRF(PrimaryModel):
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class RouteTarget(PrimaryModel):
"""
A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.

View File

@ -449,7 +449,8 @@ class VLANGroupTable(BaseTable):
name = tables.Column(linkify=True)
scope_type = ContentTypeColumn()
scope = tables.Column(
linkify=True
linkify=True,
orderable=False
)
vlan_count = LinkedCountColumn(
viewname='ipam:vlan_list',

View File

@ -577,12 +577,12 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
ipaddresses = (
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar1'),
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar2'),
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
@ -598,6 +598,10 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_description(self):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self):
params = {'parent': '10.0.0.0/24'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)

View File

@ -20,17 +20,20 @@ class LoginRequiredMiddleware(object):
self.get_response = get_response
def __call__(self, request):
# Redirect unauthenticated requests (except those exempted) to the login page if LOGIN_REQUIRED is true
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
# Redirect unauthenticated requests to the login page. API requests are exempt from redirection as the API
# performs its own authentication. Also metrics can be read without login.
api_path = reverse('api-root')
if not request.path_info.startswith((api_path, '/metrics')) and request.path_info != settings.LOGIN_URL:
return HttpResponseRedirect(
'{}?next={}'.format(
settings.LOGIN_URL,
parse.quote(request.get_full_path_info())
)
)
# Determine exempt paths
exempt_paths = [
reverse('api-root')
]
if settings.METRICS_ENABLED:
exempt_paths.append(reverse('prometheus-django-metrics'))
# Redirect unauthenticated requests
if not request.path_info.startswith(tuple(exempt_paths)) and request.path_info != settings.LOGIN_URL:
login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}'
return HttpResponseRedirect(login_url)
return self.get_response(request)

View File

@ -57,7 +57,7 @@ class TenantGroup(NestedGroupModel):
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Tenant(PrimaryModel):
"""
A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal

View File

@ -89,6 +89,7 @@ class UserAdmin(UserAdmin_):
('Important dates', {'fields': ('last_login', 'date_joined')}),
)
filter_horizontal = ('groups',)
list_filter = ('is_active', 'is_staff', 'is_superuser', 'groups__name')
def get_inlines(self, request, obj):
if obj is not None:

View File

@ -130,22 +130,24 @@ class ColorChoices(ChoiceSet):
class ButtonColorChoices(ChoiceSet):
"""
Map standard button color choices to Bootstrap color classes
Map standard button color choices to Bootstrap 3 button classes
"""
DEFAULT = 'outline-dark'
BLUE = 'primary'
GREY = 'secondary'
CYAN = 'info'
GREEN = 'success'
RED = 'danger'
YELLOW = 'warning'
GREY = 'secondary'
BLACK = 'dark'
CHOICES = (
(DEFAULT, 'Default'),
(BLUE, 'Blue'),
(GREY, 'Grey'),
(CYAN, 'Cyan'),
(GREEN, 'Green'),
(RED, 'Red'),
(YELLOW, 'Yellow'),
(GREY, 'Grey'),
(BLACK, 'Black')
)

View File

@ -4,7 +4,9 @@ from django.core.paginator import Paginator, Page
class EnhancedPaginator(Paginator):
def __init__(self, object_list, per_page, **kwargs):
def __init__(self, object_list, per_page, orphans=None, **kwargs):
# Determine the page size
try:
per_page = int(per_page)
if per_page < 1:
@ -12,7 +14,13 @@ class EnhancedPaginator(Paginator):
except ValueError:
per_page = settings.PAGINATE_COUNT
super().__init__(object_list, per_page, **kwargs)
# Set orphans count based on page size
if orphans is None and per_page <= 50:
orphans = 5
elif orphans is None:
orphans = 10
super().__init__(object_list, per_page, orphans=orphans, **kwargs)
def _get_page(self, *args, **kwargs):
return EnhancedPage(*args, **kwargs)

View File

@ -5,7 +5,6 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import RelatedField
from django.urls import reverse
from django.utils.html import strip_tags
from django.utils.safestring import mark_safe
from django_tables2 import RequestConfig
from django_tables2.data import TableQuerysetData
@ -15,19 +14,6 @@ from extras.models import CustomField
from .paginator import EnhancedPaginator, get_paginate_count
def stripped_value(self, **kwargs):
"""
Replaces TemplateColumn's value() method to both strip HTML tags and remove any leading/trailing whitespace.
"""
html = super(tables.TemplateColumn, self).value(**kwargs)
return strip_tags(html).strip() if isinstance(html, str) else html
# TODO: We're monkey-patching TemplateColumn here to strip leading/trailing whitespace. This will no longer
# be necessary under django-tables2 v2.3.5+. (See #5926)
tables.TemplateColumn.value = stripped_value
class BaseTable(tables.Table):
"""
Default table for object lists

View File

@ -116,7 +116,7 @@ class ClusterGroup(OrganizationalModel):
# Clusters
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Cluster(PrimaryModel):
"""
A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
@ -199,7 +199,7 @@ class Cluster(PrimaryModel):
# Virtual machines
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class VirtualMachine(PrimaryModel, ConfigContextModel):
"""
A virtual machine which runs inside a Cluster.
@ -374,7 +374,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
# Interfaces
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class VMInterface(PrimaryModel, BaseInterface):
virtual_machine = models.ForeignKey(
to='virtualization.VirtualMachine',

View File

@ -1,4 +1,4 @@
Django==3.2.2
Django==3.2.3
django-cacheops==6.0
django-cors-headers==3.7.0
django-debug-toolbar==3.2.1
@ -7,13 +7,13 @@ django-mptt==0.12.0
django-pglocks==1.0.4
django-prometheus==2.1.0
django-rq==2.4.1
django-tables2==2.3.4
django-tables2==2.4.0
django-taggit==1.4.0
django-timezone-field==4.1.2
djangorestframework==3.12.4
drf-yasg[validation]==1.20.0
gunicorn==20.1.0
Jinja2==2.11.3
Jinja2==3.0.1
Markdown==3.3.4
netaddr==0.8.0
Pillow==8.2.0