forked from vyos/vyos.vyos
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
vyos_config improved diffing (vyos#179)
- Loading branch information
Showing
5 changed files
with
402 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
#!/usr/bin/python | ||
# | ||
# 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 VyosConf: | ||
def __init__(self, commands=[]): | ||
self.config = {} | ||
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 | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.