From 216260cbb47f488284733c223a8a21e99f3e3acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A9ri=20Le=20Bouder?= Date: Thu, 16 May 2019 01:10:08 -0400 Subject: [PATCH] VMware: vsphere_copy: ability to target an esxi instance (#55930) *`vsphere_copy` was only able to interact with a vCenter instance. This patch change that. * In addition, it also makes use of the `vmware_argument_spec`. Co-Authored-By: Abhijeet Kasurde --- .../55930-vsphere_copy_for_esxi.yaml | 4 + .../modules/cloud/vmware/vsphere_copy.py | 152 +++++++++++------- 2 files changed, 99 insertions(+), 57 deletions(-) create mode 100644 changelogs/fragments/55930-vsphere_copy_for_esxi.yaml diff --git a/changelogs/fragments/55930-vsphere_copy_for_esxi.yaml b/changelogs/fragments/55930-vsphere_copy_for_esxi.yaml new file mode 100644 index 0000000000..1eeac99ea3 --- /dev/null +++ b/changelogs/fragments/55930-vsphere_copy_for_esxi.yaml @@ -0,0 +1,4 @@ +minor_changes: +- vsphere_copy - The module can now also be used with standalone ESXi. +- vsphere_copy - The ``host`` and ``login`` parameters are deprecated, use `hostname` and ``username`` + like for the other VMware modules. diff --git a/lib/ansible/modules/cloud/vmware/vsphere_copy.py b/lib/ansible/modules/cloud/vmware/vsphere_copy.py index 3c876b10ee..ff1c46f7ea 100644 --- a/lib/ansible/modules/cloud/vmware/vsphere_copy.py +++ b/lib/ansible/modules/cloud/vmware/vsphere_copy.py @@ -7,57 +7,61 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} DOCUMENTATION = ''' --- module: vsphere_copy -short_description: Copy a file to a vCenter datastore +short_description: Copy a file to a VMware datastore description: - - Upload files to a vCenter datastore + - Upload files to a VMware datastore through a VCenter REST API. version_added: 2.0 author: - Dag Wieers (@dagwieers) options: + hostname: + version_added: "2.9" + port: + version_added: "2.9" + username: + version_added: "2.9" host: description: - - The vCenter server on which the datastore is available. - required: true + - Use C(hostname) instead like the other VMware modules. + - The vCenter or ESXi server on which the datastore is available. + - This option is deprecated and will eventually be removed in 2.12. aliases: ['hostname'] login: description: - - The login name to authenticate on the vCenter server. - required: true + - Use C(username) instead like the other VMware modules. + - The login name to authenticate on the vCenter or ESXi server. + - This option is deprecated and will eventually be removed in 2.12. aliases: ['username'] - password: - description: - - The password to authenticate on the vCenter server. - required: true src: description: - - The file to push to vCenter + - The file to push to vCenter. required: true + type: str datacenter: description: - The datacenter on the vCenter server that holds the datastore. - required: true + required: false + type: str datastore: description: - - The datastore on the vCenter server to push files to. + - The datastore to push files to. required: true + type: str path: description: - - The file to push to the datastore on the vCenter server. + - The file to push to the datastore. required: true - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be - set to C(no) when no other option exists. - default: 'yes' - type: bool + type: str timeout: description: - The timeout in seconds for the upload to the datastore. @@ -66,26 +70,40 @@ options: version_added: "2.8" notes: - - "This module ought to be run from a system that can access vCenter directly and has the file to transfer. + - "This module ought to be run from a system that can access the vCenter or the ESXi directly and has the file to transfer. It can be the normal remote target or you can change it either by using C(transport: local) or using C(delegate_to)." - - Tested on vSphere 5.5 + - Tested on vSphere 5.5 and ESXi 6.7 +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' -- vsphere_copy: - host: '{{ vhost }}' - login: '{{ vuser }}' - password: '{{ vpass }}' +- name: Copy file to datastore using delegate_to + vsphere_copy: + hostname: '{{ vcenter_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' src: /some/local/file datacenter: DC1 Someplace datastore: datastore1 path: some/remote/file delegate_to: localhost -- vsphere_copy: - host: '{{ vhost }}' - login: '{{ vuser }}' - password: '{{ vpass }}' +- name: Copy file to datastore when datacenter is inside folder called devel + vsphere_copy: + hostname: '{{ vcenter_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' + src: /some/local/file + datacenter: devel/DC1 + datastore: datastore1 + path: some/remote/file + delegate_to: localhost + +- name: Copy file to datastore using other_system + vsphere_copy: + hostname: '{{ vcenter_hostname }}' + username: '{{ vcenter_username }}' + password: '{{ vcenter_password }}' src: /other/local/file datacenter: DC2 Someplace datastore: datastore2 @@ -96,6 +114,7 @@ EXAMPLES = ''' import atexit import errno import mmap +import os import socket import traceback @@ -103,6 +122,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six.moves.urllib.parse import urlencode, quote from ansible.module_utils._text import to_native from ansible.module_utils.urls import open_url +from ansible.module_utils.vmware import vmware_argument_spec def vmware_path(datastore, datacenter, path): @@ -110,36 +130,41 @@ def vmware_path(datastore, datacenter, path): path = "/folder/%s" % quote(path.lstrip("/")) # Due to a software bug in vSphere, it fails to handle ampersand in datacenter names # The solution is to do what vSphere does (when browsing) and double-encode ampersands, maybe others ? - datacenter = datacenter.replace('&', '%26') if not path.startswith("/"): path = "/" + path params = dict(dsName=datastore) if datacenter: + datacenter = datacenter.replace('&', '%26') params["dcPath"] = datacenter params = urlencode(params) return "%s?%s" % (path, params) def main(): + argument_spec = vmware_argument_spec() + argument_spec.update(dict( + host=dict(required=False, removedin_version='2.12'), + login=dict(required=False, removedin_version='2.12'), + src=dict(required=True, aliases=['name']), + datacenter=dict(required=False), + datastore=dict(required=True), + dest=dict(required=True, aliases=['path']), + timeout=dict(default=10, type='int')) + ) module = AnsibleModule( - argument_spec=dict( - host=dict(required=True, aliases=['hostname']), - login=dict(required=True, aliases=['username']), - password=dict(required=True, no_log=True), - src=dict(required=True, aliases=['name']), - datacenter=dict(required=True), - datastore=dict(required=True), - dest=dict(required=True, aliases=['path']), - validate_certs=dict(default=True, type='bool'), - timeout=dict(default=10, type='int') - ), + argument_spec=argument_spec, # Implementing check-mode using HEAD is impossible, since size/date is not 100% reliable supports_check_mode=False, ) - host = module.params.get('host') - login = module.params.get('login') + if module.params['host'] is not None: + module.deprecate("The 'host' option is being replaced by 'hostname'", version='2.12') + if module.params['login'] is not None: + module.deprecate("The 'login' option is being replaced by 'username'", version='2.12') + + hostname = module.params['hostname'] or module.params['host'] + username = module.params['username'] or module.params['login'] password = module.params.get('password') src = module.params.get('src') datacenter = module.params.get('datacenter') @@ -148,14 +173,23 @@ def main(): validate_certs = module.params.get('validate_certs') timeout = module.params.get('timeout') - fd = open(src, "rb") - atexit.register(fd.close) + try: + fd = open(src, "rb") + atexit.register(fd.close) + except Exception as e: + module.fail_json(msg="Failed to open src file %s" % to_native(e)) - data = mmap.mmap(fd.fileno(), 0, access=mmap.ACCESS_READ) - atexit.register(data.close) + if os.stat(src).st_size == 0: + data = '' + else: + data = mmap.mmap(fd.fileno(), 0, access=mmap.ACCESS_READ) + atexit.register(data.close) remote_path = vmware_path(datastore, datacenter, dest) - url = 'https://%s%s' % (host, remote_path) + + if not all([hostname, username, password]): + module.fail_json(msg="One of following parameter is missing - hostname, username, password") + url = 'https://%s%s' % (hostname, remote_path) headers = { "Content-Type": "application/octet-stream", @@ -164,12 +198,16 @@ def main(): try: r = open_url(url, data=data, headers=headers, method='PUT', timeout=timeout, - url_username=login, url_password=password, validate_certs=validate_certs, + url_username=username, url_password=password, validate_certs=validate_certs, force_basic_auth=True) except socket.error as e: - if isinstance(e.args, tuple) and e[0] == errno.ECONNRESET: - # VSphere resets connection if the file is in use and cannot be replaced - module.fail_json(msg='Failed to upload, image probably in use', status=None, errno=e[0], reason=to_native(e), url=url) + if isinstance(e.args, tuple): + if len(e.args) > 0: + if e[0] == errno.ECONNRESET: + # VSphere resets connection if the file is in use and cannot be replaced + module.fail_json(msg='Failed to upload, image probably in use', status=None, errno=e[0], reason=to_native(e), url=url) + else: + module.fail_json(msg=to_native(e)) else: module.fail_json(msg=str(e), status=None, errno=e[0], reason=str(e), url=url, exception=traceback.format_exc()) @@ -178,7 +216,7 @@ def main(): try: if isinstance(e[0], int): error_code = e[0] - except KeyError: + except (KeyError, TypeError): pass module.fail_json(msg=to_native(e), status=None, errno=error_code, reason=to_native(e), url=url, exception=traceback.format_exc())