diff --git a/plugins/cliconf_utils/vyosconf.py b/plugins/cliconf_utils/vyosconf.py
index d0849852..a94e74bb 100644
--- a/plugins/cliconf_utils/vyosconf.py
+++ b/plugins/cliconf_utils/vyosconf.py
@@ -1,222 +1,237 @@
#
# 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
KEEP_EXISTING_VALUES = "..."
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 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 type(structure) is not 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):
- 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]
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
+ 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)
if KEEP_EXISTING_VALUES 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 == 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)
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/tests/unit/cliconf/__init__.py b/tests/unit/cliconf/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/unit/cliconf/test_utils_vyosconf.py b/tests/unit/cliconf/test_utils_vyosconf.py
index 9d354968..bc533012 100644
--- a/tests/unit/cliconf/test_utils_vyosconf.py
+++ b/tests/unit/cliconf/test_utils_vyosconf.py
@@ -1,160 +1,178 @@
#
# 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"]),
)
+ 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"])
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()