diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 2d6ca5700..df5ac6e81 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.2.2 + placeholder: v3.2.3 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 13b162741..422b87f52 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.2.2 + placeholder: v3.2.3 validations: required: true - type: dropdown diff --git a/README.md b/README.md index d75c2c1a5..60f007946 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ The complete documentation for NetBox can be found at [docs.netbox.dev](https://            [![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com/)
+ [![Sentry](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/sentry.png)](https://sentry.io/) +            [![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech/) diff --git a/base_requirements.txt b/base_requirements.txt index 095906914..6bb537a6a 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -102,6 +102,10 @@ psycopg2-binary # https://github.com/yaml/pyyaml PyYAML +# Sentry SDK +# https://github.com/getsentry/sentry-python +sentry-sdk + # Social authentication framework # https://github.com/python-social-auth/social-core social-auth-core diff --git a/docs/administration/error-reporting.md b/docs/administration/error-reporting.md new file mode 100644 index 000000000..e04372338 --- /dev/null +++ b/docs/administration/error-reporting.md @@ -0,0 +1,46 @@ +# Error Reporting + +## Sentry + +### Enabling Error Reporting + +NetBox v3.2.3 and later support native integration with [Sentry](https://sentry.io/) for automatic error reporting. To enable this functionality, simply set `SENTRY_ENABLED` to True in `configuration.py`. Errors will be sent to a Sentry ingestor maintained by the NetBox team for analysis. + +```python +SENTRY_ENABLED = True +``` + +### Using a Custom DSN + +If you prefer instead to use your own Sentry ingestor, you'll need to first create a new project under your Sentry account to represent your NetBox deployment and obtain its corresponding data source name (DSN). This looks like a URL similar to the example below: + +``` +https://examplePublicKey@o0.ingest.sentry.io/0 +``` + +Once you have obtained a DSN, configure Sentry in NetBox's `configuration.py` file with the following parameters: + +```python +SENTRY_ENABLED = True +SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0" +``` + +### Assigning Tags + +You can optionally attach one or more arbitrary tags to the outgoing error reports if desired by setting the `SENTRY_TAGS` parameter: + +```python +SENTRY_TAGS = { + "custom.foo": "123", + "custom.bar": "abc", +} +``` + +!!! warning "Reserved tag prefixes" + Avoid using any tag names which begin with `netbox.`, as this prefix is reserved by the NetBox application. + +### Testing + +Once the configuration has been saved, restart the NetBox service. + +To test Sentry operation, try generating a 404 (page not found) error by navigating to an invalid URL, such as `https://netbox/404-error-testing`. (Be sure that debug mode has been disabled.) After receiving a 404 response from the NetBox server, you should see the issue appear shortly in Sentry. diff --git a/docs/configuration/error-reporting.md b/docs/configuration/error-reporting.md new file mode 100644 index 000000000..d1c47e2fb --- /dev/null +++ b/docs/configuration/error-reporting.md @@ -0,0 +1,54 @@ +# Error Reporting Settings + +## SENTRY_DSN + +Default: None + +Defines a Sentry data source name (DSN) for automated error reporting. `SENTRY_ENABLED` must be True for this parameter to take effect. For example: + +``` +SENTRY_DSN = "https://examplePublicKey@o0.ingest.sentry.io/0" +``` + +--- + +## SENTRY_ENABLED + +Default: False + +Set to True to enable automatic error reporting via [Sentry](https://sentry.io/). + +--- + +## SENTRY_SAMPLE_RATE + +Default: 1.0 (all) + +The sampling rate for errors. Must be a value between 0 (disabled) and 1.0 (report on all errors). + +--- + +## SENTRY_TAGS + +An optional dictionary of tag names and values to apply to Sentry error reports.For example: + +``` +SENTRY_TAGS = { + "custom.foo": "123", + "custom.bar": "abc", +} +``` + +!!! warning "Reserved tag prefixes" + Avoid using any tag names which begin with `netbox.`, as this prefix is reserved by the NetBox application. + +--- + +## SENTRY_TRACES_SAMPLE_RATE + +Default: 0 (disabled) + +The sampling rate for transactions. Must be a value between 0 (disabled) and 1.0 (report on all transactions). + +!!! warning "Consider performance implications" + A high sampling rate for transactions can induce significant performance penalties. If transaction reporting is desired, it is recommended to use a relatively low sample rate of 10% to 20% (0.1 to 0.2). diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 6eadb3d9e..408d572c7 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -1,21 +1,30 @@ # NetBox v3.2 -## v3.2.3 (FUTURE) +## v3.2.4 (FUTURE) + +--- + +## v3.2.3 (2022-05-12) ### Enhancements +* [#8805](https://github.com/netbox-community/netbox/issues/8805) - Add "mixed" option for device airflow indication * [#8894](https://github.com/netbox-community/netbox/issues/8894) - Include full names when listing users * [#8998](https://github.com/netbox-community/netbox/issues/8998) - Enable filtering racks & reservations by site group * [#9122](https://github.com/netbox-community/netbox/issues/9122) - Introduce `clearcache` management command & clear cache during upgrade +* [#9221](https://github.com/netbox-community/netbox/issues/9221) - Add definition list support for Markdown * [#9260](https://github.com/netbox-community/netbox/issues/9260) - Apply user preferences to tables under object detail views * [#9278](https://github.com/netbox-community/netbox/issues/9278) - Linkify device types count under manufacturers list * [#9280](https://github.com/netbox-community/netbox/issues/9280) - Allow adopting existing components when installing a module * [#9314](https://github.com/netbox-community/netbox/issues/9314) - Add device and VM filters for FHRP group assignments +* [#9340](https://github.com/netbox-community/netbox/issues/9340) - Introduce support for error reporting via Sentry +* [#9343](https://github.com/netbox-community/netbox/issues/9343) - Add Ubiquiti SmartPower power outlet type ### Bug Fixes * [#9190](https://github.com/netbox-community/netbox/issues/9190) - Prevent exception when attempting to instantiate module components which already exist on the parent device * [#9267](https://github.com/netbox-community/netbox/issues/9267) - Remove invalid entry in IP address role choices +* [#9296](https://github.com/netbox-community/netbox/issues/9296) - Improve Markdown link sanitization * [#9306](https://github.com/netbox-community/netbox/issues/9306) - Include VC master interfaces when selecting a LAG/bridge for a VC member interface * [#9311](https://github.com/netbox-community/netbox/issues/9311) - Permit creating contact assignment without a priority via the REST API * [#9313](https://github.com/netbox-community/netbox/issues/9313) - Remove HTML code from CSV output of many-to-many relationships diff --git a/mkdocs.yml b/mkdocs.yml index 225c6d4bf..5c973e0d6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,6 +73,7 @@ nav: - Required Settings: 'configuration/required-settings.md' - Optional Settings: 'configuration/optional-settings.md' - Dynamic Settings: 'configuration/dynamic-settings.md' + - Error Reporting: 'configuration/error-reporting.md' - Remote Authentication: 'configuration/remote-authentication.md' - Core Functionality: - IP Address Management: 'core-functionality/ipam.md' @@ -123,6 +124,7 @@ nav: - Microsoft Azure AD: 'administration/authentication/microsoft-azure-ad.md' - Okta: 'administration/authentication/okta.md' - Permissions: 'administration/permissions.md' + - Error Reporting: 'administration/error-reporting.md' - Housekeeping: 'administration/housekeeping.md' - Replicating NetBox: 'administration/replicating-netbox.md' - NetBox Shell: 'administration/netbox-shell.md' diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index e369201b4..a89960457 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -159,6 +159,7 @@ class DeviceAirflowChoices(ChoiceSet): AIRFLOW_RIGHT_TO_LEFT = 'right-to-left' AIRFLOW_SIDE_TO_REAR = 'side-to-rear' AIRFLOW_PASSIVE = 'passive' + AIRFLOW_MIXED = 'mixed' CHOICES = ( (AIRFLOW_FRONT_TO_REAR, 'Front to rear'), @@ -167,6 +168,7 @@ class DeviceAirflowChoices(ChoiceSet): (AIRFLOW_RIGHT_TO_LEFT, 'Right to left'), (AIRFLOW_SIDE_TO_REAR, 'Side to rear'), (AIRFLOW_PASSIVE, 'Passive'), + (AIRFLOW_MIXED, 'Mixed'), ) @@ -575,6 +577,7 @@ class PowerOutletTypeChoices(ChoiceSet): TYPE_NEUTRIK_POWERCON_32A = 'neutrik-powercon-32a' TYPE_NEUTRIK_POWERCON_TRUE1 = 'neutrik-powercon-true1' TYPE_NEUTRIK_POWERCON_TRUE1_TOP = 'neutrik-powercon-true1-top' + TYPE_UBIQUITI_SMARTPOWER = 'ubiquiti-smartpower' # Other TYPE_HARDWIRED = 'hardwired' @@ -683,6 +686,7 @@ class PowerOutletTypeChoices(ChoiceSet): (TYPE_NEUTRIK_POWERCON_32A, 'Neutrik powerCON (32A)'), (TYPE_NEUTRIK_POWERCON_TRUE1, 'Neutrik powerCON TRUE1'), (TYPE_NEUTRIK_POWERCON_TRUE1_TOP, 'Neutrik powerCON TRUE1 TOP'), + (TYPE_UBIQUITI_SMARTPOWER, 'Ubiquiti SmartPower'), )), ('Other', ( (TYPE_HARDWIRED, 'Hardwired'), diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 0db41373f..ff10d1096 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -1,3 +1,4 @@ +import hashlib import importlib import logging import os @@ -8,9 +9,11 @@ import sys import warnings from urllib.parse import urlsplit +import sentry_sdk from django.contrib.messages import constants as messages from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.validators import URLValidator +from sentry_sdk.integrations.django import DjangoIntegration from netbox.config import PARAMS @@ -40,6 +43,7 @@ if sys.version_info < (3, 8): f"NetBox requires Python 3.8 or later. (Currently installed: Python {platform.python_version()})" ) +DEFAULT_SENTRY_DSN = 'https://198cf560b29d4054ab8e583a1d10ea58@o1242133.ingest.sentry.io/6396485' # # Configuration import @@ -68,6 +72,9 @@ DATABASE = getattr(configuration, 'DATABASE') REDIS = getattr(configuration, 'REDIS') SECRET_KEY = getattr(configuration, 'SECRET_KEY') +# Calculate a unique deployment ID from the secret key +DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16] + # Set static config parameters ADMINS = getattr(configuration, 'ADMINS', []) AUTH_PASSWORD_VALIDATORS = getattr(configuration, 'AUTH_PASSWORD_VALIDATORS', []) @@ -113,6 +120,11 @@ REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATO REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/') RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300) SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/') +SENTRY_DSN = getattr(configuration, 'SENTRY_DSN', DEFAULT_SENTRY_DSN) +SENTRY_ENABLED = getattr(configuration, 'SENTRY_ENABLED', False) +SENTRY_SAMPLE_RATE = getattr(configuration, 'SENTRY_SAMPLE_RATE', 1.0) +SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE', 0) +SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {}) SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None) SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid') SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') @@ -428,6 +440,36 @@ EXEMPT_PATHS = ( ) +# +# Sentry +# + +if SENTRY_ENABLED: + if not SENTRY_DSN: + raise ImproperlyConfigured("SENTRY_ENABLED is True but SENTRY_DSN has not been defined.") + # If using the default DSN, force sampling rates + if SENTRY_DSN == DEFAULT_SENTRY_DSN: + SENTRY_SAMPLE_RATE = 1.0 + SENTRY_TRACES_SAMPLE_RATE = 0 + # Initialize the SDK + sentry_sdk.init( + dsn=SENTRY_DSN, + release=VERSION, + integrations=[DjangoIntegration()], + sample_rate=SENTRY_SAMPLE_RATE, + traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE, + send_default_pii=True, + http_proxy=HTTP_PROXIES.get('http') if HTTP_PROXIES else None, + https_proxy=HTTP_PROXIES.get('https') if HTTP_PROXIES else None + ) + # Assign any configured tags + for k, v in SENTRY_TAGS.items(): + sentry_sdk.set_tag(k, v) + # If using the default DSN, append a unique deployment ID tag for error correlation + if SENTRY_DSN == DEFAULT_SENTRY_DSN: + sentry_sdk.set_tag('netbox.deployment_id', DEPLOYMENT_ID) + + # # Django social auth # diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index e76efe0fe..e8ee4b7b6 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -100,4 +100,5 @@ urlpatterns = [ path('{}'.format(settings.BASE_PATH), include(_patterns)) ] +handler404 = 'netbox.views.handler_404' handler500 = 'netbox.views.server_error' diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py index fad347c36..f159ee637 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -2,7 +2,6 @@ import platform import sys from django.conf import settings -from django.contrib.contenttypes.models import ContentType from django.core.cache import cache from django.db.models import F from django.http import HttpResponseServerError @@ -11,9 +10,10 @@ from django.template import loader from django.template.exceptions import TemplateDoesNotExist from django.urls import reverse from django.views.decorators.csrf import requires_csrf_token -from django.views.defaults import ERROR_500_TEMPLATE_NAME +from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found from django.views.generic import View from packaging import version +from sentry_sdk import capture_message from circuits.models import Circuit, Provider from dcim.models import ( @@ -190,13 +190,21 @@ class StaticMediaFailureView(View): """ Display a user-friendly error message with troubleshooting tips when a static media file fails to load. """ - def get(self, request): return render(request, 'media_failure.html', { 'filename': request.GET.get('filename') }) +def handler_404(request, exception): + """ + Wrap Django's default 404 handler to enable Sentry reporting. + """ + capture_message("Page not found", level="error") + + return page_not_found(request, exception) + + @requires_csrf_token def server_error(request, template_name=ERROR_500_TEMPLATE_NAME): """ diff --git a/netbox/utilities/templatetags/builtins/filters.py b/netbox/utilities/templatetags/builtins/filters.py index 4a3db0a3c..44ad5ac47 100644 --- a/netbox/utilities/templatetags/builtins/filters.py +++ b/netbox/utilities/templatetags/builtins/filters.py @@ -150,15 +150,15 @@ def render_markdown(value): value = strip_tags(value) # Sanitize Markdown links - pattern = fr'\[([^\]]+)\]\((?!({schemes})).*:(.+)\)' + pattern = fr'\[([^\]]+)\]\(\s*(?!({schemes})).*:(.+)\)' value = re.sub(pattern, '[\\1](\\3)', value, flags=re.IGNORECASE) # Sanitize Markdown reference links - pattern = fr'\[(.+)\]:\s*(?!({schemes}))\w*:(.+)' + pattern = fr'\[([^\]]+)\]:\s*(?!({schemes}))\w*:(.+)' value = re.sub(pattern, '[\\1]: \\3', value, flags=re.IGNORECASE) # Render Markdown - html = markdown(value, extensions=['fenced_code', 'tables', StrikethroughExtension()]) + html = markdown(value, extensions=['def_list', 'fenced_code', 'tables', StrikethroughExtension()]) # If the string is not empty wrap it in rendered-markdown to style tables if html: diff --git a/requirements.txt b/requirements.txt index 32c13d455..0a15fcf20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Django==4.0.4 -django-cors-headers==3.11.0 +django-cors-headers==3.12.0 django-debug-toolbar==3.2.4 django-filter==21.1 django-graphiql-debug-toolbar==0.2.0 @@ -16,14 +16,15 @@ drf-yasg[validation]==1.20.0 graphene-django==2.15.0 gunicorn==20.1.0 Jinja2==3.1.2 -Markdown==3.3.6 +Markdown==3.3.7 markdown-include==0.6.0 -mkdocs-material==8.2.11 +mkdocs-material==8.2.14 mkdocstrings[python-legacy]==0.18.1 netaddr==0.8.0 Pillow==9.1.0 psycopg2-binary==2.9.3 PyYAML==6.0 +sentry-sdk==1.5.12 social-auth-app-django==5.0.0 social-auth-core==4.2.0 svgwrite==1.4.2