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 <akasurde@redhat.com>
This commit is contained in:
Gonéri Le Bouder 2019-05-16 01:10:08 -04:00 committed by Abhijeet Kasurde
parent ca7ff2ad05
commit 216260cbb4
2 changed files with 99 additions and 57 deletions

View file

@ -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.

View file

@ -7,57 +7,61 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1', ANSIBLE_METADATA = {
'status': ['preview'], 'metadata_version': '1.1',
'supported_by': 'community'} 'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: vsphere_copy module: vsphere_copy
short_description: Copy a file to a vCenter datastore short_description: Copy a file to a VMware datastore
description: description:
- Upload files to a vCenter datastore - Upload files to a VMware datastore through a VCenter REST API.
version_added: 2.0 version_added: 2.0
author: author:
- Dag Wieers (@dagwieers) - Dag Wieers (@dagwieers)
options: options:
hostname:
version_added: "2.9"
port:
version_added: "2.9"
username:
version_added: "2.9"
host: host:
description: description:
- The vCenter server on which the datastore is available. - Use C(hostname) instead like the other VMware modules.
required: true - 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'] aliases: ['hostname']
login: login:
description: description:
- The login name to authenticate on the vCenter server. - Use C(username) instead like the other VMware modules.
required: true - 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'] aliases: ['username']
password:
description:
- The password to authenticate on the vCenter server.
required: true
src: src:
description: description:
- The file to push to vCenter - The file to push to vCenter.
required: true required: true
type: str
datacenter: datacenter:
description: description:
- The datacenter on the vCenter server that holds the datastore. - The datacenter on the vCenter server that holds the datastore.
required: true required: false
type: str
datastore: datastore:
description: description:
- The datastore on the vCenter server to push files to. - The datastore to push files to.
required: true required: true
type: str
path: path:
description: description:
- The file to push to the datastore on the vCenter server. - The file to push to the datastore.
required: true required: true
validate_certs: type: str
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
timeout: timeout:
description: description:
- The timeout in seconds for the upload to the datastore. - The timeout in seconds for the upload to the datastore.
@ -66,26 +70,40 @@ options:
version_added: "2.8" version_added: "2.8"
notes: 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)." 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 = ''' EXAMPLES = '''
- vsphere_copy: - name: Copy file to datastore using delegate_to
host: '{{ vhost }}' vsphere_copy:
login: '{{ vuser }}' hostname: '{{ vcenter_hostname }}'
password: '{{ vpass }}' username: '{{ vcenter_username }}'
password: '{{ vcenter_password }}'
src: /some/local/file src: /some/local/file
datacenter: DC1 Someplace datacenter: DC1 Someplace
datastore: datastore1 datastore: datastore1
path: some/remote/file path: some/remote/file
delegate_to: localhost delegate_to: localhost
- vsphere_copy: - name: Copy file to datastore when datacenter is inside folder called devel
host: '{{ vhost }}' vsphere_copy:
login: '{{ vuser }}' hostname: '{{ vcenter_hostname }}'
password: '{{ vpass }}' 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 src: /other/local/file
datacenter: DC2 Someplace datacenter: DC2 Someplace
datastore: datastore2 datastore: datastore2
@ -96,6 +114,7 @@ EXAMPLES = '''
import atexit import atexit
import errno import errno
import mmap import mmap
import os
import socket import socket
import traceback 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.six.moves.urllib.parse import urlencode, quote
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible.module_utils.urls import open_url from ansible.module_utils.urls import open_url
from ansible.module_utils.vmware import vmware_argument_spec
def vmware_path(datastore, datacenter, path): def vmware_path(datastore, datacenter, path):
@ -110,36 +130,41 @@ def vmware_path(datastore, datacenter, path):
path = "/folder/%s" % quote(path.lstrip("/")) path = "/folder/%s" % quote(path.lstrip("/"))
# Due to a software bug in vSphere, it fails to handle ampersand in datacenter names # 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 ? # The solution is to do what vSphere does (when browsing) and double-encode ampersands, maybe others ?
datacenter = datacenter.replace('&', '%26')
if not path.startswith("/"): if not path.startswith("/"):
path = "/" + path path = "/" + path
params = dict(dsName=datastore) params = dict(dsName=datastore)
if datacenter: if datacenter:
datacenter = datacenter.replace('&', '%26')
params["dcPath"] = datacenter params["dcPath"] = datacenter
params = urlencode(params) params = urlencode(params)
return "%s?%s" % (path, params) return "%s?%s" % (path, params)
def main(): 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( module = AnsibleModule(
argument_spec=dict( argument_spec=argument_spec,
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')
),
# Implementing check-mode using HEAD is impossible, since size/date is not 100% reliable # Implementing check-mode using HEAD is impossible, since size/date is not 100% reliable
supports_check_mode=False, supports_check_mode=False,
) )
host = module.params.get('host') if module.params['host'] is not None:
login = module.params.get('login') 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') password = module.params.get('password')
src = module.params.get('src') src = module.params.get('src')
datacenter = module.params.get('datacenter') datacenter = module.params.get('datacenter')
@ -148,14 +173,23 @@ def main():
validate_certs = module.params.get('validate_certs') validate_certs = module.params.get('validate_certs')
timeout = module.params.get('timeout') timeout = module.params.get('timeout')
fd = open(src, "rb") try:
atexit.register(fd.close) 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) if os.stat(src).st_size == 0:
atexit.register(data.close) data = ''
else:
data = mmap.mmap(fd.fileno(), 0, access=mmap.ACCESS_READ)
atexit.register(data.close)
remote_path = vmware_path(datastore, datacenter, dest) 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 = { headers = {
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
@ -164,12 +198,16 @@ def main():
try: try:
r = open_url(url, data=data, headers=headers, method='PUT', timeout=timeout, 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) force_basic_auth=True)
except socket.error as e: except socket.error as e:
if isinstance(e.args, tuple) and e[0] == errno.ECONNRESET: if isinstance(e.args, tuple):
# VSphere resets connection if the file is in use and cannot be replaced if len(e.args) > 0:
module.fail_json(msg='Failed to upload, image probably in use', status=None, errno=e[0], reason=to_native(e), url=url) 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: else:
module.fail_json(msg=str(e), status=None, errno=e[0], reason=str(e), module.fail_json(msg=str(e), status=None, errno=e[0], reason=str(e),
url=url, exception=traceback.format_exc()) url=url, exception=traceback.format_exc())
@ -178,7 +216,7 @@ def main():
try: try:
if isinstance(e[0], int): if isinstance(e[0], int):
error_code = e[0] error_code = e[0]
except KeyError: except (KeyError, TypeError):
pass pass
module.fail_json(msg=to_native(e), status=None, errno=error_code, module.fail_json(msg=to_native(e), status=None, errno=error_code,
reason=to_native(e), url=url, exception=traceback.format_exc()) reason=to_native(e), url=url, exception=traceback.format_exc())