1"""
2Utilities for comparing and updating configurations while keeping track of
3changes in a way that can be easily reported in a state.
4"""
5
6
7def compare_and_update_config(config, update_config, changes, namespace=""):
8    """
9    Recursively compare two configs, writing any needed changes to the
10    update_config and capturing changes in the changes dict.
11    """
12    if isinstance(config, dict):
13        if not update_config:
14            if config:
15                # the updated config is more valid--report that we are using it
16                changes[namespace] = {
17                    "new": config,
18                    "old": update_config,
19                }
20            return config
21        elif not isinstance(update_config, dict):
22            # new config is a dict, other isn't--new one wins
23            changes[namespace] = {
24                "new": config,
25                "old": update_config,
26            }
27            return config
28        else:
29            # compare each key in the base config with the values in the
30            # update_config, overwriting the values that are different but
31            # keeping any that are not defined in config
32            for key, value in config.items():
33                _namespace = key
34                if namespace:
35                    _namespace = "{}.{}".format(namespace, _namespace)
36                update_config[key] = compare_and_update_config(
37                    value,
38                    update_config.get(key, None),
39                    changes,
40                    namespace=_namespace,
41                )
42            return update_config
43
44    elif isinstance(config, list):
45        if not update_config:
46            if config:
47                # the updated config is more valid--report that we are using it
48                changes[namespace] = {
49                    "new": config,
50                    "old": update_config,
51                }
52            return config
53        elif not isinstance(update_config, list):
54            # new config is a list, other isn't--new one wins
55            changes[namespace] = {
56                "new": config,
57                "old": update_config,
58            }
59            return config
60        else:
61            # iterate through config list, ensuring that each index in the
62            # update_config list is the same
63            for idx, item in enumerate(config):
64                _namespace = "[{}]".format(idx)
65                if namespace:
66                    _namespace = "{}{}".format(namespace, _namespace)
67                _update = None
68                if len(update_config) > idx:
69                    _update = update_config[idx]
70                if _update:
71                    update_config[idx] = compare_and_update_config(
72                        config[idx],
73                        _update,
74                        changes,
75                        namespace=_namespace,
76                    )
77                else:
78                    changes[_namespace] = {
79                        "new": config[idx],
80                        "old": _update,
81                    }
82                    update_config.append(config[idx])
83
84            if len(update_config) > len(config):
85                # trim any items in update_config that are not in config
86                for idx, old_item in enumerate(update_config):
87                    if idx < len(config):
88                        continue
89                    _namespace = "[{}]".format(idx)
90                    if namespace:
91                        _namespace = "{}{}".format(namespace, _namespace)
92                    changes[_namespace] = {
93                        "new": None,
94                        "old": old_item,
95                    }
96                del update_config[len(config) :]
97            return update_config
98
99    else:
100        if config != update_config:
101            changes[namespace] = {
102                "new": config,
103                "old": update_config,
104            }
105        return config
106