Merge branch 'develop-2.7' into 3569-api-choice-slugs

This commit is contained in:
Jeremy Stretch 2019-12-05 17:43:11 -05:00
commit 17898a4c57
27 changed files with 875 additions and 43 deletions

View File

@ -36,13 +36,6 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases) instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases)
and run `upgrade.sh`. and run `upgrade.sh`.
## Alternative Installations
* [Docker container](https://github.com/netbox-community/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
* [Kubernetes deployment](https://github.com/CENGN/netbox-kubernetes) (via [@CENGN](https://github.com/CENGN))
# Providing Feedback # Providing Feedback
Feature requests and bug reports must be submitted as GiHub issues. (Please be Feature requests and bug reports must be submitted as GiHub issues. (Please be

View File

@ -182,7 +182,7 @@ class NewBranchScript(Script):
class Meta: class Meta:
name = "New Branch" name = "New Branch"
description = "Provision a new branch site" description = "Provision a new branch site"
fields = ['site_name', 'switch_count', 'switch_model'] field_order = ['site_name', 'switch_count', 'switch_model']
site_name = StringVar( site_name = StringVar(
description="Name of the new site" description="Name of the new site"

View File

@ -32,7 +32,6 @@ server {
proxy_set_header X-Forwarded-Host $server_name; proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
} }
} }
``` ```

View File

@ -1,3 +1,18 @@
# v2.6.8 (FUTURE)
## Enhancements
* [#3139](https://github.com/netbox-community/netbox/issues/3139) - Disable password change form for LDAP-authenticated users
* [#3457](https://github.com/netbox-community/netbox/issues/3457) - Display cable colors on device view
* [#3329](https://github.com/netbox-community/netbox/issues/3329) - Remove obsolete P3P policy header
* [#3663](https://github.com/netbox-community/netbox/issues/3663) - Add query filters for `created` and `last_updated` fields
## Bug Fixes
* [#3669](https://github.com/netbox-community/netbox/issues/3669) - Include `weight` field in prefix/VLAN role form
* [#3674](https://github.com/netbox-community/netbox/issues/3674) - Include comments on PowerFeed view
* [#3679](https://github.com/netbox-community/netbox/issues/3679) - Fix link for assigned ipaddress in interface page
# v2.6.7 (2019-11-01) # v2.6.7 (2019-11-01)
## Enhancements ## Enhancements

View File

@ -31,6 +31,11 @@ console-ports:
This new functionality replaces the existing CSV-based import form, which did not allow for component template import. This new functionality replaces the existing CSV-based import form, which did not allow for component template import.
### Bulk Import of Device Components ([#822](https://github.com/netbox-community/netbox/issues/822))
NetBox now supports the bulk import of device components such as console ports, power ports, and interfaces. Device
components can be imported in CSV-format.
## Changes ## Changes
### Topology Maps Removed ([#2745](https://github.com/netbox-community/netbox/issues/2745)) ### Topology Maps Removed ([#2745](https://github.com/netbox-community/netbox/issues/2745))
@ -88,7 +93,8 @@ Full connection details are required in both sections, even if they are the same
* [#1865](https://github.com/digitalocean/netbox/issues/1865) - Add console port and console server port types * [#1865](https://github.com/digitalocean/netbox/issues/1865) - Add console port and console server port types
* [#2902](https://github.com/digitalocean/netbox/issues/2902) - Replace supervisord with systemd * [#2902](https://github.com/digitalocean/netbox/issues/2902) - Replace supervisord with systemd
* [#3455](https://github.com/digitalocean/netbox/issues/3455) - Add tenant assignment to cluster * [#3455](https://github.com/digitalocean/netbox/issues/3455) - Add tenant assignment to cluster
* [#3538](https://github.com/digitalocean/netbox/issues/3538) - * [#3564](https://github.com/digitalocean/netbox/issues/3564) - Add interface, ports & bays list view
* [#3538](https://github.com/digitalocean/netbox/issues/3538) - Introduce a REST API endpoint for executing custom scripts
## API Changes ## API Changes

View File

@ -2,14 +2,14 @@ import django_filters
from django.db.models import Q from django.db.models import Q
from dcim.models import Region, Site from dcim.models import Region, Site
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
from .choices import * from .choices import *
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
class ProviderFilter(CustomFieldFilterSet): class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet): class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

View File

@ -2,7 +2,7 @@ import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES from utilities.constants import COLOR_CHOICES
@ -39,7 +39,7 @@ class RegionFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class SiteFilter(TenancyFilterSet, CustomFieldFilterSet): class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -117,7 +117,7 @@ class RackRoleFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'color'] fields = ['id', 'name', 'slug', 'color']
class RackFilter(TenancyFilterSet, CustomFieldFilterSet): class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -252,7 +252,7 @@ class ManufacturerFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class DeviceTypeFilter(CustomFieldFilterSet): class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -424,7 +424,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver'] fields = ['id', 'name', 'slug', 'napalm_driver']
class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet): class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -621,6 +621,26 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
region_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__region__in',
queryset=Region.objects.all(),
label='Region name (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__slug',
queryset=Site.objects.all(),
label='Site name (slug)',
)
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device (ID)', label='Device (ID)',
@ -713,6 +733,27 @@ class InterfaceFilter(django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
region_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__region__in',
queryset=Region.objects.all(),
label='Region name (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__slug',
to_field_name='slug',
queryset=Site.objects.all(),
label='Site name (slug)',
)
device = django_filters.CharFilter( device = django_filters.CharFilter(
method='filter_device', method='filter_device',
field_name='name', field_name='name',
@ -1113,7 +1154,7 @@ class PowerPanelFilter(django_filters.FilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class PowerFeedFilter(CustomFieldFilterSet): class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

View File

@ -25,7 +25,7 @@ from utilities.forms import (
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField, ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
) )
from virtualization.models import Cluster, ClusterGroup from virtualization.models import Cluster, ClusterGroup, VirtualMachine
from .choices import * from .choices import *
from .constants import * from .constants import *
from .models import ( from .models import (
@ -56,6 +56,33 @@ def get_device_by_name_or_pk(name):
return device return device
class DeviceComponentFilterForm(BootstrapMixin, forms.Form):
field_order = [
'q', 'region', 'site'
]
q = forms.CharField(
required=False,
label='Search'
)
region = TreeNodeChoiceField(
queryset=Region.objects.all(),
required=False,
widget=APISelect(
api_url="/api/dcim/regions/"
)
)
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False,
help_text='Name of parent site',
error_messages={
'invalid_choice': 'Site not found.',
}
)
class InterfaceCommonForm: class InterfaceCommonForm:
def clean(self): def clean(self):
@ -2043,6 +2070,11 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
# Console ports # Console ports
# #
class ConsolePortFilterForm(DeviceComponentFilterForm):
model = ConsolePort
class ConsolePortForm(BootstrapMixin, forms.ModelForm): class ConsolePortForm(BootstrapMixin, forms.ModelForm):
tags = TagField( tags = TagField(
required=False required=False
@ -2076,10 +2108,30 @@ class ConsolePortCreateForm(ComponentForm):
) )
class ConsolePortCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
class Meta:
model = ConsolePort
fields = ConsolePort.csv_headers
# #
# Console server ports # Console server ports
# #
class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
model = ConsoleServerPort
class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
tags = TagField( tags = TagField(
required=False required=False
@ -2148,10 +2200,30 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
) )
class ConsoleServerPortCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
class Meta:
model = ConsoleServerPort
fields = ConsoleServerPort.csv_headers
# #
# Power ports # Power ports
# #
class PowerPortFilterForm(DeviceComponentFilterForm):
model = PowerPort
class PowerPortForm(BootstrapMixin, forms.ModelForm): class PowerPortForm(BootstrapMixin, forms.ModelForm):
tags = TagField( tags = TagField(
required=False required=False
@ -2195,10 +2267,30 @@ class PowerPortCreateForm(ComponentForm):
) )
class PowerPortCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
class Meta:
model = PowerPort
fields = PowerPort.csv_headers
# #
# Power outlets # Power outlets
# #
class PowerOutletFilterForm(DeviceComponentFilterForm):
model = PowerOutlet
class PowerOutletForm(BootstrapMixin, forms.ModelForm): class PowerOutletForm(BootstrapMixin, forms.ModelForm):
power_port = forms.ModelChoiceField( power_port = forms.ModelChoiceField(
queryset=PowerPort.objects.all(), queryset=PowerPort.objects.all(),
@ -2260,6 +2352,56 @@ class PowerOutletCreateForm(ComponentForm):
self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent) self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent)
class PowerOutletCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
power_port = FlexibleModelChoiceField(
queryset=PowerPort.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of Power Port',
error_messages={
'invalid_choice': 'Power Port not found.',
}
)
feed_leg = CSVChoiceField(
choices=POWERFEED_LEG_CHOICES,
required=False,
)
class Meta:
model = PowerOutlet
fields = PowerOutlet.csv_headers
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit PowerPort choices to those belonging to this device (or VC master)
if self.is_bound:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
device = None
else:
try:
device = self.instance.device
except Device.DoesNotExist:
device = None
if device:
self.fields['power_port'].queryset = PowerPort.objects.filter(
device__in=[device, device.get_vc_master()]
)
else:
self.fields['power_port'].queryset = PowerPort.objects.none()
class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=PowerOutlet.objects.all(), queryset=PowerOutlet.objects.all(),
@ -2312,6 +2454,11 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm):
# Interfaces # Interfaces
# #
class InterfaceFilterForm(DeviceComponentFilterForm):
model = Interface
class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
untagged_vlan = forms.ModelChoiceField( untagged_vlan = forms.ModelChoiceField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
@ -2514,6 +2661,73 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
self.fields['tagged_vlans'].choices = vlan_choices self.fields['tagged_vlans'].choices = vlan_choices
class InterfaceCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
virtual_machine = FlexibleModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of virtual machine',
error_messages={
'invalid_choice': 'Virtual machine not found.',
}
)
lag = FlexibleModelChoiceField(
queryset=Interface.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of LAG interface',
error_messages={
'invalid_choice': 'LAG interface not found.',
}
)
type = CSVChoiceField(
choices=IFACE_TYPE_CHOICES,
)
mode = CSVChoiceField(
choices=IFACE_MODE_CHOICES,
required=False,
)
class Meta:
model = Interface
fields = Interface.csv_headers
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device (or VC master)
if self.is_bound:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
device = None
else:
device = self.instance.device
if device:
self.fields['lag'].queryset = Interface.objects.filter(
device__in=[device, device.get_vc_master()], type=IFACE_TYPE_LAG
)
else:
self.fields['lag'].queryset = Interface.objects.none()
def clean_enabled(self):
# Make sure enabled is True when it's not included in the uploaded data
if 'enabled' not in self.data:
return True
else:
return self.cleaned_data['enabled']
class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
@ -2644,6 +2858,10 @@ class InterfaceBulkDisconnectForm(ConfirmationForm):
# Front pass-through ports # Front pass-through ports
# #
class FrontPortFilterForm(DeviceComponentFilterForm):
model = FrontPort
class FrontPortForm(BootstrapMixin, forms.ModelForm): class FrontPortForm(BootstrapMixin, forms.ModelForm):
tags = TagField( tags = TagField(
required=False required=False
@ -2730,6 +2948,54 @@ class FrontPortCreateForm(ComponentForm):
} }
class FrontPortCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
rear_port = FlexibleModelChoiceField(
queryset=RearPort.objects.all(),
to_field_name='name',
help_text='Name or ID of Rear Port',
error_messages={
'invalid_choice': 'Rear Port not found.',
}
)
type = CSVChoiceField(
choices=PORT_TYPE_CHOICES,
)
class Meta:
model = FrontPort
fields = FrontPort.csv_headers
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit RearPort choices to those belonging to this device (or VC master)
if self.is_bound:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
device = None
else:
try:
device = self.instance.device
except Device.DoesNotExist:
device = None
if device:
self.fields['rear_port'].queryset = RearPort.objects.filter(
device__in=[device, device.get_vc_master()]
)
else:
self.fields['rear_port'].queryset = RearPort.objects.none()
class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=FrontPort.objects.all(), queryset=FrontPort.objects.all(),
@ -2769,6 +3035,10 @@ class FrontPortBulkDisconnectForm(ConfirmationForm):
# Rear pass-through ports # Rear pass-through ports
# #
class RearPortFilterForm(DeviceComponentFilterForm):
model = RearPort
class RearPortForm(BootstrapMixin, forms.ModelForm): class RearPortForm(BootstrapMixin, forms.ModelForm):
tags = TagField( tags = TagField(
required=False required=False
@ -2804,6 +3074,24 @@ class RearPortCreateForm(ComponentForm):
) )
class RearPortCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
type = CSVChoiceField(
choices=PORT_TYPE_CHOICES,
)
class Meta:
model = RearPort
fields = RearPort.csv_headers
class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=RearPort.objects.all(), queryset=RearPort.objects.all(),
@ -3327,6 +3615,10 @@ class CableFilterForm(BootstrapMixin, forms.Form):
# Device bays # Device bays
# #
class DeviceBayFilterForm(DeviceComponentFilterForm):
model = DeviceBay
class DeviceBayForm(BootstrapMixin, forms.ModelForm): class DeviceBayForm(BootstrapMixin, forms.ModelForm):
tags = TagField( tags = TagField(
required=False required=False
@ -3372,6 +3664,56 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
).exclude(pk=device_bay.device.pk) ).exclude(pk=device_bay.device.pk)
class DeviceBayCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Device not found.',
}
)
installed_device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text='Name or ID of device',
error_messages={
'invalid_choice': 'Child device not found.',
}
)
class Meta:
model = DeviceBay
fields = DeviceBay.csv_headers
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit installed device choices to devices of the correct type and location
if self.is_bound:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
device = None
else:
try:
device = self.instance.device
except Device.DoesNotExist:
device = None
if device:
self.fields['installed_device'].queryset = Device.objects.filter(
site=device.site,
rack=device.rack,
parent_bay__isnull=True,
device_type__u_height=0,
device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
).exclude(pk=device.pk)
else:
self.fields['installed_device'].queryset = Interface.objects.none()
class DeviceBayBulkRenameForm(BulkRenameForm): class DeviceBayBulkRenameForm(BulkRenameForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=DeviceBay.objects.all(), queryset=DeviceBay.objects.all(),

View File

@ -418,6 +418,15 @@ class ConsolePortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class ConsolePortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = ConsolePort
fields = ('device', 'name', 'description')
empty_text = False
class ConsoleServerPortTemplateTable(BaseTable): class ConsoleServerPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@ -432,6 +441,15 @@ class ConsoleServerPortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class ConsoleServerPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = ConsoleServerPort
fields = ('device', 'name', 'description')
empty_text = False
class PowerPortTemplateTable(BaseTable): class PowerPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@ -446,6 +464,15 @@ class PowerPortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class PowerPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = PowerPort
fields = ('device', 'name', 'description', 'maximum_draw', 'allocated_draw')
empty_text = False
class PowerOutletTemplateTable(BaseTable): class PowerOutletTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@ -460,6 +487,15 @@ class PowerOutletTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class PowerOutletImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = PowerOutlet
fields = ('device', 'name', 'description', 'power_port', 'feed_leg')
empty_text = False
class InterfaceTemplateTable(BaseTable): class InterfaceTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}") mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
@ -475,6 +511,16 @@ class InterfaceTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class InterfaceImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
virtual_machine = tables.LinkColumn('virtualization:virtualmachine', args=[Accessor('virtual_machine.pk')], verbose_name='Virtual Machine')
class Meta(BaseTable.Meta):
model = Interface
fields = ('device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode')
empty_text = False
class FrontPortTemplateTable(BaseTable): class FrontPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
rear_port_position = tables.Column( rear_port_position = tables.Column(
@ -492,6 +538,15 @@ class FrontPortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class FrontPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = FrontPort
fields = ('device', 'name', 'description', 'type', 'rear_port', 'rear_port_position')
empty_text = False
class RearPortTemplateTable(BaseTable): class RearPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@ -506,6 +561,15 @@ class RearPortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class RearPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = RearPort
fields = ('device', 'name', 'description', 'type', 'position')
empty_text = False
class DeviceBayTemplateTable(BaseTable): class DeviceBayTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@ -635,6 +699,16 @@ class DeviceImportTable(BaseTable):
# Device components # Device components
# #
class DeviceComponentDetailTable(BaseTable):
pk = ToggleColumn()
cable = tables.LinkColumn()
class Meta(BaseTable.Meta):
order_by = ('device', 'name')
fields = ('pk', 'device', 'name', 'type', 'description', 'cable')
sequence = ('pk', 'device', 'name', 'type', 'description', 'cable')
class ConsolePortTable(BaseTable): class ConsolePortTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -642,6 +716,13 @@ class ConsolePortTable(BaseTable):
fields = ('name', 'type') fields = ('name', 'type')
class ConsolePortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta):
pass
class ConsoleServerPortTable(BaseTable): class ConsoleServerPortTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -649,6 +730,13 @@ class ConsoleServerPortTable(BaseTable):
fields = ('name', 'description') fields = ('name', 'description')
class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta):
pass
class PowerPortTable(BaseTable): class PowerPortTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -656,6 +744,13 @@ class PowerPortTable(BaseTable):
fields = ('name', 'type') fields = ('name', 'type')
class PowerPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta):
pass
class PowerOutletTable(BaseTable): class PowerOutletTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -663,6 +758,13 @@ class PowerOutletTable(BaseTable):
fields = ('name', 'type', 'description') fields = ('name', 'type', 'description')
class PowerOutletDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta):
pass
class InterfaceTable(BaseTable): class InterfaceTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -670,6 +772,15 @@ class InterfaceTable(BaseTable):
fields = ('name', 'type', 'lag', 'enabled', 'mgmt_only', 'description') fields = ('name', 'type', 'lag', 'enabled', 'mgmt_only', 'description')
class InterfaceDetailTable(DeviceComponentDetailTable):
parent = tables.LinkColumn(order_by=('device', 'virtual_machine'))
class Meta(InterfaceTable.Meta):
order_by = ('parent', 'name')
fields = ('pk', 'parent', 'name', 'type', 'description', 'cable')
sequence = ('pk', 'parent', 'name', 'type', 'description', 'cable')
class FrontPortTable(BaseTable): class FrontPortTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -678,6 +789,13 @@ class FrontPortTable(BaseTable):
empty_text = "None" empty_text = "None"
class FrontPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta):
pass
class RearPortTable(BaseTable): class RearPortTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -686,6 +804,13 @@ class RearPortTable(BaseTable):
empty_text = "None" empty_text = "None"
class RearPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta):
pass
class DeviceBayTable(BaseTable): class DeviceBayTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -693,6 +818,26 @@ class DeviceBayTable(BaseTable):
fields = ('name',) fields = ('name',)
class DeviceBayDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
installed_device = tables.LinkColumn()
class Meta(DeviceBayTable.Meta):
fields = ('pk', 'name', 'device', 'installed_device')
sequence = ('pk', 'name', 'device', 'installed_device')
exclude = ('cable',)
class DeviceBayImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
installed_device = tables.LinkColumn('dcim:device', args=[Accessor('installed_device.pk')], verbose_name='Installed Device')
class Meta(BaseTable.Meta):
model = DeviceBay
fields = ('device', 'name', 'installed_device', 'description')
empty_text = False
# #
# Cables # Cables
# #

View File

@ -171,49 +171,58 @@ urlpatterns = [
path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
path(r'devices/<int:pk>/console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'), path(r'devices/<int:pk>/console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
path(r'devices/<int:pk>/console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'), path(r'devices/<int:pk>/console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
path(r'console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
path(r'console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), path(r'console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
path(r'console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
# Console server ports # Console server ports
path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
path(r'devices/<int:pk>/console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), path(r'devices/<int:pk>/console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
path(r'devices/<int:pk>/console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'), path(r'devices/<int:pk>/console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'),
path(r'devices/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), path(r'devices/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
path(r'console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'),
path(r'console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), path(r'console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'), path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
path(r'console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'),
# Power ports # Power ports
path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
path(r'devices/<int:pk>/power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'), path(r'devices/<int:pk>/power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
path(r'devices/<int:pk>/power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'), path(r'devices/<int:pk>/power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
path(r'power-ports/', views.PowerPortListView.as_view(), name='powerport_list'),
path(r'power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), path(r'power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
path(r'power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
# Power outlets # Power outlets
path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
path(r'devices/<int:pk>/power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), path(r'devices/<int:pk>/power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
path(r'devices/<int:pk>/power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'), path(r'devices/<int:pk>/power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'),
path(r'devices/<int:pk>/power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), path(r'devices/<int:pk>/power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
path(r'power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'),
path(r'power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), path(r'power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'), path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
path(r'power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'),
# Interfaces # Interfaces
path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
path(r'devices/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), path(r'devices/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
path(r'devices/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), path(r'devices/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
path(r'devices/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), path(r'devices/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
path(r'interfaces/', views.InterfaceListView.as_view(), name='interface_list'),
path(r'interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), path(r'interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'), path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'), path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
@ -222,40 +231,47 @@ urlpatterns = [
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'), path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
path(r'interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'),
# Front ports # Front ports
# path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), # path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
path(r'devices/<int:pk>/front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'), path(r'devices/<int:pk>/front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'),
path(r'devices/<int:pk>/front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'), path(r'devices/<int:pk>/front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
path(r'devices/<int:pk>/front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'), path(r'devices/<int:pk>/front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
path(r'front-ports/', views.FrontPortListView.as_view(), name='frontport_list'),
path(r'front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), path(r'front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'), path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'), path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
path(r'front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'),
# Rear ports # Rear ports
# path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), # path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
path(r'devices/<int:pk>/rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'), path(r'devices/<int:pk>/rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'),
path(r'devices/<int:pk>/rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'), path(r'devices/<int:pk>/rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
path(r'devices/<int:pk>/rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'), path(r'devices/<int:pk>/rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
path(r'rear-ports/', views.RearPortListView.as_view(), name='rearport_list'),
path(r'rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), path(r'rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'), path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'), path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'), path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
path(r'rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'),
# Device bays # Device bays
path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
path(r'devices/<int:pk>/bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'), path(r'devices/<int:pk>/bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
path(r'devices/<int:pk>/bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), path(r'devices/<int:pk>/bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
path(r'device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'), path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
path(r'device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
# Inventory items # Inventory items
path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'), path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'),

View File

@ -1197,6 +1197,15 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Console ports # Console ports
# #
class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_consoleport'
queryset = ConsolePort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.ConsolePortFilter
filter_form = forms.ConsolePortFilterForm
table = tables.ConsolePortDetailTable
template_name = 'dcim/device_component_list.html'
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleport' permission_required = 'dcim.add_consoleport'
parent_model = Device parent_model = Device
@ -1218,6 +1227,14 @@ class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = ConsolePort model = ConsolePort
class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_consoleport'
model_form = forms.ConsolePortCSVForm
table = tables.ConsolePortImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:consoleport_list'
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport' permission_required = 'dcim.delete_consoleport'
queryset = ConsolePort.objects.all() queryset = ConsolePort.objects.all()
@ -1229,6 +1246,15 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Console server ports # Console server ports
# #
class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_consoleserverport'
queryset = ConsoleServerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.ConsoleServerPortFilter
filter_form = forms.ConsoleServerPortFilterForm
table = tables.ConsoleServerPortDetailTable
template_name = 'dcim/device_component_list.html'
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverport' permission_required = 'dcim.add_consoleserverport'
parent_model = Device parent_model = Device
@ -1250,6 +1276,14 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = ConsoleServerPort model = ConsoleServerPort
class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_consoleserverport'
model_form = forms.ConsoleServerPortCSVForm
table = tables.ConsoleServerPortImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:consoleserverport_list'
class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView): class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_consoleserverport' permission_required = 'dcim.change_consoleserverport'
queryset = ConsoleServerPort.objects.all() queryset = ConsoleServerPort.objects.all()
@ -1281,6 +1315,15 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Power ports # Power ports
# #
class PowerPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_powerport'
queryset = PowerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.PowerPortFilter
filter_form = forms.PowerPortFilterForm
table = tables.PowerPortDetailTable
template_name = 'dcim/device_component_list.html'
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerport' permission_required = 'dcim.add_powerport'
parent_model = Device parent_model = Device
@ -1302,6 +1345,14 @@ class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = PowerPort model = PowerPort
class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_powerport'
model_form = forms.PowerPortCSVForm
table = tables.PowerPortImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:powerport_list'
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport' permission_required = 'dcim.delete_powerport'
queryset = PowerPort.objects.all() queryset = PowerPort.objects.all()
@ -1313,6 +1364,15 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Power outlets # Power outlets
# #
class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_poweroutlet'
queryset = PowerOutlet.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.PowerOutletFilter
filter_form = forms.PowerOutletFilterForm
table = tables.PowerOutletDetailTable
template_name = 'dcim/device_component_list.html'
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlet' permission_required = 'dcim.add_poweroutlet'
parent_model = Device parent_model = Device
@ -1334,6 +1394,14 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = PowerOutlet model = PowerOutlet
class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_poweroutlet'
model_form = forms.PowerOutletCSVForm
table = tables.PowerOutletImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:poweroutlet_list'
class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView): class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_poweroutlet' permission_required = 'dcim.change_poweroutlet'
queryset = PowerOutlet.objects.all() queryset = PowerOutlet.objects.all()
@ -1365,6 +1433,15 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Interfaces # Interfaces
# #
class InterfaceListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_interface'
queryset = Interface.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.InterfaceFilter
filter_form = forms.InterfaceFilterForm
table = tables.InterfaceDetailTable
template_name = 'dcim/device_component_list.html'
class InterfaceView(PermissionRequiredMixin, View): class InterfaceView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_interface' permission_required = 'dcim.view_interface'
@ -1423,6 +1500,14 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = Interface model = Interface
class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_interface'
model_form = forms.InterfaceCSVForm
table = tables.InterfaceImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:interface_list'
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface' permission_required = 'dcim.change_interface'
queryset = Interface.objects.all() queryset = Interface.objects.all()
@ -1454,6 +1539,15 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Front ports # Front ports
# #
class FrontPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_frontport'
queryset = FrontPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.FrontPortFilter
filter_form = forms.FrontPortFilterForm
table = tables.FrontPortDetailTable
template_name = 'dcim/device_component_list.html'
class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView): class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_frontport' permission_required = 'dcim.add_frontport'
parent_model = Device parent_model = Device
@ -1475,6 +1569,14 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = FrontPort model = FrontPort
class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_frontport'
model_form = forms.FrontPortCSVForm
table = tables.FrontPortImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:frontport_list'
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView): class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_frontport' permission_required = 'dcim.change_frontport'
queryset = FrontPort.objects.all() queryset = FrontPort.objects.all()
@ -1506,6 +1608,15 @@ class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Rear ports # Rear ports
# #
class RearPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_rearport'
queryset = RearPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.RearPortFilter
filter_form = forms.RearPortFilterForm
table = tables.RearPortDetailTable
template_name = 'dcim/device_component_list.html'
class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView): class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_rearport' permission_required = 'dcim.add_rearport'
parent_model = Device parent_model = Device
@ -1527,6 +1638,14 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = RearPort model = RearPort
class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_rearport'
model_form = forms.RearPortCSVForm
table = tables.RearPortImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:rearport_list'
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView): class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rearport' permission_required = 'dcim.change_rearport'
queryset = RearPort.objects.all() queryset = RearPort.objects.all()
@ -1558,6 +1677,17 @@ class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Device bays # Device bays
# #
class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_devicebay'
queryset = DeviceBay.objects.prefetch_related(
'device', 'device__site', 'installed_device', 'installed_device__site'
)
filter = filters.DeviceBayFilter
filter_form = forms.DeviceBayFilterForm
table = tables.DeviceBayDetailTable
template_name = 'dcim/device_component_list.html'
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView): class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebay' permission_required = 'dcim.add_devicebay'
parent_model = Device parent_model = Device
@ -1648,6 +1778,14 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View):
}) })
class DeviceBayBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_devicebay'
model_form = forms.DeviceBayCSVForm
table = tables.DeviceBayImportTable
# TODO: change after netbox-community#3564 has been implemented
# default_return_url = 'dcim:devicebay_list'
class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView): class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_devicebay' permission_required = 'dcim.change_devicebay'
queryset = DeviceBay.objects.all() queryset = DeviceBay.objects.all()

View File

@ -18,7 +18,7 @@ router.APIRootView = ExtrasRootView
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice') router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
# Custom field choices # Custom field choices
router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, base_name='custom-field-choice') router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')
# Graphs # Graphs
router.register(r'graphs', views.GraphViewSet) router.register(r'graphs', views.GraphViewSet)

View File

@ -229,3 +229,24 @@ class ObjectChangeFilter(django_filters.FilterSet):
Q(user_name__icontains=value) | Q(user_name__icontains=value) |
Q(object_repr__icontains=value) Q(object_repr__icontains=value)
) )
class CreatedUpdatedFilterSet(django_filters.FilterSet):
created = django_filters.DateFilter()
created__gte = django_filters.DateFilter(
field_name='created',
lookup_expr='gte'
)
created__lte = django_filters.DateFilter(
field_name='created',
lookup_expr='lte'
)
last_updated = django_filters.DateTimeFilter()
last_updated__gte = django_filters.DateTimeFilter(
field_name='last_updated',
lookup_expr='gte'
)
last_updated__lte = django_filters.DateTimeFilter(
field_name='last_updated',
lookup_expr='lte'
)

View File

@ -5,7 +5,7 @@ from django.db.models import Q
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
@ -13,7 +13,7 @@ from .choices import *
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
class VRFFilter(TenancyFilterSet, CustomFieldFilterSet): class VRFFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -49,7 +49,7 @@ class RIRFilter(NameSlugSearchFilterSet):
fields = ['name', 'slug', 'is_private'] fields = ['name', 'slug', 'is_private']
class AggregateFilter(CustomFieldFilterSet): class AggregateFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -110,7 +110,7 @@ class RoleFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet): class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -247,7 +247,7 @@ class PrefixFilter(TenancyFilterSet, CustomFieldFilterSet):
return queryset.filter(prefix__net_mask_length=value) return queryset.filter(prefix__net_mask_length=value)
class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet): class IPAddressFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -384,7 +384,7 @@ class VLANGroupFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class VLANFilter(TenancyFilterSet, CustomFieldFilterSet): class VLANFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -444,7 +444,7 @@ class VLANFilter(TenancyFilterSet, CustomFieldFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class ServiceFilter(django_filters.FilterSet): class ServiceFilter(CreatedUpdatedFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',

View File

@ -240,7 +240,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = Role model = Role
fields = [ fields = [
'name', 'slug', 'name', 'slug', 'weight',
] ]

View File

@ -85,7 +85,7 @@ IPADDRESS_LINK = """
""" """
IPADDRESS_ASSIGN_LINK = """ IPADDRESS_ASSIGN_LINK = """
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a> <a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ record.interface.pk }}&return_url={{ request.path }}">{{ record }}</a>
""" """
IPADDRESS_PARENT = """ IPADDRESS_PARENT = """
@ -292,7 +292,7 @@ class RoleTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Role model = Role
fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'actions') fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'weight', 'actions')
# #

View File

@ -457,6 +457,14 @@ table.report th a {
width: 80px; width: 80px;
border: 1px solid grey; border: 1px solid grey;
} }
.inline-color-block {
display: inline-block;
width: 1.5em;
height: 1.5em;
border: 1px solid grey;
border-radius: .25em;
vertical-align: middle;
}
.text-nowrap { .text-nowrap {
white-space: nowrap; white-space: nowrap;
} }

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 Device from dcim.models import Device
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .models import Secret, SecretRole from .models import Secret, SecretRole
@ -14,7 +14,7 @@ class SecretRoleFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class SecretFilter(CustomFieldFilterSet): class SecretFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

View File

@ -0,0 +1,20 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}{{ table.Meta.model|model_name|capfirst }}s{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'responsive_table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -48,6 +48,9 @@
<td class="text-nowrap"> <td class="text-nowrap">
{% if iface.cable %} {% if iface.cable %}
<a href="{{ iface.cable.get_absolute_url }}">{{ iface.cable }}</a> <a href="{{ iface.cable.get_absolute_url }}">{{ iface.cable }}</a>
{% if iface.cable.color %}
<span class="inline-color-block" style="background-color: #{{ iface.cable.color }}">&nbsp;</span>
{% endif %}
<a href="{% url 'dcim:interface_trace' pk=iface.pk %}" class="btn btn-primary btn-xs" title="Trace"> <a href="{% url 'dcim:interface_trace' pk=iface.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i> <i class="fa fa-share-alt" aria-hidden="true"></i>
</a> </a>

View File

@ -121,6 +121,18 @@
</tr> </tr>
</table> </table>
</div> </div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body rendered-markdown">
{% if powerfeed.comments %}
{{ powerfeed.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -183,6 +183,72 @@
<li{% if not perms.dcim.view_interface %} class="disabled"{% endif %}> <li{% if not perms.dcim.view_interface %} class="disabled"{% endif %}>
<a href="{% url 'dcim:interface_connections_list' %}">Interface Connections</a> <a href="{% url 'dcim:interface_connections_list' %}">Interface Connections</a>
</li> </li>
<li class="divider"></li>
<li class="dropdown-header">Device Components</li>
<li{% if not perms.dcim.view_interface %} class="disabled"{% endif %}>
{% if perms.dcim.add_interface %}
<div class="buttons pull-right">
<a href="{% url 'dcim:interface_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:interface_list' %}">Interfaces</a>
</li>
<li{% if not perms.dcim.view_frontport %} class="disabled"{% endif %}>
{% if perms.dcim.add_frontport %}
<div class="buttons pull-right">
<a href="{% url 'dcim:frontport_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:frontport_list' %}">Front Ports</a>
</li>
<li{% if not perms.dcim.view_rearport %} class="disabled"{% endif %}>
{% if perms.dcim.add_rearport %}
<div class="buttons pull-right">
<a href="{% url 'dcim:rearport_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:rearport_list' %}">Rear Ports</a>
</li>
<li{% if not perms.dcim.view_consoleport %} class="disabled"{% endif %}>
{% if perms.dcim.add_consoleport %}
<div class="buttons pull-right">
<a href="{% url 'dcim:consoleport_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:consoleport_list' %}">Console Ports</a>
</li>
<li{% if not perms.dcim.view_consoleserverport %} class="disabled"{% endif %}>
{% if perms.dcim.add_consoleserverport %}
<div class="buttons pull-right">
<a href="{% url 'dcim:consoleserverport_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:consoleserverport_list' %}">Console Server Ports</a>
</li>
<li{% if not perms.dcim.view_powerport %} class="disabled"{% endif %}>
{% if perms.dcim.add_powerport %}
<div class="buttons pull-right">
<a href="{% url 'dcim:powerport_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:powerport_list' %}">Power Ports</a>
</li>
<li{% if not perms.dcim.view_poweroutlet %} class="disabled"{% endif %}>
{% if perms.dcim.add_poweroutlet %}
<div class="buttons pull-right">
<a href="{% url 'dcim:poweroutlet_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:poweroutlet_list' %}">Power Outlet</a>
</li>
<li{% if not perms.dcim.view_devicebay %} class="disabled"{% endif %}>
{% if perms.dcim.add_devicebay %}
<div class="buttons pull-right">
<a href="{% url 'dcim:devicebay_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:devicebay_list' %}">Device Bays</a>
</li>
</ul> </ul>
</li> </li>
<li class="dropdown"> <li class="dropdown">

View File

@ -12,9 +12,11 @@
<li{% ifequal active_tab "profile" %} class="active"{% endifequal %}> <li{% ifequal active_tab "profile" %} class="active"{% endifequal %}>
<a href="{% url 'user:profile' %}">Profile</a> <a href="{% url 'user:profile' %}">Profile</a>
</li> </li>
{% if not request.user.ldap_username %}
<li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}> <li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}>
<a href="{% url 'user:change_password' %}">Change Password</a> <a href="{% url 'user:change_password' %}">Change Password</a>
</li> </li>
{% endif %}
<li{% ifequal active_tab "api_tokens" %} class="active"{% endifequal %}> <li{% ifequal active_tab "api_tokens" %} class="active"{% endifequal %}>
<a href="{% url 'user:token_list' %}">API Tokens</a> <a href="{% url 'user:token_list' %}">API Tokens</a>
</li> </li>

View File

@ -1,7 +1,7 @@
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from .models import Tenant, TenantGroup from .models import Tenant, TenantGroup
@ -13,7 +13,7 @@ class TenantGroupFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class TenantFilter(CustomFieldFilterSet): class TenantFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

View File

@ -95,6 +95,11 @@ class ChangePasswordView(LoginRequiredMixin, View):
template_name = 'users/change_password.html' template_name = 'users/change_password.html'
def get(self, request): def get(self, request):
# LDAP users cannot change their password here
if getattr(request.user, 'ldap_username'):
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
return redirect('user:profile')
form = PasswordChangeForm(user=request.user) form = PasswordChangeForm(user=request.user)
return render(request, self.template_name, { return render(request, self.template_name, {

View File

@ -4,9 +4,9 @@ from netaddr import EUI
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from dcim.models import DeviceRole, Interface, Platform, Region, Site from dcim.models import DeviceRole, Interface, Platform, Region, Site
from tenancy.models import Tenant from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from extras.filters import CustomFieldFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant
from utilities.filters import ( from utilities.filters import (
MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter, MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
) )
@ -28,7 +28,7 @@ class ClusterGroupFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class ClusterFilter(CustomFieldFilterSet): class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@ -86,7 +86,7 @@ class ClusterFilter(CustomFieldFilterSet):
) )
class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet): class VirtualMachineFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

View File

@ -106,7 +106,7 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='tenants', related_name='clusters',
blank=True, blank=True,
null=True null=True
) )