Skip to content
2 changes: 2 additions & 0 deletions docs/vyos.vyos.vyos_config_module.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ Parameters
<td>
<ul style="margin: 0; padding: 0"><b>Choices:</b>
<li><div style="color: blue"><b>line</b>&nbsp;&larr;</div></li>
<li>smart</li>
<li>none</li>
</ul>
</td>
Expand Down Expand Up @@ -234,6 +235,7 @@ Examples

- 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
Expand Down
11 changes: 9 additions & 2 deletions plugins/cliconf/vyos.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@
from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils._text import to_text
from ansible.module_utils.common._collections_compat import Mapping
from ansible.plugins.cliconf import CliconfBase
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import (
NetworkConfig,
)
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list
from ansible_collections.ansible.netcommon.plugins.plugin_utils.cliconf_base import CliconfBase

from ansible_collections.vyos.vyos.plugins.cliconf_utils.vyosconf import VyosConf


class Cliconf(CliconfBase):
Expand Down Expand Up @@ -256,6 +258,11 @@ def get_diff(
if diff_match == "none":
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()]

Expand Down Expand Up @@ -328,7 +335,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": [],
}
Expand Down
Empty file.
235 changes: 235 additions & 0 deletions plugins/cliconf_utils/vyosconf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
#
# 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


KEEP_EXISTING_VALUES = "..."


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 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 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.
: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 not isinstance(target[key], 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 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.
: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]
4 changes: 3 additions & 1 deletion plugins/modules/vyos_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
default: line
choices:
- line
- smart
- none
backup:
description:
Expand Down Expand Up @@ -140,6 +141,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
Expand Down Expand Up @@ -331,7 +333,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),
Expand Down
Empty file.
Loading
Loading