mirror of https://github.com/ansible/ansible.git
VMware: vmware_tools connection plugin (#55059)
* variable standardisation and cleanup - connection_address -> vmware_host - connection_username -> vmware_user - connection_password -> vmware_password - connection_verify_ssl -> validate_certs - connection_ignore_ssl_warnings -> silence_tls_warnings - ansible_vmware_tools_vm_path -> ansible_vmware_guest_path - standardize user/pass vars - fallback to default ansible conneciton vars - accept VMware standard env vars: - note lack of "become" support - add example usage - more reasonable default sleep interval - auto-silence tls warnings if validate_certs=false - get_option for executable - remove unsafe 'makedirs_safe' * executable: support env vars and inipull/55164/head
parent
7a1e2ef746
commit
2b8cef340e
@ -0,0 +1,493 @@
|
||||
# Copyright: (c) 2018, Deric Crago <deric.crago@gmail.com>
|
||||
# Copyright: (c) 2018, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
from os.path import exists, getsize
|
||||
from socket import gaierror
|
||||
from ssl import SSLEOFError, SSLError
|
||||
from time import sleep
|
||||
import urllib3
|
||||
import traceback
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
import requests
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
REQUESTS_IMP_ERR = traceback.format_exc()
|
||||
HAS_REQUESTS = False
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleConnectionFailure
|
||||
from ansible.module_utils._text import to_bytes, to_native
|
||||
from ansible.plugins.connection import ConnectionBase
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
try:
|
||||
from pyVim.connect import Disconnect, SmartConnect, SmartConnectNoSSL
|
||||
from pyVmomi import vim
|
||||
|
||||
HAS_PYVMOMI = True
|
||||
except ImportError:
|
||||
HAS_PYVMOMI = False
|
||||
PYVMOMI_IMP_ERR = traceback.format_exc()
|
||||
|
||||
|
||||
DOCUMENTATION = """
|
||||
author: Deric Crago <deric.crago@gmail.com>
|
||||
connection: vmware_tools
|
||||
short_description: Execute tasks inside a VM via VMware Tools
|
||||
description:
|
||||
- Use VMware tools to run tasks in, or put/fetch files to guest operating systems running in VMware infrastructure.
|
||||
- In case of Windows VMs, set C(ansible_shell_type) to C(powershell).
|
||||
- Does not work with 'become'.
|
||||
version_added: "2.8"
|
||||
requirements:
|
||||
- pyvmomi (Python library)
|
||||
- requests (Python library)
|
||||
options:
|
||||
vmware_host:
|
||||
description:
|
||||
- FQDN or IP Address for the connection (vCenter or ESXi Host).
|
||||
env:
|
||||
- name: VI_SERVER
|
||||
- name: VMWARE_HOST
|
||||
vars:
|
||||
- name: ansible_host
|
||||
- name: ansible_vmware_host
|
||||
required: True
|
||||
vmware_user:
|
||||
description:
|
||||
- Username for the connection.
|
||||
- "Requires the following permissions on the VM:
|
||||
- VirtualMachine.GuestOperations.Execute
|
||||
- VirtualMachine.GuestOperations.Modify
|
||||
- VirtualMachine.GuestOperations.Query"
|
||||
env:
|
||||
- name: VI_USERNAME
|
||||
- name: VMWARE_USER
|
||||
vars:
|
||||
- name: ansible_vmware_user
|
||||
required: True
|
||||
vmware_password:
|
||||
description:
|
||||
- Password for the connection.
|
||||
env:
|
||||
- name: VI_PASSWORD
|
||||
- name: VMWARE_PASSWORD
|
||||
vars:
|
||||
- name: ansible_vmware_password
|
||||
required: True
|
||||
vmware_port:
|
||||
description:
|
||||
- Port for the connection.
|
||||
env:
|
||||
- name: VI_PORTNUMBER
|
||||
- name: VMWARE_PORT
|
||||
vars:
|
||||
- name: ansible_port
|
||||
- name: ansible_vmware_port
|
||||
required: False
|
||||
default: 443
|
||||
validate_certs:
|
||||
description:
|
||||
- Verify SSL for the connection.
|
||||
- "Note: This will validate certs for both C(vmware_host) and the ESXi host running the VM."
|
||||
env:
|
||||
- name: VMWARE_VALIDATE_CERTS
|
||||
vars:
|
||||
- name: ansible_vmware_validate_certs
|
||||
default: True
|
||||
type: bool
|
||||
silence_tls_warnings:
|
||||
description:
|
||||
- Don't output warnings about insecure connections.
|
||||
vars:
|
||||
- name: ansible_vmware_silence_tls_warnings
|
||||
default: True
|
||||
type: bool
|
||||
vm_path:
|
||||
description:
|
||||
- VM path absolute to the connection.
|
||||
- "vCenter Example: C(Datacenter/vm/Discovered virtual machine/testVM)."
|
||||
- "ESXi Host Example: C(ha-datacenter/vm/testVM)."
|
||||
- Must include VM name, appended to 'folder' as would be passed to M(vmware_guest).
|
||||
- Needs to include I(vm) between the Datacenter and the rest of the VM path.
|
||||
- Datacenter default value for ESXi server is C(ha-datacenter).
|
||||
- Folder I(vm) is not visible in the vSphere Web Client but necessary for VMware API to work.
|
||||
vars:
|
||||
- name: ansible_vmware_guest_path
|
||||
required: True
|
||||
vm_user:
|
||||
description:
|
||||
- VM username.
|
||||
vars:
|
||||
- name: ansible_user
|
||||
- name: ansible_vmware_tools_user
|
||||
required: True
|
||||
vm_password:
|
||||
description:
|
||||
- Password for the user in guest operating system.
|
||||
vars:
|
||||
- name: ansible_password
|
||||
- name: ansible_vmware_tools_password
|
||||
required: True
|
||||
exec_command_sleep_interval:
|
||||
description:
|
||||
- Time in seconds to sleep between execution of command.
|
||||
vars:
|
||||
- name: ansible_vmware_tools_exec_command_sleep_interval
|
||||
default: 0.5
|
||||
type: float
|
||||
file_chunk_size:
|
||||
description:
|
||||
- File chunk size.
|
||||
- "(Applicable when writing a file to disk, example: using the C(fetch) module.)"
|
||||
vars:
|
||||
- name: ansible_vmware_tools_file_chunk_size
|
||||
default: 128
|
||||
type: integer
|
||||
executable:
|
||||
description:
|
||||
- shell to use for execution inside container
|
||||
default: /bin/sh
|
||||
ini:
|
||||
- section: defaults
|
||||
key: executable
|
||||
env:
|
||||
- name: ANSIBLE_EXECUTABLE
|
||||
vars:
|
||||
- name: ansible_executable
|
||||
- name: ansible_vmware_tools_executable
|
||||
"""
|
||||
|
||||
EXAMPLES = r'''
|
||||
# example vars.yml
|
||||
---
|
||||
ansible_connection: vmware_tools
|
||||
|
||||
ansible_vmware_host: vcenter.example.com
|
||||
ansible_vmware_user: administrator@vsphere.local
|
||||
ansible_vmware_password: Secr3tP4ssw0rd!12
|
||||
ansible_vmware_validate_certs: no # default is yes
|
||||
ansible_vmware_silence_tls_warnings: yes # default is yes
|
||||
|
||||
# vCenter Connection VM Path Example
|
||||
ansible_vmware_guest_path: DATACENTER/vm/FOLDER/{{ inventory_hostname }}
|
||||
# ESXi Connection VM Path Example
|
||||
ansible_vmware_guest_path: ha-datacenter/vm/{{ inventory_hostname }}
|
||||
|
||||
ansible_vmware_tools_user: root
|
||||
ansible_vmware_tools_password: MyR00tPassw0rD
|
||||
|
||||
# if the target VM guest is Windows set the 'ansible_shell_type' to 'powershell'
|
||||
ansible_shell_type: powershell
|
||||
|
||||
|
||||
# example playbook_linux.yml
|
||||
---
|
||||
- name: Test VMware Tools Connection Plugin for Linux
|
||||
hosts: linux
|
||||
tasks:
|
||||
- command: whoami
|
||||
|
||||
- ping:
|
||||
|
||||
- copy:
|
||||
src: foo
|
||||
dest: /home/user/foo
|
||||
|
||||
- fetch:
|
||||
src: /home/user/foo
|
||||
dest: linux-foo
|
||||
flat: yes
|
||||
|
||||
- file:
|
||||
path: /home/user/foo
|
||||
state: absent
|
||||
|
||||
|
||||
# example playbook_windows.yml
|
||||
---
|
||||
- name: Test VMware Tools Connection Plugin for Windows
|
||||
hosts: windows
|
||||
tasks:
|
||||
- win_command: whoami
|
||||
|
||||
- win_ping:
|
||||
|
||||
- win_copy:
|
||||
src: foo
|
||||
dest: C:\Users\user\foo
|
||||
|
||||
- fetch:
|
||||
src: C:\Users\user\foo
|
||||
dest: windows-foo
|
||||
flat: yes
|
||||
|
||||
- win_file:
|
||||
path: C:\Users\user\foo
|
||||
state: absent
|
||||
'''
|
||||
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
"""VMware Tools Connection."""
|
||||
|
||||
transport = "vmware_tools"
|
||||
|
||||
@property
|
||||
def vmware_host(self):
|
||||
"""Read-only property holding the connection address."""
|
||||
return self.get_option("vmware_host")
|
||||
|
||||
@property
|
||||
def validate_certs(self):
|
||||
"""Read-only property holding whether the connection should validate certs."""
|
||||
return self.get_option("validate_certs")
|
||||
|
||||
@property
|
||||
def authManager(self):
|
||||
"""Guest Authentication Manager."""
|
||||
return self._si.content.guestOperationsManager.authManager
|
||||
|
||||
@property
|
||||
def fileManager(self):
|
||||
"""Guest File Manager."""
|
||||
return self._si.content.guestOperationsManager.fileManager
|
||||
|
||||
@property
|
||||
def processManager(self):
|
||||
"""Guest Process Manager."""
|
||||
return self._si.content.guestOperationsManager.processManager
|
||||
|
||||
@property
|
||||
def windowsGuest(self):
|
||||
"""Return if VM guest family is windows."""
|
||||
return self.vm.guest.guestFamily == "windowsGuest"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""init."""
|
||||
super(Connection, self).__init__(*args, **kwargs)
|
||||
if hasattr(self, "_shell") and self._shell.SHELL_FAMILY == "powershell":
|
||||
self.module_implementation_preferences = (".ps1", ".exe", "")
|
||||
self.become_methods = ["runas"]
|
||||
self.allow_executable = False
|
||||
self.has_pipelining = True
|
||||
self.allow_extras = True
|
||||
|
||||
def _establish_connection(self):
|
||||
connection_kwargs = {
|
||||
"host": self.vmware_host,
|
||||
"user": self.get_option("vmware_user"),
|
||||
"pwd": self.get_option("vmware_password"),
|
||||
"port": self.get_option("vmware_port"),
|
||||
}
|
||||
|
||||
if self.validate_certs:
|
||||
connect = SmartConnect
|
||||
else:
|
||||
if self.get_option("silence_tls_warnings"):
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
connect = SmartConnectNoSSL
|
||||
|
||||
try:
|
||||
self._si = connect(**connection_kwargs)
|
||||
except SSLError:
|
||||
raise AnsibleError("SSL Error: Certificate verification failed.")
|
||||
except (gaierror, SSLEOFError):
|
||||
raise AnsibleError("Connection Error: Unable to connect to '%s'." % to_native(connection_kwargs["host"]))
|
||||
except vim.fault.InvalidLogin as e:
|
||||
raise AnsibleError("Connection Login Error: %s" % to_native(e.msg))
|
||||
|
||||
def _establish_vm(self):
|
||||
searchIndex = self._si.content.searchIndex
|
||||
self.vm = searchIndex.FindByInventoryPath(self.get_option("vm_path"))
|
||||
|
||||
if self.vm is None:
|
||||
raise AnsibleError("Unable to find VM by path '%s'" % to_native(self.get_option("vm_path")))
|
||||
|
||||
self.vm_auth = vim.NamePasswordAuthentication(
|
||||
username=self.get_option("vm_user"), password=self.get_option("vm_password"), interactiveSession=False
|
||||
)
|
||||
|
||||
try:
|
||||
self.authManager.ValidateCredentialsInGuest(vm=self.vm, auth=self.vm_auth)
|
||||
except vim.fault.InvalidPowerState as e:
|
||||
raise AnsibleError("VM Power State Error: %s" % to_native(e.msg))
|
||||
except vim.fault.RestrictedVersion as e:
|
||||
raise AnsibleError("Restricted Version Error: %s" % to_native(e.msg))
|
||||
except vim.fault.GuestOperationsUnavailable as e:
|
||||
raise AnsibleError("VM Guest Operations (VMware Tools) Error: %s" % to_native(e.msg))
|
||||
except vim.fault.InvalidGuestLogin as e:
|
||||
raise AnsibleError("VM Login Error: %s" % to_native(e.msg))
|
||||
except vim.fault.NoPermission as e:
|
||||
raise AnsibleConnectionFailure("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId)))
|
||||
|
||||
def _connect(self):
|
||||
if not HAS_REQUESTS:
|
||||
raise AnsibleError("%s : %s" % (missing_required_lib('requests'), REQUESTS_IMP_ERR))
|
||||
|
||||
if not HAS_PYVMOMI:
|
||||
raise AnsibleError("%s : %s" % (missing_required_lib('PyVmomi'), PYVMOMI_IMP_ERR))
|
||||
|
||||
super(Connection, self)._connect()
|
||||
|
||||
if self.connected:
|
||||
pass
|
||||
|
||||
self._establish_connection()
|
||||
self._establish_vm()
|
||||
|
||||
self._connected = True
|
||||
|
||||
def close(self):
|
||||
"""Close connection."""
|
||||
super(Connection, self).close()
|
||||
|
||||
Disconnect(self._si)
|
||||
self._connected = False
|
||||
|
||||
def reset(self):
|
||||
"""Reset the connection."""
|
||||
super(Connection, self).reset()
|
||||
|
||||
self.close()
|
||||
self._connect()
|
||||
|
||||
def create_temporary_file_in_guest(self, prefix="", suffix=""):
|
||||
"""Create a temporary file in the VM."""
|
||||
try:
|
||||
return self.fileManager.CreateTemporaryFileInGuest(vm=self.vm, auth=self.vm_auth, prefix=prefix, suffix=suffix)
|
||||
except vim.fault.NoPermission as e:
|
||||
raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId)))
|
||||
|
||||
def _get_program_spec_program_path_and_arguments(self, cmd):
|
||||
if self.windowsGuest:
|
||||
cmd_parts = self._shell._encode_script(cmd, as_list=False, strict_mode=False, preserve_rc=False)
|
||||
|
||||
program_path = "cmd.exe"
|
||||
arguments = "/c %s" % cmd_parts
|
||||
else:
|
||||
program_path = self.get_option("executable")
|
||||
arguments = re.sub(r"^%s\s*" % program_path, "", cmd)
|
||||
|
||||
return program_path, arguments
|
||||
|
||||
def _get_guest_program_spec(self, cmd, stdout, stderr):
|
||||
guest_program_spec = vim.GuestProgramSpec()
|
||||
|
||||
program_path, arguments = self._get_program_spec_program_path_and_arguments(cmd)
|
||||
|
||||
arguments += " 1> %s 2> %s" % (stdout, stderr)
|
||||
|
||||
guest_program_spec.programPath = program_path
|
||||
guest_program_spec.arguments = arguments
|
||||
|
||||
return guest_program_spec
|
||||
|
||||
def _get_pid_info(self, pid):
|
||||
try:
|
||||
processes = self.processManager.ListProcessesInGuest(vm=self.vm, auth=self.vm_auth, pids=[pid])
|
||||
except vim.fault.NoPermission as e:
|
||||
raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId)))
|
||||
return processes[0]
|
||||
|
||||
def _fix_url_for_hosts(self, url):
|
||||
"""
|
||||
Fix url if connection is a host.
|
||||
|
||||
The host part of the URL is returned as '*' if the hostname to be used is the name of the server to which the call was made. For example, if the call is
|
||||
made to esx-svr-1.domain1.com, and the file is available for download from http://esx-svr-1.domain1.com/guestFile?id=1&token=1234, the URL returned may
|
||||
be http://*/guestFile?id=1&token=1234. The client replaces the asterisk with the server name on which it invoked the call.
|
||||
|
||||
https://code.vmware.com/apis/358/vsphere#/doc/vim.vm.guest.FileManager.FileTransferInformation.html
|
||||
"""
|
||||
return url.replace("*", self.vmware_host)
|
||||
|
||||
def _fetch_file_from_vm(self, guestFilePath):
|
||||
try:
|
||||
fileTransferInformation = self.fileManager.InitiateFileTransferFromGuest(vm=self.vm, auth=self.vm_auth, guestFilePath=guestFilePath)
|
||||
except vim.fault.NoPermission as e:
|
||||
raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId)))
|
||||
|
||||
url = self._fix_url_for_hosts(fileTransferInformation.url)
|
||||
response = requests.get(url, verify=self.validate_certs, stream=True)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise AnsibleError("Failed to fetch file")
|
||||
|
||||
return response
|
||||
|
||||
def delete_file_in_guest(self, filePath):
|
||||
"""Delete file from VM."""
|
||||
try:
|
||||
self.fileManager.DeleteFileInGuest(vm=self.vm, auth=self.vm_auth, filePath=filePath)
|
||||
except vim.fault.NoPermission as e:
|
||||
raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId)))
|
||||
|
||||
def exec_command(self, cmd, in_data=None, sudoable=True):
|
||||
"""Execute command."""
|
||||
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||
|
||||
stdout = self.create_temporary_file_in_guest(suffix=".stdout")
|
||||
stderr = self.create_temporary_file_in_guest(suffix=".stderr")
|
||||
|
||||
guest_program_spec = self._get_guest_program_spec(cmd, stdout, stderr)
|
||||
|
||||
try:
|
||||
pid = self.processManager.StartProgramInGuest(vm=self.vm, auth=self.vm_auth, spec=guest_program_spec)
|
||||
except vim.fault.NoPermission as e:
|
||||
raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId)))
|
||||
except vim.fault.FileNotFound as e:
|
||||
raise AnsibleError("StartProgramInGuest Error: %s" % to_native(e.msg))
|
||||
|
||||
pid_info = self._get_pid_info(pid)
|
||||
|
||||
while pid_info.endTime is None:
|
||||
sleep(self.get_option("exec_command_sleep_interval"))
|
||||
pid_info = self._get_pid_info(pid)
|
||||
|
||||
stdout_response = self._fetch_file_from_vm(stdout)
|
||||
self.delete_file_in_guest(stdout)
|
||||
|
||||
stderr_response = self._fetch_file_from_vm(stderr)
|
||||
self.delete_file_in_guest(stderr)
|
||||
|
||||
return pid_info.exitCode, stdout_response.text, stderr_response.text
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
"""Fetch file."""
|
||||
super(Connection, self).fetch_file(in_path, out_path)
|
||||
|
||||
in_path_response = self._fetch_file_from_vm(in_path)
|
||||
|
||||
with open(out_path, "wb") as fd:
|
||||
for chunk in in_path_response.iter_content(chunk_size=self.get_option("file_chunk_size")):
|
||||
fd.write(chunk)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
"""Put file."""
|
||||
super(Connection, self).put_file(in_path, out_path)
|
||||
|
||||
if not exists(to_bytes(in_path, errors="surrogate_or_strict")):
|
||||
raise AnsibleFileNotFound("file or module does not exist: '%s'" % to_native(in_path))
|
||||
|
||||
try:
|
||||
put_url = self.fileManager.InitiateFileTransferToGuest(
|
||||
vm=self.vm, auth=self.vm_auth, guestFilePath=out_path, fileAttributes=vim.GuestFileAttributes(), fileSize=getsize(in_path), overwrite=True
|
||||
)
|
||||
except vim.fault.NoPermission as e:
|
||||
raise AnsibleError("No Permission Error: %s %s" % (to_native(e.msg), to_native(e.privilegeId)))
|
||||
|
||||
url = self._fix_url_for_hosts(put_url)
|
||||
|
||||
# file size of 'in_path' must be greater than 0
|
||||
with open(in_path, "rb") as fd:
|
||||
response = requests.put(url, verify=self.validate_certs, data=fd)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise AnsibleError("File transfer failed")
|
Loading…
Reference in New Issue