From a137349522c0040a02bd1bf2d98c2f12a85d2394 Mon Sep 17 00:00:00 2001 From: QijunPan Date: Fri, 26 May 2017 00:26:11 +0800 Subject: [PATCH] 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 issue --- .../network/cloudengine/ce_file_copy.py | 395 ++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 lib/ansible/modules/network/cloudengine/ce_file_copy.py diff --git a/lib/ansible/modules/network/cloudengine/ce_file_copy.py b/lib/ansible/modules/network/cloudengine/ce_file_copy.py new file mode 100644 index 00000000000..17e3630ad12 --- /dev/null +++ b/lib/ansible/modules/network/cloudengine/ce_file_copy.py @@ -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 . +# + +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 = """ + + + + + %s + %s + + + + + +""" + +CE_NC_GET_SCP_ENABLE = """ + + + + + + + +""" + + +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 "" 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 "" 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()