Skip to content

Commit 14b0ce8

Browse files
committed
vyos_config improved diffing (vyos#179)
1 parent 36004b2 commit 14b0ce8

File tree

5 files changed

+410
-5
lines changed

5 files changed

+410
-5
lines changed

plugins/cliconf/vyos.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
to_list,
5656
)
5757
from ansible.plugins.cliconf import CliconfBase
58+
from ansible_collections.vyos.vyos.plugins.cliconf_utils.vyosconf import VyosConf
5859

5960

6061
class Cliconf(CliconfBase):
@@ -263,6 +264,12 @@ def get_diff(
263264
diff["config_diff"] = list(candidate_commands)
264265
return diff
265266

267+
if diff_match == "smart":
268+
running_conf = VyosConf(running.splitlines())
269+
candidate_conf = VyosConf(candidate_commands)
270+
diff["config_diff"] = running_conf.diff_commands_to(candidate_conf)
271+
return diff
272+
266273
running_commands = [
267274
str(c).replace("'", "") for c in running.splitlines()
268275
]

plugins/cliconf_utils/__init__.py

Whitespace-only changes.

plugins/cliconf_utils/vyosconf.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
#!/usr/bin/python
2+
#
3+
# This file is part of Ansible
4+
#
5+
# Ansible is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# Ansible is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
#from __future__ import absolute_import, division, print_function
19+
20+
#__metaclass__ = type
21+
22+
import re
23+
24+
class VyosConf:
25+
def __init__(self, commands=[]):
26+
self.config = {}
27+
self.run_commands(commands)
28+
29+
def set_entry(self, path, leaf):
30+
"""
31+
This function sets a value in the configuration given a path.
32+
:param path: list of strings to traveser in the config
33+
:param leaf: value to set at the destination
34+
:return: dict
35+
"""
36+
target = self.config
37+
path = path + [leaf]
38+
for key in path:
39+
if not key in target or type(target[key]) is not dict:
40+
target[key] = {}
41+
target = target[key]
42+
return self.config
43+
44+
def del_entry(self, path, leaf):
45+
"""
46+
This function deletes a value from the configuration given a path
47+
and also removes all the parents that are now empty.
48+
:param path: list of strings to traveser in the config
49+
:param leaf: value to delete at the destination
50+
:return: dict
51+
"""
52+
target = self.config
53+
firstNoSiblingKey = None
54+
for key in path:
55+
if not key in target:
56+
return self.config
57+
if len(target[key]) <= 1:
58+
if firstNoSiblingKey is None:
59+
firstNoSiblingKey = [target, key]
60+
else:
61+
firstNoSiblingKey = None
62+
target = target[key]
63+
64+
if firstNoSiblingKey is None:
65+
firstNoSiblingKey = [target, leaf]
66+
67+
target = firstNoSiblingKey[0]
68+
targetKey = firstNoSiblingKey[1]
69+
del target[targetKey]
70+
return self.config
71+
72+
def check_entry(self, path, leaf):
73+
"""
74+
This function checks if a value exists in the config.
75+
:param path: list of strings to traveser in the config
76+
:param leaf: value to check for existence
77+
:return: bool
78+
"""
79+
target = self.config
80+
path = path + [leaf]
81+
existing = []
82+
for key in path:
83+
if not key in target or type(target[key]) is not dict:
84+
return False
85+
existing.append(key)
86+
target = target[key]
87+
return True;
88+
89+
def parse_line(self, line):
90+
"""
91+
This function parses a given command from string.
92+
:param line: line to parse
93+
:return: [command, path, leaf]
94+
"""
95+
line = re.match(r"^('(.*)'|\"(.*)\"|([^#\"']*))*", line).group(0).strip()
96+
path = re.findall(r"('.*?'|\".*?\"|\S+)", line)
97+
leaf = path[-1]
98+
if leaf.startswith('"') and leaf.endswith('"'):
99+
leaf = leaf[1:-1]
100+
if leaf.startswith("'") and leaf.endswith("'"):
101+
leaf = leaf[1:-1]
102+
return [path[0], path[1:-1], leaf]
103+
104+
def run_command(self, command):
105+
"""
106+
This function runs a given command string.
107+
:param command: command to run
108+
:return: dict
109+
"""
110+
[cmd, path, leaf] = self.parse_line(command)
111+
if cmd.startswith('set'): self.set_entry(path, leaf)
112+
if cmd.startswith('del'): self.del_entry(path, leaf)
113+
return self.config
114+
115+
def run_commands(self, commands):
116+
"""
117+
This function runs a a list of command strings.
118+
:param commands: commands to run
119+
:return: dict
120+
"""
121+
for c in commands:
122+
self.run_command(c)
123+
return self.config
124+
125+
def check_command(self, command):
126+
"""
127+
This function checkes a command for existance in the config.
128+
:param command: command to check
129+
:return: bool
130+
"""
131+
[cmd, path, leaf] = self.parse_line(command)
132+
if cmd.startswith('set'): return self.check_entry(path, leaf)
133+
if cmd.startswith('del'): return not self.check_entry(path, leaf)
134+
return True
135+
136+
def check_commands(self, commands):
137+
"""
138+
This function checkes a list of commands for existance in the config.
139+
:param commands: list of commands to check
140+
:return: [bool]
141+
"""
142+
return [self.check_command(c) for c in commands]
143+
144+
def build_commands(self, structure = None, nested = False):
145+
"""
146+
This function builds a list of commands to recreate the current configuration.
147+
:return: [str]
148+
"""
149+
if type(structure) is not dict: structure = self.config
150+
if len(structure) == 0:
151+
return [''] if nested else []
152+
commands = []
153+
for (key, value) in structure.items():
154+
for c in self.build_commands(value, True):
155+
if ' ' in key or '"' in key:
156+
key = "'"+key+"'"
157+
commands.append((key+' '+c).strip())
158+
if nested:
159+
return commands
160+
return ['set '+c for c in commands]
161+
162+
def diff_to(self, other, structure):
163+
if type(other) is not dict:
164+
other = {}
165+
if len(structure) == 0:
166+
return ([], [''])
167+
if type(structure) is not dict:
168+
structure = {}
169+
if len(other) == 0:
170+
return ([''], [])
171+
if len(other) == 0 and len(structure) == 0:
172+
return ([], [])
173+
174+
toset = []
175+
todel = []
176+
for key in structure.keys():
177+
quoted_key = "'"+key+"'" if ' ' in key or '"' in key else key
178+
if key in other:
179+
# keys in both configs, pls compare subkeys
180+
(subset, subdel) = self.diff_to(other[key], structure[key])
181+
for s in subset:
182+
toset.append(quoted_key+' '+s)
183+
if '!' not in other[key]:
184+
for d in subdel:
185+
todel.append(quoted_key+' '+d)
186+
else:
187+
# keys only in this, pls del
188+
todel.append(quoted_key)
189+
continue # del
190+
for (key, value) in other.items():
191+
if key == '!':
192+
continue
193+
quoted_key = "'"+key+"'" if ' ' in key or '"' in key else key
194+
if key not in structure:
195+
# keys only in other, pls set all subkeys
196+
(subset, subdel) = self.diff_to(other[key], None)
197+
for s in subset:
198+
toset.append(quoted_key+' '+s)
199+
200+
return (toset, todel)
201+
202+
def diff_commands_to(self, other):
203+
"""
204+
This function calculates the required commands to change the current into
205+
the given configuration.
206+
:param other: VyosConf
207+
:return: [str]
208+
"""
209+
(toset, todel) = self.diff_to(other.config, self.config)
210+
return ['delete '+c.strip() for c in todel] + ['set '+c.strip() for c in toset]

plugins/modules/vyos_config.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,18 @@
5858
match:
5959
description:
6060
- The C(match) argument controls the method used to match against the current
61-
active configuration. By default, the desired config is matched against the
62-
active config and the deltas are loaded. If the C(match) argument is set to
63-
C(none) the active configuration is ignored and the configuration is always
64-
loaded.
61+
active configuration. By default, the configuration commands config are
62+
matched against the active config and the deltas are loaded line by line.
63+
If the C(match) argument is set to C(none) the active configuration is ignored
64+
and the configuration is always loaded. If the C(match) argument is set to C(smart)
65+
both the active configuration and the target configuration are simlulated
66+
and the results compared to bring the target device into a reliable and
67+
reproducable state.
6568
type: str
6669
default: line
6770
choices:
6871
- line
72+
- smart
6973
- none
7074
backup:
7175
description:
@@ -139,6 +143,7 @@
139143
140144
- name: render a Jinja2 template onto the VyOS router
141145
vyos.vyos.vyos_config:
146+
match: smart
142147
src: vyos_template.j2
143148
144149
- name: for idempotency, use full-form commands
@@ -211,7 +216,7 @@
211216
DEFAULT_COMMENT = "configured by vyos_config"
212217

213218
CONFIG_FILTERS = [
214-
re.compile(r"set system login user \S+ authentication encrypted-password")
219+
re.compile(r"set system login user \S+ authentication encrypted-password !")
215220
]
216221

217222

0 commit comments

Comments
 (0)