Merge pull request #10758 from netbox-community/develop

Release v3.3.6
This commit is contained in:
Jeremy Stretch 2022-10-26 10:18:44 -04:00 committed by GitHub
commit f1a7bceef2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 1958 additions and 2313 deletions

View File

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

View File

@ -19,11 +19,15 @@ body:
label: Area label: Area
description: To what section of the documentation does this change primarily pertain? description: To what section of the documentation does this change primarily pertain?
options: options:
- Installation instructions - Features
- Configuration parameters - Installation/upgrade
- Functionality/features - Getting started
- REST API - Configuration
- Administration/development - Customization
- Integrations/API
- Plugins
- Administration
- Development
- Other - Other
validations: validations:
required: true required: true

View File

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

View File

@ -1,13 +1,14 @@
<!-- <!--
Thank you for your interest in contributing to NetBox! Please note that Thank you for your interest in contributing to NetBox! Please note that
our contribution policy requires that a feature request or bug report be our contribution policy requires that a feature request or bug report be
approved and assigned prior to filing a pull request. This helps avoid approved and assigned prior to opening a pull request. This helps avoid
wasting time and effort on something that we might not be able to accept. waste time and effort on a proposed change that we might not be able to
accept.
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
TO YOU, IT WE BE CLOSED AUTOMATICALLY. TO YOU, IT WILL BE CLOSED AUTOMATICALLY.
Specify your assigned issue number on the line below. Please specify your assigned issue number on the line below.
--> -->
### Fixes: #1234 ### Fixes: #1234

View File

@ -46,7 +46,7 @@ Next, create a file in the same directory as `configuration.py` (typically `/opt
### General Server Configuration ### General Server Configuration
!!! info !!! info
When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure. When using Active Directory you may need to specify a port on `AUTH_LDAP_SERVER_URI` to authenticate users from all domains in the forest. Use `3269` for secure, or `3268` for non-secure access to the GC (Global Catalog).
```python ```python
import ldap import ldap
@ -67,6 +67,16 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
# Note that this is a NetBox-specific setting which sets: # Note that this is a NetBox-specific setting which sets:
# ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) # ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
LDAP_IGNORE_CERT_ERRORS = True LDAP_IGNORE_CERT_ERRORS = True
# Include this setting if you want to validate the LDAP server certificates against a CA certificate directory on your server
# Note that this is a NetBox-specific setting which sets:
# ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, LDAP_CA_CERT_DIR)
LDAP_CA_CERT_DIR = '/etc/ssl/certs'
# Include this setting if you want to validate the LDAP server certificates against your own CA.
# Note that this is a NetBox-specific setting which sets:
# ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, LDAP_CA_CERT_FILE)
LDAP_CA_CERT_FILE = '/path/to/example-CA.crt'
``` ```
STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme. STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme.

View File

@ -144,73 +144,73 @@ class MyModelFilterForm(NetBoxModelFilterSetForm):
In addition to the [form fields provided by Django](https://docs.djangoproject.com/en/stable/ref/forms/fields/), NetBox provides several field classes for use within forms to handle specific types of data. These can be imported from `utilities.forms.fields` and are documented below. In addition to the [form fields provided by Django](https://docs.djangoproject.com/en/stable/ref/forms/fields/), NetBox provides several field classes for use within forms to handle specific types of data. These can be imported from `utilities.forms.fields` and are documented below.
::: utilities.forms.ColorField ::: utilities.forms.ColorField
selection: options:
members: false members: false
::: utilities.forms.CommentField ::: utilities.forms.CommentField
selection: options:
members: false members: false
::: utilities.forms.JSONField ::: utilities.forms.JSONField
selection: options:
members: false members: false
::: utilities.forms.MACAddressField ::: utilities.forms.MACAddressField
selection: options:
members: false members: false
::: utilities.forms.SlugField ::: utilities.forms.SlugField
selection: options:
members: false members: false
## Choice Fields ## Choice Fields
::: utilities.forms.ChoiceField ::: utilities.forms.ChoiceField
selection: options:
members: false members: false
::: utilities.forms.MultipleChoiceField ::: utilities.forms.MultipleChoiceField
selection: options:
members: false members: false
## Dynamic Object Fields ## Dynamic Object Fields
::: utilities.forms.DynamicModelChoiceField ::: utilities.forms.DynamicModelChoiceField
selection: options:
members: false members: false
::: utilities.forms.DynamicModelMultipleChoiceField ::: utilities.forms.DynamicModelMultipleChoiceField
selection: options:
members: false members: false
## Content Type Fields ## Content Type Fields
::: utilities.forms.ContentTypeChoiceField ::: utilities.forms.ContentTypeChoiceField
selection: options:
members: false members: false
::: utilities.forms.ContentTypeMultipleChoiceField ::: utilities.forms.ContentTypeMultipleChoiceField
selection: options:
members: false members: false
## CSV Import Fields ## CSV Import Fields
::: utilities.forms.CSVChoiceField ::: utilities.forms.CSVChoiceField
selection: options:
members: false members: false
::: utilities.forms.CSVMultipleChoiceField ::: utilities.forms.CSVMultipleChoiceField
selection: options:
members: false members: false
::: utilities.forms.CSVModelChoiceField ::: utilities.forms.CSVModelChoiceField
selection: options:
members: false members: false
::: utilities.forms.CSVContentTypeField ::: utilities.forms.CSVContentTypeField
selection: options:
members: false members: false
::: utilities.forms.CSVMultipleContentTypeField ::: utilities.forms.CSVMultipleContentTypeField
selection: options:
members: false members: false

View File

@ -32,11 +32,11 @@ schema = MyQuery
NetBox provides two object type classes for use by plugins. NetBox provides two object type classes for use by plugins.
::: netbox.graphql.types.BaseObjectType ::: netbox.graphql.types.BaseObjectType
selection: options:
members: false members: false
::: netbox.graphql.types.NetBoxObjectType ::: netbox.graphql.types.NetBoxObjectType
selection: options:
members: false members: false
## GraphQL Fields ## GraphQL Fields
@ -44,9 +44,9 @@ NetBox provides two object type classes for use by plugins.
NetBox provides two field classes for use by plugins. NetBox provides two field classes for use by plugins.
::: netbox.graphql.fields.ObjectField ::: netbox.graphql.fields.ObjectField
selection: options:
members: false members: false
::: netbox.graphql.fields.ObjectListField ::: netbox.graphql.fields.ObjectListField
selection: options:
members: false members: false

View File

@ -52,38 +52,38 @@ This will automatically apply any user-specific preferences for the table. (If u
The table column classes listed below are supported for use in plugins. These classes can be imported from `netbox.tables.columns`. The table column classes listed below are supported for use in plugins. These classes can be imported from `netbox.tables.columns`.
::: netbox.tables.BooleanColumn ::: netbox.tables.BooleanColumn
selection: options:
members: false members: false
::: netbox.tables.ChoiceFieldColumn ::: netbox.tables.ChoiceFieldColumn
selection: options:
members: false members: false
::: netbox.tables.ColorColumn ::: netbox.tables.ColorColumn
selection: options:
members: false members: false
::: netbox.tables.ColoredLabelColumn ::: netbox.tables.ColoredLabelColumn
selection: options:
members: false members: false
::: netbox.tables.ContentTypeColumn ::: netbox.tables.ContentTypeColumn
selection: options:
members: false members: false
::: netbox.tables.ContentTypesColumn ::: netbox.tables.ContentTypesColumn
selection: options:
members: false members: false
::: netbox.tables.MarkdownColumn ::: netbox.tables.MarkdownColumn
selection: options:
members: false members: false
::: netbox.tables.TagColumn ::: netbox.tables.TagColumn
selection: options:
members: false members: false
::: netbox.tables.TemplateColumn ::: netbox.tables.TemplateColumn
selection: options:
members: members:
- __init__ - __init__

View File

@ -84,24 +84,24 @@ Below are the class definitions for NetBox's object views. These views handle CR
::: netbox.views.generic.base.BaseObjectView ::: netbox.views.generic.base.BaseObjectView
::: netbox.views.generic.ObjectView ::: netbox.views.generic.ObjectView
selection: options:
members: members:
- get_object - get_object
- get_template_name - get_template_name
::: netbox.views.generic.ObjectEditView ::: netbox.views.generic.ObjectEditView
selection: options:
members: members:
- get_object - get_object
- alter_object - alter_object
::: netbox.views.generic.ObjectDeleteView ::: netbox.views.generic.ObjectDeleteView
selection: options:
members: members:
- get_object - get_object
::: netbox.views.generic.ObjectChildrenView ::: netbox.views.generic.ObjectChildrenView
selection: options:
members: members:
- get_children - get_children
- prep_table_data - prep_table_data
@ -113,22 +113,22 @@ Below are the class definitions for NetBox's multi-object views. These views han
::: netbox.views.generic.base.BaseMultiObjectView ::: netbox.views.generic.base.BaseMultiObjectView
::: netbox.views.generic.ObjectListView ::: netbox.views.generic.ObjectListView
selection: options:
members: members:
- get_table - get_table
- export_table - export_table
- export_template - export_template
::: netbox.views.generic.BulkImportView ::: netbox.views.generic.BulkImportView
selection: options:
members: false members: false
::: netbox.views.generic.BulkEditView ::: netbox.views.generic.BulkEditView
selection: options:
members: false members: false
::: netbox.views.generic.BulkDeleteView ::: netbox.views.generic.BulkDeleteView
selection: options:
members: members:
- get_form - get_form
@ -137,12 +137,12 @@ Below are the class definitions for NetBox's multi-object views. These views han
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path. These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
::: netbox.views.generic.ObjectChangeLogView ::: netbox.views.generic.ObjectChangeLogView
selection: options:
members: members:
- get_form - get_form
::: netbox.views.generic.ObjectJournalView ::: netbox.views.generic.ObjectJournalView
selection: options:
members: members:
- get_form - get_form

View File

@ -1,5 +1,34 @@
# NetBox v3.3 # NetBox v3.3
## v3.3.6 (2022-10-26)
### Enhancements
* [#9584](https://github.com/netbox-community/netbox/issues/9584) - Enable filtering devices by device type slug
* [#9722](https://github.com/netbox-community/netbox/issues/9722) - Add LDAP configuration parameters to specify certificates
* [#10580](https://github.com/netbox-community/netbox/issues/10580) - Link "assigned" checkbox in IP address table to assigned interface
* [#10639](https://github.com/netbox-community/netbox/issues/10639) - Set cookie paths according to configured `BASE_PATH`
* [#10685](https://github.com/netbox-community/netbox/issues/10685) - Position A/Z termination cards above the fold under circuit view
### Bug Fixes
* [#9669](https://github.com/netbox-community/netbox/issues/9669) - Strip colons from usernames when using remote authentication
* [#10575](https://github.com/netbox-community/netbox/issues/10575) - Include OIDC dependencies for python-social-auth
* [#10584](https://github.com/netbox-community/netbox/issues/10584) - Fix service clone link
* [#10610](https://github.com/netbox-community/netbox/issues/10610) - Allow assignment of VC member to LAG on non-master peer
* [#10643](https://github.com/netbox-community/netbox/issues/10643) - Ensure consistent display of custom fields for all model forms
* [#10646](https://github.com/netbox-community/netbox/issues/10646) - Fix filtering of power feed by power panel when connecting a cable
* [#10655](https://github.com/netbox-community/netbox/issues/10655) - Correct display of assigned contacts in object tables
* [#10682](https://github.com/netbox-community/netbox/issues/10682) - Correct home view links to connection lists
* [#10712](https://github.com/netbox-community/netbox/issues/10712) - Fix ModuleNotFoundError exception when generating API schema under Python 3.9+
* [#10716](https://github.com/netbox-community/netbox/issues/10716) - Add left/right page plugin content embeds for tag view
* [#10719](https://github.com/netbox-community/netbox/issues/10719) - Prevent user without sufficient permission from creating an IP address via FHRP group creation
* [#10723](https://github.com/netbox-community/netbox/issues/10723) - Distinguish between inside/outside NAT assignments for device/VM primary IPs
* [#10745](https://github.com/netbox-community/netbox/issues/10745) - Correct display of status field in clusters list
* [#10746](https://github.com/netbox-community/netbox/issues/10746) - Add missing status attribute to cluster view
---
## v3.3.5 (2022-10-05) ## v3.3.5 (2022-10-05)
### Enhancements ### Enhancements

View File

@ -30,7 +30,7 @@ plugins:
- os.chdir('netbox/') - os.chdir('netbox/')
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings") - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
- django.setup() - django.setup()
rendering: options:
heading_level: 3 heading_level: 3
members_order: source members_order: source
show_root_heading: true show_root_heading: true

View File

@ -76,6 +76,12 @@ class ProviderNetworkForm(NetBoxModelForm):
class CircuitTypeForm(NetBoxModelForm): class CircuitTypeForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = (
('Circuit Type', (
'name', 'slug', 'description', 'tags',
)),
)
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = [ fields = [

View File

@ -1,8 +1,9 @@
import django_tables2 as tables import django_tables2 as tables
from circuits.models import * from circuits.models import *
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from .columns import CommitRateColumn from .columns import CommitRateColumn
__all__ = ( __all__ = (
@ -39,7 +40,7 @@ class CircuitTypeTable(NetBoxTable):
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug') default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
class CircuitTable(TenancyColumnsMixin, NetBoxTable): class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
cid = tables.Column( cid = tables.Column(
linkify=True, linkify=True,
verbose_name='Circuit ID' verbose_name='Circuit ID'
@ -58,9 +59,6 @@ class CircuitTable(TenancyColumnsMixin, NetBoxTable):
) )
commit_rate = CommitRateColumn() commit_rate = CommitRateColumn()
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='circuits:circuit_list' url_name='circuits:circuit_list'
) )

View File

@ -1,7 +1,8 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor
from circuits.models import * from circuits.models import *
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
__all__ = ( __all__ = (
@ -10,7 +11,7 @@ __all__ = (
) )
class ProviderTable(NetBoxTable): class ProviderTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
@ -31,9 +32,6 @@ class ProviderTable(NetBoxTable):
verbose_name='Circuits' verbose_name='Circuits'
) )
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='circuits:provider_list' url_name='circuits:provider_list'
) )

View File

@ -800,6 +800,12 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
to_field_name='slug', to_field_name='slug',
label='Manufacturer (slug)', label='Manufacturer (slug)',
) )
device_type = django_filters.ModelMultipleChoiceFilter(
field_name='device_type__slug',
queryset=DeviceType.objects.all(),
to_field_name='slug',
label='Device type (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter( device_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
label='Device type (ID)', label='Device type (ID)',
@ -1357,7 +1363,7 @@ class InterfaceFilterSet(
try: try:
devices = Device.objects.filter(pk__in=id_list) devices = Device.objects.filter(pk__in=id_list)
for device in devices: for device in devices:
vc_interface_ids += device.vc_interfaces().values_list('id', flat=True) vc_interface_ids += device.vc_interfaces(if_master=False).values_list('id', flat=True)
return queryset.filter(pk__in=vc_interface_ids) return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist: except Device.DoesNotExist:
return queryset.none() return queryset.none()

View File

@ -108,7 +108,7 @@ def get_cable_form(a_type, b_type):
label='Power Feed', label='Power Feed',
disabled_indicator='_occupied', disabled_indicator='_occupied',
query_params={ query_params={
'powerpanel_id': f'$termination_{cable_end}_powerpanel', 'power_panel_id': f'$termination_{cable_end}_powerpanel',
} }
) )

View File

@ -78,6 +78,12 @@ class RegionForm(NetBoxModelForm):
) )
slug = SlugField() slug = SlugField()
fieldsets = (
('Region', (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
class Meta: class Meta:
model = Region model = Region
fields = ( fields = (
@ -92,6 +98,12 @@ class SiteGroupForm(NetBoxModelForm):
) )
slug = SlugField() slug = SlugField()
fieldsets = (
('Site Group', (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
class Meta: class Meta:
model = SiteGroup model = SiteGroup
fields = ( fields = (
@ -213,6 +225,12 @@ class LocationForm(TenancyForm, NetBoxModelForm):
class RackRoleForm(NetBoxModelForm): class RackRoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = (
('Rack Role', (
'name', 'slug', 'color', 'description', 'tags',
)),
)
class Meta: class Meta:
model = RackRole model = RackRole
fields = [ fields = [
@ -340,6 +358,12 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
class ManufacturerForm(NetBoxModelForm): class ManufacturerForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = (
('Manufacturer', (
'name', 'slug', 'description', 'tags',
)),
)
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = [ fields = [
@ -406,6 +430,12 @@ class ModuleTypeForm(NetBoxModelForm):
class DeviceRoleForm(NetBoxModelForm): class DeviceRoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = (
('Device Role', (
'name', 'slug', 'color', 'vm_role', 'description', 'tags',
)),
)
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = [ fields = [
@ -422,6 +452,13 @@ class PlatformForm(NetBoxModelForm):
max_length=64 max_length=64
) )
fieldsets = (
('Platform', (
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
)),
)
class Meta: class Meta:
model = Platform model = Platform
fields = [ fields = [
@ -1577,6 +1614,12 @@ class InventoryItemForm(DeviceComponentForm):
class InventoryItemRoleForm(NetBoxModelForm): class InventoryItemRoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = (
('Inventory Item Role', (
'name', 'slug', 'color', 'description', 'tags',
)),
)
class Meta: class Meta:
model = InventoryItemRole model = InventoryItemRole
fields = [ fields = [

View File

@ -1,12 +1,26 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor
from dcim.models import ( from dcim.models import (
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, FrontPort, Interface, InventoryItem, ConsolePort,
InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis, ConsoleServerPort,
Device,
DeviceBay,
DeviceRole,
FrontPort,
Interface,
InventoryItem,
InventoryItemRole,
ModuleBay,
Platform,
PowerOutlet,
PowerPort,
RearPort,
VirtualChassis,
) )
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from .template_code import * from .template_code import *
__all__ = ( __all__ = (
@ -137,7 +151,7 @@ class PlatformTable(NetBoxTable):
# Devices # Devices
# #
class DeviceTable(TenancyColumnsMixin, NetBoxTable): class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.TemplateColumn( name = tables.TemplateColumn(
order_by=('_name',), order_by=('_name',),
template_code=DEVICE_LINK template_code=DEVICE_LINK
@ -201,9 +215,6 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
verbose_name='VC Priority' verbose_name='VC Priority'
) )
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:device_list' url_name='dcim:device_list'
) )

View File

@ -1,10 +1,22 @@
import django_tables2 as tables import django_tables2 as tables
from dcim.models import ( from dcim.models import (
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate, ConsolePortTemplate,
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, ConsoleServerPortTemplate,
DeviceBayTemplate,
DeviceType,
FrontPortTemplate,
InterfaceTemplate,
InventoryItemTemplate,
Manufacturer,
ModuleBayTemplate,
PowerOutletTemplate,
PowerPortTemplate,
RearPortTemplate,
) )
from tenancy.tables import ContactsColumnMixin
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS
__all__ = ( __all__ = (
@ -27,7 +39,7 @@ __all__ = (
# Manufacturers # Manufacturers
# #
class ManufacturerTable(NetBoxTable): class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
@ -43,9 +55,6 @@ class ManufacturerTable(NetBoxTable):
verbose_name='Platforms' verbose_name='Platforms'
) )
slug = tables.Column() slug = tables.Column()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:manufacturer_list' url_name='dcim:manufacturer_list'
) )

View File

@ -1,7 +1,9 @@
import django_tables2 as tables import django_tables2 as tables
from dcim.models import PowerFeed, PowerPanel from dcim.models import PowerFeed, PowerPanel
from tenancy.tables import ContactsColumnMixin
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from .devices import CableTerminationTable from .devices import CableTerminationTable
__all__ = ( __all__ = (
@ -14,7 +16,7 @@ __all__ = (
# Power panels # Power panels
# #
class PowerPanelTable(NetBoxTable): class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
@ -29,9 +31,6 @@ class PowerPanelTable(NetBoxTable):
url_params={'power_panel_id': 'pk'}, url_params={'power_panel_id': 'pk'},
verbose_name='Feeds' verbose_name='Feeds'
) )
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:powerpanel_list' url_name='dcim:powerpanel_list'
) )

View File

@ -1,9 +1,9 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor
from dcim.models import Rack, RackReservation, RackRole from dcim.models import Rack, RackReservation, RackRole
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
__all__ = ( __all__ = (
'RackTable', 'RackTable',
@ -37,7 +37,7 @@ class RackRoleTable(NetBoxTable):
# Racks # Racks
# #
class RackTable(TenancyColumnsMixin, NetBoxTable): class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
order_by=('_name',), order_by=('_name',),
linkify=True linkify=True
@ -68,9 +68,6 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
orderable=False, orderable=False,
verbose_name='Power' verbose_name='Power'
) )
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:rack_list' url_name='dcim:rack_list'
) )

View File

@ -1,8 +1,9 @@
import django_tables2 as tables import django_tables2 as tables
from dcim.models import Location, Region, Site, SiteGroup from dcim.models import Location, Region, Site, SiteGroup
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from .template_code import LOCATION_BUTTONS from .template_code import LOCATION_BUTTONS
__all__ = ( __all__ = (
@ -17,7 +18,7 @@ __all__ = (
# Regions # Regions
# #
class RegionTable(NetBoxTable): class RegionTable(ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn( name = columns.MPTTColumn(
linkify=True linkify=True
) )
@ -26,9 +27,6 @@ class RegionTable(NetBoxTable):
url_params={'region_id': 'pk'}, url_params={'region_id': 'pk'},
verbose_name='Sites' verbose_name='Sites'
) )
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:region_list' url_name='dcim:region_list'
) )
@ -46,7 +44,7 @@ class RegionTable(NetBoxTable):
# Site groups # Site groups
# #
class SiteGroupTable(NetBoxTable): class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn( name = columns.MPTTColumn(
linkify=True linkify=True
) )
@ -55,9 +53,6 @@ class SiteGroupTable(NetBoxTable):
url_params={'group_id': 'pk'}, url_params={'group_id': 'pk'},
verbose_name='Sites' verbose_name='Sites'
) )
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:sitegroup_list' url_name='dcim:sitegroup_list'
) )
@ -75,7 +70,7 @@ class SiteGroupTable(NetBoxTable):
# Sites # Sites
# #
class SiteTable(TenancyColumnsMixin, NetBoxTable): class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
@ -97,9 +92,6 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
verbose_name='ASN Count' verbose_name='ASN Count'
) )
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:site_list' url_name='dcim:site_list'
) )
@ -118,7 +110,7 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable):
# Locations # Locations
# #
class LocationTable(TenancyColumnsMixin, NetBoxTable): class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn( name = columns.MPTTColumn(
linkify=True linkify=True
) )
@ -136,9 +128,6 @@ class LocationTable(TenancyColumnsMixin, NetBoxTable):
url_params={'location_id': 'pk'}, url_params={'location_id': 'pk'},
verbose_name='Devices' verbose_name='Devices'
) )
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:location_list' url_name='dcim:location_list'
) )

View File

@ -1643,6 +1643,8 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
device_types = DeviceType.objects.all()[:2] device_types = DeviceType.objects.all()[:2]
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]} params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'device_type': [device_types[0].slug, device_types[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_devicerole(self): def test_devicerole(self):
device_roles = DeviceRole.objects.all()[:2] device_roles = DeviceRole.objects.all()[:2]

View File

@ -88,6 +88,12 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm):
class RIRForm(NetBoxModelForm): class RIRForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = (
('RIR', (
'name', 'slug', 'is_private', 'description', 'tags',
)),
)
class Meta: class Meta:
model = RIR model = RIR
fields = [ fields = [
@ -164,6 +170,12 @@ class ASNForm(TenancyForm, NetBoxModelForm):
class RoleForm(NetBoxModelForm): class RoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = (
('Role', (
'name', 'slug', 'weight', 'description', 'tags',
)),
)
class Meta: class Meta:
model = Role model = Role
fields = [ fields = [
@ -540,6 +552,7 @@ class FHRPGroupForm(NetBoxModelForm):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs) instance = super().save(*args, **kwargs)
user = getattr(instance, '_user', None) # Set under FHRPGroupEditView.alter_object()
# Check if we need to create a new IPAddress for the group # Check if we need to create a new IPAddress for the group
if self.cleaned_data.get('ip_address'): if self.cleaned_data.get('ip_address'):
@ -553,7 +566,7 @@ class FHRPGroupForm(NetBoxModelForm):
ipaddress.save() ipaddress.save()
# Check that the new IPAddress conforms with any assigned object-level permissions # Check that the new IPAddress conforms with any assigned object-level permissions
if not IPAddress.objects.filter(pk=ipaddress.pk).first(): if not IPAddress.objects.restrict(user, 'add').filter(pk=ipaddress.pk).first():
raise PermissionsViolation() raise PermissionsViolation()
return instance return instance
@ -784,6 +797,12 @@ class ServiceTemplateForm(NetBoxModelForm):
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen." help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
) )
fieldsets = (
('Service Template', (
'name', 'protocol', 'ports', 'description', 'tags',
)),
)
class Meta: class Meta:
model = ServiceTemplate model = ServiceTemplate
fields = ('name', 'protocol', 'ports', 'description', 'tags') fields = ('name', 'protocol', 'ports', 'description', 'tags')

View File

@ -92,6 +92,8 @@ class Service(ServiceBase, NetBoxModel):
verbose_name='IP addresses' verbose_name='IP addresses'
) )
clone_fields = ['protocol', 'ports', 'description', 'device', 'virtual_machine', 'ipaddresses', ]
class Meta: class Meta:
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique

View File

@ -375,7 +375,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
) )
assigned = columns.BooleanColumn( assigned = columns.BooleanColumn(
accessor='assigned_object_id', accessor='assigned_object_id',
linkify=True, linkify=lambda record: record.assigned_object.get_absolute_url(),
verbose_name='Assigned' verbose_name='Assigned'
) )
tags = columns.TagColumn( tags = columns.TagColumn(

View File

@ -930,6 +930,12 @@ class FHRPGroupEditView(generic.ObjectEditView):
return return_url return return_url
def alter_object(self, obj, request, url_args, url_kwargs):
# Workaround to solve #10719. Capture the current user on the FHRPGroup instance so that
# we can evaluate permissions during the creation of a new IPAddress within the form.
obj._user = request.user
return obj
class FHRPGroupDeleteView(generic.ObjectDeleteView): class FHRPGroupDeleteView(generic.ObjectDeleteView):
queryset = FHRPGroup.objects.all() queryset = FHRPGroup.objects.all()

View File

@ -58,22 +58,24 @@ class TokenAuthentication(authentication.TokenAuthentication):
if token.is_expired: if token.is_expired:
raise exceptions.AuthenticationFailed("Token expired") raise exceptions.AuthenticationFailed("Token expired")
if not token.user.is_active: user = token.user
raise exceptions.AuthenticationFailed("User inactive")
# When LDAP authentication is active try to load user data from LDAP directory # When LDAP authentication is active try to load user data from LDAP directory
if settings.REMOTE_AUTH_BACKEND == 'netbox.authentication.LDAPBackend': if settings.REMOTE_AUTH_BACKEND == 'netbox.authentication.LDAPBackend':
from netbox.authentication import LDAPBackend from netbox.authentication import LDAPBackend
ldap_backend = LDAPBackend() ldap_backend = LDAPBackend()
# Load from LDAP if FIND_GROUP_PERMS is active # Load from LDAP if FIND_GROUP_PERMS is active
if ldap_backend.settings.FIND_GROUP_PERMS: # Always query LDAP when user is not active, otherwise it is never activated again
user = ldap_backend.populate_user(token.user.username) if ldap_backend.settings.FIND_GROUP_PERMS or not token.user.is_active:
ldap_user = ldap_backend.populate_user(token.user.username)
# If the user is found in the LDAP directory use it, if not fallback to the local user # If the user is found in the LDAP directory use it, if not fallback to the local user
if user: if ldap_user:
return user, token user = ldap_user
return token.user, token if not user.is_active:
raise exceptions.AuthenticationFailed("User inactive")
return user, token
class TokenPermissions(DjangoObjectPermissions): class TokenPermissions(DjangoObjectPermissions):

View File

@ -108,6 +108,5 @@ class ObjectValidationMixin:
conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count() conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
if conforming_count != len(instance): if conforming_count != len(instance):
raise ObjectDoesNotExist raise ObjectDoesNotExist
else: elif not self.queryset.filter(pk=instance.pk).exists():
# Check that the instance is matched by the view's queryset raise ObjectDoesNotExist
self.queryset.get(pk=instance.pk)

View File

@ -351,6 +351,14 @@ class LDAPBackend:
if getattr(ldap_config, 'LDAP_IGNORE_CERT_ERRORS', False): if getattr(ldap_config, 'LDAP_IGNORE_CERT_ERRORS', False):
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
# Optionally set CA cert directory
if ca_cert_dir := getattr(ldap_config, 'LDAP_CA_CERT_DIR', None):
ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, ca_cert_dir)
# Optionally set CA cert file
if ca_cert_file := getattr(ldap_config, 'LDAP_CA_CERT_FILE', None):
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, ca_cert_file)
return obj return obj

View File

@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
# Environment setup # Environment setup
# #
VERSION = '3.3.5' VERSION = '3.3.6'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()
@ -85,6 +85,7 @@ CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', []) CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', []) CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken') CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken')
CSRF_COOKIE_PATH = BASE_PATH or '/'
CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', []) CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', [])
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y') DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
@ -129,6 +130,8 @@ SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE',
SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {}) SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {})
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None) SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid') SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
SESSION_COOKIE_PATH = BASE_PATH or '/'
LANGUAGE_COOKIE_PATH = BASE_PATH or '/'
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
@ -498,7 +501,7 @@ for param in dir(configuration):
# Force usage of PostgreSQL's JSONB field for extra data # Force usage of PostgreSQL's JSONB field for extra data
SOCIAL_AUTH_JSONFIELD_ENABLED = True SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'netbox.users.utils.clean_username'
# #
# Django Prometheus # Django Prometheus

View File

@ -1,5 +1,6 @@
import platform import platform
import sys import sys
from collections import namedtuple
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
@ -8,6 +9,7 @@ from django.shortcuts import redirect, render
from django.template import loader from django.template import loader
from django.template.exceptions import TemplateDoesNotExist from django.template.exceptions import TemplateDoesNotExist
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _
from django.views.decorators.csrf import requires_csrf_token from django.views.decorators.csrf import requires_csrf_token
from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
from django.views.generic import View from django.views.generic import View
@ -24,100 +26,90 @@ from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
from netbox.constants import SEARCH_MAX_RESULTS from netbox.constants import SEARCH_MAX_RESULTS
from netbox.forms import SearchForm from netbox.forms import SearchForm
from netbox.search import SEARCH_TYPES from netbox.search import SEARCH_TYPES
from tenancy.models import Tenant from tenancy.models import Contact, Tenant
from virtualization.models import Cluster, VirtualMachine from virtualization.models import Cluster, VirtualMachine
from wireless.models import WirelessLAN, WirelessLink from wireless.models import WirelessLAN, WirelessLink
Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count'))
class HomeView(View): class HomeView(View):
template_name = 'home.html' template_name = 'home.html'
def get(self, request): def get(self, request):
if settings.LOGIN_REQUIRED and not request.user.is_authenticated: if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
return redirect("login") return redirect('login')
connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter( console_connections = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
_path__is_complete=True _path__is_complete=True
) ).count
connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter( power_connections = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
_path__is_complete=True _path__is_complete=True
) ).count
connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter( interface_connections = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
_path__is_complete=True _path__is_complete=True
) ).count
def get_count_queryset(model):
return model.objects.restrict(request.user, 'view').count
def build_stats(): def build_stats():
org = ( org = (
("dcim.view_site", "Sites", Site.objects.restrict(request.user, 'view').count), Link(_('Sites'), 'dcim:site_list', 'dcim.view_site', get_count_queryset(Site)),
("tenancy.view_tenant", "Tenants", Tenant.objects.restrict(request.user, 'view').count), Link(_('Tenants'), 'tenancy:tenant_list', 'tenancy.view_tenant', get_count_queryset(Tenant)),
Link(_('Contacts'), 'tenancy:contact_list', 'tenancy.view_contact', get_count_queryset(Contact)),
) )
dcim = ( dcim = (
("dcim.view_rack", "Racks", Rack.objects.restrict(request.user, 'view').count), Link(_('Racks'), 'dcim:rack_list', 'dcim.view_rack', get_count_queryset(Rack)),
("dcim.view_devicetype", "Device Types", DeviceType.objects.restrict(request.user, 'view').count), Link(_('Device Types'), 'dcim:devicetype_list', 'dcim.view_devicetype', get_count_queryset(DeviceType)),
("dcim.view_device", "Devices", Device.objects.restrict(request.user, 'view').count), Link(_('Devices'), 'dcim:device_list', 'dcim.view_device', get_count_queryset(Device)),
) )
ipam = ( ipam = (
("ipam.view_vrf", "VRFs", VRF.objects.restrict(request.user, 'view').count), Link(_('VRFs'), 'ipam:vrf_list', 'ipam.view_vrf', get_count_queryset(VRF)),
("ipam.view_aggregate", "Aggregates", Aggregate.objects.restrict(request.user, 'view').count), Link(_('Aggregates'), 'ipam:aggregate_list', 'ipam.view_aggregate', get_count_queryset(Aggregate)),
("ipam.view_prefix", "Prefixes", Prefix.objects.restrict(request.user, 'view').count), Link(_('Prefixes'), 'ipam:prefix_list', 'ipam.view_prefix', get_count_queryset(Prefix)),
("ipam.view_iprange", "IP Ranges", IPRange.objects.restrict(request.user, 'view').count), Link(_('IP Ranges'), 'ipam:iprange_list', 'ipam.view_iprange', get_count_queryset(IPRange)),
("ipam.view_ipaddress", "IP Addresses", IPAddress.objects.restrict(request.user, 'view').count), Link(_('IP Addresses'), 'ipam:ipaddress_list', 'ipam.view_ipaddress', get_count_queryset(IPAddress)),
("ipam.view_vlan", "VLANs", VLAN.objects.restrict(request.user, 'view').count) Link(_('VLANs'), 'ipam:vlan_list', 'ipam.view_vlan', get_count_queryset(VLAN)),
) )
circuits = ( circuits = (
("circuits.view_provider", "Providers", Provider.objects.restrict(request.user, 'view').count), Link(_('Providers'), 'circuits:provider_list', 'circuits.view_provider', get_count_queryset(Provider)),
("circuits.view_circuit", "Circuits", Circuit.objects.restrict(request.user, 'view').count), Link(_('Circuits'), 'circuits:circuit_list', 'circuits.view_circuit', get_count_queryset(Circuit))
) )
virtualization = ( virtualization = (
("virtualization.view_cluster", "Clusters", Cluster.objects.restrict(request.user, 'view').count), Link(_('Clusters'), 'virtualization:cluster_list', 'virtualization.view_cluster',
("virtualization.view_virtualmachine", "Virtual Machines", VirtualMachine.objects.restrict(request.user, 'view').count), get_count_queryset(Cluster)),
Link(_('Virtual Machines'), 'virtualization:virtualmachine_list', 'virtualization.view_virtualmachine',
get_count_queryset(VirtualMachine)),
) )
connections = ( connections = (
("dcim.view_cable", "Cables", Cable.objects.restrict(request.user, 'view').count), Link(_('Cables'), 'dcim:cable_list', 'dcim.view_cable', get_count_queryset(Cable)),
("dcim.view_consoleport", "Console", connected_consoleports.count), Link(_('Interfaces'), 'dcim:interface_connections_list', 'dcim.view_interface', interface_connections),
("dcim.view_interface", "Interfaces", connected_interfaces.count), Link(_('Console'), 'dcim:console_connections_list', 'dcim.view_consoleport', console_connections),
("dcim.view_powerport", "Power Connections", connected_powerports.count), Link(_('Power'), 'dcim:power_connections_list', 'dcim.view_powerport', power_connections),
) )
power = ( power = (
("dcim.view_powerpanel", "Power Panels", PowerPanel.objects.restrict(request.user, 'view').count), Link(_('Power Panels'), 'dcim:powerpanel_list', 'dcim.view_powerpanel', get_count_queryset(PowerPanel)),
("dcim.view_powerfeed", "Power Feeds", PowerFeed.objects.restrict(request.user, 'view').count), Link(_('Power Feeds'), 'dcim:powerfeed_list', 'dcim.view_powerfeed', get_count_queryset(PowerFeed)),
) )
wireless = ( wireless = (
("wireless.view_wirelesslan", "Wireless LANs", WirelessLAN.objects.restrict(request.user, 'view').count), Link(_('Wireless LANs'), 'wireless:wirelesslan_list', 'wireless.view_wirelesslan',
("wireless.view_wirelesslink", "Wireless Links", WirelessLink.objects.restrict(request.user, 'view').count), get_count_queryset(WirelessLAN)),
Link(_('Wireless Links'), 'wireless:wirelesslink_list', 'wireless.view_wirelesslink',
get_count_queryset(WirelessLink)),
) )
sections = ( stats = (
("Organization", org, "domain"), (_('Organization'), org, 'domain'),
("IPAM", ipam, "counter"), (_('IPAM'), ipam, 'counter'),
("Virtualization", virtualization, "monitor"), (_('Virtualization'), virtualization, 'monitor'),
("Inventory", dcim, "server"), (_('Inventory'), dcim, 'server'),
("Circuits", circuits, "transit-connection-variant"), (_('Circuits'), circuits, 'transit-connection-variant'),
("Connections", connections, "cable-data"), (_('Connections'), connections, 'cable-data'),
("Power", power, "flash"), (_('Power'), power, 'flash'),
("Wireless", wireless, "wifi"), (_('Wireless'), wireless, 'wifi'),
) )
stats = []
for section_label, section_items, icon_class in sections:
items = []
for perm, item_label, get_count in section_items:
app, scope = perm.split(".")
url = ":".join((app, scope.replace("view_", "") + "_list"))
item = {
"label": item_label,
"count": None,
"url": url,
"disabled": True,
"icon": icon_class,
}
if request.user.has_perm(perm):
item["count"] = get_count()
item["disabled"] = False
items.append(item)
stats.append((section_label, items, icon_class))
return stats return stats
# Compile changelog table # Compile changelog table

View File

@ -173,7 +173,7 @@ class ObjectImportView(GetReturnURLMixin, BaseObjectView):
obj = model_form.save() obj = model_form.save()
# Enforce object-level permissions # Enforce object-level permissions
if not self.queryset.filter(pk=obj.pk).first(): if not self.queryset.filter(pk=obj.pk).exists():
raise PermissionsViolation() raise PermissionsViolation()
# Iterate through the related object forms (if any), validating and saving each instance. # Iterate through the related object forms (if any), validating and saving each instance.
@ -390,7 +390,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
obj = form.save() obj = form.save()
# Check that the new object conforms with any assigned object-level permissions # Check that the new object conforms with any assigned object-level permissions
if not self.queryset.filter(pk=obj.pk).first(): if not self.queryset.filter(pk=obj.pk).exists():
raise PermissionsViolation() raise PermissionsViolation()
msg = '{} {}'.format( msg = '{} {}'.format(

View File

@ -31,8 +31,7 @@
} }
}, },
"rules": { "rules": {
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-unused-vars-experimental": "error",
"no-unused-vars": "off", "no-unused-vars": "off",
"no-inner-declarations": "off", "no-inner-declarations": "off",
"comma-dangle": ["error", "always-multiline"], "comma-dangle": ["error", "always-multiline"],

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -22,43 +22,38 @@
"validate:formatting:scripts": "prettier -c src/**/*.ts" "validate:formatting:scripts": "prettier -c src/**/*.ts"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^5.9.55", "@mdi/font": "^7.0.96",
"@popperjs/core": "^2.9.2", "@popperjs/core": "^2.11.6",
"bootstrap": "~5.0.2", "bootstrap": "~5.0.2",
"clipboard": "^2.0.8", "clipboard": "^2.0.11",
"color2k": "^1.2.4", "color2k": "^2.0.0",
"dayjs": "^1.10.4", "dayjs": "^1.11.5",
"flatpickr": "4.6.3", "flatpickr": "4.6.13",
"htmx.org": "^1.6.1", "htmx.org": "^1.8.0",
"just-debounce-it": "^1.4.0", "just-debounce-it": "^3.1.1",
"masonry-layout": "^4.2.2", "masonry-layout": "^4.2.2",
"query-string": "^6.14.1", "query-string": "^7.1.1",
"sass": "^1.32.8", "sass": "^1.55.0",
"simplebar": "^5.3.4", "simplebar": "^5.3.9",
"slim-select": "^1.27.0" "slim-select": "^1.27.1"
}, },
"devDependencies": { "devDependencies": {
"@types/bootstrap": "^5.0.12", "@types/bootstrap": "^5.0.17",
"@types/cookie": "^0.4.0", "@types/cookie": "^0.5.1",
"@types/masonry-layout": "^4.2.2", "@types/masonry-layout": "^4.2.5",
"@typescript-eslint/eslint-plugin": "^4.29.3", "@typescript-eslint/eslint-plugin": "^5.39.0",
"@typescript-eslint/parser": "^4.29.3", "@typescript-eslint/parser": "^5.39.0",
"esbuild": "^0.12.24", "esbuild": "^0.13.15",
"esbuild-sass-plugin": "^1.5.2", "esbuild-sass-plugin": "^2.3.3",
"eslint": "^7.32.0", "eslint": "^8.24.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^2.4.0", "eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.24.2", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^3.4.1", "eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.3.2", "prettier": "^2.7.1",
"typescript": "~4.3.5" "typescript": "~4.8.4"
}, },
"resolutions": { "resolutions": {
"eslint-import-resolver-typescript/**/path-parse": "^1.0.7", "@types/bootstrap/**/@popperjs/core": "^2.11.6"
"slim-select/**/trim-newlines": "^3.0.1",
"eslint/glob-parent": "^5.1.2",
"esbuild-sass-plugin/**/glob-parent": "^5.1.2",
"@typescript-eslint/**/glob-parent": "^5.1.2",
"eslint-plugin-import/**/hosted-git-info": "^2.8.9"
} }
} }

View File

@ -37,14 +37,12 @@ function initDocument(): void {
} }
function initWindow(): void { function initWindow(): void {
const documentForms = document.forms;
const documentForms = document.forms for (const documentForm of documentForms) {
for (var documentForm of documentForms) {
if (documentForm.method.toUpperCase() == 'GET') { if (documentForm.method.toUpperCase() == 'GET') {
// @ts-ignore: Our version of typescript seems to be too old for FormDataEvent documentForm.addEventListener('formdata', function (event: FormDataEvent) {
documentForm.addEventListener('formdata', function(event: FormDataEvent) { const formData: FormData = event.formData;
let formData: FormData = event.formData; for (const [name, value] of Array.from(formData.entries())) {
for (let [name, value] of Array.from(formData.entries())) {
if (value === '') formData.delete(name); if (value === '') formData.delete(name);
} }
}); });

View File

@ -32,7 +32,7 @@ $spacing-s: $input-padding-x;
} }
} }
@import './node_modules/slim-select/src/slim-select/slimselect'; @import '../node_modules/slim-select/src/slim-select/slimselect';
.ss-main { .ss-main {
color: $form-select-color; color: $form-select-color;

File diff suppressed because it is too large Load Diff

View File

@ -60,23 +60,17 @@
</div> </div>
{% include 'inc/panels/custom_fields.html' %} {% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %} {% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %} {% plugin_left_page object %}
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'inc/panels/comments.html' %} {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
{% include 'inc/panels/contacts.html' %} {% include 'inc/panels/contacts.html' %}
{% include 'inc/panels/image_attachments.html' %} {% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
</div> </div>
<div class="row">
<div class="col col-md-6">
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
</div>
<div class="col col-md-6">
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
</div>
</div>
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
{% plugin_full_width_page object %} {% plugin_full_width_page object %}

View File

@ -178,7 +178,7 @@
{% if object.primary_ip4.nat_inside %} {% if object.primary_ip4.nat_inside %}
(NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>) (NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
{% elif object.primary_ip4.nat_outside.exists %} {% elif object.primary_ip4.nat_outside.exists %}
(NAT for {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}) (NAT: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %} {% endif %}
{% else %} {% else %}
{{ ''|placeholder }} {{ ''|placeholder }}
@ -193,7 +193,7 @@
{% if object.primary_ip6.nat_inside %} {% if object.primary_ip6.nat_inside %}
(NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>) (NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
{% elif object.primary_ip6.nat_outside.exists %} {% elif object.primary_ip6.nat_outside.exists %}
(NAT for {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}) (NAT: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %} {% endif %}
{% else %} {% else %}
{{ ''|placeholder }} {{ ''|placeholder }}

View File

@ -77,10 +77,10 @@
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Outlet</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Power Outlet</a>
</li> </li>
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Feed</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Power Feed</a>
</li> </li>
</ul> </ul>
</span> </span>

View File

@ -105,16 +105,16 @@
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Interface</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Interface</a>
</li> </li>
<li> <li>
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Front Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
</li> </li>
<li> <li>
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}">Rear Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
</li> </li>
<li> <li>
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.device.site.pk }}&return_url={{ object.get_absolute_url }}">Circuit Termination</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ object.device.site.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Circuit Termination</a>
</li> </li>
</ul> </ul>
</span> </span>

View File

@ -39,6 +39,7 @@
</table> </table>
</div> </div>
</div> </div>
{% plugin_left_page object %}
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
@ -64,6 +65,7 @@
</table> </table>
</div> </div>
</div> </div>
{% plugin_right_page object %}
</div> </div>
</div> </div>
<div class="row"> <div class="row">

View File

@ -36,8 +36,8 @@
<div class="card-body"> <div class="card-body">
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
{% for item in items %} {% for item in items %}
{% if not item.disabled %} {% if item.permission in perms %}
<a href="{% url item.url %}" class="list-group-item list-group-item-action"> <a href="{% url item.viewname %}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between align-items-center"> <div class="d-flex w-100 justify-content-between align-items-center">
{{ item.label }} {{ item.label }}
<h4 class="mb-1">{{ item.count }}</h4> <h4 class="mb-1">{{ item.count }}</h4>

View File

@ -19,6 +19,10 @@
<th scope="row">Type</th> <th scope="row">Type</th>
<td>{{ object.type|linkify }}</td> <td>{{ object.type|linkify }}</td>
</tr> </tr>
<tr>
<th scope="row">Status</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr> <tr>
<th scope="row">Group</th> <th scope="row">Group</th>
<td>{{ object.group|linkify|placeholder }}</td> <td>{{ object.group|linkify|placeholder }}</td>

View File

@ -46,7 +46,7 @@
{% if object.primary_ip4.nat_inside %} {% if object.primary_ip4.nat_inside %}
(NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>) (NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
{% elif object.primary_ip4.nat_outside.exists %} {% elif object.primary_ip4.nat_outside.exists %}
(NAT for {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}) (NAT: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %} {% endif %}
{% else %} {% else %}
{{ ''|placeholder }} {{ ''|placeholder }}
@ -61,7 +61,7 @@
{% if object.primary_ip6.nat_inside %} {% if object.primary_ip6.nat_inside %}
(NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>) (NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
{% elif object.primary_ip6.nat_outside.exists %} {% elif object.primary_ip6.nat_outside.exists %}
(NAT for {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}) (NAT: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %} {% endif %}
{% else %} {% else %}
{{ ''|placeholder }} {{ ''|placeholder }}

View File

@ -27,6 +27,12 @@ class TenantGroupForm(NetBoxModelForm):
) )
slug = SlugField() slug = SlugField()
fieldsets = (
('Tenant Group', (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
class Meta: class Meta:
model = TenantGroup model = TenantGroup
fields = [ fields = [
@ -64,6 +70,12 @@ class ContactGroupForm(NetBoxModelForm):
) )
slug = SlugField() slug = SlugField()
fieldsets = (
('Contact Group', (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
class Meta: class Meta:
model = ContactGroup model = ContactGroup
fields = ('parent', 'name', 'slug', 'description', 'tags') fields = ('parent', 'name', 'slug', 'description', 'tags')
@ -72,6 +84,12 @@ class ContactGroupForm(NetBoxModelForm):
class ContactRoleForm(NetBoxModelForm): class ContactRoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = (
('Contact Role', (
'name', 'slug', 'description', 'tags',
)),
)
class Meta: class Meta:
model = ContactRole model = ContactRole
fields = ('name', 'slug', 'description', 'tags') fields = ('name', 'slug', 'description', 'tags')

View File

@ -1,6 +1,9 @@
import django_tables2 as tables import django_tables2 as tables
from netbox.tables import columns
__all__ = ( __all__ = (
'ContactsColumnMixin',
'TenantColumn', 'TenantColumn',
'TenantGroupColumn', 'TenantGroupColumn',
'TenancyColumnsMixin', 'TenancyColumnsMixin',
@ -55,3 +58,10 @@ class TenantGroupColumn(tables.TemplateColumn):
class TenancyColumnsMixin(tables.Table): class TenancyColumnsMixin(tables.Table):
tenant_group = TenantGroupColumn() tenant_group = TenantGroupColumn()
tenant = TenantColumn() tenant = TenantColumn()
class ContactsColumnMixin(tables.Table):
contacts = columns.ManyToManyColumn(
linkify_item=True,
transform=lambda obj: obj.contact.name
)

View File

@ -1,7 +1,8 @@
import django_tables2 as tables import django_tables2 as tables
from tenancy.models import *
from tenancy.tables import ContactsColumnMixin
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.models import *
__all__ = ( __all__ = (
'TenantGroupTable', 'TenantGroupTable',
@ -30,7 +31,7 @@ class TenantGroupTable(NetBoxTable):
default_columns = ('pk', 'name', 'tenant_count', 'description') default_columns = ('pk', 'name', 'tenant_count', 'description')
class TenantTable(NetBoxTable): class TenantTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
@ -38,9 +39,6 @@ class TenantTable(NetBoxTable):
linkify=True linkify=True
) )
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='tenancy:contact_list' url_name='tenancy:contact_list'
) )

9
netbox/users/utils.py Normal file
View File

@ -0,0 +1,9 @@
from social_core.storage import NO_ASCII_REGEX, NO_SPECIAL_REGEX
def clean_username(value):
"""Clean username removing any unsupported character"""
value = NO_ASCII_REGEX.sub('', value)
value = NO_SPECIAL_REGEX.sub('', value)
value = value.replace(':', '')
return value

View File

@ -28,6 +28,12 @@ __all__ = (
class ClusterTypeForm(NetBoxModelForm): class ClusterTypeForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = (
('Cluster Type', (
'name', 'slug', 'description', 'tags',
)),
)
class Meta: class Meta:
model = ClusterType model = ClusterType
fields = ( fields = (
@ -38,6 +44,12 @@ class ClusterTypeForm(NetBoxModelForm):
class ClusterGroupForm(NetBoxModelForm): class ClusterGroupForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = (
('Cluster Group', (
'name', 'slug', 'description', 'tags',
)),
)
class Meta: class Meta:
model = ClusterGroup model = ClusterGroup
fields = ( fields = (

View File

@ -1,8 +1,8 @@
import django_tables2 as tables import django_tables2 as tables
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from virtualization.models import Cluster, ClusterGroup, ClusterType
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
from virtualization.models import Cluster, ClusterGroup, ClusterType
__all__ = ( __all__ = (
'ClusterTable', 'ClusterTable',
@ -32,7 +32,7 @@ class ClusterTypeTable(NetBoxTable):
default_columns = ('pk', 'name', 'cluster_count', 'description') default_columns = ('pk', 'name', 'cluster_count', 'description')
class ClusterGroupTable(NetBoxTable): class ClusterGroupTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
@ -41,9 +41,6 @@ class ClusterGroupTable(NetBoxTable):
url_params={'group_id': 'pk'}, url_params={'group_id': 'pk'},
verbose_name='Clusters' verbose_name='Clusters'
) )
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='virtualization:clustergroup_list' url_name='virtualization:clustergroup_list'
) )
@ -57,7 +54,7 @@ class ClusterGroupTable(NetBoxTable):
default_columns = ('pk', 'name', 'cluster_count', 'description') default_columns = ('pk', 'name', 'cluster_count', 'description')
class ClusterTable(TenancyColumnsMixin, NetBoxTable): class ClusterTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
@ -67,6 +64,7 @@ class ClusterTable(TenancyColumnsMixin, NetBoxTable):
group = tables.Column( group = tables.Column(
linkify=True linkify=True
) )
status = columns.ChoiceFieldColumn()
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )
@ -81,9 +79,6 @@ class ClusterTable(TenancyColumnsMixin, NetBoxTable):
verbose_name='VMs' verbose_name='VMs'
) )
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='virtualization:cluster_list' url_name='virtualization:cluster_list'
) )

View File

@ -1,10 +1,10 @@
import django_tables2 as tables import django_tables2 as tables
from dcim.tables.devices import BaseInterfaceTable from dcim.tables.devices import BaseInterfaceTable
from netbox.tables import NetBoxTable, columns from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from tenancy.tables import TenancyColumnsMixin
from virtualization.models import VirtualMachine, VMInterface from virtualization.models import VirtualMachine, VMInterface
from netbox.tables import NetBoxTable, columns
__all__ = ( __all__ = (
'VirtualMachineTable', 'VirtualMachineTable',
'VirtualMachineVMInterfaceTable', 'VirtualMachineVMInterfaceTable',
@ -37,7 +37,7 @@ VMINTERFACE_BUTTONS = """
# Virtual machines # Virtual machines
# #
class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable): class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column( name = tables.Column(
order_by=('_name',), order_by=('_name',),
linkify=True linkify=True
@ -67,9 +67,6 @@ class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
order_by=('primary_ip4', 'primary_ip6'), order_by=('primary_ip4', 'primary_ip6'),
verbose_name='IP Address' verbose_name='IP Address'
) )
contacts = columns.ManyToManyColumn(
linkify_item=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='virtualization:virtualmachine_list' url_name='virtualization:virtualmachine_list'
) )

View File

@ -19,6 +19,12 @@ class WirelessLANGroupForm(NetBoxModelForm):
) )
slug = SlugField() slug = SlugField()
fieldsets = (
('Wireless LAN Group', (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
class Meta: class Meta:
model = WirelessLANGroup model = WirelessLANGroup
fields = [ fields = [

View File

@ -19,18 +19,21 @@ graphene-django==2.15.0
gunicorn==20.1.0 gunicorn==20.1.0
Jinja2==3.1.2 Jinja2==3.1.2
Markdown==3.3.7 Markdown==3.3.7
mkdocs-material==8.5.6 mkdocs-material==8.5.7
mkdocstrings[python-legacy]==0.19.0 mkdocstrings[python-legacy]==0.19.0
netaddr==0.8.0 netaddr==0.8.0
Pillow==9.2.0 Pillow==9.2.0
psycopg2-binary==2.9.3 psycopg2-binary==2.9.5
PyYAML==6.0 PyYAML==6.0
sentry-sdk==1.9.10 sentry-sdk==1.10.1
social-auth-app-django==5.0.0 social-auth-app-django==5.0.0
social-auth-core==4.3.0 social-auth-core[openidconnect]==4.3.0
svgwrite==1.4.3 svgwrite==1.4.3
tablib==3.2.1 tablib==3.2.1
tzdata==2022.4 tzdata==2022.5
# Workaround for #7401 # Workaround for #7401
jsonschema==3.2.0 jsonschema==3.2.0
# Temporary fix for #10712
swagger-spec-validator==2.7.6