diff --git a/CHANGELOG.md b/CHANGELOG.md index d9433aacf..e496d31d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ v2.4.9 (FUTURE) ## Enhancements * [#2089](https://github.com/digitalocean/netbox/issues/2089) - Add SONET interface form factors +* [#2495](https://github.com/digitalocean/netbox/issues/2495) - Enable deep-merging of config context data * [#2597](https://github.com/digitalocean/netbox/issues/2597) - Add FibreChannel SFP28 (32GFC) interface form factor ## Bug Fixes diff --git a/netbox/extras/models.py b/netbox/extras/models.py index de3edca9b..1605df6df 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -18,7 +18,7 @@ from django.utils.encoding import python_2_unicode_compatible from django.utils.safestring import mark_safe from dcim.constants import CONNECTION_STATUS_CONNECTED -from utilities.utils import foreground_color +from utilities.utils import deepmerge, foreground_color from .constants import * from .querysets import ConfigContextQuerySet @@ -727,11 +727,11 @@ class ConfigContextModel(models.Model): # Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs data = OrderedDict() for context in ConfigContext.objects.get_for_object(self): - data.update(context.data) + data = deepmerge(data, context.data) - # If the object has local config context data defined, that data overwrites all rendered data + # If the object has local config context data defined, merge it last if self.local_context_data is not None: - data.update(self.local_context_data) + data = deepmerge(data, self.local_context_data) return data diff --git a/netbox/utilities/tests/test_utils.py b/netbox/utilities/tests/test_utils.py new file mode 100644 index 000000000..4e0fec1ba --- /dev/null +++ b/netbox/utilities/tests/test_utils.py @@ -0,0 +1,89 @@ +from django.test import TestCase + +from utilities.utils import deepmerge + + +class DeepMergeTest(TestCase): + """ + Validate the behavior of the deepmerge() utility. + """ + + def setUp(self): + return + + def test_deepmerge(self): + + dict1 = { + 'active': True, + 'foo': 123, + 'fruits': { + 'orange': 1, + 'apple': 2, + 'pear': 3, + }, + 'vegetables': None, + 'dairy': { + 'milk': 1, + 'cheese': 2, + }, + 'deepnesting': { + 'foo': { + 'a': 10, + 'b': 20, + 'c': 30, + }, + }, + } + + dict2 = { + 'active': False, + 'bar': 456, + 'fruits': { + 'banana': 4, + 'grape': 5, + }, + 'vegetables': { + 'celery': 1, + 'carrots': 2, + 'corn': 3, + }, + 'dairy': None, + 'deepnesting': { + 'foo': { + 'a': 100, + 'd': 40, + }, + }, + } + + merged = { + 'active': False, + 'foo': 123, + 'bar': 456, + 'fruits': { + 'orange': 1, + 'apple': 2, + 'pear': 3, + 'banana': 4, + 'grape': 5, + }, + 'vegetables': { + 'celery': 1, + 'carrots': 2, + 'corn': 3, + }, + 'dairy': None, + 'deepnesting': { + 'foo': { + 'a': 100, + 'b': 20, + 'c': 30, + 'd': 40, + }, + }, + } + + self.assertEqual( + deepmerge(dict1, dict2), + merged + ) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 14c29d211..642242d30 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from collections import OrderedDict import datetime import json import six @@ -109,3 +110,16 @@ def serialize_object(obj, extra=None): data.update(extra) return data + + +def deepmerge(original, new): + """ + Deep merge two dictionaries (new into original) and return a new dict + """ + merged = OrderedDict(original) + for key, val in new.items(): + if key in original and isinstance(original[key], dict) and isinstance(val, dict): + merged[key] = deepmerge(original[key], val) + else: + merged[key] = val + return merged