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

This commit is contained in:
thatmattlove 2021-08-31 23:56:18 -07:00
parent d743dc160a
commit 14d87a3584
14 changed files with 67 additions and 12 deletions

View File

@ -4,6 +4,7 @@
### Bug Fixes ### 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 * [#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 * [#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 * [#7072](https://github.com/netbox-community/netbox/issues/7072) - Fix table configuration under prefix child object views

View File

@ -22,7 +22,7 @@ from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.exceptions import ServiceUnavailable from netbox.api.exceptions import ServiceUnavailable
from netbox.api.metadata import ContentTypeMetadata from netbox.api.metadata import ContentTypeMetadata
from utilities.api import get_serializer_for_model 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 virtualization.models import VirtualMachine
from . import serializers from . import serializers
from .exceptions import MissingFilterException from .exceptions import MissingFilterException
@ -498,7 +498,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
response[method] = {'error': 'Only get_* NAPALM methods are supported'} response[method] = {'error': 'Only get_* NAPALM methods are supported'}
continue continue
try: try:
response[method] = getattr(d, method)() response[method] = decode_dict(getattr(d, method)())
except NotImplementedError: except NotImplementedError:
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)} response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
except Exception as e: except Exception as e:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -13,18 +13,26 @@ function initConfig(): void {
.then(data => { .then(data => {
if (hasError(data)) { if (hasError(data)) {
createToast('danger', 'Error Fetching Device Config', data.error).show(); 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; return;
} else { } else {
const configTypes = [ const configTypes = ['running', 'startup', 'candidate'] as DeviceConfigType[];
'running',
'startup',
'candidate',
] as (keyof DeviceConfig['get_config'])[];
for (const configType of configTypes) { for (const configType of configTypes) {
const element = document.getElementById(`${configType}_config`); const element = document.getElementById(`${configType}_config`);
if (element !== null) { 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);
}
} }
} }
} }

View File

@ -152,12 +152,15 @@ type LLDPNeighborDetail = {
type DeviceConfig = { type DeviceConfig = {
get_config: { get_config: {
candidate: string; candidate: string | Record<string, unknown>;
running: string; running: string | Record<string, unknown>;
startup: string; startup: string | Record<string, unknown>;
error?: string;
}; };
}; };
type DeviceConfigType = Exclude<keyof DeviceConfig['get_config'], 'error'>;
type DeviceEnvironment = { type DeviceEnvironment = {
cpu?: { cpu?: {
[core: string]: { '%usage': number }; [core: string]: { '%usage': number };

View File

@ -19,7 +19,9 @@ export function isApiError(data: Record<string, unknown>): data is APIError {
return 'error' in data && 'exception' in data; 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; return 'error' in data;
} }

View File

@ -1,7 +1,9 @@
import datetime import datetime
import json import json
import urllib
from collections import OrderedDict from collections import OrderedDict
from itertools import count, groupby from itertools import count, groupby
from typing import Any, Dict, List, Tuple
from django.core.serializers import serialize from django.core.serializers import serialize
from django.db.models import Count, OuterRef, Subquery from django.db.models import Count, OuterRef, Subquery
@ -286,6 +288,45 @@ def flatten_dict(d, prefix='', separator='.'):
return ret 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) # Taken from django.utils.functional (<3.0)
def curry(_curried_func, *args, **kwargs): def curry(_curried_func, *args, **kwargs):
def _curried(*moreargs, **morekwargs): def _curried(*moreargs, **morekwargs):