mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
vyos and ios cliconf plugin refactor (#41846)
* vyos and ios cliconf plugin refactor * Refactor vyos cliconf plugin * Change vyos module_utils and vyos_config as per refactor * Minor changes in ios cliconf plugin * Fix unit test failure * Fix sanity issues * Add get_diff to rpc list
This commit is contained in:
parent
9c5d40ff15
commit
773c031d33
8 changed files with 285 additions and 101 deletions
|
@ -166,6 +166,7 @@ def load_config(module, commands):
|
||||||
connection = get_connection(module)
|
connection = get_connection(module)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return connection.edit_config(commands)
|
diff, response = connection.edit_config(commands)
|
||||||
|
return response
|
||||||
except ConnectionError as exc:
|
except ConnectionError as exc:
|
||||||
module.fail_json(msg=to_text(exc))
|
module.fail_json(msg=to_text(exc))
|
||||||
|
|
|
@ -133,29 +133,8 @@ def load_config(module, commands, commit=False, comment=None):
|
||||||
connection = get_connection(module)
|
connection = get_connection(module)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
out = connection.edit_config(commands)
|
diff_config, resp = connection.edit_config(candidate=commands, commit=commit, diff=module._diff, comment=comment)
|
||||||
except ConnectionError as exc:
|
except ConnectionError as exc:
|
||||||
module.fail_json(msg=to_text(exc))
|
module.fail_json(msg=to_text(exc))
|
||||||
|
|
||||||
diff = None
|
return diff_config
|
||||||
if module._diff:
|
|
||||||
out = connection.get('compare')
|
|
||||||
out = to_text(out, errors='surrogate_or_strict')
|
|
||||||
|
|
||||||
if not out.startswith('No changes'):
|
|
||||||
out = connection.get('show')
|
|
||||||
diff = to_text(out, errors='surrogate_or_strict').strip()
|
|
||||||
|
|
||||||
if commit:
|
|
||||||
try:
|
|
||||||
out = connection.commit(comment)
|
|
||||||
except ConnectionError:
|
|
||||||
connection.discard_changes()
|
|
||||||
module.fail_json(msg='commit failed: %s' % out)
|
|
||||||
else:
|
|
||||||
connection.get('exit')
|
|
||||||
else:
|
|
||||||
connection.discard_changes()
|
|
||||||
|
|
||||||
if diff:
|
|
||||||
return diff
|
|
||||||
|
|
|
@ -441,9 +441,9 @@ def main():
|
||||||
# them with the current running config
|
# them with the current running config
|
||||||
if not module.check_mode:
|
if not module.check_mode:
|
||||||
if commands:
|
if commands:
|
||||||
connection.edit_config(commands)
|
connection.edit_config(candidate=commands)
|
||||||
if banner_diff:
|
if banner_diff:
|
||||||
connection.edit_banner(json.dumps(banner_diff), multiline_delimiter=module.params['multiline_delimiter'])
|
connection.edit_banner(candidate=json.dumps(banner_diff), multiline_delimiter=module.params['multiline_delimiter'])
|
||||||
|
|
||||||
result['changed'] = True
|
result['changed'] = True
|
||||||
|
|
||||||
|
|
|
@ -130,11 +130,11 @@ backup_path:
|
||||||
sample: /playbooks/ansible/backup/vyos_config.2016-07-16@22:28:34
|
sample: /playbooks/ansible/backup/vyos_config.2016-07-16@22:28:34
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
from ansible.module_utils.network.common.config import NetworkConfig
|
|
||||||
from ansible.module_utils.network.vyos.vyos import load_config, get_config, run_commands
|
from ansible.module_utils.network.vyos.vyos import load_config, get_config, run_commands
|
||||||
from ansible.module_utils.network.vyos.vyos import vyos_argument_spec
|
from ansible.module_utils.network.vyos.vyos import vyos_argument_spec, get_connection
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_COMMENT = 'configured by vyos_config'
|
DEFAULT_COMMENT = 'configured by vyos_config'
|
||||||
|
@ -144,35 +144,13 @@ CONFIG_FILTERS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def config_to_commands(config):
|
|
||||||
set_format = config.startswith('set') or config.startswith('delete')
|
|
||||||
candidate = NetworkConfig(indent=4, contents=config)
|
|
||||||
if not set_format:
|
|
||||||
candidate = [c.line for c in candidate.items]
|
|
||||||
commands = list()
|
|
||||||
# this filters out less specific lines
|
|
||||||
for item in candidate:
|
|
||||||
for index, entry in enumerate(commands):
|
|
||||||
if item.startswith(entry):
|
|
||||||
del commands[index]
|
|
||||||
break
|
|
||||||
commands.append(item)
|
|
||||||
|
|
||||||
commands = ['set %s' % cmd.replace(' {', '') for cmd in commands]
|
|
||||||
|
|
||||||
else:
|
|
||||||
commands = str(candidate).split('\n')
|
|
||||||
|
|
||||||
return commands
|
|
||||||
|
|
||||||
|
|
||||||
def get_candidate(module):
|
def get_candidate(module):
|
||||||
contents = module.params['src'] or module.params['lines']
|
contents = module.params['src'] or module.params['lines']
|
||||||
|
|
||||||
if module.params['lines']:
|
if module.params['lines']:
|
||||||
contents = '\n'.join(contents)
|
contents = '\n'.join(contents)
|
||||||
|
|
||||||
return config_to_commands(contents)
|
return contents
|
||||||
|
|
||||||
|
|
||||||
def diff_config(commands, config):
|
def diff_config(commands, config):
|
||||||
|
@ -225,7 +203,10 @@ def run(module, result):
|
||||||
candidate = get_candidate(module)
|
candidate = get_candidate(module)
|
||||||
|
|
||||||
# create loadable config that includes only the configuration updates
|
# create loadable config that includes only the configuration updates
|
||||||
commands = diff_config(candidate, config)
|
connection = get_connection(module)
|
||||||
|
response = connection.get_diff(candidate=candidate, running=config, match=module.params['match'])
|
||||||
|
diff_obj = json.loads(response)
|
||||||
|
commands = diff_obj.get('config_diff')
|
||||||
sanitize_config(commands, result)
|
sanitize_config(commands, result)
|
||||||
|
|
||||||
result['commands'] = commands
|
result['commands'] = commands
|
||||||
|
@ -233,8 +214,9 @@ def run(module, result):
|
||||||
commit = not module.check_mode
|
commit = not module.check_mode
|
||||||
comment = module.params['comment']
|
comment = module.params['comment']
|
||||||
|
|
||||||
|
diff = None
|
||||||
if commands:
|
if commands:
|
||||||
load_config(module, commands, commit=commit, comment=comment)
|
diff = load_config(module, commands, commit=commit, comment=comment)
|
||||||
|
|
||||||
if result.get('filtered'):
|
if result.get('filtered'):
|
||||||
result['warnings'].append('Some configuration commands were '
|
result['warnings'].append('Some configuration commands were '
|
||||||
|
@ -242,6 +224,9 @@ def run(module, result):
|
||||||
|
|
||||||
result['changed'] = True
|
result['changed'] = True
|
||||||
|
|
||||||
|
if module._diff:
|
||||||
|
result['diff'] = diff
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
argument_spec = dict(
|
argument_spec = dict(
|
||||||
|
|
|
@ -93,7 +93,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
||||||
display.display('closing shell due to command timeout (%s seconds).' % self._connection._play_context.timeout, log_only=True)
|
display.display('closing shell due to command timeout (%s seconds).' % self._connection._play_context.timeout, log_only=True)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def send_command(self, command, prompt=None, answer=None, sendonly=False, newline=True, prompt_retry_check=False):
|
def send_command(self, command=None, prompt=None, answer=None, sendonly=False, newline=True, prompt_retry_check=False):
|
||||||
"""Executes a command over the device connection
|
"""Executes a command over the device connection
|
||||||
|
|
||||||
This method will execute a command over the device connection and
|
This method will execute a command over the device connection and
|
||||||
|
@ -184,7 +184,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def edit_config(self, candidate, check_mode=False, replace=None):
|
def edit_config(self, candidate=None, commit=True, replace=False, diff=False, comment=None):
|
||||||
"""Loads the candidate configuration into the network device
|
"""Loads the candidate configuration into the network device
|
||||||
|
|
||||||
This method will load the specified candidate config into the device
|
This method will load the specified candidate config into the device
|
||||||
|
@ -195,20 +195,22 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
||||||
:param candidate: The configuration to load into the device and merge
|
:param candidate: The configuration to load into the device and merge
|
||||||
with the current running configuration
|
with the current running configuration
|
||||||
|
|
||||||
:param check_mode: Boolean value that indicates if the device candidate
|
:param commit: Boolean value that indicates if the device candidate
|
||||||
configuration should be pushed in the running configuration or discarded.
|
configuration should be pushed in the running configuration or discarded.
|
||||||
|
|
||||||
:param replace: Specifies the way in which provided config value should replace
|
:param replace: Boolean flag to indicate if running configuration should be completely
|
||||||
the configuration running on the remote device. If the device
|
replace by candidate configuration.
|
||||||
doesn't support config replace, an error is return.
|
:param diff: Boolean flag to indicate if configuration that is applied on remote host should
|
||||||
|
generated and returned in response or not
|
||||||
:return: Returns response of executing the configuration command received
|
:param comment: Commit comment provided it is supported by remote host
|
||||||
from remote host
|
:return: Returns a tuple, the first entry of tupe is configuration diff if diff flag is enable else
|
||||||
|
it is None. Second entry is the list of response received from remote host on executing
|
||||||
|
configuration commands.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True):
|
def get(self, command=None, prompt=None, answer=None, sendonly=False, newline=True):
|
||||||
"""Execute specified command on remote device
|
"""Execute specified command on remote device
|
||||||
This method will retrieve the specified data and
|
This method will retrieve the specified data and
|
||||||
return it to the caller as a string.
|
return it to the caller as a string.
|
||||||
|
@ -242,7 +244,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
||||||
'network_os_platform': <str>,
|
'network_os_platform': <str>,
|
||||||
},
|
},
|
||||||
'device_operations': {
|
'device_operations': {
|
||||||
'supports_replace': <bool>, # identify if config should be merged or replaced is supported
|
'supports_diff_replace': <bool>, # identify if config should be merged or replaced is supported
|
||||||
'supports_commit': <bool>, # identify if commit is supported by device or not
|
'supports_commit': <bool>, # identify if commit is supported by device or not
|
||||||
'supports_rollback': <bool>, # identify if rollback is supported or not
|
'supports_rollback': <bool>, # identify if rollback is supported or not
|
||||||
'supports_defaults': <bool>, # identify if fetching running config with default is supported
|
'supports_defaults': <bool>, # identify if fetching running config with default is supported
|
||||||
|
@ -250,12 +252,13 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
||||||
'supports_onbox_diff: <bool>, # identify if on box diff capability is supported or not
|
'supports_onbox_diff: <bool>, # identify if on box diff capability is supported or not
|
||||||
'supports_generate_diff: <bool>, # identify if diff capability is supported within plugin
|
'supports_generate_diff: <bool>, # identify if diff capability is supported within plugin
|
||||||
'supports_multiline_delimiter: <bool>, # identify if multiline demiliter is supported within config
|
'supports_multiline_delimiter: <bool>, # identify if multiline demiliter is supported within config
|
||||||
'support_match: <bool>, # identify if match is supported
|
'support_diff_match: <bool>, # identify if match is supported
|
||||||
'support_diff_ignore_lines: <bool>, # identify if ignore line in diff is supported
|
'support_diff_ignore_lines: <bool>, # identify if ignore line in diff is supported
|
||||||
|
'support_config_replace': <bool>, # identify if running config replace with candidate config is supported
|
||||||
}
|
}
|
||||||
'format': [list of supported configuration format],
|
'format': [list of supported configuration format],
|
||||||
'match': ['line', 'strict', 'exact', 'none'],
|
'diff_match': [list of supported match values],
|
||||||
'replace': ['line', 'block', 'config'],
|
'diff_replace': [list of supported replace values],
|
||||||
}
|
}
|
||||||
:return: capability as json string
|
:return: capability as json string
|
||||||
"""
|
"""
|
||||||
|
@ -326,3 +329,38 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
||||||
elif proto == 'sftp':
|
elif proto == 'sftp':
|
||||||
with ssh.open_sftp() as sftp:
|
with ssh.open_sftp() as sftp:
|
||||||
sftp.get(source, destination)
|
sftp.get(source, destination)
|
||||||
|
|
||||||
|
def get_diff(self, candidate=None, running=None, match=None, diff_ignore_lines=None, path=None, replace=None):
|
||||||
|
"""
|
||||||
|
Generate diff between candidate and running configuration. If the
|
||||||
|
remote host supports onbox diff capabilities ie. supports_onbox_diff in that case
|
||||||
|
candidate and running configurations are not required to be passed as argument.
|
||||||
|
In case if onbox diff capability is not supported candidate argument is mandatory
|
||||||
|
and running argument is optional.
|
||||||
|
:param candidate: The configuration which is expected to be present on remote host.
|
||||||
|
:param running: The base configuration which is used to generate diff.
|
||||||
|
:param match: Instructs how to match the candidate configuration with current device configuration
|
||||||
|
Valid values are 'line', 'strict', 'exact', 'none'.
|
||||||
|
'line' - commands are matched line by line
|
||||||
|
'strict' - command lines are matched with respect to position
|
||||||
|
'exact' - command lines must be an equal match
|
||||||
|
'none' - will not compare the candidate configuration with the running configuration
|
||||||
|
:param diff_ignore_lines: Use this argument to specify one or more lines that should be
|
||||||
|
ignored during the diff. This is used for lines in the configuration
|
||||||
|
that are automatically updated by the system. This argument takes
|
||||||
|
a list of regular expressions or exact line matches.
|
||||||
|
:param path: The ordered set of parents that uniquely identify the section or hierarchy
|
||||||
|
the commands should be checked against. If the parents argument
|
||||||
|
is omitted, the commands are checked against the set of top
|
||||||
|
level or global commands.
|
||||||
|
:param replace: Instructs on the way to perform the configuration on the device.
|
||||||
|
If the replace argument is set to I(line) then the modified lines are
|
||||||
|
pushed to the device in configuration mode. If the replace argument is
|
||||||
|
set to I(block) then the entire command block is pushed to the device in
|
||||||
|
configuration mode if any line is not correct.
|
||||||
|
:return: Configuration and/or banner diff in json format.
|
||||||
|
{
|
||||||
|
'config_diff': ''
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
|
@ -66,8 +66,7 @@ class Cliconf(CliconfBase):
|
||||||
'line' - commands are matched line by line
|
'line' - commands are matched line by line
|
||||||
'strict' - command lines are matched with respect to position
|
'strict' - command lines are matched with respect to position
|
||||||
'exact' - command lines must be an equal match
|
'exact' - command lines must be an equal match
|
||||||
'none' - will not compare the candidate configuration with
|
'none' - will not compare the candidate configuration with the running configuration
|
||||||
the running configuration on the remote device
|
|
||||||
:param diff_ignore_lines: Use this argument to specify one or more lines that should be
|
:param diff_ignore_lines: Use this argument to specify one or more lines that should be
|
||||||
ignored during the diff. This is used for lines in the configuration
|
ignored during the diff. This is used for lines in the configuration
|
||||||
that are automatically updated by the system. This argument takes
|
that are automatically updated by the system. This argument takes
|
||||||
|
@ -90,9 +89,16 @@ class Cliconf(CliconfBase):
|
||||||
"""
|
"""
|
||||||
diff = {}
|
diff = {}
|
||||||
device_operations = self.get_device_operations()
|
device_operations = self.get_device_operations()
|
||||||
|
option_values = self.get_option_values()
|
||||||
|
|
||||||
if candidate is None and not device_operations['supports_onbox_diff']:
|
if candidate is None and not device_operations['supports_onbox_diff']:
|
||||||
raise ValueError('candidate configuration is required to generate diff')
|
raise ValueError("candidate configuration is required to generate diff")
|
||||||
|
|
||||||
|
if match not in option_values['diff_match']:
|
||||||
|
raise ValueError("'match' value %s in invalid, valid values are %s" % (match, option_values['diff_match']))
|
||||||
|
|
||||||
|
if replace not in option_values['diff_replace']:
|
||||||
|
raise ValueError("'replace' value %s in invalid, valid values are %s" % (replace, option_values['diff_replace']))
|
||||||
|
|
||||||
# prepare candidate configuration
|
# prepare candidate configuration
|
||||||
candidate_obj = NetworkConfig(indent=1)
|
candidate_obj = NetworkConfig(indent=1)
|
||||||
|
@ -118,19 +124,22 @@ class Cliconf(CliconfBase):
|
||||||
return json.dumps(diff)
|
return json.dumps(diff)
|
||||||
|
|
||||||
@enable_mode
|
@enable_mode
|
||||||
def edit_config(self, candidate, check_mode=False, replace=None):
|
def edit_config(self, candidate=None, commit=True, replace=False, diff=False, comment=None):
|
||||||
if not candidate:
|
if not candidate:
|
||||||
raise ValueError('must provide a candidate config to load')
|
raise ValueError("must provide a candidate config to load")
|
||||||
|
|
||||||
if check_mode not in (True, False):
|
if commit not in (True, False):
|
||||||
raise ValueError('`check_mode` must be a bool, got %s' % check_mode)
|
raise ValueError("'commit' must be a bool, got %s" % commit)
|
||||||
|
|
||||||
options = self.get_option_values()
|
if replace not in (True, False):
|
||||||
if replace and replace not in options['replace']:
|
raise ValueError("'replace' must be a bool, got %s" % replace)
|
||||||
raise ValueError('`replace` value %s in invalid, valid values are %s' % (replace, options['replace']))
|
|
||||||
|
operations = self.get_device_operations()
|
||||||
|
if replace and not operations['supports_replace']:
|
||||||
|
raise ValueError("configuration replace is not supported on ios")
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
if not check_mode:
|
if commit:
|
||||||
for line in chain(['configure terminal'], to_list(candidate)):
|
for line in chain(['configure terminal'], to_list(candidate)):
|
||||||
if not isinstance(line, collections.Mapping):
|
if not isinstance(line, collections.Mapping):
|
||||||
line = {'command': line}
|
line = {'command': line}
|
||||||
|
@ -140,16 +149,23 @@ class Cliconf(CliconfBase):
|
||||||
results.append(self.send_command(**line))
|
results.append(self.send_command(**line))
|
||||||
|
|
||||||
results.append(self.send_command('end'))
|
results.append(self.send_command('end'))
|
||||||
return results[1:-1]
|
|
||||||
|
|
||||||
def get(self, command, prompt=None, answer=None, sendonly=False):
|
diff_config = None
|
||||||
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly)
|
if diff:
|
||||||
|
diff_config = candidate
|
||||||
|
|
||||||
|
return diff_config, results[1:-1]
|
||||||
|
|
||||||
|
def get(self, command=None, prompt=None, answer=None, sendonly=False):
|
||||||
|
if not command:
|
||||||
|
raise ValueError('must provide value of command to execute')
|
||||||
|
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly)
|
||||||
|
|
||||||
def get_device_info(self):
|
def get_device_info(self):
|
||||||
device_info = {}
|
device_info = {}
|
||||||
|
|
||||||
device_info['network_os'] = 'ios'
|
device_info['network_os'] = 'ios'
|
||||||
reply = self.get('show version')
|
reply = self.get(command='show version')
|
||||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||||
|
|
||||||
match = re.search(r'Version (\S+)', data)
|
match = re.search(r'Version (\S+)', data)
|
||||||
|
@ -168,47 +184,50 @@ class Cliconf(CliconfBase):
|
||||||
|
|
||||||
def get_device_operations(self):
|
def get_device_operations(self):
|
||||||
return {
|
return {
|
||||||
'supports_replace': True,
|
'supports_diff_replace': True,
|
||||||
'supports_commit': False,
|
'supports_commit': False,
|
||||||
'supports_rollback': False,
|
'supports_rollback': False,
|
||||||
'supports_defaults': True,
|
'supports_defaults': True,
|
||||||
'supports_onbox_diff': False,
|
'supports_onbox_diff': False,
|
||||||
'supports_commit_comment': False,
|
'supports_commit_comment': False,
|
||||||
'supports_multiline_delimiter': False,
|
'supports_multiline_delimiter': False,
|
||||||
'support_match': True,
|
'support_diff_match': True,
|
||||||
'support_diff_ignore_lines': True,
|
'support_diff_ignore_lines': True,
|
||||||
'supports_generate_diff': True,
|
'supports_generate_diff': True,
|
||||||
|
'supports_replace': False
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_option_values(self):
|
def get_option_values(self):
|
||||||
return {
|
return {
|
||||||
'format': ['text'],
|
'format': ['text'],
|
||||||
'match': ['line', 'strict', 'exact', 'none'],
|
'diff_match': ['line', 'strict', 'exact', 'none'],
|
||||||
'replace': ['line', 'block']
|
'diff_replace': ['line', 'block']
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_capabilities(self):
|
def get_capabilities(self):
|
||||||
result = dict()
|
result = dict()
|
||||||
result['rpc'] = self.get_base_rpc() + ['edit_banner']
|
result['rpc'] = self.get_base_rpc() + ['edit_banner', 'get_diff']
|
||||||
result['network_api'] = 'cliconf'
|
result['network_api'] = 'cliconf'
|
||||||
result['device_info'] = self.get_device_info()
|
result['device_info'] = self.get_device_info()
|
||||||
result['device_operations'] = self.get_device_operations()
|
result['device_operations'] = self.get_device_operations()
|
||||||
result.update(self.get_option_values())
|
result.update(self.get_option_values())
|
||||||
return json.dumps(result)
|
return json.dumps(result)
|
||||||
|
|
||||||
def edit_banner(self, banners, multiline_delimiter="@", check_mode=False):
|
def edit_banner(self, candidate=None, multiline_delimiter="@", commit=True, diff=False):
|
||||||
"""
|
"""
|
||||||
Edit banner on remote device
|
Edit banner on remote device
|
||||||
:param banners: Banners to be loaded in json format
|
:param banners: Banners to be loaded in json format
|
||||||
:param multiline_delimiter: Line delimiter for banner
|
:param multiline_delimiter: Line delimiter for banner
|
||||||
:param check_mode: Boolean value that indicates if the device candidate
|
:param commit: Boolean value that indicates if the device candidate
|
||||||
configuration should be pushed in the running configuration or discarded.
|
configuration should be pushed in the running configuration or discarded.
|
||||||
|
:param diff: Boolean flag to indicate if configuration that is applied on remote host should
|
||||||
|
generated and returned in response or not
|
||||||
:return: Returns response of executing the configuration command received
|
:return: Returns response of executing the configuration command received
|
||||||
from remote host
|
from remote host
|
||||||
"""
|
"""
|
||||||
banners_obj = json.loads(banners)
|
banners_obj = json.loads(candidate)
|
||||||
results = []
|
results = []
|
||||||
if not check_mode:
|
if commit:
|
||||||
for key, value in iteritems(banners_obj):
|
for key, value in iteritems(banners_obj):
|
||||||
key += ' %s' % multiline_delimiter
|
key += ' %s' % multiline_delimiter
|
||||||
for cmd in ['config terminal', key, value, multiline_delimiter, 'end']:
|
for cmd in ['config terminal', key, value, multiline_delimiter, 'end']:
|
||||||
|
@ -218,7 +237,11 @@ class Cliconf(CliconfBase):
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
results.append(self.send_command('\n'))
|
results.append(self.send_command('\n'))
|
||||||
|
|
||||||
return results[1:-1]
|
diff_banner = None
|
||||||
|
if diff:
|
||||||
|
diff_banner = candidate
|
||||||
|
|
||||||
|
return diff_banner, results[1:-1]
|
||||||
|
|
||||||
def _extract_banners(self, config):
|
def _extract_banners(self, config):
|
||||||
banners = {}
|
banners = {}
|
||||||
|
|
|
@ -24,7 +24,9 @@ import json
|
||||||
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleConnectionFailure
|
||||||
from ansible.module_utils._text import to_text
|
from ansible.module_utils._text import to_text
|
||||||
|
from ansible.module_utils.network.common.config import NetworkConfig, dumps
|
||||||
from ansible.module_utils.network.common.utils import to_list
|
from ansible.module_utils.network.common.utils import to_list
|
||||||
from ansible.plugins.cliconf import CliconfBase
|
from ansible.plugins.cliconf import CliconfBase
|
||||||
|
|
||||||
|
@ -51,14 +53,56 @@ class Cliconf(CliconfBase):
|
||||||
|
|
||||||
return device_info
|
return device_info
|
||||||
|
|
||||||
def get_config(self, source='running', format='text'):
|
def get_config(self, filter=None, format='set'):
|
||||||
return self.send_command('show configuration commands')
|
if format == 'text':
|
||||||
|
out = self.send_command('show configuration')
|
||||||
|
else:
|
||||||
|
out = self.send_command('show configuration commands')
|
||||||
|
return out
|
||||||
|
|
||||||
def edit_config(self, command):
|
def edit_config(self, candidate=None, commit=True, replace=False, diff=False, comment=None):
|
||||||
for cmd in chain(['configure'], to_list(command)):
|
if not candidate:
|
||||||
self.send_command(cmd)
|
raise ValueError('must provide a candidate config to load')
|
||||||
|
|
||||||
def get(self, command, prompt=None, answer=None, sendonly=False):
|
if commit not in (True, False):
|
||||||
|
raise ValueError("'commit' must be a bool, got %s" % commit)
|
||||||
|
|
||||||
|
if replace not in (True, False):
|
||||||
|
raise ValueError("'replace' must be a bool, got %s" % replace)
|
||||||
|
|
||||||
|
operations = self.get_device_operations()
|
||||||
|
if replace and not operations['supports_replace']:
|
||||||
|
raise ValueError("configuration replace is not supported on vyos")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for cmd in chain(['configure'], to_list(candidate)):
|
||||||
|
results.append(self.send_command(cmd))
|
||||||
|
|
||||||
|
diff_config = None
|
||||||
|
if diff:
|
||||||
|
out = self.get('compare')
|
||||||
|
out = to_text(out, errors='surrogate_or_strict')
|
||||||
|
if not out.startswith('No changes'):
|
||||||
|
diff_config = out
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
try:
|
||||||
|
self.commit(comment)
|
||||||
|
except AnsibleConnectionFailure as e:
|
||||||
|
msg = 'commit failed: %s' % e.message
|
||||||
|
self.discard_changes()
|
||||||
|
raise AnsibleConnectionFailure(msg)
|
||||||
|
else:
|
||||||
|
self.get('exit')
|
||||||
|
else:
|
||||||
|
self.discard_changes()
|
||||||
|
|
||||||
|
return diff_config, results[1:]
|
||||||
|
|
||||||
|
def get(self, command=None, prompt=None, answer=None, sendonly=False):
|
||||||
|
if not command:
|
||||||
|
raise ValueError('must provide value of command to execute')
|
||||||
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly)
|
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly)
|
||||||
|
|
||||||
def commit(self, comment=None):
|
def commit(self, comment=None):
|
||||||
|
@ -68,12 +112,105 @@ class Cliconf(CliconfBase):
|
||||||
command = 'commit'
|
command = 'commit'
|
||||||
self.send_command(command)
|
self.send_command(command)
|
||||||
|
|
||||||
def discard_changes(self, *args, **kwargs):
|
def discard_changes(self):
|
||||||
self.send_command('exit discard')
|
self.send_command('exit discard')
|
||||||
|
|
||||||
|
def get_diff(self, candidate=None, running=None, match='line', diff_ignore_lines=None, path=None, replace=None):
|
||||||
|
diff = {}
|
||||||
|
device_operations = self.get_device_operations()
|
||||||
|
option_values = self.get_option_values()
|
||||||
|
|
||||||
|
if candidate is None and not device_operations['supports_onbox_diff']:
|
||||||
|
raise ValueError("candidate configuration is required to generate diff")
|
||||||
|
|
||||||
|
if match not in option_values['diff_match']:
|
||||||
|
raise ValueError("'match' value %s in invalid, valid values are %s" % (match, option_values['diff_match']))
|
||||||
|
|
||||||
|
if replace:
|
||||||
|
raise ValueError("'replace' in diff is not supported on vyos")
|
||||||
|
|
||||||
|
if diff_ignore_lines:
|
||||||
|
raise ValueError("'diff_ignore_lines' in diff is not supported on vyos")
|
||||||
|
|
||||||
|
if path:
|
||||||
|
raise ValueError("'path' in diff is not supported on vyos")
|
||||||
|
|
||||||
|
set_format = candidate.startswith('set') or candidate.startswith('delete')
|
||||||
|
candidate_obj = NetworkConfig(indent=4, contents=candidate)
|
||||||
|
if not set_format:
|
||||||
|
config = [c.line for c in candidate_obj.items]
|
||||||
|
commands = list()
|
||||||
|
# this filters out less specific lines
|
||||||
|
for item in config:
|
||||||
|
for index, entry in enumerate(commands):
|
||||||
|
if item.startswith(entry):
|
||||||
|
del commands[index]
|
||||||
|
break
|
||||||
|
commands.append(item)
|
||||||
|
|
||||||
|
candidate_commands = ['set %s' % cmd.replace(' {', '') for cmd in commands]
|
||||||
|
|
||||||
|
else:
|
||||||
|
candidate_commands = str(candidate).strip().split('\n')
|
||||||
|
|
||||||
|
if match == 'none':
|
||||||
|
diff['config_diff'] = list(candidate_commands)
|
||||||
|
return json.dumps(diff)
|
||||||
|
|
||||||
|
running_commands = [str(c).replace("'", '') for c in running.splitlines()]
|
||||||
|
|
||||||
|
updates = list()
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
for line in candidate_commands:
|
||||||
|
item = str(line).replace("'", '')
|
||||||
|
|
||||||
|
if not item.startswith('set') and not item.startswith('delete'):
|
||||||
|
raise ValueError('line must start with either `set` or `delete`')
|
||||||
|
|
||||||
|
elif item.startswith('set') and item not in running_commands:
|
||||||
|
updates.append(line)
|
||||||
|
|
||||||
|
elif item.startswith('delete'):
|
||||||
|
if not running_commands:
|
||||||
|
updates.append(line)
|
||||||
|
else:
|
||||||
|
item = re.sub(r'delete', 'set', item)
|
||||||
|
for entry in running_commands:
|
||||||
|
if entry.startswith(item) and line not in visited:
|
||||||
|
updates.append(line)
|
||||||
|
visited.add(line)
|
||||||
|
|
||||||
|
diff['config_diff'] = list(updates)
|
||||||
|
return json.dumps(diff)
|
||||||
|
|
||||||
|
def get_device_operations(self):
|
||||||
|
return {
|
||||||
|
'supports_diff_replace': False,
|
||||||
|
'supports_commit': True,
|
||||||
|
'supports_rollback': True,
|
||||||
|
'supports_defaults': False,
|
||||||
|
'supports_onbox_diff': False,
|
||||||
|
'supports_commit_comment': True,
|
||||||
|
'supports_multiline_delimiter': False,
|
||||||
|
'support_diff_match': True,
|
||||||
|
'support_diff_ignore_lines': False,
|
||||||
|
'supports_generate_diff': True,
|
||||||
|
'supports_replace': False
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_option_values(self):
|
||||||
|
return {
|
||||||
|
'format': ['set', 'text'],
|
||||||
|
'diff_match': ['line', 'none'],
|
||||||
|
'diff_replace': [],
|
||||||
|
}
|
||||||
|
|
||||||
def get_capabilities(self):
|
def get_capabilities(self):
|
||||||
result = {}
|
result = {}
|
||||||
result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes']
|
result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes', 'get_diff']
|
||||||
result['network_api'] = 'cliconf'
|
result['network_api'] = 'cliconf'
|
||||||
result['device_info'] = self.get_device_info()
|
result['device_info'] = self.get_device_info()
|
||||||
|
result['device_operations'] = self.get_device_operations()
|
||||||
|
result.update(self.get_option_values())
|
||||||
return json.dumps(result)
|
return json.dumps(result)
|
||||||
|
|
|
@ -20,8 +20,9 @@
|
||||||
from __future__ import (absolute_import, division, print_function)
|
from __future__ import (absolute_import, division, print_function)
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
from ansible.compat.tests.mock import patch
|
from ansible.compat.tests.mock import patch, MagicMock
|
||||||
from ansible.modules.network.vyos import vyos_config
|
from ansible.modules.network.vyos import vyos_config
|
||||||
|
from ansible.plugins.cliconf.vyos import Cliconf
|
||||||
from units.modules.utils import set_module_args
|
from units.modules.utils import set_module_args
|
||||||
from .vyos_module import TestVyosModule, load_fixture
|
from .vyos_module import TestVyosModule, load_fixture
|
||||||
|
|
||||||
|
@ -42,12 +43,23 @@ class TestVyosConfigModule(TestVyosModule):
|
||||||
self.mock_run_commands = patch('ansible.modules.network.vyos.vyos_config.run_commands')
|
self.mock_run_commands = patch('ansible.modules.network.vyos.vyos_config.run_commands')
|
||||||
self.run_commands = self.mock_run_commands.start()
|
self.run_commands = self.mock_run_commands.start()
|
||||||
|
|
||||||
|
self.mock_get_connection = patch('ansible.modules.network.vyos.vyos_config.get_connection')
|
||||||
|
self.get_connection = self.mock_get_connection.start()
|
||||||
|
|
||||||
|
self.cliconf_obj = Cliconf(MagicMock())
|
||||||
|
self.running_config = load_fixture('vyos_config_config.cfg')
|
||||||
|
|
||||||
|
self.conn = self.get_connection()
|
||||||
|
self.conn.edit_config = MagicMock()
|
||||||
|
self.running_config = load_fixture('vyos_config_config.cfg')
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(TestVyosConfigModule, self).tearDown()
|
super(TestVyosConfigModule, self).tearDown()
|
||||||
|
|
||||||
self.mock_get_config.stop()
|
self.mock_get_config.stop()
|
||||||
self.mock_load_config.stop()
|
self.mock_load_config.stop()
|
||||||
self.mock_run_commands.stop()
|
self.mock_run_commands.stop()
|
||||||
|
self.mock_get_connection.stop()
|
||||||
|
|
||||||
def load_fixtures(self, commands=None):
|
def load_fixtures(self, commands=None):
|
||||||
config_file = 'vyos_config_config.cfg'
|
config_file = 'vyos_config_config.cfg'
|
||||||
|
@ -56,6 +68,7 @@ class TestVyosConfigModule(TestVyosModule):
|
||||||
|
|
||||||
def test_vyos_config_unchanged(self):
|
def test_vyos_config_unchanged(self):
|
||||||
src = load_fixture('vyos_config_config.cfg')
|
src = load_fixture('vyos_config_config.cfg')
|
||||||
|
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(src, src))
|
||||||
set_module_args(dict(src=src))
|
set_module_args(dict(src=src))
|
||||||
self.execute_module()
|
self.execute_module()
|
||||||
|
|
||||||
|
@ -63,12 +76,14 @@ class TestVyosConfigModule(TestVyosModule):
|
||||||
src = load_fixture('vyos_config_src.cfg')
|
src = load_fixture('vyos_config_src.cfg')
|
||||||
set_module_args(dict(src=src))
|
set_module_args(dict(src=src))
|
||||||
commands = ['set system host-name foo', 'delete interfaces ethernet eth0 address']
|
commands = ['set system host-name foo', 'delete interfaces ethernet eth0 address']
|
||||||
|
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(src, self.running_config))
|
||||||
self.execute_module(changed=True, commands=commands)
|
self.execute_module(changed=True, commands=commands)
|
||||||
|
|
||||||
def test_vyos_config_src_brackets(self):
|
def test_vyos_config_src_brackets(self):
|
||||||
src = load_fixture('vyos_config_src_brackets.cfg')
|
src = load_fixture('vyos_config_src_brackets.cfg')
|
||||||
set_module_args(dict(src=src))
|
set_module_args(dict(src=src))
|
||||||
commands = ['set interfaces ethernet eth0 address 10.10.10.10/24', 'set system host-name foo']
|
commands = ['set interfaces ethernet eth0 address 10.10.10.10/24', 'set system host-name foo']
|
||||||
|
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(src, self.running_config))
|
||||||
self.execute_module(changed=True, commands=commands)
|
self.execute_module(changed=True, commands=commands)
|
||||||
|
|
||||||
def test_vyos_config_backup(self):
|
def test_vyos_config_backup(self):
|
||||||
|
@ -79,16 +94,22 @@ class TestVyosConfigModule(TestVyosModule):
|
||||||
def test_vyos_config_lines(self):
|
def test_vyos_config_lines(self):
|
||||||
commands = ['set system host-name foo']
|
commands = ['set system host-name foo']
|
||||||
set_module_args(dict(lines=commands))
|
set_module_args(dict(lines=commands))
|
||||||
|
candidate = '\n'.join(commands)
|
||||||
|
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate, self.running_config))
|
||||||
self.execute_module(changed=True, commands=commands)
|
self.execute_module(changed=True, commands=commands)
|
||||||
|
|
||||||
def test_vyos_config_config(self):
|
def test_vyos_config_config(self):
|
||||||
config = 'set system host-name localhost'
|
config = 'set system host-name localhost'
|
||||||
new_config = ['set system host-name router']
|
new_config = ['set system host-name router']
|
||||||
set_module_args(dict(lines=new_config, config=config))
|
set_module_args(dict(lines=new_config, config=config))
|
||||||
|
candidate = '\n'.join(new_config)
|
||||||
|
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate, config))
|
||||||
self.execute_module(changed=True, commands=new_config)
|
self.execute_module(changed=True, commands=new_config)
|
||||||
|
|
||||||
def test_vyos_config_match_none(self):
|
def test_vyos_config_match_none(self):
|
||||||
lines = ['set system interfaces ethernet eth0 address 1.2.3.4/24',
|
lines = ['set system interfaces ethernet eth0 address 1.2.3.4/24',
|
||||||
'set system interfaces ethernet eth0 description test string']
|
'set system interfaces ethernet eth0 description test string']
|
||||||
set_module_args(dict(lines=lines, match='none'))
|
set_module_args(dict(lines=lines, match='none'))
|
||||||
|
candidate = '\n'.join(lines)
|
||||||
|
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate, None, match='none'))
|
||||||
self.execute_module(changed=True, commands=lines, sort=False)
|
self.execute_module(changed=True, commands=lines, sort=False)
|
||||||
|
|
Loading…
Reference in a new issue