Bläddra i källkod

Fixes #7041: Properly format JSON config object returned from a NAPALM device

thatmattlove 4 år sedan
förälder
incheckning
14d87a3584

+ 1 - 0
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

+ 2 - 2
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:

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
netbox/project-static/dist/config.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
netbox/project-static/dist/config.js.map


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
netbox/project-static/dist/jobs.js.map


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
netbox/project-static/dist/lldp.js.map


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
netbox/project-static/dist/netbox.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
netbox/project-static/dist/netbox.js.map


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
netbox/project-static/dist/status.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
netbox/project-static/dist/status.js.map


+ 14 - 6
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<Required<DeviceConfig['get_config']>>(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);
+              }
             }
           }
         }

+ 6 - 3
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<string, unknown>;
+    running: string | Record<string, unknown>;
+    startup: string | Record<string, unknown>;
+    error?: string;
   };
 };
 
+type DeviceConfigType = Exclude<keyof DeviceConfig['get_config'], 'error'>;
+
 type DeviceEnvironment = {
   cpu?: {
     [core: string]: { '%usage': number };

+ 3 - 1
netbox/project-static/src/util.ts

@@ -19,7 +19,9 @@ export function isApiError(data: Record<string, unknown>): data is APIError {
   return 'error' in data && 'exception' in data;
 }
 
-export function hasError(data: Record<string, unknown>): data is ErrorBase {
+export function hasError<E extends ErrorBase = ErrorBase>(
+  data: Record<string, unknown>,
+): data is E {
   return 'error' in data;
 }
 

+ 41 - 0
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):

Vissa filer visades inte eftersom för många filer har ändrats