# # 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 _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_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]