* 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:
Kedar Kekan 2018-02-01 19:45:32 +05:30 committed by John R Barker
parent 2479b6d635
commit 684e953b50
11 changed files with 227 additions and 48 deletions

View file

@ -29,6 +29,7 @@
import json import json
from difflib import Differ from difflib import Differ
from copy import deepcopy from copy import deepcopy
from time import sleep
from ansible.module_utils._text import to_text, to_bytes from ansible.module_utils._text import to_text, to_bytes
from ansible.module_utils.basic import env_fallback 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: if module._diff:
diff = get_config_diff(module) 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) commit_config(module, comment=comment)
conn.edit_config('end') conn.edit_config('end')
else: else:
@ -428,20 +436,36 @@ def run_command(module, commands):
conn = get_connection(module) conn = get_connection(module)
responses = list() responses = list()
for cmd in to_list(commands): for cmd in to_list(commands):
try: try:
cmd = json.loads(cmd) if isinstance(cmd, str):
command = cmd['command'] cmd = json.loads(cmd)
prompt = cmd['prompt'] command = cmd.get('command', None)
answer = cmd['answer'] prompt = cmd.get('prompt', None)
answer = cmd.get('answer', None)
sendonly = cmd.get('sendonly', False)
newline = cmd.get('newline', True)
except: except:
command = cmd command = cmd
prompt = None prompt = None
answer = 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: try:
responses.append(to_text(out, errors='surrogate_or_strict')) responses.append(to_text(out, errors='surrogate_or_strict'))
except UnicodeError: except UnicodeError:
module.fail_json(msg=u'failed to decode output from {0}:{1}'.format(cmd, to_text(out))) module.fail_json(msg=u'failed to decode output from {0}:{1}'.format(cmd, to_text(out)))
return responses 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)

View file

@ -94,7 +94,7 @@ tasks:
commands: commands:
- show version - show version
- show interfaces - 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 - name: run multiple commands and evaluate the output
iosxr_command: iosxr_command:

View file

@ -25,7 +25,9 @@ description:
a deterministic way. a deterministic way.
extends_documentation_fragment: iosxr extends_documentation_fragment: iosxr
notes: 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: options:
lines: lines:
description: description:
@ -164,6 +166,7 @@ EXAMPLES = """
- name: load a config from disk and replace the current config - name: load a config from disk and replace the current config
iosxr_config: iosxr_config:
src: config.cfg src: config.cfg
replace: config
backup: yes backup: yes
""" """
@ -181,13 +184,26 @@ backup_path:
""" """
from ansible.module_utils.basic import AnsibleModule 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 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 from ansible.module_utils.network.common.config import NetworkConfig, dumps
DEFAULT_COMMIT_COMMENT = 'configured by iosxr_config' 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): def check_args(module, warnings):
if module.params['comment']: if module.params['comment']:
if len(module.params['comment']) > 60: if len(module.params['comment']) > 60:
@ -224,18 +240,30 @@ def run(module, result):
admin = module.params['admin'] admin = module.params['admin']
check_mode = module.check_mode 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': if match != 'none' and replace != 'config':
contents = get_running_config(module) commands = candidate_config.difference(running_config, path=path, match=match, replace=replace)
configobj = NetworkConfig(contents=contents, indent=1) elif replace_config:
commands = candidate.difference(configobj, path=path, match=match, can_config = candidate_config.difference(running_config, path=path, match=match, replace=replace)
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: else:
commands = candidate.items commands = candidate_config.items
if commands: 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 any((module.params['lines'], module.params['src'])):
if module.params['before']: if module.params['before']:
@ -247,10 +275,10 @@ def run(module, result):
result['commands'] = commands result['commands'] = commands
commit = not check_mode commit = not check_mode
diff = load_config(module, commands, commit=commit, replace=replace_config, diff = load_config(module, commands, commit=commit, replace=replace_config, comment=comment, admin=admin)
comment=comment, admin=admin)
if diff: if diff:
result['diff'] = dict(prepared=diff) result['diff'] = dict(prepared=diff)
result['changed'] = True result['changed'] = True

View file

@ -586,7 +586,7 @@ class NCConfiguration(ConfigBase):
elif item['dest'] == 'host' and item['name'] in host_list: elif item['dest'] == 'host' and item['name'] in host_list:
item['level'] = severity_level[item['level']] item['level'] = severity_level[item['level']]
host_params.append(item) 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']}) console_params.update({'console-level': item['level']})
elif item['dest'] == 'monitor' and have_monitor: elif item['dest'] == 'monitor' and have_monitor:
monitor_params.update({'monitor-level': item['level']}) monitor_params.update({'monitor-level': item['level']})

View file

@ -34,7 +34,6 @@ try:
except ImportError: except ImportError:
HAS_SCP = False HAS_SCP = False
try: try:
from __main__ import display from __main__ import display
except ImportError: except ImportError:
@ -135,7 +134,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
pass pass
@abstractmethod @abstractmethod
def edit_config(self, commands): def edit_config(self, commands=None):
"""Loads the specified commands into the remote device """Loads the specified commands into the remote device
This method will load the commands into the remote device. This This method will load the commands into the remote device. This
method will make sure the device is in the proper context before method will make sure the device is in the proper context before
@ -150,7 +149,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
pass pass
@abstractmethod @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 """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.
@ -181,18 +180,26 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
"Discard changes in candidate datastore" "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) 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): def copy_file(self, source=None, destination=None, proto='scp'):
"""Copies file over scp to remote device""" """Copies file over scp/sftp to remote device"""
if not HAS_SCP: ssh = self._connection.paramiko_conn._connect_uncached()
self._connection.internal_error("Required library scp is not installed. Please install it using `pip install scp`") if proto == 'scp':
ssh = self._connection._connect_uncached() if not HAS_SCP:
with SCPClient(ssh.get_transport()) as scp: self._connection.internal_error("Required library scp is not installed. Please install it using `pip install scp`")
scp.put(source, destination) 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): def get_file(self, source=None, destination=None, proto='scp'):
"""Fetch file over scp from remote device""" """Fetch file over scp/sftp from remote device"""
if not HAS_SCP: ssh = self._connection.paramiko_conn._connect_uncached()
self._connection.internal_error("Required library scp is not installed. Please install it using `pip install scp`") if proto == 'scp':
ssh = self._connection._connect_uncached() if not HAS_SCP:
with SCPClient(ssh.get_transport()) as scp: self._connection.internal_error("Required library scp is not installed. Please install it using `pip install scp`")
scp.get(source, destination) with SCPClient(ssh.get_transport()) as scp:
scp.get(source, destination)
elif proto == 'sftp':
with ssh.open_sftp() as sftp:
sftp.get(source, destination)

View file

@ -67,12 +67,27 @@ class Cliconf(CliconfBase):
return self.send_command(cmd) return self.send_command(cmd)
def edit_config(self, command): def edit_config(self, commands=None):
for cmd in chain(to_list(command)): for cmd in chain(to_list(commands)):
self.send_command(cmd) 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): self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline)
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly)
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): def commit(self, comment=None):
if comment: if comment:

View file

@ -283,10 +283,10 @@ class Connection(ConnectionBase):
if self.connected: if self.connected:
return return
p = connection_loader.get('paramiko', self._play_context, '/dev/null') self.paramiko_conn = 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)}) self.paramiko_conn.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 self.paramiko_conn.force_persistence = self.force_persistence
ssh = p._connect() ssh = self.paramiko_conn._connect()
display.vvvv('ssh connection done, setting terminal', host=self._play_context.remote_addr) display.vvvv('ssh connection done, setting terminal', host=self._play_context.remote_addr)

View file

@ -3,7 +3,7 @@
- name: run invalid command - name: run invalid command
iosxr_command: iosxr_command:
commands: [{command: 'show foo', prompt: 'fooprompt', answer: 'yes'}] commands: {command: 'show foo', prompt: 'fooprompt', answer: 'yes'}
register: result register: result
ignore_errors: yes ignore_errors: yes
@ -15,7 +15,7 @@
iosxr_command: iosxr_command:
commands: commands:
- show version - show version
- [{command: 'show foo', prompt: 'fooprompt', answer: 'yes'}] - {command: 'show foo', prompt: 'fooprompt', answer: 'yes'}
register: result register: result
ignore_errors: yes ignore_errors: yes

View file

@ -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

View file

@ -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

View file

@ -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 }}"