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:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
- Rewritten as a plugin by (@mikewiebe)
notes:
- 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),
8.3(0)
8.3(0), 9.2, 9.3
- When pushing files (file_pull is False) to the NXOS device,
feature scp-server must be enabled.
- When pulling files (file_pull is True) to the NXOS device,
@ -56,7 +57,7 @@ options:
description:
- When (file_pull is False) this is the path to the local file on the Ansible controller.
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:
description:
- 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.
file_system:
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
their default values.
default: "bootflash:"
connect_ssh_port:
description:
- SSH port to connect to server during transfer of file
- SSH server port used for file transfer.
default: 22
version_added: "2.5"
file_pull:
@ -85,33 +86,53 @@ options:
type: bool
default: False
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:
description:
- 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
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"
file_pull_timeout:
description:
- Use this parameter to set timeout in seconds, when transferring
large files or when the network is slow.
- When (file_pull is False), this is not used.
default: 300
version_added: "2.7"
remote_scp_server:
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.
- When (file_pull is False), this is not used.
version_added: "2.7"
remote_scp_server_user:
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.
- When (file_pull is False), this is not used.
version_added: "2.7"
remote_scp_server_password:
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.
- When (file_pull is False), this is not used.
version_added: "2.7"
vrf:
description:
@ -143,8 +164,7 @@ EXAMPLES = '''
RETURN = '''
transfer_status:
description: Whether a file was transferred. "No Transfer" or "Sent".
If file_pull is successful, it is set to "Received".
description: Whether a file was transferred to the nxos device.
returned: success
type: str
sample: 'Sent'
@ -158,300 +178,14 @@ remote_file:
returned: success
type: str
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:
- 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 }}"
loop_control:
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,35 +1,45 @@
---
- 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"
nxos_command: &remove_file
commands:
- terminal dont-ask
- delete network-integration.cfg
- delete bootflash:/dir1/dir2/dir3/network-integration_copy.cfg
- "delete {{ test_source_file }}"
- "delete {{ test_destination_file }}"
- "delete bootflash:/dir1/dir2/dir3/*"
- rmdir dir1/dir2/dir3
- rmdir dir1/dir2
- rmdir dir1
ignore_errors: yes
- name: "Setup - Turn on feature scp-server"
nxos_feature:
nxos_feature:
feature: scp-server
state: enabled
- 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
local_file: "./network-integration.cfg"
local_file: "./{{ test_source_file }}"
file_system: "bootflash:"
connect_ssh_port: "{{ ansible_ssh_port }}"
register: result
- assert: &true
- assert:
that:
- "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
register: result
@ -41,47 +51,77 @@
nxos_command: *remove_file
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
local_file: "./inventory.networking.template"
remote_file: "network-integration.cfg"
local_file: "./{{ test_source_file }}"
remote_file: "{{ test_destination_file }}"
file_system: "bootflash:"
connect_ssh_port: "{{ ansible_ssh_port }}"
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
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
- block:
- name: "Copy file using file_pull"
nxos_file_copy: &copy_pull
file_pull: True
file_pull_timeout: 1200
remote_file: "/network-integration.cfg"
local_file: "network-integration_copy.cfg"
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 }}"
register: result
# This step validates the ability to initiate the copy from the nxos device
# 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
file_pull: True
file_pull_timeout: 30
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: "{{ ansible_ssh_pass }}"
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"
nxos_file_copy: *copy_pull
register: result
- name: "Overwrite the file"
nxos_file_copy: *copy_pull
register: result
- assert: *true
ignore_errors: yes
rescue:
- debug: msg="TRANSPORT:CLI nxos_file_copy failure detected"
- assert: *overwrite
always:

View file

@ -12,6 +12,6 @@
- assert:
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"

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:parameter-type-not-in-doc
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 metaclass-boilerplate
lib/ansible/modules/network/nxos/nxos_gir.py validate-modules:doc-default-does-not-match-spec