Rewrite nxos_file_copy as an action plugin (#60643)

* Initial nxos_file_copy action plugin work
* Remove code from nxos_file_copy module
* Add file_push and file_pull support
* Additional refactoring and shipable updates
* Simplify outcomes and update doc header
* Add more error data information for easier debugging
* Reorder outcomes and add additional tests
* Capture more data for permission denied outcome
This commit is contained in:
Mike Wiebe 2019-08-29 15:57:39 -04:00 committed by Toshio Kuratomi
parent 2215a62057
commit 02572cb9ec
9 changed files with 792 additions and 354 deletions

View file

@ -36,10 +36,11 @@ description:
author: author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele) - Gabriele Gerbino (@GGabriele)
- Rewritten as a plugin by (@mikewiebe)
notes: notes:
- Tested against NXOS 7.0(3)I2(5), 7.0(3)I4(6), 7.0(3)I5(3), - Tested against NXOS 7.0(3)I2(5), 7.0(3)I4(6), 7.0(3)I5(3),
7.0(3)I6(1), 7.0(3)I7(3), 6.0(2)A8(8), 7.0(3)F3(4), 7.3(0)D1(1), 7.0(3)I6(1), 7.0(3)I7(3), 6.0(2)A8(8), 7.0(3)F3(4), 7.3(0)D1(1),
8.3(0) 8.3(0), 9.2, 9.3
- When pushing files (file_pull is False) to the NXOS device, - When pushing files (file_pull is False) to the NXOS device,
feature scp-server must be enabled. feature scp-server must be enabled.
- When pulling files (file_pull is True) to the NXOS device, - When pulling files (file_pull is True) to the NXOS device,
@ -56,7 +57,7 @@ options:
description: description:
- When (file_pull is False) this is the path to the local file on the Ansible controller. - When (file_pull is False) this is the path to the local file on the Ansible controller.
The local directory must exist. The local directory must exist.
- When (file_pull is True) this is the file name used on the NXOS device. - When (file_pull is True) this is the target file name on the NXOS device.
remote_file: remote_file:
description: description:
- When (file_pull is False) this is the remote file path on the NXOS device. - When (file_pull is False) this is the remote file path on the NXOS device.
@ -66,13 +67,13 @@ options:
server to be copied to the NXOS device. server to be copied to the NXOS device.
file_system: file_system:
description: description:
- The remote file system of the device. If omitted, - The remote file system on the nxos device. If omitted,
devices that support a I(file_system) parameter will use devices that support a I(file_system) parameter will use
their default values. their default values.
default: "bootflash:" default: "bootflash:"
connect_ssh_port: connect_ssh_port:
description: description:
- SSH port to connect to server during transfer of file - SSH server port used for file transfer.
default: 22 default: 22
version_added: "2.5" version_added: "2.5"
file_pull: file_pull:
@ -85,33 +86,53 @@ options:
type: bool type: bool
default: False default: False
version_added: "2.7" version_added: "2.7"
file_pull_compact:
description:
- When file_pull is True, this is used to compact nxos image files.
This option can only be used with nxos image files.
- When (file_pull is False), this is not used.
type: bool
default: False
version_added: "2.9"
file_pull_kstack:
description:
- When file_pull is True, this can be used to speed up file copies when
the nxos running image supports the use-kstack option.
- When (file_pull is False), this is not used.
type: bool
default: False
version_added: "2.9"
local_file_directory: local_file_directory:
description: description:
- When (file_pull is True) file is copied from a remote SCP server to the NXOS device, - When (file_pull is True) file is copied from a remote SCP server to the NXOS device,
and written to this directory on the NXOS device. If the directory does not exist, it and written to this directory on the NXOS device. If the directory does not exist, it
will be created under the file_system. This is an optional parameter. will be created under the file_system. This is an optional parameter.
- When (file_pull is False), this not used. - When (file_pull is False), this is not used.
version_added: "2.7" version_added: "2.7"
file_pull_timeout: file_pull_timeout:
description: description:
- Use this parameter to set timeout in seconds, when transferring - Use this parameter to set timeout in seconds, when transferring
large files or when the network is slow. large files or when the network is slow.
- When (file_pull is False), this is not used.
default: 300 default: 300
version_added: "2.7" version_added: "2.7"
remote_scp_server: remote_scp_server:
description: description:
- The remote scp server address which is used to pull the file. - The remote scp server address when file_pull is True.
This is required if file_pull is True. This is required if file_pull is True.
- When (file_pull is False), this is not used.
version_added: "2.7" version_added: "2.7"
remote_scp_server_user: remote_scp_server_user:
description: description:
- The remote scp server username which is used to pull the file. - The remote scp server username when file_pull is True.
This is required if file_pull is True. This is required if file_pull is True.
- When (file_pull is False), this is not used.
version_added: "2.7" version_added: "2.7"
remote_scp_server_password: remote_scp_server_password:
description: description:
- The remote scp server password which is used to pull the file. - The remote scp server password when file_pull is True.
This is required if file_pull is True. This is required if file_pull is True.
- When (file_pull is False), this is not used.
version_added: "2.7" version_added: "2.7"
vrf: vrf:
description: description:
@ -143,8 +164,7 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
transfer_status: transfer_status:
description: Whether a file was transferred. "No Transfer" or "Sent". description: Whether a file was transferred to the nxos device.
If file_pull is successful, it is set to "Received".
returned: success returned: success
type: str type: str
sample: 'Sent' sample: 'Sent'
@ -158,300 +178,14 @@ remote_file:
returned: success returned: success
type: str type: str
sample: '/path/to/remote/file' sample: '/path/to/remote/file'
remote_scp_server:
description: The name of the scp server when file_pull is True.
returned: success
type: str
sample: 'fileserver.example.com'
changed:
description: Indicates wheather or not the file was copied.
returned: success
type: bool
sample: true
''' '''
import hashlib
import os
import re
import time
import traceback
from ansible.module_utils.compat.paramiko import paramiko
from ansible.module_utils.network.nxos.nxos import run_commands
from ansible.module_utils.network.nxos.nxos import nxos_argument_spec, check_args
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native, to_text, to_bytes
try:
from scp import SCPClient
HAS_SCP = True
except ImportError:
HAS_SCP = False
try:
import pexpect
HAS_PEXPECT = True
except ImportError:
HAS_PEXPECT = False
def md5sum_check(module, dst, file_system):
command = 'show file {0}{1} md5sum'.format(file_system, dst)
remote_filehash = run_commands(module, {'command': command, 'output': 'text'})[0]
remote_filehash = to_bytes(remote_filehash, errors='surrogate_or_strict')
local_file = module.params['local_file']
try:
with open(local_file, 'rb') as f:
filecontent = f.read()
except (OSError, IOError) as exc:
module.fail_json(msg="Error reading the file: %s" % to_text(exc))
filecontent = to_bytes(filecontent, errors='surrogate_or_strict')
local_filehash = hashlib.md5(filecontent).hexdigest()
if local_filehash == remote_filehash:
return True
else:
return False
def remote_file_exists(module, dst, file_system='bootflash:'):
command = 'dir {0}/{1}'.format(file_system, dst)
body = run_commands(module, {'command': command, 'output': 'text'})[0]
if 'No such file' in body:
return False
else:
return md5sum_check(module, dst, file_system)
def verify_remote_file_exists(module, dst, file_system='bootflash:'):
command = 'dir {0}/{1}'.format(file_system, dst)
body = run_commands(module, {'command': command, 'output': 'text'})[0]
if 'No such file' in body:
return 0
return body.split()[0].strip()
def local_file_exists(module):
return os.path.isfile(module.params['local_file'])
def get_flash_size(module):
command = 'dir {0}'.format(module.params['file_system'])
body = run_commands(module, {'command': command, 'output': 'text'})[0]
match = re.search(r'(\d+) bytes free', body)
bytes_free = match.group(1)
return int(bytes_free)
def enough_space(module):
flash_size = get_flash_size(module)
file_size = os.path.getsize(module.params['local_file'])
if file_size > flash_size:
return False
return True
def transfer_file_to_device(module, dest):
file_size = os.path.getsize(module.params['local_file'])
if not enough_space(module):
module.fail_json(msg='Could not transfer file. Not enough space on device.')
hostname = module.params['host']
username = module.params['username']
password = module.params['password']
port = module.params['connect_ssh_port']
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(
hostname=hostname,
username=username,
password=password,
port=port)
full_remote_path = '{0}{1}'.format(module.params['file_system'], dest)
scp = SCPClient(ssh.get_transport())
try:
scp.put(module.params['local_file'], full_remote_path)
except Exception:
time.sleep(10)
temp_size = verify_remote_file_exists(
module, dest, file_system=module.params['file_system'])
if int(temp_size) == int(file_size):
pass
else:
module.fail_json(msg='Could not transfer file. There was an error '
'during transfer. Please make sure remote '
'permissions are set.', temp_size=temp_size,
file_size=file_size)
scp.close()
ssh.close()
return True
def copy_file_from_remote(module, local, local_file_directory, file_system='bootflash:'):
hostname = module.params['host']
username = module.params['username']
password = module.params['password']
port = module.params['connect_ssh_port']
try:
child = pexpect.spawn('ssh ' + username + '@' + hostname + ' -p' + str(port))
# response could be unknown host addition or Password
index = child.expect(['yes', '(?i)Password', '#'])
if index == 0:
child.sendline('yes')
child.expect('(?i)Password')
if index == 1:
child.sendline(password)
child.expect('#')
ldir = '/'
if local_file_directory:
dir_array = local_file_directory.split('/')
for each in dir_array:
if each:
child.sendline('mkdir ' + ldir + each)
child.expect('#')
ldir += each + '/'
cmdroot = 'copy scp://'
ruser = module.params['remote_scp_server_user'] + '@'
rserver = module.params['remote_scp_server']
rfile = module.params['remote_file'] + ' '
vrf = ' vrf ' + module.params['vrf']
command = (cmdroot + ruser + rserver + rfile + file_system + ldir + local + vrf)
child.sendline(command)
# response could be remote host connection time out,
# there is already an existing file with the same name,
# unknown host addition or password
index = child.expect(['timed out', 'existing', 'yes', '(?i)password'], timeout=180)
if index == 0:
module.fail_json(msg='Timeout occured due to remote scp server not responding')
elif index == 1:
child.sendline('y')
# response could be unknown host addition or Password
sub_index = child.expect(['yes', '(?i)password'])
if sub_index == 0:
child.sendline('yes')
child.expect('(?i)password')
elif index == 2:
child.sendline('yes')
child.expect('(?i)password')
child.sendline(module.params['remote_scp_server_password'])
fpt = module.params['file_pull_timeout']
# response could be that there is no space left on device,
# permission denied due to wrong user/password,
# remote file non-existent or success,
# timeout due to large file transfer or network too slow,
# success
index = child.expect(['No space', 'Permission denied', 'No such file', pexpect.TIMEOUT, '#'], timeout=fpt)
if index == 0:
module.fail_json(msg='File copy failed due to no space left on the device')
elif index == 1:
module.fail_json(msg='Username/Password for remote scp server is wrong')
elif index == 2:
module.fail_json(msg='File copy failed due to remote file not present')
elif index == 3:
module.fail_json(msg='Timeout occured, please increase "file_pull_timeout" and try again!')
except pexpect.ExceptionPexpect as e:
module.fail_json(msg='%s' % to_native(e), exception=traceback.format_exc())
child.close()
def main():
argument_spec = dict(
local_file=dict(type='str'),
remote_file=dict(type='str'),
file_system=dict(required=False, default='bootflash:'),
connect_ssh_port=dict(required=False, type='int', default=22),
file_pull=dict(type='bool', default=False),
file_pull_timeout=dict(type='int', default=300),
local_file_directory=dict(required=False, type='str'),
remote_scp_server=dict(type='str'),
remote_scp_server_user=dict(type='str'),
remote_scp_server_password=dict(no_log=True),
vrf=dict(required=False, type='str', default='management'),
)
argument_spec.update(nxos_argument_spec)
required_if = [("file_pull", True, ["remote_file", "remote_scp_server"]),
("file_pull", False, ["local_file"])]
required_together = [['remote_scp_server',
'remote_scp_server_user',
'remote_scp_server_password']]
module = AnsibleModule(argument_spec=argument_spec,
required_if=required_if,
required_together=required_together,
supports_check_mode=True)
file_pull = module.params['file_pull']
if file_pull:
if not HAS_PEXPECT:
module.fail_json(
msg='library pexpect is required when file_pull is True but does not appear to be '
'installed. It can be installed using `pip install pexpect`'
)
else:
if paramiko is None:
module.fail_json(
msg='library paramiko is required when file_pull is False but does not appear to be '
'installed. It can be installed using `pip install paramiko`'
)
if not HAS_SCP:
module.fail_json(
msg='library scp is required when file_pull is False but does not appear to be '
'installed. It can be installed using `pip install scp`'
)
warnings = list()
check_args(module, warnings)
results = dict(changed=False, warnings=warnings)
local_file = module.params['local_file']
remote_file = module.params['remote_file']
file_system = module.params['file_system']
local_file_directory = module.params['local_file_directory']
results['transfer_status'] = 'No Transfer'
results['file_system'] = file_system
if file_pull:
src = remote_file.split('/')[-1]
local = local_file or src
if not module.check_mode:
copy_file_from_remote(module, local, local_file_directory, file_system=file_system)
results['transfer_status'] = 'Received'
results['changed'] = True
results['remote_file'] = src
results['local_file'] = local
else:
if not local_file_exists(module):
module.fail_json(msg="Local file {0} not found".format(local_file))
dest = remote_file or os.path.basename(local_file)
remote_exists = remote_file_exists(module, dest, file_system=file_system)
if not remote_exists:
results['changed'] = True
file_exists = False
else:
file_exists = True
if not module.check_mode and not file_exists:
transfer_file_to_device(module, dest)
results['transfer_status'] = 'Sent'
results['local_file'] = local_file
if remote_file is None:
remote_file = os.path.basename(local_file)
results['remote_file'] = remote_file
module.exit_json(**results)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,475 @@
#
# 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 copy
import hashlib
import os
import re
import sys
import time
import traceback
import uuid
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_text, to_bytes
from ansible.module_utils.connection import Connection
from ansible.plugins.action import ActionBase
from ansible.module_utils.six.moves.urllib.parse import urlsplit
from ansible.utils.display import Display
from ansible.module_utils.compat.paramiko import paramiko
from ansible.module_utils.network.nxos.nxos import run_commands
from ansible.module_utils._text import to_native, to_text, to_bytes
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils import six
try:
from scp import SCPClient
HAS_SCP = True
except ImportError:
HAS_SCP = False
try:
import pexpect
HAS_PEXPECT = True
except ImportError:
HAS_PEXPECT = False
display = Display()
class ActionModule(ActionBase):
def process_playbook_values(self):
''' Get playbook values and perform input validation '''
argument_spec = dict(
vrf=dict(type='str', default='management'),
connect_ssh_port=dict(type='int', default=22),
file_system=dict(type='str', default='bootflash:'),
file_pull=dict(type='bool', default=False),
file_pull_timeout=dict(type='int', default=300),
file_pull_compact=dict(type='bool', default=False),
file_pull_kstack=dict(type='bool', default=False),
local_file=dict(type='str'),
local_file_directory=dict(type='str'),
remote_file=dict(type='str'),
remote_scp_server=dict(type='str'),
remote_scp_server_user=dict(type='str'),
remote_scp_server_password=dict(no_log=True),
)
playvals = {}
# Process key value pairs from playbook task
for key in argument_spec.keys():
playvals[key] = self._task.args.get(key, argument_spec[key].get('default'))
if playvals[key] is None:
continue
if argument_spec[key].get('type') is None:
argument_spec[key]['type'] = 'str'
type_ok = False
type = argument_spec[key]['type']
if type == 'str':
if isinstance(playvals[key], six.string_types):
type_ok = True
elif type == 'int':
if isinstance(playvals[key], int):
type_ok = True
elif type == 'bool':
if isinstance(playvals[key], bool):
type_ok = True
else:
raise AnsibleError('Unrecognized type <{0}> for playbook parameter <{1}>'.format(type, key))
if not type_ok:
raise AnsibleError('Playbook parameter <{0}> value should be of type <{1}>'.format(key, type))
# Validate playbook dependencies
if playvals['file_pull']:
if playvals.get('remote_file') is None:
raise AnsibleError('Playbook parameter <remote_file> required when <file_pull> is True')
if playvals.get('remote_scp_server') is None:
raise AnsibleError('Playbook parameter <remote_scp_server> required when <file_pull> is True')
if playvals['remote_scp_server'] or \
playvals['remote_scp_server_user'] or \
playvals['remote_scp_server_password']:
if None in (playvals['remote_scp_server'],
playvals['remote_scp_server_user'],
playvals['remote_scp_server_password']):
params = '<remote_scp_server>, <remote_scp_server_user>, ,remote_scp_server_password>'
raise AnsibleError('Playbook parameters {0} must all be set together'.format(params))
return playvals
def check_library_dependencies(self, file_pull):
if file_pull:
if not HAS_PEXPECT:
msg = 'library pexpect is required when file_pull is True but does not appear to be '
msg += 'installed. It can be installed using `pip install pexpect`'
raise AnsibleError(msg)
else:
if paramiko is None:
msg = 'library paramiko is required when file_pull is False but does not appear to be '
msg += 'installed. It can be installed using `pip install paramiko`'
raise AnsibleError(msg)
if not HAS_SCP:
msg = 'library scp is required when file_pull is False but does not appear to be '
msg += 'installed. It can be installed using `pip install scp`'
raise AnsibleError(msg)
def md5sum_check(self, dst, file_system):
command = 'show file {0}{1} md5sum'.format(file_system, dst)
remote_filehash = self.conn.exec_command(command)
remote_filehash = to_bytes(remote_filehash, errors='surrogate_or_strict')
local_file = self.playvals['local_file']
try:
with open(local_file, 'rb') as f:
filecontent = f.read()
except (OSError, IOError) as exc:
raise AnsibleError('Error reading the file: {0}'.format(to_text(exc)))
filecontent = to_bytes(filecontent, errors='surrogate_or_strict')
local_filehash = hashlib.md5(filecontent).hexdigest()
if local_filehash == remote_filehash:
return True
else:
return False
def remote_file_exists(self, remote_file, file_system):
command = 'dir {0}/{1}'.format(file_system, remote_file)
body = self.conn.exec_command(command)
if 'No such file' in body:
return False
else:
return self.md5sum_check(remote_file, file_system)
def verify_remote_file_exists(self, dst, file_system):
command = 'dir {0}/{1}'.format(file_system, dst)
body = self.conn.exec_command(command)
if 'No such file' in body:
return 0
return body.split()[0].strip()
def local_file_exists(self, file):
return os.path.isfile(file)
def get_flash_size(self, file_system):
command = 'dir {0}'.format(file_system)
body = self.conn.exec_command(command)
match = re.search(r'(\d+) bytes free', body)
if match:
bytes_free = match.group(1)
return int(bytes_free)
match = re.search(r'No such file or directory', body)
if match:
raise AnsibleError('Invalid nxos filesystem {0}'.format(file_system))
else:
raise AnsibleError('Unable to determine size of filesystem {0}'.format(file_system))
def enough_space(self, file, file_system):
flash_size = self.get_flash_size(file_system)
file_size = os.path.getsize(file)
if file_size > flash_size:
return False
return True
def transfer_file_to_device(self, remote_file):
timeout = self.socket_timeout
local_file = self.playvals['local_file']
file_system = self.playvals['file_system']
file_size = os.path.getsize(local_file)
if not self.enough_space(local_file, file_system):
raise AnsibleError('Could not transfer file. Not enough space on device.')
# frp = full_remote_path, flp = full_local_path
frp = '{0}{1}'.format(file_system, remote_file)
flp = os.path.join(os.path.abspath(local_file))
try:
self.conn.copy_file(source=flp, destination=frp, proto='scp', timeout=timeout)
except Exception as exc:
self.results['failed'] = True
self.results['msg'] = ('Exception received : %s' % exc)
def file_push(self):
local_file = self.playvals['local_file']
remote_file = self.playvals['remote_file'] or os.path.basename(local_file)
file_system = self.playvals['file_system']
if not self.local_file_exists(local_file):
raise AnsibleError('Local file {0} not found'.format(local_file))
remote_file = remote_file or os.path.basename(local_file)
remote_exists = self.remote_file_exists(remote_file, file_system)
if not remote_exists:
self.results['changed'] = True
file_exists = False
else:
self.results['transfer_status'] = 'No Transfer: File already copied to remote device.'
file_exists = True
if not self.play_context.check_mode and not file_exists:
self.transfer_file_to_device(remote_file)
self.results['transfer_status'] = 'Sent: File copied to remote device.'
self.results['local_file'] = local_file
if remote_file is None:
remote_file = os.path.basename(local_file)
self.results['remote_file'] = remote_file
def copy_file_from_remote(self, local, local_file_directory, file_system):
self.results['failed'] = False
nxos_hostname = self.play_context.remote_addr
nxos_username = self.play_context.remote_user
nxos_password = self.play_context.password
port = self.playvals['connect_ssh_port']
# Build copy command components that will be used to initiate copy from the nxos device.
cmdroot = 'copy scp://'
ruser = self.playvals['remote_scp_server_user'] + '@'
rserver = self.playvals['remote_scp_server']
rfile = self.playvals['remote_file'] + ' '
vrf = ' vrf ' + self.playvals['vrf']
local_dir_root = '/'
if self.playvals['file_pull_compact']:
compact = ' compact '
else:
compact = ''
if self.playvals['file_pull_kstack']:
kstack = ' use-kstack '
else:
kstack = ''
def process_outcomes(session, timeout=None):
if timeout is None:
timeout = 10
outcome = {}
outcome['user_response_required'] = False
outcome['password_prompt_detected'] = False
outcome['existing_file_with_same_name'] = False
outcome['final_prompt_detected'] = False
outcome['copy_complete'] = False
outcome['expect_timeout'] = False
outcome['error'] = False
outcome['error_data'] = None
# Possible outcomes key:
# 0) - Are you sure you want to continue connecting (yes/no)
# 1) - Password: or @servers's password:
# 2) - Warning: There is already a file existing with this name. Do you want to overwrite (y/n)?[n]
# 3) - Timeout conditions
# 4) - No space on nxos device file_system
# 5) - Username/Password or file permission issues
# 6) - File does not exist on remote scp server
# 7) - invalid nxos command
# 8) - compact option not supported
# 9) - compaction attempt failed
# 10) - other failures like attempting to compact non image file
# 11) - failure to resolve hostname
# 12) - Too many authentication failures
# 13) - Copy to / from this server not permitted
# 14) - Copy completed without issues
# 15) - nxos_router_prompt#
# 16) - pexpect timeout
possible_outcomes = ['yes',
'(?i)Password',
'file existing with this name',
'timed out',
'(?i)No space.*#',
'(?i)Permission denied.*#',
'(?i)No such file.*#',
'.*Invalid command.*#',
'Compaction is not supported on this platform.*#',
'Compact of.*failed.*#',
'(?i)Failed.*#',
'(?i)Could not resolve hostname',
'(?i)Too many authentication failures',
r'(?i)Copying to\/from this server name is not permitted',
'(?i)Copy complete',
r'#\s',
pexpect.TIMEOUT]
index = session.expect(possible_outcomes, timeout=timeout)
# Each index maps to items in possible_outcomes
if index == 0:
outcome['user_response_required'] = True
return outcome
elif index == 1:
outcome['password_prompt_detected'] = True
return outcome
elif index == 2:
outcome['existing_file_with_same_name'] = True
return outcome
elif index in [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]:
before = session.before.strip().replace(' \x08', '')
after = session.after.strip().replace(' \x08', '')
outcome['error'] = True
outcome['error_data'] = 'COMMAND {0} ERROR {1}'.format(before, after)
return outcome
elif index == 14:
outcome['copy_complete'] = True
return outcome
elif index == 15:
outcome['final_prompt_detected'] = True
return outcome
elif index == 16:
# The before property will contain all text up to the expected string pattern.
# The after string will contain the text that was matched by the expected pattern.
outcome['expect_timeout'] = True
outcome['error_data'] = 'Expect Timeout error occured: BEFORE {0} AFTER {1}'.format(session.before, session.after)
return outcome
else:
outcome['error'] = True
outcome['error_data'] = 'Unrecognized error occured: BEFORE {0} AFTER {1}'.format(session.before, session.after)
return outcome
return outcome
# Spawn pexpect connection to NX-OS device.
nxos_session = pexpect.spawn('ssh ' + nxos_username + '@' + nxos_hostname + ' -p' + str(port))
# There might be multiple user_response_required prompts or intermittent timeouts
# spawning the expect session so loop up to 5 times during the spwan process.
for connect_attempt in range(6):
outcome = process_outcomes(nxos_session)
if outcome['user_response_required']:
nxos_session.sendline('yes')
continue
if outcome['password_prompt_detected']:
nxos_session.sendline(nxos_password)
continue
if outcome['final_prompt_detected']:
break
if outcome['error'] or outcome['expect_timeout']:
self.results['failed'] = True
self.results['error_data'] = 'Failed to spawn expect session! ' + outcome['error_data']
return
else:
# The before property will contain all text up to the expected string pattern.
# The after string will contain the text that was matched by the expected pattern.
msg = 'After {0} attempts, failed to spawn pexpect session to {1}'
msg += 'BEFORE: {2}, AFTER: {3}'
raise AnsibleError(msg.format(connect_attempt, nxos_hostname, nxos_session.before, nxos_session.before))
# Create local file directory under NX-OS filesystem if
# local_file_directory playbook parameter is set.
if local_file_directory:
dir_array = local_file_directory.split('/')
for each in dir_array:
if each:
mkdir_cmd = 'mkdir ' + local_dir_root + each
nxos_session.sendline(mkdir_cmd)
outcome = process_outcomes(nxos_session)
if outcome['error'] or outcome['expect_timeout']:
self.results['mkdir_cmd'] = mkdir_cmd
self.results['failed'] = True
self.results['error_data'] = outcome['error_data']
return
local_dir_root += each + '/'
# Initiate file copy
copy_cmd = (cmdroot + ruser + rserver + rfile + file_system + local_dir_root + local + compact + vrf + kstack)
self.results['copy_cmd'] = copy_cmd
nxos_session.sendline(copy_cmd)
for copy_attempt in range(6):
outcome = process_outcomes(nxos_session, self.playvals['file_pull_timeout'])
if outcome['user_response_required']:
nxos_session.sendline('yes')
continue
if outcome['password_prompt_detected']:
nxos_session.sendline(self.playvals['remote_scp_server_password'])
continue
if outcome['existing_file_with_same_name']:
nxos_session.sendline('y')
continue
if outcome['copy_complete']:
self.results['transfer_status'] = 'Received: File copied/pulled to nxos device from remote scp server.'
break
if outcome['error'] or outcome['expect_timeout']:
self.results['failed'] = True
self.results['error_data'] = outcome['error_data']
return
else:
# The before property will contain all text up to the expected string pattern.
# The after string will contain the text that was matched by the expected pattern.
msg = 'After {0} attempts, failed to copy file to {1}'
msg += 'BEFORE: {2}, AFTER: {3}, CMD: {4}'
raise AnsibleError(msg.format(copy_attempt, nxos_hostname, nxos_session.before, nxos_session.before, copy_cmd))
def file_pull(self):
local_file = self.playvals['local_file']
remote_file = self.playvals['remote_file']
file_system = self.playvals['file_system']
# Note: This is the local file directory on the remote nxos device.
local_file_dir = self.playvals['local_file_directory']
local_file = local_file or self.playvals['remote_file'].split('/')[-1]
if not self.play_context.check_mode:
self.copy_file_from_remote(local_file, local_file_dir, file_system)
if not self.results['failed']:
self.results['changed'] = True
self.results['remote_file'] = remote_file
if local_file_dir:
dir = local_file_dir
else:
dir = ''
self.results['local_file'] = file_system + dir + '/' + local_file
self.results['remote_scp_server'] = self.playvals['remote_scp_server']
# This is the main run method for the action plugin to copy files
def run(self, tmp=None, task_vars=None):
socket_path = None
self.play_context = copy.deepcopy(self._play_context)
self.results = super(ActionModule, self).run(task_vars=task_vars)
if self.play_context.connection != 'network_cli':
# Plugin is supported only with network_cli
self.results['failed'] = True
self.results['msg'] = ('Connection type must be <network_cli>')
return self.results
# Get playbook values
self.playvals = self.process_playbook_values()
file_pull = self.playvals['file_pull']
self.check_library_dependencies(file_pull)
if socket_path is None:
socket_path = self._connection.socket_path
self.conn = Connection(socket_path)
self.socket_timeout = self.conn.get_option('persistent_command_timeout')
# This action plugin support two modes of operation.
# - file_pull is False - Push files from the ansible controller to nxos switch.
# - file_pull is True - Initiate copy from the device to pull files to the nxos switch.
self.results['transfer_status'] = 'No Transfer'
self.results['file_system'] = self.playvals['file_system']
if file_pull:
self.file_pull()
else:
self.file_push()
return self.results

View file

@ -1,2 +1,5 @@
dependencies: dependencies:
- prepare_nxos_tests # prepare_nxos_tests is not needed for this test and simply adds overhead.
# This can be uncommented in the future if needed.
#
# - prepare_nxos_tests

View file

@ -25,9 +25,3 @@
with_items: "{{ test_items }}" with_items: "{{ test_items }}"
loop_control: loop_control:
loop_var: test_case_to_run loop_var: test_case_to_run
- name: run test cases (connection=local)
include: "{{ test_case_to_run }} ansible_connection=local"
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

View file

@ -0,0 +1,65 @@
---
- debug: msg="START nxos_file_copy input_validation test"
- name: "Input Validation - param should be type <str>"
nxos_file_copy:
remote_file: 500
register: result
ignore_errors: true
- assert:
that:
- result is search('Playbook parameter <remote_file> value should be of type <str>')
- name: "Input Validation - param should be type <int>"
nxos_file_copy:
file_pull_timeout: 'foobar'
register: result
ignore_errors: true
- assert:
that:
- result is search('Playbook parameter <file_pull_timeout> value should be of type <int>')
- name: "Input Validation - param should be type <bool>"
nxos_file_copy:
file_pull: 'foobar'
register: result
ignore_errors: true
- assert:
that:
- result is search('Playbook parameter <file_pull> value should be of type <bool>')
- name: "Input Validation - param <file_pull> <remote_file> dependency"
nxos_file_copy:
file_pull: True
register: result
ignore_errors: true
- assert:
that:
- result is search('Playbook parameter <remote_file> required when <file_pull> is True')
- name: "Input Validation - param <file_pull> <remote_scp_server> dependency"
nxos_file_copy:
file_pull: True
remote_file: "/network-integration.cfg"
register: result
ignore_errors: true
- assert:
that:
- result is search('Playbook parameter <remote_scp_server> required when <file_pull> is True')
- name: "Input Validation - remote_scp_server params together"
nxos_file_copy:
remote_scp_server: "{{ inventory_hostname_short }}"
register: result
ignore_errors: true
- assert:
that:
- result is search('Playbook parameters <remote_scp_server>, <remote_scp_server_user>, ,remote_scp_server_password> must all be set together')
- debug: msg="END nxos_file_copy input_validation test"

View file

@ -0,0 +1,133 @@
---
- debug: msg="START nxos_file_copy negative test"
# This test uses a file that is committed to the Ansible core repository.
# The file name and relative path is test/integration/targets/network-integration.cfg
- set_fact: test_source_file="network-integration.cfg"
- set_fact: test_destination_file="test_destination_file"
# -------------------------
# Tests for file_pull False
# -------------------------
- name: "Attempt to copy file to invalid file_system"
nxos_file_copy:
file_pull: False
local_file: "./{{ test_source_file }}"
file_system: "invalid_media_type:"
connect_ssh_port: "{{ ansible_ssh_port }}"
register: result
ignore_errors: true
- assert:
that:
- result is search('Invalid nxos filesystem invalid_media_type:')
- name: "Attempt to copy source file that does not exist on Ansible controller"
nxos_file_copy:
file_pull: False
local_file: "./{{ test_source_file }}_does_not_exist"
file_system: "bootflash:"
connect_ssh_port: "{{ ansible_ssh_port }}"
register: result
ignore_errors: true
- assert:
that:
- result is search('Local file ./network-integration.cfg_does_not_exist not found')
# -------------------------
# Tests for file_pull True
# -------------------------
- name: "Try and copy file using an invalid remote scp server name"
nxos_file_copy:
file_pull: True
file_pull_timeout: 10
remote_file: "/{{ test_destination_file }}"
local_file: "{{ test_destination_file }}_copy"
local_file_directory: "dir1/dir2/dir3"
remote_scp_server: "scp_server_gone.example.com"
remote_scp_server_user: "{{ ansible_ssh_user }}"
remote_scp_server_password: "{{ ansible_ssh_pass }}"
register: result
ignore_errors: true
- assert:
that:
- "result.changed == false"
- "'copy scp:' in result.copy_cmd"
- "'bootflash:' in result.file_system"
- "'No Transfer' in result.transfer_status"
- assert:
that:
- result.error_data is search("ERROR Could not resolve hostname|Copying to.*from this server name is not permitted")
- name: "Try and copy file using an invalid remote scp server ip address"
nxos_file_copy:
file_pull: True
file_pull_timeout: 300
remote_file: "/{{ test_destination_file }}"
local_file: "{{ test_destination_file }}_copy"
local_file_directory: "dir1/dir2/dir3"
remote_scp_server: "192.168.55.55"
remote_scp_server_user: "{{ ansible_ssh_user }}"
remote_scp_server_password: "{{ ansible_ssh_pass }}"
register: result
ignore_errors: true
- assert:
that:
- "result.changed == false"
- "'copy scp:' in result.copy_cmd"
- "'timed out' in result.error_data"
- "'bootflash:' in result.file_system"
- "'No Transfer' in result.transfer_status"
# Sometimes the previous negative test needs a few seconds after the timeout
# failure before the next negative test is executed.
- pause:
seconds: 10
- name: "Try and copy file using an invalid username"
nxos_file_copy:
file_pull: True
file_pull_timeout: 10
remote_file: "/{{ test_destination_file }}"
local_file: "{{ test_destination_file }}_copy"
local_file_directory: "dir1/dir2/dir3"
remote_scp_server: "{{ inventory_hostname_short }}"
remote_scp_server_user: "invalid_user_name"
remote_scp_server_password: "{{ ansible_ssh_pass }}"
register: result
ignore_errors: true
- assert:
that:
- "result.changed == false"
- "'copy scp:' in result.copy_cmd"
- "'Too many authentication failures' in result.error_data"
- "'bootflash:' in result.file_system"
- "'No Transfer' in result.transfer_status"
- name: "Try and copy file using an invalid password"
nxos_file_copy:
file_pull: True
file_pull_timeout: 10
remote_file: "/{{ test_destination_file }}"
local_file: "{{ test_destination_file }}_copy"
local_file_directory: "dir1/dir2/dir3"
remote_scp_server: "{{ inventory_hostname_short }}"
remote_scp_server_user: "{{ ansible_ssh_user }}"
remote_scp_server_password: "invalid_password"
register: result
ignore_errors: true
- assert:
that:
- "result.changed == false"
- "'copy scp:' in result.copy_cmd"
- "'Too many authentication failures' in result.error_data"
- "'bootflash:' in result.file_system"
- "'No Transfer' in result.transfer_status"
- debug: msg="END nxos_file_copy negative test"

View file

@ -1,12 +1,18 @@
--- ---
- debug: msg="START connection={{ ansible_connection }} nxos_file_copy sanity test" - debug: msg="START connection={{ ansible_connection }} nxos_file_copy sanity test"
# This test uses a file that is committed to the Ansible core repository.
# The file name and relative path is test/integration/targets/network-integration.cfg
- set_fact: test_source_file="network-integration.cfg"
- set_fact: test_destination_file="test_destination_file"
- name: "Setup - Remove existing file" - name: "Setup - Remove existing file"
nxos_command: &remove_file nxos_command: &remove_file
commands: commands:
- terminal dont-ask - terminal dont-ask
- delete network-integration.cfg - "delete {{ test_source_file }}"
- delete bootflash:/dir1/dir2/dir3/network-integration_copy.cfg - "delete {{ test_destination_file }}"
- "delete bootflash:/dir1/dir2/dir3/*"
- rmdir dir1/dir2/dir3 - rmdir dir1/dir2/dir3
- rmdir dir1/dir2 - rmdir dir1/dir2
- rmdir dir1 - rmdir dir1
@ -18,18 +24,22 @@
state: enabled state: enabled
- block: - block:
- name: "Copy network-integration.cfg to bootflash" - name: "Copy {{ test_source_file }} file from Ansible controller to bootflash"
nxos_file_copy: &copy_file_same_name nxos_file_copy: &copy_file_same_name
local_file: "./network-integration.cfg" local_file: "./{{ test_source_file }}"
file_system: "bootflash:" file_system: "bootflash:"
connect_ssh_port: "{{ ansible_ssh_port }}" connect_ssh_port: "{{ ansible_ssh_port }}"
register: result register: result
- assert: &true - assert:
that: that:
- "result.changed == true" - "result.changed == true"
- "'bootflash:' in result.file_system"
- "'./{{ test_source_file }}' in result.local_file"
- "'network-integration.cfg' in result.remote_file"
- "'Sent: File copied to remote device.' in result.transfer_status"
- name: "Check Idempotence - Copy network-integration.cfg to bootflash" - name: "Idempotence - Copy {{ test_source_file }} file from Ansible controller to bootflash"
nxos_file_copy: *copy_file_same_name nxos_file_copy: *copy_file_same_name
register: result register: result
@ -41,47 +51,77 @@
nxos_command: *remove_file nxos_command: *remove_file
register: result register: result
- name: "Copy inventory.networking.template to bootflash as another name" - name: "Copy {{ test_source_file }} file from Ansible controller to bootflash renamed as {{ test_destination_file }}"
nxos_file_copy: &copy_file_different_name nxos_file_copy: &copy_file_different_name
local_file: "./inventory.networking.template" local_file: "./{{ test_source_file }}"
remote_file: "network-integration.cfg" remote_file: "{{ test_destination_file }}"
file_system: "bootflash:" file_system: "bootflash:"
connect_ssh_port: "{{ ansible_ssh_port }}" connect_ssh_port: "{{ ansible_ssh_port }}"
register: result register: result
- assert: *true - assert:
that:
- "result.changed == true"
- "'bootflash:' in result.file_system"
- "'./{{ test_source_file }}' in result.local_file"
- "'{{ test_destination_file }}' in result.remote_file"
- "'Sent: File copied to remote device.' in result.transfer_status"
- name: "Check Idempotence - Copy inventory.networking.template to bootflash as another name" - name: "Idempotence - Copy {{ test_source_file }} file from Ansible controller to bootflash renamed as {{ test_destination_file }}"
nxos_file_copy: *copy_file_different_name nxos_file_copy: *copy_file_different_name
register: result register: result
- name: "Verify file_pull true options have no impact when file_true is false"
nxos_file_copy:
file_pull: False
file_pull_timeout: 1200
file_pull_compact: True
file_pull_kstack: True
local_file_directory: "dir1/dir2/dir3"
remote_scp_server: "{{ inventory_hostname_short }}"
remote_scp_server_user: "{{ ansible_ssh_user }}"
remote_scp_server_password: "{{ ansible_ssh_pass }}"
# Parameters above are only used when file_pull is True
local_file: "./{{ test_source_file }}"
remote_file: "{{ test_destination_file }}"
file_system: "bootflash:"
connect_ssh_port: "{{ ansible_ssh_port }}"
register: result
- assert: *false - assert: *false
- block: # This step validates the ability to initiate the copy from the nxos device
- name: "Copy file using file_pull" # to pull a file from a remote file server to the nxos bootflash device.
#
# In this case we are using the nxos device as the remote file server so we
# copy a file from bootflash: to bootflash:dir1/dir2/dir3
- name: "Initiate copy from nxos device to copy {{ test_destination_file }} to bootflash:dir1/dir2/dir3/{{ test_destination_file }}_copy"
nxos_file_copy: &copy_pull nxos_file_copy: &copy_pull
file_pull: True file_pull: True
file_pull_timeout: 1200 file_pull_timeout: 30
remote_file: "/network-integration.cfg" remote_file: "/{{ test_destination_file }}"
local_file: "network-integration_copy.cfg" local_file: "{{ test_destination_file }}_copy"
local_file_directory: "dir1/dir2/dir3" local_file_directory: "dir1/dir2/dir3"
remote_scp_server: "{{ inventory_hostname_short }}" remote_scp_server: "{{ inventory_hostname_short }}"
remote_scp_server_user: "{{ ansible_ssh_user }}" remote_scp_server_user: "{{ ansible_ssh_user }}"
remote_scp_server_password: "{{ ansible_ssh_pass }}" remote_scp_server_password: "{{ ansible_ssh_pass }}"
register: result register: result
- assert: *true - assert: &overwrite
that:
- "result.changed == true"
- "'copy scp:' in result.copy_cmd"
- "'bootflash:' in result.file_system"
- "'bootflash:dir1/dir2/dir3/{{ test_destination_file }}_copy' in result.local_file"
- "'/{{ test_destination_file }}' in result.remote_file"
- "'Received: File copied/pulled to nxos device from remote scp server.' in result.transfer_status"
- "'{{ inventory_hostname_short }}' in result.remote_scp_server"
- name: "Overwrite the file" - name: "Overwrite the file"
nxos_file_copy: *copy_pull nxos_file_copy: *copy_pull
register: result register: result
- assert: *true - assert: *overwrite
ignore_errors: yes
rescue:
- debug: msg="TRANSPORT:CLI nxos_file_copy failure detected"
always: always:

View file

@ -12,6 +12,6 @@
- assert: - assert:
that: that:
- result.failed and result.msg is search('Transport') - result.failed and result.msg is search('Connection type must be <network_cli>')
- debug: msg="END nxapi/badtransport.yaml" - debug: msg="END nxapi/badtransport.yaml"

View file

@ -4221,12 +4221,6 @@ lib/ansible/modules/network/nxos/nxos_feature.py validate-modules:doc-default-do
lib/ansible/modules/network/nxos/nxos_feature.py validate-modules:doc-default-incompatible-type lib/ansible/modules/network/nxos/nxos_feature.py validate-modules:doc-default-incompatible-type
lib/ansible/modules/network/nxos/nxos_feature.py validate-modules:parameter-type-not-in-doc lib/ansible/modules/network/nxos/nxos_feature.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/nxos/nxos_feature.py validate-modules:doc-missing-type lib/ansible/modules/network/nxos/nxos_feature.py validate-modules:doc-missing-type
lib/ansible/modules/network/nxos/nxos_file_copy.py future-import-boilerplate
lib/ansible/modules/network/nxos/nxos_file_copy.py metaclass-boilerplate
lib/ansible/modules/network/nxos/nxos_file_copy.py validate-modules:doc-default-does-not-match-spec
lib/ansible/modules/network/nxos/nxos_file_copy.py validate-modules:doc-default-incompatible-type
lib/ansible/modules/network/nxos/nxos_file_copy.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/network/nxos/nxos_file_copy.py validate-modules:doc-missing-type
lib/ansible/modules/network/nxos/nxos_gir.py future-import-boilerplate lib/ansible/modules/network/nxos/nxos_gir.py future-import-boilerplate
lib/ansible/modules/network/nxos/nxos_gir.py metaclass-boilerplate lib/ansible/modules/network/nxos/nxos_gir.py metaclass-boilerplate
lib/ansible/modules/network/nxos/nxos_gir.py validate-modules:doc-default-does-not-match-spec lib/ansible/modules/network/nxos/nxos_gir.py validate-modules:doc-default-does-not-match-spec