diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 907ad6cf7c4..56c14e96659 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.3.5 + placeholder: v3.3.6 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/documentation_change.yaml index 0f87115fcb1..cb097d579d0 100644 --- a/.github/ISSUE_TEMPLATE/documentation_change.yaml +++ b/.github/ISSUE_TEMPLATE/documentation_change.yaml @@ -19,11 +19,15 @@ body: label: Area description: To what section of the documentation does this change primarily pertain? options: - - Installation instructions - - Configuration parameters - - Functionality/features - - REST API - - Administration/development + - Features + - Installation/upgrade + - Getting started + - Configuration + - Customization + - Integrations/API + - Plugins + - Administration + - Development - Other validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 3cd9bc4ee7e..bef1ce58714 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.3.5 + placeholder: v3.3.6 validations: required: true - type: dropdown diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 33134cb453c..0bbbe90c708 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,14 @@ ### Fixes: #1234 diff --git a/docs/installation/6-ldap.md b/docs/installation/6-ldap.md index 163ace70dfe..ffba6889bfb 100644 --- a/docs/installation/6-ldap.md +++ b/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 !!! 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 import ldap @@ -67,6 +67,16 @@ AUTH_LDAP_BIND_PASSWORD = "demo" # 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_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. diff --git a/docs/plugins/development/forms.md b/docs/plugins/development/forms.md index e1eefa7a5e1..dee0d379654 100644 --- a/docs/plugins/development/forms.md +++ b/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. ::: utilities.forms.ColorField - selection: + options: members: false ::: utilities.forms.CommentField - selection: + options: members: false ::: utilities.forms.JSONField - selection: + options: members: false ::: utilities.forms.MACAddressField - selection: + options: members: false ::: utilities.forms.SlugField - selection: + options: members: false ## Choice Fields ::: utilities.forms.ChoiceField - selection: + options: members: false ::: utilities.forms.MultipleChoiceField - selection: + options: members: false ## Dynamic Object Fields ::: utilities.forms.DynamicModelChoiceField - selection: + options: members: false ::: utilities.forms.DynamicModelMultipleChoiceField - selection: + options: members: false ## Content Type Fields ::: utilities.forms.ContentTypeChoiceField - selection: + options: members: false ::: utilities.forms.ContentTypeMultipleChoiceField - selection: + options: members: false ## CSV Import Fields ::: utilities.forms.CSVChoiceField - selection: + options: members: false ::: utilities.forms.CSVMultipleChoiceField - selection: + options: members: false ::: utilities.forms.CSVModelChoiceField - selection: + options: members: false ::: utilities.forms.CSVContentTypeField - selection: + options: members: false ::: utilities.forms.CSVMultipleContentTypeField - selection: + options: members: false diff --git a/docs/plugins/development/graphql-api.md b/docs/plugins/development/graphql-api.md index 0dadf021fa3..f802e802518 100644 --- a/docs/plugins/development/graphql-api.md +++ b/docs/plugins/development/graphql-api.md @@ -32,11 +32,11 @@ schema = MyQuery NetBox provides two object type classes for use by plugins. ::: netbox.graphql.types.BaseObjectType - selection: + options: members: false ::: netbox.graphql.types.NetBoxObjectType - selection: + options: members: false ## 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.graphql.fields.ObjectField - selection: + options: members: false ::: netbox.graphql.fields.ObjectListField - selection: + options: members: false diff --git a/docs/plugins/development/tables.md b/docs/plugins/development/tables.md index 6dccb4ee209..f846139f0b8 100644 --- a/docs/plugins/development/tables.md +++ b/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`. ::: netbox.tables.BooleanColumn - selection: + options: members: false ::: netbox.tables.ChoiceFieldColumn - selection: + options: members: false ::: netbox.tables.ColorColumn - selection: + options: members: false ::: netbox.tables.ColoredLabelColumn - selection: + options: members: false ::: netbox.tables.ContentTypeColumn - selection: + options: members: false ::: netbox.tables.ContentTypesColumn - selection: + options: members: false ::: netbox.tables.MarkdownColumn - selection: + options: members: false ::: netbox.tables.TagColumn - selection: + options: members: false ::: netbox.tables.TemplateColumn - selection: + options: members: - __init__ diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md index cabcd704526..2afe332c550 100644 --- a/docs/plugins/development/views.md +++ b/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.ObjectView - selection: + options: members: - get_object - get_template_name ::: netbox.views.generic.ObjectEditView - selection: + options: members: - get_object - alter_object ::: netbox.views.generic.ObjectDeleteView - selection: + options: members: - get_object ::: netbox.views.generic.ObjectChildrenView - selection: + options: members: - get_children - 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.ObjectListView - selection: + options: members: - get_table - export_table - export_template ::: netbox.views.generic.BulkImportView - selection: + options: members: false ::: netbox.views.generic.BulkEditView - selection: + options: members: false ::: netbox.views.generic.BulkDeleteView - selection: + options: members: - 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. ::: netbox.views.generic.ObjectChangeLogView - selection: + options: members: - get_form ::: netbox.views.generic.ObjectJournalView - selection: + options: members: - get_form diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index daf5420224b..ffb831e9d41 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -1,5 +1,34 @@ # 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) ### Enhancements diff --git a/mkdocs.yml b/mkdocs.yml index a10fd6e670d..58617cbadfd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,7 +30,7 @@ plugins: - os.chdir('netbox/') - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings") - django.setup() - rendering: + options: heading_level: 3 members_order: source show_root_heading: true diff --git a/netbox/circuits/forms/models.py b/netbox/circuits/forms/models.py index 7bd7abbbff4..aebdce10e2a 100644 --- a/netbox/circuits/forms/models.py +++ b/netbox/circuits/forms/models.py @@ -76,6 +76,12 @@ class ProviderNetworkForm(NetBoxModelForm): class CircuitTypeForm(NetBoxModelForm): slug = SlugField() + fieldsets = ( + ('Circuit Type', ( + 'name', 'slug', 'description', 'tags', + )), + ) + class Meta: model = CircuitType fields = [ diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index f9ab7e190a1..477f9c1abdf 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -1,8 +1,9 @@ import django_tables2 as tables - from circuits.models import * +from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin + from netbox.tables import NetBoxTable, columns -from tenancy.tables import TenancyColumnsMixin + from .columns import CommitRateColumn __all__ = ( @@ -39,7 +40,7 @@ class CircuitTypeTable(NetBoxTable): default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug') -class CircuitTable(TenancyColumnsMixin, NetBoxTable): +class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): cid = tables.Column( linkify=True, verbose_name='Circuit ID' @@ -58,9 +59,6 @@ class CircuitTable(TenancyColumnsMixin, NetBoxTable): ) commit_rate = CommitRateColumn() comments = columns.MarkdownColumn() - contacts = columns.ManyToManyColumn( - linkify_item=True - ) tags = columns.TagColumn( url_name='circuits:circuit_list' ) diff --git a/netbox/circuits/tables/providers.py b/netbox/circuits/tables/providers.py index 0ec6d439d00..e2a1d93f6c4 100644 --- a/netbox/circuits/tables/providers.py +++ b/netbox/circuits/tables/providers.py @@ -1,7 +1,8 @@ import django_tables2 as tables -from django_tables2.utils import Accessor - from circuits.models import * +from django_tables2.utils import Accessor +from tenancy.tables import ContactsColumnMixin + from netbox.tables import NetBoxTable, columns __all__ = ( @@ -10,7 +11,7 @@ __all__ = ( ) -class ProviderTable(NetBoxTable): +class ProviderTable(ContactsColumnMixin, NetBoxTable): name = tables.Column( linkify=True ) @@ -31,9 +32,6 @@ class ProviderTable(NetBoxTable): verbose_name='Circuits' ) comments = columns.MarkdownColumn() - contacts = columns.ManyToManyColumn( - linkify_item=True - ) tags = columns.TagColumn( url_name='circuits:provider_list' ) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 0a4439173fa..917f579239c 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -800,6 +800,12 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter to_field_name='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( queryset=DeviceType.objects.all(), label='Device type (ID)', @@ -1357,7 +1363,7 @@ class InterfaceFilterSet( try: devices = Device.objects.filter(pk__in=id_list) 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) except Device.DoesNotExist: return queryset.none() diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index cc5cf362faa..5e3948baa10 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -108,7 +108,7 @@ def get_cable_form(a_type, b_type): label='Power Feed', disabled_indicator='_occupied', query_params={ - 'powerpanel_id': f'$termination_{cable_end}_powerpanel', + 'power_panel_id': f'$termination_{cable_end}_powerpanel', } ) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 1f1c869a517..8f1626361eb 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -78,6 +78,12 @@ class RegionForm(NetBoxModelForm): ) slug = SlugField() + fieldsets = ( + ('Region', ( + 'parent', 'name', 'slug', 'description', 'tags', + )), + ) + class Meta: model = Region fields = ( @@ -92,6 +98,12 @@ class SiteGroupForm(NetBoxModelForm): ) slug = SlugField() + fieldsets = ( + ('Site Group', ( + 'parent', 'name', 'slug', 'description', 'tags', + )), + ) + class Meta: model = SiteGroup fields = ( @@ -213,6 +225,12 @@ class LocationForm(TenancyForm, NetBoxModelForm): class RackRoleForm(NetBoxModelForm): slug = SlugField() + fieldsets = ( + ('Rack Role', ( + 'name', 'slug', 'color', 'description', 'tags', + )), + ) + class Meta: model = RackRole fields = [ @@ -340,6 +358,12 @@ class RackReservationForm(TenancyForm, NetBoxModelForm): class ManufacturerForm(NetBoxModelForm): slug = SlugField() + fieldsets = ( + ('Manufacturer', ( + 'name', 'slug', 'description', 'tags', + )), + ) + class Meta: model = Manufacturer fields = [ @@ -406,6 +430,12 @@ class ModuleTypeForm(NetBoxModelForm): class DeviceRoleForm(NetBoxModelForm): slug = SlugField() + fieldsets = ( + ('Device Role', ( + 'name', 'slug', 'color', 'vm_role', 'description', 'tags', + )), + ) + class Meta: model = DeviceRole fields = [ @@ -422,6 +452,13 @@ class PlatformForm(NetBoxModelForm): max_length=64 ) + fieldsets = ( + ('Platform', ( + 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags', + + )), + ) + class Meta: model = Platform fields = [ @@ -1577,6 +1614,12 @@ class InventoryItemForm(DeviceComponentForm): class InventoryItemRoleForm(NetBoxModelForm): slug = SlugField() + fieldsets = ( + ('Inventory Item Role', ( + 'name', 'slug', 'color', 'description', 'tags', + )), + ) + class Meta: model = InventoryItemRole fields = [ diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 142c7ef67fe..3b129c963d3 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -1,12 +1,26 @@ import django_tables2 as tables -from django_tables2.utils import Accessor - 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 tenancy.tables import TenancyColumnsMixin + from .template_code import * __all__ = ( @@ -137,7 +151,7 @@ class PlatformTable(NetBoxTable): # Devices # -class DeviceTable(TenancyColumnsMixin, NetBoxTable): +class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): name = tables.TemplateColumn( order_by=('_name',), template_code=DEVICE_LINK @@ -201,9 +215,6 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable): verbose_name='VC Priority' ) comments = columns.MarkdownColumn() - contacts = columns.ManyToManyColumn( - linkify_item=True - ) tags = columns.TagColumn( url_name='dcim:device_list' ) diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index ec71245f78f..566c56a9043 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -1,10 +1,22 @@ import django_tables2 as tables - 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 tenancy.tables import ContactsColumnMixin + from netbox.tables import NetBoxTable, columns + from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS __all__ = ( @@ -27,7 +39,7 @@ __all__ = ( # Manufacturers # -class ManufacturerTable(NetBoxTable): +class ManufacturerTable(ContactsColumnMixin, NetBoxTable): name = tables.Column( linkify=True ) @@ -43,9 +55,6 @@ class ManufacturerTable(NetBoxTable): verbose_name='Platforms' ) slug = tables.Column() - contacts = columns.ManyToManyColumn( - linkify_item=True - ) tags = columns.TagColumn( url_name='dcim:manufacturer_list' ) diff --git a/netbox/dcim/tables/power.py b/netbox/dcim/tables/power.py index 6696d516aac..04012ea4a21 100644 --- a/netbox/dcim/tables/power.py +++ b/netbox/dcim/tables/power.py @@ -1,7 +1,9 @@ import django_tables2 as tables - from dcim.models import PowerFeed, PowerPanel +from tenancy.tables import ContactsColumnMixin + from netbox.tables import NetBoxTable, columns + from .devices import CableTerminationTable __all__ = ( @@ -14,7 +16,7 @@ __all__ = ( # Power panels # -class PowerPanelTable(NetBoxTable): +class PowerPanelTable(ContactsColumnMixin, NetBoxTable): name = tables.Column( linkify=True ) @@ -29,9 +31,6 @@ class PowerPanelTable(NetBoxTable): url_params={'power_panel_id': 'pk'}, verbose_name='Feeds' ) - contacts = columns.ManyToManyColumn( - linkify_item=True - ) tags = columns.TagColumn( url_name='dcim:powerpanel_list' ) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index 39553bac066..ab6ee6eb392 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -1,9 +1,9 @@ import django_tables2 as tables -from django_tables2.utils import Accessor - 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 tenancy.tables import TenancyColumnsMixin __all__ = ( 'RackTable', @@ -37,7 +37,7 @@ class RackRoleTable(NetBoxTable): # Racks # -class RackTable(TenancyColumnsMixin, NetBoxTable): +class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): name = tables.Column( order_by=('_name',), linkify=True @@ -68,9 +68,6 @@ class RackTable(TenancyColumnsMixin, NetBoxTable): orderable=False, verbose_name='Power' ) - contacts = columns.ManyToManyColumn( - linkify_item=True - ) tags = columns.TagColumn( url_name='dcim:rack_list' ) diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 5dc2aa61127..f013025f706 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -1,8 +1,9 @@ import django_tables2 as tables - from dcim.models import Location, Region, Site, SiteGroup +from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin + from netbox.tables import NetBoxTable, columns -from tenancy.tables import TenancyColumnsMixin + from .template_code import LOCATION_BUTTONS __all__ = ( @@ -17,7 +18,7 @@ __all__ = ( # Regions # -class RegionTable(NetBoxTable): +class RegionTable(ContactsColumnMixin, NetBoxTable): name = columns.MPTTColumn( linkify=True ) @@ -26,9 +27,6 @@ class RegionTable(NetBoxTable): url_params={'region_id': 'pk'}, verbose_name='Sites' ) - contacts = columns.ManyToManyColumn( - linkify_item=True - ) tags = columns.TagColumn( url_name='dcim:region_list' ) @@ -46,7 +44,7 @@ class RegionTable(NetBoxTable): # Site groups # -class SiteGroupTable(NetBoxTable): +class SiteGroupTable(ContactsColumnMixin, NetBoxTable): name = columns.MPTTColumn( linkify=True ) @@ -55,9 +53,6 @@ class SiteGroupTable(NetBoxTable): url_params={'group_id': 'pk'}, verbose_name='Sites' ) - contacts = columns.ManyToManyColumn( - linkify_item=True - ) tags = columns.TagColumn( url_name='dcim:sitegroup_list' ) @@ -75,7 +70,7 @@ class SiteGroupTable(NetBoxTable): # Sites # -class SiteTable(TenancyColumnsMixin, NetBoxTable): +class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): name = tables.Column( linkify=True ) @@ -97,9 +92,6 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable): verbose_name='ASN Count' ) comments = columns.MarkdownColumn() - contacts = columns.ManyToManyColumn( - linkify_item=True - ) tags = columns.TagColumn( url_name='dcim:site_list' ) @@ -118,7 +110,7 @@ class SiteTable(TenancyColumnsMixin, NetBoxTable): # Locations # -class LocationTable(TenancyColumnsMixin, NetBoxTable): +class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): name = columns.MPTTColumn( linkify=True ) @@ -136,9 +128,6 @@ class LocationTable(TenancyColumnsMixin, NetBoxTable): url_params={'location_id': 'pk'}, verbose_name='Devices' ) - contacts = columns.ManyToManyColumn( - linkify_item=True - ) tags = columns.TagColumn( url_name='dcim:location_list' ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index feef4e90c7e..05bb647964f 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1643,6 +1643,8 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): device_types = DeviceType.objects.all()[:2] params = {'device_type_id': [device_types[0].pk, device_types[1].pk]} 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): device_roles = DeviceRole.objects.all()[:2] diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index 7248125856d..1986b159086 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -88,6 +88,12 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm): class RIRForm(NetBoxModelForm): slug = SlugField() + fieldsets = ( + ('RIR', ( + 'name', 'slug', 'is_private', 'description', 'tags', + )), + ) + class Meta: model = RIR fields = [ @@ -164,6 +170,12 @@ class ASNForm(TenancyForm, NetBoxModelForm): class RoleForm(NetBoxModelForm): slug = SlugField() + fieldsets = ( + ('Role', ( + 'name', 'slug', 'weight', 'description', 'tags', + )), + ) + class Meta: model = Role fields = [ @@ -540,6 +552,7 @@ class FHRPGroupForm(NetBoxModelForm): def save(self, *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 if self.cleaned_data.get('ip_address'): @@ -553,7 +566,7 @@ class FHRPGroupForm(NetBoxModelForm): ipaddress.save() # 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() 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." ) + fieldsets = ( + ('Service Template', ( + 'name', 'protocol', 'ports', 'description', 'tags', + )), + ) + class Meta: model = ServiceTemplate fields = ('name', 'protocol', 'ports', 'description', 'tags') diff --git a/netbox/ipam/models/services.py b/netbox/ipam/models/services.py index 70ad381979d..b566db3754e 100644 --- a/netbox/ipam/models/services.py +++ b/netbox/ipam/models/services.py @@ -92,6 +92,8 @@ class Service(ServiceBase, NetBoxModel): verbose_name='IP addresses' ) + clone_fields = ['protocol', 'ports', 'description', 'device', 'virtual_machine', 'ipaddresses', ] + class Meta: ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index a820385ed83..44f40b8a1ce 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -375,7 +375,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable): ) assigned = columns.BooleanColumn( accessor='assigned_object_id', - linkify=True, + linkify=lambda record: record.assigned_object.get_absolute_url(), verbose_name='Assigned' ) tags = columns.TagColumn( diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 04d07e35664..72483d40fdc 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -930,6 +930,12 @@ class FHRPGroupEditView(generic.ObjectEditView): 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): queryset = FHRPGroup.objects.all() diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index b8607a0bbe8..814ca1ed60c 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -58,22 +58,24 @@ class TokenAuthentication(authentication.TokenAuthentication): if token.is_expired: raise exceptions.AuthenticationFailed("Token expired") - if not token.user.is_active: - raise exceptions.AuthenticationFailed("User inactive") - + user = token.user # When LDAP authentication is active try to load user data from LDAP directory if settings.REMOTE_AUTH_BACKEND == 'netbox.authentication.LDAPBackend': from netbox.authentication import LDAPBackend ldap_backend = LDAPBackend() # Load from LDAP if FIND_GROUP_PERMS is active - if ldap_backend.settings.FIND_GROUP_PERMS: - user = ldap_backend.populate_user(token.user.username) + # Always query LDAP when user is not active, otherwise it is never activated again + 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 user: - return user, token + if ldap_user: + user = ldap_user - return token.user, token + if not user.is_active: + raise exceptions.AuthenticationFailed("User inactive") + + return user, token class TokenPermissions(DjangoObjectPermissions): diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index 7dc1111f34d..b47c88a4e20 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -108,6 +108,5 @@ class ObjectValidationMixin: conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count() if conforming_count != len(instance): raise ObjectDoesNotExist - else: - # Check that the instance is matched by the view's queryset - self.queryset.get(pk=instance.pk) + elif not self.queryset.filter(pk=instance.pk).exists(): + raise ObjectDoesNotExist diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py index b1f4858c704..a7e56e2799e 100644 --- a/netbox/netbox/authentication.py +++ b/netbox/netbox/authentication.py @@ -351,6 +351,14 @@ class LDAPBackend: if getattr(ldap_config, 'LDAP_IGNORE_CERT_ERRORS', False): 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 diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 03e7eacc0ac..cb26652b9fb 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str # Environment setup # -VERSION = '3.3.5' +VERSION = '3.3.6' # Hostname 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_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', []) CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken') +CSRF_COOKIE_PATH = BASE_PATH or '/' CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', []) DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y') 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', {}) SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None) 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_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') 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 SOCIAL_AUTH_JSONFIELD_ENABLED = True - +SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'netbox.users.utils.clean_username' # # Django Prometheus diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py index bc1f0e2ca90..18b64344f01 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -1,5 +1,6 @@ import platform import sys +from collections import namedtuple from django.conf import settings from django.core.cache import cache @@ -8,6 +9,7 @@ from django.shortcuts import redirect, render from django.template import loader from django.template.exceptions import TemplateDoesNotExist from django.urls import reverse +from django.utils.translation import gettext as _ from django.views.decorators.csrf import requires_csrf_token from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found 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.forms import SearchForm from netbox.search import SEARCH_TYPES -from tenancy.models import Tenant +from tenancy.models import Contact, Tenant from virtualization.models import Cluster, VirtualMachine from wireless.models import WirelessLAN, WirelessLink +Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count')) + + class HomeView(View): template_name = 'home.html' def get(self, request): 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 - ) - connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter( + ).count + power_connections = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter( _path__is_complete=True - ) - connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter( + ).count + interface_connections = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter( _path__is_complete=True - ) + ).count + + def get_count_queryset(model): + return model.objects.restrict(request.user, 'view').count def build_stats(): org = ( - ("dcim.view_site", "Sites", Site.objects.restrict(request.user, 'view').count), - ("tenancy.view_tenant", "Tenants", Tenant.objects.restrict(request.user, 'view').count), + Link(_('Sites'), 'dcim:site_list', 'dcim.view_site', get_count_queryset(Site)), + 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.view_rack", "Racks", Rack.objects.restrict(request.user, 'view').count), - ("dcim.view_devicetype", "Device Types", DeviceType.objects.restrict(request.user, 'view').count), - ("dcim.view_device", "Devices", Device.objects.restrict(request.user, 'view').count), + Link(_('Racks'), 'dcim:rack_list', 'dcim.view_rack', get_count_queryset(Rack)), + Link(_('Device Types'), 'dcim:devicetype_list', 'dcim.view_devicetype', get_count_queryset(DeviceType)), + Link(_('Devices'), 'dcim:device_list', 'dcim.view_device', get_count_queryset(Device)), ) ipam = ( - ("ipam.view_vrf", "VRFs", VRF.objects.restrict(request.user, 'view').count), - ("ipam.view_aggregate", "Aggregates", Aggregate.objects.restrict(request.user, 'view').count), - ("ipam.view_prefix", "Prefixes", Prefix.objects.restrict(request.user, 'view').count), - ("ipam.view_iprange", "IP Ranges", IPRange.objects.restrict(request.user, 'view').count), - ("ipam.view_ipaddress", "IP Addresses", IPAddress.objects.restrict(request.user, 'view').count), - ("ipam.view_vlan", "VLANs", VLAN.objects.restrict(request.user, 'view').count) - + Link(_('VRFs'), 'ipam:vrf_list', 'ipam.view_vrf', get_count_queryset(VRF)), + Link(_('Aggregates'), 'ipam:aggregate_list', 'ipam.view_aggregate', get_count_queryset(Aggregate)), + Link(_('Prefixes'), 'ipam:prefix_list', 'ipam.view_prefix', get_count_queryset(Prefix)), + Link(_('IP Ranges'), 'ipam:iprange_list', 'ipam.view_iprange', get_count_queryset(IPRange)), + Link(_('IP Addresses'), 'ipam:ipaddress_list', 'ipam.view_ipaddress', get_count_queryset(IPAddress)), + Link(_('VLANs'), 'ipam:vlan_list', 'ipam.view_vlan', get_count_queryset(VLAN)), ) circuits = ( - ("circuits.view_provider", "Providers", Provider.objects.restrict(request.user, 'view').count), - ("circuits.view_circuit", "Circuits", Circuit.objects.restrict(request.user, 'view').count), + Link(_('Providers'), 'circuits:provider_list', 'circuits.view_provider', get_count_queryset(Provider)), + Link(_('Circuits'), 'circuits:circuit_list', 'circuits.view_circuit', get_count_queryset(Circuit)) ) virtualization = ( - ("virtualization.view_cluster", "Clusters", Cluster.objects.restrict(request.user, 'view').count), - ("virtualization.view_virtualmachine", "Virtual Machines", VirtualMachine.objects.restrict(request.user, 'view').count), - + Link(_('Clusters'), 'virtualization:cluster_list', 'virtualization.view_cluster', + get_count_queryset(Cluster)), + Link(_('Virtual Machines'), 'virtualization:virtualmachine_list', 'virtualization.view_virtualmachine', + get_count_queryset(VirtualMachine)), ) connections = ( - ("dcim.view_cable", "Cables", Cable.objects.restrict(request.user, 'view').count), - ("dcim.view_consoleport", "Console", connected_consoleports.count), - ("dcim.view_interface", "Interfaces", connected_interfaces.count), - ("dcim.view_powerport", "Power Connections", connected_powerports.count), + Link(_('Cables'), 'dcim:cable_list', 'dcim.view_cable', get_count_queryset(Cable)), + Link(_('Interfaces'), 'dcim:interface_connections_list', 'dcim.view_interface', interface_connections), + Link(_('Console'), 'dcim:console_connections_list', 'dcim.view_consoleport', console_connections), + Link(_('Power'), 'dcim:power_connections_list', 'dcim.view_powerport', power_connections), ) power = ( - ("dcim.view_powerpanel", "Power Panels", PowerPanel.objects.restrict(request.user, 'view').count), - ("dcim.view_powerfeed", "Power Feeds", PowerFeed.objects.restrict(request.user, 'view').count), + Link(_('Power Panels'), 'dcim:powerpanel_list', 'dcim.view_powerpanel', get_count_queryset(PowerPanel)), + Link(_('Power Feeds'), 'dcim:powerfeed_list', 'dcim.view_powerfeed', get_count_queryset(PowerFeed)), ) wireless = ( - ("wireless.view_wirelesslan", "Wireless LANs", WirelessLAN.objects.restrict(request.user, 'view').count), - ("wireless.view_wirelesslink", "Wireless Links", WirelessLink.objects.restrict(request.user, 'view').count), + Link(_('Wireless LANs'), 'wireless:wirelesslan_list', 'wireless.view_wirelesslan', + get_count_queryset(WirelessLAN)), + Link(_('Wireless Links'), 'wireless:wirelesslink_list', 'wireless.view_wirelesslink', + get_count_queryset(WirelessLink)), ) - sections = ( - ("Organization", org, "domain"), - ("IPAM", ipam, "counter"), - ("Virtualization", virtualization, "monitor"), - ("Inventory", dcim, "server"), - ("Circuits", circuits, "transit-connection-variant"), - ("Connections", connections, "cable-data"), - ("Power", power, "flash"), - ("Wireless", wireless, "wifi"), + stats = ( + (_('Organization'), org, 'domain'), + (_('IPAM'), ipam, 'counter'), + (_('Virtualization'), virtualization, 'monitor'), + (_('Inventory'), dcim, 'server'), + (_('Circuits'), circuits, 'transit-connection-variant'), + (_('Connections'), connections, 'cable-data'), + (_('Power'), power, 'flash'), + (_('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 # Compile changelog table diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index a56a832b6e5..3b0c772512a 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -173,7 +173,7 @@ class ObjectImportView(GetReturnURLMixin, BaseObjectView): obj = model_form.save() # Enforce object-level permissions - if not self.queryset.filter(pk=obj.pk).first(): + if not self.queryset.filter(pk=obj.pk).exists(): raise PermissionsViolation() # Iterate through the related object forms (if any), validating and saving each instance. @@ -390,7 +390,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): obj = form.save() # 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() msg = '{} {}'.format( diff --git a/netbox/project-static/.eslintrc b/netbox/project-static/.eslintrc index 802fa7a3e01..30b47308b5c 100644 --- a/netbox/project-static/.eslintrc +++ b/netbox/project-static/.eslintrc @@ -31,8 +31,7 @@ } }, "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-inner-declarations": "off", "comma-dangle": ["error", "always-multiline"], diff --git a/netbox/project-static/dist/cable_trace.css b/netbox/project-static/dist/cable_trace.css index ff431f4ad84..a5f5ff7e98d 100644 --- a/netbox/project-static/dist/cable_trace.css +++ b/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} diff --git a/netbox/project-static/dist/config.js b/netbox/project-static/dist/config.js index 2b360c5a20a..cda30523c16 100644 --- a/netbox/project-static/dist/config.js +++ b/netbox/project-static/dist/config.js @@ -1,5 +1,5 @@ -(()=>{var cr=Object.create;var le=Object.defineProperty,ur=Object.defineProperties,fr=Object.getOwnPropertyDescriptor,dr=Object.getOwnPropertyDescriptors,hr=Object.getOwnPropertyNames,hn=Object.getOwnPropertySymbols,pr=Object.getPrototypeOf,pn=Object.prototype.hasOwnProperty,mr=Object.prototype.propertyIsEnumerable;var mn=(i,t,e)=>t in i?le(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e,O=(i,t)=>{for(var e in t||(t={}))pn.call(t,e)&&mn(i,e,t[e]);if(hn)for(var e of hn(t))mr.call(t,e)&&mn(i,e,t[e]);return i},Me=(i,t)=>ur(i,dr(t)),gn=i=>le(i,"__esModule",{value:!0});var Ot=(i,t)=>()=>(t||i((t={exports:{}}).exports,t),t.exports),gr=(i,t)=>{gn(i);for(var e in t)le(i,e,{get:t[e],enumerable:!0})},_r=(i,t,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of hr(t))!pn.call(i,n)&&n!=="default"&&le(i,n,{get:()=>t[n],enumerable:!(e=fr(t,n))||e.enumerable});return i},Er=i=>_r(gn(le(i!=null?cr(pr(i)):{},"default",i&&i.__esModule&&"default"in i?{get:()=>i.default,enumerable:!0}:{value:i,enumerable:!0})),i);var vi=(i,t,e)=>new Promise((n,o)=>{var r=u=>{try{l(e.next(u))}catch(p){o(p)}},s=u=>{try{l(e.throw(u))}catch(p){o(p)}},l=u=>u.done?n(u.value):Promise.resolve(u.value).then(r,s);l((e=e.apply(i,t)).next())});var tn=Ot((Jo,ai)=>{(function(i,t){typeof define=="function"&&define.amd?define(t):typeof ai=="object"&&ai.exports?ai.exports=t():i.EvEmitter=t()})(typeof window!="undefined"?window:Jo,function(){"use strict";function i(){}var t=i.prototype;return t.on=function(e,n){if(!(!e||!n)){var o=this._events=this._events||{},r=o[e]=o[e]||[];return r.indexOf(n)==-1&&r.push(n),this}},t.once=function(e,n){if(!(!e||!n)){this.on(e,n);var o=this._onceEvents=this._onceEvents||{},r=o[e]=o[e]||{};return r[n]=!0,this}},t.off=function(e,n){var o=this._events&&this._events[e];if(!(!o||!o.length)){var r=o.indexOf(n);return r!=-1&&o.splice(r,1),this}},t.emitEvent=function(e,n){var o=this._events&&this._events[e];if(!(!o||!o.length)){o=o.slice(0),n=n||[];for(var r=this._onceEvents&&this._onceEvents[e],s=0;s{(function(i,t){typeof define=="function"&&define.amd?define(t):typeof li=="object"&&li.exports?li.exports=t():i.getSize=t()})(window,function(){"use strict";function t(d){var y=parseFloat(d),E=d.indexOf("%")==-1&&!isNaN(y);return E&&y}function e(){}var n=typeof console=="undefined"?e:function(d){console.error(d)},o=["paddingLeft","paddingRight","paddingTop","paddingBottom","marginLeft","marginRight","marginTop","marginBottom","borderLeftWidth","borderRightWidth","borderTopWidth","borderBottomWidth"],r=o.length;function s(){for(var d={width:0,height:0,innerWidth:0,innerHeight:0,outerWidth:0,outerHeight:0},y=0;y