summaryrefslogtreecommitdiff
path: root/plugins/cliconf_utils
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/cliconf_utils')
-rw-r--r--plugins/cliconf_utils/__init__.py0
-rw-r--r--plugins/cliconf_utils/vyosconf.py257
2 files changed, 257 insertions, 0 deletions
diff --git a/plugins/cliconf_utils/__init__.py b/plugins/cliconf_utils/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/cliconf_utils/__init__.py
diff --git a/plugins/cliconf_utils/vyosconf.py b/plugins/cliconf_utils/vyosconf.py
new file mode 100644
index 00000000..a3ed6887
--- /dev/null
+++ b/plugins/cliconf_utils/vyosconf.py
@@ -0,0 +1,257 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import re
+
+
+class _KeepExistingSentinel:
+ """Unique marker for 'preserve whatever's already here' in a diff.
+ Deliberately not a plain string/value: a real config leaf could
+ legitimately be "..." (e.g. a description), and a string sentinel
+ would collide with it. An object identity never can.
+ """
+
+ def __repr__(self):
+ return "<keep-existing>"
+
+
+KEEP_EXISTING_VALUES = _KeepExistingSentinel()
+
+
+class VyosConf:
+ def __init__(self, commands=None):
+ self.config = {}
+ if isinstance(commands, 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 traverse 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 not isinstance(target[key], 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. If the leaf
+ does not exist at the given path, the configuration is left
+ unchanged (delete is treated as a no-op, matching VyOS's own
+ behaviour when deleting a path that isn't set).
+ :param path: list of strings to traverse in the config
+ :param leaf: value to delete at the destination
+ :return: dict
+ """
+ target = self.config
+ first_no_sibling_key = None
+ for key in path:
+ if key not in target:
+ return self.config
+ if len(target[key]) <= 1:
+ if first_no_sibling_key is None:
+ first_no_sibling_key = [target, key]
+ else:
+ first_no_sibling_key = None
+ target = target[key]
+
+ if leaf not in target:
+ return self.config
+
+ if first_no_sibling_key is None:
+ first_no_sibling_key = [target, leaf]
+
+ target = first_no_sibling_key[0]
+ target_key = first_no_sibling_key[1]
+ del target[target_key]
+ 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 traverse in the config
+ :param leaf: value to check for existence
+ :return: bool
+ """
+ target = self.config
+ path = path + [leaf]
+ for key in path:
+ if key not in target or not isinstance(target[key], dict):
+ return False
+ 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()
+ if not line:
+ return ["", [], ""]
+ path = re.findall(r"('.*?'|\".*?\"|\S+)", line)
+ if not path:
+ return ["", [], ""]
+ 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 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 checks a command for existence 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 checks a list of commands for existence in the config.
+ :param commands: list of commands to check
+ :return: [bool]
+ """
+ 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.
+ :return: [str]
+ """
+ if not isinstance(structure, dict):
+ structure = self.config
+ if len(structure) == 0:
+ 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):
+ commands.append((quoted_key + " " + c).strip())
+ if nested:
+ return commands
+ return ["set " + c for c in commands]
+
+ def diff_to(self, other, structure):
+ if not isinstance(other, dict):
+ other = {}
+ if len(structure) == 0:
+ return ([], [""])
+ if not isinstance(structure, 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 = self.quote_key(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)
+ for d in subdel:
+ todel.append(quoted_key + " " + d)
+ else:
+ # 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:
+ continue
+ 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)
+ 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. Only top-level sections present in the desired
+ configuration are enforced; top-level sections the candidate does not
+ mention at all are left completely untouched.
+ :param other: VyosConf
+ :return: [str]
+ """
+ scoped_structure = {k: v for k, v in self.config.items() if k in other.config}
+ (toset, todel) = self.diff_to(other.config, scoped_structure)
+ return ["delete " + c.strip() for c in todel] + ["set " + c.strip() for c in toset]