diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index db3bc822c..ecccb7306 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#7041](https://github.com/netbox-community/netbox/issues/7041) - Properly format JSON config object returned from a NAPALM device * [#7070](https://github.com/netbox-community/netbox/issues/7070) - Fix exception when filtering by prefix max length in UI * [#7071](https://github.com/netbox-community/netbox/issues/7071) - Fix exception when removing a primary IP from a device/VM * [#7072](https://github.com/netbox-community/netbox/issues/7072) - Fix table configuration under prefix child object views diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 3ee225335..3d23cde5c 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -22,7 +22,7 @@ from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.api.exceptions import ServiceUnavailable from netbox.api.metadata import ContentTypeMetadata from utilities.api import get_serializer_for_model -from utilities.utils import count_related +from utilities.utils import count_related, decode_dict from virtualization.models import VirtualMachine from . import serializers from .exceptions import MissingFilterException @@ -498,7 +498,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet): response[method] = {'error': 'Only get_* NAPALM methods are supported'} continue try: - response[method] = getattr(d, method)() + response[method] = decode_dict(getattr(d, method)()) except NotImplementedError: response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)} except Exception as e: diff --git a/netbox/project-static/dist/config.js b/netbox/project-static/dist/config.js index 18f4811a2..cf1022589 100644 Binary files a/netbox/project-static/dist/config.js and b/netbox/project-static/dist/config.js differ diff --git a/netbox/project-static/dist/config.js.map b/netbox/project-static/dist/config.js.map index 176f8c85e..5f27e84a6 100644 Binary files a/netbox/project-static/dist/config.js.map and b/netbox/project-static/dist/config.js.map differ diff --git a/netbox/project-static/dist/jobs.js.map b/netbox/project-static/dist/jobs.js.map index fbd950267..e07a4157d 100644 Binary files a/netbox/project-static/dist/jobs.js.map and b/netbox/project-static/dist/jobs.js.map differ diff --git a/netbox/project-static/dist/lldp.js.map b/netbox/project-static/dist/lldp.js.map index be357a5bc..028c35995 100644 Binary files a/netbox/project-static/dist/lldp.js.map and b/netbox/project-static/dist/lldp.js.map differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index a5995f96a..e9ea8ddbf 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index db0366f6b..1d3380868 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/dist/status.js b/netbox/project-static/dist/status.js index b60da0a36..2f3c2f762 100644 Binary files a/netbox/project-static/dist/status.js and b/netbox/project-static/dist/status.js differ diff --git a/netbox/project-static/dist/status.js.map b/netbox/project-static/dist/status.js.map index a249f7380..fc612cec8 100644 Binary files a/netbox/project-static/dist/status.js.map and b/netbox/project-static/dist/status.js.map differ diff --git a/netbox/project-static/src/device/config.ts b/netbox/project-static/src/device/config.ts index cbe70952e..c9c19e8d3 100644 --- a/netbox/project-static/src/device/config.ts +++ b/netbox/project-static/src/device/config.ts @@ -13,18 +13,26 @@ function initConfig(): void { .then(data => { if (hasError(data)) { createToast('danger', 'Error Fetching Device Config', data.error).show(); + console.error(data.error); + return; + } else if (hasError>(data.get_config)) { + createToast('danger', 'Error Fetching Device Config', data.get_config.error).show(); + console.error(data.get_config.error); return; } else { - const configTypes = [ - 'running', - 'startup', - 'candidate', - ] as (keyof DeviceConfig['get_config'])[]; + const configTypes = ['running', 'startup', 'candidate'] as DeviceConfigType[]; for (const configType of configTypes) { const element = document.getElementById(`${configType}_config`); if (element !== null) { - element.innerHTML = data.get_config[configType]; + const config = data.get_config[configType]; + if (typeof config === 'string') { + // If the returned config is a string, set the element innerHTML as-is. + element.innerHTML = config; + } else { + // If the returned config is an object (dict), convert it to JSON. + element.innerHTML = JSON.stringify(data.get_config[configType], null, 2); + } } } } diff --git a/netbox/project-static/src/global.d.ts b/netbox/project-static/src/global.d.ts index af5819c8e..bad12c795 100644 --- a/netbox/project-static/src/global.d.ts +++ b/netbox/project-static/src/global.d.ts @@ -152,12 +152,15 @@ type LLDPNeighborDetail = { type DeviceConfig = { get_config: { - candidate: string; - running: string; - startup: string; + candidate: string | Record; + running: string | Record; + startup: string | Record; + error?: string; }; }; +type DeviceConfigType = Exclude; + type DeviceEnvironment = { cpu?: { [core: string]: { '%usage': number }; diff --git a/netbox/project-static/src/util.ts b/netbox/project-static/src/util.ts index c3dd05b0e..9103a7b01 100644 --- a/netbox/project-static/src/util.ts +++ b/netbox/project-static/src/util.ts @@ -19,7 +19,9 @@ export function isApiError(data: Record): data is APIError { return 'error' in data && 'exception' in data; } -export function hasError(data: Record): data is ErrorBase { +export function hasError( + data: Record, +): data is E { return 'error' in data; } diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 32880fca8..d9646f38f 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -1,7 +1,9 @@ import datetime import json +import urllib from collections import OrderedDict from itertools import count, groupby +from typing import Any, Dict, List, Tuple from django.core.serializers import serialize from django.db.models import Count, OuterRef, Subquery @@ -286,6 +288,45 @@ def flatten_dict(d, prefix='', separator='.'): return ret +def decode_dict(encoded_dict: Dict, *, decode_keys: bool = True) -> Dict: + """ + Recursively URL decode string keys and values of a dict. + + For example, `{'1%2F1%2F1': {'1%2F1%2F2': ['1%2F1%2F3', '1%2F1%2F4']}}` would + become: `{'1/1/1': {'1/1/2': ['1/1/3', '1/1/4']}}` + + :param encoded_dict: Dictionary to be decoded. + :param decode_keys: (Optional) Enable/disable decoding of dict keys. + """ + + def decode_value(value: Any, _decode_keys: bool) -> Any: + """ + Handle URL decoding of any supported value type. + """ + # Decode string values. + if isinstance(value, str): + return urllib.parse.unquote(value) + # Recursively decode each list item. + elif isinstance(value, list): + return [decode_value(v, _decode_keys) for v in value] + # Recursively decode each tuple item. + elif isinstance(value, Tuple): + return tuple(decode_value(v, _decode_keys) for v in value) + # Recursively decode each dict key/value pair. + elif isinstance(value, dict): + # Don't decode keys, if `decode_keys` is false. + if not _decode_keys: + return {k: decode_value(v, _decode_keys) for k, v in value.items()} + return {urllib.parse.unquote(k): decode_value(v, _decode_keys) for k, v in value.items()} + return value + + if not decode_keys: + # Don't decode keys, if `decode_keys` is false. + return {k: decode_value(v, decode_keys) for k, v in encoded_dict.items()} + + return {urllib.parse.unquote(k): decode_value(v, decode_keys) for k, v in encoded_dict.items()} + + # Taken from django.utils.functional (<3.0) def curry(_curried_func, *args, **kwargs): def _curried(*moreargs, **morekwargs):