From b83443211eaf3e4a44cb93ed9e83fef4de003b36 Mon Sep 17 00:00:00 2001 From: Luca Haneklau Date: Thu, 29 Jul 2021 13:20:38 +0200 Subject: [PATCH 1/5] vyos_config improved diffing --- plugins/cliconf/vyos.py | 11 +- plugins/cliconf_utils/__init__.py | 0 plugins/cliconf_utils/vyosconf.py | 220 ++++++++++++++++++++++ plugins/modules/vyos_config.py | 15 +- tests/unit/cliconf/test_utils_vyosconf.py | 160 ++++++++++++++++ 5 files changed, 400 insertions(+), 6 deletions(-) create mode 100644 plugins/cliconf_utils/__init__.py create mode 100644 plugins/cliconf_utils/vyosconf.py create mode 100644 tests/unit/cliconf/test_utils_vyosconf.py diff --git a/plugins/cliconf/vyos.py b/plugins/cliconf/vyos.py index d63c677e5..0471fd5e0 100644 --- a/plugins/cliconf/vyos.py +++ b/plugins/cliconf/vyos.py @@ -55,6 +55,9 @@ to_list, ) from ansible.plugins.cliconf import CliconfBase +from ansible_collections.vyos.vyos.plugins.cliconf_utils.vyosconf import ( + VyosConf, +) class Cliconf(CliconfBase): @@ -263,6 +266,12 @@ def get_diff( diff["config_diff"] = list(candidate_commands) return diff + if diff_match == "smart": + running_conf = VyosConf(running.splitlines()) + candidate_conf = VyosConf(candidate_commands) + diff["config_diff"] = running_conf.diff_commands_to(candidate_conf) + return diff + running_commands = [ str(c).replace("'", "") for c in running.splitlines() ] @@ -339,7 +348,7 @@ def get_device_operations(self): def get_option_values(self): return { "format": ["text", "set"], - "diff_match": ["line", "none"], + "diff_match": ["line", "smart", "none"], "diff_replace": [], "output": [], } diff --git a/plugins/cliconf_utils/__init__.py b/plugins/cliconf_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/cliconf_utils/vyosconf.py b/plugins/cliconf_utils/vyosconf.py new file mode 100644 index 000000000..404948ed4 --- /dev/null +++ b/plugins/cliconf_utils/vyosconf.py @@ -0,0 +1,220 @@ +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import re + + +class VyosConf: + def __init__(self, commands=None): + self.config = {} + if type(commands) is list: + self.run_commands(commands) + + def set_entry(self, path, leaf): + """ + This function sets a value in the configuration given a path. + :param path: list of strings to traveser in the config + :param leaf: value to set at the destination + :return: dict + """ + target = self.config + path = path + [leaf] + for key in path: + if key not in target or type(target[key]) is not dict: + target[key] = {} + target = target[key] + return self.config + + def del_entry(self, path, leaf): + """ + This function deletes a value from the configuration given a path + and also removes all the parents that are now empty. + :param path: list of strings to traveser in the config + :param leaf: value to delete at the destination + :return: dict + """ + target = self.config + firstNoSiblingKey = None + for key in path: + if key not in target: + return self.config + if len(target[key]) <= 1: + if firstNoSiblingKey is None: + firstNoSiblingKey = [target, key] + else: + firstNoSiblingKey = None + target = target[key] + + if firstNoSiblingKey is None: + firstNoSiblingKey = [target, leaf] + + target = firstNoSiblingKey[0] + targetKey = firstNoSiblingKey[1] + del target[targetKey] + return self.config + + def check_entry(self, path, leaf): + """ + This function checks if a value exists in the config. + :param path: list of strings to traveser in the config + :param leaf: value to check for existence + :return: bool + """ + target = self.config + path = path + [leaf] + existing = [] + for key in path: + if key not in target or type(target[key]) is not dict: + return False + existing.append(key) + target = target[key] + return True + + def parse_line(self, line): + """ + This function parses a given command from string. + :param line: line to parse + :return: [command, path, leaf] + """ + line = ( + re.match(r"^('(.*)'|\"(.*)\"|([^#\"']*))*", line).group(0).strip() + ) + path = re.findall(r"('.*?'|\".*?\"|\S+)", line) + leaf = path[-1] + if leaf.startswith('"') and leaf.endswith('"'): + leaf = leaf[1:-1] + if leaf.startswith("'") and leaf.endswith("'"): + leaf = leaf[1:-1] + return [path[0], path[1:-1], leaf] + + def run_command(self, command): + """ + This function runs a given command string. + :param command: command to run + :return: dict + """ + [cmd, path, leaf] = self.parse_line(command) + if cmd.startswith("set"): + self.set_entry(path, leaf) + if cmd.startswith("del"): + self.del_entry(path, leaf) + return self.config + + def run_commands(self, commands): + """ + This function runs a a list of command strings. + :param commands: commands to run + :return: dict + """ + for c in commands: + self.run_command(c) + return self.config + + def check_command(self, command): + """ + This function checkes a command for existance in the config. + :param command: command to check + :return: bool + """ + [cmd, path, leaf] = self.parse_line(command) + if cmd.startswith("set"): + return self.check_entry(path, leaf) + if cmd.startswith("del"): + return not self.check_entry(path, leaf) + return True + + def check_commands(self, commands): + """ + This function checkes a list of commands for existance in the config. + :param commands: list of commands to check + :return: [bool] + """ + return [self.check_command(c) for c in commands] + + def build_commands(self, structure=None, nested=False): + """ + This function builds a list of commands to recreate the current configuration. + :return: [str] + """ + if type(structure) is not dict: + structure = self.config + if len(structure) == 0: + return [""] if nested else [] + commands = [] + for (key, value) in structure.items(): + for c in self.build_commands(value, True): + if " " in key or '"' in key: + key = "'" + key + "'" + commands.append((key + " " + c).strip()) + if nested: + return commands + return ["set " + c for c in commands] + + def diff_to(self, other, structure): + if type(other) is not dict: + other = {} + if len(structure) == 0: + return ([], [""]) + if type(structure) is not dict: + structure = {} + if len(other) == 0: + return ([""], []) + if len(other) == 0 and len(structure) == 0: + return ([], []) + + toset = [] + todel = [] + for key in structure.keys(): + quoted_key = "'" + key + "'" if " " in key or '"' in key else key + if key in other: + # keys in both configs, pls compare subkeys + (subset, subdel) = self.diff_to(other[key], structure[key]) + for s in subset: + toset.append(quoted_key + " " + s) + if "!" not in other[key]: + for d in subdel: + todel.append(quoted_key + " " + d) + else: + # keys only in this, pls del + todel.append(quoted_key) + continue # del + for (key, value) in other.items(): + if key == "!": + continue + quoted_key = "'" + key + "'" if " " in key or '"' in key else key + if key not in structure: + # keys only in other, pls set all subkeys + (subset, subdel) = self.diff_to(other[key], None) + for s in subset: + toset.append(quoted_key + " " + s) + + return (toset, todel) + + def diff_commands_to(self, other): + """ + This function calculates the required commands to change the current into + the given configuration. + :param other: VyosConf + :return: [str] + """ + (toset, todel) = self.diff_to(other.config, self.config) + return ["delete " + c.strip() for c in todel] + [ + "set " + c.strip() for c in toset + ] diff --git a/plugins/modules/vyos_config.py b/plugins/modules/vyos_config.py index 583ba0947..6c737f090 100644 --- a/plugins/modules/vyos_config.py +++ b/plugins/modules/vyos_config.py @@ -58,14 +58,18 @@ match: description: - The C(match) argument controls the method used to match against the current - active configuration. By default, the desired config is matched against the - active config and the deltas are loaded. If the C(match) argument is set to - C(none) the active configuration is ignored and the configuration is always - loaded. + active configuration. By default, the configuration commands config are + matched against the active config and the deltas are loaded line by line. + If the C(match) argument is set to C(none) the active configuration is ignored + and the configuration is always loaded. If the C(match) argument is set to C(smart) + both the active configuration and the target configuration are simlulated + and the results compared to bring the target device into a reliable and + reproducable state. type: str default: line choices: - line + - smart - none backup: description: @@ -139,6 +143,7 @@ - name: render a Jinja2 template onto the VyOS router vyos.vyos.vyos_config: + match: smart src: vyos_template.j2 - name: for idempotency, use full-form commands @@ -332,7 +337,7 @@ def main(): argument_spec = dict( src=dict(type="path"), lines=dict(type="list", elements="str"), - match=dict(default="line", choices=["line", "none"]), + match=dict(default="line", choices=["line", "smart", "none"]), comment=dict(default=DEFAULT_COMMENT), config=dict(), backup=dict(type="bool", default=False), diff --git a/tests/unit/cliconf/test_utils_vyosconf.py b/tests/unit/cliconf/test_utils_vyosconf.py new file mode 100644 index 000000000..34b49c38b --- /dev/null +++ b/tests/unit/cliconf/test_utils_vyosconf.py @@ -0,0 +1,160 @@ +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest +from ansible_collections.vyos.vyos.plugins.cliconf_utils.vyosconf import ( + VyosConf, +) + + +class TestListElements(unittest.TestCase): + def test_add(self): + conf = VyosConf() + conf.set_entry(["a", "b"], "c") + self.assertEqual(conf.config, {"a": {"b": {"c": {}}}}) + conf.set_entry(["a", "b"], "d") + self.assertEqual(conf.config, {"a": {"b": {"c": {}, "d": {}}}}) + conf.set_entry(["a", "c"], "b") + self.assertEqual( + conf.config, {"a": {"b": {"c": {}, "d": {}}, "c": {"b": {}}}} + ) + conf.set_entry(["a", "c", "b"], "d") + self.assertEqual( + conf.config, + {"a": {"b": {"c": {}, "d": {}}, "c": {"b": {"d": {}}}}}, + ) + + def test_del(self): + conf = VyosConf() + conf.set_entry(["a", "b"], "c") + conf.set_entry(["a", "c", "b"], "d") + conf.set_entry(["a", "b"], "d") + self.assertEqual( + conf.config, + {"a": {"b": {"c": {}, "d": {}}, "c": {"b": {"d": {}}}}}, + ) + conf.del_entry(["a", "c", "b"], "d") + self.assertEqual(conf.config, {"a": {"b": {"c": {}, "d": {}}}}) + conf.set_entry(["a", "b", "c"], "d") + conf.del_entry(["a", "b", "c"], "d") + self.assertEqual(conf.config, {"a": {"b": {"d": {}}}}) + + def test_parse(self): + conf = VyosConf() + self.assertListEqual( + conf.parse_line("set a b c"), ["set", ["a", "b"], "c"] + ) + self.assertListEqual( + conf.parse_line('set a b "c"'), ["set", ["a", "b"], "c"] + ) + self.assertListEqual( + conf.parse_line("set a b 'c d'"), ["set", ["a", "b"], "c d"] + ) + self.assertListEqual( + conf.parse_line("set a b 'c'"), ["set", ["a", "b"], "c"] + ) + self.assertListEqual( + conf.parse_line("delete a b 'c'"), ["delete", ["a", "b"], "c"] + ) + self.assertListEqual( + conf.parse_line("del a b 'c'"), ["del", ["a", "b"], "c"] + ) + self.assertListEqual( + conf.parse_line("set a b '\"c'"), ["set", ["a", "b"], '"c'] + ) + self.assertListEqual( + conf.parse_line("set a b 'c' #this is a comment"), + ["set", ["a", "b"], "c"], + ) + self.assertListEqual( + conf.parse_line("set a b '#c'"), ["set", ["a", "b"], "#c"] + ) + + def test_run_commands(self): + self.assertEqual( + VyosConf(["set a b 'c'", "set a c 'b'"]).config, + {"a": {"b": {"c": {}}, "c": {"b": {}}}}, + ) + self.assertEqual( + VyosConf(["set a b c 'd'", "set a c 'b'", "del a b c d"]).config, + {"a": {"c": {"b": {}}}}, + ) + + def test_build_commands(self): + self.assertEqual( + sorted( + VyosConf( + [ + "set a b 'c a'", + "set a c a", + "set a c b", + "delete a c a", + ] + ).build_commands() + ), + sorted(["set a b 'c a'", "set a c b"]), + ) + + def test_check_commands(self): + conf = VyosConf(["set a b 'c a'", "set a c b"]) + self.assertListEqual( + conf.check_commands( + ["set a b 'c a'", "del a c b", "set a b 'c'", "del a a a"] + ), + [True, False, False, True], + ) + + def test_diff_commands_to(self): + conf = VyosConf(["set a b 'c a'", "set a c b"]) + + self.assertListEqual( + conf.diff_commands_to(VyosConf(["set a c b"])), ["delete a b"] + ) + self.assertListEqual( + conf.diff_commands_to(VyosConf(["set a b 'c a'", "set a c b"])), [] + ) + + self.assertListEqual( + conf.diff_commands_to( + VyosConf( + [ + "set a b !", + ] + ) + ), + ["delete a c"], + ) + self.assertListEqual( + conf.diff_commands_to(VyosConf(["set a !", "set a d e"])), + ["set a d e"], + ) + self.assertListEqual( + conf.diff_commands_to(VyosConf(["set a b", "set a c b"])), + ["delete a b 'c a'"], + ) + + self.assertListEqual( + conf.diff_commands_to(VyosConf(["set a b 'a c'", "set a c b"])), + ["delete a b 'c a'", "set a b 'a c'"], + ) + + +if __name__ == "__main__": + unittest.main() From a2e75db22e8ff811cb334c96090b48954c9a6836 Mon Sep 17 00:00:00 2001 From: Luca Haneklau Date: Thu, 29 Jul 2021 21:10:42 +0200 Subject: [PATCH 2/5] vyos_config smart diffing `...` special value --- changelogs/fragments/vyos_config_improved_diffing.yml | 3 +++ plugins/cliconf_utils/vyosconf.py | 6 ++++-- plugins/modules/vyos_config.py | 8 +++++--- tests/unit/cliconf/test_utils_vyosconf.py | 4 ++-- 4 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 changelogs/fragments/vyos_config_improved_diffing.yml diff --git a/changelogs/fragments/vyos_config_improved_diffing.yml b/changelogs/fragments/vyos_config_improved_diffing.yml new file mode 100644 index 000000000..94b8a9a52 --- /dev/null +++ b/changelogs/fragments/vyos_config_improved_diffing.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - vyos_config - Added new `match`-value `smart` with updated configuration diffing for more reproducible provisioning of devices. diff --git a/plugins/cliconf_utils/vyosconf.py b/plugins/cliconf_utils/vyosconf.py index 404948ed4..d08498527 100644 --- a/plugins/cliconf_utils/vyosconf.py +++ b/plugins/cliconf_utils/vyosconf.py @@ -20,6 +20,8 @@ import re +KEEP_EXISTING_VALUES = "..." + class VyosConf: def __init__(self, commands=None): @@ -188,7 +190,7 @@ def diff_to(self, other, structure): (subset, subdel) = self.diff_to(other[key], structure[key]) for s in subset: toset.append(quoted_key + " " + s) - if "!" not in other[key]: + if KEEP_EXISTING_VALUES not in other[key]: for d in subdel: todel.append(quoted_key + " " + d) else: @@ -196,7 +198,7 @@ def diff_to(self, other, structure): todel.append(quoted_key) continue # del for (key, value) in other.items(): - if key == "!": + if key == KEEP_EXISTING_VALUES: continue quoted_key = "'" + key + "'" if " " in key or '"' in key else key if key not in structure: diff --git a/plugins/modules/vyos_config.py b/plugins/modules/vyos_config.py index 6c737f090..136b7b834 100644 --- a/plugins/modules/vyos_config.py +++ b/plugins/modules/vyos_config.py @@ -61,10 +61,12 @@ active configuration. By default, the configuration commands config are matched against the active config and the deltas are loaded line by line. If the C(match) argument is set to C(none) the active configuration is ignored - and the configuration is always loaded. If the C(match) argument is set to C(smart) - both the active configuration and the target configuration are simlulated + and the configuration is always loaded. If the C(match) argument is set to + C(smart) both the active configuration and the target configuration are simlulated and the results compared to bring the target device into a reliable and - reproducable state. + reproducable state. Using C(smart), the special value C(...) indicates that + this keys value should not be changed and any preexisting siblings should not + be removed from the target. type: str default: line choices: diff --git a/tests/unit/cliconf/test_utils_vyosconf.py b/tests/unit/cliconf/test_utils_vyosconf.py index 34b49c38b..9d3549689 100644 --- a/tests/unit/cliconf/test_utils_vyosconf.py +++ b/tests/unit/cliconf/test_utils_vyosconf.py @@ -135,14 +135,14 @@ def test_diff_commands_to(self): conf.diff_commands_to( VyosConf( [ - "set a b !", + "set a b ...", ] ) ), ["delete a c"], ) self.assertListEqual( - conf.diff_commands_to(VyosConf(["set a !", "set a d e"])), + conf.diff_commands_to(VyosConf(["set a ...", "set a d e"])), ["set a d e"], ) self.assertListEqual( From b86484278d936a689f66ebb96e5b4ff46f781b14 Mon Sep 17 00:00:00 2001 From: Luca Haneklau Date: Sat, 31 Jul 2021 15:29:59 +0200 Subject: [PATCH 3/5] fixed vyos_config smart diffing key quoting --- plugins/cliconf_utils/vyosconf.py | 25 ++++++++++++++++++----- tests/unit/cliconf/__init__.py | 0 tests/unit/cliconf/test_utils_vyosconf.py | 18 ++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 tests/unit/cliconf/__init__.py diff --git a/plugins/cliconf_utils/vyosconf.py b/plugins/cliconf_utils/vyosconf.py index d08498527..a94e74bb4 100644 --- a/plugins/cliconf_utils/vyosconf.py +++ b/plugins/cliconf_utils/vyosconf.py @@ -150,6 +150,22 @@ def check_commands(self, commands): """ return [self.check_command(c) for c in commands] + def quote_key(self, key): + """ + This function adds quotes to key if quotes are needed for correct parsing. + :param key: str to wrap in quotes if needed + :return: str + """ + if len(key) == 0: + return "" + if '"' in key: + return "'" + key + "'" + if "'" in key: + return '"' + key + '"' + if not re.match(r"^[a-zA-Z0-9./-]*$", key): + return "'" + key + "'" + return key + def build_commands(self, structure=None, nested=False): """ This function builds a list of commands to recreate the current configuration. @@ -161,10 +177,9 @@ def build_commands(self, structure=None, nested=False): return [""] if nested else [] commands = [] for (key, value) in structure.items(): + quoted_key = self.quote_key(key) for c in self.build_commands(value, True): - if " " in key or '"' in key: - key = "'" + key + "'" - commands.append((key + " " + c).strip()) + commands.append((quoted_key + " " + c).strip()) if nested: return commands return ["set " + c for c in commands] @@ -184,7 +199,7 @@ def diff_to(self, other, structure): toset = [] todel = [] for key in structure.keys(): - quoted_key = "'" + key + "'" if " " in key or '"' in key else key + quoted_key = self.quote_key(key) if key in other: # keys in both configs, pls compare subkeys (subset, subdel) = self.diff_to(other[key], structure[key]) @@ -200,7 +215,7 @@ def diff_to(self, other, structure): for (key, value) in other.items(): if key == KEEP_EXISTING_VALUES: continue - quoted_key = "'" + key + "'" if " " in key or '"' in key else key + quoted_key = self.quote_key(key) if key not in structure: # keys only in other, pls set all subkeys (subset, subdel) = self.diff_to(other[key], None) diff --git a/tests/unit/cliconf/__init__.py b/tests/unit/cliconf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/cliconf/test_utils_vyosconf.py b/tests/unit/cliconf/test_utils_vyosconf.py index 9d3549689..bc533012f 100644 --- a/tests/unit/cliconf/test_utils_vyosconf.py +++ b/tests/unit/cliconf/test_utils_vyosconf.py @@ -111,6 +111,24 @@ def test_build_commands(self): ), sorted(["set a b 'c a'", "set a c b"]), ) + self.assertEqual( + sorted( + VyosConf( + [ + "set a b 10.0.0.1/24", + "set a c ABCabc123+/=", + "set a d $6$ABC.abc.123.+./=..", + ] + ).build_commands() + ), + sorted( + [ + "set a b 10.0.0.1/24", + "set a c 'ABCabc123+/='", + "set a d '$6$ABC.abc.123.+./=..'", + ] + ), + ) def test_check_commands(self): conf = VyosConf(["set a b 'c a'", "set a c b"]) From e988518e14b47cea9a7b5c4ab6a115feb390c5fc Mon Sep 17 00:00:00 2001 From: Luca Haneklau Date: Sun, 1 Aug 2021 20:11:13 +0200 Subject: [PATCH 4/5] fixed vyos_config smart diffing `...` incorrectly applying to children --- plugins/cliconf_utils/vyosconf.py | 10 +++++----- tests/unit/cliconf/test_utils_vyosconf.py | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/plugins/cliconf_utils/vyosconf.py b/plugins/cliconf_utils/vyosconf.py index a94e74bb4..21c5872e0 100644 --- a/plugins/cliconf_utils/vyosconf.py +++ b/plugins/cliconf_utils/vyosconf.py @@ -205,12 +205,12 @@ def diff_to(self, other, structure): (subset, subdel) = self.diff_to(other[key], structure[key]) for s in subset: toset.append(quoted_key + " " + s) - if KEEP_EXISTING_VALUES not in other[key]: - for d in subdel: - todel.append(quoted_key + " " + d) + for d in subdel: + todel.append(quoted_key + " " + d) else: - # keys only in this, pls del - todel.append(quoted_key) + # keys only in this, delete if KEEP_EXISTING_VALUES not set + if KEEP_EXISTING_VALUES not in other: + todel.append(quoted_key) continue # del for (key, value) in other.items(): if key == KEEP_EXISTING_VALUES: diff --git a/tests/unit/cliconf/test_utils_vyosconf.py b/tests/unit/cliconf/test_utils_vyosconf.py index bc533012f..a568649a1 100644 --- a/tests/unit/cliconf/test_utils_vyosconf.py +++ b/tests/unit/cliconf/test_utils_vyosconf.py @@ -173,6 +173,13 @@ def test_diff_commands_to(self): ["delete a b 'c a'", "set a b 'a c'"], ) + self.assertListEqual( + VyosConf( + ["set a b c d", "set a b c e", "set a b d"] + ).diff_commands_to(VyosConf(["set a b c d", "set a b ..."])), + ["delete a b c e"], + ) + if __name__ == "__main__": unittest.main() From cdd8ec0886cda5a5d9c8dfda078a06d61bdb4368 Mon Sep 17 00:00:00 2001 From: Luca Haneklau Date: Mon, 2 Aug 2021 16:41:43 +0200 Subject: [PATCH 5/5] improved vyos_config smart diffing documentation --- plugins/modules/vyos_config.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/modules/vyos_config.py b/plugins/modules/vyos_config.py index 136b7b834..9d4e7e205 100644 --- a/plugins/modules/vyos_config.py +++ b/plugins/modules/vyos_config.py @@ -62,11 +62,10 @@ matched against the active config and the deltas are loaded line by line. If the C(match) argument is set to C(none) the active configuration is ignored and the configuration is always loaded. If the C(match) argument is set to - C(smart) both the active configuration and the target configuration are simlulated - and the results compared to bring the target device into a reliable and - reproducable state. Using C(smart), the special value C(...) indicates that - this keys value should not be changed and any preexisting siblings should not - be removed from the target. + C(smart) the active configuration and the target configuration are compared + and differences are added to or removed from the target. Using C(smart), the + special value C(...) indicates that this value should not be changed and its + siblings should not be removed from the target. type: str default: line choices: