mirror of https://github.com/ansible/ansible.git
Contributing lib/ansible/modules/network/cloudengine/ce_file_copy.py module to manage HUAWEI data center CloudEngine (#22045)
* add ce_file_copy add ce_file_copy * fix review issuepull/25047/head
parent
182d65d519
commit
a137349522
@ -0,0 +1,395 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'status': ['preview'],
|
||||||
|
'supported_by': 'community',
|
||||||
|
'metadata_version': '1.0'}
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: ce_file_copy
|
||||||
|
version_added: "2.4"
|
||||||
|
short_description: Copy a file to a remote cloudengine device over SCP on HUAWEI CloudEngine switches.
|
||||||
|
description:
|
||||||
|
- Copy a file to a remote cloudengine device over SCP on HUAWEI CloudEngine switches.
|
||||||
|
author:
|
||||||
|
- Zhou Zhijin (@CloudEngine-Ansible)
|
||||||
|
notes:
|
||||||
|
- The feature must be enabled with feature scp-server.
|
||||||
|
- If the file is already present, no transfer will take place.
|
||||||
|
options:
|
||||||
|
local_file:
|
||||||
|
description:
|
||||||
|
- Path to local file. Local directory must exist.
|
||||||
|
The maximum length of local_file is 4096.
|
||||||
|
required: true
|
||||||
|
remote_file:
|
||||||
|
description:
|
||||||
|
- Remote file path of the copy. Remote directories must exist.
|
||||||
|
If omitted, the name of the local file will be used.
|
||||||
|
The maximum length of remote_file is 4096.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
file_system:
|
||||||
|
description:
|
||||||
|
- The remote file system of the device. If omitted,
|
||||||
|
devices that support a file_system parameter will use
|
||||||
|
their default values.
|
||||||
|
File system indicates the storage medium and can be set to as follows,
|
||||||
|
1) 'flash:' is root directory of the flash memory on the master MPU.
|
||||||
|
2) 'slave#flash:' is root directory of the flash memory on the slave MPU.
|
||||||
|
If no slave MPU exists, this drive is unavailable.
|
||||||
|
3) 'chassis ID/slot number#flash:' is root directory of the flash memory on
|
||||||
|
a device in a stack. For example, 1/5#flash indicates the flash memory
|
||||||
|
whose chassis ID is 1 and slot number is 5.
|
||||||
|
required: false
|
||||||
|
default: 'flash:'
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: File copy test
|
||||||
|
hosts: cloudengine
|
||||||
|
connection: local
|
||||||
|
gather_facts: no
|
||||||
|
vars:
|
||||||
|
cli:
|
||||||
|
host: "{{ inventory_hostname }}"
|
||||||
|
port: "{{ ansible_ssh_port }}"
|
||||||
|
username: "{{ username }}"
|
||||||
|
password: "{{ password }}"
|
||||||
|
transport: cli
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
- name: "Copy a local file to remote device"
|
||||||
|
ce_file_copy:
|
||||||
|
local_file: /usr/vrpcfg.cfg
|
||||||
|
remote_file: /vrpcfg.cfg
|
||||||
|
file_system: 'flash:'
|
||||||
|
provider: "{{ cli }}"
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
changed:
|
||||||
|
description: check to see if a change was made on the device
|
||||||
|
returned: always
|
||||||
|
type: boolean
|
||||||
|
sample: true
|
||||||
|
transfer_result:
|
||||||
|
description: information about transfer result.
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: 'The local file has been successfully transferred to the device.'
|
||||||
|
local_file:
|
||||||
|
description: The path of the local file.
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: '/usr/work/vrpcfg.zip'
|
||||||
|
remote_file:
|
||||||
|
description: The path of the remote file.
|
||||||
|
returned: always
|
||||||
|
type: string
|
||||||
|
sample: '/vrpcfg.zip'
|
||||||
|
'''
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from xml.etree import ElementTree
|
||||||
|
import paramiko
|
||||||
|
from ansible.module_utils.shell import ShellError
|
||||||
|
from ansible.module_utils.basic import get_exception, AnsibleModule
|
||||||
|
from ansible.module_utils.ce import ce_argument_spec, run_commands, get_nc_config
|
||||||
|
|
||||||
|
try:
|
||||||
|
from scp import SCPClient
|
||||||
|
HAS_SCP = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_SCP = False
|
||||||
|
|
||||||
|
CE_NC_GET_FILE_INFO = """
|
||||||
|
<filter type="subtree">
|
||||||
|
<vfm xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0">
|
||||||
|
<dirs>
|
||||||
|
<dir>
|
||||||
|
<fileName>%s</fileName>
|
||||||
|
<dirName>%s</dirName>
|
||||||
|
<DirSize></DirSize>
|
||||||
|
</dir>
|
||||||
|
</dirs>
|
||||||
|
</vfm>
|
||||||
|
</filter>
|
||||||
|
"""
|
||||||
|
|
||||||
|
CE_NC_GET_SCP_ENABLE = """
|
||||||
|
<filter type="subtree">
|
||||||
|
<sshs xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0">
|
||||||
|
<sshServer>
|
||||||
|
<scpEnable></scpEnable>
|
||||||
|
</sshServer>
|
||||||
|
</sshs>
|
||||||
|
</filter>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_cli_exception(exc=None):
|
||||||
|
"""Get cli exception message"""
|
||||||
|
|
||||||
|
msg = list()
|
||||||
|
if not exc:
|
||||||
|
exc = get_exception()
|
||||||
|
if exc:
|
||||||
|
errs = str(exc).split("\r\n")
|
||||||
|
for err in errs:
|
||||||
|
if not err:
|
||||||
|
continue
|
||||||
|
if "matched error in response:" in err:
|
||||||
|
continue
|
||||||
|
if " at '^' position" in err:
|
||||||
|
err = err.replace(" at '^' position", "")
|
||||||
|
if err.replace(" ", "") == "^":
|
||||||
|
continue
|
||||||
|
if len(err) > 2 and err[0] in ["<", "["] and err[-1] in [">", "]"]:
|
||||||
|
continue
|
||||||
|
if err[-1] == ".":
|
||||||
|
err = err[:-1]
|
||||||
|
if err.replace(" ", "") == "":
|
||||||
|
continue
|
||||||
|
msg.append(err)
|
||||||
|
else:
|
||||||
|
msg = ["Error: Fail to get cli exception message."]
|
||||||
|
|
||||||
|
while msg[-1][-1] == ' ':
|
||||||
|
msg[-1] = msg[-1][:-1]
|
||||||
|
|
||||||
|
if msg[-1][-1] != ".":
|
||||||
|
msg[-1] += "."
|
||||||
|
|
||||||
|
return ", ".join(msg).capitalize()
|
||||||
|
|
||||||
|
|
||||||
|
class FileCopy(object):
|
||||||
|
"""File copy function class"""
|
||||||
|
|
||||||
|
def __init__(self, argument_spec):
|
||||||
|
self.spec = argument_spec
|
||||||
|
self.module = None
|
||||||
|
self.init_module()
|
||||||
|
|
||||||
|
# file copy parameters
|
||||||
|
self.local_file = self.module.params['local_file']
|
||||||
|
self.remote_file = self.module.params['remote_file']
|
||||||
|
self.file_system = self.module.params['file_system']
|
||||||
|
|
||||||
|
# state
|
||||||
|
self.transfer_result = None
|
||||||
|
self.changed = False
|
||||||
|
|
||||||
|
def init_module(self):
|
||||||
|
"""Init module"""
|
||||||
|
|
||||||
|
self.module = AnsibleModule(
|
||||||
|
argument_spec=self.spec, supports_check_mode=True)
|
||||||
|
|
||||||
|
def remote_file_exists(self, dst, file_system='flash:'):
|
||||||
|
"""Remote file whether exists"""
|
||||||
|
|
||||||
|
full_path = file_system + dst
|
||||||
|
file_name = os.path.basename(full_path)
|
||||||
|
file_path = os.path.dirname(full_path)
|
||||||
|
file_path = file_path + '/'
|
||||||
|
xml_str = CE_NC_GET_FILE_INFO % (file_name, file_path)
|
||||||
|
ret_xml = get_nc_config(self.module, xml_str)
|
||||||
|
if "<data/>" in ret_xml:
|
||||||
|
return False, 0
|
||||||
|
|
||||||
|
xml_str = ret_xml.replace('\r', '').replace('\n', '').\
|
||||||
|
replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\
|
||||||
|
replace('xmlns="http://www.huawei.com/netconf/vrp"', "")
|
||||||
|
|
||||||
|
# get file info
|
||||||
|
root = ElementTree.fromstring(xml_str)
|
||||||
|
topo = root.find("data/vfm/dirs/dir")
|
||||||
|
if topo is None:
|
||||||
|
return False, 0
|
||||||
|
|
||||||
|
for eles in topo:
|
||||||
|
if eles.tag in ["DirSize"]:
|
||||||
|
return True, int(eles.text.replace(',', ''))
|
||||||
|
|
||||||
|
return False, 0
|
||||||
|
|
||||||
|
def local_file_exists(self):
|
||||||
|
"""Local file whether exists"""
|
||||||
|
|
||||||
|
return os.path.isfile(self.local_file)
|
||||||
|
|
||||||
|
def enough_space(self):
|
||||||
|
"""Whether device has enough space"""
|
||||||
|
|
||||||
|
commands = list()
|
||||||
|
cmd = 'dir %s' % self.file_system
|
||||||
|
commands.append(cmd)
|
||||||
|
output = run_commands(self.module, commands)
|
||||||
|
if not output:
|
||||||
|
return True
|
||||||
|
|
||||||
|
match = re.search(r'\((.*) KB free\)', output[0])
|
||||||
|
kbytes_free = match.group(1)
|
||||||
|
kbytes_free = kbytes_free.replace(',', '')
|
||||||
|
file_size = os.path.getsize(self.local_file)
|
||||||
|
if int(kbytes_free) * 1024 > file_size:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def transfer_file(self, dest):
|
||||||
|
"""Begin to transfer file by scp"""
|
||||||
|
|
||||||
|
if not self.local_file_exists():
|
||||||
|
self.module.fail_json(
|
||||||
|
msg='Could not transfer file. Local file doesn\'t exist.')
|
||||||
|
|
||||||
|
if not self.enough_space():
|
||||||
|
self.module.fail_json(
|
||||||
|
msg='Could not transfer file. Not enough space on device.')
|
||||||
|
|
||||||
|
hostname = self.module.params['provider']['host']
|
||||||
|
username = self.module.params['provider']['username']
|
||||||
|
password = self.module.params['provider']['password']
|
||||||
|
port = self.module.params['provider']['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 = '{}{}'.format(self.file_system, dest)
|
||||||
|
scp = SCPClient(ssh.get_transport())
|
||||||
|
try:
|
||||||
|
scp.put(self.local_file, full_remote_path)
|
||||||
|
except:
|
||||||
|
time.sleep(10)
|
||||||
|
file_exists, temp_size = self.remote_file_exists(
|
||||||
|
dest, self.file_system)
|
||||||
|
file_size = os.path.getsize(self.local_file)
|
||||||
|
if file_exists and int(temp_size) == int(file_size):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
scp.close()
|
||||||
|
self.module.fail_json(msg='Could not transfer file. There was an error '
|
||||||
|
'during transfer. Please make sure the format of '
|
||||||
|
'input parameters is right.')
|
||||||
|
|
||||||
|
scp.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_scp_enable(self):
|
||||||
|
"""Get scp enable state"""
|
||||||
|
|
||||||
|
xml_str = CE_NC_GET_SCP_ENABLE
|
||||||
|
ret_xml = get_nc_config(self.module, xml_str)
|
||||||
|
if "<data/>" in ret_xml:
|
||||||
|
return False
|
||||||
|
|
||||||
|
xml_str = ret_xml.replace('\r', '').replace('\n', '').\
|
||||||
|
replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\
|
||||||
|
replace('xmlns="http://www.huawei.com/netconf/vrp"', "")
|
||||||
|
|
||||||
|
# get file info
|
||||||
|
root = ElementTree.fromstring(xml_str)
|
||||||
|
topo = root.find("data/sshs/sshServer")
|
||||||
|
if topo is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for eles in topo:
|
||||||
|
if eles.tag in ["scpEnable"]:
|
||||||
|
return True, eles.text
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def work(self):
|
||||||
|
"""Excute task """
|
||||||
|
|
||||||
|
if not HAS_SCP:
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="'Error: No scp package, please install it.'")
|
||||||
|
|
||||||
|
if self.local_file and len(self.local_file) > 4096:
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="'Error: The maximum length of local_file is 4096.'")
|
||||||
|
|
||||||
|
if self.remote_file and len(self.remote_file) > 4096:
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="'Error: The maximum length of remote_file is 4096.'")
|
||||||
|
|
||||||
|
retcode, cur_state = self.get_scp_enable()
|
||||||
|
if retcode and cur_state == 'Disable':
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="'Error: Please ensure SCP server is enabled.'")
|
||||||
|
|
||||||
|
if not os.path.isfile(self.local_file):
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="Local file {} not found".format(self.local_file))
|
||||||
|
|
||||||
|
dest = self.remote_file or ('/' + os.path.basename(self.local_file))
|
||||||
|
remote_exists, file_size = self.remote_file_exists(
|
||||||
|
dest, file_system=self.file_system)
|
||||||
|
if remote_exists and (os.path.getsize(self.local_file) != file_size):
|
||||||
|
remote_exists = False
|
||||||
|
|
||||||
|
if not remote_exists:
|
||||||
|
self.changed = True
|
||||||
|
file_exists = False
|
||||||
|
else:
|
||||||
|
file_exists = True
|
||||||
|
self.transfer_result = 'The local file already exists on the device.'
|
||||||
|
|
||||||
|
if not file_exists:
|
||||||
|
try:
|
||||||
|
self.transfer_file(dest)
|
||||||
|
self.transfer_result = 'The local file has been successfully ' \
|
||||||
|
'transferred to the device.'
|
||||||
|
except ShellError:
|
||||||
|
clie = get_exception()
|
||||||
|
self.module.fail_json(msg=get_cli_exception(clie))
|
||||||
|
|
||||||
|
if self.remote_file is None:
|
||||||
|
self.remote_file = '/' + os.path.basename(self.local_file)
|
||||||
|
|
||||||
|
self.module.exit_json(
|
||||||
|
changed=self.changed,
|
||||||
|
transfer_result=self.transfer_result,
|
||||||
|
local_file=self.local_file,
|
||||||
|
remote_file=self.remote_file,
|
||||||
|
file_system=self.file_system)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function entry"""
|
||||||
|
|
||||||
|
argument_spec = dict(
|
||||||
|
local_file=dict(required=True),
|
||||||
|
remote_file=dict(required=False),
|
||||||
|
file_system=dict(required=False, default='flash:')
|
||||||
|
)
|
||||||
|
argument_spec.update(ce_argument_spec)
|
||||||
|
filecopy_obj = FileCopy(argument_spec)
|
||||||
|
filecopy_obj.work()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Loading…
Reference in New Issue