Browse Source

Merge branch 'develop' into feature

jeremystretch 3 years ago
parent
commit
bd79a27e4d
60 changed files with 785 additions and 618 deletions
  1. 9 5
      .github/ISSUE_TEMPLATE/documentation_change.yaml
  2. 11 1
      docs/installation/6-ldap.md
  3. 16 16
      docs/plugins/development/forms.md
  4. 4 4
      docs/plugins/development/graphql-api.md
  5. 9 9
      docs/plugins/development/tables.md
  6. 10 10
      docs/plugins/development/views.md
  7. 15 0
      docs/release-notes/version-3.3.md
  8. 1 1
      mkdocs.yml
  9. 6 0
      netbox/circuits/forms/models.py
  10. 4 6
      netbox/circuits/tables/circuits.py
  11. 3 5
      netbox/circuits/tables/providers.py
  12. 1 1
      netbox/dcim/forms/connections.py
  13. 43 0
      netbox/dcim/forms/models.py
  14. 20 9
      netbox/dcim/tables/devices.py
  15. 14 6
      netbox/dcim/tables/devicetypes.py
  16. 4 5
      netbox/dcim/tables/power.py
  17. 2 5
      netbox/dcim/tables/racks.py
  18. 7 18
      netbox/dcim/tables/sites.py
  19. 18 0
      netbox/ipam/forms/models.py
  20. 2 0
      netbox/ipam/models/services.py
  21. 8 0
      netbox/netbox/authentication.py
  22. 1 1
      netbox/netbox/settings.py
  23. 1 2
      netbox/project-static/.eslintrc
  24. 1 1
      netbox/project-static/dist/cable_trace.css
  25. 0 0
      netbox/project-static/dist/config.js
  26. 0 0
      netbox/project-static/dist/config.js.map
  27. 0 0
      netbox/project-static/dist/graphiql.css
  28. 0 0
      netbox/project-static/dist/graphiql.js
  29. 0 0
      netbox/project-static/dist/graphiql.js.map
  30. 0 0
      netbox/project-static/dist/lldp.js
  31. 0 0
      netbox/project-static/dist/lldp.js.map
  32. BIN
      netbox/project-static/dist/materialdesignicons-webfont-DWVXV5L5.woff
  33. BIN
      netbox/project-static/dist/materialdesignicons-webfont-ER2MFQKM.woff2
  34. BIN
      netbox/project-static/dist/materialdesignicons-webfont-UHEFFMSX.eot
  35. BIN
      netbox/project-static/dist/materialdesignicons-webfont-WM6M6ZHQ.ttf
  36. 0 0
      netbox/project-static/dist/netbox-dark.css
  37. 0 0
      netbox/project-static/dist/netbox-external.css
  38. 0 0
      netbox/project-static/dist/netbox-light.css
  39. 0 0
      netbox/project-static/dist/netbox-print.css
  40. 0 0
      netbox/project-static/dist/netbox.js
  41. 0 0
      netbox/project-static/dist/netbox.js.map
  42. 1 1
      netbox/project-static/dist/rack_elevation.css
  43. 0 0
      netbox/project-static/dist/status.js
  44. 0 0
      netbox/project-static/dist/status.js.map
  45. 28 33
      netbox/project-static/package.json
  46. 5 7
      netbox/project-static/src/netbox.ts
  47. 1 1
      netbox/project-static/styles/select.scss
  48. 461 433
      netbox/project-static/yarn.lock
  49. 3 9
      netbox/templates/circuits/circuit.html
  50. 2 2
      netbox/templates/dcim/powerport.html
  51. 4 4
      netbox/templates/dcim/rearport.html
  52. 18 0
      netbox/tenancy/forms/models.py
  53. 10 0
      netbox/tenancy/tables/columns.py
  54. 3 5
      netbox/tenancy/tables/tenants.py
  55. 9 0
      netbox/users/utils.py
  56. 12 0
      netbox/virtualization/forms/models.py
  57. 4 10
      netbox/virtualization/tables/clusters.py
  58. 4 7
      netbox/virtualization/tables/virtualmachines.py
  59. 6 0
      netbox/wireless/forms/models.py
  60. 4 1
      requirements.txt

+ 9 - 5
.github/ISSUE_TEMPLATE/documentation_change.yaml

@@ -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
-        - Configuration parameters
-        - Functionality/features
-        - REST API
-        - Administration/development
+        - Features
+        - Installation/upgrade
+        - Getting started
+        - Configuration
+        - Customization
+        - Integrations/API
+        - Plugins
+        - Administration
+        - Development
         - Other
         - Other
     validations:
     validations:
       required: true
       required: true

+ 11 - 1
docs/installation/6-ldap.md

@@ -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.

+ 16 - 16
docs/plugins/development/forms.md

@@ -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

+ 4 - 4
docs/plugins/development/graphql-api.md

@@ -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

+ 9 - 9
docs/plugins/development/tables.md

@@ -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__

+ 10 - 10
docs/plugins/development/views.md

@@ -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
 
 

+ 15 - 0
docs/release-notes/version-3.3.md

@@ -2,6 +2,21 @@
 
 
 ## v3.3.6 (FUTURE)
 ## v3.3.6 (FUTURE)
 
 
+### Enhancements
+
+* [#9722](https://github.com/netbox-community/netbox/issues/9722) - Add LDAP configuration parameters to specify certificates
+* [#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
+* [#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
+* [#10712](https://github.com/netbox-community/netbox/issues/10712) - Fix ModuleNotFoundError exception when generating API schema under Python 3.9+
+
 ---
 ---
 
 
 ## v3.3.5 (2022-10-05)
 ## v3.3.5 (2022-10-05)

+ 1 - 1
mkdocs.yml

@@ -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

+ 6 - 0
netbox/circuits/forms/models.py

@@ -64,6 +64,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 = [

+ 4 - 6
netbox/circuits/tables/circuits.py

@@ -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'
     )
     )

+ 3 - 5
netbox/circuits/tables/providers.py

@@ -1,7 +1,8 @@
 import django_tables2 as tables
 import django_tables2 as tables
+from circuits.models import *
 from django_tables2.utils import Accessor
 from django_tables2.utils import Accessor
+from tenancy.tables import ContactsColumnMixin
 
 
-from circuits.models import *
 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'
     )
     )

+ 1 - 1
netbox/dcim/forms/connections.py

@@ -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',
                         }
                         }
                     )
                     )
 
 

+ 43 - 0
netbox/dcim/forms/models.py

@@ -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 = [
@@ -341,6 +359,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 = [
@@ -413,6 +437,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 = [
@@ -429,6 +459,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 = [
@@ -1584,6 +1621,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 = [

+ 20 - 9
netbox/dcim/tables/devices.py

@@ -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,
-    InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
+    ConsolePort,
+    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'
     )
     )

+ 14 - 6
netbox/dcim/tables/devicetypes.py

@@ -1,10 +1,21 @@
 import django_tables2 as tables
 import django_tables2 as tables
 
 
 from dcim.models import (
 from dcim.models import (
-    ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, DeviceType, FrontPortTemplate, InterfaceTemplate,
-    InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
+    ConsolePortTemplate,
+    ConsoleServerPortTemplate,
+    DeviceBayTemplate,
+    DeviceType,
+    FrontPortTemplate,
+    InterfaceTemplate,
+    InventoryItemTemplate,
+    Manufacturer,
+    ModuleBayTemplate,
+    PowerOutletTemplate,
+    PowerPortTemplate,
+    RearPortTemplate,
 )
 )
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
+from tenancy.tables import ContactsColumnMixin
 from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT
 from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT
 
 
 __all__ = (
 __all__ = (
@@ -27,7 +38,7 @@ __all__ = (
 # Manufacturers
 # Manufacturers
 #
 #
 
 
-class ManufacturerTable(NetBoxTable):
+class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -43,9 +54,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'
     )
     )

+ 4 - 5
netbox/dcim/tables/power.py

@@ -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'
     )
     )

+ 2 - 5
netbox/dcim/tables/racks.py

@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
 
 
 from dcim.models import Rack, RackReservation, RackRole
 from dcim.models import Rack, RackReservation, RackRole
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenancyColumnsMixin
+from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
 from .template_code import DEVICE_WEIGHT
 from .template_code import DEVICE_WEIGHT
 
 
 __all__ = (
 __all__ = (
@@ -38,7 +38,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
@@ -69,9 +69,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'
     )
     )

+ 7 - 18
netbox/dcim/tables/sites.py

@@ -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'
     )
     )

+ 18 - 0
netbox/ipam/forms/models.py

@@ -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 = [
@@ -784,6 +796,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')

+ 2 - 0
netbox/ipam/models/services.py

@@ -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
 
 

+ 8 - 0
netbox/netbox/authentication.py

@@ -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
 
 
 
 

+ 1 - 1
netbox/netbox/settings.py

@@ -493,7 +493,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

+ 1 - 2
netbox/project-static/.eslintrc

@@ -31,8 +31,7 @@
     }
     }
   },
   },
   "rules": {
   "rules": {
-    "@typescript-eslint/no-unused-vars": "off",
-    "@typescript-eslint/no-unused-vars-experimental": "error",
+    "@typescript-eslint/no-unused-vars": "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"],

+ 1 - 1
netbox/project-static/dist/cable_trace.css

@@ -1 +1 @@
-:root{--nbx-trace-color: #000;--nbx-trace-node-bg: #e9ecef;--nbx-trace-termination-bg: #f8f9fa;--nbx-trace-cable-shadow: #343a40;--nbx-trace-attachment: #ced4da}:root[data-netbox-color-mode=dark]{--nbx-trace-color: #fff;--nbx-trace-node-bg: #212529;--nbx-trace-termination-bg: #343a40;--nbx-trace-cable-shadow: #e9ecef;--nbx-trace-attachment: #6c757d}*{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:.875rem}text{text-anchor:middle;dominant-baseline:middle}text:not([fill]){fill:var(--nbx-trace-color)}text.bold{font-weight:700}svg rect{fill:var(--nbx-trace-node-bg);stroke:#606060;stroke-width:1}svg rect .termination{fill:var(--nbx-trace-termination-bg)}svg .connector text{text-anchor:start}svg line{stroke-width:5px}svg polyline{fill:none;stroke-width:5px}svg .cable-shadow{stroke:var(--nbx-trace-cable-shadow);stroke-width:7px}svg line.wireless-link{stroke:var(--nbx-trace-attachment);stroke-dasharray:4px 12px;stroke-linecap:round}svg line.attachment{stroke:var(--nbx-trace-attachment);stroke-dasharray:5px}
+:root{--nbx-trace-color: #000;--nbx-trace-node-bg: #e9ecef;--nbx-trace-termination-bg: #f8f9fa;--nbx-trace-cable-shadow: #343a40;--nbx-trace-attachment: #ced4da}:root[data-netbox-color-mode=dark]{--nbx-trace-color: #fff;--nbx-trace-node-bg: #212529;--nbx-trace-termination-bg: #343a40;--nbx-trace-cable-shadow: #e9ecef;--nbx-trace-attachment: #6c757d}*{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Liberation Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-size:.875rem}text{text-anchor:middle;dominant-baseline:middle}text:not([fill]){fill:var(--nbx-trace-color)}text.bold{font-weight:700}svg rect{fill:var(--nbx-trace-node-bg);stroke:#606060;stroke-width:1}svg rect .termination{fill:var(--nbx-trace-termination-bg)}svg .connector text{text-anchor:start}svg line{stroke-width:5px}svg polyline{fill:none;stroke-width:5px}svg .cable-shadow{stroke:var(--nbx-trace-cable-shadow);stroke-width:7px}svg line.wireless-link{stroke:var(--nbx-trace-attachment);stroke-dasharray:4px 12px;stroke-linecap:round}svg line.attachment{stroke:var(--nbx-trace-attachment);stroke-dasharray:5px}

File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/config.js


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/config.js.map


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/graphiql.css


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/graphiql.js


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/graphiql.js.map


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/lldp.js


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/lldp.js.map


BIN
netbox/project-static/dist/materialdesignicons-webfont-DWVXV5L5.woff


BIN
netbox/project-static/dist/materialdesignicons-webfont-ER2MFQKM.woff2


BIN
netbox/project-static/dist/materialdesignicons-webfont-UHEFFMSX.eot


BIN
netbox/project-static/dist/materialdesignicons-webfont-WM6M6ZHQ.ttf


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox-dark.css


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox-external.css


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox-light.css


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox-print.css


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 1 - 1
netbox/project-static/dist/rack_elevation.css

@@ -1 +1 @@
-svg{--nbx-rack-bg: #e9ecef;--nbx-rack-border: #000;--nbx-rack-slot-bg: #e9ecef;--nbx-rack-slot-border: #adb5bd;--nbx-rack-slot-hover-bg: #ced4da;--nbx-rack-link-color: #0d6efd;--nbx-rack-unit-color: #6c757d}svg[data-netbox-color-mode=dark]{--nbx-rack-bg: #343a40;--nbx-rack-border: #6c757d;--nbx-rack-slot-bg: #343a40;--nbx-rack-slot-border: #495057;--nbx-rack-slot-hover-bg: #212529;--nbx-rack-link-color: #9ec5fe;--nbx-rack-unit-color: #6c757d}*{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:.875rem}rect{box-sizing:border-box}text{text-anchor:middle;dominant-baseline:middle}svg .unit{margin:0;padding:5px 0;fill:var(--nbx-rack-unit-color)}svg .hidden{visibility:hidden}svg rect.shaded,svg image.shaded{opacity:25%}svg text.shaded{opacity:50%}svg .rack{fill:none;stroke-width:2px;stroke:var(--nbx-rack-border);background-color:var(--nbx-rack-bg)}svg .slot{fill:var(--nbx-rack-slot-bg);stroke:var(--nbx-rack-slot-border)}svg .slot:hover{fill:var(--nbx-rack-slot-hover-bg)}svg .slot+.add-device{fill:var(--nbx-rack-link-color);opacity:0;pointer-events:none}svg .slot:hover+.add-device{opacity:1}svg .slot.occupied[class],svg .slot.occupied:hover[class]{fill:url(#occupied)}svg .slot.blocked[class],svg .slot.blocked:hover[class]{fill:url(#blocked)}svg .slot.blocked:hover+.add-device{opacity:0}svg .reservation[class]{fill:url(#reserved)}
+svg{--nbx-rack-bg: #e9ecef;--nbx-rack-border: #000;--nbx-rack-slot-bg: #e9ecef;--nbx-rack-slot-border: #adb5bd;--nbx-rack-slot-hover-bg: #ced4da;--nbx-rack-link-color: #0d6efd;--nbx-rack-unit-color: #6c757d}svg[data-netbox-color-mode=dark]{--nbx-rack-bg: #343a40;--nbx-rack-border: #6c757d;--nbx-rack-slot-bg: #343a40;--nbx-rack-slot-border: #495057;--nbx-rack-slot-hover-bg: #212529;--nbx-rack-link-color: #9ec5fe;--nbx-rack-unit-color: #6c757d}*{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Liberation Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-size:.875rem}rect{box-sizing:border-box}text{text-anchor:middle;dominant-baseline:middle}svg .unit{margin:0;padding:5px 0;fill:var(--nbx-rack-unit-color)}svg .hidden{visibility:hidden}svg rect.shaded,svg image.shaded{opacity:25%}svg text.shaded{opacity:50%}svg .rack{fill:none;stroke-width:2px;stroke:var(--nbx-rack-border);background-color:var(--nbx-rack-bg)}svg .slot{fill:var(--nbx-rack-slot-bg);stroke:var(--nbx-rack-slot-border)}svg .slot:hover{fill:var(--nbx-rack-slot-hover-bg)}svg .slot+.add-device{fill:var(--nbx-rack-link-color);opacity:0;pointer-events:none}svg .slot:hover+.add-device{opacity:1}svg .slot.occupied[class],svg .slot.occupied:hover[class]{fill:url(#occupied)}svg .slot.blocked[class],svg .slot.blocked:hover[class]{fill:url(#blocked)}svg .slot.blocked:hover+.add-device{opacity:0}svg .reservation[class]{fill:url(#reserved)}

File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/status.js


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/status.js.map


+ 28 - 33
netbox/project-static/package.json

@@ -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",
-    "@popperjs/core": "^2.9.2",
+    "@mdi/font": "^7.0.96",
+    "@popperjs/core": "^2.11.6",
     "bootstrap": "~5.0.2",
     "bootstrap": "~5.0.2",
-    "clipboard": "^2.0.8",
-    "color2k": "^1.2.4",
-    "dayjs": "^1.10.4",
-    "flatpickr": "4.6.3",
-    "htmx.org": "^1.6.1",
-    "just-debounce-it": "^1.4.0",
+    "clipboard": "^2.0.11",
+    "color2k": "^2.0.0",
+    "dayjs": "^1.11.5",
+    "flatpickr": "4.6.13",
+    "htmx.org": "^1.8.0",
+    "just-debounce-it": "^3.1.1",
     "masonry-layout": "^4.2.2",
     "masonry-layout": "^4.2.2",
-    "query-string": "^6.14.1",
-    "sass": "^1.32.8",
-    "simplebar": "^5.3.4",
-    "slim-select": "^1.27.0"
+    "query-string": "^7.1.1",
+    "sass": "^1.55.0",
+    "simplebar": "^5.3.9",
+    "slim-select": "^1.27.1"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@types/bootstrap": "^5.0.12",
-    "@types/cookie": "^0.4.0",
-    "@types/masonry-layout": "^4.2.2",
-    "@typescript-eslint/eslint-plugin": "^4.29.3",
-    "@typescript-eslint/parser": "^4.29.3",
-    "esbuild": "^0.12.24",
-    "esbuild-sass-plugin": "^1.5.2",
-    "eslint": "^7.32.0",
-    "eslint-config-prettier": "^8.3.0",
-    "eslint-import-resolver-typescript": "^2.4.0",
-    "eslint-plugin-import": "^2.24.2",
-    "eslint-plugin-prettier": "^3.4.1",
-    "prettier": "^2.3.2",
-    "typescript": "~4.3.5"
+    "@types/bootstrap": "^5.0.17",
+    "@types/cookie": "^0.5.1",
+    "@types/masonry-layout": "^4.2.5",
+    "@typescript-eslint/eslint-plugin": "^5.39.0",
+    "@typescript-eslint/parser": "^5.39.0",
+    "esbuild": "^0.13.15",
+    "esbuild-sass-plugin": "^2.3.3",
+    "eslint": "^8.24.0",
+    "eslint-config-prettier": "^8.5.0",
+    "eslint-import-resolver-typescript": "^3.5.1",
+    "eslint-plugin-import": "^2.26.0",
+    "eslint-plugin-prettier": "^4.2.1",
+    "prettier": "^2.7.1",
+    "typescript": "~4.8.4"
   },
   },
   "resolutions": {
   "resolutions": {
-    "eslint-import-resolver-typescript/**/path-parse": "^1.0.7",
-    "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"
+    "@types/bootstrap/**/@popperjs/core": "^2.11.6"
   }
   }
-}
+}

+ 5 - 7
netbox/project-static/src/netbox.ts

@@ -37,14 +37,12 @@ function initDocument(): void {
 }
 }
 
 
 function initWindow(): void {
 function initWindow(): void {
-
-  const documentForms = document.forms
-  for (var documentForm of documentForms) {
+  const documentForms = document.forms;
+  for (const 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) {
-      let formData: FormData = event.formData;
-      for (let [name, value] of Array.from(formData.entries())) {
+      documentForm.addEventListener('formdata', function (event: FormDataEvent) {
+        const formData: FormData = event.formData;
+        for (const [name, value] of Array.from(formData.entries())) {
           if (value === '') formData.delete(name);
           if (value === '') formData.delete(name);
         }
         }
       });
       });

+ 1 - 1
netbox/project-static/styles/select.scss

@@ -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
+ 461 - 433
netbox/project-static/yarn.lock


+ 3 - 9
netbox/templates/circuits/circuit.html

@@ -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 %}

+ 2 - 2
netbox/templates/dcim/powerport.html

@@ -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>

+ 4 - 4
netbox/templates/dcim/rearport.html

@@ -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>

+ 18 - 0
netbox/tenancy/forms/models.py

@@ -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')

+ 10 - 0
netbox/tenancy/tables/columns.py

@@ -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
+    )

+ 3 - 5
netbox/tenancy/tables/tenants.py

@@ -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 - 0
netbox/users/utils.py

@@ -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

+ 12 - 0
netbox/virtualization/forms/models.py

@@ -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 = (

+ 4 - 10
netbox/virtualization/tables/clusters.py

@@ -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
     )
     )
@@ -81,9 +78,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'
     )
     )

+ 4 - 7
netbox/virtualization/tables/virtualmachines.py

@@ -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 TenancyColumnsMixin
+from tenancy.tables import ContactsColumnMixin, 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'
     )
     )

+ 6 - 0
netbox/wireless/forms/models.py

@@ -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 = [

+ 4 - 1
requirements.txt

@@ -27,10 +27,13 @@ psycopg2-binary==2.9.3
 PyYAML==6.0
 PyYAML==6.0
 sentry-sdk==1.9.10
 sentry-sdk==1.9.10
 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.4
 
 
 # Workaround for #7401
 # Workaround for #7401
 jsonschema==3.2.0
 jsonschema==3.2.0
+
+# Temporary fix for #10712
+swagger-spec-validator==2.7.6

Some files were not shown because too many files changed in this diff