* adds commit replace with config file for iosxr (#35564)
* * adds commit replace with config file for iosxr * fixes dci failure in iosxr_logging * * review comment changes
This commit is contained in:
parent
2479b6d635
commit
684e953b50
11 changed files with 227 additions and 48 deletions
|
@ -29,6 +29,7 @@
|
|||
import json
|
||||
from difflib import Differ
|
||||
from copy import deepcopy
|
||||
from time import sleep
|
||||
|
||||
from ansible.module_utils._text import to_text, to_bytes
|
||||
from ansible.module_utils.basic import env_fallback
|
||||
|
@ -415,7 +416,14 @@ def load_config(module, command_filter, commit=False, replace=False,
|
|||
if module._diff:
|
||||
diff = get_config_diff(module)
|
||||
|
||||
if commit:
|
||||
if replace:
|
||||
cmd = list()
|
||||
cmd.append({'command': 'commit replace',
|
||||
'prompt': 'This commit will replace or remove the entire running configuration',
|
||||
'answer': 'yes'})
|
||||
cmd.append('end')
|
||||
conn.edit_config(cmd)
|
||||
elif commit:
|
||||
commit_config(module, comment=comment)
|
||||
conn.edit_config('end')
|
||||
else:
|
||||
|
@ -428,20 +436,36 @@ def run_command(module, commands):
|
|||
conn = get_connection(module)
|
||||
responses = list()
|
||||
for cmd in to_list(commands):
|
||||
|
||||
try:
|
||||
cmd = json.loads(cmd)
|
||||
command = cmd['command']
|
||||
prompt = cmd['prompt']
|
||||
answer = cmd['answer']
|
||||
if isinstance(cmd, str):
|
||||
cmd = json.loads(cmd)
|
||||
command = cmd.get('command', None)
|
||||
prompt = cmd.get('prompt', None)
|
||||
answer = cmd.get('answer', None)
|
||||
sendonly = cmd.get('sendonly', False)
|
||||
newline = cmd.get('newline', True)
|
||||
except:
|
||||
command = cmd
|
||||
prompt = None
|
||||
answer = None
|
||||
sendonly = False
|
||||
newline = True
|
||||
|
||||
out = conn.get(command, prompt, answer)
|
||||
out = conn.get(command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline)
|
||||
|
||||
try:
|
||||
responses.append(to_text(out, errors='surrogate_or_strict'))
|
||||
except UnicodeError:
|
||||
module.fail_json(msg=u'failed to decode output from {0}:{1}'.format(cmd, to_text(out)))
|
||||
return responses
|
||||
|
||||
|
||||
def copy_file(module, src, dst, proto='scp'):
|
||||
conn = get_connection(module)
|
||||
conn.copy_file(source=src, destination=dst, proto=proto)
|
||||
|
||||
|
||||
def get_file(module, src, dst, proto='scp'):
|
||||
conn = get_connection(module)
|
||||
conn.get_file(source=src, destination=dst, proto=proto)
|
||||
|
|
|
@ -94,7 +94,7 @@ tasks:
|
|||
commands:
|
||||
- show version
|
||||
- show interfaces
|
||||
- [{ command: example command that prompts, prompt: expected prompt, answer: yes}]
|
||||
- { command: example command that prompts, prompt: expected prompt, answer: yes}
|
||||
|
||||
- name: run multiple commands and evaluate the output
|
||||
iosxr_command:
|
||||
|
|
|
@ -25,7 +25,9 @@ description:
|
|||
a deterministic way.
|
||||
extends_documentation_fragment: iosxr
|
||||
notes:
|
||||
- Tested against IOS XR 6.1.2
|
||||
- Tested against IOS XRv 6.1.2
|
||||
- Avoid service disrupting changes (viz. Management IP) from config replace.
|
||||
- Do not use C(end) in the replace config file.
|
||||
options:
|
||||
lines:
|
||||
description:
|
||||
|
@ -164,6 +166,7 @@ EXAMPLES = """
|
|||
- name: load a config from disk and replace the current config
|
||||
iosxr_config:
|
||||
src: config.cfg
|
||||
replace: config
|
||||
backup: yes
|
||||
"""
|
||||
|
||||
|
@ -181,13 +184,26 @@ backup_path:
|
|||
"""
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.network.iosxr.iosxr import load_config, get_config
|
||||
from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec
|
||||
from ansible.module_utils.network.iosxr.iosxr import iosxr_argument_spec, copy_file
|
||||
from ansible.module_utils.network.common.config import NetworkConfig, dumps
|
||||
|
||||
|
||||
DEFAULT_COMMIT_COMMENT = 'configured by iosxr_config'
|
||||
|
||||
|
||||
def copy_file_to_node(module):
|
||||
""" Copy config file to IOS-XR node. We use SFTP because older IOS-XR versions don't handle SCP very well.
|
||||
"""
|
||||
src = '/tmp/ansible_config.txt'
|
||||
file = open(src, 'wb')
|
||||
file.write(module.params['src'])
|
||||
file.close()
|
||||
|
||||
dst = '/harddisk:/ansible_config.txt'
|
||||
copy_file(module, src, dst, 'sftp')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_args(module, warnings):
|
||||
if module.params['comment']:
|
||||
if len(module.params['comment']) > 60:
|
||||
|
@ -224,18 +240,30 @@ def run(module, result):
|
|||
admin = module.params['admin']
|
||||
check_mode = module.check_mode
|
||||
|
||||
candidate = get_candidate(module)
|
||||
candidate_config = get_candidate(module)
|
||||
running_config = get_running_config(module)
|
||||
|
||||
commands = None
|
||||
if match != 'none' and replace != 'config':
|
||||
contents = get_running_config(module)
|
||||
configobj = NetworkConfig(contents=contents, indent=1)
|
||||
commands = candidate.difference(configobj, path=path, match=match,
|
||||
replace=replace)
|
||||
commands = candidate_config.difference(running_config, path=path, match=match, replace=replace)
|
||||
elif replace_config:
|
||||
can_config = candidate_config.difference(running_config, path=path, match=match, replace=replace)
|
||||
candidate = dumps(can_config, 'commands').split('\n')
|
||||
run_config = running_config.difference(candidate_config, path=path, match=match, replace=replace)
|
||||
running = dumps(run_config, 'commands').split('\n')
|
||||
|
||||
if len(candidate) > 1 or len(running) > 1:
|
||||
ret = copy_file_to_node(module)
|
||||
if not ret:
|
||||
module.fail_json(msg='Copy of config file to the node failed')
|
||||
|
||||
commands = ['load harddisk:/ansible_config.txt']
|
||||
else:
|
||||
commands = candidate.items
|
||||
commands = candidate_config.items
|
||||
|
||||
if commands:
|
||||
commands = dumps(commands, 'commands').split('\n')
|
||||
if not replace_config:
|
||||
commands = dumps(commands, 'commands').split('\n')
|
||||
|
||||
if any((module.params['lines'], module.params['src'])):
|
||||
if module.params['before']:
|
||||
|
@ -247,10 +275,10 @@ def run(module, result):
|
|||
result['commands'] = commands
|
||||
|
||||
commit = not check_mode
|
||||
diff = load_config(module, commands, commit=commit, replace=replace_config,
|
||||
comment=comment, admin=admin)
|
||||
diff = load_config(module, commands, commit=commit, replace=replace_config, comment=comment, admin=admin)
|
||||
if diff:
|
||||
result['diff'] = dict(prepared=diff)
|
||||
|
||||
result['changed'] = True
|
||||
|
||||
|
||||
|
|
|
@ -586,7 +586,7 @@ class NCConfiguration(ConfigBase):
|
|||
elif item['dest'] == 'host' and item['name'] in host_list:
|
||||
item['level'] = severity_level[item['level']]
|
||||
host_params.append(item)
|
||||
elif item['dest'] == 'console' and have_console and have_console_enable:
|
||||
elif item['dest'] == 'console' and have_console:
|
||||
console_params.update({'console-level': item['level']})
|
||||
elif item['dest'] == 'monitor' and have_monitor:
|
||||
monitor_params.update({'monitor-level': item['level']})
|
||||
|
|
|
@ -34,7 +34,6 @@ try:
|
|||
except ImportError:
|
||||
HAS_SCP = False
|
||||
|
||||
|
||||
try:
|
||||
from __main__ import display
|
||||
except ImportError:
|
||||
|
@ -135,7 +134,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def edit_config(self, commands):
|
||||
def edit_config(self, commands=None):
|
||||
"""Loads the specified commands into the remote device
|
||||
This method will load the commands into the remote device. This
|
||||
method will make sure the device is in the proper context before
|
||||
|
@ -150,7 +149,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False):
|
||||
def get(self, command=None, prompt=None, answer=None, sendonly=False, newline=True):
|
||||
"""Execute specified command on remote device
|
||||
This method will retrieve the specified data and
|
||||
return it to the caller as a string.
|
||||
|
@ -181,18 +180,26 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
|||
"Discard changes in candidate datastore"
|
||||
return self._connection.method_not_found("discard_changes is not supported by network_os %s" % self._play_context.network_os)
|
||||
|
||||
def put_file(self, source, destination):
|
||||
"""Copies file over scp to remote device"""
|
||||
if not HAS_SCP:
|
||||
self._connection.internal_error("Required library scp is not installed. Please install it using `pip install scp`")
|
||||
ssh = self._connection._connect_uncached()
|
||||
with SCPClient(ssh.get_transport()) as scp:
|
||||
scp.put(source, destination)
|
||||
def copy_file(self, source=None, destination=None, proto='scp'):
|
||||
"""Copies file over scp/sftp to remote device"""
|
||||
ssh = self._connection.paramiko_conn._connect_uncached()
|
||||
if proto == 'scp':
|
||||
if not HAS_SCP:
|
||||
self._connection.internal_error("Required library scp is not installed. Please install it using `pip install scp`")
|
||||
with SCPClient(ssh.get_transport()) as scp:
|
||||
scp.put(source, destination)
|
||||
elif proto == 'sftp':
|
||||
with ssh.open_sftp() as sftp:
|
||||
sftp.put(source, destination)
|
||||
|
||||
def fetch_file(self, source, destination):
|
||||
"""Fetch file over scp from remote device"""
|
||||
if not HAS_SCP:
|
||||
self._connection.internal_error("Required library scp is not installed. Please install it using `pip install scp`")
|
||||
ssh = self._connection._connect_uncached()
|
||||
with SCPClient(ssh.get_transport()) as scp:
|
||||
scp.get(source, destination)
|
||||
def get_file(self, source=None, destination=None, proto='scp'):
|
||||
"""Fetch file over scp/sftp from remote device"""
|
||||
ssh = self._connection.paramiko_conn._connect_uncached()
|
||||
if proto == 'scp':
|
||||
if not HAS_SCP:
|
||||
self._connection.internal_error("Required library scp is not installed. Please install it using `pip install scp`")
|
||||
with SCPClient(ssh.get_transport()) as scp:
|
||||
scp.get(source, destination)
|
||||
elif proto == 'sftp':
|
||||
with ssh.open_sftp() as sftp:
|
||||
sftp.get(source, destination)
|
||||
|
|
|
@ -67,12 +67,27 @@ class Cliconf(CliconfBase):
|
|||
|
||||
return self.send_command(cmd)
|
||||
|
||||
def edit_config(self, command):
|
||||
for cmd in chain(to_list(command)):
|
||||
self.send_command(cmd)
|
||||
def edit_config(self, commands=None):
|
||||
for cmd in chain(to_list(commands)):
|
||||
try:
|
||||
if isinstance(cmd, str):
|
||||
cmd = json.loads(cmd)
|
||||
command = cmd.get('command', None)
|
||||
prompt = cmd.get('prompt', None)
|
||||
answer = cmd.get('answer', None)
|
||||
sendonly = cmd.get('sendonly', False)
|
||||
newline = cmd.get('newline', True)
|
||||
except:
|
||||
command = cmd
|
||||
prompt = None
|
||||
answer = None
|
||||
sendonly = None
|
||||
newline = None
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False):
|
||||
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly)
|
||||
self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline)
|
||||
|
||||
def get(self, command=None, prompt=None, answer=None, sendonly=False, newline=True):
|
||||
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline)
|
||||
|
||||
def commit(self, comment=None):
|
||||
if comment:
|
||||
|
|
|
@ -283,10 +283,10 @@ class Connection(ConnectionBase):
|
|||
if self.connected:
|
||||
return
|
||||
|
||||
p = connection_loader.get('paramiko', self._play_context, '/dev/null')
|
||||
p.set_options(direct={'look_for_keys': not bool(self._play_context.password and not self._play_context.private_key_file)})
|
||||
p.force_persistence = self.force_persistence
|
||||
ssh = p._connect()
|
||||
self.paramiko_conn = connection_loader.get('paramiko', self._play_context, '/dev/null')
|
||||
self.paramiko_conn.set_options(direct={'look_for_keys': not bool(self._play_context.password and not self._play_context.private_key_file)})
|
||||
self.paramiko_conn.force_persistence = self.force_persistence
|
||||
ssh = self.paramiko_conn._connect()
|
||||
|
||||
display.vvvv('ssh connection done, setting terminal', host=self._play_context.remote_addr)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
- name: run invalid command
|
||||
iosxr_command:
|
||||
commands: [{command: 'show foo', prompt: 'fooprompt', answer: 'yes'}]
|
||||
commands: {command: 'show foo', prompt: 'fooprompt', answer: 'yes'}
|
||||
register: result
|
||||
ignore_errors: yes
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
|||
iosxr_command:
|
||||
commands:
|
||||
- show version
|
||||
- [{command: 'show foo', prompt: 'fooprompt', answer: 'yes'}]
|
||||
- {command: 'show foo', prompt: 'fooprompt', answer: 'yes'}
|
||||
register: result
|
||||
ignore_errors: yes
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
hostname iosxr01
|
||||
line default
|
||||
transport input ssh
|
||||
!
|
||||
interface Loopback888
|
||||
description test for ansible
|
||||
shutdown
|
||||
!
|
||||
interface MgmtEth0/0/CPU0/0
|
||||
ipv4 address dhcp
|
||||
!
|
||||
interface preconfigure GigabitEthernet0/0/0/3
|
||||
description test-interface-3
|
||||
mtu 256
|
||||
speed 100
|
||||
duplex full
|
||||
!
|
||||
interface GigabitEthernet0/0/0/0
|
||||
shutdown
|
||||
!
|
||||
interface GigabitEthernet0/0/0/1
|
||||
shutdown
|
||||
!
|
||||
router static
|
||||
address-family ipv4 unicast
|
||||
0.0.0.0/0 10.0.2.2
|
||||
!
|
||||
!
|
||||
netconf-yang agent
|
||||
ssh
|
||||
!
|
||||
ssh server v2
|
||||
ssh server netconf vrf default
|
|
@ -0,0 +1,27 @@
|
|||
hostname iosxr01
|
||||
line default
|
||||
transport input ssh
|
||||
!
|
||||
interface Loopback888
|
||||
description test for ansible
|
||||
shutdown
|
||||
!
|
||||
interface MgmtEth0/0/CPU0/0
|
||||
ipv4 address dhcp
|
||||
!
|
||||
interface GigabitEthernet0/0/0/0
|
||||
shutdown
|
||||
!
|
||||
interface GigabitEthernet0/0/0/1
|
||||
shutdown
|
||||
!
|
||||
router static
|
||||
address-family ipv4 unicast
|
||||
0.0.0.0/0 10.0.2.2
|
||||
!
|
||||
!
|
||||
netconf-yang agent
|
||||
ssh
|
||||
!
|
||||
ssh server v2
|
||||
ssh server netconf vrf default
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
- debug: msg="START cli/replace_config.yaml on connection={{ ansible_connection }}"
|
||||
|
||||
- name: setup
|
||||
iosxr_config:
|
||||
commands:
|
||||
- no interface GigabitEthernet0/0/0/3
|
||||
|
||||
- name: replace config (add preconfigured interface)
|
||||
iosxr_config: &addreplace
|
||||
src: "{{ role_path }}/fixtures/config_add_interface.txt"
|
||||
replace: config
|
||||
backup: yes
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- '"load harddisk:/ansible_config.txt" in result.commands'
|
||||
|
||||
- name: replace config (add preconfigured interface)(idempotence)
|
||||
iosxr_config: *addreplace
|
||||
register: result
|
||||
|
||||
- assert: &false
|
||||
that:
|
||||
- 'result.changed == false'
|
||||
|
||||
- name: replace config (del preconfigured interface)
|
||||
iosxr_config: &delreplace
|
||||
src: "{{ role_path }}/fixtures/config_del_interface.txt"
|
||||
replace: config
|
||||
backup: yes
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- '"load harddisk:/ansible_config.txt" in result.commands'
|
||||
|
||||
- name: replace config (del preconfigured interface)(idempotence)
|
||||
iosxr_config: *delreplace
|
||||
register: result
|
||||
|
||||
- assert: *false
|
||||
|
||||
- debug: msg="END cli/replace_config.yaml on connection={{ ansible_connection }}"
|
Loading…
Reference in a new issue